Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ jobs:
id: build
run: |
go build -ldflags "-X github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/cli.Version=$(git rev-parse HEAD)" -o "$RUNNER_TEMP/oac-daemon${{ runner.os == 'Windows' && '.exe' || '' }}" ./apps/daemon/cmd/oac-daemon
if [[ "$RUNNER_OS" == Linux ]]; then
CGO_ENABLED=0 go build -mod=readonly -trimpath -o "$RUNNER_TEMP/oac-sandbox-io" ./apps/sandboxio/cmd/oac-sandbox-io
fi
node --test scripts/build-native-installer.test.mjs
- name: Build and test the shared Core installer
run: |
Expand Down Expand Up @@ -128,6 +131,7 @@ jobs:
node scripts/build-native-installer-ci.mjs
- name: Bootstrap, authenticate, install and connect natively
id: onboarding
if: runner.os == 'Linux'
run: node scripts/native-onboarding-smoke.mjs
- name: Verify native Harness protocols without model requests
id: protocol
Expand Down
29 changes: 21 additions & 8 deletions apps/daemon/internal/cli/connect_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,45 @@ func decodeEnvironmentJSON(raw []byte, value any) error {

func enrollEnvironment(ctx context.Context, client *http.Client, base, environment, credential string) (environmentEnrollment, error) {
var out environmentEnrollment
raw, err := requestEnrollment(ctx, client, base, environment, credential)
if err != nil {
return out, err
}
if decodeEnvironmentJSON(raw, &out) != nil || !environmentUUID(out.DeviceID) || !environmentUUID(out.SessionID) || out.EnvironmentID != environment || out.WorkspaceDirectory == "/" || agentcapabilities.ValidateLocalDirectories([]string{out.WorkspaceDirectory}) != nil {
return environmentEnrollment{}, errors.New("connect: invalid Environment enrollment response")
}
return out, nil
}

// requestEnrollment returns the body of a successful enrollment; each caller
// decodes the response it expects.
func requestEnrollment(ctx context.Context, client *http.Client, base, environment, credential string) ([]byte, error) {
body, _ := json.Marshal(map[string]string{"environment_id": environment})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/agent-daemon/enroll", bytes.NewReader(body))
if err != nil {
return out, errors.New("connect: invalid enrollment request")
return nil, errors.New("connect: invalid enrollment request")
}
req.Header.Set("Authorization", "Bearer "+credential)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return out, errors.New("connect: Environment enrollment transport failed")
return nil, errors.New("connect: Environment enrollment transport failed")
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
case http.StatusUnauthorized:
return out, errEnvironmentCredentialRejected
return nil, errEnvironmentCredentialRejected
case http.StatusConflict:
return out, errEnvironmentBindingConflict
return nil, errEnvironmentBindingConflict
default:
return out, fmt.Errorf("connect: Environment enrollment rejected (HTTP %d)", resp.StatusCode)
return nil, fmt.Errorf("connect: Environment enrollment rejected (HTTP %d)", resp.StatusCode)
}
raw, err := io.ReadAll(io.LimitReader(resp.Body, 16*1024+1))
if err != nil || len(raw) > 16*1024 || decodeEnvironmentJSON(raw, &out) != nil || !environmentUUID(out.DeviceID) || !environmentUUID(out.SessionID) || out.EnvironmentID != environment || out.WorkspaceDirectory == "/" || agentcapabilities.ValidateLocalDirectories([]string{out.WorkspaceDirectory}) != nil {
return environmentEnrollment{}, errors.New("connect: invalid Environment enrollment response")
if err != nil || len(raw) > 16*1024 {
return nil, errors.New("connect: invalid Environment enrollment response")
}
return out, nil
return raw, nil
}

func environmentBootstrap(ctx context.Context, prof auth.Profile, remote string) (*transport.BootstrapResponse, error) {
Expand Down
10 changes: 0 additions & 10 deletions apps/daemon/internal/cli/native_harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,3 @@ func checkNativeInstallation(ctx context.Context, root string, selected []string
}
return nil
}

// Installed discovery is confined to verified adapters; ordinary tool PATH
// remains available to the selected Harness and its tools.
func nativeInstallationKinds(selected []string) map[string]bool {
kinds := make(map[string]bool, len(selected))
for _, name := range selected {
kinds[nativeHarnesses[name].AgentKind] = true
}
return kinds
}
59 changes: 28 additions & 31 deletions apps/daemon/internal/cli/native_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths"
Expand Down Expand Up @@ -178,7 +179,7 @@ func installNativeOptions(ctx context.Context, rc *runContext, o *nativeInstallO
return err
}
}
if err = nativeInstallPhase(rc.stdout, "Installing Runtime", func() error { return installNativeBinary(ctx, o.Directory, len(previous.Harnesses) > 0) }); err != nil {
if err = nativeInstallPhase(rc.stdout, "Installing Runtime", func() error { return installNativeBinary(ctx, o.Bundle, o.Directory, len(previous.Harnesses) > 0) }); err != nil {
return err
}
for _, name := range append([]string{"node"}, selected...) {
Expand All @@ -199,7 +200,7 @@ func installNativeOptions(ctx context.Context, rc *runContext, o *nativeInstallO
}
fmt.Fprintln(rc.stdout, "Installation: ready; verified Harnesses:", all)
if o.OnboardURL == "" {
fmt.Fprintln(rc.stdout, "Daemon connection: not checked by install; run the installed oac-daemon start, then check Host connection in Core.")
fmt.Fprintln(rc.stdout, "Host connection: not checked by install; run the installed oac-daemon start, then check Host connection in Core.")
}
fmt.Fprintln(rc.stdout, "Model configuration: not checked; configure the Session model provider in Core and send a Turn.")
return nil
Expand All @@ -224,7 +225,9 @@ func verifyNativeComponents(ctx context.Context, root string, selected []string)
return nil
}

func installNativeBinary(ctx context.Context, root string, existing bool) error {
// installNativeBinary installs the running oac-daemon and the distribution's
// other programs (nativeBundlePrograms) into bin.
func installNativeBinary(ctx context.Context, bundle, root string, existing bool) error {
exe, err := os.Executable()
if err != nil {
return err
Expand All @@ -233,7 +236,19 @@ func installNativeBinary(ctx context.Context, root string, existing bool) error
if err = os.MkdirAll(dir, 0700); err != nil {
return err
}
dest := filepath.Join(dir, nativeExe("oac-daemon"))
if err = installNativeProgram(ctx, exe, dir, nativeExe("oac-daemon"), existing); err != nil {
return err
}
for _, name := range nativeBundlePrograms {
if err = installNativeProgram(ctx, filepath.Join(bundle, name), dir, name, existing); err != nil {
return err
}
}
return nil
}

func installNativeProgram(ctx context.Context, source, dir, name string, existing bool) error {
dest := filepath.Join(dir, name)
digest := func(name string) (string, error) {
f, e := os.Open(name)
if e != nil {
Expand All @@ -244,22 +259,25 @@ func installNativeBinary(ctx context.Context, root string, existing bool) error
_, e = nativeCopy(ctx, h, f)
return hex.EncodeToString(h.Sum(nil)), e
}
want, err := digest(exe)
want, err := digest(source)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("install: the distribution has no %s; use the matching native distribution", name)
}
if err != nil {
return err
}
if got, e := digest(dest); e == nil {
if got != want {
return errors.New("install: existing daemon binary differs; in-place upgrades are unsupported")
return fmt.Errorf("install: existing %s differs; in-place upgrades are unsupported", name)
}
return nil
} else if !errors.Is(e, os.ErrNotExist) {
return e
}
if existing {
return errors.New("install: existing daemon binary is missing; preserve the installation and reinstall separately")
return fmt.Errorf("install: existing %s is missing; preserve the installation and reinstall separately", name)
}
in, err := os.Open(exe)
in, err := os.Open(source)
if err != nil {
return err
}
Expand All @@ -271,7 +289,7 @@ func installNativeBinary(ctx context.Context, root string, existing bool) error
if err = requireNativeSpace(dir, uint64(info.Size())); err != nil {
return err
}
out, err := os.CreateTemp(dir, ".oac-daemon-")
out, err := os.CreateTemp(dir, "."+strings.TrimSuffix(name, ".exe")+"-")
if err != nil {
return err
}
Expand Down Expand Up @@ -320,32 +338,11 @@ func runStart(rc *runContext, args []string) error {
if err == nil {
err = validateNativeInstallation(config)
}
if err == nil && len(config.Harnesses) == 0 {
err = errors.New("start: no installed Harnesses; rerun install with --harness")
}
if err == nil {
err = verifyNativeComponents(ctx, root, config.Harnesses)
}
if err == nil {
err = probeNativeInstallation(ctx, root, config.Harnesses)
}
unlock()
if err != nil {
return fmt.Errorf("start: installation unavailable or incompatible: %w", err)
}
previousKinds := rc.installedKinds
rc.installedKinds = nativeInstallationKinds(config.Harnesses)
defer func() { rc.installedKinds = previousKinds }()
values := nativeHarnessEnvironment(root, config.Harnesses)
values["OAC_RUNTIME_WORKSPACE"] = config.Workspace
values["OAC_RUNTIME_CAPABILITY_DIRECTORY"] = config.CapabilityDirectory
values["OAC_RUNTIME_TOOL_ENV_FILE"] = config.ToolEnvironmentFile
for key, value := range values {
if err = os.Setenv(key, value); err != nil {
return err
}
}
return runEnvironmentConnect(ctx, rc, paths.DefaultProfile, !*foreground, config.Remote, config.Environment, config.Credential)
return runSandboxLauncher(ctx, rc, !*foreground, root, config)
}

func useInstalledNativeHome() {
Expand Down
4 changes: 2 additions & 2 deletions apps/daemon/internal/cli/native_install_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
// These exact temporary names are reserved by the installer, never workspace data.
var nativeTemporaryNames = map[string]*regexp.Regexp{
"components": regexp.MustCompile(`^\.install-(node|codex|claude|minimax)-[0-9]+$`),
"bin": regexp.MustCompile(`^\.oac-daemon-[0-9]+$`),
"daemon": regexp.MustCompile(`^\.(installation\.json|executor-credential\.json)-[0-9a-f]{24}\.tmp$`),
"bin": regexp.MustCompile(`^\.(oac-daemon|oac-sandbox-io)-[0-9]+$`),
"daemon": regexp.MustCompile(`^\.(installation\.json|executor-credential\.json|sandbox-io-bootstrap\.json)-[0-9a-f]{24}\.tmp$`),
}

// The caller holds the installation lock, including while recovering a failed copy.
Expand Down
5 changes: 5 additions & 0 deletions apps/daemon/internal/cli/native_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ func nativeInstallFixture(t *testing.T) (*runContext, []string, string, string)
t.Fatal(err)
}
}
for _, name := range nativeBundlePrograms {
if err := os.WriteFile(filepath.Join(bundle, name), []byte("#!/bin/sh\nexit 0\n"), 0700); err != nil {
t.Fatal(err)
}
}
raw, _ := json.Marshal(b)
if err := os.WriteFile(filepath.Join(bundle, "bundle.json"), raw, 0600); err != nil {
t.Fatal(err)
Expand Down
10 changes: 5 additions & 5 deletions apps/daemon/internal/cli/native_onboarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func finishOnboarding(ctx context.Context, rc *runContext, o nativeInstallOption
command.Env = withNativeEnv(map[string]string{"OAC_RUNTIME_HOME": o.Directory, daemonize.BackgroundSentinelEnv: ""})
command.Stdout, command.Stderr = rc.stdout, rc.stderr
if err := command.Run(); err != nil {
fmt.Fprintln(rc.stderr, "Daemon connection: start failed. Rerun this command to resume, or run the installed oac-daemon start.")
fmt.Fprintln(rc.stderr, "Host connection: start failed. Rerun this command to resume, or run the installed oac-daemon start.")
return errors.New("install: daemon startup failed")
}
}
Expand All @@ -182,12 +182,12 @@ func finishOnboarding(ctx context.Context, rc *runContext, o nativeInstallOption
return err
}
if connected {
fmt.Fprintln(rc.stdout, "Daemon connection: connected to Core.")
fmt.Fprintln(rc.stdout, "Host connection: connected to Core.")
return nil
}
select {
case <-deadline.Done():
fmt.Fprintf(rc.stderr, "Daemon connection: not confirmed. The installed daemon will keep reconnecting. Check %s and Core connectivity, then rerun this command.\n", filepath.Join(o.Directory, "daemon", paths.DefaultProfile, "connect.log"))
fmt.Fprintf(rc.stderr, "Host connection: not confirmed. The installed daemon will keep reconnecting. Check %s and Core connectivity, then rerun this command.\n", filepath.Join(o.Directory, "daemon", paths.DefaultProfile, "connect.log"))
return errors.New("install: connection verification timed out")
case <-ticker.C:
}
Expand All @@ -206,7 +206,7 @@ func installedEnvironmentConnected(ctx context.Context, base, environment, secre
}
defer response.Body.Close()
if response.StatusCode == 401 || response.StatusCode == 409 {
return false, errors.New("Daemon connection: credential rejected; check the Environment credential in Core")
return false, errors.New("Host connection: credential rejected; check the Environment credential in Core")
}
if response.StatusCode != http.StatusOK {
return false, nil
Expand All @@ -216,7 +216,7 @@ func installedEnvironmentConnected(ctx context.Context, base, environment, secre
Status string `json:"status"`
}
if json.NewDecoder(io.LimitReader(response.Body, 4096)).Decode(&result) != nil || result.Environment != environment {
return false, errors.New("Daemon connection: invalid status returned by Core")
return false, errors.New("Host connection: invalid status returned by Core")
}
return result.Status == "connected", nil
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
//go:build unix
//go:build linux

package cli

import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
Expand All @@ -21,7 +19,6 @@ func TestNativeStartInterruptHelper(t *testing.T) {
if os.Getenv("OAC_TEST_START_INTERRUPT") != "1" {
t.Skip("subprocess helper")
}
probeNativeInstallation = func(context.Context, string, []string) error { return nil }
if err := runStart(&runContext{stdout: os.Stdout, stderr: os.Stderr}, nil); err != nil {
os.Exit(2)
}
Expand All @@ -32,19 +29,10 @@ func TestNativeStartInterruptCancelsEnrollment(t *testing.T) {
rc, args, root, _ := nativeInstallFixture(t)
requested := make(chan struct{})
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The enrollment never answers before the interrupt.
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
close(requested)
<-release
var config nativeInstallation
if err := readNativeJSON(filepath.Join(root, "daemon", "installation.json"), &config); err != nil {
return
}
_ = json.NewEncoder(w).Encode(environmentEnrollment{
DeviceID: "aaaaaaaa-1111-4111-8111-aaaaaaaaaaaa",
SessionID: "bbbbbbbb-1111-4111-8111-bbbbbbbbbbbb",
EnvironmentID: config.Environment,
WorkspaceDirectory: config.Workspace,
})
}))
defer server.Close()
defer close(release)
Expand Down
Loading
Loading