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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"; \
Expand Down
6 changes: 4 additions & 2 deletions apps/daemon/internal/cli/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions apps/daemon/internal/cli/connect_cleanup_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package cli

import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig"

import (
"context"
"errors"
"net/http"
"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) {
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 10 additions & 21 deletions apps/daemon/internal/cli/connect_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions apps/daemon/internal/cli/connect_environment_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"`
}
Expand All @@ -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")
Expand Down
6 changes: 4 additions & 2 deletions apps/daemon/internal/cli/connect_environment_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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) {
Expand Down
16 changes: 9 additions & 7 deletions apps/daemon/internal/cli/connect_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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) }))
Expand All @@ -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")
Expand Down
10 changes: 6 additions & 4 deletions apps/daemon/internal/cli/connect_suspend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading