From 04766126f588770eb2eb17ab19b7417979f04e22 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 19:19:15 +0000 Subject: [PATCH 1/2] Define the machine connection API in one wire file --- AGENTS.md | 2 +- Makefile | 2 +- apps/daemon/internal/cli/connect.go | 6 +- .../internal/cli/connect_cleanup_test.go | 12 +- .../internal/cli/connect_environment.go | 31 ++--- .../cli/connect_environment_binding.go | 6 +- .../cli/connect_environment_binding_test.go | 6 +- .../internal/cli/connect_environment_test.go | 16 ++- apps/daemon/internal/cli/connect_suspend.go | 10 +- .../internal/cli/connect_suspend_test.go | 8 +- .../internal/cli/native_download_test.go | 7 +- apps/daemon/internal/cli/native_install.go | 4 +- .../internal/cli/native_install_input.go | 7 +- .../internal/cli/native_install_test.go | 7 +- apps/daemon/internal/cli/native_onboarding.go | 20 ++- .../cli/native_start_interrupt_unix_test.go | 4 +- apps/daemon/internal/transport/bootstrap.go | 30 +--- .../internal/transport/bootstrap_test.go | 10 +- contracts/agents-api/machine-api.md | 2 + contracts/agents-api/runtime.openapi.yaml | 78 +++++------ contracts/agents-api/v1/installation.go | 13 -- contracts/agents-api/zh/machine-api.md | 4 +- docs/development.md | 1 + docs/zh/development.md | 3 +- internal/machineapi/machineapi.go | 129 ++++++++++++++++++ scripts/native-onboarding-smoke.mjs | 2 +- services/core/cmd/sandbox-node/main.go | 6 +- .../internal/api/environment_installation.go | 14 +- services/core/internal/api/fakes_test.go | 10 +- services/core/internal/api/sandbox_manager.go | 16 ++- .../internal/deployment/allocations_test.go | 3 +- services/core/internal/deployment/capacity.go | 4 +- services/core/internal/deployment/node.go | 37 +---- services/core/internal/deployment/nodes.go | 16 ++- .../core/internal/deployment/service_test.go | 21 +-- .../deployment/session_archive_test.go | 3 +- .../execution/runtime_replacement_test.go | 4 +- .../execution/runtime_restore_attempt_test.go | 4 +- .../bootstrap_recovery_test.go | 4 +- .../nativeinstaller/bootstrap_test.go | 4 +- .../core/internal/nativeinstaller/catalog.go | 9 +- .../deploymentpg/checkpoint_transfer_test.go | 8 +- .../postgres/deploymentpg/fixture_test.go | 6 +- .../postgres/deploymentpg/nodes_test.go | 24 ++-- .../deploymentpg/specification_test.go | 4 +- .../internal/runtimeenrollment/connection.go | 11 +- .../internal/runtimeenrollment/enrollment.go | 13 +- .../core/internal/runtimegateway/handler.go | 17 +-- .../internal/sandbox/node/capacity_test.go | 4 +- .../core/internal/sandbox/node/enrollment.go | 12 +- .../core/internal/sandbox/node/node_test.go | 12 +- services/core/internal/sandbox/node/wire.go | 32 +---- .../admin_session_archive_race_test.go | 4 +- .../credential_matrix_http_test.go | 4 +- .../device_bootstrap_binding_test.go | 4 +- .../runtime_lifecycle_nodes_test.go | 4 +- .../runtime_node_generations_test.go | 4 +- .../runtime_node_lifecycle_fixture_test.go | 4 +- .../tests/integration/runtime_nodes_test.go | 4 +- .../sandbox_deployment_switch_test.go | 6 +- .../sandbox_deployment_switch_worker_test.go | 4 +- .../sandbox_deployment_worker_test.go | 4 +- .../sandbox_node_auth_order_http_test.go | 5 +- .../sandbox_specification_lifecycle_test.go | 16 ++- 64 files changed, 441 insertions(+), 340 deletions(-) create mode 100644 internal/machineapi/machineapi.go diff --git a/AGENTS.md b/AGENTS.md index 052d766ca..e642a3d8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ Do not multiply entities without necessity. The long-term goal is minimal code, | --- | --- | --- | | Application–Core (`/v1`) | Official schema pinned by `contracts/agents-api/upstream.json` plus Go-owned `x_agents_core` extensions; `make openapi` generates public Go types and `contracts/agents-api/openapi.yaml` | [Agents API guide](docs/api/public-agent-api.md) | | Web and operators–Core (`/core/v1`) | Route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/core.openapi.yaml` | [Core administration API](contracts/agents-api/admin-api.md) | -| Nodes and daemons–Core (`/api/v1` HTTP routes; the node and daemon wire protocols are separate rows) | Route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/runtime.openapi.yaml` | [Machine connection API](contracts/agents-api/machine-api.md) | +| Nodes and daemons–Core (`/api/v1` HTTP routes; the node and daemon wire protocols are separate rows) | `internal/machineapi/machineapi.go` (wire types) plus route annotations in `services/core/internal/api/`; `make openapi` generates `contracts/agents-api/runtime.openapi.yaml` | [Machine connection API](contracts/agents-api/machine-api.md) | | Core and sandbox nodes–Workspace filesystem | `services/core/internal/workspacefs/workspacefs.go` | [Workspace filesystem protocol](docs/workspace-provider.md) | | Core–Sandbox Provider | `services/core/internal/sandbox/sandbox_provider.go` | [Sandbox Provider protocol](docs/sandbox-provider.md) | | Core–sandbox node | `services/core/internal/sandbox/node/wire.go` | [Sandbox node protocol](contracts/agents-api/node-generation-protocol.md) | diff --git a/Makefile b/Makefile index 4294afe9c..3efed26be 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ openapi: output=$$(mktemp -d "$$root/core-openapi.XXXXXX"); trap 'rm -rf "$$output"' EXIT; \ python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --swag-roots "$$output/roots.go"; \ $(SWAG) init \ - -g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,./internal/modelprovider,$$output" \ + -g cmd/server/main.go --dir "./services/core,./contracts/agents-api/v1,./internal/modelprovider,./internal/machineapi,$$output" \ --output "$$output" \ --outputTypes yaml --parseInternal; \ python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \ diff --git a/apps/daemon/internal/cli/connect.go b/apps/daemon/internal/cli/connect.go index 6185d546e..c3a74f455 100644 --- a/apps/daemon/internal/cli/connect.go +++ b/apps/daemon/internal/cli/connect.go @@ -9,6 +9,8 @@ import ( "path/filepath" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/auth" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize" @@ -206,7 +208,7 @@ func mainLoopRemote(parent context.Context, rc *runContext, profile string, prof defer cancel() bootCtx, bootCancel := context.WithTimeout(rootCtx, bootstrapTimeout) - var boot *transport.BootstrapResponse + var boot *machineapi.BootstrapResponse var err error if remote == "" { boot, err = transport.Bootstrap(bootCtx, prof.ServerURL, prof.RuntimeID, prof.RunnerCredential, Version) @@ -323,7 +325,7 @@ func advertisedKinds(registry *agent.Registry, canExport bool) []proto.Supported return kinds } -func pumpConn(parentCtx context.Context, conn *transport.Conn, registry *agent.Registry, boot *transport.BootstrapResponse, agentCLIs agentCLIDiscovery) error { +func pumpConn(parentCtx context.Context, conn *transport.Conn, registry *agent.Registry, boot *machineapi.BootstrapResponse, agentCLIs agentCLIDiscovery) error { local, err := localworkspace.Load() if err != nil { return err diff --git a/apps/daemon/internal/cli/connect_cleanup_test.go b/apps/daemon/internal/cli/connect_cleanup_test.go index e84fd7dd2..202713bd1 100644 --- a/apps/daemon/internal/cli/connect_cleanup_test.go +++ b/apps/daemon/internal/cli/connect_cleanup_test.go @@ -1,7 +1,5 @@ package cli -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" "errors" @@ -9,15 +7,17 @@ import ( "net/http/httptest" "os" "strings" + "sync/atomic" + "testing" + "time" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" "github.com/gorilla/websocket" - "sync/atomic" - "testing" - "time" ) func TestReconnectWaitsForConfirmedRouterCleanup(t *testing.T) { @@ -123,7 +123,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { }) finished := make(chan error, 1) go func() { - boot := &transport.BootstrapResponse{HeartbeatSeconds: 60} + boot := &machineapi.BootstrapResponse{HeartbeatSeconds: 60} if suspend { control := &suspendControl{signal: make(chan os.Signal, 1)} finished <- runSuspendLoop(ctx, dial, registry, boot, agentCLIDiscovery{}, control) diff --git a/apps/daemon/internal/cli/connect_environment.go b/apps/daemon/internal/cli/connect_environment.go index 15e6cc554..337b16f5b 100644 --- a/apps/daemon/internal/cli/connect_environment.go +++ b/apps/daemon/internal/cli/connect_environment.go @@ -12,20 +12,14 @@ import ( "os" "strings" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/auth" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/google/uuid" ) -type environmentEnrollment struct { - DeviceID string `json:"device_id"` - SessionID string `json:"session_id"` - EnvironmentID string `json:"environment_id"` - WorkspaceDirectory string `json:"workspace_directory"` -} - func environmentClient() *http.Client { return &http.Client{Timeout: bootstrapTimeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse @@ -49,11 +43,6 @@ func environmentBase(remote string) (string, error) { return u.String(), nil } -func environmentUUID(value string) bool { - id, err := uuid.Parse(value) - return err == nil && id != uuid.Nil && id.String() == value -} - // executorCredential returns the credential's key ID and secret token. func executorCredential(path, environment string) (string, string, error) { raw, err := readEnvironmentPrivateFile(path) @@ -65,7 +54,7 @@ func executorCredential(path, environment string) (string, string, error) { Token string `json:"executor_token"` EnvironmentID string `json:"environment_id,omitempty"` } - if decodeEnvironmentJSON(raw, &key) != nil || !environmentUUID(key.KeyID) || key.Token == "" || strings.ContainsAny(key.Token, " \t\r\n\x00") || (key.EnvironmentID != "" && key.EnvironmentID != environment) { + if decodeEnvironmentJSON(raw, &key) != nil || !machineapi.CanonicalUUID(key.KeyID) || key.Token == "" || strings.ContainsAny(key.Token, " \t\r\n\x00") || (key.EnvironmentID != "" && key.EnvironmentID != environment) { return "", "", errors.New("connect: invalid executor credential or Environment restriction") } return key.KeyID, key.Token, nil @@ -83,9 +72,9 @@ func decodeEnvironmentJSON(raw []byte, value any) error { return nil } -func enrollEnvironment(ctx context.Context, client *http.Client, base, environment, credential string) (environmentEnrollment, error) { - var out environmentEnrollment - body, _ := json.Marshal(map[string]string{"environment_id": environment}) +func enrollEnvironment(ctx context.Context, client *http.Client, base, environment, credential string) (machineapi.Enrollment, error) { + var out machineapi.Enrollment + body, _ := json.Marshal(machineapi.EnrollRequest{EnvironmentID: 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") @@ -107,13 +96,13 @@ func enrollEnvironment(ctx context.Context, client *http.Client, base, environme return out, 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 || decodeEnvironmentJSON(raw, &out) != nil || out.Validate(environment) != nil || out.WorkspaceDirectory == "/" || agentcapabilities.ValidateLocalDirectories([]string{out.WorkspaceDirectory}) != nil { + return machineapi.Enrollment{}, errors.New("connect: invalid Environment enrollment response") } return out, nil } -func environmentBootstrap(ctx context.Context, prof auth.Profile, remote string) (*transport.BootstrapResponse, error) { +func environmentBootstrap(ctx context.Context, prof auth.Profile, remote string) (*machineapi.BootstrapResponse, error) { boot, err := transport.BootstrapWithClient(ctx, environmentClient(), prof.ServerURL, prof.RuntimeID, prof.RunnerCredential, Version) if err != nil { return nil, errors.New("connect: Environment bootstrap failed") @@ -132,7 +121,7 @@ func runEnvironmentConnect(parent context.Context, rc *runContext, profile strin if err != nil { return err } - if !environmentUUID(environment) { + if !machineapi.CanonicalUUID(environment) { return errors.New("connect: canonical Environment ID required") } if err = checkEnvironmentTarget(remote, environment); err != nil { diff --git a/apps/daemon/internal/cli/connect_environment_binding.go b/apps/daemon/internal/cli/connect_environment_binding.go index 9cf2f0ca0..8fdbb8223 100644 --- a/apps/daemon/internal/cli/connect_environment_binding.go +++ b/apps/daemon/internal/cli/connect_environment_binding.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" @@ -15,7 +17,7 @@ import ( // This receipt contains identity only; executor credentials remain in their file. type environmentBinding struct { RemoteURL string `json:"remote_url"` - Enrollment environmentEnrollment `json:"enrollment"` + Enrollment machineapi.Enrollment `json:"enrollment"` LocalWorkspace string `json:"local_workspace"` CapabilityDirectory string `json:"capability_directory"` } @@ -41,7 +43,7 @@ func readEnvironmentPrivateFile(path string) ([]byte, error) { return runtimefs.ReadPrivatePath(path, 16*1024) } -func bindEnvironmentRuntime(remote string, bound environmentEnrollment, credentialFile string) error { +func bindEnvironmentRuntime(remote string, bound machineapi.Enrollment, credentialFile string) error { root, err := paths.Root() if err != nil || !filepath.IsAbs(root) { return errors.New("connect: absolute Runtime state directory required") diff --git a/apps/daemon/internal/cli/connect_environment_binding_test.go b/apps/daemon/internal/cli/connect_environment_binding_test.go index bf9e91728..991be9487 100644 --- a/apps/daemon/internal/cli/connect_environment_binding_test.go +++ b/apps/daemon/internal/cli/connect_environment_binding_test.go @@ -5,10 +5,12 @@ import ( "path/filepath" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/google/uuid" ) -func environmentRuntimeFixture(t *testing.T) (root, workspace, credential string, bound environmentEnrollment) { +func environmentRuntimeFixture(t *testing.T) (root, workspace, credential string, bound machineapi.Enrollment) { t.Helper() base := t.TempDir() root, workspace = filepath.Join(base, "private"), filepath.Join(base, "workspace") @@ -26,7 +28,7 @@ func environmentRuntimeFixture(t *testing.T) (root, workspace, credential string for _, name := range []string{"OAC_RUNTIME_ENVIRONMENT_ID", "OAC_RUNTIME_SESSION_ID"} { t.Setenv(name, "") } - return root, workspace, credential, environmentEnrollment{uuid.NewString(), uuid.NewString(), uuid.NewString(), "/workspace"} + return root, workspace, credential, machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"} } func TestBindEnvironmentRuntimeUsesCanonicalOperatorWorkspace(t *testing.T) { diff --git a/apps/daemon/internal/cli/connect_environment_test.go b/apps/daemon/internal/cli/connect_environment_test.go index 8d398b59b..fe704a709 100644 --- a/apps/daemon/internal/cli/connect_environment_test.go +++ b/apps/daemon/internal/cli/connect_environment_test.go @@ -11,6 +11,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/auth" "github.com/google/uuid" ) @@ -31,7 +33,7 @@ func TestEnvironmentConnectionURL(t *testing.T) { func TestEnvironmentEnrollmentAndBootstrap(t *testing.T) { environment := uuid.NewString() - want := environmentEnrollment{uuid.NewString(), uuid.NewString(), environment, "/workspace"} + want := machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/workspace"} var remote string var enrolls, bootstraps int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -154,7 +156,7 @@ func TestEnvironmentBindingPreservesIdentityAndHistory(t *testing.T) { if err := os.Chmod(root, 0700); err != nil { t.Fatal(err) } - want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", environmentEnrollment{uuid.NewString(), uuid.NewString(), uuid.NewString(), "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"} + want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"} if err := saveEnvironmentBinding(root, want); err != nil { t.Fatal(err) } @@ -191,7 +193,7 @@ func TestEnvironmentBindingAllowsOperatorFilePermissions(t *testing.T) { if err := os.Chmod(root, 0700); err != nil { t.Fatal(err) } - want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", environmentEnrollment{uuid.NewString(), uuid.NewString(), uuid.NewString(), "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"} + want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"} if err := os.WriteFile(filepath.Join(root, "installed-bundle"), []byte("bundle"), 0600); err != nil { t.Fatal(err) } @@ -213,7 +215,7 @@ func TestEnvironmentTargetCheckedBeforeCredentialTransmission(t *testing.T) { t.Fatal(err) } t.Setenv("OAC_RUNTIME_HOME", root) - want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", environmentEnrollment{uuid.NewString(), uuid.NewString(), uuid.NewString(), "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"} + want := environmentBinding{"wss://core/api/v1/agent-daemon/ws", machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), WorkspaceDirectory: "/workspace"}, "/environment/workspace", "/environment/initialization/capabilities"} if err := saveEnvironmentBinding(root, want); err != nil { t.Fatal(err) } @@ -230,8 +232,8 @@ func TestEnvironmentTargetCheckedBeforeCredentialTransmission(t *testing.T) { func TestEnvironmentEnrollmentRejectsWrongIdentityAndWorkspace(t *testing.T) { environment := uuid.NewString() - good := environmentEnrollment{uuid.NewString(), uuid.NewString(), environment, "/workspace"} - for _, mutate := range []func(*environmentEnrollment){func(b *environmentEnrollment) { b.EnvironmentID = uuid.NewString() }, func(b *environmentEnrollment) { b.DeviceID = "" }, func(b *environmentEnrollment) { b.SessionID = "invalid" }, func(b *environmentEnrollment) { b.WorkspaceDirectory = "relative" }} { + good := machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/workspace"} + for _, mutate := range []func(*machineapi.Enrollment){func(b *machineapi.Enrollment) { b.EnvironmentID = uuid.NewString() }, func(b *machineapi.Enrollment) { b.DeviceID = "" }, func(b *machineapi.Enrollment) { b.SessionID = "invalid" }, func(b *machineapi.Enrollment) { b.WorkspaceDirectory = "relative" }} { bad := good mutate(&bad) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(bad) })) @@ -245,7 +247,7 @@ func TestEnvironmentEnrollmentRejectsWrongIdentityAndWorkspace(t *testing.T) { func TestEnvironmentEnrollmentAcceptsPhysicalWorkspaceSelection(t *testing.T) { environment := uuid.NewString() - want := environmentEnrollment{uuid.NewString(), uuid.NewString(), environment, "/srv/runtime/workspace"} + want := machineapi.Enrollment{DeviceID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: environment, WorkspaceDirectory: "/srv/runtime/workspace"} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(want) })) defer server.Close() actual, err := enrollEnvironment(t.Context(), environmentClient(), server.URL, environment, "secret") diff --git a/apps/daemon/internal/cli/connect_suspend.go b/apps/daemon/internal/cli/connect_suspend.go index 12068bb07..8ade9ae57 100644 --- a/apps/daemon/internal/cli/connect_suspend.go +++ b/apps/daemon/internal/cli/connect_suspend.go @@ -6,6 +6,8 @@ import ( "sync" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" @@ -57,7 +59,7 @@ func (s *suspendedRouter) shutdown() { // runSuspendLoop uses the ordinary connection authentication and dispatch chain. // Only an acknowledged, fully drained suspension retains a Router across sockets. -func runSuspendLoop(ctx context.Context, dial transport.DialFn, registry *agent.Registry, boot *transport.BootstrapResponse, discovery agentCLIDiscovery, control *suspendControl) error { +func runSuspendLoop(ctx context.Context, dial transport.DialFn, registry *agent.Registry, boot *machineapi.BootstrapResponse, discovery agentCLIDiscovery, control *suspendControl) error { for ctx.Err() == nil { conn, err := transport.Reconnect(ctx, dial, transport.DefaultBackoff, nil) if err != nil { @@ -89,7 +91,7 @@ func runSuspendLoop(ctx context.Context, dial transport.DialFn, registry *agent. return nil } -func serveSuspendLifecycle(ctx context.Context, conn *transport.Conn, dial transport.DialFn, state *suspendedRouter, boot *transport.BootstrapResponse, discovery agentCLIDiscovery, control *suspendControl) error { +func serveSuspendLifecycle(ctx context.Context, conn *transport.Conn, dial transport.DialFn, state *suspendedRouter, boot *machineapi.BootstrapResponse, discovery agentCLIDiscovery, control *suspendControl) error { defer func() { _ = conn.Close() }() for { request, err := state.pump(ctx, conn, boot, discovery, control) @@ -150,14 +152,14 @@ func (s *suspendedRouter) reconnectSuspension(ctx context.Context, dial transpor } } -func (s *suspendedRouter) heartbeats(ctx context.Context, conn *transport.Conn, boot *transport.BootstrapResponse, discovery agentCLIDiscovery) { +func (s *suspendedRouter) heartbeats(ctx context.Context, conn *transport.Conn, boot *machineapi.BootstrapResponse, discovery agentCLIDiscovery) { conn.StartHeartbeats(ctx, boot.HeartbeatInterval(), func() proto.HeartbeatPayload { kinds := advertisedKinds(s.registry, s.local.CanExport()) return proto.HeartbeatPayload{SupportedAgentKinds: kinds} }, obslog.Bg()) } -func (s *suspendedRouter) pump(ctx context.Context, conn *transport.Conn, boot *transport.BootstrapResponse, discovery agentCLIDiscovery, control *suspendControl) (*proto.EnvironmentSuspendPayload, error) { +func (s *suspendedRouter) pump(ctx context.Context, conn *transport.Conn, boot *machineapi.BootstrapResponse, discovery agentCLIDiscovery, control *suspendControl) (*proto.EnvironmentSuspendPayload, error) { s.heartbeats(ctx, conn, boot, discovery) for { select { diff --git a/apps/daemon/internal/cli/connect_suspend_test.go b/apps/daemon/internal/cli/connect_suspend_test.go index 51e231388..2f167d776 100644 --- a/apps/daemon/internal/cli/connect_suspend_test.go +++ b/apps/daemon/internal/cli/connect_suspend_test.go @@ -15,6 +15,8 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -55,7 +57,7 @@ func TestPlannedReconnectRequiresAuthenticatedMatchingResume(t *testing.T) { defer cancel() done := make(chan error, 1) go func() { - done <- runSuspendLoop(ctx, dial, registry, &transport.BootstrapResponse{HeartbeatSeconds: 1}, agentCLIDiscovery{}, control) + done <- runSuspendLoop(ctx, dial, registry, &machineapi.BootstrapResponse{HeartbeatSeconds: 1}, agentCLIDiscovery{}, control) }() var first *websocket.Conn select { @@ -203,7 +205,7 @@ func TestRunningSourceOnlyAcceptsExplicitRollbackOrItsReceipt(t *testing.T) { defer cancel() done := make(chan error, 1) go func() { - done <- runSuspendLoop(ctx, dial, agent.NewRegistry(), &transport.BootstrapResponse{HeartbeatSeconds: 60}, agentCLIDiscovery{}, control) + done <- runSuspendLoop(ctx, dial, agent.NewRegistry(), &machineapi.BootstrapResponse{HeartbeatSeconds: 60}, agentCLIDiscovery{}, control) }() var peer *websocket.Conn select { @@ -373,7 +375,7 @@ func TestQuiesceRetainsExecutorAcrossReconnectAndShutdownConfirmsCleanup(t *test control := &suspendControl{path: filepath.Join(t.TempDir(), "control.json"), identity: suspendIdentity{EnvironmentID: environment}, signal: make(chan os.Signal, 1)} finished := make(chan error, 1) go func() { - finished <- runSuspendLoop(ctx, dial, registry, &transport.BootstrapResponse{HeartbeatSeconds: 60}, agentCLIDiscovery{}, control) + finished <- runSuspendLoop(ctx, dial, registry, &machineapi.BootstrapResponse{HeartbeatSeconds: 60}, agentCLIDiscovery{}, control) }() var peer *websocket.Conn select { diff --git a/apps/daemon/internal/cli/native_download_test.go b/apps/daemon/internal/cli/native_download_test.go index 86b9f7c91..d13232e29 100644 --- a/apps/daemon/internal/cli/native_download_test.go +++ b/apps/daemon/internal/cli/native_download_test.go @@ -21,7 +21,8 @@ import ( "testing" "time" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" ) @@ -52,7 +53,7 @@ func newFakeCore(t *testing.T, workspace string) (*fakeCore, string) { w.WriteHeader(core.status) return } - _ = json.NewEncoder(w).Encode(v1.NativeInstallationContext{Version: Version, ProtocolVersion: proto.Version, EnvironmentID: environment, + _ = json.NewEncoder(w).Encode(machineapi.InstallationContext{Version: Version, ProtocolVersion: proto.Version, EnvironmentID: environment, RemoteURL: strings.Replace(core.URL, "http://", "ws://", 1) + "/api/v1/agent-daemon/ws", Workspace: workspace, Harness: nativeHarnesses["codex"].AgentKind}) default: name, ext, _ := strings.Cut(strings.TrimPrefix(r.URL.Path, "/api/v1/agent-daemon/install/"+Version+"/"+runtime.GOOS+"-"+runtime.GOARCH+"-"), ".") @@ -105,7 +106,7 @@ func (c *fakeCore) add(t *testing.T, name string) nativeComponent { func nativeDownloadInstall(t *testing.T, core *fakeCore, bundle string, extra ...string) error { t.Helper() - t.Setenv(v1.NativeInstallGrantEnv, "grant") + t.Setenv(machineapi.InstallGrantEnv, "grant") out := new(bytes.Buffer) rc := &runContext{stdin: strings.NewReader(""), stdout: out, stderr: out} o, err := parseNativeInstall(rc, append([]string{"--non-interactive", "--onboard-url", core.URL + "/api/v1/agent-daemon/installation", "--bundle-dir", bundle, "--harness", "codex"}, extra...)) diff --git a/apps/daemon/internal/cli/native_install.go b/apps/daemon/internal/cli/native_install.go index 4aee448a3..bbeb47419 100644 --- a/apps/daemon/internal/cli/native_install.go +++ b/apps/daemon/internal/cli/native_install.go @@ -13,6 +13,8 @@ import ( "slices" "strings" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" "github.com/MiniMax-AI/OpenAgentCore/internal/runtimefs" @@ -71,7 +73,7 @@ func validateNativeInstallation(c nativeInstallation) error { if _, err := environmentBase(c.Remote); err != nil { return err } - if !environmentUUID(c.Environment) { + if !machineapi.CanonicalUUID(c.Environment) { return errors.New("install: --environment-id requires a canonical Environment ID") } for _, p := range []string{c.Workspace, c.Credential, c.CapabilityDirectory} { diff --git a/apps/daemon/internal/cli/native_install_input.go b/apps/daemon/internal/cli/native_install_input.go index 92398ea47..7d06d2fb6 100644 --- a/apps/daemon/internal/cli/native_install_input.go +++ b/apps/daemon/internal/cli/native_install_input.go @@ -10,8 +10,9 @@ import ( "path/filepath" "strings" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" ) type nativeInstallOptions struct { @@ -24,8 +25,8 @@ type nativeInstallOptions struct { func parseNativeInstall(rc *runContext, args []string) (nativeInstallOptions, error) { var o nativeInstallOptions // Take the grant first and drop it from the environment, which start, the daemon and Harnesses inherit. - o.Authorization = os.Getenv(v1.NativeInstallGrantEnv) - _ = os.Unsetenv(v1.NativeInstallGrantEnv) + o.Authorization = os.Getenv(machineapi.InstallGrantEnv) + _ = os.Unsetenv(machineapi.InstallGrantEnv) flags := newFlagSet("install") flags.StringVar(&o.OnboardURL, "onboard-url", "", "Core installation endpoint") flags.StringVar(&o.Remote, "remote", "", "Environment remote_url from Core") diff --git a/apps/daemon/internal/cli/native_install_test.go b/apps/daemon/internal/cli/native_install_test.go index 5f02fe54b..e5999f038 100644 --- a/apps/daemon/internal/cli/native_install_test.go +++ b/apps/daemon/internal/cli/native_install_test.go @@ -14,7 +14,8 @@ import ( "strings" "testing" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/google/uuid" ) @@ -329,14 +330,14 @@ func TestNativeInstallationConnectCannotBypassValidation(t *testing.T) { func TestNativeInstallTakesGrantOutOfTheEnvironment(t *testing.T) { rc, _, _, _ := nativeInstallFixture(t) - t.Setenv(v1.NativeInstallGrantEnv, "private-grant") + t.Setenv(machineapi.InstallGrantEnv, "private-grant") // The endpoint is invalid so parsing stops before it contacts Core; the grant is already taken. o, _ := parseNativeInstall(rc, []string{"--onboard-url", "https://core.example/other", "--non-interactive"}) if o.Authorization != "private-grant" { t.Fatal("grant not read from the environment") } for _, entry := range withNativeEnv(nil) { - if strings.HasPrefix(entry, v1.NativeInstallGrantEnv+"=") { + if strings.HasPrefix(entry, machineapi.InstallGrantEnv+"=") { t.Fatal("grant reaches child processes") } } diff --git a/apps/daemon/internal/cli/native_onboarding.go b/apps/daemon/internal/cli/native_onboarding.go index f5e517416..992579149 100644 --- a/apps/daemon/internal/cli/native_onboarding.go +++ b/apps/daemon/internal/cli/native_onboarding.go @@ -16,9 +16,10 @@ import ( "strings" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/runtimefs" ) @@ -65,7 +66,7 @@ func prepareOnboarding(o *nativeInstallOptions) error { return nil } if o.OnboardURL == "" || o.Authorization == "" { - return errors.New("install: --onboard-url and " + v1.NativeInstallGrantEnv + " are required together") + return errors.New("install: --onboard-url and " + machineapi.InstallGrantEnv + " are required together") } const suffix = "/api/v1/agent-daemon/installation" if !strings.HasSuffix(o.OnboardURL, suffix) { @@ -76,14 +77,14 @@ func prepareOnboarding(o *nativeInstallOptions) error { if _, err := environmentBase(remote); err != nil { return err } - var info v1.NativeInstallationContext + var info machineapi.InstallationContext if err := installationRequest(context.Background(), o.OnboardURL, o.Authorization, nil, &info); err != nil { return err } if info.Version != Version || !proto.VersionCompatible(info.ProtocolVersion) { return errors.New("install: installer does not match Core; obtain a fresh command from the target Core") } - if info.RemoteURL != remote || !environmentUUID(info.EnvironmentID) { + if info.RemoteURL != remote || !machineapi.CanonicalUUID(info.EnvironmentID) { return errors.New("install: Core returned an invalid connection binding") } if (o.Remote != "" && o.Remote != info.RemoteURL) || (o.Environment != "" && o.Environment != info.EnvironmentID) || (o.Workspace != "" && o.Workspace != info.Workspace) || o.Credential != "" { @@ -145,7 +146,7 @@ func prepareOnboardingCredential(ctx context.Context, o *nativeInstallOptions, h } else { return err } - err := installationRequest(ctx, o.OnboardURL+"/claim", o.Authorization, map[string]string{"executor_token": secret}, nil) + err := installationRequest(ctx, o.OnboardURL+"/claim", o.Authorization, machineapi.InstallationClaim{ExecutorToken: secret}, nil) if errors.Is(err, errCredentialClaimed) { // This directory holds no installation, and Core never accepts this secret. _ = held.Remove("executor-credential.json") @@ -214,12 +215,9 @@ func installedEnvironmentConnected(ctx context.Context, base, environment, secre if response.StatusCode != http.StatusOK { return false, nil } - var result struct { - Environment string `json:"environment_id"` - Status string `json:"status"` - } - if json.NewDecoder(io.LimitReader(response.Body, 4096)).Decode(&result) != nil || result.Environment != environment { + var result machineapi.Connection + if json.NewDecoder(io.LimitReader(response.Body, 4096)).Decode(&result) != nil || result.EnvironmentID != environment { return false, errors.New("Daemon connection: invalid status returned by Core") } - return result.Status == "connected", nil + return result.Status == machineapi.ConnectionConnected, nil } diff --git a/apps/daemon/internal/cli/native_start_interrupt_unix_test.go b/apps/daemon/internal/cli/native_start_interrupt_unix_test.go index 5f639f826..24f2812b9 100644 --- a/apps/daemon/internal/cli/native_start_interrupt_unix_test.go +++ b/apps/daemon/internal/cli/native_start_interrupt_unix_test.go @@ -15,6 +15,8 @@ import ( "strings" "testing" "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" ) func TestNativeStartInterruptHelper(t *testing.T) { @@ -39,7 +41,7 @@ func TestNativeStartInterruptCancelsEnrollment(t *testing.T) { if err := readNativeJSON(filepath.Join(root, "daemon", "installation.json"), &config); err != nil { return } - _ = json.NewEncoder(w).Encode(environmentEnrollment{ + _ = json.NewEncoder(w).Encode(machineapi.Enrollment{ DeviceID: "aaaaaaaa-1111-4111-8111-aaaaaaaaaaaa", SessionID: "bbbbbbbb-1111-4111-8111-bbbbbbbbbbbb", EnvironmentID: config.Environment, diff --git a/apps/daemon/internal/transport/bootstrap.go b/apps/daemon/internal/transport/bootstrap.go index c40e76470..5ca44a9ed 100644 --- a/apps/daemon/internal/transport/bootstrap.go +++ b/apps/daemon/internal/transport/bootstrap.go @@ -19,34 +19,18 @@ import ( "net/http" "net/url" "strings" - "time" -) - -// BootstrapResponse mirrors gateway/handler.go's Bootstrap response. -type BootstrapResponse struct { - DeviceID string `json:"device_id"` - WSURL string `json:"ws_url"` - HeartbeatSeconds int `json:"heartbeat_seconds"` -} -// HeartbeatInterval defends against the server returning 0 — a zero -// heartbeat means "never ping" which guarantees a 60s timeout, so we -// fall back to 15s instead of silently breaking liveness. -func (b BootstrapResponse) HeartbeatInterval() time.Duration { - if b.HeartbeatSeconds <= 0 { - return 15 * time.Second - } - return time.Duration(b.HeartbeatSeconds) * time.Second -} + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" +) // Bootstrap calls POST /agent-daemon/bootstrap with the // device's runner_credential as a bearer token. -func Bootstrap(ctx context.Context, serverURL, deviceID, credential, daemonVersion string) (*BootstrapResponse, error) { +func Bootstrap(ctx context.Context, serverURL, deviceID, credential, daemonVersion string) (*machineapi.BootstrapResponse, error) { return BootstrapWithClient(ctx, http.DefaultClient, serverURL, deviceID, credential, daemonVersion) } // BootstrapWithClient lets a connection entry point enforce its redirect policy. -func BootstrapWithClient(ctx context.Context, client *http.Client, serverURL, deviceID, credential, daemonVersion string) (*BootstrapResponse, error) { +func BootstrapWithClient(ctx context.Context, client *http.Client, serverURL, deviceID, credential, daemonVersion string) (*machineapi.BootstrapResponse, error) { if strings.TrimSpace(serverURL) == "" { return nil, fmt.Errorf("transport.Bootstrap: serverURL required") } @@ -56,7 +40,7 @@ func BootstrapWithClient(ctx context.Context, client *http.Client, serverURL, de if strings.TrimSpace(credential) == "" { return nil, fmt.Errorf("transport.Bootstrap: credential required") } - body, err := json.Marshal(map[string]string{"device_id": deviceID}) + body, err := json.Marshal(machineapi.BootstrapRequest{DeviceID: deviceID}) if err != nil { return nil, fmt.Errorf("transport.Bootstrap: marshal body: %w", err) } @@ -84,7 +68,7 @@ func BootstrapWithClient(ctx context.Context, client *http.Client, serverURL, de if resp.StatusCode/100 != 2 { return nil, fmt.Errorf("transport.Bootstrap: server returned %s: %s", resp.Status, strings.TrimSpace(string(raw))) } - var out BootstrapResponse + var out machineapi.BootstrapResponse if err := json.Unmarshal(raw, &out); err != nil { return nil, fmt.Errorf("transport.Bootstrap: decode response: %w", err) } @@ -115,7 +99,7 @@ func joinURL(base, path string) (string, error) { // is derived from serverBase by swapping http→ws / https→wss. Non- // absolute ws_url is rejected — the server-side handler is the only // component that knows the externally-reachable host. -func DeriveWSURL(boot BootstrapResponse, serverBase string) (string, error) { +func DeriveWSURL(boot machineapi.BootstrapResponse, serverBase string) (string, error) { if abs := strings.TrimSpace(boot.WSURL); abs != "" { u, err := url.Parse(abs) if err != nil { diff --git a/apps/daemon/internal/transport/bootstrap_test.go b/apps/daemon/internal/transport/bootstrap_test.go index 66575b941..7585e8304 100644 --- a/apps/daemon/internal/transport/bootstrap_test.go +++ b/apps/daemon/internal/transport/bootstrap_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport" ) @@ -115,7 +117,7 @@ func TestBootstrapRejectsEmptyInputs(t *testing.T) { } func TestDeriveWSURLPrefersAbsolute(t *testing.T) { - got, err := transport.DeriveWSURL(transport.BootstrapResponse{WSURL: "wss://prod/agent-daemon/ws"}, "https://anything") + got, err := transport.DeriveWSURL(machineapi.BootstrapResponse{WSURL: "wss://prod/agent-daemon/ws"}, "https://anything") if err != nil { t.Fatalf("DeriveWSURL: %v", err) } @@ -132,7 +134,7 @@ func TestDeriveWSURLFallsBackToServerBase(t *testing.T) { {"https://core.example.com/api", "wss://core.example.com/api/agent-daemon/ws"}, } for _, tc := range cases { - got, err := transport.DeriveWSURL(transport.BootstrapResponse{WSURL: ""}, tc.base) + got, err := transport.DeriveWSURL(machineapi.BootstrapResponse{WSURL: ""}, tc.base) if err != nil { t.Errorf("DeriveWSURL(%q): %v", tc.base, err) continue @@ -144,14 +146,14 @@ func TestDeriveWSURLFallsBackToServerBase(t *testing.T) { } func TestDeriveWSURLRejectsRelativeWSURL(t *testing.T) { - _, err := transport.DeriveWSURL(transport.BootstrapResponse{WSURL: "/agent-daemon/ws"}, "https://x") + _, err := transport.DeriveWSURL(machineapi.BootstrapResponse{WSURL: "/agent-daemon/ws"}, "https://x") if err == nil { t.Fatal("DeriveWSURL accepted relative ws_url") } } func TestDeriveWSURLRejectsBadScheme(t *testing.T) { - _, err := transport.DeriveWSURL(transport.BootstrapResponse{WSURL: ""}, "ftp://example") + _, err := transport.DeriveWSURL(machineapi.BootstrapResponse{WSURL: ""}, "ftp://example") if err == nil { t.Fatal("DeriveWSURL accepted ftp scheme") } diff --git a/contracts/agents-api/machine-api.md b/contracts/agents-api/machine-api.md index 81e7916d0..21f3e0fde 100644 --- a/contracts/agents-api/machine-api.md +++ b/contracts/agents-api/machine-api.md @@ -4,6 +4,8 @@ title: "Machine connection API" Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons and the self-hosted installer. Each route accepts only the credential listed for it, never the Core key or a Project API key, and a console sign-in grants nothing here. The reverse proxy sends `/api/v1` directly to Core; Web never serves these routes. +Every request and response body is a type of `internal/machineapi/machineapi.go`, except the node configuration, whose `specification` is the [Sandbox Provider protocol](../../docs/sandbox-provider.md) type. + ## Routes | Route | Caller | Credential | Contract | diff --git a/contracts/agents-api/runtime.openapi.yaml b/contracts/agents-api/runtime.openapi.yaml index 1f8718377..3d8aa825a 100644 --- a/contracts/agents-api/runtime.openapi.yaml +++ b/contracts/agents-api/runtime.openapi.yaml @@ -31,12 +31,42 @@ definitions: required: - error type: object - api.NativeInstallationClaim: + deployment.NodeConfiguration: + properties: + core_url: + type: string + generation: + type: integer + installation_id: + type: string + provider: + type: string + specification: + $ref: '#/definitions/sandbox.DeploymentSpec' + specification_digest: + type: string + type: object + machineapi.InstallationClaim: properties: executor_token: type: string type: object - deployment.Enrollment: + machineapi.InstallationContext: + properties: + environment_id: + type: string + harness: + type: string + protocol_version: + type: string + remote_url: + type: string + version: + type: string + workspace_directory: + type: string + type: object + machineapi.NodeEnrollment: properties: available_disk_bytes: type: integer @@ -65,22 +95,7 @@ definitions: total_memory_bytes: type: integer type: object - deployment.NodeConfiguration: - properties: - core_url: - type: string - generation: - type: integer - installation_id: - type: string - provider: - type: string - specification: - $ref: '#/definitions/sandbox.DeploymentSpec' - specification_digest: - type: string - type: object - deployment.NodeIdentity: + machineapi.NodeIdentity: properties: deployment_generation: type: integer @@ -97,7 +112,7 @@ definitions: specification_digest: type: string type: object - deployment.NodeStatus: + machineapi.NodeStatus: properties: connected: type: boolean @@ -171,21 +186,6 @@ definitions: required: - error type: object - v1.NativeInstallationContext: - properties: - environment_id: - type: string - harness: - type: string - protocol_version: - type: string - remote_url: - type: string - version: - type: string - workspace_directory: - type: string - type: object workspacefs.AttachmentKind: enum: - host_directory @@ -223,7 +223,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/v1.NativeInstallationContext' + $ref: '#/definitions/machineapi.InstallationContext' "401": description: Unauthorized schema: @@ -250,7 +250,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/api.NativeInstallationClaim' + $ref: '#/definitions/machineapi.InstallationClaim' responses: "204": description: No Content @@ -328,14 +328,14 @@ paths: name: body required: true schema: - $ref: '#/definitions/deployment.Enrollment' + $ref: '#/definitions/machineapi.NodeEnrollment' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/deployment.NodeIdentity' + $ref: '#/definitions/machineapi.NodeIdentity' "400": description: Bad Request schema: @@ -380,7 +380,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/deployment.NodeStatus' + $ref: '#/definitions/machineapi.NodeStatus' "400": description: Bad Request schema: diff --git a/contracts/agents-api/v1/installation.go b/contracts/agents-api/v1/installation.go index 8fab83f69..56720696e 100644 --- a/contracts/agents-api/v1/installation.go +++ b/contracts/agents-api/v1/installation.go @@ -21,16 +21,3 @@ type EnvironmentInstallation struct { Commands map[string]string `json:"commands,omitempty"` Message string `json:"message,omitempty"` } - -// NativeInstallGrantEnv names the environment variable that carries the short-lived installation grant from Core's command to the installer. It never carries an executor credential. -const NativeInstallGrantEnv = "OAC_INSTALL_GRANT" - -// NativeInstallationContext is bootstrap metadata, not an execution protocol. -type NativeInstallationContext struct { - Version string `json:"version"` - ProtocolVersion string `json:"protocol_version"` - EnvironmentID string `json:"environment_id"` - RemoteURL string `json:"remote_url"` - Workspace string `json:"workspace_directory"` - Harness string `json:"harness"` -} diff --git a/contracts/agents-api/zh/machine-api.md b/contracts/agents-api/zh/machine-api.md index 8f3a10748..5101b5d16 100644 --- a/contracts/agents-api/zh/machine-api.md +++ b/contracts/agents-api/zh/machine-api.md @@ -1,11 +1,13 @@ --- title: "机器连接 API" source: contracts/agents-api/machine-api.md -source_hash: f0b384850366cbbbe9a868c018963bd446b052432ed1617285f8e8bcf7c1569c +source_hash: c5fbb966d2d68ba4c0b08cd47d4e09da58f03e9fd271f3998b7ec6f214510422 --- 机器通过 `/api/v1` 调用 Core:包括沙箱节点、Runtime daemon 和自托管安装程序。各路由仅接受所列凭据,不接受 Core 密钥或项目 API 密钥;控制台登录也不授予此处权限。反向代理将 `/api/v1` 直接发送给 Core;Web 不提供这些路由。 +每个请求和响应体都是 `internal/machineapi/machineapi.go` 中的类型,节点配置除外:它的 `specification` 是[沙箱 Provider 协议](../../../docs/zh/sandbox-provider.md)中的类型。 + ## 路由 {#routes} | 路由 | 调用方 | 凭据 | 契约 | diff --git a/docs/development.md b/docs/development.md index e4bad1ea7..53249a157 100644 --- a/docs/development.md +++ b/docs/development.md @@ -101,6 +101,7 @@ For frontend development, run `pnpm dev:web` using the fixture or Core connectio | `services/core/internal/execution` | Durable Turn dispatch and scheduling | [Runtime protocol](./runtime-protocol.md) | | `internal/harnessconfig` | One declaration per Harness, read by Core admission and the Runtime | [Harness protocol](../contracts/agents-api/harness-onboarding.md) | | `internal/agentdaemon/proto` | Core–Runtime wire types and validators | [Runtime protocol](./runtime-protocol.md) | +| `internal/machineapi` | Machine connection API wire types | [Machine connection API](../contracts/agents-api/machine-api.md) | | `internal/runtimebootstrap` | Provider-to-Runtime startup input | [Runtime bootstrap](./runtime-bootstrap.md) | | `apps/daemon/internal/dispatch` | Runtime preparation, Executor reuse, Turn and cleanup ownership | [Harness lifecycle](../contracts/agents-api/harness-onboarding.md#required-adapter-interfaces) | | `apps/daemon/internal/agent` | Native Harness adapters | [Native references](../contracts/agents-api/harness-onboarding.md#native-references) | diff --git a/docs/zh/development.md b/docs/zh/development.md index 102c1183b..0fdfe3263 100644 --- a/docs/zh/development.md +++ b/docs/zh/development.md @@ -1,7 +1,7 @@ --- title: "开发 OpenAgentCore" source: docs/development.md -source_hash: f0e015b4f13aeafa5ec486c42a5d4210f6dd9a9ce40b34112cdef1ed3aad28ba +source_hash: 31487de5d69fee69c08b7293fba5c36c17c70251ccda8361d79e6b41bf1a5a58 --- 准备工作副本,构建组件并验证修改。如需使用已安装的实例,从[概览](getting-started/index.md)开始。修改代码前阅读[贡献者规则](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md)。 @@ -103,6 +103,7 @@ Core 构建产物和输出目录设置见[独立 Core 构建](maintainers.md#sta | `services/core/internal/execution` | 持久化 Turn 分发与调度 | [Runtime 协议](runtime-protocol.md) | | `internal/harnessconfig` | 每个 Harness 一份声明,由 Core 准入和 Runtime 共同读取 | [Harness 协议](../../contracts/agents-api/zh/harness-onboarding.md) | | `internal/agentdaemon/proto` | Core–Runtime wire 类型与验证器 | [Runtime 协议](runtime-protocol.md) | +| `internal/machineapi` | 机器连接 API 线路类型 | [机器连接 API](../../contracts/agents-api/zh/machine-api.md) | | `internal/runtimebootstrap` | 提供商到 Runtime 的启动输入 | [Runtime 引导](runtime-bootstrap.md) | | `apps/daemon/internal/dispatch` | Runtime 准备、Executor 复用、Turn 和清理所有权 | [Harness 生命周期](../../contracts/agents-api/zh/harness-onboarding.md#required-adapter-interfaces) | | `apps/daemon/internal/agent` | 原生 Harness adapter | [原生参考](../../contracts/agents-api/zh/harness-onboarding.md#native-references) | diff --git a/internal/machineapi/machineapi.go b/internal/machineapi/machineapi.go new file mode 100644 index 000000000..51988d1f2 --- /dev/null +++ b/internal/machineapi/machineapi.go @@ -0,0 +1,129 @@ +// Package machineapi is the code of the machine connection API: every request and response body of Core's /api/v1 routes. Core's handlers, the daemon and the sandbox node all use these types. The routes, credentials and errors are in contracts/agents-api/machine-api.md. +package machineapi + +import ( + "errors" + "time" + + "github.com/google/uuid" +) + +// InstallGrantEnv names the environment variable that carries the short-lived installation grant from Core's command to the installer. It never carries an executor credential. +const InstallGrantEnv = "OAC_INSTALL_GRANT" + +// InstallationContext answers POST agent-daemon/installation: the frozen connection constraints of one Session. It is bootstrap metadata, not an execution protocol. +type InstallationContext struct { + Version string `json:"version"` + ProtocolVersion string `json:"protocol_version"` + EnvironmentID string `json:"environment_id"` + RemoteURL string `json:"remote_url"` + Workspace string `json:"workspace_directory"` + Harness string `json:"harness"` +} + +// InstallationClaim is the body of POST agent-daemon/installation/claim. +type InstallationClaim struct { + ExecutorToken string `json:"executor_token"` +} + +// BootstrapRequest is the body of POST agent-daemon/bootstrap. +type BootstrapRequest struct { + DeviceID string `json:"device_id"` +} + +// BootstrapResponse answers POST agent-daemon/bootstrap. +type BootstrapResponse struct { + DeviceID string `json:"device_id"` + WSURL string `json:"ws_url"` + HeartbeatSeconds int `json:"heartbeat_seconds"` +} + +// DefaultHeartbeat applies when Core reports no positive heartbeat_seconds. A zero heartbeat would never ping and always hit the 60 second timeout. +const DefaultHeartbeat = 15 * time.Second + +func (b BootstrapResponse) HeartbeatInterval() time.Duration { + if b.HeartbeatSeconds <= 0 { + return DefaultHeartbeat + } + return time.Duration(b.HeartbeatSeconds) * time.Second +} + +// AuthError is the body of every failed agent-daemon/bootstrap and agent-daemon/ws request. +type AuthError struct { + Error string `json:"error"` + Detail string `json:"detail"` +} + +// EnrollRequest is the body of POST agent-daemon/enroll. +type EnrollRequest struct { + EnvironmentID string `json:"environment_id"` +} + +// Enrollment answers POST agent-daemon/enroll: the device bound to the Environment's Session. +type Enrollment struct { + DeviceID string `json:"device_id"` + SessionID string `json:"session_id"` + EnvironmentID string `json:"environment_id"` + WorkspaceDirectory string `json:"workspace_directory"` +} + +// Validate checks that the binding names canonical IDs and the requested Environment. +func (e Enrollment) Validate(environment string) error { + if !CanonicalUUID(e.DeviceID) || !CanonicalUUID(e.SessionID) || e.EnvironmentID != environment { + return errors.New("machineapi: invalid enrollment binding") + } + return nil +} + +// Connection answers GET agent-daemon/connection. +type Connection struct { + EnvironmentID string `json:"environment_id"` + Status string `json:"status"` +} + +const ( + ConnectionConnected = "connected" + ConnectionDisconnected = "disconnected" +) + +// CanonicalUUID reports whether value is a non-nil UUID in canonical lower-case form. +func CanonicalUUID(value string) bool { + id, err := uuid.Parse(value) + return err == nil && id != uuid.Nil && id.String() == value +} + +// NodeEnrollment is the body of POST sandbox-node/enroll. +type NodeEnrollment struct { + SpecificationDigest string `json:"specification_digest"` + DeploymentGeneration uint64 `json:"deployment_generation"` + NodeID string `json:"node_id"` + Credential string `json:"credential"` + Name string `json:"name"` + Provider string `json:"provider"` + BackendFingerprint string `json:"backend_fingerprint"` + // The Core origin this node stores and connects to, such as https://core.example. It must equal the installation public URL; otherwise enrollment gets 409 sandbox_node_address_mismatch and the token stays unused. + CoreURL string `json:"core_url"` + // Host measurements as in the node's health reports. Core derives a limit the enrollment token leaves unset from them. + CPUCount *int64 `json:"cpu_count,omitempty"` + EffectiveCPUCores *float64 `json:"effective_cpu_cores,omitempty"` + TotalMemoryBytes *int64 `json:"total_memory_bytes,omitempty"` + AvailableDiskBytes *int64 `json:"available_disk_bytes,omitempty"` +} + +// NodeIdentity answers POST sandbox-node/enroll. +type NodeIdentity struct { + SpecificationDigest string `json:"specification_digest"` + DeploymentGeneration uint64 `json:"deployment_generation"` + NodeID string `json:"node_id"` + InstallationID string `json:"installation_id"` + Provider string `json:"provider"` + MaxActive int `json:"max_active"` + MaxRetained int `json:"max_retained"` +} + +// NodeStatus answers GET sandbox-node/identity: the authenticated node's identity and Core-owned presence. +type NodeStatus struct { + NodeIdentity + Connected bool `json:"connected"` + ProviderReady bool `json:"provider_ready"` +} diff --git a/scripts/native-onboarding-smoke.mjs b/scripts/native-onboarding-smoke.mjs index 05bf83c73..e585ee999 100644 --- a/scripts/native-onboarding-smoke.mjs +++ b/scripts/native-onboarding-smoke.mjs @@ -55,7 +55,7 @@ const server = createServer(async (request, response) => { } if (!secret || request.headers.authorization !== `Bearer ${secret}`) return json(401, {}); if (url.pathname.endsWith('/enroll')) return json(200, { device_id: device, session_id: session, environment_id: environment, workspace_directory: workspace }); - if (url.pathname.endsWith('/bootstrap')) return json(200, { device_id: device, workspace_id: session, ws_url: remote, heartbeat_seconds: 15, protocol_version: protocol }); + if (url.pathname.endsWith('/bootstrap')) return json(200, { device_id: device, ws_url: remote, heartbeat_seconds: 15 }); if (url.pathname.endsWith('/connection')) return json(200, { environment_id: environment, status: connected ? 'connected' : 'disconnected' }); return json(404, {}); }); diff --git a/services/core/cmd/sandbox-node/main.go b/services/core/cmd/sandbox-node/main.go index 5b6695c6b..24f47cf4e 100644 --- a/services/core/cmd/sandbox-node/main.go +++ b/services/core/cmd/sandbox-node/main.go @@ -15,6 +15,8 @@ import ( "syscall" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" @@ -116,9 +118,9 @@ func run(ctx context.Context, args []string) error { if err != nil { return fmt.Errorf("local provider readiness check failed (%s): %w", sandbox.NodeDiagnostic(err), err) } - stored, err := node.Enroll(ctx, *coreURL, *stateDir, token, node.EnrollmentRequest{Name: *name}) + stored, err := node.Enroll(ctx, *coreURL, *stateDir, token, machineapi.NodeEnrollment{Name: *name}) if err != nil { return err } - return json.NewEncoder(os.Stdout).Encode(node.EnrollmentResponse{MaxActive: stored.Identity.MaxActive, MaxRetained: stored.Identity.MaxRetained, SpecificationDigest: stored.Identity.SpecificationDigest, DeploymentGeneration: stored.Identity.DeploymentGeneration, NodeID: stored.Identity.NodeID, InstallationID: stored.Identity.InstallationID, Provider: stored.Identity.Provider}) + return json.NewEncoder(os.Stdout).Encode(machineapi.NodeIdentity{MaxActive: stored.Identity.MaxActive, MaxRetained: stored.Identity.MaxRetained, SpecificationDigest: stored.Identity.SpecificationDigest, DeploymentGeneration: stored.Identity.DeploymentGeneration, NodeID: stored.Identity.NodeID, InstallationID: stored.Identity.InstallationID, Provider: stored.Identity.Provider}) } diff --git a/services/core/internal/api/environment_installation.go b/services/core/internal/api/environment_installation.go index 4cdbab4d6..ffd42766c 100644 --- a/services/core/internal/api/environment_installation.go +++ b/services/core/internal/api/environment_installation.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" @@ -96,7 +98,7 @@ func (h *Handler) installationAuthorization(w http.ResponseWriter, r *http.Reque // @Description Accepts a short-lived Environment installation Bearer authorization, not a Project or Core key. Returns frozen connection constraints; it does not claim or rotate credentials. // @Tags Native Installation // @Produce json -// @Success 200 {object} v1.NativeInstallationContext +// @Success 200 {object} machineapi.InstallationContext // @Failure 401,404,503 {object} CoreErrorResponse // @Router /api/v1/agent-daemon/installation [post] func (h *Handler) prepareNativeInstallation(w http.ResponseWriter, r *http.Request) { @@ -119,18 +121,14 @@ func (h *Handler) prepareNativeInstallation(w http.ResponseWriter, r *http.Reque writeSessionsError(w, r, err) return } - writeJSON(w, http.StatusOK, v1.NativeInstallationContext{Version: h.Execution.NativeInstaller.Version, ProtocolVersion: proto.Version, EnvironmentID: claim.Environment, RemoteURL: h.Execution.ExecutorURL, Workspace: response.Environment.WorkspaceDirectory, Harness: session.Engine}) -} - -type NativeInstallationClaim struct { - ExecutorToken string `json:"executor_token"` + writeJSON(w, http.StatusOK, machineapi.InstallationContext{Version: h.Execution.NativeInstaller.Version, ProtocolVersion: proto.Version, EnvironmentID: claim.Environment, RemoteURL: h.Execution.ExecutorURL, Workspace: response.Environment.WorkspaceDirectory, Harness: session.Engine}) } // @Summary Claim an Environment's installation credential // @Description A valid installation Bearer authorization can claim one connect-only key. The client persists its generated secret before submitting it. Retries must present that same secret; a different, rotated or revoked credential is never replaced. // @Tags Native Installation // @Accept json -// @Param body body api.NativeInstallationClaim true "Locally persisted executor secret" +// @Param body body machineapi.InstallationClaim true "Locally persisted executor secret" // @Success 204 // @Failure 400,401,409,503 {object} CoreErrorResponse // @Router /api/v1/agent-daemon/installation/claim [post] @@ -143,7 +141,7 @@ func (h *Handler) claimNativeInstallation(w http.ResponseWriter, r *http.Request if !ok { return } - var input NativeInstallationClaim + var input machineapi.InstallationClaim if decodeInputObject(raw, &input, "executor_token") != nil { writeSessionsError(w, r, sessions.ErrInvalidInput) return diff --git a/services/core/internal/api/fakes_test.go b/services/core/internal/api/fakes_test.go index 49e9087e1..c5f7f7e65 100644 --- a/services/core/internal/api/fakes_test.go +++ b/services/core/internal/api/fakes_test.go @@ -7,6 +7,8 @@ import ( "io" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" @@ -189,9 +191,9 @@ type fakeDeployment struct { updateNode func(context.Context, string, deployment.NodeUpdate) error removeNode func(context.Context, string) error createEnrollment func(context.Context, deployment.EnrollmentLimits) (deployment.EnrollmentToken, error) - enroll func(context.Context, string, deployment.Enrollment) (deployment.NodeIdentity, error) + enroll func(context.Context, string, machineapi.NodeEnrollment) (deployment.NodeIdentity, error) nodeConfiguration func(context.Context, string, string, uint64) (deployment.NodeConfiguration, error) - nodeStatus func(context.Context, string, string) (deployment.NodeStatus, error) + nodeStatus func(context.Context, string, string) (machineapi.NodeStatus, error) decodeConfiguration func(string, json.RawMessage, json.RawMessage) (sandbox.Configuration, error) } @@ -237,7 +239,7 @@ func (f *fakeDeployment) CreateEnrollment(a0 context.Context, a1 deployment.Enro return f.createEnrollment(a0, a1) } -func (f *fakeDeployment) Enroll(a0 context.Context, a1 string, a2 deployment.Enrollment) (deployment.NodeIdentity, error) { +func (f *fakeDeployment) Enroll(a0 context.Context, a1 string, a2 machineapi.NodeEnrollment) (deployment.NodeIdentity, error) { if f.enroll == nil { unexpectedCall(f.t, "Enroll") } @@ -251,7 +253,7 @@ func (f *fakeDeployment) NodeConfiguration(a0 context.Context, a1 string, a2 str return f.nodeConfiguration(a0, a1, a2, a3) } -func (f *fakeDeployment) NodeStatus(a0 context.Context, a1 string, a2 string) (deployment.NodeStatus, error) { +func (f *fakeDeployment) NodeStatus(a0 context.Context, a1 string, a2 string) (machineapi.NodeStatus, error) { if f.nodeStatus == nil { unexpectedCall(f.t, "NodeStatus") } diff --git a/services/core/internal/api/sandbox_manager.go b/services/core/internal/api/sandbox_manager.go index dba990413..b9934fd99 100644 --- a/services/core/internal/api/sandbox_manager.go +++ b/services/core/internal/api/sandbox_manager.go @@ -6,6 +6,8 @@ import ( "net/http" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/go-chi/chi/v5" @@ -45,9 +47,9 @@ type Deployment interface { UpdateNode(ctx context.Context, id string, update deployment.NodeUpdate) error RemoveNode(ctx context.Context, id string) error CreateEnrollment(ctx context.Context, limits deployment.EnrollmentLimits) (deployment.EnrollmentToken, error) - Enroll(ctx context.Context, token string, input deployment.Enrollment) (deployment.NodeIdentity, error) + Enroll(ctx context.Context, token string, input machineapi.NodeEnrollment) (deployment.NodeIdentity, error) NodeConfiguration(ctx context.Context, nodeID, token string, generation uint64) (deployment.NodeConfiguration, error) - NodeStatus(ctx context.Context, nodeID, credential string) (deployment.NodeStatus, error) + NodeStatus(ctx context.Context, nodeID, credential string) (machineapi.NodeStatus, error) // DecodeConfiguration decodes a submitted provider configuration and // credential with the provider's declared codec. DecodeConfiguration(provider string, public, credential json.RawMessage) (sandbox.Configuration, error) @@ -217,8 +219,8 @@ func (h *Handler) createSandboxEnrollment(w http.ResponseWriter, r *http.Request // @Produce json // @Security NodeEnrollmentAuth // @Accept json -// @Param body body deployment.Enrollment true "Request" -// @Success 201 {object} deployment.NodeIdentity +// @Param body body machineapi.NodeEnrollment true "Request" +// @Success 201 {object} machineapi.NodeIdentity // @Failure 400,401,404,409,500,503 {object} v1.ErrorResponse // @Router /api/v1/sandbox-node/enroll [post] func (h *Handler) enrollSandboxNode(w http.ResponseWriter, r *http.Request) { @@ -231,7 +233,7 @@ func (h *Handler) enrollSandboxNode(w http.ResponseWriter, r *http.Request) { if !ok { return } - var input deployment.Enrollment + var input machineapi.NodeEnrollment if decodeInputObject(raw, &input, "node_id", "credential", "name", "provider", "backend_fingerprint", "deployment_generation", "specification_digest", "core_url", "cpu_count", "effective_cpu_cores", "total_memory_bytes", "available_disk_bytes") != nil { writeDeploymentError(w, r, deployment.ErrInvalidInput) return @@ -245,7 +247,7 @@ func (h *Handler) enrollSandboxNode(w http.ResponseWriter, r *http.Request) { writeDeploymentError(w, r, err) return } - writeJSON(w, http.StatusCreated, value) + writeJSON(w, http.StatusCreated, value.NodeIdentity) } // @Summary Recover an enrolled sandbox node identity and observe its readiness @@ -254,7 +256,7 @@ func (h *Handler) enrollSandboxNode(w http.ResponseWriter, r *http.Request) { // @Produce json // @Security NodeAuth // @Param node_id query string true "Sandbox node UUID" -// @Success 200 {object} deployment.NodeStatus +// @Success 200 {object} machineapi.NodeStatus // @Failure 400,401,404,409,500,503 {object} v1.ErrorResponse // @Router /api/v1/sandbox-node/identity [get] func (h *Handler) sandboxNodeIdentity(w http.ResponseWriter, r *http.Request) { diff --git a/services/core/internal/deployment/allocations_test.go b/services/core/internal/deployment/allocations_test.go index a2288e36d..1d8117c9d 100644 --- a/services/core/internal/deployment/allocations_test.go +++ b/services/core/internal/deployment/allocations_test.go @@ -7,11 +7,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" "slices" "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" + "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" diff --git a/services/core/internal/deployment/capacity.go b/services/core/internal/deployment/capacity.go index 284e3fb11..8e7e0d954 100644 --- a/services/core/internal/deployment/capacity.go +++ b/services/core/internal/deployment/capacity.go @@ -5,6 +5,8 @@ import ( "math" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) @@ -69,7 +71,7 @@ func (s *Service) recommendations(ctx context.Context) (func(NodeHealth) *Capaci } // enrollmentHealth is the host an enrollment reports, validated like a health report. -func enrollmentHealth(e Enrollment) (NodeHealth, error) { +func enrollmentHealth(e machineapi.NodeEnrollment) (NodeHealth, error) { observed := time.Now() return normalizeHealth(NodeHealth{CPUCount: e.CPUCount, AvailableDiskBytes: e.AvailableDiskBytes, Host: &NodeHost{EffectiveCPUCores: e.EffectiveCPUCores, TotalMemoryBytes: e.TotalMemoryBytes, AvailableDiskBytes: e.AvailableDiskBytes, ObservedAt: &observed}}) } diff --git a/services/core/internal/deployment/node.go b/services/core/internal/deployment/node.go index 57c9d9439..154d364f7 100644 --- a/services/core/internal/deployment/node.go +++ b/services/core/internal/deployment/node.go @@ -3,18 +3,14 @@ package deployment import ( "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) +// NodeIdentity is the wire identity of a node and the backend namespace Core keeps for it. type NodeIdentity struct { - SpecificationDigest string `json:"specification_digest"` - DeploymentGeneration uint64 `json:"deployment_generation"` - NodeID string `json:"node_id"` - InstallationID string `json:"installation_id"` - Provider string `json:"provider"` - BackendFingerprint string `json:"-"` - MaxActive int `json:"max_active"` - MaxRetained int `json:"max_retained"` + machineapi.NodeIdentity + BackendFingerprint string `json:"-"` } // EnrollmentToken is an issued one-use node enrollment token. ID is a public, @@ -31,23 +27,6 @@ type Capacity struct { MaxRetained int `json:"max_retained" binding:"required"` } -type Enrollment struct { - SpecificationDigest string `json:"specification_digest"` - DeploymentGeneration uint64 `json:"deployment_generation"` - NodeID string `json:"node_id"` - Credential string `json:"credential"` - Name string `json:"name"` - Provider string `json:"provider"` - BackendFingerprint string `json:"backend_fingerprint"` - // The Core origin this node stores and connects to, such as https://core.example. It must equal the installation public URL; otherwise enrollment gets 409 sandbox_node_address_mismatch and the token stays unused. - CoreURL string `json:"core_url"` - // Host measurements as in the node's health reports. Core derives a limit the enrollment token leaves unset from them. - CPUCount *int64 `json:"cpu_count,omitempty"` - EffectiveCPUCores *float64 `json:"effective_cpu_cores,omitempty"` - TotalMemoryBytes *int64 `json:"total_memory_bytes,omitempty"` - AvailableDiskBytes *int64 `json:"available_disk_bytes,omitempty"` -} - // EnrollmentLimits are the limits an enrollment token approves. A nil limit is unset. type EnrollmentLimits struct { MaxActive, MaxRetained *int @@ -95,13 +74,7 @@ type NodeUpdate struct { MaxRetained int `json:"max_retained"` } -// NodeStatus reports only the authenticated node's Core-owned presence. -type NodeStatus struct { - NodeIdentity - Connected bool `json:"connected"` - ProviderReady bool `json:"provider_ready"` -} - +// NodeConfiguration answers GET /api/v1/sandbox-node/configuration. Its specification is the Sandbox Provider protocol's type, which the machine wire file cannot import. type NodeConfiguration struct { InstallationID string `json:"installation_id"` Provider string `json:"provider"` diff --git a/services/core/internal/deployment/nodes.go b/services/core/internal/deployment/nodes.go index d8069a746..1ad434a86 100644 --- a/services/core/internal/deployment/nodes.go +++ b/services/core/internal/deployment/nodes.go @@ -9,6 +9,8 @@ import ( "math" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/coremetrics" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" @@ -189,7 +191,7 @@ func (s *Service) CreateEnrollment(ctx context.Context, limits EnrollmentLimits) } // Enroll registers a node with a one-use enrollment token. -func (s *Service) Enroll(ctx context.Context, token string, input Enrollment) (NodeIdentity, error) { +func (s *Service) Enroll(ctx context.Context, token string, input machineapi.NodeEnrollment) (NodeIdentity, error) { nodeID, err := parseID(input.NodeID) if err != nil || !validNodeCredential(input.Credential) || !validDigest(input.BackendFingerprint) { return NodeIdentity{}, ErrInvalidInput @@ -304,7 +306,7 @@ func (s *Service) enrolledCapacity(d Record, spec sandbox.DeploymentSpec, host N } func identity(n StoredNode, provider string) NodeIdentity { - return NodeIdentity{SpecificationDigest: n.SpecificationDigest, DeploymentGeneration: n.DeploymentGeneration, NodeID: n.ID, InstallationID: n.InstallationID, Provider: provider, BackendFingerprint: n.BackendFingerprint, MaxActive: n.MaxActive, MaxRetained: n.MaxRetained} + return NodeIdentity{NodeIdentity: machineapi.NodeIdentity{SpecificationDigest: n.SpecificationDigest, DeploymentGeneration: n.DeploymentGeneration, NodeID: n.ID, InstallationID: n.InstallationID, Provider: provider, MaxActive: n.MaxActive, MaxRetained: n.MaxRetained}, BackendFingerprint: n.BackendFingerprint} } // AuthenticateNode checks a node credential and returns the node's identity. @@ -382,21 +384,21 @@ func (s *Service) generationSpecification(tx NodeReads, d Record, generation uin } // NodeStatus reports the authenticated node's Core-owned presence. -func (s *Service) NodeStatus(ctx context.Context, nodeID, credential string) (NodeStatus, error) { +func (s *Service) NodeStatus(ctx context.Context, nodeID, credential string) (machineapi.NodeStatus, error) { identity, err := s.AuthenticateNode(ctx, nodeID, credential) if err != nil { - return NodeStatus{}, err + return machineapi.NodeStatus{}, err } nodes, err := s.reader.Nodes(ctx) if err != nil { - return NodeStatus{}, err + return machineapi.NodeStatus{}, err } for _, n := range nodes { if n.ID == identity.NodeID { - return NodeStatus{NodeIdentity: identity, Connected: n.Online, ProviderReady: n.Online && n.ServingReady}, nil + return machineapi.NodeStatus{NodeIdentity: identity.NodeIdentity, Connected: n.Online, ProviderReady: n.Online && n.ServingReady}, nil } } - return NodeStatus{}, ErrNodeCredential + return machineapi.NodeStatus{}, ErrNodeCredential } // NodeConfiguration is the read-only bootstrap of an enrollment token or node diff --git a/services/core/internal/deployment/service_test.go b/services/core/internal/deployment/service_test.go index 51a2aca48..b95b8d12c 100644 --- a/services/core/internal/deployment/service_test.go +++ b/services/core/internal/deployment/service_test.go @@ -4,11 +4,14 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" "strings" "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" + "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" @@ -240,12 +243,12 @@ func TestSetupChangeDecisions(t *testing.T) { } } -// Enrollment checks the token before the deployment, so a bad token is a +// machineapi.NodeEnrollment checks the token before the deployment, so a bad token is a // credential error whatever the deployment's state. func TestEnrollChecksTheTokenBeforeTheDeployment(t *testing.T) { installation, token := uuid.NewString(), "enrollment-token" current := webDeployment(t, installation, "docker", 1) - valid := Enrollment{NodeID: uuid.NewString(), Name: "node", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), + valid := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "node", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: testSpecification("docker").Digest("docker"), CoreURL: testPublicURL} receipt := EnrollmentRecord{ID: uuid.NewString(), InstallationID: installation, ExpiresAt: time.Now().Add(10 * time.Minute), MaxActive: new(1), MaxRetained: new(1)} resetting := current @@ -266,7 +269,7 @@ func TestEnrollChecksTheTokenBeforeTheDeployment(t *testing.T) { deployment Record receipt EnrollmentRecord loadErr error - input Enrollment + input machineapi.NodeEnrollment want error }{ {"unknown token while resetting", resetting, EnrollmentRecord{}, ErrNotFound, valid, ErrNodeCredential}, @@ -297,11 +300,11 @@ func TestEnrollChecksTheTokenBeforeTheDeployment(t *testing.T) { // Malformed input is rejected before storage. service := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, testPublicURL) - for _, mutate := range []func(*Enrollment){ - func(e *Enrollment) { e.NodeID = "node" }, - func(e *Enrollment) { e.Credential = "short" }, - func(e *Enrollment) { e.BackendFingerprint = "fingerprint" }, - func(e *Enrollment) { e.Name = "" }, + for _, mutate := range []func(*machineapi.NodeEnrollment){ + func(e *machineapi.NodeEnrollment) { e.NodeID = "node" }, + func(e *machineapi.NodeEnrollment) { e.Credential = "short" }, + func(e *machineapi.NodeEnrollment) { e.BackendFingerprint = "fingerprint" }, + func(e *machineapi.NodeEnrollment) { e.Name = "" }, } { input := valid mutate(&input) diff --git a/services/core/internal/deployment/session_archive_test.go b/services/core/internal/deployment/session_archive_test.go index 981a5acf6..e3991ba6f 100644 --- a/services/core/internal/deployment/session_archive_test.go +++ b/services/core/internal/deployment/session_archive_test.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" "slices" "strings" "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) diff --git a/services/core/internal/execution/runtime_replacement_test.go b/services/core/internal/execution/runtime_replacement_test.go index e3b02677b..358d1713b 100644 --- a/services/core/internal/execution/runtime_replacement_test.go +++ b/services/core/internal/execution/runtime_replacement_test.go @@ -11,6 +11,8 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" @@ -347,7 +349,7 @@ func workspaceNodeSetup(t *testing.T, external bool) func(Owner, *deployment.Ser if err != nil { t.Fatal(err) } - node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "replacement node", Credential: strings.Repeat("n", 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: fixturePublicURL} + node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "replacement node", Credential: strings.Repeat("n", 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: fixturePublicURL} if _, err = service.Enroll(t.Context(), token.Token, node); err != nil { t.Fatal(err) } diff --git a/services/core/internal/execution/runtime_restore_attempt_test.go b/services/core/internal/execution/runtime_restore_attempt_test.go index 9b228727b..d0e42b359 100644 --- a/services/core/internal/execution/runtime_restore_attempt_test.go +++ b/services/core/internal/execution/runtime_restore_attempt_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/google/uuid" @@ -154,7 +156,7 @@ func TestCheckpointTransferRoutesBeforeTargetIO(t *testing.T) { t.Fatal(err) } targetNode := uuid.NewString() - node := deployment.Enrollment{NodeID: targetNode, Name: "checkpoint target", Credential: strings.Repeat("t", 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("c", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: fixturePublicURL} + node := machineapi.NodeEnrollment{NodeID: targetNode, Name: "checkpoint target", Credential: strings.Repeat("t", 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("c", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: fixturePublicURL} if _, err = r.deployments.Enroll(t.Context(), token.Token, node); err != nil { t.Fatal(err) } diff --git a/services/core/internal/nativeinstaller/bootstrap_recovery_test.go b/services/core/internal/nativeinstaller/bootstrap_recovery_test.go index 48394854e..bcc01c8d5 100644 --- a/services/core/internal/nativeinstaller/bootstrap_recovery_test.go +++ b/services/core/internal/nativeinstaller/bootstrap_recovery_test.go @@ -17,7 +17,7 @@ import ( "testing" "time" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" ) func bootstrapFixtureArchive(t *testing.T) []byte { @@ -53,7 +53,7 @@ func bootstrapCommand(t *testing.T, base, home string) *exec.Cmd { } else { command = exec.Command("bash", "assets/bootstrap.sh", base) } - command.Env = append(os.Environ(), "OAC_RUNTIME_HOME="+home, v1.NativeInstallGrantEnv+"=fixture-grant", "NO_PROXY=127.0.0.1", "no_proxy=127.0.0.1") + command.Env = append(os.Environ(), "OAC_RUNTIME_HOME="+home, machineapi.InstallGrantEnv+"=fixture-grant", "NO_PROXY=127.0.0.1", "no_proxy=127.0.0.1") return command } diff --git a/services/core/internal/nativeinstaller/bootstrap_test.go b/services/core/internal/nativeinstaller/bootstrap_test.go index 38750346f..8df684244 100644 --- a/services/core/internal/nativeinstaller/bootstrap_test.go +++ b/services/core/internal/nativeinstaller/bootstrap_test.go @@ -17,7 +17,7 @@ import ( "strings" "testing" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" ) // Exercise the shipped script against two actual TLS origins. The downloaded @@ -89,7 +89,7 @@ func TestBootstrapDownloadsVerifiedPlatformAcrossHTTPSRedirect(t *testing.T) { defer core.Close() result := filepath.Join(t.TempDir(), "result") command := exec.Command("bash", "assets/bootstrap.sh", core.URL+"/api/v1/agent-daemon/install/build", "--harness", "codex") - command.Env = append(os.Environ(), "OAC_RUNTIME_HOME="+t.TempDir(), v1.NativeInstallGrantEnv+"=private-grant", "CURL_CA_BUNDLE="+ca, "OAC_BOOTSTRAP_TEST_RESULT="+result, "NO_PROXY=127.0.0.1", "no_proxy=127.0.0.1") + command.Env = append(os.Environ(), "OAC_RUNTIME_HOME="+t.TempDir(), machineapi.InstallGrantEnv+"=private-grant", "CURL_CA_BUNDLE="+ca, "OAC_BOOTSTRAP_TEST_RESULT="+result, "NO_PROXY=127.0.0.1", "no_proxy=127.0.0.1") output, err := command.CombinedOutput() if valid { if err != nil { diff --git a/services/core/internal/nativeinstaller/catalog.go b/services/core/internal/nativeinstaller/catalog.go index 00b3ed656..58a360a75 100644 --- a/services/core/internal/nativeinstaller/catalog.go +++ b/services/core/internal/nativeinstaller/catalog.go @@ -18,7 +18,8 @@ import ( "strings" "time" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -138,10 +139,10 @@ func (c *Catalog) Commands(installerBase, authorization string) map[string]strin // Hold the small bootstrap in memory so an interrupted fetch leaves no file. // Only execute a complete successful response; preserve interactive stdin. // The grant leaves the environment of the fetch and reaches only the bootstrap. - posix := `set -e; grant=$` + v1.NativeInstallGrantEnv + `; unset ` + v1.NativeInstallGrantEnv + `; script=; for attempt in 1 2 3; do if script=$(curl -fsS --connect-timeout 15 --max-time 60 --max-filesize 1048576 ` + shellQuote(base+"/bootstrap.sh") + `); then break; fi; [ "$attempt" -lt 3 ] || exit 1; sleep "$attempt"; done; ` + v1.NativeInstallGrantEnv + `=$grant bash -c "$script" -- "$@"` + posix := `set -e; grant=$` + machineapi.InstallGrantEnv + `; unset ` + machineapi.InstallGrantEnv + `; script=; for attempt in 1 2 3; do if script=$(curl -fsS --connect-timeout 15 --max-time 60 --max-filesize 1048576 ` + shellQuote(base+"/bootstrap.sh") + `); then break; fi; [ "$attempt" -lt 3 ] || exit 1; sleep "$attempt"; done; ` + machineapi.InstallGrantEnv + `=$grant bash -c "$script" -- "$@"` return map[string]string{ // The grant travels in the environment, never in an argument; the leading space keeps it out of shell history where the shell supports that. - "posix": " " + v1.NativeInstallGrantEnv + "=" + shellQuote(authorization) + " bash -c " + shellQuote(posix) + " -- " + shellQuote(base), - "powershell": "& { $env:" + v1.NativeInstallGrantEnv + "=" + psQuote(authorization) + "; try { $source=$null; for ($attempt=1; $attempt -le 3; $attempt++) { try { $source=(Invoke-WebRequest -UseBasicParsing " + psQuote(base+"/bootstrap.ps1") + " -TimeoutSec 60 -ErrorAction Stop).Content; break } catch { if ($attempt -eq 3) { throw }; Start-Sleep -Seconds $attempt } }; & ([scriptblock]::Create($source)) -Base " + psQuote(base) + " @args } finally { Remove-Item Env:" + v1.NativeInstallGrantEnv + " -ErrorAction SilentlyContinue } }", + "posix": " " + machineapi.InstallGrantEnv + "=" + shellQuote(authorization) + " bash -c " + shellQuote(posix) + " -- " + shellQuote(base), + "powershell": "& { $env:" + machineapi.InstallGrantEnv + "=" + psQuote(authorization) + "; try { $source=$null; for ($attempt=1; $attempt -le 3; $attempt++) { try { $source=(Invoke-WebRequest -UseBasicParsing " + psQuote(base+"/bootstrap.ps1") + " -TimeoutSec 60 -ErrorAction Stop).Content; break } catch { if ($attempt -eq 3) { throw }; Start-Sleep -Seconds $attempt } }; & ([scriptblock]::Create($source)) -Base " + psQuote(base) + " @args } finally { Remove-Item Env:" + machineapi.InstallGrantEnv + " -ErrorAction SilentlyContinue } }", } } diff --git a/services/core/internal/persistence/postgres/deploymentpg/checkpoint_transfer_test.go b/services/core/internal/persistence/postgres/deploymentpg/checkpoint_transfer_test.go index 2bead77c1..b61ed0a35 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/checkpoint_transfer_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/checkpoint_transfer_test.go @@ -3,11 +3,13 @@ package deploymentpg_test import ( "encoding/json" "errors" - "github.com/google/uuid" "sync" "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/google/uuid" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" @@ -16,7 +18,7 @@ import ( var checkpointClass = sandbox.CheckpointCompatibility{ArtifactDomain: "fixture-archive", ExecutionClass: "fixture-machine"} -func readyCheckpointNode(t *testing.T, f fixture, node deployment.Enrollment, view deployment.View, compatibility sandbox.CheckpointCompatibility) { +func readyCheckpointNode(t *testing.T, f fixture, node machineapi.NodeEnrollment, view deployment.View, compatibility sandbox.CheckpointCompatibility) { t.Helper() connection := uuid.NewString() epoch, err := f.adapter.OwnerEpoch(t.Context()) @@ -31,7 +33,7 @@ func readyCheckpointNode(t *testing.T, f fixture, node deployment.Enrollment, vi } } -func suspendedCheckpointOwner(t *testing.T, f fixture, changes *deployment.ExecutionOperations, installation string, node deployment.Enrollment, view deployment.View) deployment.Allocation { +func suspendedCheckpointOwner(t *testing.T, f fixture, changes *deployment.ExecutionOperations, installation string, node machineapi.NodeEnrollment, view deployment.View) deployment.Allocation { t.Helper() owner := runningPressureOwner(t, f, changes, installation, node.NodeID, view.Generation) if _, err := f.pool.Exec(t.Context(), `UPDATE runtime_allocations SET compute_phase='suspended',compute_state='{"snapshot":{"ID":"snapshot","Compatibility":{"artifact_domain":"fixture-archive","execution_class":"fixture-machine"}}}',compute_retained_until=clock_timestamp()+interval '24 hours',compute_wake_requested=true WHERE id=$1`, owner.ID); err != nil { diff --git a/services/core/internal/persistence/postgres/deploymentpg/fixture_test.go b/services/core/internal/persistence/postgres/deploymentpg/fixture_test.go index 1d6def668..9237131a6 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/fixture_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/fixture_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/credentialcrypto" @@ -117,13 +119,13 @@ func (f fixture) initialize(t *testing.T, changes *deployment.ExecutionOperation // enroll issues an enrollment token and enrolls a node on the current // generation with it. -func (f fixture) enroll(t *testing.T, view deployment.View, capacity deployment.Capacity) deployment.Enrollment { +func (f fixture) enroll(t *testing.T, view deployment.View, capacity deployment.Capacity) machineapi.NodeEnrollment { t.Helper() token, err := f.service.CreateEnrollment(admin(t), deployment.EnrollmentLimits{MaxActive: &capacity.MaxActive, MaxRetained: &capacity.MaxRetained}) if err != nil { t.Fatal(err) } - input := deployment.Enrollment{NodeID: uuid.NewString(), Name: "fixture node", Credential: strings.Repeat("n", 64), Provider: view.Provider, + input := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "fixture node", Credential: strings.Repeat("n", 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: fixturePublicURL} if _, err := f.service.Enroll(t.Context(), token.Token, input); err != nil { t.Fatal(err) diff --git a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go index 09df492d9..a41edffb0 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -30,8 +32,8 @@ func nodesTokenDigest(token string) string { } // nodesEnrollment is a valid enrollment of a new node on view. -func nodesEnrollment(view deployment.View, name, credential string) deployment.Enrollment { - return deployment.Enrollment{NodeID: uuid.NewString(), Name: name, Credential: strings.Repeat(credential, 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("b", 64), +func nodesEnrollment(view deployment.View, name, credential string) machineapi.NodeEnrollment { + return machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: name, Credential: strings.Repeat(credential, 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("b", 64), SpecificationDigest: view.SpecificationDigest, DeploymentGeneration: view.Generation, CoreURL: fixturePublicURL} } @@ -78,12 +80,12 @@ func TestEnrollmentDerivesUnsetLimitsFromHost(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) - host := func(input deployment.Enrollment) deployment.Enrollment { + host := func(input machineapi.NodeEnrollment) machineapi.NodeEnrollment { cpus, memory, disk := int64(64), int64(256<<30), int64(8<<40) input.CPUCount, input.TotalMemoryBytes, input.AvailableDiskBytes = &cpus, &memory, &disk return input } - enroll := func(limits deployment.EnrollmentLimits, input deployment.Enrollment) (deployment.NodeIdentity, error) { + enroll := func(limits deployment.EnrollmentLimits, input machineapi.NodeEnrollment) (deployment.NodeIdentity, error) { token, err := f.service.CreateEnrollment(t.Context(), limits) if err != nil { t.Fatal(err) @@ -141,7 +143,7 @@ func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { changes, _ := f.execution(t) unknown := strings.Repeat("u", 64) // Before initialization the deployment is unavailable. - early := deployment.Enrollment{NodeID: uuid.NewString(), Name: "early", Credential: strings.Repeat("e", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), + early := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "early", Credential: strings.Repeat("e", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), SpecificationDigest: strings.Repeat("d", 64), DeploymentGeneration: 1, CoreURL: fixturePublicURL} if _, err := f.service.Enroll(t.Context(), unknown, early); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("uninitialized deployment judged before the token", err) @@ -167,13 +169,13 @@ func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { } for _, tc := range []struct { name string - change func(*deployment.Enrollment) + change func(*machineapi.NodeEnrollment) want error }{ - {"provider", func(e *deployment.Enrollment) { e.Provider = "microsandbox" }, deployment.ErrInvalidInput}, - {"generation", func(e *deployment.Enrollment) { e.DeploymentGeneration++ }, deployment.ErrSpecificationMismatch}, - {"specification", func(e *deployment.Enrollment) { e.SpecificationDigest = strings.Repeat("0", 64) }, deployment.ErrSpecificationMismatch}, - {"address", func(e *deployment.Enrollment) { e.CoreURL = "https://other.example" }, deployment.ErrNodeAddressMismatch}, + {"provider", func(e *machineapi.NodeEnrollment) { e.Provider = "microsandbox" }, deployment.ErrInvalidInput}, + {"generation", func(e *machineapi.NodeEnrollment) { e.DeploymentGeneration++ }, deployment.ErrSpecificationMismatch}, + {"specification", func(e *machineapi.NodeEnrollment) { e.SpecificationDigest = strings.Repeat("0", 64) }, deployment.ErrSpecificationMismatch}, + {"address", func(e *machineapi.NodeEnrollment) { e.CoreURL = "https://other.example" }, deployment.ErrNodeAddressMismatch}, } { input := nodesEnrollment(view, "refused", "r") tc.change(&input) @@ -285,7 +287,7 @@ func TestEnrollmentReplay(t *testing.T) { } other := input other.Credential = strings.Repeat("o", 64) - for _, candidate := range []deployment.Enrollment{input, other} { + for _, candidate := range []machineapi.NodeEnrollment{input, other} { if _, err := f.service.Enroll(t.Context(), second.Token, candidate); !errors.Is(err, deployment.ErrNodeExists) { t.Fatal("enrolled node ID reused", err) } diff --git a/services/core/internal/persistence/postgres/deploymentpg/specification_test.go b/services/core/internal/persistence/postgres/deploymentpg/specification_test.go index 101753e7e..d6f62cbc7 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/specification_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/specification_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" @@ -80,7 +82,7 @@ func TestEnrollmentRecordsItsIDAndRefusesAnotherAddress(t *testing.T) { if err != nil || uuid.Validate(issued.ID) != nil || issued.Token == "" { t.Fatal(issued.ID, err) } - input := deployment.Enrollment{NodeID: uuid.NewString(), Name: "addressed", Credential: strings.Repeat("a", 64), Provider: "docker", + input := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "addressed", Credential: strings.Repeat("a", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: "https://other.example"} if _, err := f.service.Enroll(t.Context(), issued.Token, input); !errors.Is(err, deployment.ErrNodeAddressMismatch) { t.Fatal("enrolled a node that uses another Core address", err) diff --git a/services/core/internal/runtimeenrollment/connection.go b/services/core/internal/runtimeenrollment/connection.go index bd5b13e6f..8377e9cd0 100644 --- a/services/core/internal/runtimeenrollment/connection.go +++ b/services/core/internal/runtimeenrollment/connection.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" @@ -55,15 +57,12 @@ func ConnectionHandler(s ConnectionStore, registry *runtimegateway.Registry) htt case err != nil: fail(http.StatusServiceUnavailable) default: - status := "disconnected" + status := machineapi.ConnectionDisconnected if connected { - status = "connected" + status = machineapi.ConnectionConnected } w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(struct { - EnvironmentID string `json:"environment_id"` - Status string `json:"status"` - }{environment, status}) + _ = json.NewEncoder(w).Encode(machineapi.Connection{EnvironmentID: environment, Status: status}) } }) } diff --git a/services/core/internal/runtimeenrollment/enrollment.go b/services/core/internal/runtimeenrollment/enrollment.go index 2a2ff3ce6..7febd2cbb 100644 --- a/services/core/internal/runtimeenrollment/enrollment.go +++ b/services/core/internal/runtimeenrollment/enrollment.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -32,9 +34,7 @@ func EnrollmentHandler(s EnrollmentStore) http.Handler { fail(http.StatusUnauthorized) return } - var input struct { - EnvironmentID string `json:"environment_id"` - } + var input machineapi.EnrollRequest decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)) decoder.DisallowUnknownFields() if len(r.URL.Query()) != 0 || decoder.Decode(&input) != nil || input.EnvironmentID == "" || decoder.Decode(new(any)) != io.EOF { @@ -54,12 +54,7 @@ func EnrollmentHandler(s EnrollmentStore) http.Handler { default: w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") - _ = json.NewEncoder(w).Encode(struct { - DeviceID string `json:"device_id"` - SessionID string `json:"session_id"` - EnvironmentID string `json:"environment_id"` - WorkspaceDirectory string `json:"workspace_directory"` - }{binding.DeviceID, binding.SessionID, binding.EnvironmentID, binding.WorkspaceDirectory}) + _ = json.NewEncoder(w).Encode(machineapi.Enrollment{DeviceID: binding.DeviceID, SessionID: binding.SessionID, EnvironmentID: binding.EnvironmentID, WorkspaceDirectory: binding.WorkspaceDirectory}) } }) } diff --git a/services/core/internal/runtimegateway/handler.go b/services/core/internal/runtimegateway/handler.go index 8f5ff6822..d02c16dce 100644 --- a/services/core/internal/runtimegateway/handler.go +++ b/services/core/internal/runtimegateway/handler.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/gorilla/websocket" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -155,9 +157,7 @@ func (h *Handler) Bootstrap(w http.ResponseWriter, r *http.Request) { writeAuthError(w, http.StatusUnauthorized, "missing_bearer", "Authorization: Bearer required") return } - var body struct { - DeviceID string `json:"device_id"` - } + var body machineapi.BootstrapRequest if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeAuthError(w, http.StatusBadRequest, "bad_json", err.Error()) return @@ -174,11 +174,7 @@ func (h *Handler) Bootstrap(w http.ResponseWriter, r *http.Request) { writeAuthError(w, status, code, err.Error()) return } - resp := map[string]any{ - "device_id": auth.DeviceID, - "ws_url": h.cfg.PublicWSURL, - "heartbeat_seconds": int(h.cfg.HeartbeatInterval.Seconds()), - } + resp := machineapi.BootstrapResponse{DeviceID: auth.DeviceID, WSURL: h.cfg.PublicWSURL, HeartbeatSeconds: int(h.cfg.HeartbeatInterval.Seconds())} w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(resp) @@ -202,10 +198,7 @@ func bearerFromAuthHeader(r *http.Request) string { func writeAuthError(w http.ResponseWriter, status int, code, detail string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(map[string]any{ - "error": code, - "detail": detail, - }) + _ = json.NewEncoder(w).Encode(machineapi.AuthError{Error: code, Detail: detail}) } // mapAuthError translates a typed auth error into (HTTP status, diff --git a/services/core/internal/sandbox/node/capacity_test.go b/services/core/internal/sandbox/node/capacity_test.go index 3a3aee5b2..5cbad7079 100644 --- a/services/core/internal/sandbox/node/capacity_test.go +++ b/services/core/internal/sandbox/node/capacity_test.go @@ -8,13 +8,15 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "sync" ) func TestCapacityRefreshUsesAuthenticatedCoreIdentity(t *testing.T) { id := identity() var stored StoredIdentity - approved := EnrollmentResponse{SpecificationDigest: id.SpecificationDigest, DeploymentGeneration: id.DeploymentGeneration, NodeID: id.NodeID, InstallationID: id.InstallationID, Provider: id.Provider, MaxActive: 2, MaxRetained: 8} + approved := machineapi.NodeStatus{NodeIdentity: machineapi.NodeIdentity{SpecificationDigest: id.SpecificationDigest, DeploymentGeneration: id.DeploymentGeneration, NodeID: id.NodeID, InstallationID: id.InstallationID, Provider: id.Provider, MaxActive: 2, MaxRetained: 8}} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/sandbox-node/identity" || r.URL.Query().Get("node_id") != id.NodeID || r.Header.Get("Authorization") != "Bearer "+stored.Credential { t.Error("identity refresh lacked retained authority") diff --git a/services/core/internal/sandbox/node/enrollment.go b/services/core/internal/sandbox/node/enrollment.go index 7b5a98acd..b0d8594c6 100644 --- a/services/core/internal/sandbox/node/enrollment.go +++ b/services/core/internal/sandbox/node/enrollment.go @@ -12,11 +12,13 @@ import ( "regexp" "strconv" "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" ) // Enroll consumes a short-lived enrollment token. InitIdentity must have been // called with the local provider identity before invoking this function. -func Enroll(ctx context.Context, coreURL, dir, token string, input EnrollmentRequest) (StoredIdentity, error) { +func Enroll(ctx context.Context, coreURL, dir, token string, input machineapi.NodeEnrollment) (StoredIdentity, error) { release, err := lockDirectory(dir) if err != nil { return StoredIdentity{}, err @@ -77,9 +79,9 @@ func Enroll(ctx context.Context, coreURL, dir, token string, input EnrollmentReq } return retainEnrollment(dir, stored, out) } -func readEnrollment(r *http.Response) (EnrollmentResponse, error) { +func readEnrollment(r *http.Response) (machineapi.NodeStatus, error) { defer r.Body.Close() - var out EnrollmentResponse + var out machineapi.NodeStatus if r.StatusCode < 200 || r.StatusCode >= 300 { return out, enrollmentRejection(r) } @@ -90,7 +92,7 @@ func readEnrollment(r *http.Response) (EnrollmentResponse, error) { } return out, nil } -func verifyEnrollment(s StoredIdentity, out EnrollmentResponse) (StoredIdentity, error) { +func verifyEnrollment(s StoredIdentity, out machineapi.NodeStatus) (StoredIdentity, error) { if out.SpecificationDigest != s.Identity.SpecificationDigest || out.DeploymentGeneration != s.Identity.DeploymentGeneration || out.NodeID != s.Identity.NodeID || out.InstallationID != s.Identity.InstallationID || out.Provider != s.Identity.Provider { return StoredIdentity{}, errors.New("enrolled node identity mismatch") } @@ -101,7 +103,7 @@ func verifyEnrollment(s StoredIdentity, out EnrollmentResponse) (StoredIdentity, return s, nil } -func retainEnrollment(dir string, stored StoredIdentity, out EnrollmentResponse) (StoredIdentity, error) { +func retainEnrollment(dir string, stored StoredIdentity, out machineapi.NodeStatus) (StoredIdentity, error) { verified, err := verifyEnrollment(stored, out) if err != nil { return StoredIdentity{}, err diff --git a/services/core/internal/sandbox/node/node_test.go b/services/core/internal/sandbox/node/node_test.go index 6cd59ea97..113447028 100644 --- a/services/core/internal/sandbox/node/node_test.go +++ b/services/core/internal/sandbox/node/node_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" @@ -234,14 +236,14 @@ func TestEnrollmentLostResponseRecoversWithPersistedCredential(t *testing.T) { w.WriteHeader(http.StatusUnauthorized) return } - _ = json.NewEncoder(w).Encode(EnrollmentResponse{SpecificationDigest: id.SpecificationDigest, DeploymentGeneration: id.DeploymentGeneration, NodeID: id.NodeID, InstallationID: id.InstallationID, Provider: id.Provider, MaxActive: 2, MaxRetained: 8}) + _ = json.NewEncoder(w).Encode(machineapi.NodeStatus{NodeIdentity: machineapi.NodeIdentity{SpecificationDigest: id.SpecificationDigest, DeploymentGeneration: id.DeploymentGeneration, NodeID: id.NodeID, InstallationID: id.InstallationID, Provider: id.Provider, MaxActive: 2, MaxRetained: 8}}) return } if r.URL.Path != "/api/v1/sandbox-node/enroll" || r.Header.Get("Authorization") != "Bearer enrollment" { w.WriteHeader(http.StatusUnauthorized) return } - var body EnrollmentRequest + var body machineapi.NodeEnrollment if json.NewDecoder(r.Body).Decode(&body) != nil || body.Credential != stored.Credential || body.NodeID != id.NodeID { t.Error("unpersisted credential used") } @@ -266,10 +268,10 @@ func TestEnrollmentLostResponseRecoversWithPersistedCredential(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = Enroll(context.Background(), server.URL, dir, "enrollment", EnrollmentRequest{Name: "test"}); err == nil { + if _, err = Enroll(context.Background(), server.URL, dir, "enrollment", machineapi.NodeEnrollment{Name: "test"}); err == nil { t.Fatal("lost response reported success") } - recovered, err := Enroll(context.Background(), server.URL, dir, "enrollment", EnrollmentRequest{Name: "test"}) + recovered, err := Enroll(context.Background(), server.URL, dir, "enrollment", machineapi.NodeEnrollment{Name: "test"}) if err != nil { t.Fatal(err) } @@ -280,7 +282,7 @@ func TestEnrollmentLostResponseRecoversWithPersistedCredential(t *testing.T) { if retry, err := InitIdentity(dir, server.URL, id); err != nil || retry != recovered { t.Fatal("register retry treated absent local capacity as an override", err) } - if _, err := Enroll(t.Context(), server.URL, dir, "consumed", EnrollmentRequest{Name: "test"}); err != nil || enrollments != 1 { + if _, err := Enroll(t.Context(), server.URL, dir, "consumed", machineapi.NodeEnrollment{Name: "test"}); err != nil || enrollments != 1 { t.Fatal("register retry failed to recover approved identity", err) } } diff --git a/services/core/internal/sandbox/node/wire.go b/services/core/internal/sandbox/node/wire.go index 5a48fde61..c12b3cda0 100644 --- a/services/core/internal/sandbox/node/wire.go +++ b/services/core/internal/sandbox/node/wire.go @@ -7,10 +7,11 @@ import ( "context" "encoding/json" "errors" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "io" "time" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/workspacefs" @@ -50,35 +51,6 @@ type Health struct { AvailableDiskBytes *int64 `json:"available_disk_bytes,omitempty"` } -type EnrollmentRequest struct { - SpecificationDigest string `json:"specification_digest"` - DeploymentGeneration uint64 `json:"deployment_generation"` - NodeID string `json:"node_id"` - Credential string `json:"credential"` - Name string `json:"name"` - Provider string `json:"provider"` - BackendFingerprint string `json:"backend_fingerprint"` - // The retained Core origin; Core refuses an enrollment whose address is not its public URL. - CoreURL string `json:"core_url"` - // The host measurements of Health. Core derives from them each limit the enrollment token leaves unset. - CPUCount *int64 `json:"cpu_count,omitempty"` - EffectiveCPUCores *float64 `json:"effective_cpu_cores,omitempty"` - TotalMemoryBytes *int64 `json:"total_memory_bytes,omitempty"` - AvailableDiskBytes *int64 `json:"available_disk_bytes,omitempty"` -} - -type EnrollmentResponse struct { - MaxActive int `json:"max_active"` - MaxRetained int `json:"max_retained"` - SpecificationDigest string `json:"specification_digest"` - DeploymentGeneration uint64 `json:"deployment_generation"` - Connected bool `json:"connected,omitempty"` - ProviderReady bool `json:"provider_ready,omitempty"` - NodeID string `json:"node_id"` - InstallationID string `json:"installation_id"` - Provider string `json:"provider"` -} - type request struct { DeploymentGeneration uint64 `json:"deployment_generation,omitempty"` ID string `json:"id"` diff --git a/services/core/tests/integration/admin_session_archive_race_test.go b/services/core/tests/integration/admin_session_archive_race_test.go index d932eee93..d8244af31 100644 --- a/services/core/tests/integration/admin_session_archive_race_test.go +++ b/services/core/tests/integration/admin_session_archive_race_test.go @@ -6,6 +6,8 @@ import ( "sync" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" @@ -30,7 +32,7 @@ func TestManagedSessionArchiveReleasesPendingNodePlacement(t *testing.T) { t.Fatal(err) } nodeID := uuid.NewString() - if _, err := nodes.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Archive fixture", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { + if _, err := nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Archive fixture", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { t.Fatal(err) } onlineManagerNode(t, s, nodeID) diff --git a/services/core/tests/integration/credential_matrix_http_test.go b/services/core/tests/integration/credential_matrix_http_test.go index 61d4ef3d6..4ee0c36f0 100644 --- a/services/core/tests/integration/credential_matrix_http_test.go +++ b/services/core/tests/integration/credential_matrix_http_test.go @@ -10,6 +10,8 @@ import ( "sync" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" @@ -115,7 +117,7 @@ func TestCredentialNamespaceMatrix(t *testing.T) { var enrollment api.SandboxEnrollmentToken created("POST", "/core/v1/sandbox/enrollment-tokens", coreKey, `{"max_active":2,"max_retained":2}`, &enrollment) nodeID, nodeCredential := uuid.NewString(), strings.Repeat("n", 64) - enroll, _ := json.Marshal(deployment.Enrollment{NodeID: nodeID, Credential: nodeCredential, Name: "Matrix node", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), + enroll, _ := json.Marshal(machineapi.NodeEnrollment{NodeID: nodeID, Credential: nodeCredential, Name: "Matrix node", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: specification.Digest("docker"), CoreURL: "https://core.example"}) var node deployment.NodeIdentity created("POST", "/api/v1/sandbox-node/enroll", enrollment.Token, string(enroll), &node) diff --git a/services/core/tests/integration/device_bootstrap_binding_test.go b/services/core/tests/integration/device_bootstrap_binding_test.go index e0295490b..153b083b8 100644 --- a/services/core/tests/integration/device_bootstrap_binding_test.go +++ b/services/core/tests/integration/device_bootstrap_binding_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" @@ -19,7 +21,7 @@ func TestDeviceCredentialCarriesPersistedAllocationNode(t *testing.T) { t.Fatal(err) } remote := uuid.NewString() - _, err = nodes.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: remote, Credential: strings.Repeat("x", 64), + _, err = nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: remote, Credential: strings.Repeat("x", 64), Name: "remote", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) if err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/runtime_lifecycle_nodes_test.go b/services/core/tests/integration/runtime_lifecycle_nodes_test.go index e8ecfa47c..0bdb9326e 100644 --- a/services/core/tests/integration/runtime_lifecycle_nodes_test.go +++ b/services/core/tests/integration/runtime_lifecycle_nodes_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" @@ -21,7 +23,7 @@ func lifecycleTestNode(t *testing.T, s *Store) string { t.Fatal(err) } id := uuid.NewString() - _, err = nodes.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: id, Credential: strings.Repeat("n", 64), Name: "second", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) + _, err = nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: id, Credential: strings.Repeat("n", 64), Name: "second", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_node_generations_test.go b/services/core/tests/integration/runtime_node_generations_test.go index 9a8a5e86a..73af2fa97 100644 --- a/services/core/tests/integration/runtime_node_generations_test.go +++ b/services/core/tests/integration/runtime_node_generations_test.go @@ -4,6 +4,8 @@ import ( "errors" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" @@ -27,7 +29,7 @@ func changeNodeTarget(t *testing.T, w *Store, view deployment.View, input sandbo return next, input } -func generationHeartbeat(t *testing.T, s *Store, node deployment.Enrollment, connection string, view deployment.View, state string) { +func generationHeartbeat(t *testing.T, s *Store, node machineapi.NodeEnrollment, connection string, view deployment.View, state string) { t.Helper() status := sandbox.GenerationStatus{Generation: view.Generation, SpecificationDigest: view.SpecificationDigest, State: state} if state == "ready" && view.Provider == "microsandbox" { diff --git a/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go b/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go index 1444fbc12..1b46c0207 100644 --- a/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go +++ b/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/environmentconfig" @@ -138,7 +140,7 @@ func (f *nodeIsolationFixture) enroll(id string) { if err != nil { f.t.Fatal(err) } - _, err = f.nodes.Enroll(f.t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: id, Credential: strings.Repeat("x", 64), Name: id, Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: f.store.placement.PublicURL()}) + _, err = f.nodes.Enroll(f.t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: id, Credential: strings.Repeat("x", 64), Name: id, Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: f.store.placement.PublicURL()}) if err != nil { f.t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_nodes_test.go b/services/core/tests/integration/runtime_nodes_test.go index ad231d148..3dbdce6a2 100644 --- a/services/core/tests/integration/runtime_nodes_test.go +++ b/services/core/tests/integration/runtime_nodes_test.go @@ -8,6 +8,8 @@ import ( "sync" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" @@ -233,7 +235,7 @@ func TestRuntimeNodesEnrollmentAndEpoch(t *testing.T) { if err != nil { t.Fatal(err) } - input := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Credential: strings.Repeat("x", 64), Name: "remote", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} + input := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Credential: strings.Repeat("x", 64), Name: "remote", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, input); !errors.Is(err, deployment.ErrInvalidInput) { t.Fatal("mixed provider accepted", err) } diff --git a/services/core/tests/integration/sandbox_deployment_switch_test.go b/services/core/tests/integration/sandbox_deployment_switch_test.go index 00216b66f..5beeb5598 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_test.go @@ -10,6 +10,8 @@ import ( "sync" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" @@ -159,7 +161,7 @@ func TestSandboxSwitchRetiresNodesAndEnrollment(t *testing.T) { if err != nil { t.Fatal(err) } - node := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Name: "Machine", Provider: "docker", Credential: strings.Repeat("c", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Name: "Machine", Provider: "docker", Credential: strings.Repeat("c", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, node); err != nil { t.Fatal(err) } @@ -359,7 +361,7 @@ func TestUnspecifiedNodeDeploymentRejectedWithoutMutation(t *testing.T) { if err != nil { t.Fatal(err) } - node := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: spec.Digest("docker"), NodeID: uuid.NewString(), Name: "Legacy", Provider: "docker", Credential: strings.Repeat("l", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: spec.Digest("docker"), NodeID: uuid.NewString(), Name: "Legacy", Provider: "docker", Credential: strings.Repeat("l", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, node); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_switch_worker_test.go b/services/core/tests/integration/sandbox_deployment_switch_worker_test.go index bb4e39edd..c219cb02a 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_worker_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_worker_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" @@ -71,7 +73,7 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { if err != nil { t.Fatal(err) } - node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "retained candidate fixture", Provider: "docker", Credential: strings.Repeat("n", 64), BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "retained candidate fixture", Provider: "docker", Credential: strings.Repeat("n", 64), BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), CoreURL: s.placement.PublicURL()} if _, err := deployments.Enroll(t.Context(), enrollment, node); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_worker_test.go b/services/core/tests/integration/sandbox_deployment_worker_test.go index e67932b5f..dafe4e91e 100644 --- a/services/core/tests/integration/sandbox_deployment_worker_test.go +++ b/services/core/tests/integration/sandbox_deployment_worker_test.go @@ -8,6 +8,8 @@ import ( "sync" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" @@ -62,7 +64,7 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { t.Fatal(err) } nodeID := uuid.NewString() - if _, err := deployments.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Remote", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { + if _, err := deployments.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Remote", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { t.Fatal(err) } connect := func() { diff --git a/services/core/tests/integration/sandbox_node_auth_order_http_test.go b/services/core/tests/integration/sandbox_node_auth_order_http_test.go index 338390099..3ec1663f5 100644 --- a/services/core/tests/integration/sandbox_node_auth_order_http_test.go +++ b/services/core/tests/integration/sandbox_node_auth_order_http_test.go @@ -7,8 +7,9 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/api" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" ) @@ -36,7 +37,7 @@ func TestSandboxNodeRoutesAuthenticateBeforeDeploymentState(t *testing.T) { token, claimedToken, claimed := strings.Repeat("e", 64), strings.Repeat("c", 64), uuid.NewString() enrollment(token, uuid.NewString()) enrollment(claimedToken, claimed) - enroll, _ := json.Marshal(deployment.Enrollment{NodeID: uuid.NewString(), Credential: strings.Repeat("n", 64), Name: "Early node", Provider: "docker", + enroll, _ := json.Marshal(machineapi.NodeEnrollment{NodeID: uuid.NewString(), Credential: strings.Repeat("n", 64), Name: "Early node", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: strings.Repeat("d", 64), CoreURL: "https://core.example"}) nodeID := uuid.NewString() type check struct { diff --git a/services/core/tests/integration/sandbox_specification_lifecycle_test.go b/services/core/tests/integration/sandbox_specification_lifecycle_test.go index a157d6004..68ea7f21a 100644 --- a/services/core/tests/integration/sandbox_specification_lifecycle_test.go +++ b/services/core/tests/integration/sandbox_specification_lifecycle_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment/placement" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" @@ -39,20 +41,20 @@ func webSpecificationFixture(t *testing.T, provider string) (*Store, *Store, dep return s, w, view, input } -func specificationNode(t *testing.T, s *Store, view deployment.View) deployment.Enrollment { +func specificationNode(t *testing.T, s *Store, view deployment.View) machineapi.NodeEnrollment { t.Helper() return enrollNode(t, s, view, deployment.Capacity{MaxActive: 4, MaxRetained: 16}) } // enrollNode enrolls an online node with capacity on the committed setup view. -func enrollNode(t *testing.T, s *Store, view deployment.View, capacity deployment.Capacity) deployment.Enrollment { +func enrollNode(t *testing.T, s *Store, view deployment.View, capacity deployment.Capacity) machineapi.NodeEnrollment { t.Helper() nodes := deploymentService(t, s) token, err := EnrollmentTestToken(nodes.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: &capacity.MaxActive, MaxRetained: &capacity.MaxRetained})) if err != nil { t.Fatal(err) } - input := deployment.Enrollment{NodeID: uuid.NewString(), Name: "specification fixture", Credential: strings.Repeat("n", 64), Provider: view.Provider, + input := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "specification fixture", Credential: strings.Repeat("n", 64), Provider: view.Provider, BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, input); err != nil { t.Fatal(err) @@ -77,10 +79,10 @@ func TestSandboxSpecificationBootstrapReadDoesNotConsumeEnrollment(t *testing.T) if _, err := nodes.NodeConfiguration(t.Context(), "", "invalid-token", 0); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("unauthenticated configuration read", err) } - node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "bootstrap", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: s.placement.PublicURL()} - for _, change := range []func(*deployment.Enrollment){ - func(n *deployment.Enrollment) { n.DeploymentGeneration++ }, - func(n *deployment.Enrollment) { n.SpecificationDigest = strings.Repeat("c", 64) }, + node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "bootstrap", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: s.placement.PublicURL()} + for _, change := range []func(*machineapi.NodeEnrollment){ + func(n *machineapi.NodeEnrollment) { n.DeploymentGeneration++ }, + func(n *machineapi.NodeEnrollment) { n.SpecificationDigest = strings.Repeat("c", 64) }, } { wrong := node change(&wrong) From bc35d802ac413c966bec8b0d382c6dd01dcfe9bf Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 19:22:05 +0000 Subject: [PATCH 2/2] Include the machine API package in the Core source set --- scripts/build-core.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-core.sh b/scripts/build-core.sh index 78e6a2326..2943aed1e 100755 --- a/scripts/build-core.sh +++ b/scripts/build-core.sh @@ -27,7 +27,7 @@ tar -C "$repo_root" -cf - \ contracts/agents-api/v1 \ contracts/agents-api/openapi.go contracts/agents-api/openapi.yaml contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml \ internal/agentdaemon/proto \ - internal/runtimefs internal/runtimebootstrap internal/agentbundle internal/agentcapabilities internal/agentplugin internal/agentskill internal/harnessconfig internal/modelprovider internal/providerassets internal/obs/log services/core \ + internal/runtimefs internal/runtimebootstrap internal/machineapi internal/agentbundle internal/agentcapabilities internal/agentplugin internal/agentskill internal/harnessconfig internal/modelprovider internal/providerassets internal/obs/log services/core \ | tar -C "$build_context" -xf - (