From 84f1880ece76114032e29e71648273e5a079873a Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 22:42:34 +0000 Subject: [PATCH 1/5] Classify unprojectable Runtime frames as invalid executor results A Runtime frame that projects to no Item, such as a delta without an item_id, failed the Turn with event_persistence_failed. items.Project now wraps every failure in ErrInvalidObservation, and delivery reports a journal write that fails with it as invalid_executor_result at every flush. Storage failures stay event_persistence_failed. --- services/core/internal/execution/delivery.go | 14 +++++++------- services/core/internal/execution/finish.go | 4 ++-- services/core/internal/execution/journal.go | 12 ++++++++++++ services/core/internal/items/command_output.go | 3 ++- services/core/internal/items/messages.go | 14 +++++++++++++- .../tests/integration/execution_events_test.go | 15 +++++++++++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/services/core/internal/execution/delivery.go b/services/core/internal/execution/delivery.go index 27a1f0384..53b0b232b 100644 --- a/services/core/internal/execution/delivery.go +++ b/services/core/internal/execution/delivery.go @@ -79,7 +79,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe result.ErrorCode, status = "event_persistence_failed", sessions.TurnFailed } if err := journal.flush(finishCtx); err != nil { - result.ErrorCode, status = "event_persistence_failed", sessions.TurnFailed + result.ErrorCode, status = journalFailure(err), sessions.TurnFailed } if subscription.Err() != nil { result.ErrorCode, status = "event_stream_incomplete", sessions.TurnFailed @@ -166,15 +166,15 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe return } case <-flushTicker.C: - if journal.flush(ctx) != nil { - result.ErrorCode = "event_persistence_failed" + if err := journal.flush(ctx); err != nil { + result.ErrorCode = journalFailure(err) return } case reply := <-cancelReply: drainErr := journal.drain(upstream, &result) receiptErr := recordCancellation(ctx, journal, reply, &result) - if drainErr != nil || receiptErr != nil { - result.ErrorCode = "event_persistence_failed" + if err := errors.Join(drainErr, receiptErr); err != nil { + result.ErrorCode = journalFailure(err) return } if reply.err == nil && reply.ack.Applied { @@ -206,7 +206,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe return } if writeErr != nil { - result.ErrorCode = "event_persistence_failed" + result.ErrorCode = journalFailure(writeErr) return } switch env.Type { @@ -222,7 +222,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe done, upstream = true, nil case proto.TypeFunctionCall: if err := journal.flush(ctx); err != nil { - result.ErrorCode = "event_persistence_failed" + result.ErrorCode = journalFailure(err) return } if err := functions.record(ctx, env); err != nil { diff --git a/services/core/internal/execution/finish.go b/services/core/internal/execution/finish.go index 8521e241e..f1fffaf5d 100644 --- a/services/core/internal/execution/finish.go +++ b/services/core/internal/execution/finish.go @@ -10,8 +10,8 @@ func finishDelivery(ctx context.Context, journal *journal, result *Result, cance if cancelReply != nil { select { case reply := <-cancelReply: - if recordCancellation(ctx, journal, reply, result) != nil { - result.ErrorCode = "event_persistence_failed" + if err := recordCancellation(ctx, journal, reply, result); err != nil { + result.ErrorCode = journalFailure(err) return sessions.TurnFailed } if reply.err == nil && reply.ack.Applied { diff --git a/services/core/internal/execution/journal.go b/services/core/internal/execution/journal.go index cf3a15438..d4039ec89 100644 --- a/services/core/internal/execution/journal.go +++ b/services/core/internal/execution/journal.go @@ -3,9 +3,11 @@ package execution import ( "context" "encoding/json" + "errors" "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/items" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -23,6 +25,16 @@ type eventWriter interface { AppendTurnEvents(context.Context, string, string, string, int32, []sessions.ExecutionEvent) error } +// journalFailure is the Turn's error code for a failed journal write: an +// observation that projects to no Item is the Runtime's fault, and every other +// failure is storage's. +func journalFailure(err error) string { + if errors.Is(err, items.ErrInvalidObservation) { + return "invalid_executor_result" + } + return "event_persistence_failed" +} + func recordCancellation(ctx context.Context, journal *journal, reply cancellationResult, result *Result) error { if reply.err != nil { return nil diff --git a/services/core/internal/items/command_output.go b/services/core/internal/items/command_output.go index d10c7df9b..f9963b8b0 100644 --- a/services/core/internal/items/command_output.go +++ b/services/core/internal/items/command_output.go @@ -3,6 +3,7 @@ package items import ( "encoding/json" "errors" + "fmt" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -21,7 +22,7 @@ func projectCommandOutput(turn string, raw json.RawMessage) ([]Update, error) { func mergeCommandOutput(update Update, previous v1.Item) (v1.Item, error) { if previous.ID != update.Item.ID || previous.TurnID != update.Item.TurnID || previous.Type != "command_execution" { - return v1.Item{}, errors.New("command output requires its existing command") + return v1.Item{}, fmt.Errorf("%w: command output requires its existing command", ErrInvalidObservation) } if previous.Status != "in_progress" { return previous, nil diff --git a/services/core/internal/items/messages.go b/services/core/internal/items/messages.go index 11d041038..cc7cb794e 100644 --- a/services/core/internal/items/messages.go +++ b/services/core/internal/items/messages.go @@ -3,6 +3,7 @@ package items import ( "encoding/json" "errors" + "fmt" "slices" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" @@ -29,7 +30,18 @@ func message(turn, key, role, text, status string) v1.Item { Content: []v1.ItemContent{{Type: contentType, Text: &text}}} } -func Project(turn, kind string, sequence int64, raw json.RawMessage) ([]Update, error) { +// ErrInvalidObservation marks an execution observation that projects to no +// valid Item: the Runtime sent it, so it is never a storage failure. +var ErrInvalidObservation = errors.New("invalid execution observation") + +// Project returns the Item updates an observation of kind makes. Every error +// wraps ErrInvalidObservation. +func Project(turn, kind string, sequence int64, raw json.RawMessage) (updates []Update, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("%w: %w", ErrInvalidObservation, err) + } + }() switch kind { case "message": return inputMessages(turn, sequence, raw), nil diff --git a/services/core/tests/integration/execution_events_test.go b/services/core/tests/integration/execution_events_test.go index bf04e5072..7fd4676a5 100644 --- a/services/core/tests/integration/execution_events_test.go +++ b/services/core/tests/integration/execution_events_test.go @@ -102,3 +102,18 @@ func TestExecutionDoesNotCompleteAfterEventPersistenceFailure(t *testing.T) { t.Fatalf("events=%+v err=%v", events, err) } } + +func TestExecutionFailsUnprojectableRuntimeFrame(t *testing.T) { + h := newDispatchHarness(t) + ctx := context.Background() + input := h.message("start", "Malformed output") + result := h.run(ctx, input.TurnID) + h.read(testExecutionRequest) + h.write(input.TurnID, proto.TypeDelta, proto.DeltaPayload{Delta: "no item", Sequence: 1}) + turn := h.finished(result, sessions.TurnFailed) + var outcome execution.Result + _ = json.Unmarshal(turn.Outcome, &outcome) + if outcome.ErrorCode != "invalid_executor_result" { + t.Fatal(outcome.ErrorCode) + } +} From fca9ed2d33857d8761ba6bfded1ba83e93594b59 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 22:43:39 +0000 Subject: [PATCH 2/5] Delete the write-only model provider marker Session creation added model_provider_configured to the stored configuration whenever a provider bundle was frozen, and nothing reads it: the execution Snapshot field was set only by test fixtures. The marker, the field and the fixtures go, and the retry identity hashes the configuration as requested. --- .../internal/execution/disabled_tools_test.go | 2 +- .../core/internal/execution/dispatcher.go | 9 +++--- .../internal/execution/mcp_support_test.go | 2 +- .../execution/model_execution_test.go | 2 +- .../core/internal/execution/recovery_test.go | 2 +- .../execution/structured_output_test.go | 2 +- .../postgres/sessionpg/creation_test.go | 2 +- services/core/internal/sessions/creation.go | 28 +++++-------------- .../core/internal/sessions/creation_test.go | 2 +- .../session_execution_configuration_test.go | 2 +- 10 files changed, 19 insertions(+), 34 deletions(-) diff --git a/services/core/internal/execution/disabled_tools_test.go b/services/core/internal/execution/disabled_tools_test.go index 4f2147acd..1d2187900 100644 --- a/services/core/internal/execution/disabled_tools_test.go +++ b/services/core/internal/execution/disabled_tools_test.go @@ -20,7 +20,7 @@ func TestDisabledToolsAreCommonControls(t *testing.T) { func TestDisabledToolRequestPreservesIntentOnResume(t *testing.T) { for _, disabled := range []bool{false, true} { - snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model"}} + snapshot := Snapshot{Agent: v1.Agent{Model: "model"}} if disabled { snapshot.Agent.Tools = []json.RawMessage{json.RawMessage(`{"type":"programmatic_tool_calling","enabled":false}`)} } diff --git a/services/core/internal/execution/dispatcher.go b/services/core/internal/execution/dispatcher.go index deedc7313..c9b28e3fa 100644 --- a/services/core/internal/execution/dispatcher.go +++ b/services/core/internal/execution/dispatcher.go @@ -20,11 +20,10 @@ import ( // Snapshot is the Session configuration frozen at creation. type Snapshot struct { - ModelProviderConfigured bool `json:"model_provider_configured,omitempty"` - Agent v1.Agent `json:"agent"` - Environment *v1.Environment `json:"environment"` - VaultIDs []string `json:"vault_ids,omitempty"` - MCPCredentials []vaults.MCPCredentialBinding `json:"mcp_credentials,omitempty"` + Agent v1.Agent `json:"agent"` + Environment *v1.Environment `json:"environment"` + VaultIDs []string `json:"vault_ids,omitempty"` + MCPCredentials []vaults.MCPCredentialBinding `json:"mcp_credentials,omitempty"` } type Dispatcher struct { diff --git a/services/core/internal/execution/mcp_support_test.go b/services/core/internal/execution/mcp_support_test.go index 314fba931..fbd42db23 100644 --- a/services/core/internal/execution/mcp_support_test.go +++ b/services/core/internal/execution/mcp_support_test.go @@ -28,7 +28,7 @@ func mcpSupportFixture(t *testing.T) Snapshot { t.Helper() vault, credential := uuid.NewString(), uuid.NewString() tool := json.RawMessage(`{"type":"mcp","server_label":"tickets","connection_origin":"service","transport":{"type":"http","server_url":"https://mcp.example/tools"}}`) - snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model", Tools: []json.RawMessage{tool}}, Environment: &v1.Environment{Type: "none"}, VaultIDs: []string{vault}, + snapshot := Snapshot{Agent: v1.Agent{Model: "model", Tools: []json.RawMessage{tool}}, Environment: &v1.Environment{Type: "none"}, VaultIDs: []string{vault}, MCPCredentials: []vaults.MCPCredentialBinding{{ServerLabel: "tickets", ServerURL: "https://mcp.example/tools", VaultID: vault, CredentialID: credential, AuthType: "static_bearer"}}} return snapshot } diff --git a/services/core/internal/execution/model_execution_test.go b/services/core/internal/execution/model_execution_test.go index 90a1da2e2..09435c33f 100644 --- a/services/core/internal/execution/model_execution_test.go +++ b/services/core/internal/execution/model_execution_test.go @@ -33,7 +33,7 @@ func TestSessionModelExecutionNeverFallsBack(t *testing.T) { reader, _ := testSessions(t, pgtest.Open(t), nil) d := Dispatcher{SessionsReader: reader} session := sessions.Session{TenantID: uuid.NewString(), ID: uuid.NewString(), Engine: "codex"} - if _, err := d.executionRequest(t.Context(), session, Snapshot{ModelProviderConfigured: true}, proto.Declaration{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { + if _, err := d.executionRequest(t.Context(), session, Snapshot{}, proto.Declaration{}, sessions.ExecutionBinding{}); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("missing Session credentials fell back", err) } } diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index 771100ea7..17ae2ff58 100644 --- a/services/core/internal/execution/recovery_test.go +++ b/services/core/internal/execution/recovery_test.go @@ -13,7 +13,7 @@ func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { for _, nativeID := range []string{"", "native"} { for _, capable := range []bool{false, true} { wantRecovery := started && nativeID == "" - req, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{ModelProviderConfigured: true}, proto.Declaration{Capabilities: proto.AgentKindCapabilities{NativeSessionRecovery: proto.CapabilityFromBool(capable)}}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) + req, err := (&Dispatcher{SessionsReader: frozenProvider{engine: engine}}).executionRequest(t.Context(), sessions.Session{ID: "session", Engine: engine}, Snapshot{}, proto.Declaration{Capabilities: proto.AgentKindCapabilities{NativeSessionRecovery: proto.CapabilityFromBool(capable)}}, sessions.ExecutionBinding{HasStartedTurn: started, NativeSessionID: nativeID}) if wantRecovery && !capable { if err == nil { t.Fatal("unverified recovery admitted", engine) diff --git a/services/core/internal/execution/structured_output_test.go b/services/core/internal/execution/structured_output_test.go index d7deae765..03ab27e44 100644 --- a/services/core/internal/execution/structured_output_test.go +++ b/services/core/internal/execution/structured_output_test.go @@ -13,7 +13,7 @@ import ( func TestStructuredOutputRequestKeepsFrozenSchemaAndInstructions(t *testing.T) { schema := json.RawMessage(`{"type":"object","properties":{"number":{"const":9007199254740992}}}`) instructions := "Keep these original instructions." - snapshot := Snapshot{ModelProviderConfigured: true, Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} + snapshot := Snapshot{Agent: v1.Agent{Model: "model", Instructions: &instructions, Text: v1.TextConfig{Format: v1.TextFormat{Type: "json_schema", Schema: schema}}}} request, err := (&Dispatcher{SessionsReader: frozenProvider{engine: "claude_sdk"}}).executionRequest(context.Background(), sessions.Session{Engine: "claude_sdk"}, snapshot, proto.Declaration{}, sessions.ExecutionBinding{}) if err != nil || request.ExecutionControls.OutputFormat == nil { t.Fatal(err) diff --git a/services/core/internal/persistence/postgres/sessionpg/creation_test.go b/services/core/internal/persistence/postgres/sessionpg/creation_test.go index 1552275b8..0d9577c95 100644 --- a/services/core/internal/persistence/postgres/sessionpg/creation_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/creation_test.go @@ -392,7 +392,7 @@ func TestCreationFreezesResourcesOnce(t *testing.T) { t.Fatal(err) } id := created.Session.ID - if bytes.Contains(created.Session.Configuration, []byte(canary)) || !bytes.Contains(created.Session.Configuration, []byte(`"model_provider_configured":true`)) { + if bytes.Contains(created.Session.Configuration, []byte(canary)) { t.Fatal("Session configuration", string(created.Session.Configuration)) } sealed := `FROM (SELECT encrypted_config AS b FROM session_model_execution WHERE session_id=$1 diff --git a/services/core/internal/sessions/creation.go b/services/core/internal/sessions/creation.go index 08b31c203..50b75eceb 100644 --- a/services/core/internal/sessions/creation.go +++ b/services/core/internal/sessions/creation.go @@ -382,37 +382,23 @@ func prepareCreation(input CreateSession, fingerprint func(string) (string, erro return NewSession{}, nil, nil, err } } - // The retry identity covers the configuration as requested; the marker - // added for a provider bundle below is not caller input. - requested := configuration if input.ModelProvider != nil { if err := input.ModelProvider.ValidateHarness(engine); err != nil { return NewSession{}, nil, nil, fmt.Errorf("%w: %s", ErrInvalidInput, err) } - // Raw values keep the caller's numbers exact. - var fields map[string]json.RawMessage - if json.Unmarshal(configuration, &fields) != nil { - return NewSession{}, nil, nil, ErrInvalidInput - } - fields["model_provider_configured"] = json.RawMessage("true") - if configuration, err = json.Marshal(fields); err != nil { - return NewSession{}, nil, nil, err - } } var initialization *environmentconfig.Setup if !input.Initialization.Empty() { initialization = &input.Initialization } // The retry identity carries a caller's provider key only as a keyed - // fingerprint. A deployment default is not caller input: leaving it and - // its configuration marker out keeps retries equivalent when the default - // is set, replaced or removed. + // fingerprint. A deployment default is not caller input: leaving it out + // keeps retries equivalent when the default is set, replaced or removed. var fingerprinted *v1.ModelProviderInput - hashed := configuration - if input.ModelProviderSource == v1.ModelProviderSourceDeployment { - hashed = requested - } else if fingerprinted, err = fingerprintedProvider(input.ModelProvider, fingerprint); err != nil { - return NewSession{}, nil, nil, err + if input.ModelProviderSource != v1.ModelProviderSourceDeployment { + if fingerprinted, err = fingerprintedProvider(input.ModelProvider, fingerprint); err != nil { + return NewSession{}, nil, nil, err + } } // encoding/json sorts map keys, so key order does not affect retries. canonical, err := json.Marshal(struct { @@ -423,7 +409,7 @@ func prepareCreation(input CreateSession, fingerprint func(string) (string, erro InitialInputs json.RawMessage `json:",omitempty"` InitialFiles []environmentconfig.InitialFile `json:",omitempty"` Initialization *environmentconfig.Setup `json:",omitempty"` - }{fingerprinted, engine, labels, hashed, encodedInput, input.InitialFiles, initialization}) + }{fingerprinted, engine, labels, configuration, encodedInput, input.InitialFiles, initialization}) if err != nil { return NewSession{}, nil, nil, fmt.Errorf("%w: input: %v", ErrInvalidInput, err) } diff --git a/services/core/internal/sessions/creation_test.go b/services/core/internal/sessions/creation_test.go index d3115affc..e1cc49beb 100644 --- a/services/core/internal/sessions/creation_test.go +++ b/services/core/internal/sessions/creation_test.go @@ -202,7 +202,7 @@ func TestPrepareCreation(t *testing.T) { } deployed := prepare(t, withProvider("none", "deployment-key", v1.ModelProviderSourceDeployment), unavailable) plain := prepare(t, func(input *CreateSession) { input.Configuration = creationInput("none").Configuration }, unavailable) - if !strings.Contains(string(deployed.Configuration), `"model_provider_configured":true`) || deployed.RequestHash != plain.RequestHash { + if deployed.RequestHash != plain.RequestHash { t.Fatalf("the deployment default joined the identity: %s", deployed.Configuration) } if intent := prepare(t, func(input *CreateSession) { input.CreationRequest = json.RawMessage(`{"agent_id":"a"}`) }, fingerprints).IntentHash; intent == nil { diff --git a/services/core/tests/integration/session_execution_configuration_test.go b/services/core/tests/integration/session_execution_configuration_test.go index c57b44886..7a5179958 100644 --- a/services/core/tests/integration/session_execution_configuration_test.go +++ b/services/core/tests/integration/session_execution_configuration_test.go @@ -128,7 +128,7 @@ func TestSessionExecutionConfigurationFrozenAcrossCreationPathsAndRetry(t *testi func TestSessionExecutionConfigurationHistoricalProvenance(t *testing.T) { s, pool := testStore(t) tenant := uuid.NewString() - for _, configuration := range []string{`{}`, `{"agent":{"model":null}}`, `{"agent":{"model":"historical-model"},"model_provider_configured":true}`} { + for _, configuration := range []string{`{}`, `{"agent":{"model":null}}`, `{"agent":{"model":"historical-model"}}`} { input := sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: []byte(configuration)} session, err := s.CreateSession(t.Context(), tenant, input) if err != nil { From eeb1b1daf032ceffd070053a061dfd7cc5a9f1cd Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 22:45:25 +0000 Subject: [PATCH 3/5] Delete unread heartbeat and done fields Core reads only supported_agent_kinds and home_removal from a heartbeat, and nothing reads DonePayload.Transcript. The heartbeat's ts, active_requests and daemon_version and the transcript go. The connect option that carries the wire version stays. --- apps/daemon/internal/cli/connect.go | 10 +++------- apps/daemon/internal/cli/connect_suspend.go | 2 +- apps/daemon/internal/dispatch/router.go | 3 +-- apps/daemon/internal/transport/ws.go | 2 +- apps/daemon/internal/transport/ws_test.go | 4 ++-- docs/runtime-protocol.md | 2 +- docs/zh/runtime-protocol.md | 4 ++-- internal/agentdaemon/proto/inbound.go | 4 ---- services/core/internal/runtimegateway/session_test.go | 2 +- 9 files changed, 12 insertions(+), 21 deletions(-) diff --git a/apps/daemon/internal/cli/connect.go b/apps/daemon/internal/cli/connect.go index 70bb9ef1b..b5b0352f9 100644 --- a/apps/daemon/internal/cli/connect.go +++ b/apps/daemon/internal/cli/connect.go @@ -242,10 +242,9 @@ func mainLoopRemote(parent context.Context, rc *runContext, profile string, prof WSURL: wsURL, DeviceID: boot.DeviceID, Credential: prof.RunnerCredential, - // DaemonVersion is the WIRE-PROTOCOL version, not the build - // tag. proto.VersionCompatible requires an exact version - // match against proto.Version. Build-tag reporting goes - // in heartbeat's DaemonVersion field. + // DaemonVersion is the wire-protocol version, not the build + // tag: proto.VersionCompatible requires an exact match + // against proto.Version. DaemonVersion: proto.Version, }) if remote != "" && err != nil { @@ -362,9 +361,6 @@ func pumpConn(parentCtx context.Context, conn *transport.Conn, cfg dispatch.Conf conn.StartHeartbeats(parentCtx, boot.HeartbeatInterval(), func() proto.HeartbeatPayload { return proto.HeartbeatPayload{ - Timestamp: time.Now().Unix(), - ActiveRequests: router.ActiveRuns(), - DaemonVersion: Version, SupportedAgentKinds: cfg.Registry.SupportedAgentKinds(), HomeRemoval: proto.CapabilityFromBool(cfg.RemoveHome != nil), } diff --git a/apps/daemon/internal/cli/connect_suspend.go b/apps/daemon/internal/cli/connect_suspend.go index 6783706fb..888ce58a5 100644 --- a/apps/daemon/internal/cli/connect_suspend.go +++ b/apps/daemon/internal/cli/connect_suspend.go @@ -147,7 +147,7 @@ func (s *suspendedRouter) reconnectSuspension(ctx context.Context, dial transpor func (s *suspendedRouter) heartbeats(ctx context.Context, conn *transport.Conn, boot *transport.BootstrapResponse, discovery agentCLIDiscovery) { conn.StartHeartbeats(ctx, boot.HeartbeatInterval(), func() proto.HeartbeatPayload { - return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), ActiveRequests: s.router.ActiveRuns(), DaemonVersion: Version, SupportedAgentKinds: s.registry.SupportedAgentKinds(), HomeRemoval: proto.CapabilityUnsupported} + return proto.HeartbeatPayload{SupportedAgentKinds: s.registry.SupportedAgentKinds(), HomeRemoval: proto.CapabilityUnsupported} }, obslog.Bg()) } diff --git a/apps/daemon/internal/dispatch/router.go b/apps/daemon/internal/dispatch/router.go index 1fe1c9ef2..309d552eb 100644 --- a/apps/daemon/internal/dispatch/router.go +++ b/apps/daemon/internal/dispatch/router.go @@ -224,8 +224,7 @@ func adoptEnvelopeTrace(ctx context.Context, env proto.Envelope) context.Context return ctx } -// ActiveRuns returns the in-flight run count. Wired into the heartbeat -// payload supplier. +// ActiveRuns returns the in-flight run count. func (r *Router) ActiveRuns() int { r.mu.Lock() defer r.mu.Unlock() diff --git a/apps/daemon/internal/transport/ws.go b/apps/daemon/internal/transport/ws.go index e39411074..2cd3fa366 100644 --- a/apps/daemon/internal/transport/ws.go +++ b/apps/daemon/internal/transport/ws.go @@ -192,7 +192,7 @@ func (c *Conn) Close() error { // StartHeartbeats kicks off a ticker that calls payloadFn every // interval and Sends the resulting HeartbeatPayload. Returns // immediately. Caller-controlled because the heartbeat carries fields -// (active_requests, supported_agent_kinds) only the agent layer knows. +// (supported_agent_kinds, home_removal) only the agent layer knows. // Nil logger falls back to log.Bg(). func (c *Conn) StartHeartbeats(parentCtx context.Context, interval time.Duration, payloadFn func() proto.HeartbeatPayload, logger *slog.Logger) { if interval <= 0 || payloadFn == nil { diff --git a/apps/daemon/internal/transport/ws_test.go b/apps/daemon/internal/transport/ws_test.go index 7e6b2fbf7..f8e511a17 100644 --- a/apps/daemon/internal/transport/ws_test.go +++ b/apps/daemon/internal/transport/ws_test.go @@ -269,7 +269,7 @@ func TestStartHeartbeatsTicks(t *testing.T) { var calls atomic.Int32 conn.StartHeartbeats(context.Background(), 30*time.Millisecond, func() proto.HeartbeatPayload { calls.Add(1) - return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), DaemonVersion: "0.0.0-dev", HomeRemoval: proto.CapabilityUnsupported} + return proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported} }, nil) deadline := time.Now().Add(time.Second) @@ -311,7 +311,7 @@ func TestStartHeartbeatsSendsImmediately(t *testing.T) { defer cancel() conn.StartHeartbeats(hbCtx, time.Hour, func() proto.HeartbeatPayload { calls.Add(1) - return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), DaemonVersion: "0.0.0-dev", HomeRemoval: proto.CapabilityUnsupported} + return proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported} }, nil) deadline := time.Now().Add(2 * time.Second) diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index d945fd47c..2582f27b8 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -18,7 +18,7 @@ A Runtime connects in this order: 4. Send a heartbeat at once, then at the interval bootstrap returned. Each heartbeat declares `supported_agent_kinds`, their availability and their [capabilities](#capability-declarations), and whether the Runtime removes a Session's native home on release (`home_removal`, [Session assignments](#session-assignments)). Before the first heartbeat, capabilities are unknown; a kind missing from a heartbeat is not advertised. Neither permits inference. 5. Exchange ordered JSON [envelopes](#envelope-and-identity). Heartbeats establish liveness only, never execution progress or a receipt for an earlier message. -The wire version is [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go), independent of the Runtime build version that heartbeats report. Core accepts only an exact match, including the patch component. A mismatch returns HTTP 426 `incompatible_version` before any dispatch; the daemon treats it as permanent and stops reconnecting. Deploy matching peers together. +The wire version is [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go). Core accepts only an exact match, including the patch component. A mismatch returns HTTP 426 `incompatible_version` before any dispatch; the daemon treats it as permanent and stops reconnecting. Deploy matching peers together. Each physical connection has fresh routing, admission handles and transfer state. A newer connection for the same device replaces the previous one: Core closes the previous connection and evicts its Run routes, and the new connection inherits none of them. A valid credential and connection are never authority to choose another Session or Environment binding; [assignments](#session-assignments) fence which Session a frame may act for. diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index e62aeac15..1c84a8ab4 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: b381607cf77ca2e933c5d8345e26585b585c63f686863096013d20ffd862ca47 +source_hash: 6cbb3db01de6635b1e68f8639a6d8ce3b8fb022ad551a834d80c5a341b86437d --- 此协议在 Runtime daemon 获取机器凭据后连接 Core 与 daemon,定义 daemon 连接上消息的含义和顺序。wire 类型、限制和验证器仅在 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 中定义一次;Core 的 [gateway](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/runtimegateway) 与参考 Runtime 的 [dispatcher](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch) 都使用它们,因此无需同步第二套 payload schema。签发凭据和打开连接的 HTTP 路由见[机器连接 API](../../contracts/agents-api/zh/machine-api.md)。 @@ -20,7 +20,7 @@ Runtime 按以下顺序连接: 4. 立即发送 heartbeat,之后按 bootstrap 返回的间隔发送。每个 heartbeat 声明 `supported_agent_kinds`、其可用性和[能力](#capability-declarations),以及 Runtime 是否在释放时删除 Session 的原生 home(`home_removal`,见 [Session 分配](#session-assignments))。第一个 heartbeat 之前能力未知;heartbeat 中缺失的 kind 视为未声明。两者都不允许推断。 5. 交换有序 JSON [envelope](#envelope-and-identity)。Heartbeat 仅证明存活,不证明执行进度,也不充当此前消息的回执。 -wire 版本为 [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go),独立于 heartbeat 报告的 Runtime 构建版本。Core 仅接受精确匹配,包括 patch 部分。不匹配时,在任何 dispatch 前返回 HTTP 426 `incompatible_version`;daemon 将其视为永久错误并停止重连。应一起部署版本匹配的两端。 +wire 版本为 [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go)。Core 仅接受精确匹配,包括 patch 部分。不匹配时,在任何 dispatch 前返回 HTTP 426 `incompatible_version`;daemon 将其视为永久错误并停止重连。应一起部署版本匹配的两端。 每条物理连接拥有新的路由、admission handle 和传输状态。同一设备的新连接替代旧连接:Core 关闭旧连接并移除其 Run 路由,新连接不继承这些状态。有效凭据和连接不授权选择其他 Session 或 Environment 绑定;[分配](#session-assignments)约束 frame 可代表哪个 Session 行事。 diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 7d34d204f..ebdc748a8 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -147,7 +147,6 @@ type ErrorPayload struct { type DonePayload struct { // SourceCompletedAtMS freezes the native root completion before child settlement. SourceCompletedAtMS *int64 `json:"source_completed_at_ms,omitempty"` - Transcript string `json:"transcript,omitempty"` Usage Usage `json:"usage,omitzero"` Metadata map[string]any `json:"metadata,omitempty"` } @@ -194,9 +193,6 @@ type SupportedAgentKind struct { // supported_agent_kinds establishes no engine availability or capabilities. // HomeRemoval declares whether assignment_release accepts RemoveHome. type HeartbeatPayload struct { - Timestamp int64 `json:"ts"` - ActiveRequests int `json:"active_requests"` - DaemonVersion string `json:"daemon_version,omitempty"` SupportedAgentKinds []SupportedAgentKind `json:"supported_agent_kinds,omitempty"` HomeRemoval CapabilitySupport `json:"home_removal"` } diff --git a/services/core/internal/runtimegateway/session_test.go b/services/core/internal/runtimegateway/session_test.go index aeb840056..59c406056 100644 --- a/services/core/internal/runtimegateway/session_test.go +++ b/services/core/internal/runtimegateway/session_test.go @@ -362,7 +362,7 @@ func TestSession_HeartbeatDoesNotInferCapabilities(t *testing.T) { sess.Start() defer sess.Close("test done") - conn.Feed([]byte(`{"type":"heartbeat","payload":{"ts":1710000100,"claude_available":true,"home_removal":false}}`)) + conn.Feed([]byte(`{"type":"heartbeat","payload":{"claude_available":true,"home_removal":false}}`)) heartbeat.waitDaemonHeartbeat(t) if info, found, known := sess.AgentKindStatus("claude_sdk"); found || !known { t.Fatalf("undeclared capabilities inferred: %#v", info) From d91b8ba53c7a0147cf6a21ac7429ae070ea10b96 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 22:48:25 +0000 Subject: [PATCH 4/5] Drop Codex's always-true terminal error flag --- apps/daemon/internal/agent/codex/executor.go | 2 +- .../internal/agent/codex/executor_turn.go | 4 +- apps/daemon/internal/agent/codex/session.go | 41 +++++++------------ .../internal/agent/codex/session_log_test.go | 39 +----------------- .../codex/session_steering_lifecycle_test.go | 2 +- .../internal/agent/codex/session_tools.go | 2 +- .../agent/codex/session_usage_test.go | 2 +- .../agent/codex/subagent_observations.go | 2 +- 8 files changed, 23 insertions(+), 71 deletions(-) diff --git a/apps/daemon/internal/agent/codex/executor.go b/apps/daemon/internal/agent/codex/executor.go index 26f9a10e1..4e531ab76 100644 --- a/apps/daemon/internal/agent/codex/executor.go +++ b/apps/daemon/internal/agent/codex/executor.go @@ -118,7 +118,7 @@ func (e *Executor) StartTurn(ctx context.Context, runID string, input proto.Mess } } if err != nil { - s.emitTerminal("codex: native start failed", true) + s.emitTerminal("codex: native start failed") } go s.settleExecutorTurn(err) return s, err diff --git a/apps/daemon/internal/agent/codex/executor_turn.go b/apps/daemon/internal/agent/codex/executor_turn.go index 7a848578a..c3cf658dc 100644 --- a/apps/daemon/internal/agent/codex/executor_turn.go +++ b/apps/daemon/internal/agent/codex/executor_turn.go @@ -24,9 +24,9 @@ func (s *Session) settleExecutorTurn(startErr error) { select { case <-s.outputDone: case <-s.rpc.Done(): - s.emitTerminal("codex: connection closed before settlement", true) + s.emitTerminal("codex: connection closed before settlement") case <-s.cancelCtx.Done(): - s.emitTerminal("codex: execution owner closed", true) + s.emitTerminal("codex: execution owner closed") } } // Detach the old callbacks before waiting for their captured Turn and native diff --git a/apps/daemon/internal/agent/codex/session.go b/apps/daemon/internal/agent/codex/session.go index 2f6997c04..90f7f509a 100644 --- a/apps/daemon/internal/agent/codex/session.go +++ b/apps/daemon/internal/agent/codex/session.go @@ -205,7 +205,7 @@ func (s *Session) onTurnCompleted(raw json.RawMessage) { if errText != "" { body = appendOnNewline(body, errText) } - s.emitTerminalFailure(body, true, classifyTurnError(p.Turn.Error)) + s.emitTerminalFailure(body, classifyTurnError(p.Turn.Error)) return } var completedAt *int64 @@ -270,7 +270,7 @@ func (s *Session) onTurnFailed(raw json.RawMessage) { "turn_id", p.Turn.ID, "turn_status", p.Turn.Status, "last_err_text_present", s.peekLastErrText() != "") - s.emitTerminal("codex: turn failed", true) + s.emitTerminal("codex: turn failed") } func (s *Session) onErrorNotif(raw json.RawMessage) { @@ -348,39 +348,28 @@ func (s *Session) emitUsage(u TurnUsage) { s.trySend(env) } -func (s *Session) emitTerminal(message string, asError bool) { - s.emitTerminalFailure(message, asError, proto.ErrorPayload{}) +func (s *Session) emitTerminal(message string) { + s.emitTerminalFailure(message, proto.ErrorPayload{}) } -func (s *Session) emitTerminalFailure(message string, asError bool, failure proto.ErrorPayload) { +// emitTerminalFailure ends the Turn with an error and then done. +func (s *Session) emitTerminalFailure(message string, failure proto.ErrorPayload) { defer s.finishAfterTerminal() if !s.terminal.CompareAndSwap(false, true) { return } s.stopSteering() s.stopFunctionCalls() - // Always log: this is the only place the daemon decides "the prompt is - // over, here's what went wrong (if anything)". Without this, post- - // mortem requires correlating server-side TypeError frames against - // daemon timestamps with no message body anywhere. - if asError { - s.cfg.logger.Warn("codex: emitting terminal error", - "run_id", s.runID, - "thread_id", s.currentThreadID(), - "message", message) - } else { - s.cfg.logger.Info("codex: emitting terminal done", - "run_id", s.runID, - "thread_id", s.currentThreadID(), - "message_len", len(message)) - } + // Always log the failure the daemon decided on: Core persists only its + // error frame, and the message is the post-mortem's starting point. + s.cfg.logger.Warn("codex: emitting terminal error", + "run_id", s.runID, + "thread_id", s.currentThreadID(), + "message", message) + failure.Error = message var events []proto.Envelope - if asError { - failure.Error = message - env, err := proto.NewEnvelope(proto.TypeError, s.runID, failure) - if err == nil { - events = append(events, env) - } + if env, err := proto.NewEnvelope(proto.TypeError, s.runID, failure); err == nil { + events = append(events, env) } doneMeta := map[string]any{} if tid := s.currentThreadID(); tid != "" { diff --git a/apps/daemon/internal/agent/codex/session_log_test.go b/apps/daemon/internal/agent/codex/session_log_test.go index ae51381c7..737beaaa6 100644 --- a/apps/daemon/internal/agent/codex/session_log_test.go +++ b/apps/daemon/internal/agent/codex/session_log_test.go @@ -39,7 +39,7 @@ func TestEmitTerminal_LogsErrorMessage(t *testing.T) { s.setThreadID("thread-abc") const message = "codex: thread/start: bad provider config" - s.emitTerminal(message, true) + s.emitTerminal(message) logs := buf.String() for _, want := range []string{ @@ -72,43 +72,6 @@ func TestEmitTerminal_LogsErrorMessage(t *testing.T) { } } -// TestEmitTerminal_LogsDoneMessage covers the success-path log so a -// future change that flips asError=false on a real prompt completion -// still leaves a trace in the daemon log. -func TestEmitTerminal_LogsDoneMessage(t *testing.T) { - var buf bytes.Buffer - logger := slog.New(slog.NewJSONHandler(&buf, nil)) - - out := make(chan proto.Envelope, 4) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - s := &Session{ - runID: "run-test-456", - cfg: sessionConfig{logger: logger}, - out: out, - cancelCtx: ctx, - } - - s.emitTerminal("hello world", false) - - logs := buf.String() - for _, want := range []string{ - `"msg":"codex: emitting terminal done"`, - `"run_id":"run-test-456"`, - `"message_len":11`, - } { - if !strings.Contains(logs, want) { - t.Errorf("log missing %q\n--- log ---\n%s", want, logs) - } - } - // asError=false: only TypeDone, no TypeError. - got := drainEnvelopes(out) - if len(got) != 1 || got[0].Type != proto.TypeDone { - t.Fatalf("envelopes = %+v, want exactly 1 done", got) - } -} - // TestOnErrorNotif_LogsAndBuffers verifies the codex `error` // notification path stays inspectable. Codex's gateway / provider // failures (a 401 from a misconfigured custom header, a 400 from diff --git a/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go b/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go index 00dd4fa2b..8172658cd 100644 --- a/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go +++ b/apps/daemon/internal/agent/codex/session_steering_lifecycle_test.go @@ -55,7 +55,7 @@ func TestSteeringReceiptTimeoutAndCompletionKeepProcessAlive(t *testing.T) { t.Fatal("response wait killed retained process") } if complete { - s.emitTerminal("late disconnect", true) + s.emitTerminal("late disconnect") var frames []proto.Envelope for env := range out { frames = append(frames, env) diff --git a/apps/daemon/internal/agent/codex/session_tools.go b/apps/daemon/internal/agent/codex/session_tools.go index d1d4b4c94..500478a3f 100644 --- a/apps/daemon/internal/agent/codex/session_tools.go +++ b/apps/daemon/internal/agent/codex/session_tools.go @@ -22,7 +22,7 @@ func (s *Session) sendItemEvents(events []proto.Envelope, notification json.RawM var err error tool.Observation, err = normalizeToolObservation(tool.ID, tool.Stage, native.Item) if err != nil { - s.emitTerminal("codex: invalid tool observation", true) + s.emitTerminal("codex: invalid tool observation") return } payload, err := json.Marshal(tool) diff --git a/apps/daemon/internal/agent/codex/session_usage_test.go b/apps/daemon/internal/agent/codex/session_usage_test.go index e37b0d535..c604de134 100644 --- a/apps/daemon/internal/agent/codex/session_usage_test.go +++ b/apps/daemon/internal/agent/codex/session_usage_test.go @@ -137,7 +137,7 @@ func TestAbnormalTerminationTransmitsKnownUsage(t *testing.T) { s.setThreadID("thread") s.onTurnStarted(json.RawMessage(`{"threadId":"thread","turn":{"id":"turn"}}`)) s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"turn","tokenUsage":{"total":{"inputTokens":10,"cachedInputTokens":4,"outputTokens":3,"reasoningOutputTokens":2,"totalTokens":13}}}`)) - s.emitTerminal("native connection closed", true) + s.emitTerminal("native connection closed") s.closeOut() var done proto.DonePayload for e := range out { diff --git a/apps/daemon/internal/agent/codex/subagent_observations.go b/apps/daemon/internal/agent/codex/subagent_observations.go index 29d1dcaf3..005c013e1 100644 --- a/apps/daemon/internal/agent/codex/subagent_observations.go +++ b/apps/daemon/internal/agent/codex/subagent_observations.go @@ -96,7 +96,7 @@ func (s *Session) collectSubagentFacts() { continue } if terminal == nil { - s.emitTerminal("codex: subagent facts could not be confirmed", true) + s.emitTerminal("codex: subagent facts could not be confirmed") select { case terminal = <-o.terminal: case <-o.ctx.Done(): From 9089368decdfe22c6d2100df791b64a88f03ed16 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 23:33:07 +0000 Subject: [PATCH 5/5] Prepare each Executor from one request The Core-Runtime wire carries only what Core decides. Delete AgentStateKey, LocalEnvironment's Capabilities, NetworkAccess and AllowedDomains, the prepared configuration's RunID and Input, and every json:"-" field. The Runtime derives the state key from the Session ID, the Environment owner knows its installed capabilities, and Core admits only an enabled workspace network. ExecutorFactory and ViewExecutorFactory take agent.PrepareRequest: the wire configuration, the model configuration the Registry prepares once, the state key, the assignment, and the Environment's workspace and installed Capabilities that its owner fills. Adapters no longer prepare or re-parse the model configuration, and MiniMax Code keeps each Turn's Run ID in its own state. Codex runs the shared text lifecycle contract. --- .../claudesdk/cancellation_live_linux_test.go | 4 +- .../agent/claudesdk/cancellation_test.go | 8 +- .../agent/claudesdk/commands_session_test.go | 8 +- .../claudesdk/error_classification_test.go | 4 +- .../claudesdk/execution_controls_test.go | 12 +- .../internal/agent/claudesdk/executor.go | 12 +- .../claudesdk/executor_confirmation_test.go | 2 +- .../agent/claudesdk/executor_fixture_test.go | 15 ++- .../claudesdk/executor_live_linux_test.go | 8 +- .../internal/agent/claudesdk/executor_test.go | 14 +-- .../agent/claudesdk/functions_test.go | 4 +- .../agent/claudesdk/harness_config_test.go | 7 +- .../agent/claudesdk/live_linux_test.go | 4 +- .../internal/agent/claudesdk/local_test.go | 40 ++----- apps/daemon/internal/agent/claudesdk/mcp.go | 2 +- .../agent/claudesdk/mcp_bearer_test.go | 7 +- .../agent/claudesdk/mcp_environment.go | 4 +- .../agent/claudesdk/mcp_environment_test.go | 35 +++--- .../internal/agent/claudesdk/mcp_test.go | 2 +- .../internal/agent/claudesdk/messages_test.go | 4 +- .../agent/claudesdk/model_route_test.go | 28 ----- .../internal/agent/claudesdk/options.go | 15 ++- .../internal/agent/claudesdk/options_test.go | 50 ++++---- .../claudesdk/preparation_fixture_test.go | 1 - .../agent/claudesdk/preparation_test.go | 16 +-- .../agent/claudesdk/readiness_test.go | 12 +- .../agent/claudesdk/restrictions_test.go | 4 +- .../internal/agent/claudesdk/session_test.go | 8 +- .../internal/agent/claudesdk/steering_test.go | 4 +- .../agent/claudesdk/subagents_test.go | 8 +- .../agent/claudesdk/tool_environment_test.go | 8 +- .../internal/agent/claudesdk/usage_test.go | 6 +- apps/daemon/internal/agent/claudesdk/view.go | 19 ++- .../internal/agent/claudesdk/view_test.go | 14 +-- .../internal/agent/claudesdk/workspace.go | 14 +-- .../agent/claudesdk/workspace_launch_test.go | 4 +- .../claudesdk/workspace_live_linux_test.go | 6 +- .../claudesdk/workspace_structured_test.go | 2 +- .../agent/claudesdk/workspace_test.go | 21 ++-- .../agent/codex/execution_controls_test.go | 6 +- apps/daemon/internal/agent/codex/executor.go | 2 +- .../agent/codex/executor_native_test.go | 6 +- .../internal/agent/codex/executor_test.go | 14 ++- .../agent/codex/harness_config_test.go | 15 +-- .../agent/codex/mcp_environment_test.go | 23 ++-- apps/daemon/internal/agent/codex/mcp_http.go | 5 +- .../agent/codex/mcp_http_bearer_test.go | 8 +- .../agent/codex/mcp_http_preflight_test.go | 2 +- .../internal/agent/codex/mcp_http_test.go | 19 ++- .../internal/agent/codex/model_route_test.go | 32 ----- .../agent/codex/model_verbosity_test.go | 4 +- apps/daemon/internal/agent/codex/options.go | 15 +-- .../internal/agent/codex/options_test.go | 10 +- .../agent/codex/permission_profile.go | 14 --- .../internal/agent/codex/preparation.go | 11 +- .../agent/codex/preparation_helpers_test.go | 27 ++++- .../agent/codex/preparation_router_test.go | 7 +- .../agent/codex/provider_config_test.go | 6 +- .../internal/agent/codex/recovery_test.go | 7 +- .../internal/agent/codex/session_plan.go | 11 +- ...n_profile_test.go => session_plan_test.go} | 35 +++--- .../agent/codex/session_policy_test.go | 2 +- apps/daemon/internal/agent/codex/view.go | 19 ++- apps/daemon/internal/agent/codex/view_test.go | 21 ++-- .../internal/agent/configuration_test.go | 12 +- apps/daemon/internal/agent/harness.go | 64 ++++++---- .../internal/agent/mcode/environment_mcp.go | 5 +- .../agent/mcode/environment_mcp_test.go | 38 +++--- apps/daemon/internal/agent/mcode/execution.go | 2 +- .../internal/agent/mcode/execution_test.go | 4 +- apps/daemon/internal/agent/mcode/executor.go | 16 +-- .../agent/mcode/executor_native_test.go | 5 +- .../internal/agent/mcode/executor_test.go | 12 +- .../agent/mcode/harness_config_test.go | 20 ---- .../agent/mcode/mcp_observations_test.go | 13 +- .../agent/mcode/model_provider_test.go | 30 +---- .../internal/agent/mcode/native_test.go | 8 +- apps/daemon/internal/agent/mcode/options.go | 43 +++---- .../internal/agent/mcode/options_test.go | 12 +- apps/daemon/internal/agent/mcode/session.go | 3 +- .../internal/agent/mcode/session_test.go | 29 +++-- .../agent/mcode/tool_environment_test.go | 15 +-- .../agent/mcode/tool_observations_test.go | 2 +- apps/daemon/internal/agent/mcode/view.go | 22 ++-- apps/daemon/internal/agent/mcode/view_test.go | 15 +-- apps/daemon/internal/agent/mcode/workspace.go | 10 +- .../agent/mcode/workspace_network_test.go | 1 - .../agent/mcode/workspace_skills_test.go | 4 +- apps/daemon/internal/agent/mcp_binding.go | 25 ++-- .../daemon/internal/agent/mcp_binding_test.go | 34 +++--- apps/daemon/internal/agent/registry_test.go | 8 +- apps/daemon/internal/agent/view_test.go | 19 +-- .../agent/viewloader/loader_linux_test.go | 3 +- apps/daemon/internal/agenthost/admit.go | 34 ++---- .../internal/agenthost/admit_linux_test.go | 54 ++++----- .../agenthost/agenthost_linux_test.go | 31 +++-- .../internal/agenthost/environment_linux.go | 53 ++++----- .../agenthost/environment_linux_test.go | 18 ++- .../internal/agenthost/executor_linux.go | 6 +- .../internal/agenthost/host_linux_test.go | 2 +- .../internal/agenthost/view_linux_test.go | 8 +- .../agenthostqualify/qualify_linux_test.go | 6 +- .../internal/cli/agent_host_linux_test.go | 2 +- .../cli/claude_sdk_live_linux_test.go | 2 +- .../internal/cli/connect_cleanup_test.go | 4 +- .../internal/dispatch/assignment_test.go | 2 +- .../internal/dispatch/cancellation_test.go | 2 +- .../dispatch/capability_admission_test.go | 2 +- apps/daemon/internal/dispatch/environment.go | 13 +- .../internal/dispatch/environment_test.go | 62 +++++++++- apps/daemon/internal/dispatch/executor.go | 46 +++---- .../dispatch/executor_cancel_receipt_test.go | 2 +- .../dispatch/executor_handoff_test.go | 4 +- .../daemon/internal/dispatch/executor_test.go | 7 +- .../internal/dispatch/functions_test.go | 4 +- .../internal/dispatch/local_directory_test.go | 12 +- .../daemon/internal/dispatch/mcp_http_test.go | 5 +- apps/daemon/internal/dispatch/preparation.go | 6 +- .../preparation_executor_fixture_test.go | 4 +- .../internal/dispatch/preparation_test.go | 43 ++----- .../internal/dispatch/receipt_order_test.go | 4 +- .../dispatch/receipt_shutdown_test.go | 2 +- apps/daemon/internal/dispatch/router_test.go | 18 ++- .../runtime_preparation_execution_test.go | 1 - .../dispatch/steering_lifetime_test.go | 4 +- .../daemon/internal/dispatch/steering_test.go | 8 +- .../dispatch/workspace_export_test.go | 6 +- apps/daemon/internal/gateway/mcp_test.go | 2 +- .../daemon/internal/localworkspace/binding.go | 47 +++----- .../internal/localworkspace/binding_test.go | 43 ++----- .../internal/localworkspace/capabilities.go | 26 ++-- .../localworkspace/capabilities_test.go | 28 +---- .../capability_preparation_test.go | 62 +++++----- apps/daemon/internal/localworkspace/mcp.go | 10 +- .../internal/localworkspace/mcp_test.go | 4 +- .../internal/localworkspace/native_binding.go | 2 +- .../localworkspace/network_policy_test.go | 52 +------- .../runtime_initialization_test.go | 4 +- .../localworkspace/snapshot_marker_test.go | 10 +- .../internal/wireconformance/wire_test.go | 2 +- apps/daemon/testdata/onboarding/main.go | 11 +- contracts/agents-api/harness-onboarding.md | 16 +-- contracts/agents-api/zh/harness-onboarding.md | 18 +-- docs/runtime-protocol.md | 4 +- docs/zh/runtime-protocol.md | 6 +- internal/agentdaemon/proto/environment.go | 29 +---- internal/agentdaemon/proto/mcp.go | 4 +- .../agentdaemon/proto/message_input_test.go | 2 +- internal/agentdaemon/proto/outbound.go | 15 --- internal/agentdaemon/proto/prototest/wire.go | 3 +- .../agentdaemon/proto/runtime_prepare_test.go | 23 ---- internal/agentdaemon/proto/selection.go | 13 +- .../proto/workspace_read_preparation.go | 3 +- internal/harnessconfig/preparation_test.go | 8 ++ scripts/name-allowlist.json | 112 +----------------- services/core/internal/execution/delivery.go | 12 +- .../execution/directory_preparation.go | 2 +- .../core/internal/execution/dispatcher.go | 5 +- .../environment_capabilities_test.go | 2 +- .../execution/environment_placement.go | 9 +- .../execution/environment_placement_test.go | 20 +--- .../execution/executor_preparation.go | 6 +- .../internal/execution/prepared_dispatch.go | 7 +- .../core/internal/execution/recovery_test.go | 2 +- services/core/internal/execution/request.go | 3 +- .../mcp_bearer_live_linux_test.go | 2 +- .../core/tests/integration/dispatch_test.go | 6 +- .../integration/environment_directory_test.go | 2 +- .../environment_worker_helpers_test.go | 2 +- .../integration/environment_worker_test.go | 3 +- .../integration/executor_fixture_test.go | 15 ++- .../integration/executor_recovery_test.go | 2 +- .../integration/harness_onboarding_test.go | 16 ++- .../integration/prepared_dispatch_test.go | 2 +- .../integration/public_execution_test.go | 2 +- .../tests/integration/worker_capacity_test.go | 4 +- 176 files changed, 959 insertions(+), 1432 deletions(-) delete mode 100644 apps/daemon/internal/agent/claudesdk/model_route_test.go delete mode 100644 apps/daemon/internal/agent/codex/model_route_test.go delete mode 100644 apps/daemon/internal/agent/codex/permission_profile.go rename apps/daemon/internal/agent/codex/{permission_profile_test.go => session_plan_test.go} (59%) delete mode 100644 apps/daemon/internal/agent/mcode/harness_config_test.go diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go index 7000d0172..4585589ff 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go @@ -66,8 +66,8 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() out := make(chan proto.Envelope, 64) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."} - running, err := startSingleTurn(ctx, config, request, out) + request := proto.PromptRequestPayload{AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."} + running, err := startSingleTurn(ctx, config, request, uuid.NewString(), proto.TextInput(prompt), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/cancellation_test.go b/apps/daemon/internal/agent/claudesdk/cancellation_test.go index 4a4945406..549964499 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_test.go @@ -27,7 +27,7 @@ func TestCancellationWaitsForDrainAndPublishesOutcome(t *testing.T) { defer cancel() // A stopped consumer must not prevent native output draining or cancellation. out := make(chan proto.Envelope) - running, err := startSingleTurn(ctx, config, cancellationRequest(), out) + running, err := startSingleTurn(ctx, config, cancellationRequest(), "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -89,7 +89,7 @@ func TestFailureKeepsOnlyVerifiedNativeIdentity(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 8) - running, err := startSingleTurn(ctx, cancellationConfig(root, mode), cancellationRequest(), out) + running, err := startSingleTurn(ctx, cancellationConfig(root, mode), cancellationRequest(), "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -125,7 +125,7 @@ func TestCancellationDrainsIntoReadyConsumer(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := startSingleTurn(ctx, config, cancellationRequest(), out) + running, err := startSingleTurn(ctx, config, cancellationRequest(), "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -163,7 +163,7 @@ func cancellationConfig(root, mode string) Config { } func cancellationRequest() proto.PromptRequestPayload { - return proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + return proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} } func runCancellationHelper(request startRequest, mode string, scanner *bufio.Scanner, emit func(bridgeEvent)) { diff --git a/apps/daemon/internal/agent/claudesdk/commands_session_test.go b/apps/daemon/internal/agent/claudesdk/commands_session_test.go index bd928f6d7..d5da4b08d 100644 --- a/apps/daemon/internal/agent/claudesdk/commands_session_test.go +++ b/apps/daemon/internal/agent/claudesdk/commands_session_test.go @@ -18,7 +18,7 @@ import ( func TestWorkspaceCommandsRequirePackagedFeature(t *testing.T) { config := preparationFixture(t, "old-command-runtime") - if _, err := NewExecutorFactory(config)(t.Context(), preparationRequest()); err == nil || !strings.Contains(err.Error(), "workspace preparation is unavailable") { + if _, err := NewExecutorFactory(config)(t.Context(), prepared(t, preparationRequest())); err == nil || !strings.Contains(err.Error(), "workspace preparation is unavailable") { t.Fatal("old bridge accepted command observations", err) } if _, err := os.Stat(filepath.Join(config.StateDir, "launched")); !os.IsNotExist(err) { @@ -28,7 +28,7 @@ func TestWorkspaceCommandsRequirePackagedFeature(t *testing.T) { func TestWorkspaceCommandFramesKeepStartIdentityAndObservedOutput(t *testing.T) { config := preparationFixture(t, "commands-success") - resource, err := NewExecutorFactory(config)(t.Context(), preparationRequest()) + resource, err := NewExecutorFactory(config)(t.Context(), prepared(t, preparationRequest())) if err != nil { t.Fatal(err) } @@ -77,7 +77,7 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t req := workspaceRequest() req.AgentSessionID = "native-session" out := make(chan proto.Envelope, 32) - s, err := startSingleTurn(ctx, config, req, out) + s, err := startSingleTurn(ctx, config, req, "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -85,7 +85,7 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t frames := map[string][]proto.ToolCallPayload{} failed, done := false, 0 for event := range out { - if event.ID != req.RunID || event.Type == proto.TypeCommandOutput { + if event.ID != "run" || event.Type == proto.TypeCommandOutput { t.Fatal("command frame changed execution identity or fabricated deltas") } switch event.Type { diff --git a/apps/daemon/internal/agent/claudesdk/error_classification_test.go b/apps/daemon/internal/agent/claudesdk/error_classification_test.go index fa946bf92..f5f8a4d67 100644 --- a/apps/daemon/internal/agent/claudesdk/error_classification_test.go +++ b/apps/daemon/internal/agent/claudesdk/error_classification_test.go @@ -21,11 +21,11 @@ func TestClassifiedBridgeFailurePreservesTerminalEvidence(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=classified-" + mode, "GORACE=atexit_sleep_ms=0"}} - req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - s, err := startSingleTurn(ctx, config, req, out) + s, err := startSingleTurn(ctx, config, req, "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index 269098a48..e33dadef5 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -18,13 +18,13 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "native-model", SystemPrompt: "Keep these exact instructions.\nDo not replace them."} - ordinary, _, err := prepareConfiguration(config, request) + ordinary, _, err := prepareConfiguration(config, prepared(t, request)) if err != nil { t.Fatal(err) } request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium"} before, _ := json.Marshal(request) - controlled, _, err := prepareConfiguration(config, request) + controlled, _, err := prepareConfiguration(config, prepared(t, request)) if err != nil { t.Fatal(err) } @@ -39,8 +39,8 @@ func TestMCPWithoutEnvironmentNoneRejectedBeforeSetup(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} - request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Input"), MCPHTTPServers: &servers, Model: "fixture", ModelProvider: fixtureProvider()} - _, err := startSingleTurn(t.Context(), config, request, make(chan proto.Envelope, 1)) + request := proto.PromptRequestPayload{MCPHTTPServers: &servers, Model: "fixture", ModelProvider: fixtureProvider()} + _, err := startSingleTurn(t.Context(), config, request, "run", proto.TextInput("Input"), make(chan proto.Envelope, 1)) if err == nil || !strings.Contains(err.Error(), "service-origin MCP requires a service execution host") { t.Fatal("MCP reached an unsupported environment", err) } @@ -55,7 +55,7 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) { config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} schema := json.RawMessage(`{"type":"object","properties":{"n":{"const":9007199254740992}}}`) request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}} - start, _, err := prepareConfiguration(config, request) + start, _, err := prepareConfiguration(config, prepared(t, request)) if err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestToolDiscoveryPreservesFrozenFunctions(t *testing.T) { {Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"properties":{}}`)}, {Name: "note", Description: "Note", Parameters: json.RawMessage(`{"type":["object","null"]}`)}, }} - start, _, err := prepareConfiguration(config, request) + start, _, err := prepareConfiguration(config, prepared(t, request)) if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions[0], request.FunctionTools[0]) || string(start.Functions[1].Parameters) != `{"properties":{},"type":"object"}` || string(start.Functions[2].Parameters) != `{"type":"object"}` { t.Fatal("function discovery changed native definitions", err) } diff --git a/apps/daemon/internal/agent/claudesdk/executor.go b/apps/daemon/internal/agent/claudesdk/executor.go index 0224611ff..694b8f449 100644 --- a/apps/daemon/internal/agent/claudesdk/executor.go +++ b/apps/daemon/internal/agent/claudesdk/executor.go @@ -33,13 +33,10 @@ func NewExecutorFactory(config Config) agent.ExecutorFactory { config.Workspace = &workspace } checked := &runtimeCheckCache{} - return func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { + return func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { if ctx == nil { ctx = context.Background() } - if err := preparationOnly(req); err != nil { - return nil, err - } start, env, err := prepareConfiguration(config, req) if err != nil { return nil, err @@ -48,13 +45,6 @@ func NewExecutorFactory(config Config) agent.ExecutorFactory { } } -func preparationOnly(req proto.PromptRequestPayload) error { - if req.RunID != "" || len(req.Input) != 0 { - return errors.New("claudesdk: Executor preparation cannot submit input") - } - return nil -} - // startExecutor checks the installed bridge against probe, starts it through // run and waits until it is ready for Turns. func startExecutor(ctx context.Context, checked *runtimeCheckCache, probe Config, start startRequest, run func() (*session, error)) (agent.Executor, error) { diff --git a/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go b/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go index 44d703232..2ae8beda7 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go @@ -17,7 +17,7 @@ func TestExecutorNativeConfirmationSurvivesCleanup(t *testing.T) { if mode == "pending_function" || mode == "pending_function_unconfirmed" { req.FunctionTools = []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`)}} } - owner, err := NewExecutorFactory(config)(t.Context(), req) + owner, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go b/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go index e5bc554ae..9a6392c25 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_fixture_test.go @@ -12,12 +12,15 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -// startSingleTurn prepares an Executor for one Turn and closes it once that -// Turn settles, so each test observes the complete native lifecycle. -func startSingleTurn(ctx context.Context, config Config, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Turn, error) { - run, input := req.RunID, req.Input - req.RunID, req.Input = "", nil - resource, err := NewExecutorFactory(config)(ctx, req) +// startSingleTurn prepares an Executor as the registry does, starts one Turn +// and closes the Executor once that Turn settles, so each test observes the +// complete native lifecycle. +func startSingleTurn(ctx context.Context, config Config, req proto.PromptRequestPayload, run string, input proto.MessageInput, out chan<- proto.Envelope) (agent.Turn, error) { + configuration, err := Declaration.Configuration.Prepare(req) + if err != nil { + return nil, err + } + resource, err := NewExecutorFactory(config)(ctx, agent.PrepareRequest{PromptRequestPayload: req, Prepared: configuration}) if err != nil { return nil, err } diff --git a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go index 46b93d007..cc87b82c5 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go @@ -85,12 +85,12 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { defer persist() request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: model, ModelProvider: provider, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."} factory := NewExecutorFactory(config) - prepared := time.Now() - owner, err := factory(ctx, request) + started := time.Now() + owner, err := factory(ctx, prepared(t, request)) if err != nil { t.Fatal("executor preparation failed", err) } - evidence.PrepareMS = time.Since(prepared).Milliseconds() + evidence.PrepareMS = time.Since(started).Milliseconds() defer func() { closeCtx, stop := context.WithTimeout(context.Background(), 10*time.Second) defer stop() @@ -195,7 +195,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { } request.AgentSessionID = native request.RequireExistingNativeSession = true - owner, err = factory(ctx, request) + owner, err = factory(ctx, prepared(t, request)) if err != nil { t.Fatal("history recovery failed", err) } diff --git a/apps/daemon/internal/agent/claudesdk/executor_test.go b/apps/daemon/internal/agent/claudesdk/executor_test.go index bf043ccf3..4c8870bd7 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_test.go @@ -152,7 +152,7 @@ func awaitExecutorTurn(t *testing.T, turn agent.Turn, out <-chan proto.Envelope, func TestExecutorRetainsProcessAcrossTurnsAndCancellation(t *testing.T) { config, req := persistentConfig(t, "") req.ModelProvider = &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://provider.example/anthropic", APIKey: "fixture-key"} - owner, err := NewExecutorFactory(config)(t.Context(), req) + owner, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } @@ -188,7 +188,7 @@ func TestExecutorRetainsProcessAcrossTurnsAndCancellation(t *testing.T) { } func TestExecutorLateTurnEventInvalidatesWithoutRetargeting(t *testing.T) { config, req := persistentConfig(t, "late") - owner, err := NewExecutorFactory(config)(t.Context(), req) + owner, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } @@ -213,7 +213,7 @@ func TestExecutorCachesReadinessUntilInstalledArtifactChanges(t *testing.T) { config, req := persistentConfig(t, "") factory := NewExecutorFactory(config) for range 2 { - owner, err := factory(t.Context(), req) + owner, err := factory(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } @@ -232,7 +232,7 @@ func TestExecutorCachesReadinessUntilInstalledArtifactChanges(t *testing.T) { if err := os.WriteFile(config.Entrypoint, []byte("version-two-changed"), 0600); err != nil { t.Fatal(err) } - owner, err := factory(t.Context(), req) + owner, err := factory(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } @@ -244,7 +244,7 @@ func TestExecutorCachesReadinessUntilInstalledArtifactChanges(t *testing.T) { func TestExecutorSeparatesPreInputRejectionFromUnknownWrite(t *testing.T) { config, req := persistentConfig(t, "block") - owner, err := NewExecutorFactory(config)(t.Context(), req) + owner, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } @@ -278,7 +278,7 @@ func TestExecutorSeparatesPreInputRejectionFromUnknownWrite(t *testing.T) { func TestExecutorCancellationDeadlineInterruptsBlockedTransport(t *testing.T) { config, req := persistentConfig(t, "block") - owner, err := NewExecutorFactory(config)(t.Context(), req) + owner, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } @@ -314,7 +314,7 @@ func TestExecutorCancellationDeadlineInterruptsBlockedTransport(t *testing.T) { func TestSharedTextLifecycle(t *testing.T) { config, req := persistentConfig(t, "text_contract") - owner, err := NewExecutorFactory(config)(t.Context(), req) + owner, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/functions_test.go b/apps/daemon/internal/agent/claudesdk/functions_test.go index 8178a5fa0..cc0bee53d 100644 --- a/apps/daemon/internal/agent/claudesdk/functions_test.go +++ b/apps/daemon/internal/agent/claudesdk/functions_test.go @@ -21,11 +21,11 @@ func TestFunctionTurnNativeReceipts(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions", FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions", FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := startSingleTurn(ctx, config, request, out) + running, err := startSingleTurn(ctx, config, request, "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/harness_config_test.go b/apps/daemon/internal/agent/claudesdk/harness_config_test.go index b2f969773..9d20e2cc5 100644 --- a/apps/daemon/internal/agent/claudesdk/harness_config_test.go +++ b/apps/daemon/internal/agent/claudesdk/harness_config_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" ) func TestHarnessConfigReachesBridge(t *testing.T) { @@ -14,7 +13,7 @@ func TestHarnessConfigReachesBridge(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "fixture", HarnessConfig: proto.HarnessConfig(`{"effort":"high","thinking":{"type":"enabled","budgetTokens":1024}}`)} - start, _, err := prepareConfiguration(config, req) + start, _, err := prepareConfiguration(config, prepared(t, req)) if err != nil { t.Fatal(err) } @@ -33,8 +32,4 @@ func TestHarnessConfigReachesBridge(t *testing.T) { if applied["effort"] != "high" || applied["thinking"].(map[string]any)["budgetTokens"] != float64(1024) { t.Fatal("bridge lost native configuration") } - req.HarnessConfig = proto.HarnessConfig(`{"env":{"ANTHROPIC_BASE_URL":"bypass"}}`) - if _, _, err = prepareConfiguration(config, req); err != harnessconfig.ErrHarnessConfig { - t.Fatalf("provider override accepted: %v", err) - } } diff --git a/apps/daemon/internal/agent/claudesdk/live_linux_test.go b/apps/daemon/internal/agent/claudesdk/live_linux_test.go index dea9bb66d..2245b4100 100644 --- a/apps/daemon/internal/agent/claudesdk/live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/live_linux_test.go @@ -138,12 +138,12 @@ func TestLiveClaudeSDKTextResume(t *testing.T) { requestStart := len(requests) mu.Unlock() out := make(chan proto.Envelope, 64) - request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Answer briefly and preserve the exact verification value in the conversation. Use no tools."} + request := proto.PromptRequestPayload{AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Answer briefly and preserve the exact verification value in the conversation. Use no tools."} if success != nil { request.SystemPrompt = "Call lookup exactly once as requested, then report both result parts and any prior verification value. Never retry a failed tool." request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} } - running, err := startSingleTurn(ctx, config, request, out) + running, err := startSingleTurn(ctx, config, request, uuid.NewString(), proto.TextInput(prompt), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/local_test.go b/apps/daemon/internal/agent/claudesdk/local_test.go index 8ad52ac55..86ea1677a 100644 --- a/apps/daemon/internal/agent/claudesdk/local_test.go +++ b/apps/daemon/internal/agent/claudesdk/local_test.go @@ -12,24 +12,21 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) -func TestLocalWorkspaceBindingNetworkAndRequiredHistory(t *testing.T) { +func TestLocalWorkspaceBindingAndRequiredHistory(t *testing.T) { config := workspaceFixture(t) config.Workspace.PublicDirectory = config.Workspace.Directory config.Workspace.NetworkAccess = "enabled" req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory} + req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment"} req.RequireExistingNativeSession = true - start, _, err := prepareConfiguration(config, req) + bound := prepared(t, req) + bound.WorkspaceRoot = config.Workspace.Directory + start, _, err := prepareConfiguration(config, bound) if err != nil || !start.RequireHistory || start.Workspace.NetworkAccess != "enabled" { t.Fatal(start, err) } - req.LocalEnvironment.NetworkAccess = "disabled" - if _, _, err := prepareConfiguration(config, req); err == nil { - t.Fatal("accepted different Runtime network policy") - } - req.LocalEnvironment.NetworkAccess = "enabled" config.Workspace.PublicDirectory = config.Workspace.HomeDir - if _, _, err := prepareConfiguration(config, req); err == nil { + if _, _, err := prepareConfiguration(config, bound); err == nil { t.Fatal("accepted a different public workspace") } alias := filepath.Join(filepath.Dir(config.Workspace.Directory), "alias") @@ -37,27 +34,18 @@ func TestLocalWorkspaceBindingNetworkAndRequiredHistory(t *testing.T) { t.Fatal(err) } config.Workspace.PublicDirectory = alias - if _, _, err := prepareConfiguration(config, req); err != nil { + if _, _, err := prepareConfiguration(config, bound); err != nil { t.Fatal("same workspace alias rejected", err) } - } -func TestRestrictedWorkspacePolicyUsesExactBoundAuthority(t *testing.T) { +func TestRestrictedWorkspacePolicyIsRejected(t *testing.T) { config := workspaceFixture(t) config.Workspace.NetworkAccess = "restricted" - config.Workspace.AllowedDomains = []string{"Example.com", "api.example.com", "example.com"} - req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "restricted", AllowedDomains: []string{"api.example.com", "EXAMPLE.COM"}, WorkspaceRoot: config.Workspace.Directory} - if _, _, err := prepareConfiguration(config, req); err == nil { + config.Workspace.AllowedDomains = []string{"api.example.com"} + if _, _, err := prepareConfiguration(config, prepared(t, workspaceRequest())); err == nil { t.Fatal("Runtime must not promise inner network isolation") } - for _, domains := range [][]string{{"example.com"}, {"example.org"}, nil} { - req.LocalEnvironment.AllowedDomains = domains - if _, _, err := prepareConfiguration(config, req); err == nil { - t.Fatal("different policy entered bound Runtime", domains) - } - } } func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { @@ -65,7 +53,7 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { original := slices.Clone(config.Env) req := workspaceRequest() req.ModelProvider = &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://provider.example/anthropic", APIKey: "selected-secret"} - start, env, err := prepareConfiguration(config, req) + start, env, err := prepareConfiguration(config, prepared(t, req)) if err != nil { t.Fatal(err) } @@ -76,10 +64,4 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { if !slices.Contains(env, "ANTHROPIC_API_KEY=selected-secret") || slices.ContainsFunc(env, func(entry string) bool { return strings.HasPrefix(entry, "ANTHROPIC_AUTH_TOKEN=") }) { t.Fatal("provider selection was not exclusive") } - for _, baseURL := range []string{"http://provider.example", "https://user:pass@provider.example"} { - req.ModelProvider = &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: baseURL, APIKey: "secret"} - if _, _, err := prepareConfiguration(config, req); err == nil || strings.Contains(err.Error(), "secret") { - t.Fatal("unsafe provider accepted or disclosed") - } - } } diff --git a/apps/daemon/internal/agent/claudesdk/mcp.go b/apps/daemon/internal/agent/claudesdk/mcp.go index 854a8fbff..7185da3bb 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp.go +++ b/apps/daemon/internal/agent/claudesdk/mcp.go @@ -13,7 +13,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func validateMCP(req proto.PromptRequestPayload) error { +func validateMCP(req agent.PrepareRequest) error { if req.MCPHTTPServers == nil { return nil } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go b/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go index 19684da9f..250de7702 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -25,7 +26,7 @@ func TestMCPBearerUsesFreshOwnedEnvironmentReferences(t *testing.T) { req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} seen := map[string]bool{} for range 2 { - start, env, err := prepareConfiguration(config, req) + start, env, err := prepareConfiguration(config, prepared(t, req)) if err != nil { t.Fatal(err) } @@ -67,7 +68,7 @@ func TestMCPBearerRejectsInvalidCredentialBeforeStateCreation(t *testing.T) { config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", BearerToken: &token}} req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, Model: "fixture"} - if _, _, err := prepareConfiguration(config, req); err == nil || err.Error() != "claudesdk: unsupported HTTPS MCP bearer credential" { + if _, _, err := prepareConfiguration(config, prepared(t, req)); err == nil || err.Error() != "claudesdk: unsupported HTTPS MCP bearer credential" { t.Fatal("invalid bearer accepted or unsafe error returned") } entries, err := os.ReadDir(root) @@ -78,7 +79,7 @@ func TestMCPBearerRejectsInvalidCredentialBeforeStateCreation(t *testing.T) { for _, url := range []string{"http://example.invalid/mcp", "https://example.invalid/mcp#", "https://example.invalid/mcp?", "https://user:secret@example.invalid/mcp"} { token := "synthetic-token" servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: url, BearerToken: &token}} - if err := validateMCP(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &servers}); err == nil { + if err := validateMCP(agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &servers}}); err == nil { t.Fatal("unsafe authenticated endpoint accepted") } } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment.go b/apps/daemon/internal/agent/claudesdk/mcp_environment.go index 99076465b..e107d85d9 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment.go @@ -16,7 +16,7 @@ type environmentMCPServer struct { Args []string `json:"args,omitempty"` } -func prepareRuntimeMCP(req proto.PromptRequestPayload) ([]environmentMCPServer, []string, error) { +func prepareRuntimeMCP(req agent.PrepareRequest) ([]environmentMCPServer, []string, error) { bindings, err := agent.ResolveMCPBindings(req) if err != nil { return nil, nil, err @@ -27,7 +27,7 @@ func prepareRuntimeMCP(req proto.PromptRequestPayload) ([]environmentMCPServer, // mcpServers renders resolved bindings, each stdio binding with the command // and arguments stdio gives it and each credential in a private environment // variable. -func mcpServers(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) ([]environmentMCPServer, []string, error) { +func mcpServers(bindings []agent.MCPBinding, stdio func(agent.EnvironmentMCP) (string, []string)) ([]environmentMCPServer, []string, error) { var servers []environmentMCPServer var env []string for _, binding := range bindings { diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go index 72e02a64b..ac8800552 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" ) @@ -16,15 +17,18 @@ func TestEnvironmentMCPUsesInstalledLauncherAndSelectedCredential(t *testing.T) req := workspaceRequest() token := "selected-user-token" t.Setenv("MCP_TOKEN", "unselected-native-token") - req.LocalEnvironment = &proto.LocalEnvironment{CapabilityRoot: "/private/runtime/capabilities", NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory, MCP: []proto.EnvironmentMCP{ + req.LocalEnvironment = &proto.LocalEnvironment{} + bound := prepared(t, req) + bound.CapabilityRoot, bound.WorkspaceRoot = "/private/runtime/capabilities", config.Workspace.Directory + bound.MCP = []agent.EnvironmentMCP{ {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/local", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "untrusted-package-command", Args: []string{"package-argument"}, EnvVars: []string{"MCP_TOKEN"}}}, {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp", BearerTokenEnvVar: "MCP_TOKEN"}, BearerToken: &token}, - }} - start, env, err := prepareConfiguration(config, req) + } + start, env, err := prepareConfiguration(config, bound) if err != nil { t.Fatal(err) } - if start.MCPHTTPServers != nil || len(start.Workspace.MCP) != 2 || start.Workspace.CapabilityRoot != req.LocalEnvironment.CapabilityRoot { + if start.MCPHTTPServers != nil || len(start.Workspace.MCP) != 2 || start.Workspace.CapabilityRoot != bound.CapabilityRoot { t.Fatal("environment declarations changed authority") } stdio := start.Workspace.MCP[0] @@ -51,21 +55,20 @@ func TestEnvironmentMCPUsesInstalledLauncherAndSelectedCredential(t *testing.T) } func TestEnvironmentMCPRejectsUnqualifiedCombinations(t *testing.T) { - for _, mutate := range []func(*proto.LocalEnvironment){ - func(e *proto.LocalEnvironment) { e.NetworkAccess = "restricted" }, - func(e *proto.LocalEnvironment) { e.MCP = append(e.MCP, e.MCP[0]) }, - func(e *proto.LocalEnvironment) { e.MCP[0].Server.Type = "sse" }, - func(e *proto.LocalEnvironment) { e.MCP[0].Server.HTTPHeaders = map[string]string{"X-Key": "literal"} }, - func(e *proto.LocalEnvironment) { e.MCP[0].Server.BearerTokenEnvVar = "MISSING" }, - func(e *proto.LocalEnvironment) { + for _, mutate := range []func(*agent.PrepareRequest){ + func(r *agent.PrepareRequest) { r.MCP = append(r.MCP, r.MCP[0]) }, + func(r *agent.PrepareRequest) { r.MCP[0].Server.Type = "sse" }, + func(r *agent.PrepareRequest) { r.MCP[0].Server.HTTPHeaders = map[string]string{"X-Key": "literal"} }, + func(r *agent.PrepareRequest) { r.MCP[0].Server.BearerTokenEnvVar = "MISSING" }, + func(r *agent.PrepareRequest) { token := "token" - e.MCP[0].BearerToken = &token - e.MCP[0].Server.URL = "http://example.invalid/mcp" + r.MCP[0].BearerToken = &token + r.MCP[0].Server.URL = "http://example.invalid/mcp" }, } { - environment := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}}}} - mutate(environment) - if _, _, err := prepareRuntimeMCP(proto.PromptRequestPayload{LocalEnvironment: environment}); err == nil { + req := agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}}, MCP: []agent.EnvironmentMCP{{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}}}} + mutate(&req) + if _, _, err := prepareRuntimeMCP(req); err == nil { t.Fatal("unsupported declaration accepted") } } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_test.go b/apps/daemon/internal/agent/claudesdk/mcp_test.go index 6dbb5037c..1abb522f8 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_test.go @@ -41,7 +41,7 @@ func TestHTTPMCPDeclaration(t *testing.T) { case "environment": req.DisableExecutionEnvironment = false } - start, _, err := prepareConfiguration(config, req) + start, _, err := prepareConfiguration(config, prepared(t, req)) valid := mode == "unrestricted" || mode == "selected" || mode == "empty" || mode == "nil-slice" || mode == "auth" || mode == "required" if (err == nil) != valid { t.Fatalf("unexpected admission: %v", err) diff --git a/apps/daemon/internal/agent/claudesdk/messages_test.go b/apps/daemon/internal/agent/claudesdk/messages_test.go index 241b68176..ef392280f 100644 --- a/apps/daemon/internal/agent/claudesdk/messages_test.go +++ b/apps/daemon/internal/agent/claudesdk/messages_test.go @@ -20,11 +20,11 @@ func TestMessageObservations(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := startSingleTurn(ctx, config, request, out) + running, err := startSingleTurn(ctx, config, request, "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/model_route_test.go b/apps/daemon/internal/agent/claudesdk/model_route_test.go deleted file mode 100644 index 72f28b550..000000000 --- a/apps/daemon/internal/agent/claudesdk/model_route_test.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build unix - -package claudesdk - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" -) - -func TestExecutorRejectsNonNativeFrozenProviderBeforeStartup(t *testing.T) { - for _, protocol := range []modelprovider.Protocol{modelprovider.Responses, modelprovider.ChatCompletions} { - t.Run(string(protocol), func(t *testing.T) { - config, req := persistentConfig(t, "complete") - req.ModelProvider = &modelprovider.Provider{Protocol: protocol, BaseURL: "https://model.invalid/v1", APIKey: "private-sentinel"} - resource, err := NewExecutorFactory(config)(t.Context(), req) - if resource != nil || err == nil || !strings.Contains(err.Error(), "does not support") || strings.Contains(err.Error(), "private-sentinel") { - t.Fatalf("non-native provider acquired native ownership: %v", err) - } - if _, err := os.Stat(filepath.Join(filepath.Dir(config.Entrypoint), "probes")); !os.IsNotExist(err) { - t.Fatal("unsupported snapshot reached native readiness") - } - }) - } -} diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index c4bd96dfd..875197992 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -6,9 +6,9 @@ import ( "path/filepath" "strings" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/claudesdk" ) type Config struct { @@ -39,7 +39,7 @@ type startRequest struct { RequireHistory bool `json:"require_history,omitempty"` } -func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { +func prepareConfiguration(config Config, req agent.PrepareRequest) (startRequest, []string, error) { start, provider, err := prepareOptions(req) if err != nil { return startRequest{}, nil, err @@ -94,15 +94,12 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR // prepareOptions renders the request's execution configuration and the // selected model provider. The registered factory already admitted the // selection against the declaration. -func prepareOptions(req proto.PromptRequestPayload) (startRequest, []string, error) { +func prepareOptions(req agent.PrepareRequest) (startRequest, []string, error) { start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, ToolSearch: req.ToolSearch} fail := func(reason string) (startRequest, []string, error) { return startRequest{}, nil, fmt.Errorf("claudesdk: %s", reason) } - modelConfiguration, err := harnessconfiguration.Configuration().Prepare(req) - if err != nil { - return startRequest{}, nil, err - } + modelConfiguration := req.Prepared start.NativeModelOptions = compileNativeModelOptions(modelConfiguration.HarnessConfig) if err := validateMCP(req); err != nil { return startRequest{}, nil, err @@ -128,9 +125,11 @@ func prepareOptions(req proto.PromptRequestPayload) (startRequest, []string, err } start.Subagents = &subagentOptions{MaxConcurrent: limit} } - if start.Functions, err = functionTools(req.FunctionTools); err != nil { + functions, err := functionTools(req.FunctionTools) + if err != nil { return startRequest{}, nil, err } + start.Functions = functions start.Model, start.SystemPrompt = modelConfiguration.Model, req.SystemPrompt // The key renders as ANTHROPIC_API_KEY, which Claude Code sends as the // X-Api-Key header that the anthropic protocol declares. diff --git a/apps/daemon/internal/agent/claudesdk/options_test.go b/apps/daemon/internal/agent/claudesdk/options_test.go index 5535b6191..afd9971a9 100644 --- a/apps/daemon/internal/agent/claudesdk/options_test.go +++ b/apps/daemon/internal/agent/claudesdk/options_test.go @@ -1,45 +1,39 @@ package claudesdk import ( - "errors" - "os" "path/filepath" + "slices" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) -// A request needs a model and a provider: device credentials never stand in. -func TestModelAndProviderAreRequired(t *testing.T) { +// Device credentials never stand in for the prepared provider. +func TestPreparedModelAndProviderReachTheBridge(t *testing.T) { t.Setenv("ANTHROPIC_API_KEY", "device-key") - for _, tc := range []struct { - name string - req proto.PromptRequestPayload - want string - err error - }{ - {"no system prompt", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "test-model"}, "", nil}, - {"system prompt", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "test-model", SystemPrompt: "instructions"}, "instructions", nil}, - {"no model", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), SystemPrompt: "instructions"}, "", harnessconfig.ErrModel}, - {"no provider", proto.PromptRequestPayload{Model: "test-model"}, "", harnessconfig.ErrModelProvider}, - } { - t.Run(tc.name, func(t *testing.T) { - root := t.TempDir() - t.Setenv("OAC_RUNTIME_HOME", root) - config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} - start, _, err := prepareConfiguration(config, tc.req) - if !errors.Is(err, tc.err) || (err == nil) != (tc.err == nil) || start.SystemPrompt != tc.want { - t.Fatalf("system prompt %q, error %v", start.SystemPrompt, err) - } - if _, statErr := os.Stat(config.StateDir); tc.err != nil && !os.IsNotExist(statErr) { - t.Fatal("rejected request created native state") - } - }) + for _, prompt := range []string{"", "instructions"} { + root := t.TempDir() + t.Setenv("OAC_RUNTIME_HOME", root) + config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} + start, env, err := prepareConfiguration(config, prepared(t, proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "test-model", SystemPrompt: prompt})) + if err != nil || start.Model != "test-model" || start.SystemPrompt != prompt || !slices.Contains(env, "ANTHROPIC_API_KEY=fixture-key") { + t.Fatalf("model %q, system prompt %q, error %v", start.Model, start.SystemPrompt, err) + } } } +// prepared is req as the registry hands it to the factory. +func prepared(t testing.TB, req proto.PromptRequestPayload) agent.PrepareRequest { + t.Helper() + configuration, err := Declaration.Configuration.Prepare(req) + if err != nil { + t.Fatal(err) + } + return agent.PrepareRequest{PromptRequestPayload: req, Prepared: configuration} +} + // fixtureProvider is the provider every Claude request carries. func fixtureProvider() *modelprovider.Provider { return &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://model.example", APIKey: "fixture-key"} diff --git a/apps/daemon/internal/agent/claudesdk/preparation_fixture_test.go b/apps/daemon/internal/agent/claudesdk/preparation_fixture_test.go index e387cfa17..65b9baf85 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_fixture_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_fixture_test.go @@ -30,7 +30,6 @@ func preparationFixture(t *testing.T, mode string) Config { func preparationRequest() proto.PromptRequestPayload { req := workspaceRequest() - req.RunID, req.Input = "", nil req.AgentSessionID = "native-session" return req } diff --git a/apps/daemon/internal/agent/claudesdk/preparation_test.go b/apps/daemon/internal/agent/claudesdk/preparation_test.go index 861373fa1..255a15222 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_test.go @@ -24,7 +24,7 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) { result := make(chan agent.Executor, 1) failed := make(chan error, 1) go func() { - e, err := NewExecutorFactory(config)(ctx, req) + e, err := NewExecutorFactory(config)(ctx, prepared(t, req)) if err != nil { failed <- err return @@ -96,18 +96,12 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) { } } -func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) { - for _, name := range []string{"run", "prompt", "attachments", "subagents", "none", "functions", "mcp", "old-runtime"} { +func TestPreparationRejectsUnavailableProfilesBeforeLaunch(t *testing.T) { + for _, name := range []string{"subagents", "none", "functions", "mcp", "old-runtime"} { t.Run(name, func(t *testing.T) { config := preparationFixture(t, name) req := preparationRequest() switch name { - case "run": - req.RunID = "unexpected" - case "prompt": - req.Input = proto.TextInput("unexpected") - case "attachments": - req.Input = proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image"}}}} case "subagents": req.ObserveSubagentIdentities = true case "none": @@ -117,7 +111,7 @@ func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) case "mcp": req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} } - if _, err := NewExecutorFactory(config)(t.Context(), req); err == nil { + if _, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)); err == nil { t.Fatal("invalid preparation was accepted") } if _, err := os.Stat(filepath.Join(config.StateDir, "launched")); !os.IsNotExist(err) { @@ -133,7 +127,7 @@ func TestPreparationFailureAndUnusedRelease(t *testing.T) { config := preparationFixture(t, mode) owner, stop := context.WithCancel(t.Context()) defer stop() - resource, err := NewExecutorFactory(config)(owner, preparationRequest()) + resource, err := NewExecutorFactory(config)(owner, prepared(t, preparationRequest())) if mode == "history-missing" || mode == "invalid-receipt" { if err == nil || mode == "history-missing" && !strings.Contains(err.Error(), "history_unavailable") { t.Fatal("preparation failure was lost", err) diff --git a/apps/daemon/internal/agent/claudesdk/readiness_test.go b/apps/daemon/internal/agent/claudesdk/readiness_test.go index 286f90799..cd5ec52ed 100644 --- a/apps/daemon/internal/agent/claudesdk/readiness_test.go +++ b/apps/daemon/internal/agent/claudesdk/readiness_test.go @@ -23,9 +23,9 @@ func TestRequiredMCPNeedsQualifiedRuntime(t *testing.T) { config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state"), Env: []string{ "GO_CLAUDE_READINESS_HELPER=1", "READINESS_MODE=ready-http-mcp", "GORACE=atexit_sleep_ms=0", }} - req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, Model: "fixture", MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", Required: true}}} - if _, err := startSingleTurn(t.Context(), config, req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support required HTTP MCP" { + if _, err := startSingleTurn(t.Context(), config, req, "run", proto.TextInput("hello"), make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support required HTTP MCP" { t.Fatalf("unqualified runtime executed required MCP: %v", err) } } @@ -44,9 +44,9 @@ func TestHTTPMCPRejectsOldPackagedRuntime(t *testing.T) { if !info.SupportsHTTPMCP() { t.Fatal("runtime feature not recognized") } - req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, Model: "fixture", MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp"}}} - if _, err := startSingleTurn(t.Context(), config, req, make(chan proto.Envelope, 1)); err == nil || !strings.Contains(err.Error(), "packaged runtime does not support HTTP MCP") { + if _, err := startSingleTurn(t.Context(), config, req, "run", proto.TextInput("hello"), make(chan proto.Envelope, 1)); err == nil || !strings.Contains(err.Error(), "packaged runtime does not support HTTP MCP") { t.Fatalf("old runtime was not rejected before execution: %v", err) } } @@ -81,9 +81,9 @@ func TestMCPBearerRejectsAnonymousOnlyRuntimeWithoutProbeSecrets(t *testing.T) { "GO_CLAUDE_READINESS_HELPER=1", "READINESS_MODE=ready-http-mcp", "GORACE=atexit_sleep_ms=0", }} token := "private-fixture-token" - req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, + req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, Model: "fixture", MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", BearerToken: &token}}} - if _, err := startSingleTurn(t.Context(), config, req, make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support authenticated HTTP MCP" { + if _, err := startSingleTurn(t.Context(), config, req, "run", proto.TextInput("hello"), make(chan proto.Envelope, 1)); err == nil || err.Error() != "claudesdk: packaged runtime does not support authenticated HTTP MCP" { t.Fatalf("old runtime executed authenticated request or readiness received its secret: %v", err) } } diff --git a/apps/daemon/internal/agent/claudesdk/restrictions_test.go b/apps/daemon/internal/agent/claudesdk/restrictions_test.go index 72d7d56c8..9232c4882 100644 --- a/apps/daemon/internal/agent/claudesdk/restrictions_test.go +++ b/apps/daemon/internal/agent/claudesdk/restrictions_test.go @@ -25,11 +25,11 @@ func TestTextTurnAcceptsRestrictiveCapabilities(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=success", "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "restricted-run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", DisableExecutionEnvironment: test.environment, DisableSubagents: test.subagents, ExecutionControls: test.controls, Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", DisableExecutionEnvironment: test.environment, DisableSubagents: test.subagents, ExecutionControls: test.controls, Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := startSingleTurn(ctx, config, request, out) + running, err := startSingleTurn(ctx, config, request, "restricted-run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/session_test.go b/apps/daemon/internal/agent/claudesdk/session_test.go index 6fb37780c..9069f23dd 100644 --- a/apps/daemon/internal/agent/claudesdk/session_test.go +++ b/apps/daemon/internal/agent/claudesdk/session_test.go @@ -22,11 +22,11 @@ func TestTextTurnCompletionAndFailures(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - s, err := startSingleTurn(ctx, config, request, out) + s, err := startSingleTurn(ctx, config, request, "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -74,14 +74,14 @@ func TestUnsupportedRequestRejectedBeforeLaunch(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), Model: "fake"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "fake"} switch kind { case "tool": request.FunctionTools = []proto.FunctionTool{{}} case "outside": config.StateDir = filepath.Dir(root) } - _, err := startSingleTurn(context.Background(), config, request, make(chan proto.Envelope, 1)) + _, err := startSingleTurn(context.Background(), config, request, "run", proto.TextInput("hello"), make(chan proto.Envelope, 1)) if err == nil || !strings.HasPrefix(err.Error(), "claudesdk:") { t.Fatalf("expected pre-launch rejection, got %v", err) } diff --git a/apps/daemon/internal/agent/claudesdk/steering_test.go b/apps/daemon/internal/agent/claudesdk/steering_test.go index ed3b12364..d855838c7 100644 --- a/apps/daemon/internal/agent/claudesdk/steering_test.go +++ b/apps/daemon/internal/agent/claudesdk/steering_test.go @@ -27,11 +27,11 @@ func TestSteeringReceiptsAndLifecycle(t *testing.T) { if mode == "phased" { config.Env[1] = "SDK_HELPER_MODE=steering-timeout" } - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - running, err := startSingleTurn(ctx, config, request, out) + running, err := startSingleTurn(ctx, config, request, "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/subagents_test.go b/apps/daemon/internal/agent/claudesdk/subagents_test.go index 4293d38f1..6475ad9e4 100644 --- a/apps/daemon/internal/agent/claudesdk/subagents_test.go +++ b/apps/daemon/internal/agent/claudesdk/subagents_test.go @@ -9,18 +9,18 @@ import ( func TestSubagentConfigurationUsesExplicitRequestAndFrozenLimit(t *testing.T) { config := workspaceFixture(t) req := workspaceRequest() - start, _, err := prepareConfiguration(config, req) + start, _, err := prepareConfiguration(config, prepared(t, req)) if err != nil || start.Subagents != nil { t.Fatal("ordinary execution changed", err) } req.DisableSubagents, req.ObserveSubagentIdentities = false, true - start, _, err = prepareConfiguration(config, req) + start, _, err = prepareConfiguration(config, prepared(t, req)) if err != nil || start.Subagents == nil || start.Subagents.MaxConcurrent != 6 { t.Fatal("missing default native admission limit", err) } limit := 2 req.MaxConcurrentSubagents = &limit - start, _, err = prepareConfiguration(config, req) + start, _, err = prepareConfiguration(config, prepared(t, req)) limit = 4 if err != nil || start.Subagents.MaxConcurrent != 2 { t.Fatal("subagent configuration was not frozen", err) @@ -36,7 +36,7 @@ func TestSubagentConfigurationRejectsUnqualifiedAuthority(t *testing.T) { req := workspaceRequest() req.DisableSubagents, req.ObserveSubagentIdentities = false, true change(&req) - if _, _, err := prepareConfiguration(config, req); err == nil { + if _, _, err := prepareConfiguration(config, prepared(t, req)); err == nil { t.Fatal("unqualified subagent combination accepted") } } diff --git a/apps/daemon/internal/agent/claudesdk/tool_environment_test.go b/apps/daemon/internal/agent/claudesdk/tool_environment_test.go index 7b53ca139..c809688bd 100644 --- a/apps/daemon/internal/agent/claudesdk/tool_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/tool_environment_test.go @@ -17,8 +17,10 @@ func TestSelfHostedToolEnvironment(t *testing.T) { } t.Setenv("OAC_RUNTIME_TOOL_ENV_FILE", file) req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory} - profile, _, err := prepareWorkspace(config, req) + req.LocalEnvironment = &proto.LocalEnvironment{} + bound := prepared(t, req) + bound.WorkspaceRoot = config.Workspace.Directory + profile, _, err := prepareWorkspace(config, bound) if err != nil { t.Fatal(err) } @@ -28,7 +30,7 @@ func TestSelfHostedToolEnvironment(t *testing.T) { if err := os.WriteFile(file, []byte(`[]`), 0600); err != nil { t.Fatal(err) } - if _, _, err := prepareWorkspace(config, req); err == nil { + if _, _, err := prepareWorkspace(config, bound); err == nil { t.Fatal("invalid explicit tool configuration was ignored") } } diff --git a/apps/daemon/internal/agent/claudesdk/usage_test.go b/apps/daemon/internal/agent/claudesdk/usage_test.go index 94cd210bd..420119b75 100644 --- a/apps/daemon/internal/agent/claudesdk/usage_test.go +++ b/apps/daemon/internal/agent/claudesdk/usage_test.go @@ -23,11 +23,11 @@ func TestUsageTransportPreservesSnapshotOnFailureAndDone(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Node: os.Args[0], Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state"), Env: []string{"GO_CLAUDE_SDK_HELPER=1", "SDK_HELPER_MODE=usage-" + mode, "GORACE=atexit_sleep_ms=0"}} - request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "usage-run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} + request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentSessionID: "native-session", Model: "fake-model", SystemPrompt: "instructions"} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out := make(chan proto.Envelope, 16) - s, err := startSingleTurn(ctx, config, request, out) + s, err := startSingleTurn(ctx, config, request, "usage-run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -36,7 +36,7 @@ func TestUsageTransportPreservesSnapshotOnFailureAndDone(t *testing.T) { var done proto.DonePayload var kinds []string for event := range out { - if event.ID != request.RunID { + if event.ID != "usage-run" { t.Fatal("usage escaped run identity") } kinds = append(kinds, event.Type) diff --git a/apps/daemon/internal/agent/claudesdk/view.go b/apps/daemon/internal/agent/claudesdk/view.go index 42567ae56..bbbb7839e 100644 --- a/apps/daemon/internal/agent/claudesdk/view.go +++ b/apps/daemon/internal/agent/claudesdk/view.go @@ -15,7 +15,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/viewloader" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // In an agent-host view, node, the bridge and the SDK's native claude run from @@ -91,13 +90,10 @@ func declareView(probe Config, info RuntimeInfo, node, root, bridge string, load func newViewExecutorFactory(probe Config, layout viewLayout) agent.ViewExecutorFactory { checked := &runtimeCheckCache{} - return func(ctx context.Context, req proto.PromptRequestPayload, view agent.ViewSession) (agent.Executor, error) { + return func(ctx context.Context, req agent.PrepareRequest, view agent.ViewSession) (agent.Executor, error) { if ctx == nil { ctx = context.Background() } - if err := preparationOnly(req); err != nil { - return nil, err - } start, env, err := prepareView(layout, req, view) if err != nil { return nil, err @@ -112,14 +108,14 @@ func newViewExecutorFactory(probe Config, layout viewLayout) agent.ViewExecutorF // the view: the workspace profile in the sandbox's workspace, or no workspace // in the work directory with environment none. MCP comes only from the view, // and the environment is closed. -func prepareView(layout viewLayout, req proto.PromptRequestPayload, view agent.ViewSession) (startRequest, []string, error) { +func prepareView(layout viewLayout, req agent.PrepareRequest, view agent.ViewSession) (startRequest, []string, error) { environment := req.LocalEnvironment - if (environment == nil) != req.DisableExecutionEnvironment || environment != nil && !workspacePathSyntax(environment.WorkspaceRoot) || view.Launch == nil || view.Proxy == "" { + if (environment == nil) != req.DisableExecutionEnvironment || environment != nil && !workspacePathSyntax(req.WorkspaceRoot) || view.Launch == nil || view.Proxy == "" { return startRequest{}, nil, errors.New("claudesdk: a view Executor requires the sandbox workspace or environment none, Launch and the gateway proxy") } // The gateway adds each credential and header, and the Harness runs each // stdio alias without arguments. - servers, _, err := mcpServers(view.MCP, func(stdio proto.EnvironmentMCP) (string, []string) { return stdio.Server.Command, nil }) + servers, _, err := mcpServers(view.MCP, func(stdio agent.EnvironmentMCP) (string, []string) { return stdio.Server.Command, nil }) if err != nil { return startRequest{}, nil, err } @@ -143,9 +139,10 @@ func prepareView(layout viewLayout, req proto.PromptRequestPayload, view agent.V } return start, env, nil } - profile.NetworkAccess, profile.MCP = environment.NetworkAccess, servers - profile.Skills, profile.CapabilityRoot = environment.Skills, environment.CapabilityRoot - start.Workspace, start.Cwd = profile, environment.WorkspaceRoot + // The workspace network is enabled by construction. + profile.NetworkAccess, profile.MCP = "enabled", servers + profile.Skills, profile.CapabilityRoot = req.Skills, req.CapabilityRoot + start.Workspace, start.Cwd = profile, req.WorkspaceRoot return start, env, nil } diff --git a/apps/daemon/internal/agent/claudesdk/view_test.go b/apps/daemon/internal/agent/claudesdk/view_test.go index c455d1070..cf87aef87 100644 --- a/apps/daemon/internal/agent/claudesdk/view_test.go +++ b/apps/daemon/internal/agent/claudesdk/view_test.go @@ -45,8 +45,9 @@ func TestViewExecutorLaunchesAClosedGatewayEnvironment(t *testing.T) { return startViewBridge(options, requests) }, } - req := proto.PromptRequestPayload{DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{ID: "environment", WorkspaceRoot: "/workspace", NetworkAccess: "enabled"}, - Model: "fixture", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "http://127.0.0.1:17101", APIKey: modelprovider.Placeholder}} + req := prepared(t, proto.PromptRequestPayload{DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{ID: "environment"}, + Model: "fixture", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "http://127.0.0.1:17101", APIKey: modelprovider.Placeholder}}) + req.WorkspaceRoot = "/workspace" executor, err := view.Executor(t.Context(), req, session) if err != nil { t.Fatal(err) @@ -110,12 +111,11 @@ func TestViewExecutorLaunchesAClosedGatewayEnvironment(t *testing.T) { // The Harness gets the installed Skill in the sandbox's capability root // and runs a stdio binding's alias without arguments. docs := session.MCP - session.MCP = []agent.MCPBinding{{ServerLabel: "local", Transport: "stdio", Stdio: &proto.EnvironmentMCP{ + session.MCP = []agent.MCPBinding{{ServerLabel: "local", Transport: "stdio", Stdio: &agent.EnvironmentMCP{ Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: agent.ViewAlias(0)}}}} - installed, local := req, *req.LocalEnvironment - local.CapabilityRoot, local.Skills = agentcapabilities.Directory, []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, + installed := req + installed.CapabilityRoot, installed.Skills = agentcapabilities.Directory, []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, Metadata: agentskill.Metadata{Type: "inline", Name: "review", Description: "Review."}, RelativeRoot: "skills/review", PackageRoot: "skills/review"}} - installed.LocalEnvironment = &local stdio, err := view.Executor(t.Context(), installed, session) if err != nil { t.Fatal(err) @@ -130,7 +130,7 @@ func TestViewExecutorLaunchesAClosedGatewayEnvironment(t *testing.T) { // With environment none the bridge runs without a workspace in the work directory. none := req - none.LocalEnvironment, none.DisableExecutionEnvironment = nil, true + none.LocalEnvironment, none.DisableExecutionEnvironment, none.WorkspaceRoot = nil, true, "" session.MCP = docs noneExecutor, err := view.Executor(t.Context(), none, session) if err != nil { diff --git a/apps/daemon/internal/agent/claudesdk/workspace.go b/apps/daemon/internal/agent/claudesdk/workspace.go index 34a41588c..bd41cd035 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace.go +++ b/apps/daemon/internal/agent/claudesdk/workspace.go @@ -6,10 +6,9 @@ import ( "path/filepath" "strings" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" ) // WorkspaceConfig binds one trusted private placement. It does not create an @@ -39,16 +38,13 @@ type workspaceProfile struct { AllowedDomains []string `json:"allowed_domains,omitempty"` } -func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspaceProfile, []string, error) { +func prepareWorkspace(config Config, req agent.PrepareRequest) (*workspaceProfile, []string, error) { if req.DisableExecutionEnvironment { return nil, nil, fmt.Errorf("claudesdk: workspace profile does not support the requested execution combination") } - if req.LocalEnvironment != nil && req.LocalEnvironment.WorkspaceRoot != config.Workspace.Directory { + if req.LocalEnvironment != nil && req.WorkspaceRoot != config.Workspace.Directory { return nil, nil, fmt.Errorf("claudesdk: workspace root conflicts with the trusted workspace binding") } - if req.LocalEnvironment != nil && !(agentnetwork.Policy{Access: config.Workspace.NetworkAccess, AllowedDomains: config.Workspace.AllowedDomains}).Equal(agentnetwork.Policy{Access: req.LocalEnvironment.NetworkAccess, AllowedDomains: req.LocalEnvironment.AllowedDomains}) { - return nil, nil, fmt.Errorf("claudesdk: local Runtime network policy mismatch") - } profile, env, err := workspaceEnvironment(config) if err != nil { return nil, nil, err @@ -61,8 +57,8 @@ func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspace } if req.LocalEnvironment != nil { - profile.Skills = req.LocalEnvironment.Skills - profile.CapabilityRoot = req.LocalEnvironment.CapabilityRoot + profile.Skills = req.Skills + profile.CapabilityRoot = req.CapabilityRoot } servers, credentials, err := prepareRuntimeMCP(req) if err != nil { diff --git a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go index 2b6971175..92c69c17d 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go @@ -41,7 +41,7 @@ esac t.Fatal("readiness did not receive replacement environment", err) } out := make(chan proto.Envelope, 8) - s, err := startSingleTurn(t.Context(), config, workspaceRequest(), out) + s, err := startSingleTurn(t.Context(), config, workspaceRequest(), "run", proto.TextInput("hello"), out) if err != nil { t.Fatal(err) } @@ -64,7 +64,7 @@ esac if err := os.WriteFile(config.Node, []byte(script), 0o700); err != nil { t.Fatal(err) } - if _, err := startSingleTurn(t.Context(), config, workspaceRequest(), out); err == nil { + if _, err := startSingleTurn(t.Context(), config, workspaceRequest(), "run", proto.TextInput("hello"), out); err == nil { t.Fatal("old packaged bridge accepted workspace execution") } if _, err := os.Stat(filepath.Join(config.StateDir, "unexpected-start")); !os.IsNotExist(err) { diff --git a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go index 778374292..b12385438 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go @@ -88,10 +88,10 @@ func TestLiveClaudeWorkspaceTurns(t *testing.T) { defer cancel() out := make(chan proto.Envelope, 64) req := workspaceRequest() - req.RunID, req.Input, req.AgentSessionID = uuid.NewString(), proto.TextInput(prompt), resume + req.AgentSessionID = resume req.Model, req.SystemPrompt = "MiniMax-M3", "Follow the exact verification instructions using the requested native tools. Preserve conversation facts. No other files, network operations or background work." - proof := evidence{RunID: req.RunID} - running, err := startSingleTurn(ctx, config, req, out) + proof := evidence{RunID: uuid.NewString()} + running, err := startSingleTurn(ctx, config, req, proof.RunID, proto.TextInput(prompt), out) if err != nil { if name == "missing-history" && running == nil && strings.Contains(err.Error(), "history_unavailable") { proof.Failure = err.Error() diff --git a/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go b/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go index 4b77d59ee..86698b323 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_structured_test.go @@ -20,7 +20,7 @@ func TestWorkspaceStructuredPreparationQualificationAndFrozenSchema(t *testing.T req := preparationRequest() schema := `{"type":"object","properties":{"n":{"const":9007199254740992}}}` req.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: json.RawMessage(schema)}} - e, err := NewExecutorFactory(config)(t.Context(), req) + e, err := NewExecutorFactory(config)(t.Context(), prepared(t, req)) if mode == "structured-missing" { if err == nil || !strings.Contains(err.Error(), "workspace structured output") { t.Fatal("unqualified bundle admitted", err) diff --git a/apps/daemon/internal/agent/claudesdk/workspace_test.go b/apps/daemon/internal/agent/claudesdk/workspace_test.go index b343b09a3..7d9504a9d 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_test.go @@ -35,7 +35,7 @@ func workspaceFixture(t *testing.T) Config { } func workspaceRequest() proto.PromptRequestPayload { - return proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("hello"), DisableSubagents: true, + return proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, Model: "fixture"} } @@ -44,7 +44,7 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) { t.Setenv("OAC_TEST_PARENT_SECRET", "parent-only") t.Setenv("ANTHROPIC_API_KEY", "unselected-provider") config.Env = append(config.Env, "ANTHROPIC_BASE_URL=https://unselected.example") - start, env, err := prepareConfiguration(config, workspaceRequest()) + start, env, err := prepareConfiguration(config, prepared(t, workspaceRequest())) if err != nil { t.Fatal(err) } @@ -81,13 +81,13 @@ func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) { for _, name := range []string{"none", "workspace-root", "mcp", "relative", "missing", "ambient-setting", "duplicate-env", "bad-env"} { t.Run(name, func(t *testing.T) { config := workspaceFixture(t) - req := workspaceRequest() + req, root := workspaceRequest(), "" switch name { case "none": req.DisableExecutionEnvironment = true case "workspace-root": config.Workspace.NetworkAccess = "enabled" - req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.ScratchDir} + req.LocalEnvironment, root = &proto.LocalEnvironment{ID: "environment"}, config.Workspace.ScratchDir case "mcp": req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} case "relative": @@ -113,9 +113,10 @@ func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) { config.Env = append(config.Env, "HTTPS_PROXY=http://second.example") case "bad-env": config.Env = append(config.Env, "NO_PROXY=bad\x00value") - } - if _, _, err := prepareConfiguration(config, req); err == nil { + bound := prepared(t, req) + bound.WorkspaceRoot = root + if _, _, err := prepareConfiguration(config, bound); err == nil { t.Fatal("invalid binding or request accepted") } entries, err := os.ReadDir(config.StateDir) @@ -130,7 +131,7 @@ func TestWorkspaceRetainsDeclaredFunctions(t *testing.T) { config := workspaceFixture(t) req := workspaceRequest() req.FunctionTools = []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`)}} - start, _, err := prepareConfiguration(config, req) + start, _, err := prepareConfiguration(config, prepared(t, req)) if err != nil { t.Fatal(err) } @@ -143,11 +144,13 @@ func TestPublicMCPUsesWorkspaceProjectionWithoutCredentialCopy(t *testing.T) { config := workspaceFixture(t) config.Workspace.NetworkAccess = "enabled" req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory} + req.LocalEnvironment = &proto.LocalEnvironment{} token := "vault-selected-canary" tools := []string{"prove"} req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "remote", ServerURL: "https://example.test/mcp", AllowedTools: &tools, Required: true, BearerToken: &token}} - start, env, err := prepareConfiguration(config, req) + bound := prepared(t, req) + bound.WorkspaceRoot = config.Workspace.Directory + start, env, err := prepareConfiguration(config, bound) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/execution_controls_test.go b/apps/daemon/internal/agent/codex/execution_controls_test.go index ca67f6f47..c355fbf05 100644 --- a/apps/daemon/internal/agent/codex/execution_controls_test.go +++ b/apps/daemon/internal/agent/codex/execution_controls_test.go @@ -9,7 +9,7 @@ import ( func TestExecutionControlsSelectNativeSettings(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state"}) + plan, err := BuildSessionPlan(prepared(t, "state", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider()})) if err != nil { t.Fatal(err) } @@ -18,7 +18,7 @@ func TestExecutionControlsSelectNativeSettings(t *testing.T) { t.Fatal("native settings without ExecutionControls", plan.ExtraConfig) } for _, verbosity := range []string{"low", "medium", "high"} { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state", ExecutionControls: &proto.ExecutionControls{TextVerbosity: verbosity}}) + plan, err := BuildSessionPlan(prepared(t, "state", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), ExecutionControls: &proto.ExecutionControls{TextVerbosity: verbosity}})) if err != nil { t.Fatal(err) } @@ -33,7 +33,7 @@ func TestExecutionControlsSelectNativeSettings(t *testing.T) { func TestExecutionControlsRejectIncompleteOrInvalidValues(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) for _, controls := range []proto.ExecutionControls{{}, {TextVerbosity: "invalid"}} { - if plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "state", ExecutionControls: &controls}); err == nil { + if plan, err := BuildSessionPlan(prepared(t, "state", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), ExecutionControls: &controls})); err == nil { plan.Cleanup() t.Fatal("invalid controls accepted", controls) } diff --git a/apps/daemon/internal/agent/codex/executor.go b/apps/daemon/internal/agent/codex/executor.go index 4e531ab76..3e6d72c09 100644 --- a/apps/daemon/internal/agent/codex/executor.go +++ b/apps/daemon/internal/agent/codex/executor.go @@ -25,7 +25,7 @@ type Executor struct { closeMu sync.Mutex } -func PrepareExecutor(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { +func PrepareExecutor(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { e, err := newExecutor(ctx, req, defaultSessionConfig()) if e == nil { return nil, err diff --git a/apps/daemon/internal/agent/codex/executor_native_test.go b/apps/daemon/internal/agent/codex/executor_native_test.go index ff76189ab..37d40a2b4 100644 --- a/apps/daemon/internal/agent/codex/executor_native_test.go +++ b/apps/daemon/internal/agent/codex/executor_native_test.go @@ -49,13 +49,13 @@ func TestExecutorNativeReuse(t *testing.T) { cfg := defaultSessionConfig() cfg.codexBinary = binary cfg.logger = slog.New(slog.DiscardHandler) - req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "executor-native", + req := prepared(t, "executor-native", proto.PromptRequestPayload{ + AgentKind: "codex", DisableExecutionEnvironment: true, DisableSubagents: true, Model: model, ModelProvider: &modelprovider.Provider{BaseURL: endpoint, Protocol: modelprovider.Responses, APIKey: strings.TrimSpace(string(key))}, FunctionTools: []proto.FunctionTool{{Name: "hold", Description: "Wait until the host supplies a result.", Parameters: json.RawMessage("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}")}}, - } + }) ctx, cancel := context.WithTimeout(t.Context(), 4*time.Minute) defer cancel() began := time.Now() diff --git a/apps/daemon/internal/agent/codex/executor_test.go b/apps/daemon/internal/agent/codex/executor_test.go index 7404a823b..422d8447c 100644 --- a/apps/daemon/internal/agent/codex/executor_test.go +++ b/apps/daemon/internal/agent/codex/executor_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "os" "strings" "sync" @@ -13,6 +14,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/contracttest" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -27,7 +29,7 @@ func executorFixture(t *testing.T, mode string) (*Executor, string) { } // testExecutor prepares through the production factory and closes the owner at cleanup. -func testExecutor(t *testing.T, mode string, req proto.PromptRequestPayload, cfg sessionConfig) (*Executor, error) { +func testExecutor(t *testing.T, mode string, req agent.PrepareRequest, cfg sessionConfig) (*Executor, error) { t.Helper() t.Setenv("OAC_TEST_EXECUTOR_MODE", mode) ownerCtx, cancelOwner := context.WithCancel(context.Background()) @@ -372,3 +374,13 @@ func TestExecutorCloseTerminatesAfterMissingCancellationTerminal(t *testing.T) { t.Fatal("original cancellation waiter was abandoned") } } + +func TestSharedTextLifecycle(t *testing.T) { + e, _ := executorFixture(t, "complete") + contracttest.TextLifecycle(t, contracttest.TextFixture{ + Executor: e, + CompleteInput: proto.TextInput("answer"), ActiveInput: proto.TextInput("hold"), SteeringInput: proto.TextInput("continue"), + Ready: func(e proto.Envelope) bool { return e.Type == proto.TypeDelta }, + NativeOwner: func() string { return fmt.Sprint(e.base.rpc.process.Cmd.Process.Pid) }, + }) +} diff --git a/apps/daemon/internal/agent/codex/harness_config_test.go b/apps/daemon/internal/agent/codex/harness_config_test.go index ea06e435d..443fbd970 100644 --- a/apps/daemon/internal/agent/codex/harness_config_test.go +++ b/apps/daemon/internal/agent/codex/harness_config_test.go @@ -8,16 +8,15 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) func TestHarnessConfigAppliedWithoutChangingProvider(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan(proto.PromptRequestPayload{ - RunID: "run", AgentStateKey: "native-config", Model: "fixture", HarnessConfig: proto.HarnessConfig(`{"model_reasoning_effort":"high"}`), + plan, err := BuildSessionPlan(prepared(t, "native-config", proto.PromptRequestPayload{ + Model: "fixture", HarnessConfig: proto.HarnessConfig(`{"model_reasoning_effort":"high"}`), ModelProvider: &modelprovider.Provider{BaseURL: "https://provider.invalid/v1", Protocol: modelprovider.Responses, APIKey: "test-key"}, - }) + })) if err != nil { t.Fatal(err) } @@ -27,13 +26,6 @@ func TestHarnessConfigAppliedWithoutChangingProvider(t *testing.T) { } } -func TestHarnessConfigConflictFailsBeforePreparation(t *testing.T) { - _, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), RunID: "run", HarnessConfig: proto.HarnessConfig(`{"model_provider":"bypass"}`)}) - if err != harnessconfig.ErrHarnessConfig { - t.Fatalf("configuration must fail before filesystem preparation: %v", err) - } -} - func TestHarnessConfigReachesEveryNativeTurn(t *testing.T) { for _, resumeID := range []string{"", "fixture-native-thread"} { t.Run("resume="+resumeID, func(t *testing.T) { @@ -41,6 +33,7 @@ func TestHarnessConfigReachesEveryNativeTurn(t *testing.T) { t.Setenv("OAC_TEST_EXECUTOR_MODE", "complete") req.AgentSessionID = resumeID req.HarnessConfig = proto.HarnessConfig(`{"model_reasoning_effort":"high"}`) + req = prepared(t, req.StateKey, req.PromptRequestPayload) ownerCtx, cancelOwner := context.WithCancel(context.Background()) t.Cleanup(cancelOwner) e, err := newExecutor(ownerCtx, req, cfg) diff --git a/apps/daemon/internal/agent/codex/mcp_environment_test.go b/apps/daemon/internal/agent/codex/mcp_environment_test.go index 0013b3a76..9ab54aaec 100644 --- a/apps/daemon/internal/agent/codex/mcp_environment_test.go +++ b/apps/daemon/internal/agent/codex/mcp_environment_test.go @@ -7,17 +7,18 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" ) func TestEnvironmentMCPProjectsIsolatedStdioAndPrivateHTTPReferences(t *testing.T) { token := "user-token" - local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{ + req := agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}}, MCP: []agent.EnvironmentMCP{ {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "must-not-be-native-command", Args: []string{"private-argument"}}}, {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/1", BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.com/mcp", HTTPHeaders: map[string]string{"X-Key": "literal-${DO_NOT_EXPAND}"}}}, }} - servers, env, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}) + servers, env, err := runtimeMCPServers(req) if err != nil || len(servers) != 2 || len(env) != 2 { t.Fatal("environment declarations were not projected", err) } @@ -49,20 +50,16 @@ func TestEnvironmentMCPProjectsIsolatedStdioAndPrivateHTTPReferences(t *testing. } } -func TestEnvironmentMCPRejectsUnqualifiedNetworkAndCredentialChanges(t *testing.T) { - for _, access := range []string{"", "restricted", "disabled"} { - local := &proto.LocalEnvironment{NetworkAccess: access, MCP: []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "local", Type: "stdio"}}}} - if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}); err == nil { - t.Errorf("unqualified MCP network accepted: %s", access) - } - } +func TestEnvironmentMCPRejectsPlaintextBearerAndCollisions(t *testing.T) { token := "user-token" - local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "http://example.com/mcp"}}}} - if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}); err == nil { + req := agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}}, + MCP: []agent.EnvironmentMCP{{BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "http://example.com/mcp"}}}} + if _, _, err := runtimeMCPServers(req); err == nil { t.Fatal("plaintext bearer accepted") } - local.MCP[0].Server.URL = "https://example.com/mcp" - if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local, MCPHTTPServers: &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.com/mcp"}}}); err == nil { + req.MCP[0].Server.URL = "https://example.com/mcp" + req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.com/mcp"}} + if _, _, err := runtimeMCPServers(req); err == nil { t.Fatal("service and environment identity collision accepted") } } diff --git a/apps/daemon/internal/agent/codex/mcp_http.go b/apps/daemon/internal/agent/codex/mcp_http.go index 84dd40dbd..4109a604c 100644 --- a/apps/daemon/internal/agent/codex/mcp_http.go +++ b/apps/daemon/internal/agent/codex/mcp_http.go @@ -8,12 +8,11 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // One projection consumes both public and installed Runtime bindings. A non-nil // empty public declaration still owns the complete native MCP configuration. -func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConfig, []string, error) { +func runtimeMCPServers(req agent.PrepareRequest) (map[string]mcpServerConfig, []string, error) { bindings, err := agent.ResolveMCPBindings(req) if err != nil { return nil, nil, err @@ -27,7 +26,7 @@ func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConf // mcpServersFromBindings renders resolved bindings, each stdio binding with // the command and arguments stdio gives it and each credential in a private // environment variable. -func mcpServersFromBindings(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) (map[string]mcpServerConfig, []string, error) { +func mcpServersFromBindings(bindings []agent.MCPBinding, stdio func(agent.EnvironmentMCP) (string, []string)) (map[string]mcpServerConfig, []string, error) { servers := make(map[string]mcpServerConfig, len(bindings)) var env []string for _, binding := range bindings { diff --git a/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go b/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go index 875f07bbe..41ab96cc4 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_bearer_test.go @@ -19,7 +19,7 @@ func TestMCPHTTPBearerPlanSeparatesServersAndProcesses(t *testing.T) { {ConnectionOrigin: "service", ServerLabel: "second", ServerURL: "https://second.example/mcp", BearerToken: &tokens[1]}, {ConnectionOrigin: "service", ServerLabel: "public", ServerURL: "http://public.example/mcp"}, } - req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "retained-mcp", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := prepared(t, "retained-mcp", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &servers}) seen := map[string]bool{} for range 2 { plan, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()) @@ -57,7 +57,7 @@ func TestMCPHTTPBearerRejectsInvalidTokensWithoutPersistence(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) for _, token := range []string{"", "=", " has-space", "has-space ", "has space", "line\r\ninjection", "nul\x00byte", "opaque中文", "middle=padding", "punctuation:invalid"} { servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} - req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "invalid-bearer", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := prepared(t, "invalid-bearer", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &servers}) if _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()); err == nil || err.Error() != "invalid HTTPS MCP bearer credential" { t.Fatal("invalid bearer value accepted or unsafe error returned") } @@ -80,8 +80,8 @@ func TestMCPHTTPBearerDoesNotReachModelCatalogProbe(t *testing.T) { } token := "synthetic-catalog-secret" servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} - req := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "catalog", DisableExecutionEnvironment: true, MCPHTTPServers: &servers, - Model: "fixture-model", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} + req := prepared(t, "catalog", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, + Model: "fixture-model", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}}) cfg := defaultSessionConfig() cfg.codexBinary = binary plan, err := prepareSessionPlan(t.Context(), req, cfg) diff --git a/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go b/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go index d3f643744..f7b5fc792 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go @@ -166,7 +166,7 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) { t.Fatal(err) } assertPreparationOnly(t, root) - home, err := allocCodexHome(req.AgentStateKey) + home, err := allocCodexHome(req.StateKey) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/mcp_http_test.go b/apps/daemon/internal/agent/codex/mcp_http_test.go index 309c5b183..0c0b28782 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -20,7 +21,7 @@ func TestPublicMCPHTTPPlanOwnsConfigurationAndPreservesHistory(t *testing.T) { {ConnectionOrigin: "service", ServerLabel: "docs.server", ServerURL: "https://docs.example/mcp", AllowedTools: &tools, Required: true}, {ConnectionOrigin: "service", ServerLabel: "blocked", ServerURL: "http://127.0.0.1:12345/mcp", AllowedTools: &denyAll}, } - req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "public-mcp", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} + req := prepared(t, "public-mcp", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &servers}) plan, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()) if err != nil { t.Fatal(err) @@ -29,7 +30,7 @@ func TestPublicMCPHTTPPlanOwnsConfigurationAndPreservesHistory(t *testing.T) { if !slices.Contains(plan.DisableFeatures, "apps") || !slices.Contains(plan.DisableFeatures, "plugins") || !slices.Contains(plan.ExtraConfig, [2]string{"mcp_oauth_credentials_store", `"file"`}) { t.Fatal("native profile was not pinned") } - home, err := allocCodexHome(req.AgentStateKey) + home, err := allocCodexHome(req.StateKey) if err != nil { t.Fatal(err) } @@ -71,12 +72,8 @@ func TestPublicMCPHTTPPlanOwnsConfigurationAndPreservesHistory(t *testing.T) { func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { valid := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "https://docs.example/mcp"}} - for _, req := range []proto.PromptRequestPayload{ - {MCPHTTPServers: &valid}, - } { - if _, _, err := runtimeMCPServers(req); err == nil { - t.Fatal("non-service profile accepted") - } + if _, _, err := runtimeMCPServers(agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{MCPHTTPServers: &valid}}); err == nil { + t.Fatal("non-service profile accepted") } for _, server := range []proto.MCPHTTPServer{ {ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "https://user:synthetic-secret@docs.example/mcp"}, @@ -84,7 +81,7 @@ func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { {ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "file:///tmp/mcp"}, } { servers := []proto.MCPHTTPServer{server} - if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &servers}); err == nil || strings.Contains(err.Error(), "synthetic-secret") { + if _, _, err := runtimeMCPServers(agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &servers}}); err == nil || strings.Contains(err.Error(), "synthetic-secret") { t.Fatal("unsupported configuration was accepted or exposed", err) } } @@ -98,7 +95,7 @@ func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { if err := os.WriteFile(path, stored, 0o600); err != nil { t.Fatal(err) } - req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "credentials", DisableExecutionEnvironment: true, MCPHTTPServers: &valid} + req := prepared(t, "credentials", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, MCPHTTPServers: &valid}) if _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()); err == nil { t.Fatal("existing MCP credentials accepted") } @@ -135,7 +132,7 @@ func writeMCPHTTPConfigResponse(t *testing.T, path string, response any) { } func TestPublicMCPBearerRequiresHTTPS(t *testing.T) { - req := proto.PromptRequestPayload{DisableExecutionEnvironment: true} + req := agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{DisableExecutionEnvironment: true}} token := "synthetic-private-token" servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "tools", ServerURL: "http://tools.example/mcp", BearerToken: &token}} req.MCPHTTPServers = &servers diff --git a/apps/daemon/internal/agent/codex/model_route_test.go b/apps/daemon/internal/agent/codex/model_route_test.go deleted file mode 100644 index bdcc18b50..000000000 --- a/apps/daemon/internal/agent/codex/model_route_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package codex - -import ( - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" -) - -func TestPlanRejectsNonNativeFrozenProvider(t *testing.T) { - t.Setenv("HOME", t.TempDir()) - for _, protocol := range []modelprovider.Protocol{modelprovider.Anthropic, modelprovider.ChatCompletions} { - t.Run(string(protocol), func(t *testing.T) { - provider := &modelprovider.Provider{Protocol: protocol, BaseURL: "https://model.invalid", APIKey: "private-sentinel"} - plan, err := BuildSessionPlan(proto.PromptRequestPayload{RunID: "recovered", AgentStateKey: "frozen-state", Model: "frozen-model", ModelProvider: provider}) - if plan.Cleanup != nil { - plan.Cleanup() - } - if err == nil || !strings.Contains(err.Error(), "does not support") || strings.Contains(err.Error(), "private-sentinel") { - t.Fatalf("non-native snapshot accepted: %v", err) - } - }) - } -} - -func TestPlanRejectsProviderWithoutModel(t *testing.T) { - provider := &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://model.example/v1", APIKey: "fixture"} - if _, err := BuildSessionPlan(proto.PromptRequestPayload{AgentStateKey: "state", ModelProvider: provider}); err == nil { - t.Fatal("a provider without a model was accepted") - } -} diff --git a/apps/daemon/internal/agent/codex/model_verbosity_test.go b/apps/daemon/internal/agent/codex/model_verbosity_test.go index cef4d59ee..8a0bda9e2 100644 --- a/apps/daemon/internal/agent/codex/model_verbosity_test.go +++ b/apps/daemon/internal/agent/codex/model_verbosity_test.go @@ -38,7 +38,7 @@ func TestPrepareModelVerbosity(t *testing.T) { if err := os.WriteFile(binary, []byte("#!/bin/sh\nprintf '%s' '"+catalog+"'\n"), 0700); err != nil { t.Fatal(err) } - plan, err := BuildSessionPlan(proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "state", Model: "known-model", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "high"}}) + plan, err := BuildSessionPlan(prepared(t, "state", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "known-model", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "high"}})) if err != nil { t.Fatal(err) } @@ -81,7 +81,7 @@ func TestPrepareDefaultModelVerbosity(t *testing.T) { for _, model := range []string{"supported", "unsupported", "unknown-provider-model"} { for _, level := range []string{"low", "medium", "high"} { t.Run(model+"/"+level, func(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "state", Model: model, ExecutionControls: &proto.ExecutionControls{TextVerbosity: level}}) + plan, err := BuildSessionPlan(prepared(t, "state", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: model, ExecutionControls: &proto.ExecutionControls{TextVerbosity: level}})) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index ab3d815c3..48e1cec2a 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -10,8 +10,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/codex" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) @@ -66,27 +64,24 @@ type SessionPlan struct { Cleanup func() } -// BuildSessionPlan derives a SessionPlan from the request's frozen model +// BuildSessionPlan derives a SessionPlan from the request's prepared model // configuration and ExecutionControls. The codex binary is resolved via PATH. -func BuildSessionPlan(req proto.PromptRequestPayload) (SessionPlan, error) { +func BuildSessionPlan(req agent.PrepareRequest) (SessionPlan, error) { return buildSessionPlan(req, func() (agent.ViewDir, error) { - home, err := allocCodexHome(req.AgentStateKey) + home, err := allocCodexHome(req.StateKey) return agent.ViewDir{Host: home, View: home}, err }) } // buildSessionPlan derives the plan with CODEX_HOME from allocHome, which runs // only after the request validates. -func buildSessionPlan(req proto.PromptRequestPayload, allocHome func() (agent.ViewDir, error)) (SessionPlan, error) { +func buildSessionPlan(req agent.PrepareRequest, allocHome func() (agent.ViewDir, error)) (SessionPlan, error) { plan := SessionPlan{ // Harnesses run unattended: Codex never offers its ask-the-user tool. ExtraConfig: [][2]string{{"tools.experimental_request_user_input.enabled", "false"}}, Cleanup: func() {}, } - prepared, err := harnessconfiguration.Configuration().Prepare(req) - if err != nil { - return plan, err - } + prepared := req.Prepared plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"web_search", strconv("disabled")}) if controls := req.ExecutionControls; controls != nil { switch controls.TextVerbosity { diff --git a/apps/daemon/internal/agent/codex/options_test.go b/apps/daemon/internal/agent/codex/options_test.go index d8c04c387..15bbe1656 100644 --- a/apps/daemon/internal/agent/codex/options_test.go +++ b/apps/daemon/internal/agent/codex/options_test.go @@ -9,7 +9,7 @@ import ( ) func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "conv-1/agent-1/codex"}) + plan, err := BuildSessionPlan(prepared(t, "conv-1/agent-1/codex", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider()})) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } @@ -23,7 +23,7 @@ func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { } func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "conv-1/agent-1/codex"}) + plan, err := BuildSessionPlan(prepared(t, "conv-1/agent-1/codex", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider()})) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } @@ -48,11 +48,11 @@ func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { func TestBuildSessionPlan_StableCodexHomeByStateKey(t *testing.T) { stateKey := "conv-stable/agent-stable/codex" - planA, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), RunID: "run-a", AgentStateKey: stateKey}) + planA, err := BuildSessionPlan(prepared(t, stateKey, proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider()})) if err != nil { t.Fatalf("BuildSessionPlan A: %v", err) } - planB, err := BuildSessionPlan(proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), RunID: "run-b", AgentStateKey: stateKey}) + planB, err := BuildSessionPlan(prepared(t, stateKey, proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider()})) if err != nil { t.Fatalf("BuildSessionPlan B: %v", err) } @@ -72,7 +72,7 @@ func codexHomeFromEnv(env []string) string { func TestBuildSessionPlan_CarriesModelAndSystemPrompt(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan(proto.PromptRequestPayload{ModelProvider: fixtureProvider(), AgentStateKey: "conv/agent/codex", Model: "MiniMax-M3", SystemPrompt: "current reference"}) + plan, err := BuildSessionPlan(prepared(t, "conv/agent/codex", proto.PromptRequestPayload{ModelProvider: fixtureProvider(), Model: "MiniMax-M3", SystemPrompt: "current reference"})) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/permission_profile.go b/apps/daemon/internal/agent/codex/permission_profile.go deleted file mode 100644 index f5e7cb807..000000000 --- a/apps/daemon/internal/agent/codex/permission_profile.go +++ /dev/null @@ -1,14 +0,0 @@ -package codex - -import ( - "fmt" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -// Runtime execution uses the current user; isolation belongs to its outer host. -func validatePermissionProfile(req proto.PromptRequestPayload) error { - if req.LocalEnvironment != nil && (req.DisableExecutionEnvironment || req.WorkspaceReadOnly || req.LocalEnvironment.NetworkAccess != "enabled" || len(req.LocalEnvironment.AllowedDomains) != 0) { - return fmt.Errorf("codex: Runtime execution requires unrestricted host access") - } - return nil -} diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 3c1f45bb7..75d73c474 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -7,17 +7,14 @@ import ( "os" "sync" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" obslog "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" ) -func newExecutor(parent context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (*Executor, error) { +func newExecutor(parent context.Context, req agent.PrepareRequest, cfg sessionConfig) (*Executor, error) { if req.WorkspaceReadOnly { return nil, errors.New("codex: workspace reads use the local Runtime interface") } - if req.RunID != "" || len(req.Input) != 0 { - return nil, errors.New("codex: preparation does not accept a run identity or prompt") - } if cfg.logger == nil { cfg.logger = obslog.Bg() } @@ -102,8 +99,8 @@ func newExecutor(parent context.Context, req proto.PromptRequestPayload, cfg ses return e.preparationFailed(err) } } - if local := req.LocalEnvironment; local != nil && len(local.Skills) > 0 { - if err := registerSkills(cancelCtx, rpc, plan.Cwd, local.Skills); err != nil { + if len(req.Skills) > 0 { + if err := registerSkills(cancelCtx, rpc, plan.Cwd, req.Skills); err != nil { return e.preparationFailed(err) } } diff --git a/apps/daemon/internal/agent/codex/preparation_helpers_test.go b/apps/daemon/internal/agent/codex/preparation_helpers_test.go index ccc527807..54102b229 100644 --- a/apps/daemon/internal/agent/codex/preparation_helpers_test.go +++ b/apps/daemon/internal/agent/codex/preparation_helpers_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) @@ -21,7 +22,7 @@ type preparationFrame struct { Params json.RawMessage `json:"params"` } -func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig, string) { +func preparationFixture(t *testing.T) (agent.PrepareRequest, sessionConfig, string) { t.Helper() root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) @@ -40,17 +41,27 @@ func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig } cfg := defaultSessionConfig() cfg.codexBinary = binary - req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "prepared-session", + req := prepared(t, "prepared-session", proto.PromptRequestPayload{ + AgentKind: "codex", Model: "fixture-model", ModelProvider: fixtureProvider(), ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, DisableExecutionEnvironment: true, FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"value":{"type":"integer"}}}`)}}, - } + }) return req, cfg, root } +// prepared is req as the registry and dispatch hand it to a Codex factory. +func prepared(t testing.TB, stateKey string, req proto.PromptRequestPayload) agent.PrepareRequest { + t.Helper() + configuration, err := Declaration.Configuration.Prepare(req) + if err != nil { + t.Fatal(err) + } + return agent.PrepareRequest{PromptRequestPayload: req, Prepared: configuration, StateKey: stateKey} +} + func fixtureProvider() *modelprovider.Provider { return &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://model.example/v1", APIKey: "fixture-key"} } @@ -222,6 +233,8 @@ func TestPreparationFakeCodexProcess(t *testing.T) { if allowed == nil { _ = os.WriteFile(os.Getenv("OAC_TEST_PREPARATION_FRAMES")+".terminated", nil, 0600) } + case "turn/steer": + result = map[string]any{"turnId": currentTurn} case "turn/interrupt": if executorMode == "interrupt-error" { _ = output.Encode(map[string]any{"id": frame.ID, "error": map[string]any{"code": -32603, "message": "interrupt rejected"}}) @@ -254,10 +267,14 @@ func TestPreparationFakeCodexProcess(t *testing.T) { if frame.Method == "turn/interrupt" && executorMode == "interrupt-no-terminal" { continue } + held := strings.Contains(string(frame.Params), "hold") if frame.Method == "turn/start" { _ = output.Encode(map[string]any{"method": "turn/started", "params": map[string]any{"threadId": "fixture-native-thread", "turn": map[string]string{"id": currentTurn}}}) + if held { + _ = output.Encode(map[string]any{"method": "item/agentMessage/delta", "params": map[string]any{"threadId": "fixture-native-thread", "turnId": currentTurn, "itemId": "held-message", "delta": "holding"}}) + } } - if frame.Method == "turn/interrupt" || !strings.Contains(string(frame.Params), "hold") { + if frame.Method == "turn/interrupt" || !held { status := "completed" if frame.Method == "turn/interrupt" { status = "interrupted" diff --git a/apps/daemon/internal/agent/codex/preparation_router_test.go b/apps/daemon/internal/agent/codex/preparation_router_test.go index f343b05e0..0fe775666 100644 --- a/apps/daemon/internal/agent/codex/preparation_router_test.go +++ b/apps/daemon/internal/agent/codex/preparation_router_test.go @@ -53,13 +53,12 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { if err != nil { t.Fatal(err) } - req.AgentStateKey = "agents-api-" + session req.DisableExecutionEnvironment = false - req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}} + req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}} registry := agent.NewRegistry() registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfiguration.Configuration()) prepared := make(chan *Executor, 1) - registry.RegisterExecutor("codex", func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { + registry.RegisterExecutor("codex", func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { e, err := newExecutor(ctx, req, cfg) if err != nil { return nil, err @@ -114,7 +113,7 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { } } } - send(proto.TypeExecutionPrepare, "prepare-request", proto.ExecutionPreparePayload{SessionID: session, Configuration: req}) + send(proto.TypeExecutionPrepare, "prepare-request", proto.ExecutionPreparePayload{SessionID: session, Configuration: req.PromptRequestPayload}) ready := await("ready") p := <-prepared assertPreparationOnly(t, root) diff --git a/apps/daemon/internal/agent/codex/provider_config_test.go b/apps/daemon/internal/agent/codex/provider_config_test.go index a7194c7d4..4a1bff726 100644 --- a/apps/daemon/internal/agent/codex/provider_config_test.go +++ b/apps/daemon/internal/agent/codex/provider_config_test.go @@ -118,10 +118,10 @@ func TestWriteCodexProviderConfig_AppendsAlongsideMCP(t *testing.T) { } func TestBuildSessionPlan_PinsModelProviderWhenProviderSet(t *testing.T) { - plan, err := BuildSessionPlan(proto.PromptRequestPayload{ - RunID: "run-x", AgentStateKey: "conv-1/agent-1/codex", Model: "fixture-model", + plan, err := BuildSessionPlan(prepared(t, "conv-1/agent-1/codex", proto.PromptRequestPayload{ + Model: "fixture-model", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://x/v1", APIKey: "sk-x"}, - }) + })) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } diff --git a/apps/daemon/internal/agent/codex/recovery_test.go b/apps/daemon/internal/agent/codex/recovery_test.go index bd84ec2bd..f3a4912e2 100644 --- a/apps/daemon/internal/agent/codex/recovery_test.go +++ b/apps/daemon/internal/agent/codex/recovery_test.go @@ -149,7 +149,7 @@ func TestPreparedRecoveryCannotStartWithoutExistingHistory(t *testing.T) { t.Run(environment, func(t *testing.T) { req, cfg, root := preparationFixture(t) req.RequireExistingNativeSession = true - cwd, err := allocCodexHome(req.AgentStateKey) + cwd, err := allocCodexHome(req.StateKey) if err != nil { t.Fatal(err) } @@ -159,7 +159,8 @@ func TestPreparedRecoveryCannotStartWithoutExistingHistory(t *testing.T) { t.Fatal(err) } req.DisableExecutionEnvironment = false - req.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString(), WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}, WorkspaceRoot: cwd} + req.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString(), WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}} + req.WorkspaceRoot = cwd } e, err := testExecutor(t, "complete", req, cfg) if err != nil { @@ -198,7 +199,7 @@ func TestRecoveryRequiresWritableAgentState(t *testing.T) { req.RequireExistingNativeSession = true switch mode { case "no-state": - req.AgentStateKey = "" + req.StateKey = "" case "read-only": req.WorkspaceReadOnly = true } diff --git a/apps/daemon/internal/agent/codex/session_plan.go b/apps/daemon/internal/agent/codex/session_plan.go index f69fd07f1..5fe5499f0 100644 --- a/apps/daemon/internal/agent/codex/session_plan.go +++ b/apps/daemon/internal/agent/codex/session_plan.go @@ -5,17 +5,14 @@ import ( "fmt" "strings" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (SessionPlan, error) { +func prepareSessionPlan(ctx context.Context, req agent.PrepareRequest, cfg sessionConfig) (SessionPlan, error) { if err := validateNativeTransportEnvironment(); err != nil { return SessionPlan{}, err } - if err := validatePermissionProfile(req); err != nil { - return SessionPlan{}, err - } mcpServers, mcpEnv, err := runtimeMCPServers(req) if err != nil { return SessionPlan{}, err @@ -24,13 +21,13 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg if err != nil { return SessionPlan{}, fmt.Errorf("codex: build session plan: %w", err) } - if err := configureSubagentObservations(&plan, req); err != nil { + if err := configureSubagentObservations(&plan, req.PromptRequestPayload); err != nil { plan.Cleanup() return SessionPlan{}, err } disableProgrammaticTools(&plan, req.ExecutionControls) if req.LocalEnvironment != nil { - plan.Cwd = req.LocalEnvironment.WorkspaceRoot + plan.Cwd = req.WorkspaceRoot } else { // environment:none has no workspace; the Session's private home is its cwd. plan.Cwd = plan.home.View diff --git a/apps/daemon/internal/agent/codex/permission_profile_test.go b/apps/daemon/internal/agent/codex/session_plan_test.go similarity index 59% rename from apps/daemon/internal/agent/codex/permission_profile_test.go rename to apps/daemon/internal/agent/codex/session_plan_test.go index f93bb8472..3152fa6fa 100644 --- a/apps/daemon/internal/agent/codex/permission_profile_test.go +++ b/apps/daemon/internal/agent/codex/session_plan_test.go @@ -10,26 +10,19 @@ import ( func TestRuntimeUsesHostPermissions(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - { - req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()}} - plan, err := prepareSessionPlan(t.Context(), req, sessionConfig{}) - if err != nil { - t.Fatal(err) - } - if plan.Cwd != req.LocalEnvironment.WorkspaceRoot { - t.Fatal("native cwd is not the bound workspace root", plan.Cwd) - } - for _, kv := range plan.ExtraConfig { - if kv[0] == "default_permissions" { - t.Fatal("obsolete permission wrapper", kv) - } - } - plan.Cleanup() - for _, network := range []string{"disabled", "restricted"} { - req.LocalEnvironment.NetworkAccess = network - if _, err := prepareSessionPlan(t.Context(), req, sessionConfig{}); err == nil { - t.Fatal("unsupported network admitted") - } + req := prepared(t, "session", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{}}) + req.WorkspaceRoot = t.TempDir() + plan, err := prepareSessionPlan(t.Context(), req, sessionConfig{}) + if err != nil { + t.Fatal(err) + } + defer plan.Cleanup() + if plan.Cwd != req.WorkspaceRoot { + t.Fatal("native cwd is not the bound workspace root", plan.Cwd) + } + for _, kv := range plan.ExtraConfig { + if kv[0] == "default_permissions" { + t.Fatal("obsolete permission wrapper", kv) } } } @@ -48,7 +41,7 @@ func TestSelfHostedToolEnvironmentCannotRedirectNativeHistory(t *testing.T) { for key, value := range map[string]string{"OAC_RUNTIME_ENVIRONMENT_ID": "b3d154b8-543b-4248-97b1-665f9f418d52", "OAC_RUNTIME_SESSION_ID": "33e02e0d-6fc8-4904-9d7a-4b61b9094ae0", "OAC_RUNTIME_WORKSPACE": workspace, "OAC_RUNTIME_CAPABILITY_DIRECTORY": filepath.Join(root, "capabilities"), "OAC_RUNTIME_NETWORK_ACCESS": "enabled", "OAC_RUNTIME_TOOL_ENV_FILE": config} { t.Setenv(key, value) } - req := proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled"}} + req := prepared(t, "session", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{}}) plan, err := prepareSessionPlan(t.Context(), req, sessionConfig{}) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/codex/session_policy_test.go b/apps/daemon/internal/agent/codex/session_policy_test.go index f1fdb082c..4aca86090 100644 --- a/apps/daemon/internal/agent/codex/session_policy_test.go +++ b/apps/daemon/internal/agent/codex/session_policy_test.go @@ -16,7 +16,7 @@ func TestThreadRequestsApplyDeploymentPolicy(t *testing.T) { for _, method := range []string{"thread/start", "thread/resume"} { t.Run(method, func(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := prepareSessionPlan(context.Background(), proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), AgentStateKey: "conv/agent/codex", DisableSubagents: true, DisableExecutionEnvironment: true}, sessionConfig{}) + plan, err := prepareSessionPlan(context.Background(), prepared(t, "conv/agent/codex", proto.PromptRequestPayload{Model: "fixture", ModelProvider: fixtureProvider(), DisableSubagents: true, DisableExecutionEnvironment: true}), sessionConfig{}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/view.go b/apps/daemon/internal/agent/codex/view.go index d8f905cc4..725d6da2b 100644 --- a/apps/daemon/internal/agent/codex/view.go +++ b/apps/daemon/internal/agent/codex/view.go @@ -13,7 +13,6 @@ import ( "slices" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) @@ -86,7 +85,7 @@ func newView(binary string, codeModeHost bool) agent.View { ShimPaths: []string{"/bin/bash"}, ForwardEnv: slices.Clone(viewForwardEnv), Proxy: agent.ViewProxyEnv, - Executor: func(ctx context.Context, req proto.PromptRequestPayload, session agent.ViewSession) (agent.Executor, error) { + Executor: func(ctx context.Context, req agent.PrepareRequest, session agent.ViewSession) (agent.Executor, error) { cfg := defaultSessionConfig() cfg.codexBinary = binary cfg.view = &viewLaunch{ViewSession: session, binary: launch} @@ -124,24 +123,21 @@ func staticELF(name string) bool { // workspace as cwd, or the work directory with environment none, CODEX_HOME // and TMPDIR in the Session home, MCP only from the Session, and a closed // environment. -func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (SessionPlan, error) { +func prepareViewPlan(ctx context.Context, req agent.PrepareRequest, cfg sessionConfig) (SessionPlan, error) { view := cfg.view if !filepath.IsAbs(view.Home.Host) || !path.IsAbs(view.Home.View) { return SessionPlan{}, errors.New("codex: view home must be absolute") } cwd := path.Join(view.Home.View, agent.ViewWorkName) - if local := req.LocalEnvironment; local != nil { - cwd = local.WorkspaceRoot + if req.LocalEnvironment != nil { + cwd = req.WorkspaceRoot } if (req.LocalEnvironment == nil) != req.DisableExecutionEnvironment || !path.IsAbs(cwd) { return SessionPlan{}, fmt.Errorf("%w: codex: a view runs in an Environment workspace or with environment none", agent.ErrUnsupportedOperation) } - if err := validatePermissionProfile(req); err != nil { - return SessionPlan{}, err - } // The Harness runs each stdio alias without arguments, which the native // configuration reports as an empty list. - servers, _, err := mcpServersFromBindings(view.MCP, func(stdio proto.EnvironmentMCP) (string, []string) { return stdio.Server.Command, []string{} }) + servers, _, err := mcpServersFromBindings(view.MCP, func(stdio agent.EnvironmentMCP) (string, []string) { return stdio.Server.Command, []string{} }) if err != nil { return SessionPlan{}, err } @@ -149,7 +145,7 @@ func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg se if err != nil { return SessionPlan{}, fmt.Errorf("codex: build session plan: %w", err) } - if err := configureSubagentObservations(&plan, req); err != nil { + if err := configureSubagentObservations(&plan, req.PromptRequestPayload); err != nil { plan.Cleanup() return SessionPlan{}, err } @@ -176,8 +172,7 @@ func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg se // project trust and records its own on thread/start. plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"allow_login_shell", "false"}, [2]string{"project_root_markers", "[]"}) if req.ExecutionControls != nil { - // buildSessionPlan admitted the request's provider. - if err := viewModelVerbosity(ctx, cfg.codexBinary, &plan, *req.ModelProvider); err != nil { + if err := viewModelVerbosity(ctx, cfg.codexBinary, &plan, req.Prepared.Provider); err != nil { plan.Cleanup() return SessionPlan{}, err } diff --git a/apps/daemon/internal/agent/codex/view_test.go b/apps/daemon/internal/agent/codex/view_test.go index 65d302df8..23f412adf 100644 --- a/apps/daemon/internal/agent/codex/view_test.go +++ b/apps/daemon/internal/agent/codex/view_test.go @@ -56,12 +56,12 @@ func TestViewExecutorLaunchesInTheSessionView(t *testing.T) { return nil, errors.New("recorded") }, } - req := proto.PromptRequestPayload{ - AgentStateKey: "state", + req := prepared(t, "state", proto.PromptRequestPayload{ Model: "m", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "http://127.0.0.1:17101", APIKey: modelprovider.Placeholder}, - LocalEnvironment: &proto.LocalEnvironment{WorkspaceRoot: "/workspace", NetworkAccess: "enabled"}, - } + LocalEnvironment: &proto.LocalEnvironment{}, + }) + req.WorkspaceRoot = "/workspace" if _, err := view.Executor(t.Context(), req, session); err == nil || len(launched) != 1 { t.Fatalf("launches %d, err %v", len(launched), err) } @@ -123,7 +123,7 @@ func TestViewExecutorLaunchesInTheSessionView(t *testing.T) { t.Fatalf("outside file changed: %q, %v", body, err) } - session.MCP = []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &proto.EnvironmentMCP{ + session.MCP = []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &agent.EnvironmentMCP{ Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: agent.ViewAlias(0)}}}} req.LocalEnvironment, req.DisableExecutionEnvironment = nil, true if _, err := view.Executor(t.Context(), req, session); err == nil || len(launched) != 2 { @@ -147,7 +147,7 @@ func TestViewHandsCodexTheInstalledSkillAndMCP(t *testing.T) { session := agent.ViewSession{ Home: agent.ViewDir{Host: home, View: agent.ViewPrivateRoot + "/" + agent.ViewHomeName}, Proxy: "http://127.0.0.1:17100", - MCP: []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &proto.EnvironmentMCP{ + MCP: []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &agent.EnvironmentMCP{ Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: agent.ViewAlias(0)}}}}, // The fake Codex runs on the host, outside the view. Launch: func(opts clirunner.StartOptions) (*clirunner.Process, error) { @@ -160,10 +160,11 @@ func TestViewHandsCodexTheInstalledSkillAndMCP(t *testing.T) { writeMCPHTTPConfigResponse(t, config, map[string]any{"config": map[string]any{"mcp_servers": map[string]any{"local": server}, "features": map[string]any{"plugins": false, "apps": false}, "mcp_oauth_credentials_store": "file"}}) t.Setenv("OAC_TEST_PREPARATION_MCP_CONFIG", config) - req := proto.PromptRequestPayload{AgentStateKey: "state", Model: "m", - ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "http://127.0.0.1:17101", APIKey: modelprovider.Placeholder}, - LocalEnvironment: &proto.LocalEnvironment{WorkspaceRoot: "/workspace", NetworkAccess: "enabled", CapabilityRoot: agentcapabilities.Directory, - Skills: []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, RelativeRoot: "skills/review", PackageRoot: "skills/review"}}}} + req := prepared(t, "state", proto.PromptRequestPayload{Model: "m", + ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "http://127.0.0.1:17101", APIKey: modelprovider.Placeholder}, + LocalEnvironment: &proto.LocalEnvironment{}}) + req.WorkspaceRoot, req.CapabilityRoot = "/workspace", agentcapabilities.Directory + req.Skills = []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, RelativeRoot: "skills/review", PackageRoot: "skills/review"}} e, err := declared.Executor(t.Context(), req, session) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/configuration_test.go b/apps/daemon/internal/agent/configuration_test.go index 4b1e61c5e..8a9b710b4 100644 --- a/apps/daemon/internal/agent/configuration_test.go +++ b/apps/daemon/internal/agent/configuration_test.go @@ -9,6 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "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/modelprovider" ) @@ -18,10 +19,12 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { registry := agent.NewRegistry() configuration := prototest.ModelConfiguration() calls := 0 + var prepared harnessconfig.PreparedConfiguration expected := errors.New("native entry reached") registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration) - registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registry.RegisterExecutor("fixture", func(_ context.Context, req agent.PrepareRequest) (agent.Executor, error) { calls++ + prepared = req.Prepared return nil, expected }) configuration.Providers[0].Protocol = "anthropic" @@ -33,14 +36,17 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { {Model: "fixture"}, {HarnessConfig: proto.HarnessConfig(`{"unknown":"private-sentinel"}`)}, } { - _, err := executor(t.Context(), req) + _, err := executor(t.Context(), agent.PrepareRequest{PromptRequestPayload: req}) if err == nil || errors.Is(err, expected) || calls != 0 || strings.Contains(err.Error(), "private-sentinel") { t.Fatal("invalid configuration reached native entry or leaked values") } } - if _, err := executor(t.Context(), proto.PromptRequestPayload{Model: "fixture", ModelProvider: responses}); !errors.Is(err, expected) || calls != 1 { + if _, err := executor(t.Context(), agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{Model: "fixture", ModelProvider: responses}}); !errors.Is(err, expected) || calls != 1 { t.Fatal("bound declaration was lost or mutated", err) } + if prepared.Model != "fixture" || prepared.Provider != *responses { + t.Fatalf("the factory received %+v, not the prepared configuration", prepared) + } } func TestRegistryRejectsInvalidConfigurationDeclaration(t *testing.T) { diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 5bd10beaf..e53e77bc5 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -24,7 +24,7 @@ // narrowed declaration admits. Requests, events and capability descriptors // use the existing internal/agentdaemon/proto types. An Environment execution // request carries the Runtime's bound workspace directory in -// LocalEnvironment.WorkspaceRoot; the native Harness runs there. +// PrepareRequest.WorkspaceRoot; the native Harness runs there. package agent import ( @@ -42,6 +42,7 @@ import ( "strings" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" @@ -276,9 +277,8 @@ const ( // ViewExecutorFactory prepares the Session's Executor in its view. The agent // host has already pointed the request's model provider at the Session's // gateway, with the placeholder in place of the key, and moved its MCP into -// ViewSession.MCP: the request carries neither MCPHTTPServers nor -// LocalEnvironment.MCP. -type ViewExecutorFactory func(context.Context, proto.PromptRequestPayload, ViewSession) (Executor, error) +// ViewSession.MCP: the request carries neither MCPHTTPServers nor MCP. +type ViewExecutorFactory func(context.Context, PrepareRequest, ViewSession) (Executor, error) // ViewSession is what the agent host gives a view Executor factory. type ViewSession struct { @@ -332,15 +332,15 @@ type ViewSession struct { // reaches the network only through the Session's gateway: the model provider // is the gateway with the placeholder key, and MCP arrives only in session.MCP, // without credentials and with stdio only under its alias. -func checkViewHandoff(req proto.PromptRequestPayload, prepared harnessconfig.PreparedConfiguration, session ViewSession) error { - if provider := prepared.Provider; provider.APIKey != modelprovider.Placeholder || !isGatewayURL(provider.BaseURL, false) { +func checkViewHandoff(req PrepareRequest, session ViewSession) error { + if provider := req.Prepared.Provider; provider.APIKey != modelprovider.Placeholder || !isGatewayURL(provider.BaseURL, false) { return fmt.Errorf("%w: the model provider is not the Session's gateway", ErrViewHandoff) } - if req.MCPHTTPServers != nil || (req.LocalEnvironment != nil && len(req.LocalEnvironment.MCP) > 0) { + if req.MCPHTTPServers != nil || len(req.MCP) > 0 { return fmt.Errorf("%w: MCP outside ViewSession.MCP", ErrViewHandoff) } for i, binding := range session.MCP { - alias := proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: binding.ServerLabel, Type: "stdio", Command: ViewAlias(i)}} + alias := EnvironmentMCP{Server: agentplugin.MCPServer{Name: binding.ServerLabel, Type: "stdio", Command: ViewAlias(i)}} if binding.BearerToken != nil || len(binding.HTTPHeaders) > 0 || (binding.Transport == "http" && !isGatewayURL(binding.ServerURL, true)) || (binding.Transport == "stdio" && (binding.Stdio == nil || !reflect.DeepEqual(*binding.Stdio, alias))) { return fmt.Errorf("%w: MCP binding %q is not a credential-free gateway endpoint or alias", ErrViewHandoff, binding.ServerLabel) @@ -474,26 +474,21 @@ func isWithin(p, dir string) bool { } // RegisterView validates and installs the kind's agent-host view after -// RegisterKind. Its Executor factory validates the model configuration like -// RegisterExecutor and then checks the request against the view's gateway rule. +// RegisterKind. Its Executor factory takes the request the agent host's +// RegisterExecutor prepared and checks it against the view's gateway rule. func (r *Registry) RegisterView(kind string, view View) { if err := view.Validate(); err != nil { panic(err) } r.mu.Lock() defer r.mu.Unlock() - configuration, declared := r.configurations[kind] - if !declared { + if _, declared := r.configurations[kind]; !declared { panic("agent.Registry.RegisterView: registered kind required") } view = view.clone() factory := view.Executor - view.Executor = func(ctx context.Context, req proto.PromptRequestPayload, session ViewSession) (Executor, error) { - prepared, err := configuration.Prepare(req) - if err != nil { - return nil, err - } - if err := checkViewHandoff(req, prepared, session); err != nil { + view.Executor = func(ctx context.Context, req PrepareRequest, session ViewSession) (Executor, error) { + if err := checkViewHandoff(req, session); err != nil { return nil, err } return factory(ctx, req, session) @@ -532,7 +527,32 @@ func (r *Registry) ResolveView(kind string) (View, error) { // ExecutorFactory prepares without model input. A failed factory retains any // unconfirmed cleanup in its non-nil Executor. -type ExecutorFactory func(context.Context, proto.PromptRequestPayload) (Executor, error) +type ExecutorFactory func(context.Context, PrepareRequest) (Executor, error) + +// PrepareRequest is what an Executor is prepared from: the Session's +// execution configuration as execution_prepare carries it, and what the +// Runtime resolved for it. Dispatch builds it once per Executor, the +// Session's Environment owner fills the Environment fields, and the Registry +// sets Prepared. A Turn's run ID and input arrive in Executor.StartTurn. +type PrepareRequest struct { + proto.PromptRequestPayload + // Prepared is the model configuration, validated against the kind's + // declaration. An adapter takes its model, provider and native parameters + // only from here. + Prepared harnessconfig.PreparedConfiguration + // StateKey names the Session's native state on this Runtime. + StateKey string + // Assignment is the assignment the Runtime admitted the preparation under. + Assignment proto.AssignmentRef + // WorkspaceRoot is the Environment's workspace, where the Harness runs. + // It is empty with environment none. + WorkspaceRoot string + // CapabilityRoot, Skills and MCP are the Environment's installed + // Capabilities. Never log MCP: its headers and bearer may be confidential. + CapabilityRoot string + Skills []agentcapabilities.InstalledSkill + MCP []EnvironmentMCP +} // Executor retains a fixed native configuration across independently owned Turns. type Executor interface { @@ -620,10 +640,12 @@ func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { if !declared { panic("agent.Registry.RegisterExecutor: configuration required") } - r.executors[kind] = func(ctx context.Context, req proto.PromptRequestPayload) (Executor, error) { - if _, err := configuration.Prepare(req); err != nil { + r.executors[kind] = func(ctx context.Context, req PrepareRequest) (Executor, error) { + prepared, err := configuration.Prepare(req.PromptRequestPayload) + if err != nil { return nil, err } + req.Prepared = prepared return factory(ctx, req) } } diff --git a/apps/daemon/internal/agent/mcode/environment_mcp.go b/apps/daemon/internal/agent/mcode/environment_mcp.go index 8381d24a1..3f391adc5 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp.go @@ -7,10 +7,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, []agent.MCPBinding, error) { +func runtimeMCP(req agent.PrepareRequest) ([]map[string]any, []agent.MCPBinding, error) { bindings, err := agent.ResolveMCPBindings(req) if err != nil { return nil, nil, err @@ -21,7 +20,7 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, []agent.MCPBi // workspaceMCP renders the Session's MCP bindings as ACP servers, each stdio // binding with the command and arguments stdio gives it. -func workspaceMCP(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) ([]map[string]any, error) { +func workspaceMCP(bindings []agent.MCPBinding, stdio func(agent.EnvironmentMCP) (string, []string)) ([]map[string]any, error) { var servers []map[string]any for _, binding := range bindings { if binding.Transport != "http" { diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index 04ecc4ea1..c3925dc47 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -10,12 +10,13 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" ) -func environmentMCPFixture() proto.EnvironmentMCP { - return proto.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/fixture", Server: agentplugin.MCPServer{ +func environmentMCPFixture() agent.EnvironmentMCP { + return agent.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/fixture", Server: agentplugin.MCPServer{ Name: "proof.server", Type: "stdio", Command: "never-exec-before-sandbox", Args: []string{"private-argument"}, EnvVars: []string{"USER_SELECTED"}, CWD: "resources", }} @@ -25,8 +26,7 @@ func TestEnvironmentMCPUsesFixedLauncherForNewAndLoadedSessions(t *testing.T) { for _, resume := range []bool{false, true} { t.Run(map[bool]string{false: "new", true: "load"}[resume], func(t *testing.T) { c, req, record := workspaceFixture(t) - c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" - req.LocalEnvironment.MCP = []proto.EnvironmentMCP{environmentMCPFixture()} + req.MCP = []agent.EnvironmentMCP{environmentMCPFixture()} if resume { req.AgentSessionID = "native-1" } @@ -54,8 +54,8 @@ func TestEnvironmentMCPUsesFixedLauncherForNewAndLoadedSessions(t *testing.T) { t.Fatal("environment MCP displaced workspace tools") } cwd, err := os.ReadFile(record + ".cwd") - workspace, pathErr := filepath.EvalSymlinks(req.LocalEnvironment.WorkspaceRoot) - if err != nil || pathErr != nil || string(cwd) != workspace || params.Cwd != req.LocalEnvironment.WorkspaceRoot { + workspace, pathErr := filepath.EvalSymlinks(req.WorkspaceRoot) + if err != nil || pathErr != nil || string(cwd) != workspace || params.Cwd != req.WorkspaceRoot { t.Fatalf("native process and ACP Session must use the declared workspace: process=%q ACP=%q", cwd, params.Cwd) } server := params.MCP[1] @@ -75,26 +75,25 @@ func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T for _, name := range []string{"http-headers", "http-bearer-insecure", "http-bearer-missing", "restricted", "disabled", "duplicate"} { t.Run(name, func(t *testing.T) { c, req, _ := workspaceFixture(t) - c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" - req.LocalEnvironment.MCP = []proto.EnvironmentMCP{environmentMCPFixture()} + req.MCP = []agent.EnvironmentMCP{environmentMCPFixture()} switch name { case "http-headers", "http-bearer-insecure", "http-bearer-missing": - req.LocalEnvironment.MCP[0].Server = agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"} + req.MCP[0].Server = agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"} if name == "http-headers" { - req.LocalEnvironment.MCP[0].Server.HTTPHeaders = map[string]string{"X-Private": "secret"} + req.MCP[0].Server.HTTPHeaders = map[string]string{"X-Private": "secret"} } if name == "http-bearer-missing" { - req.LocalEnvironment.MCP[0].Server.BearerTokenEnvVar = "SELECTED_TOKEN" + req.MCP[0].Server.BearerTokenEnvVar = "SELECTED_TOKEN" } if name == "http-bearer-insecure" { - req.LocalEnvironment.MCP[0].Server.URL = "http://example.invalid/mcp" + req.MCP[0].Server.URL = "http://example.invalid/mcp" token := "confidential-http-token" - req.LocalEnvironment.MCP[0].BearerToken = &token + req.MCP[0].BearerToken = &token } case "restricted", "disabled": - c.Network, req.LocalEnvironment.NetworkAccess = name, name + c.Network = name case "duplicate": - req.LocalEnvironment.MCP = append(req.LocalEnvironment.MCP, environmentMCPFixture()) + req.MCP = append(req.MCP, environmentMCPFixture()) } if _, err := prepareWorkspaceOptions(c, req); err == nil || strings.Contains(err.Error(), "confidential-http-token") { t.Fatal("unqualified declaration accepted or credential exposed") @@ -105,8 +104,7 @@ func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T func TestEnvironmentMCPCancelSettlesPendingObservationBeforeDone(t *testing.T) { c, req, _ := workspaceFixture(t) - c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" - req.LocalEnvironment.MCP = []proto.EnvironmentMCP{environmentMCPFixture()} + req.MCP = []agent.EnvironmentMCP{environmentMCPFixture()} script, err := os.ReadFile(c.Binary) if err != nil { t.Fatal(err) @@ -180,14 +178,13 @@ func mcpRegistryEntry(server, segment, tool, toolSegment string) map[string]any func TestEnvironmentHTTPMCPUsesEphemeralACPConfiguration(t *testing.T) { for _, authenticated := range []bool{false, true} { c, req, record := workspaceFixture(t) - c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" - item := proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}} + item := agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}} const token = "private-mcp-canary" if authenticated { value := token item.BearerToken = &value } - req.LocalEnvironment.MCP = []proto.EnvironmentMCP{item} + req.MCP = []agent.EnvironmentMCP{item} resource, err := NewExecutorFactory(&c)(t.Context(), req) if err != nil { t.Fatal(err) @@ -232,7 +229,6 @@ func TestEnvironmentHTTPMCPUsesEphemeralACPConfiguration(t *testing.T) { func TestPublicEnvironmentHTTPMCPKeepsCredentialTransient(t *testing.T) { c, req, _ := workspaceFixture(t) - c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" token := "selected-public-vault-canary" req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "remote", ServerURL: "https://example.test/mcp", BearerToken: &token}} opts, err := prepareWorkspaceOptions(c, req) diff --git a/apps/daemon/internal/agent/mcode/execution.go b/apps/daemon/internal/agent/mcode/execution.go index 094cec7ca..c1705278c 100644 --- a/apps/daemon/internal/agent/mcode/execution.go +++ b/apps/daemon/internal/agent/mcode/execution.go @@ -8,7 +8,7 @@ import ( ) func validateExecutionRequest(req proto.PromptRequestPayload) error { - if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || req.ExecutionControls == nil { + if !req.DisableExecutionEnvironment || req.LocalEnvironment != nil || req.RequireExistingNativeSession || req.ExecutionControls == nil { return fmt.Errorf("mcode: unsupported execution configuration") } if !req.DisableSubagents && (req.MaxConcurrentSubagents == nil || *req.MaxConcurrentSubagents < 1) { diff --git a/apps/daemon/internal/agent/mcode/execution_test.go b/apps/daemon/internal/agent/mcode/execution_test.go index 198532f2f..e48c5cbbd 100644 --- a/apps/daemon/internal/agent/mcode/execution_test.go +++ b/apps/daemon/internal/agent/mcode/execution_test.go @@ -14,7 +14,7 @@ func TestExecutionOptionsInheritUserEnvironment(t *testing.T) { r := testRequest(t) t.Setenv("OAC_TEST_SECRET_CANARY", "secret") t.Setenv("NODE_OPTIONS", "--import=untrusted") - opts, err := prepareOptions(r) + opts, err := prepareOptions(prepared(t, r)) if err != nil { t.Fatal(err) } @@ -47,7 +47,7 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { } { r := testRequest(t) change(&r) - if _, err := prepareOptions(r); err == nil { + if _, err := prepareOptions(prepared(t, r)); err == nil { t.Fatal("unsupported execution accepted") } } diff --git a/apps/daemon/internal/agent/mcode/executor.go b/apps/daemon/internal/agent/mcode/executor.go index 50294a84a..104d38d1f 100644 --- a/apps/daemon/internal/agent/mcode/executor.go +++ b/apps/daemon/internal/agent/mcode/executor.go @@ -32,12 +32,12 @@ func NewExecutorFactory(config *WorkspaceConfig) agent.ExecutorFactory { value.AllowedDomains = append([]string(nil), config.AllowedDomains...) frozen = &value } - return func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { + return func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { binary := defaultBinary() if frozen != nil { binary = frozen.Binary } - return startExecutor(ctx, req, binary, func() (launchOptions, error) { + return startExecutor(ctx, req.PromptRequestPayload, binary, func() (launchOptions, error) { if frozen == nil { return prepareOptions(req) } @@ -46,15 +46,11 @@ func NewExecutorFactory(config *WorkspaceConfig) agent.ExecutorFactory { } } -// startExecutor prepares the native owner for req, which carries no Turn -// input, and starts binary. +// startExecutor prepares the native owner for req and starts binary. func startExecutor(ctx context.Context, req proto.PromptRequestPayload, binary string, prepare func() (launchOptions, error)) (agent.Executor, error) { if ctx == nil { ctx = context.Background() } - if req.RunID != "" || len(req.Input) != 0 { - return nil, fmt.Errorf("mcode: Executor configuration cannot contain Turn input") - } opts, err := prepare() if err != nil { return nil, err @@ -129,10 +125,8 @@ func (e *executor) StartTurn(ctx context.Context, runID string, input proto.Mess return nil, fmt.Errorf("mcode: native process exited") default: } - req := e.req - req.RunID = runID - s := newTurnSession(e.connection.process.Context(), req, e.opts, e.connection, out) - s.executor, s.sessionID, s.nativeModel = e, e.nativeSession, e.model + s := newTurnSession(e.connection.process.Context(), e.req, e.opts, e.connection, out) + s.executor, s.runID, s.sessionID, s.nativeModel = e, runID, e.nativeSession, e.model s.settled, s.inputDone = make(chan struct{}), make(chan struct{}) s.outputContext, s.outputCancel = context.WithCancel(context.Background()) e.active = s diff --git a/apps/daemon/internal/agent/mcode/executor_native_test.go b/apps/daemon/internal/agent/mcode/executor_native_test.go index 4cb515ed9..c950c3688 100644 --- a/apps/daemon/internal/agent/mcode/executor_native_test.go +++ b/apps/daemon/internal/agent/mcode/executor_native_test.go @@ -29,11 +29,10 @@ func TestNativeMCodeExecutorReuse(t *testing.T) { t.Fatal("private provider options unavailable") } req := testRequest(t) - req.RunID, req.Input = "", nil if json.Unmarshal(raw, &req) != nil { t.Fatal("invalid private provider options") } - value, err := NewExecutorFactory(nil)(ctx, req) + value, err := NewExecutorFactory(nil)(ctx, prepared(t, req)) if err != nil { t.Fatal("native Executor preparation failed") } @@ -156,7 +155,7 @@ func TestNativeMCodeExecutorReuse(t *testing.T) { } cleanupStop() req.AgentSessionID = nativeID - recovered, recoverErr := NewExecutorFactory(nil)(ctx, req) + recovered, recoverErr := NewExecutorFactory(nil)(ctx, prepared(t, req)) if recoverErr != nil || recovered == nil { t.Fatal("exact native history recovery failed") } diff --git a/apps/daemon/internal/agent/mcode/executor_test.go b/apps/daemon/internal/agent/mcode/executor_test.go index 210cbffa9..160ae43b6 100644 --- a/apps/daemon/internal/agent/mcode/executor_test.go +++ b/apps/daemon/internal/agent/mcode/executor_test.go @@ -13,12 +13,13 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func workspaceFixture(t *testing.T) (WorkspaceConfig, proto.PromptRequestPayload, string) { +func workspaceFixture(t *testing.T) (WorkspaceConfig, agent.PrepareRequest, string) { t.Helper() r := testRequest(t) - r.RunID, r.Input = "", nil r.DisableExecutionEnvironment = false - r.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()} + r.LocalEnvironment = &proto.LocalEnvironment{ID: "environment"} + req := prepared(t, r) + req.WorkspaceRoot = t.TempDir() record := filepath.Join(t.TempDir(), "calls") exe, err := os.Executable() if err != nil { @@ -30,7 +31,7 @@ func workspaceFixture(t *testing.T) (WorkspaceConfig, proto.PromptRequestPayload if err := os.WriteFile(binary, []byte(script), 0700); err != nil { t.Fatal(err) } - return WorkspaceConfig{Binary: binary, Node: "/usr/bin/node", Bridge: "/opt/bridge.mjs", Directory: r.LocalEnvironment.WorkspaceRoot, Network: "enabled", Scratch: t.TempDir()}, r, record + return WorkspaceConfig{Binary: binary, Node: "/usr/bin/node", Bridge: "/opt/bridge.mjs", Directory: req.WorkspaceRoot, Network: "enabled", Scratch: t.TempDir()}, req, record } func executorFixture(t *testing.T, scenario string, workspace bool) (*executor, string) { @@ -47,8 +48,7 @@ func executorFixture(t *testing.T, scenario string, workspace bool) (*executor, if workspace { factory = NewExecutorFactory(&config) } else { - req = testRequest(t) - req.RunID, req.Input = "", nil + req = prepared(t, testRequest(t)) t.Setenv("OAC_RUNTIME_MCODE_BIN", config.Binary) factory = NewExecutorFactory(nil) } diff --git a/apps/daemon/internal/agent/mcode/harness_config_test.go b/apps/daemon/internal/agent/mcode/harness_config_test.go deleted file mode 100644 index 6eeffe63b..000000000 --- a/apps/daemon/internal/agent/mcode/harness_config_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package mcode - -import ( - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" -) - -func TestNativeConfigDoesNotPretendToSupportParameters(t *testing.T) { - req := testRequest(t) - req.HarnessConfig = proto.HarnessConfig(`{}`) - if _, err := prepareOptions(req); err != nil { - t.Fatal(err) - } - req.HarnessConfig = proto.HarnessConfig(`{"temperature":0.5}`) - if _, err := prepareOptions(req); err != harnessconfig.ErrHarnessConfig { - t.Fatalf("unsupported parameter accepted: %v", err) - } -} diff --git a/apps/daemon/internal/agent/mcode/mcp_observations_test.go b/apps/daemon/internal/agent/mcode/mcp_observations_test.go index 4a80eb133..cad5df984 100644 --- a/apps/daemon/internal/agent/mcode/mcp_observations_test.go +++ b/apps/daemon/internal/agent/mcode/mcp_observations_test.go @@ -16,10 +16,9 @@ func mcpObservationSession(t *testing.T) (*Session, chan proto.Envelope) { t.Helper() out := make(chan proto.Envelope, 16) s := &Session{ctx: context.Background(), outputContext: context.Background(), opts: launchOptions{DataDir: t.TempDir()}, - req: proto.PromptRequestPayload{RunID: "run", - LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{environmentMCPFixture()}}}, + req: proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}}, runID: "run", out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}, completedMessages: map[string]bool{}, active: true, sessionID: "native-session"} - resolveTestBindings(s) + s.opts.bindings, _ = agent.ResolveMCPBindings(agent.PrepareRequest{PromptRequestPayload: s.req, MCP: []agent.EnvironmentMCP{environmentMCPFixture()}}) if err := writeMCPRegistry(s.opts.DataDir, mcpRegistryEntry("proof.server", "proof_server_2", "read.status", "read_status_2")); err != nil { t.Fatal(err) } @@ -198,16 +197,10 @@ func TestEnvironmentMCPNativeJSONRetainsIntegerPrecision(t *testing.T) { // Both declaration sources must produce the same native observation semantics. func usePublicMCP(s *Session) { - s.req.LocalEnvironment.MCP = nil s.req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ ConnectionOrigin: "environment", ServerLabel: "proof.server", ServerURL: "https://mcp.example.test", }} - resolveTestBindings(s) -} - -// resolveTestBindings records the request's bindings as preparation does. -func resolveTestBindings(s *Session) { - s.opts.bindings, _ = agent.ResolveMCPBindings(s.req) + s.opts.bindings, _ = agent.ResolveMCPBindings(agent.PrepareRequest{PromptRequestPayload: s.req}) } func TestResumedMessageFailsTheTurn(t *testing.T) { diff --git a/apps/daemon/internal/agent/mcode/model_provider_test.go b/apps/daemon/internal/agent/mcode/model_provider_test.go index 3896ce5fb..f14b6c55c 100644 --- a/apps/daemon/internal/agent/mcode/model_provider_test.go +++ b/apps/daemon/internal/agent/mcode/model_provider_test.go @@ -18,7 +18,7 @@ func TestOptionsModelProviderProtocols(t *testing.T) { req := testRequest(t) req.ModelProvider.Protocol = modelprovider.Protocol(tc.protocol) req.Model = "chosen-model" - opts, err := prepareOptions(req) + opts, err := prepareOptions(prepared(t, req)) if err != nil { t.Fatal(err) } @@ -44,31 +44,3 @@ func TestOptionsModelProviderProtocols(t *testing.T) { }) } } - -func TestOptionsRejectInvalidProvider(t *testing.T) { - for name, edit := range map[string]func(*modelprovider.Provider){ - "unknown protocol": func(p *modelprovider.Provider) { p.Protocol = "unknown" }, - "protocol alias": func(p *modelprovider.Provider) { p.Protocol = "chat-completions" }, - "remote HTTP": func(p *modelprovider.Provider) { p.BaseURL = "http://provider.example" }, - "empty key": func(p *modelprovider.Provider) { p.APIKey = "" }, - "missing context": func(p *modelprovider.Provider) { p.ContextWindow = 0 }, - "missing output": func(p *modelprovider.Provider) { p.MaxOutputTokens = 0 }, - "excess output": func(p *modelprovider.Provider) { p.MaxOutputTokens = 64001 }, - } { - t.Run(name, func(t *testing.T) { - req := testRequest(t) - edit(req.ModelProvider) - if _, err := prepareOptions(req); err == nil { - t.Fatal("invalid model provider accepted") - } - }) - } -} - -func TestOptionsAllowLoopbackProviderFixture(t *testing.T) { - req := testRequest(t) - req.ModelProvider.BaseURL = "http://127.0.0.1:4321" - if _, err := prepareOptions(req); err != nil { - t.Fatal(err) - } -} diff --git a/apps/daemon/internal/agent/mcode/native_test.go b/apps/daemon/internal/agent/mcode/native_test.go index ad21c9f94..ba69e75b7 100644 --- a/apps/daemon/internal/agent/mcode/native_test.go +++ b/apps/daemon/internal/agent/mcode/native_test.go @@ -41,12 +41,12 @@ func TestNativeMCodeACP(t *testing.T) { defer model.Close() req.ModelProvider = &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: model.URL, APIKey: "fixture-only", ContextWindow: 64000, MaxOutputTokens: 4096} req.SystemPrompt = "SP-MCODE-672: reply concisely." - req.Input = proto.TextInput("Reply OAC-MCODE-OK.") + runID, input := "run-1", proto.TextInput("Reply OAC-MCODE-OK.") run := func() proto.DonePayload { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) t.Cleanup(cancel) out := make(chan proto.Envelope, 64) - if _, err := startTurn(t, ctx, req, out); err != nil { + if _, err := startTurn(t, ctx, req, runID, input, out); err != nil { t.Fatal(err) } var done proto.DonePayload @@ -77,11 +77,11 @@ func TestNativeMCodeACP(t *testing.T) { if !strings.Contains(firstRequests, "SP-MCODE-672") { t.Fatal("native instructions missing") } - req.RunID = "run-2" + runID = "run-2" req.AgentSessionID = done.Metadata[proto.DoneMetaAgentSessionID].(string) req.SystemPrompt = "SP-MCODE-NEW: reply concisely." req.Model = "fixture-new" - req.Input = proto.TextInput("Now reply OAC-MCODE-OK.") + input = proto.TextInput("Now reply OAC-MCODE-OK.") run() mu.Lock() resumed := strings.Join(requests, "\n") diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index 439101514..93f4f14c5 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -15,8 +15,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - harnessconfiguration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode" ) type launchOptions struct { @@ -42,15 +40,15 @@ type launchOptions struct { home string } -func prepareOptions(req proto.PromptRequestPayload) (launchOptions, error) { +func prepareOptions(req agent.PrepareRequest) (launchOptions, error) { return prepareOptionsWithTools(req, nil) } // prepareOptionsWithTools prepares the Session's native data directory. With // tools, the workspace bridge presents the Environment's workspace and Skills. -func prepareOptionsWithTools(req proto.PromptRequestPayload, tools *workspaceTools) (launchOptions, error) { +func prepareOptionsWithTools(req agent.PrepareRequest, tools *workspaceTools) (launchOptions, error) { var result launchOptions - prepared, err := validateOptions(req) + err := validateExecutionRequest(req.PromptRequestPayload) if err != nil { return result, err } @@ -59,7 +57,7 @@ func prepareOptionsWithTools(req proto.PromptRequestPayload, tools *workspaceToo return result, err } } - if result.DataDir, err = dataDirectory(req); err != nil { + if result.DataDir, err = dataDirectory(req.StateKey); err != nil { return result, err } result.Dir = filepath.Join(result.DataDir, "workspace") @@ -74,33 +72,20 @@ func prepareOptionsWithTools(req proto.PromptRequestPayload, tools *workspaceToo return result, err } defer data.Close() - if err := writeNativeConfig(req, prepared, data, result.DataDir, tools); err != nil { + if err := writeNativeConfig(req, data, result.DataDir, tools); err != nil { return result, err } - result.Model = prepared.Model - result.Env = append(executionEnvironment(), nativeEnvironment(req, result.DataDir)...) + result.Model = req.Prepared.Model + result.Env = append(executionEnvironment(), nativeEnvironment(req.PromptRequestPayload, result.DataDir)...) result.MCP = []map[string]any{} return result, nil } -// validateOptions checks the request before any native effect and returns its -// model configuration. -func validateOptions(req proto.PromptRequestPayload) (harnessconfig.PreparedConfiguration, error) { - prepared, err := harnessconfiguration.Configuration().Prepare(req) - if err != nil { - return prepared, err - } - if err := validateExecutionRequest(req); err != nil { - return prepared, err - } - return prepared, nil -} - // writeNativeConfig writes the instructions and native configuration into the // data directory, which the native process sees at dataDir. With tools, the // workspace bridge replaces native permissions and sandbox, and Subagents use // it too. -func writeNativeConfig(req proto.PromptRequestPayload, prepared harnessconfig.PreparedConfiguration, data *os.Root, dataDir string, tools *workspaceTools) error { +func writeNativeConfig(req agent.PrepareRequest, data *os.Root, dataDir string, tools *workspaceTools) error { if len(req.SystemPrompt) > 32*1024 { return fmt.Errorf("mcode: combined instructions exceed the CLI's 32 KiB limit") } @@ -108,7 +93,7 @@ func writeNativeConfig(req proto.PromptRequestPayload, prepared harnessconfig.Pr return err } config := map[string]any{"logLevel": "error", "skills": map[string]any{"external": map[string]any{"enabled": false}}} - config["custom_provider"] = map[string]any{"oac": modelProviderConfig(prepared.Provider, prepared.Model)} + config["custom_provider"] = map[string]any{"oac": modelProviderConfig(req.Prepared.Provider, req.Prepared.Model)} configureTextExecution(config) if !req.DisableSubagents { config["agents"] = map[string]any{"default": map[string]any{ @@ -209,21 +194,21 @@ func (o launchOptions) readData(name string) ([]byte, error) { return root.ReadFile(rel) } -// dataDirectory returns the native data directory of the request's agent -// state. It never derives runtime state from the subprocess cwd. -func dataDirectory(req proto.PromptRequestPayload) (string, error) { +// dataDirectory returns the native data directory of the Session's state +// key. It never derives runtime state from the subprocess cwd. +func dataDirectory(stateKey string) (string, error) { root, err := paths.Root() if err != nil { return "", fmt.Errorf("mcode: resolve data directory: %w", err) } parts := []string{root, "runtime", "mcode", "state"} - for _, part := range strings.Split(req.AgentStateKey, "/") { + for _, part := range strings.Split(stateKey, "/") { if safe := safePathPart(part); safe != "" { parts = append(parts, safe) } } if len(parts) == 4 { - return "", fmt.Errorf("mcode: invalid agent state key %q", req.AgentStateKey) + return "", fmt.Errorf("mcode: invalid agent state key %q", stateKey) } return filepath.Join(parts...), nil } diff --git a/apps/daemon/internal/agent/mcode/options_test.go b/apps/daemon/internal/agent/mcode/options_test.go index 59a8ea559..a6396ac82 100644 --- a/apps/daemon/internal/agent/mcode/options_test.go +++ b/apps/daemon/internal/agent/mcode/options_test.go @@ -13,7 +13,7 @@ import ( func TestOptionsRefreshManagedState(t *testing.T) { t.Setenv("MINIMAX_DATA_DIR", "/wrong") req := testRequest(t) - opts, err := prepareOptions(req) + opts, err := prepareOptions(prepared(t, req)) if err != nil { t.Fatal(err) } @@ -31,7 +31,7 @@ func TestOptionsRefreshManagedState(t *testing.T) { } req.SystemPrompt = "" req.AgentSessionID = "native-1" - refreshed, err := prepareOptions(req) + refreshed, err := prepareOptions(prepared(t, req)) if err != nil { t.Fatal(err) } @@ -68,14 +68,12 @@ func TestOptionsRejectDroppedContext(t *testing.T) { edit func(*proto.PromptRequestPayload) }{ {"oversized instructions", func(r *proto.PromptRequestPayload) { r.SystemPrompt = strings.Repeat("x", 32*1024+1) }}, - {"missing model", func(r *proto.PromptRequestPayload) { r.Model = "" }}, - {"missing provider", func(r *proto.PromptRequestPayload) { r.ModelProvider = nil }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := testRequest(t) tt.edit(&req) - if _, err := prepareOptions(req); err == nil { + if _, err := prepareOptions(prepared(t, req)); err == nil { t.Fatal("expected validation failure") } }) @@ -86,11 +84,11 @@ func TestDataDirectoryRequiresAgentState(t *testing.T) { home := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", home) for _, key := range []string{"", " ", "../.."} { - if _, err := dataDirectory(proto.PromptRequestPayload{AgentStateKey: key, RunID: "ignored"}); err == nil { + if _, err := dataDirectory(key); err == nil { t.Fatalf("state key %q accepted", key) } } - got, err := dataDirectory(proto.PromptRequestPayload{AgentStateKey: "../session-1/a b"}) + got, err := dataDirectory("../session-1/a b") if want := filepath.Join(home, "runtime", "mcode", "state", "session-1", "a_b"); err != nil || got != want { t.Fatalf("data directory = %q, %v; want %q", got, err, want) } diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index eb8230dad..ea574ef16 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -34,6 +34,7 @@ type Session struct { frames chan rpcFrame finished chan struct{} mu sync.Mutex + runID string sessionID string nativeModel string outputContext context.Context @@ -251,7 +252,7 @@ func (s *Session) call(method string, params any, result any, prompt bool) error } func (s *Session) emit(kind string, payload any) { - env, err := proto.NewEnvelope(kind, s.req.RunID, payload) + env, err := proto.NewEnvelope(kind, s.runID, payload) if err != nil { return } diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index d49709610..fcd746ee5 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) @@ -18,8 +19,7 @@ import ( func testRequest(t *testing.T) proto.PromptRequestPayload { t.Helper() t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - return proto.PromptRequestPayload{RunID: "run-1", AgentStateKey: "conversation-1/agent-1/mcode", Input: proto.TextInput("Hello"), - Model: "fixture", SystemPrompt: "Current instructions", + return proto.PromptRequestPayload{Model: "fixture", SystemPrompt: "Current instructions", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://provider.example", APIKey: "fixture-key", ContextWindow: 64000, MaxOutputTokens: 4096}, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} } @@ -45,12 +45,21 @@ func helperRequest(t *testing.T, scenario string, resume bool) proto.PromptReque return req } -// prepareExecutor prepares req without its Turn input; cleanup closes the -// Executor and reaps its CLI. +// prepared is req as the registry hands it to the factory, with the state key +// of one Session. +func prepared(t testing.TB, req proto.PromptRequestPayload) agent.PrepareRequest { + t.Helper() + configuration, err := Declaration.Configuration.Prepare(req) + if err != nil { + t.Fatal(err) + } + return agent.PrepareRequest{PromptRequestPayload: req, Prepared: configuration, StateKey: "session-state"} +} + +// prepareExecutor prepares req; cleanup closes the Executor and reaps its CLI. func prepareExecutor(t *testing.T, ctx context.Context, req proto.PromptRequestPayload) (*executor, error) { t.Helper() - req.RunID, req.Input = "", nil - value, err := NewExecutorFactory(nil)(ctx, req) + value, err := NewExecutorFactory(nil)(ctx, prepared(t, req)) if value == nil { return nil, err } @@ -65,14 +74,14 @@ func prepareExecutor(t *testing.T, ctx context.Context, req proto.PromptRequestP return e, err } -// startTurn prepares an Executor for req and starts req.Input as its Turn. -func startTurn(t *testing.T, ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (*Session, error) { +// startTurn prepares an Executor for req and starts input as its Turn run. +func startTurn(t *testing.T, ctx context.Context, req proto.PromptRequestPayload, run string, input proto.MessageInput, out chan<- proto.Envelope) (*Session, error) { t.Helper() e, err := prepareExecutor(t, ctx, req) if err != nil { return nil, err } - turn, err := e.StartTurn(ctx, req.RunID, req.Input, out) + turn, err := e.StartTurn(ctx, run, input, out) if err != nil { return nil, err } @@ -85,7 +94,7 @@ func helperSession(t *testing.T, scenario string, resume bool) (*Session, <-chan ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) out := make(chan proto.Envelope, 32) - session, err := startTurn(t, ctx, req, out) + session, err := startTurn(t, ctx, req, "run-1", proto.TextInput("Hello"), out) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/mcode/tool_environment_test.go b/apps/daemon/internal/agent/mcode/tool_environment_test.go index ab6accd5a..f7a995ce6 100644 --- a/apps/daemon/internal/agent/mcode/tool_environment_test.go +++ b/apps/daemon/internal/agent/mcode/tool_environment_test.go @@ -39,9 +39,8 @@ func TestWorkspaceCredentialsRemainInRuntimeSnapshotAcrossReconnect(t *testing.T t.Setenv("OAC_RUNTIME_CAPABILITY_DIRECTORY", t.TempDir()) t.Setenv("OAC_RUNTIME_NETWORK_ACCESS", "enabled") t.Setenv("OAC_RUNTIME_ALLOWED_DOMAINS", "") - req.AgentStateKey = "agents-api-" + session + req.StateKey = "agents-api-" + session req.LocalEnvironment.ID, req.LocalEnvironment.WorkspaceDirectory = environment, config.Directory - req.LocalEnvironment.Capabilities = true req.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{plugin}} for _, resume := range []bool{false, true} { if resume { @@ -52,15 +51,14 @@ func TestWorkspaceCredentialsRemainInRuntimeSnapshotAcrossReconnect(t *testing.T if err != nil { t.Fatal(err) } - configured, err := binding.Configure(req) - if err != nil { + if err := binding.Configure(req.PromptRequestPayload); err != nil { t.Fatal(err) } - prepared, err := binding.Prepare(t.Context(), configured) + bound, err := binding.Prepare(t.Context(), req) if err != nil { t.Fatal(err) } - opts, err := prepareWorkspaceOptions(config, prepared) + opts, err := prepareWorkspaceOptions(config, bound) if err != nil { t.Fatal(err) } @@ -108,11 +106,10 @@ func TestWorkspaceCredentialsRemainInRuntimeSnapshotAcrossReconnect(t *testing.T if err != nil { t.Fatal(err) } - configured, err := binding.Configure(req) - if err != nil { + if err := binding.Configure(req.PromptRequestPayload); err != nil { t.Fatal(err) } - if _, err := binding.Prepare(t.Context(), configured); err == nil { + if _, err := binding.Prepare(t.Context(), req); err == nil { t.Fatal("missing frozen credential source was recreated or fell back to anonymous") } } diff --git a/apps/daemon/internal/agent/mcode/tool_observations_test.go b/apps/daemon/internal/agent/mcode/tool_observations_test.go index 703e3cb90..60be5f2db 100644 --- a/apps/daemon/internal/agent/mcode/tool_observations_test.go +++ b/apps/daemon/internal/agent/mcode/tool_observations_test.go @@ -12,7 +12,7 @@ func TestWorkspaceCommandObservationsWaitForArgumentsAndRetainOutcome(t *testing for _, status := range []string{"completed", "failed"} { t.Run(status, func(t *testing.T) { out := make(chan proto.Envelope, 8) - s := &Session{ctx: context.Background(), outputContext: context.Background(), req: proto.PromptRequestPayload{RunID: "run"}, out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} + s := &Session{ctx: context.Background(), outputContext: context.Background(), runID: "run", out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} s.emitTool(toolUpdate{ID: "call", Name: "mcp__oac_workspace__workspace_bash"}) if len(out) != 0 { t.Fatal("command item emitted before native arguments") diff --git a/apps/daemon/internal/agent/mcode/view.go b/apps/daemon/internal/agent/mcode/view.go index 86a17290a..35c13416a 100644 --- a/apps/daemon/internal/agent/mcode/view.go +++ b/apps/daemon/internal/agent/mcode/view.go @@ -12,7 +12,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/viewloader" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // In an agent-host view, node runs the CLI from the closure and the native @@ -136,8 +135,8 @@ func (i viewInstall) view() agent.View { return view } -func (i viewInstall) executor(ctx context.Context, req proto.PromptRequestPayload, session agent.ViewSession) (agent.Executor, error) { - return startExecutor(ctx, req, i.node, func() (launchOptions, error) { +func (i viewInstall) executor(ctx context.Context, req agent.PrepareRequest, session agent.ViewSession) (agent.Executor, error) { + return startExecutor(ctx, req.PromptRequestPayload, i.node, func() (launchOptions, error) { return i.prepare(req, session) }) } @@ -147,27 +146,26 @@ func (i viewInstall) executor(ctx context.Context, req proto.PromptRequestPayloa // names and the MCP in session. The request's workspace is the sandbox's, and // the workspace tools present it; with environment none the CLI runs in the // work directory without them. -func (i viewInstall) prepare(req proto.PromptRequestPayload, session agent.ViewSession) (launchOptions, error) { +func (i viewInstall) prepare(req agent.PrepareRequest, session agent.ViewSession) (launchOptions, error) { local := req.LocalEnvironment if (local == nil) != req.DisableExecutionEnvironment || req.WorkspaceReadOnly { return launchOptions{}, fmt.Errorf("%w: a MiniMax Code view runs Agents API execution in a writable Environment workspace or with environment none", agent.ErrUnsupportedOperation) } dir := path.Join(session.Home.View, agent.ViewWorkName) if local != nil { - dir = local.WorkspaceRoot + dir = req.WorkspaceRoot } if !path.IsAbs(dir) || path.Clean(dir) != dir || dir == "/" { return launchOptions{}, errors.New("mcode: the workspace is not a canonical absolute path") } // The Harness runs each stdio alias without arguments. - servers, err := workspaceMCP(session.MCP, func(stdio proto.EnvironmentMCP) (string, []string) { return stdio.Server.Command, []string{} }) + servers, err := workspaceMCP(session.MCP, func(stdio agent.EnvironmentMCP) (string, []string) { return stdio.Server.Command, []string{} }) if err != nil { return launchOptions{}, err } private := req private.LocalEnvironment, private.DisableExecutionEnvironment, private.MCPHTTPServers = nil, true, nil - prepared, err := validateOptions(private) - if err != nil { + if err := validateExecutionRequest(private.PromptRequestPayload); err != nil { return launchOptions{}, err } @@ -193,13 +191,13 @@ func (i viewInstall) prepare(req proto.PromptRequestPayload, session agent.ViewS var tools *workspaceTools opts.MCP = []map[string]any{} if local != nil { - tools = &workspaceTools{node: i.node, bridge: i.bridge, profile: map[string]any{"workspace": dir, "scratch": tempDir, "network": "enabled"}, skills: local.Skills} + tools = &workspaceTools{node: i.node, bridge: i.bridge, profile: map[string]any{"workspace": dir, "scratch": tempDir, "network": "enabled"}, skills: req.Skills} opts.MCP = append(opts.MCP, tools.server(dataDir)) } - if err := writeNativeConfig(private, prepared, data, dataDir, tools); err != nil { + if err := writeNativeConfig(private, data, dataDir, tools); err != nil { return opts, err } - opts.Model = prepared.Model + opts.Model = req.Prepared.Model opts.MCP = append(opts.MCP, servers...) opts.Env = []string{ "PATH=" + path.Join(agent.ViewPrivateRoot, agent.ViewShimName), @@ -212,7 +210,7 @@ func (i viewInstall) prepare(req proto.PromptRequestPayload, session agent.ViewS if i.loader.LibraryPath != "" { opts.Env = append(opts.Env, "LD_LIBRARY_PATH="+i.loader.LibraryPath) } - opts.Env = append(opts.Env, nativeEnvironment(private, dataDir)...) + opts.Env = append(opts.Env, nativeEnvironment(private.PromptRequestPayload, dataDir)...) opts.spawn = session.Spawn opts.reader = clirunner.StartOptions{Binary: i.node, Args: []string{path.Join(path.Dir(i.bridge), "subagent-snapshot.mjs"), dataDir}, Dir: dataDir, Env: opts.Env} return opts, nil diff --git a/apps/daemon/internal/agent/mcode/view_test.go b/apps/daemon/internal/agent/mcode/view_test.go index cbfa4fe92..cd75e9ad6 100644 --- a/apps/daemon/internal/agent/mcode/view_test.go +++ b/apps/daemon/internal/agent/mcode/view_test.go @@ -25,7 +25,7 @@ const viewRealKey = "sk-view-real-key-sentinel" // viewFixture registers a view over a fake install and returns it with a // request as the agent host hands it over. -func viewFixture(t *testing.T) (viewInstall, agent.View, proto.PromptRequestPayload) { +func viewFixture(t *testing.T) (viewInstall, agent.View, agent.PrepareRequest) { t.Helper() harness, bin, libs := t.TempDir(), t.TempDir(), t.TempDir() for _, file := range []string{"bridge.mjs", "native/cli.js", "native/assets/skills/.keep", "native/assets/agents/.keep", filepath.Join(bin, "node")} { @@ -59,11 +59,12 @@ func viewFixture(t *testing.T) (viewInstall, agent.View, proto.PromptRequestPayl t.Setenv("OAC_TEST_VIEW_SENTINEL", "daemon-only") t.Setenv("ANTHROPIC_API_KEY", viewRealKey) req := testRequest(t) - req.RunID, req.Input = "", nil req.DisableExecutionEnvironment = false - req.LocalEnvironment = &proto.LocalEnvironment{WorkspaceRoot: "/workspace", NetworkAccess: "enabled"} + req.LocalEnvironment = &proto.LocalEnvironment{} req.ModelProvider = &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "http://127.0.0.1:4101", APIKey: modelprovider.Placeholder, ContextWindow: 64000, MaxOutputTokens: 4096} - return install, view, req + bound := prepared(t, req) + bound.WorkspaceRoot = "/workspace" + return install, view, bound } func viewSession(launched *clirunner.StartOptions) agent.ViewSession { @@ -129,15 +130,15 @@ func TestViewRunsEnvironmentNoneStdioAliasesAndSkills(t *testing.T) { install, _, req := viewFixture(t) session := agent.ViewSession{Home: agent.ViewDir{Host: t.TempDir(), View: path.Join(agent.ViewPrivateRoot, agent.ViewHomeName)}} none := req - none.LocalEnvironment, none.DisableExecutionEnvironment = nil, true + none.LocalEnvironment, none.DisableExecutionEnvironment, none.WorkspaceRoot = nil, true, "" opts, err := install.prepare(none, session) if err != nil || opts.Dir != "/.oac/home/work" || opts.MCP == nil || len(opts.MCP) != 0 { t.Fatalf("environment none runs in %q with MCP %v: %v", opts.Dir, opts.MCP, err) } - session.MCP = []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &proto.EnvironmentMCP{ + session.MCP = []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &agent.EnvironmentMCP{ Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: agent.ViewAlias(0)}}}} - req.LocalEnvironment.CapabilityRoot, req.LocalEnvironment.Skills = agentcapabilities.Directory, []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, + req.CapabilityRoot, req.Skills = agentcapabilities.Directory, []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, Metadata: agentskill.Metadata{Type: "inline", Name: "review", Description: "Review."}, RelativeRoot: "skills/review", PackageRoot: "skills/review"}} if opts, err = install.prepare(req, session); err != nil || len(opts.MCP) != 2 || opts.MCP[0]["name"] != "oac_workspace" || opts.MCP[1]["command"] != agent.ViewAlias(0) { t.Fatalf("stdio MCP = %v: %v", opts.MCP, err) diff --git a/apps/daemon/internal/agent/mcode/workspace.go b/apps/daemon/internal/agent/mcode/workspace.go index 68219c142..a3aec4f26 100644 --- a/apps/daemon/internal/agent/mcode/workspace.go +++ b/apps/daemon/internal/agent/mcode/workspace.go @@ -6,9 +6,9 @@ import ( "path/filepath" "runtime" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" ) @@ -49,19 +49,19 @@ func ConfigureLocal(binary, node, bridge, root, workspace string, network agentn return c, nil } -func prepareWorkspaceOptions(c WorkspaceConfig, req proto.PromptRequestPayload) (launchOptions, error) { +func prepareWorkspaceOptions(c WorkspaceConfig, req agent.PrepareRequest) (launchOptions, error) { if c.Network != "enabled" || len(c.AllowedDomains) != 0 { return launchOptions{}, fmt.Errorf("mcode: Runtime does not implement network isolation") } - if req.LocalEnvironment == nil || req.LocalEnvironment.WorkspaceRoot != c.Directory || req.DisableExecutionEnvironment || !(agentnetwork.Policy{Access: c.Network, AllowedDomains: c.AllowedDomains}).Equal(agentnetwork.Policy{Access: req.LocalEnvironment.NetworkAccess, AllowedDomains: req.LocalEnvironment.AllowedDomains}) || req.WorkspaceReadOnly { + if req.LocalEnvironment == nil || req.WorkspaceRoot != c.Directory || req.DisableExecutionEnvironment || req.WorkspaceReadOnly { return launchOptions{}, fmt.Errorf("mcode: execution does not match the dedicated workspace") } servers, bindings, err := runtimeMCP(req) if err != nil { return launchOptions{}, err } - tools := workspaceTools{node: c.Node, bridge: c.Bridge, profile: map[string]any{"capabilityRoot": req.LocalEnvironment.CapabilityRoot, "workspace": c.Directory, "scratch": c.Scratch, "network": c.Network, "allowedDomains": (agentnetwork.Policy{Access: c.Network, AllowedDomains: c.AllowedDomains}).Hosts(), "skills": len(req.LocalEnvironment.Skills) > 0}, - skills: req.LocalEnvironment.Skills} + tools := workspaceTools{node: c.Node, bridge: c.Bridge, profile: map[string]any{"capabilityRoot": req.CapabilityRoot, "workspace": c.Directory, "scratch": c.Scratch, "network": c.Network, "allowedDomains": (agentnetwork.Policy{Access: c.Network, AllowedDomains: c.AllowedDomains}).Hosts(), "skills": len(req.Skills) > 0}, + skills: req.Skills} file, err := localworkspace.ToolEnvironmentFile() if err != nil { return launchOptions{}, err diff --git a/apps/daemon/internal/agent/mcode/workspace_network_test.go b/apps/daemon/internal/agent/mcode/workspace_network_test.go index 83b3156ee..eb158cb52 100644 --- a/apps/daemon/internal/agent/mcode/workspace_network_test.go +++ b/apps/daemon/internal/agent/mcode/workspace_network_test.go @@ -6,7 +6,6 @@ func TestWorkspaceRejectsInnerNetworkIsolation(t *testing.T) { for _, access := range []string{"disabled", "restricted"} { c, req, _ := workspaceFixture(t) c.Network = access - req.LocalEnvironment.NetworkAccess = access if _, err := prepareWorkspaceOptions(c, req); err == nil { t.Fatal("unsupported network isolation accepted") } diff --git a/apps/daemon/internal/agent/mcode/workspace_skills_test.go b/apps/daemon/internal/agent/mcode/workspace_skills_test.go index 0d7c9470d..1a847d515 100644 --- a/apps/daemon/internal/agent/mcode/workspace_skills_test.go +++ b/apps/daemon/internal/agent/mcode/workspace_skills_test.go @@ -22,8 +22,8 @@ func TestWorkspaceSkillsUseSelectedSnapshotAndNativeLoader(t *testing.T) { if err := os.WriteFile(filepath.Join(path, "SKILL.md"), []byte(body), 0600); err != nil { t.Fatal(err) } - req.LocalEnvironment.CapabilityRoot = root - req.LocalEnvironment.Skills = []agentcapabilities.InstalledSkill{{ + req.CapabilityRoot = root + req.Skills = []agentcapabilities.InstalledSkill{{ InstallationRoot: root, RelativeRoot: "plugin/skills/proof", PackageRoot: "plugin", Metadata: agentskill.Metadata{Name: "proof", Description: "Read a marker"}, }} diff --git a/apps/daemon/internal/agent/mcp_binding.go b/apps/daemon/internal/agent/mcp_binding.go index 94c157b69..ef30c1dfc 100644 --- a/apps/daemon/internal/agent/mcp_binding.go +++ b/apps/daemon/internal/agent/mcp_binding.go @@ -7,7 +7,6 @@ import ( "slices" "strings" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" ) @@ -25,7 +24,17 @@ type MCPBinding struct { BearerToken *string HTTPHeaders map[string]string // Stdio retains the installed package identity for the fixed Runtime launcher. - Stdio *proto.EnvironmentMCP + Stdio *EnvironmentMCP +} + +// EnvironmentMCP is one MCP server installed in the Environment. Never log it: +// HTTP headers and the selected bearer may be confidential. +type EnvironmentMCP struct { + InstallationRoot string + WorkspaceRoot string + PackageRoot string + Server agentplugin.MCPServer + BearerToken *string } // EnvironmentMCPCredentials reports whether an installed MCP server takes @@ -39,12 +48,12 @@ func EnvironmentMCPCredentials(server agentplugin.MCPServer) bool { // ResolveMCPBindings combines public declarations with the frozen installation. // Public declarations retain explicit origin and Vault authority; installed MCP // retains Environment configuration authority. Neither may relocate implicitly. -func ResolveMCPBindings(req proto.PromptRequestPayload) ([]MCPBinding, error) { +func ResolveMCPBindings(req PrepareRequest) ([]MCPBinding, error) { var bindings []MCPBinding if req.MCPHTTPServers != nil { bindings = make([]MCPBinding, 0, len(*req.MCPHTTPServers)) for _, server := range *req.MCPHTTPServers { - if err := server.ValidateConnectionOrigin(req); err != nil { + if err := server.ValidateConnectionOrigin(req.PromptRequestPayload); err != nil { return nil, err } item := MCPBinding{ServerLabel: server.ServerLabel, ConnectionOrigin: server.ConnectionOrigin, CredentialAuthority: "none", Transport: "http", ServerURL: server.ServerURL, Required: server.Required, BearerToken: server.BearerToken} @@ -58,11 +67,11 @@ func ResolveMCPBindings(req proto.PromptRequestPayload) ([]MCPBinding, error) { bindings = append(bindings, item) } } - if local := req.LocalEnvironment; local != nil && len(local.MCP) > 0 { - if req.DisableExecutionEnvironment || local.NetworkAccess != "enabled" { - return nil, errors.New("environment MCP requires an enabled workspace network") + if len(req.MCP) > 0 { + if req.DisableExecutionEnvironment || req.LocalEnvironment == nil { + return nil, errors.New("environment MCP requires a workspace") } - for _, installed := range local.MCP { + for _, installed := range req.MCP { declaration := installed.Server item := MCPBinding{ServerLabel: declaration.Name, ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: declaration.Type, ServerURL: declaration.URL, BearerToken: installed.BearerToken, HTTPHeaders: maps.Clone(declaration.HTTPHeaders)} if declaration.BearerTokenEnvVar != "" && installed.BearerToken == nil { diff --git a/apps/daemon/internal/agent/mcp_binding_test.go b/apps/daemon/internal/agent/mcp_binding_test.go index 77f8a3019..05ba5a330 100644 --- a/apps/daemon/internal/agent/mcp_binding_test.go +++ b/apps/daemon/internal/agent/mcp_binding_test.go @@ -12,7 +12,7 @@ func TestMCPBindingsPreserveOriginAuthorityAndPolicy(t *testing.T) { empty := []string{} for _, allow := range []*[]string{nil, &empty} { public := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp", AllowedTools: allow, Required: true, BearerToken: &token}} - got, err := ResolveMCPBindings(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &public}) + got, err := ResolveMCPBindings(PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &public}}) if err != nil || len(got) != 1 { t.Fatal("public binding unavailable", err) } @@ -25,12 +25,12 @@ func TestMCPBindingsPreserveOriginAuthorityAndPolicy(t *testing.T) { t.Fatal("binding retained mutable credential pointer") } } - local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{ + installed := []EnvironmentMCP{ {Server: agentplugin.MCPServer{Name: "stdio", Type: "stdio", Command: "node", Args: []string{"tool.js"}}, PackageRoot: "plugins/proof", InstallationRoot: "/private/installed"}, {Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp", HTTPHeaders: map[string]string{"X-Selected": "literal"}}, BearerToken: &token}, {Server: agentplugin.MCPServer{Name: "configured", Type: "stdio", Command: "node", EnvVars: []string{"TOOL_TOKEN"}}, PackageRoot: "plugins/proof", InstallationRoot: "/private/installed"}, - }} - got, err := ResolveMCPBindings(proto.PromptRequestPayload{LocalEnvironment: local}) + } + got, err := ResolveMCPBindings(PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}}, MCP: installed}) if err != nil || len(got) != 3 { t.Fatal("installed bindings unavailable", err) } @@ -40,18 +40,18 @@ func TestMCPBindingsPreserveOriginAuthorityAndPolicy(t *testing.T) { } got[0].Stdio.Server.Args[0] = "mutated" got[1].HTTPHeaders["X-Selected"] = "mutated" - if local.MCP[0].Server.Args[0] != "tool.js" || local.MCP[1].Server.HTTPHeaders["X-Selected"] != "literal" { + if installed[0].Server.Args[0] != "tool.js" || installed[1].Server.HTTPHeaders["X-Selected"] != "literal" { t.Fatal("projection changed frozen installation") } } func TestMCPBindingsDistinguishAbsentAndEmptyProfile(t *testing.T) { - got, err := ResolveMCPBindings(proto.PromptRequestPayload{}) + got, err := ResolveMCPBindings(PrepareRequest{}) if err != nil || got != nil { t.Fatal("absent profile changed") } empty := []proto.MCPHTTPServer{} - got, err = ResolveMCPBindings(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &empty}) + got, err = ResolveMCPBindings(PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &empty}}) if err != nil || got == nil || len(got) != 0 { t.Fatal("explicit empty profile lost ownership") } @@ -59,13 +59,13 @@ func TestMCPBindingsDistinguishAbsentAndEmptyProfile(t *testing.T) { func TestMCPBindingsRejectRelocationAndAmbiguousInstallation(t *testing.T) { public := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} - local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp"}}}} - for _, req := range []proto.PromptRequestPayload{ - {MCPHTTPServers: &public, LocalEnvironment: local}, - {DisableExecutionEnvironment: true, LocalEnvironment: local}, - {LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: append(local.MCP, local.MCP[0])}}, - {LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "disabled", MCP: local.MCP}}, - {LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp", BearerTokenEnvVar: "MISSING"}}}}}, + local := &proto.LocalEnvironment{} + installed := []EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp"}}} + for _, req := range []PrepareRequest{ + {PromptRequestPayload: proto.PromptRequestPayload{MCPHTTPServers: &public, LocalEnvironment: local}, MCP: installed}, + {PromptRequestPayload: proto.PromptRequestPayload{DisableExecutionEnvironment: true, LocalEnvironment: local}, MCP: installed}, + {PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: local}, MCP: append(installed, installed[0])}, + {PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: local}, MCP: []EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp", BearerTokenEnvVar: "MISSING"}}}}, } { if _, err := ResolveMCPBindings(req); err == nil { t.Fatal("unsupported authority accepted") @@ -77,7 +77,7 @@ func TestPublicEnvironmentMCPRetainsVaultAuthority(t *testing.T) { token := "selected-vault-canary" names := []string{"prove"} public := []proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "remote", ServerURL: "https://example.test/mcp", BearerToken: &token, AllowedTools: &names, Required: true}} - req := proto.PromptRequestPayload{MCPHTTPServers: &public, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled"}} + req := PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{MCPHTTPServers: &public, LocalEnvironment: &proto.LocalEnvironment{}}} got, err := ResolveMCPBindings(req) if err != nil || len(got) != 1 { t.Fatal("environment binding unavailable", err) @@ -97,11 +97,11 @@ func TestPublicEnvironmentMCPRetainsVaultAuthority(t *testing.T) { } } public[0].ConnectionOrigin = "environment" - req.LocalEnvironment.MCP = []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp"}}} + req.MCP = []EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp"}}} if _, err := ResolveMCPBindings(req); err == nil { t.Fatal("public/Plugin identity collision accepted") } - req.LocalEnvironment = nil + req.MCP, req.LocalEnvironment = nil, nil if _, err := ResolveMCPBindings(req); err == nil { t.Fatal("unprepared environment accepted") } diff --git a/apps/daemon/internal/agent/registry_test.go b/apps/daemon/internal/agent/registry_test.go index 17354f190..60eaf62c4 100644 --- a/apps/daemon/internal/agent/registry_test.go +++ b/apps/daemon/internal/agent/registry_test.go @@ -60,7 +60,7 @@ func TestRegistryRegisterPanicsOnEmptyKind(t *testing.T) { func TestRegistryRegisterRejectsFactoriesForUnavailableRuntime(t *testing.T) { info := proto.SupportedAgentKind{Kind: "k", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})} - executor := func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil } + executor := func(context.Context, agent.PrepareRequest) (agent.Executor, error) { return nil, nil } registry := agent.NewRegistry() defer func() { if recover() == nil { @@ -115,12 +115,12 @@ func TestRegistryExecutorRequiresExplicitRegistration(t *testing.T) { t.Fatal("kind registration implied reusable execution") } expected := errors.New("executor factory") - registry.RegisterExecutor("native", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, expected }) + registry.RegisterExecutor("native", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { return nil, expected }) factory, err := registry.ResolveExecutor("native") if err != nil { t.Fatal(err) } - if _, err := factory(t.Context(), prototest.WithModel(proto.PromptRequestPayload{})); !errors.Is(err, expected) { + if _, err := factory(t.Context(), agent.PrepareRequest{PromptRequestPayload: prototest.WithModel(proto.PromptRequestPayload{})}); !errors.Is(err, expected) { t.Fatal(err) } registry.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, prototest.ModelConfiguration()) @@ -136,7 +136,7 @@ func TestRegistryRejectsEveryOmittedCapabilityBeforeReplacement(t *testing.T) { registry := agent.NewRegistry() original := proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: valid} registry.RegisterKind(original, prototest.ModelConfiguration()) - registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return nil, nil }) + registry.RegisterExecutor("fixture", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { return nil, nil }) missing := valid reflect.ValueOf(&missing).Elem().Field(i).Set(reflect.ValueOf(proto.CapabilityUnspecified)) func() { diff --git a/apps/daemon/internal/agent/view_test.go b/apps/daemon/internal/agent/view_test.go index 7e65e7327..629753f38 100644 --- a/apps/daemon/internal/agent/view_test.go +++ b/apps/daemon/internal/agent/view_test.go @@ -11,6 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) @@ -66,7 +67,7 @@ func TestRegistryResolvesOnlyDeclaredViews(t *testing.T) { func TestViewExecutorReceivesOnlyGatewayConnections(t *testing.T) { reg := agent.NewRegistry() declared := validView(t) - declared.Executor = func(context.Context, proto.PromptRequestPayload, agent.ViewSession) (agent.Executor, error) { + declared.Executor = func(context.Context, agent.PrepareRequest, agent.ViewSession) (agent.Executor, error) { return nil, errReached } info := proto.SupportedAgentKind{Kind: "viewed", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})} @@ -76,25 +77,25 @@ func TestViewExecutorReceivesOnlyGatewayConnections(t *testing.T) { if err != nil { t.Fatal(err) } - request := func(baseURL, key string) proto.PromptRequestPayload { - return proto.PromptRequestPayload{Model: "m", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: baseURL, APIKey: key}} + request := func(baseURL, key string) agent.PrepareRequest { + return agent.PrepareRequest{Prepared: harnessconfig.PreparedConfiguration{Model: "m", Provider: modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: baseURL, APIKey: key}}} } gatewayRequest := request("http://127.0.0.1:4101", modelprovider.Placeholder) requestMCP, installedMCP := gatewayRequest, gatewayRequest requestMCP.MCPHTTPServers = &[]proto.MCPHTTPServer{} - installedMCP.LocalEnvironment = &proto.LocalEnvironment{MCP: []proto.EnvironmentMCP{{}}} + installedMCP.MCP = []agent.EnvironmentMCP{{}} token := "secret" gateway := agent.MCPBinding{ServerLabel: "docs", Transport: "http", ServerURL: "http://127.0.0.1:4100/mcp/docs"} withBearer, withHeaders, remote := gateway, gateway, gateway withBearer.BearerToken = &token withHeaders.HTTPHeaders = map[string]string{"X-Api-Key": token} remote.ServerURL = "https://mcp.example.com/docs" - alias := agent.MCPBinding{ServerLabel: "tools", Transport: "stdio", Stdio: &proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: agent.ViewAlias(1)}}} + alias := agent.MCPBinding{ServerLabel: "tools", Transport: "stdio", Stdio: &agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: agent.ViewAlias(1)}}} command, misplaced := alias, alias - command.Stdio = &proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "node", Args: []string{"tools.js"}}} - misplaced.Stdio = &proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: agent.ViewAlias(0)}} + command.Stdio = &agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "node", Args: []string{"tools.js"}}} + misplaced.Stdio = &agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: agent.ViewAlias(0)}} for name, c := range map[string]struct { - req proto.PromptRequestPayload + req agent.PrepareRequest mcp []agent.MCPBinding }{ "gateway": {req: gatewayRequest, mcp: []agent.MCPBinding{gateway, alias}}, @@ -134,7 +135,7 @@ func validView(t *testing.T) agent.View { ShimPaths: []string{"/bin/sh"}, ForwardEnv: []string{"GIT_EDITOR"}, Proxy: agent.ViewProxyEnv, - Executor: func(context.Context, proto.PromptRequestPayload, agent.ViewSession) (agent.Executor, error) { + Executor: func(context.Context, agent.PrepareRequest, agent.ViewSession) (agent.Executor, error) { return nil, errors.New("not started") }, } diff --git a/apps/daemon/internal/agent/viewloader/loader_linux_test.go b/apps/daemon/internal/agent/viewloader/loader_linux_test.go index 23832f780..62b9589bc 100644 --- a/apps/daemon/internal/agent/viewloader/loader_linux_test.go +++ b/apps/daemon/internal/agent/viewloader/loader_linux_test.go @@ -11,7 +11,6 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) func TestForPresentsTheHostLoader(t *testing.T) { @@ -36,7 +35,7 @@ func TestForPresentsTheHostLoader(t *testing.T) { t.Fatalf("masks = %+v", fragment.Masks) } view := agent.View{Proxy: agent.ViewProxyNone, LocalExec: []string{fragment.Overlays[0].Path}, - Executor: func(context.Context, proto.PromptRequestPayload, agent.ViewSession) (agent.Executor, error) { + Executor: func(context.Context, agent.PrepareRequest, agent.ViewSession) (agent.Executor, error) { return nil, nil }} fragment.AddTo(&view) diff --git a/apps/daemon/internal/agenthost/admit.go b/apps/daemon/internal/agenthost/admit.go index f9cbcc84d..cfe64e806 100644 --- a/apps/daemon/internal/agenthost/admit.go +++ b/apps/daemon/internal/agenthost/admit.go @@ -14,7 +14,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/gateway" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/processbroker" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink" @@ -30,7 +29,7 @@ type plan struct { view agent.View gateway gateway.Config // request is the request the view Executor factory receives. - request proto.PromptRequestPayload + request agent.PrepareRequest // mcp and proxy are ViewSession.MCP and ViewSession.Proxy. mcp []agent.MCPBinding proxy string @@ -92,7 +91,7 @@ func loadRoots(dir string) (*x509.CertPool, error) { // admit checks req and derives its plan without touching anything. env is // the Session's Environment, and openNetwork its Network dial for the // gateway, which a Session with environment none never uses. -func admit(cfg Config, roots *x509.CertPool, req proto.PromptRequestPayload, env Environment, openNetwork func(context.Context) (sandboxlink.Stream, error)) (*plan, error) { +func admit(cfg Config, roots *x509.CertPool, req agent.PrepareRequest, env Environment, openNetwork func(context.Context) (sandboxlink.Stream, error)) (*plan, error) { view, err := cfg.Harnesses.ResolveView(req.AgentKind) if err != nil { return nil, fmt.Errorf("%w: admit: %w", ErrUnsupported, err) @@ -103,17 +102,10 @@ func admit(cfg Config, roots *x509.CertPool, req proto.PromptRequestPayload, env return nil, invalidSession("a Session with neither a workspace nor environment none is an incomplete binding") case local != nil && !isViewPath(local.WorkspaceDirectory): return nil, invalidSession("workspace %q is not absolute and clean", local.WorkspaceDirectory) - case local != nil && local.Capabilities && local.CapabilityRoot == "": - return nil, unsupported("installed Capabilities that no preparation resolved") - case local != nil && (local.NetworkAccess != "enabled" || len(local.AllowedDomains) > 0): - return nil, unsupported("a restricted workspace network") case !none && len(view.Shims) > 0 && !hasPATH(env): return nil, invalidSession("the view's shims run names on the sandbox PATH, and the Environment sets no PATH") } - if req.ModelProvider == nil { - return nil, unsupported("a Session without a frozen model provider") - } - provider := *req.ModelProvider + provider := req.Prepared.Provider if err := provider.Validate(false); err != nil { return nil, invalidSession("model provider: %v", err) } @@ -121,7 +113,7 @@ func admit(cfg Config, roots *x509.CertPool, req proto.PromptRequestPayload, env if err != nil { return nil, invalidSession("MCP: %v", err) } - gw := gateway.Config{Model: provider, Prompt: req, RootCAs: roots, Proxy: view.Proxy == agent.ViewProxyEnv} + gw := gateway.Config{Model: provider, Prompt: req.PromptRequestPayload, RootCAs: roots, Proxy: view.Proxy == agent.ViewProxyEnv} table := processbroker.Executables{Aliases: map[string]processbroker.Command{}} if !none { gw.OpenNetwork, table.Names, table.Paths = openNetwork, identity(view.Shims), identity(view.ShimPaths) @@ -158,7 +150,7 @@ func admit(cfg Config, roots *x509.CertPool, req proto.PromptRequestPayload, env if b.Stdio != nil { // The Harness runs the binding under its alias, which the process // broker maps to the frozen command. - b.Stdio = &proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: b.ServerLabel, Type: "stdio", Command: agent.ViewAlias(i)}} + b.Stdio = &agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: b.ServerLabel, Type: "stdio", Command: agent.ViewAlias(i)}} } b.ServerURL, b.BearerToken, b.HTTPHeaders = endpoints.MCP[b.ServerLabel], nil, nil if b.AllowedTools != nil { @@ -171,18 +163,12 @@ func admit(cfg Config, roots *x509.CertPool, req proto.PromptRequestPayload, env } // handoff rewrites the request as a view Executor receives it: the model -// provider is the gateway's listener with the placeholder key, MCP is only in -// ViewSession.MCP, and the workspace, if any, is the Environment's declared -// directory, which the view shows from the sandbox. -func handoff(req proto.PromptRequestPayload, provider modelprovider.Provider, endpoints gateway.Endpoints) proto.PromptRequestPayload { +// provider is the gateway's listener with the placeholder key, and MCP is only +// in ViewSession.MCP. +func handoff(req agent.PrepareRequest, provider modelprovider.Provider, endpoints gateway.Endpoints) agent.PrepareRequest { provider.BaseURL, provider.APIKey = endpoints.Model, modelprovider.Placeholder - req.ModelProvider = &provider - req.MCPHTTPServers = nil - if req.LocalEnvironment != nil { - local := *req.LocalEnvironment - local.MCP, local.WorkspaceRoot = nil, local.WorkspaceDirectory - req.LocalEnvironment = &local - } + req.ModelProvider, req.Prepared.Provider = &provider, provider + req.MCPHTTPServers, req.MCP = nil, nil return req } diff --git a/apps/daemon/internal/agenthost/admit_linux_test.go b/apps/daemon/internal/agenthost/admit_linux_test.go index f6454fa5d..7eb489e46 100644 --- a/apps/daemon/internal/agenthost/admit_linux_test.go +++ b/apps/daemon/internal/agenthost/admit_linux_test.go @@ -32,7 +32,7 @@ var errFactory = errors.New("factory reached") // no view. type viewFixture struct { cfg Config - req proto.PromptRequestPayload + req agent.PrepareRequest session agent.ViewSession homeSet bool } @@ -46,7 +46,7 @@ func newViewFixture(t *testing.T) *viewFixture { Closure: []agent.ViewMount{{Name: "harness", HostDir: t.TempDir()}}, LocalExec: []string{"/.oac/harness/harness"}, Proxy: agent.ViewProxyEnv, - Executor: func(_ context.Context, req proto.PromptRequestPayload, s agent.ViewSession) (agent.Executor, error) { + Executor: func(_ context.Context, req agent.PrepareRequest, s agent.ViewSession) (agent.Executor, error) { f.req, f.session = req, s info, err := os.Stat(s.Home.Host) f.homeSet = err == nil && info.IsDir() @@ -70,29 +70,25 @@ func TestAdmissionRejectsBeforeAnyEffect(t *testing.T) { unsupported := []error{ErrUnsupported, agent.ErrUnsupportedOperation} for name, c := range map[string]struct { kind string - change func(*proto.PromptRequestPayload) + change func(*agent.PrepareRequest) want []error }{ - "kind without a view": {"plain", func(*proto.PromptRequestPayload) {}, unsupported}, - "view meeting the agent host's /etc": {"masked", func(*proto.PromptRequestPayload) {}, []error{ErrUnsupported, agent.ErrInvalidView}}, - "incomplete binding": {"viewed", func(r *proto.PromptRequestPayload) { r.LocalEnvironment = nil }, []error{ErrInvalidSession}}, - "shim name without PATH": {"shimmed", func(*proto.PromptRequestPayload) {}, []error{ErrInvalidSession}}, - "relative workspace": {"viewed", func(r *proto.PromptRequestPayload) { r.LocalEnvironment.WorkspaceDirectory = "workspace" }, []error{ErrInvalidSession}}, - "no model provider": {"viewed", func(r *proto.PromptRequestPayload) { r.ModelProvider = nil }, []error{ErrUnsupported}}, - "restricted network": {"viewed", func(r *proto.PromptRequestPayload) { r.LocalEnvironment.NetworkAccess = "disabled" }, unsupported}, - "allowed domains only": {"viewed", func(r *proto.PromptRequestPayload) { r.LocalEnvironment.AllowedDomains = []string{"example.com"} }, unsupported}, - "unprepared Capabilities": {"viewed", func(r *proto.PromptRequestPayload) { r.LocalEnvironment.Capabilities = true }, unsupported}, - "credentialed stdio MCP": {"viewed", func(r *proto.PromptRequestPayload) { - r.LocalEnvironment.MCP = []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "tools", EnvVars: []string{"TOKEN"}}}} + "kind without a view": {"plain", func(*agent.PrepareRequest) {}, unsupported}, + "view meeting the agent host's /etc": {"masked", func(*agent.PrepareRequest) {}, []error{ErrUnsupported, agent.ErrInvalidView}}, + "incomplete binding": {"viewed", func(r *agent.PrepareRequest) { r.LocalEnvironment = nil }, []error{ErrInvalidSession}}, + "shim name without PATH": {"shimmed", func(*agent.PrepareRequest) {}, []error{ErrInvalidSession}}, + "relative workspace": {"viewed", func(r *agent.PrepareRequest) { r.LocalEnvironment.WorkspaceDirectory = "workspace" }, []error{ErrInvalidSession}}, + "credentialed stdio MCP": {"viewed", func(r *agent.PrepareRequest) { + r.MCP = []agent.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "tools", EnvVars: []string{"TOKEN"}}}} }, []error{ErrUnsupported, agent.ErrViewHandoff}}, - "stdio MCP without an absolute directory": {"viewed", func(r *proto.PromptRequestPayload) { - r.LocalEnvironment.MCP = []proto.EnvironmentMCP{{PackageRoot: "pkg", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "/bin/tools"}}} + "stdio MCP without an absolute directory": {"viewed", func(r *agent.PrepareRequest) { + r.MCP = []agent.EnvironmentMCP{{PackageRoot: "pkg", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "/bin/tools"}}} }, []error{ErrInvalidSession}}, - "stdio MCP name without PATH": {"viewed", func(r *proto.PromptRequestPayload) { - r.LocalEnvironment.MCP = []proto.EnvironmentMCP{{InstallationRoot: "/capabilities", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "tools"}}} + "stdio MCP name without PATH": {"viewed", func(r *agent.PrepareRequest) { + r.MCP = []agent.EnvironmentMCP{{InstallationRoot: "/capabilities", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "tools"}}} }, []error{ErrInvalidSession}}, } { - req := request(c.kind, "/workspace", "https://model.test", "sk-test") + req := prepared(request(c.kind, "/workspace", "https://model.test", "sk-test")) c.change(&req) var dials atomic.Int32 e, err := open(context.Background(), f.cfg, req, bindTo(newBinding(newResource())), deps{dial: countingDial(&dials), tasks: noTasks}) @@ -117,7 +113,7 @@ func TestAdmissionRejectsBeforeAnyEffect(t *testing.T) { b := newBinding(newResource()) change(&b) var dials atomic.Int32 - e, err := open(context.Background(), f.cfg, request("viewed", "/workspace", "https://model.test", "sk-test"), bindTo(b), deps{dial: countingDial(&dials), tasks: noTasks}) + e, err := open(context.Background(), f.cfg, prepared(request("viewed", "/workspace", "https://model.test", "sk-test")), bindTo(b), deps{dial: countingDial(&dials), tasks: noTasks}) if e != nil || !errors.Is(err, ErrInvalidSession) || dials.Load() != 0 { t.Errorf("%s: open = %v after %d dials, want ErrInvalidSession", name, err, dials.Load()) } @@ -135,8 +131,8 @@ func TestStdioMCPRunsUnderItsAlias(t *testing.T) { if err != nil { t.Fatal(err) } - req := request("viewed", "/workspace", "https://model.test", "sk-test") - req.LocalEnvironment.MCP = []proto.EnvironmentMCP{ + req := prepared(request("viewed", "/workspace", "https://model.test", "sk-test")) + req.MCP = []agent.EnvironmentMCP{ {Server: agentplugin.MCPServer{Name: "docs", Type: "http", URL: "https://mcp.test/docs"}}, {InstallationRoot: "/capabilities", PackageRoot: "pkg", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "bin/tools", Args: []string{"--stdio"}, CWD: "run"}}, } @@ -145,7 +141,7 @@ func TestStdioMCPRunsUnderItsAlias(t *testing.T) { if err != nil { t.Fatal(err) } - alias := proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: agent.ViewAlias(1)}} + alias := agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: agent.ViewAlias(1)}} if len(p.mcp) != 2 || p.mcp[1].Stdio == nil || !reflect.DeepEqual(*p.mcp[1].Stdio, alias) || len(p.gateway.MCP) != 1 || p.gateway.MCP[0].ServerLabel != "docs" { t.Errorf("ViewSession.MCP %+v and gateway MCP %+v; want the stdio binding under its alias and only HTTP at the gateway", p.mcp, p.gateway.MCP) } @@ -173,24 +169,24 @@ func TestRegistryRunsKindsWithViews(t *testing.T) { func TestViewExecutorReceivesTheGatewayRequest(t *testing.T) { f := newViewFixture(t) bearer := "mcp-secret" - req := request("viewed", "/workspace", "https://model.test", "sk-test") + req := prepared(request("viewed", "/workspace", "https://model.test", "sk-test")) req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "docs", ServerURL: "https://mcp.test/docs?tenant=a", BearerToken: &bearer}} skills := []agentcapabilities.InstalledSkill{{InstallationRoot: agentcapabilities.Directory, RelativeRoot: "skills/review", PackageRoot: "skills/review"}} - req.LocalEnvironment.Capabilities, req.LocalEnvironment.CapabilityRoot, req.LocalEnvironment.Skills = true, agentcapabilities.Directory, skills + req.CapabilityRoot, req.Skills = agentcapabilities.Directory, skills original := *req.ModelProvider var dials atomic.Int32 e, err := open(context.Background(), f.cfg, req, bindTo(newBinding(newResource())), deps{dial: countingDial(&dials), tasks: noTasks}) if !errors.Is(err, errFactory) { t.Fatalf("open = %v, want the factory's error", err) } - if provider := f.req.ModelProvider; provider == nil || provider.BaseURL != "http://127.0.0.1:17101" || provider.APIKey != modelprovider.Placeholder || provider.Protocol != modelprovider.Anthropic { + if provider := f.req.ModelProvider; provider == nil || *provider != f.req.Prepared.Provider || provider.BaseURL != "http://127.0.0.1:17101" || provider.APIKey != modelprovider.Placeholder || provider.Protocol != modelprovider.Anthropic { t.Errorf("model provider %+v; want the gateway with the placeholder", provider) } - if *req.ModelProvider != original { + if *req.ModelProvider != original || req.Prepared.Provider != original { t.Error("the Session's request changed") } - if local := f.req.LocalEnvironment; f.req.MCPHTTPServers != nil || local == nil || local.MCP != nil || local.WorkspaceRoot != "/workspace" || - local.CapabilityRoot != agentcapabilities.Directory || !reflect.DeepEqual(local.Skills, skills) { + if f.req.MCPHTTPServers != nil || f.req.MCP != nil || f.req.WorkspaceRoot != "/workspace" || + f.req.CapabilityRoot != agentcapabilities.Directory || !reflect.DeepEqual(f.req.Skills, skills) { t.Error("the request still carries MCP, does not run in the Environment's workspace or lost its installed Skills") } mcp := f.session.MCP diff --git a/apps/daemon/internal/agenthost/agenthost_linux_test.go b/apps/daemon/internal/agenthost/agenthost_linux_test.go index 05c3167e9..98d9f9ca2 100644 --- a/apps/daemon/internal/agenthost/agenthost_linux_test.go +++ b/apps/daemon/internal/agenthost/agenthost_linux_test.go @@ -23,6 +23,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "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/modelprovider" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink" @@ -88,10 +89,20 @@ func request(kind, workspace, baseURL, key string) proto.PromptRequestPayload { AgentKind: kind, Model: "m", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: baseURL, APIKey: key}, - LocalEnvironment: &proto.LocalEnvironment{WorkspaceDirectory: workspace, NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}}, + LocalEnvironment: &proto.LocalEnvironment{WorkspaceDirectory: workspace, CapabilitySources: &agentcapabilities.Input{}}, } } +// prepared is req as the registry and the Environment owner hand it to the +// agent host's factory. +func prepared(req proto.PromptRequestPayload) agent.PrepareRequest { + p := agent.PrepareRequest{PromptRequestPayload: req, Prepared: harnessconfig.PreparedConfiguration{Model: req.Model, Provider: *req.ModelProvider}} + if req.LocalEnvironment != nil { + p.WorkspaceRoot = req.LocalEnvironment.WorkspaceDirectory + } + return p +} + // newBinding returns the binding of a new Session on resource. func newBinding(resource sandboxlink.ResourceRef) Binding { return Binding{Resource: resource, SessionID: sandboxwire.NewID(), AssignmentID: sandboxwire.NewID(), AssignmentEpoch: 1, @@ -99,8 +110,8 @@ func newBinding(resource sandboxlink.ResourceRef) Binding { } // bindTo binds every request to b. -func bindTo(b Binding) func(proto.PromptRequestPayload) (Binding, Environment, error) { - return func(proto.PromptRequestPayload) (Binding, Environment, error) { return b, Environment{}, nil } +func bindTo(b Binding) func(agent.PrepareRequest) (Binding, Environment, error) { + return func(agent.PrepareRequest) (Binding, Environment, error) { return b, Environment{}, nil } } func newResource() sandboxlink.ResourceRef { @@ -147,7 +158,7 @@ type daemon struct { router *dispatch.Router // mcp is the installed MCP that the Environment's preparation resolves // into each request; the wire does not carry it. - mcp []proto.EnvironmentMCP + mcp []agent.EnvironmentMCP mu sync.Mutex frames map[string]chan proto.Envelope // by envelope ID opened map[string]*session // by Session ID @@ -156,16 +167,14 @@ type daemon struct { func newDaemon(t *testing.T, cfg Config, d deps) *daemon { t.Helper() dm := &daemon{host: &Host{cfg: cfg, owners: owners{d: d}}} - dm.route(t, registry(cfg.Harnesses, func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { + dm.route(t, registry(cfg.Harnesses, func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { if dm.mcp != nil { - local := *req.LocalEnvironment - local.MCP = dm.mcp - req.LocalEnvironment = &local + req.MCP = dm.mcp } e, err := dm.host.openExecutor(ctx, req) if s, ok := e.(*session); ok { dm.mu.Lock() - dm.opened[strings.TrimPrefix(req.AgentStateKey, stateKeyPrefix)] = s + dm.opened[req.Assignment.SessionID] = s dm.mu.Unlock() } return e, err @@ -185,9 +194,6 @@ func (dm *daemon) route(t *testing.T, reg *agent.Registry) { t.Cleanup(func() { dm.shutdown() }) } -// stateKeyPrefix and the Session ID make the state key dispatch requires. -const stateKeyPrefix = "agents-api-" - // ref is the reference of b's assignment. func ref(b Binding) proto.AssignmentRef { return proto.AssignmentRef{SessionID: uuid.UUID(b.SessionID).String(), AssignmentID: uuid.UUID(b.AssignmentID).String(), Epoch: b.AssignmentEpoch} @@ -288,7 +294,6 @@ func (dm *daemon) prepare(t *testing.T, b Binding, req proto.PromptRequestPayloa local.ID = environmentID(b) req.LocalEnvironment = &local } - req.AgentStateKey = stateKeyPrefix + session id := sandboxwire.NewID().String() dm.handle(t, ref(b), proto.TypeExecutionPrepare, id, proto.ExecutionPreparePayload{SessionID: session, Configuration: req}) for { diff --git a/apps/daemon/internal/agenthost/environment_linux.go b/apps/daemon/internal/agenthost/environment_linux.go index 0659bf62e..9c14f99ef 100644 --- a/apps/daemon/internal/agenthost/environment_linux.go +++ b/apps/daemon/internal/agenthost/environment_linux.go @@ -139,7 +139,7 @@ func canonicalID(s string) (sandboxwire.ID, error) { // executor returns the binding and Environment of an Executor of req's // Session, whose preparation the owner prepared. -func (h *Host) executor(ctx context.Context, req proto.PromptRequestPayload) (Binding, Environment, error) { +func (h *Host) executor(ctx context.Context, req agent.PrepareRequest) (Binding, Environment, error) { session, err := canonicalID(req.Assignment.SessionID) if err != nil { return Binding{}, Environment{}, invalidSession("binding: %v", err) @@ -260,44 +260,36 @@ func (o *environment) Close(ctx context.Context) error { } // Configure checks the request against the owner's Environment, which -// dispatch resolved from the Session's assignment, and returns it with the -// sandbox workspace as its root. -func (o *environment) Configure(r proto.PromptRequestPayload) (proto.PromptRequestPayload, error) { +// dispatch resolved from the Session's assignment. +func (o *environment) Configure(r proto.PromptRequestPayload) error { local := r.LocalEnvironment switch { case o.id == "": if local != nil || !r.DisableExecutionEnvironment { - return r, errors.New("the Session has no Environment") + return errors.New("the Session has no Environment") } - return r, nil + return nil case local == nil || r.DisableExecutionEnvironment || local.ID != o.id: - return r, errors.New("the request does not name the Session's Environment") + return errors.New("the request does not name the Session's Environment") case r.WorkspaceReadOnly: - return r, nil + return nil case local.WorkspaceDirectory != sandboxWorkspace: - return r, fmt.Errorf("the workspace is not %s", sandboxWorkspace) + return fmt.Errorf("the workspace is not %s", sandboxWorkspace) case local.CapabilitySources == nil || agentcapabilities.ValidateInput(*local.CapabilitySources) != nil: - return r, agentcapabilities.ErrInvalid - } - sources := *local.CapabilitySources - if present := len(sources.Skills)+len(sources.Plugins)+len(sources.Directories) > 0; present != local.Capabilities { - return r, agentcapabilities.ErrInvalid + return agentcapabilities.ErrInvalid } - configured := *local - configured.Skills, configured.MCP, configured.CapabilityRoot, configured.WorkspaceRoot = nil, nil, "", sandboxWorkspace - r.LocalEnvironment = &configured - return r, nil + return nil } // Prepare completes the Session's installation when Core sent no finalize, -// checks it against the frozen selection, and fills the request's Skills, -// MCP and capability root as sandbox paths. -func (o *environment) Prepare(ctx context.Context, r proto.PromptRequestPayload) (proto.PromptRequestPayload, error) { +// checks it against the frozen selection, and fills the request's workspace +// root, Skills, MCP and capability root as sandbox paths. +func (o *environment) Prepare(ctx context.Context, r agent.PrepareRequest) (agent.PrepareRequest, error) { if r.WorkspaceReadOnly || o.id == "" && r.LocalEnvironment == nil { return r, nil } if o.id == "" || r.LocalEnvironment == nil || r.LocalEnvironment.ID != o.id || r.LocalEnvironment.CapabilitySources == nil || - r.LocalEnvironment.WorkspaceRoot != sandboxWorkspace { + r.LocalEnvironment.WorkspaceDirectory != sandboxWorkspace { return r, agentcapabilities.ErrInvalid } if err := o.acquire(ctx); err != nil { @@ -309,7 +301,7 @@ func (o *environment) Prepare(ctx context.Context, r proto.PromptRequestPayload) return r, err } defer o.done(w) - local := *r.LocalEnvironment + local := r.LocalEnvironment identity := o.identity() name, body, err := agentcapabilities.Marker(identity, agentcapabilities.Directory) if err != nil { @@ -340,25 +332,22 @@ func (o *environment) Prepare(ctx context.Context, r proto.PromptRequestPayload) return r, err } } - local.Skills, local.MCP, local.CapabilityRoot = manifest.Skills, nil, agentcapabilities.Directory - for i := range local.Skills { - local.Skills[i].InstallationRoot = agentcapabilities.Directory - } + var mcp []agent.EnvironmentMCP if len(manifest.MCP) != 0 { - if local.NetworkAccess != "enabled" { - return r, agentcapabilities.ErrInvalid - } tokens, err := agentcapabilities.ResolveMCP(manifest.MCP, values) if err != nil { return r, err } for i, item := range manifest.MCP { - local.MCP = append(local.MCP, proto.EnvironmentMCP{InstallationRoot: agentcapabilities.Directory, WorkspaceRoot: sandboxWorkspace, + mcp = append(mcp, agent.EnvironmentMCP{InstallationRoot: agentcapabilities.Directory, WorkspaceRoot: sandboxWorkspace, PackageRoot: item.PackageRoot, Server: item.Server, BearerToken: tokens[i]}) } } + for i := range manifest.Skills { + manifest.Skills[i].InstallationRoot = agentcapabilities.Directory + } o.tool = values - r.LocalEnvironment = &local + r.WorkspaceRoot, r.CapabilityRoot, r.Skills, r.MCP = sandboxWorkspace, agentcapabilities.Directory, manifest.Skills, mcp return r, nil } diff --git a/apps/daemon/internal/agenthost/environment_linux_test.go b/apps/daemon/internal/agenthost/environment_linux_test.go index 83fbbee45..e0b85a4e1 100644 --- a/apps/daemon/internal/agenthost/environment_linux_test.go +++ b/apps/daemon/internal/agenthost/environment_linux_test.go @@ -53,7 +53,7 @@ func TestEnvironmentOwnerServesTheSandbox(t *testing.T) { t.Fatal(err) } harnesses := agent.NewRegistry() - register(harnesses, "test", &agent.View{Proxy: agent.ViewProxyEnv, Executor: func(context.Context, proto.PromptRequestPayload, agent.ViewSession) (agent.Executor, error) { + register(harnesses, "test", &agent.View{Proxy: agent.ViewProxyEnv, Executor: func(context.Context, agent.PrepareRequest, agent.ViewSession) (agent.Executor, error) { return nil, errors.New("the test's factory replaces the view's") }}) cfg := Config{StateDir: t.TempDir(), RelayURL: sb.url, RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: harnesses} @@ -62,7 +62,7 @@ func TestEnvironmentOwnerServesTheSandbox(t *testing.T) { h := &Host{cfg: cfg, owners: owners{d: deps{dial: relayDial(cfg)}}} // The factory records what the owner prepared. prepared := make(chan preparedExecutor, 4) - reg := registry(harnesses, func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { + reg := registry(harnesses, func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { _, env, err := h.executor(ctx, req) prepared <- preparedExecutor{req: req, env: env} if err != nil { @@ -75,7 +75,6 @@ func TestEnvironmentOwnerServesTheSandbox(t *testing.T) { manifest := []byte("---\nname: probe-skill\ndescription: Probe the installation.\n---\nProbe.\n") req := request("test", sandboxWorkspace, "https://model.invalid", "key") req.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Skills: []agentskill.Metadata{skill}} - req.LocalEnvironment.Capabilities = true // Prepare as Core does: configure, a setup step that sees the tool // environment, the Skill and finalize, then the Executor. A plugin whose @@ -110,10 +109,9 @@ func TestEnvironmentOwnerServesTheSandbox(t *testing.T) { t.Fatalf("the preparation is %s (%s), want ready", status.State, status.ErrorCode) } got := <-prepared - local := got.req.LocalEnvironment - if local.WorkspaceRoot != sandboxWorkspace || local.CapabilityRoot != agentcapabilities.Directory || len(local.Skills) != 1 || - local.Skills[0].Metadata != skill || local.Skills[0].InstallationRoot != agentcapabilities.Directory || local.Skills[0].RelativeRoot != "skills/probe-skill" { - t.Fatalf("the Executor's Environment is %+v", local) + if r := got.req; r.WorkspaceRoot != sandboxWorkspace || r.CapabilityRoot != agentcapabilities.Directory || len(r.Skills) != 1 || + r.Skills[0].Metadata != skill || r.Skills[0].InstallationRoot != agentcapabilities.Directory || r.Skills[0].RelativeRoot != "skills/probe-skill" { + t.Fatalf("the Executor's Environment is %+v", r) } if got.env.Tool["PROBE"] != "probe-value" || got.env.Sandbox["PATH"] != sandboxBaseline["PATH"] { t.Fatalf("the Executor's environments are %+v", got.env) @@ -135,8 +133,8 @@ func TestEnvironmentOwnerServesTheSandbox(t *testing.T) { if status.State != "ready" { t.Fatalf("the reopened preparation is %s (%s), want ready", status.State, status.ErrorCode) } - if got := <-prepared; len(got.req.LocalEnvironment.Skills) != 1 || got.env.Tool["PROBE"] != "probe-value" { - t.Fatalf("the reopened Executor's Environment is %+v, %+v", got.req.LocalEnvironment, got.env) + if got := <-prepared; len(got.req.Skills) != 1 || got.env.Tool["PROBE"] != "probe-value" { + t.Fatalf("the reopened Executor's Environment is %+v, %+v", got.req.Skills, got.env) } if requests, mutations := p.counts(); requests == 0 || mutations != 0 { t.Fatalf("the reopen sent %d File requests, %d of them mutations, on the probed world", requests, mutations) @@ -269,7 +267,7 @@ func TestUnreachableSandboxRejectsRuntimePreparation(t *testing.T) { } type preparedExecutor struct { - req proto.PromptRequestPayload + req agent.PrepareRequest env Environment } diff --git a/apps/daemon/internal/agenthost/executor_linux.go b/apps/daemon/internal/agenthost/executor_linux.go index 26848a8e5..768344ecb 100644 --- a/apps/daemon/internal/agenthost/executor_linux.go +++ b/apps/daemon/internal/agenthost/executor_linux.go @@ -35,8 +35,8 @@ func (h *Host) Registry() *agent.Registry { return registry(h.cfg.Harnesses, h.openExecutor) } -func (h *Host) openExecutor(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { - return open(ctx, h.cfg, req, func(r proto.PromptRequestPayload) (Binding, Environment, error) { return h.executor(ctx, r) }, h.owners.d) +func (h *Host) openExecutor(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { + return open(ctx, h.cfg, req, func(r agent.PrepareRequest) (Binding, Environment, error) { return h.executor(ctx, r) }, h.owners.d) } // registry registers each kind in harnesses that declares a view, with @@ -93,7 +93,7 @@ type session struct { // Executor's uid, prepares the Session directory and calls the view's // Executor factory. Once it has an effect, it returns the session even when // it fails, and the session's Close releases what it holds. -func open(ctx context.Context, cfg Config, req proto.PromptRequestPayload, bind func(proto.PromptRequestPayload) (Binding, Environment, error), d deps) (agent.Executor, error) { +func open(ctx context.Context, cfg Config, req agent.PrepareRequest, bind func(agent.PrepareRequest) (Binding, Environment, error), d deps) (agent.Executor, error) { roots, err := checkConfig(cfg) if err != nil { return nil, err diff --git a/apps/daemon/internal/agenthost/host_linux_test.go b/apps/daemon/internal/agenthost/host_linux_test.go index 4df5d2f1a..018b3e2b5 100644 --- a/apps/daemon/internal/agenthost/host_linux_test.go +++ b/apps/daemon/internal/agenthost/host_linux_test.go @@ -180,7 +180,7 @@ func plantSession(t *testing.T, cfg Config, id sandboxwire.ID) sessionDir { func TestRemoveHome(t *testing.T) { f := newViewFixture(t) h, b := &Host{cfg: f.cfg}, newBinding(newResource()) - req := request("viewed", "/workspace", "https://model.test", "sk-test") + req := prepared(request("viewed", "/workspace", "https://model.test", "sk-test")) var dials atomic.Int32 // The open stops once it has claimed the Session, before its uid. claimed, proceed := make(chan struct{}), make(chan struct{}) diff --git a/apps/daemon/internal/agenthost/view_linux_test.go b/apps/daemon/internal/agenthost/view_linux_test.go index d3dfc3945..695c50824 100644 --- a/apps/daemon/internal/agenthost/view_linux_test.go +++ b/apps/daemon/internal/agenthost/view_linux_test.go @@ -107,11 +107,11 @@ func TestSessionRunsInAViewOverItsAttachment(t *testing.T) { ShimPaths: []string{"/bin/sh"}, ForwardEnv: []string{"KEEP"}, Proxy: agent.ViewProxyEnv, - Executor: func(_ context.Context, req proto.PromptRequestPayload, s agent.ViewSession) (agent.Executor, error) { + Executor: func(_ context.Context, req agent.PrepareRequest, s agent.ViewSession) (agent.Executor, error) { e := &testExecutor{session: s, dir: workDir, - env: []string{harnessEnv + "=1", modelEnv + "=" + req.ModelProvider.BaseURL, caEnv + "=" + cfg.CADir, proxyEnv + "=" + s.Proxy}} + env: []string{harnessEnv + "=1", modelEnv + "=" + req.Prepared.Provider.BaseURL, caEnv + "=" + cfg.CADir, proxyEnv + "=" + s.Proxy}} if req.LocalEnvironment != nil { - e.dir = req.LocalEnvironment.WorkspaceRoot + e.dir = req.WorkspaceRoot } for _, b := range s.MCP { e.env = append(e.env, aliasEnv+"="+b.Stdio.Server.Command) @@ -237,7 +237,7 @@ func TestSessionRunsInAViewOverItsAttachment(t *testing.T) { if err := os.Mkdir(pkg, 0o755); err != nil { t.Fatal(err) } - d.mcp = []proto.EnvironmentMCP{{InstallationRoot: workspace, PackageRoot: "pkg", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "/bin/sh", + d.mcp = []agent.EnvironmentMCP{{InstallationRoot: workspace, PackageRoot: "pkg", Server: agentplugin.MCPServer{Name: "tools", Type: "stdio", Command: "/bin/sh", Args: []string{"-c", `printf '%s %s %s %s\n' "$0" "$#" "$(pwd -P)" "${KEEP-unset}"; exec /bin/sleep 1000`, "frozen"}}}} // The Harness exits while the alias's process runs on. if r := d.turn(t, b, req, "alias"); r.Stdout != "frozen 0 "+pkg+" unset\n" || r.Exit != "" { diff --git a/apps/daemon/internal/agenthostqualify/qualify_linux_test.go b/apps/daemon/internal/agenthostqualify/qualify_linux_test.go index 2a3f651b2..7650bb971 100644 --- a/apps/daemon/internal/agenthostqualify/qualify_linux_test.go +++ b/apps/daemon/internal/agenthostqualify/qualify_linux_test.go @@ -150,7 +150,7 @@ func qualify(t *testing.T, h *agenthost.Host, cfg agenthost.Config, sb *sandbox, configuration := proto.PromptRequestPayload{AgentKind: kind, DisableSubagents: true, Model: model.Model, ModelProvider: model.ModelProvider, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, - LocalEnvironment: &proto.LocalEnvironment{ID: uuid.UUID(sb.resource.EnvironmentID).String(), WorkspaceDirectory: workspace, NetworkAccess: "enabled", + LocalEnvironment: &proto.LocalEnvironment{ID: uuid.UUID(sb.resource.EnvironmentID).String(), WorkspaceDirectory: workspace, CapabilitySources: &agentcapabilities.Input{}}} if caps.FunctionTools.IsSupported() { configuration.FunctionTools = []proto.FunctionTool{lookupTicket} @@ -210,7 +210,7 @@ func qualify(t *testing.T, h *agenthost.Host, cfg agenthost.Config, sb *sandbox, word, code := strings.ToLower(rand.Text()[:12]), strings.ToLower(rand.Text()[:12]) installed, local := configuration, *configuration.LocalEnvironment installed.FunctionTools, installed.ToolSearch = nil, false - local.Capabilities, local.CapabilitySources = true, &agentcapabilities.Input{Plugins: []agentplugin.Metadata{qualifyPlugin}} + local.CapabilitySources = &agentcapabilities.Input{Plugins: []agentplugin.Metadata{qualifyPlugin}} installed.LocalEnvironment = &local sb.reset(t, cfg) s := sb.session(h, cfg, installed) @@ -332,7 +332,6 @@ type session struct { func (sb *sandbox) session(h *agenthost.Host, cfg agenthost.Config, configuration proto.PromptRequestPayload) *session { s := &session{h: h, cfg: cfg, binding: sb.binding(), configuration: configuration} s.id = uuid.UUID(s.binding.SessionID).String() - s.configuration.AgentStateKey = "agents-api-" + s.id sb.grant(s.binding, cfg.RuntimeID) return s } @@ -341,7 +340,6 @@ func (sb *sandbox) session(h *agenthost.Host, cfg agenthost.Config, configuratio func (s *session) with(configuration proto.PromptRequestPayload) *session { next := *s next.configuration = configuration - next.configuration.AgentStateKey = s.configuration.AgentStateKey return &next } diff --git a/apps/daemon/internal/cli/agent_host_linux_test.go b/apps/daemon/internal/cli/agent_host_linux_test.go index 32fd13362..c50470634 100644 --- a/apps/daemon/internal/cli/agent_host_linux_test.go +++ b/apps/daemon/internal/cli/agent_host_linux_test.go @@ -101,7 +101,7 @@ func TestAgentHostReportsItsDeclarations(t *testing.T) { return &agent.Runtime{Info: info, View: view} }} } - view := &agent.View{Proxy: agent.ViewProxyEnv, Executor: func(context.Context, proto.PromptRequestPayload, agent.ViewSession) (agent.Executor, error) { + view := &agent.View{Proxy: agent.ViewProxyEnv, Executor: func(context.Context, agent.PrepareRequest, agent.ViewSession) (agent.Executor, error) { return nil, errors.New("no Executor") }} // The image may install no Harness: the agent host still connects and diff --git a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go b/apps/daemon/internal/cli/claude_sdk_live_linux_test.go index 1035023e2..737fd01ff 100644 --- a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go +++ b/apps/daemon/internal/cli/claude_sdk_live_linux_test.go @@ -91,7 +91,7 @@ func TestLiveRegisteredClaudeSDK(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second) defer cancel() id := uuid.NewString() - request := proto.PromptRequestPayload{AgentKind: "claude_sdk", AgentStateKey: prototest.StateKey, AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider} + request := proto.PromptRequestPayload{AgentKind: "claude_sdk", AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider} if callFunction { request.FunctionTools = []proto.FunctionTool{{Name: "lookup", Description: "Return a verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}`)}} } diff --git a/apps/daemon/internal/cli/connect_cleanup_test.go b/apps/daemon/internal/cli/connect_cleanup_test.go index bd1c1d4e7..87c8a8692 100644 --- a/apps/daemon/internal/cli/connect_cleanup_test.go +++ b/apps/daemon/internal/cli/connect_cleanup_test.go @@ -116,7 +116,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { registry := agent.NewRegistry() registry.RegisterKind(proto.SupportedAgentKind{Kind: "cleanup", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration()) var factories atomic.Int32 - registry.RegisterExecutor("cleanup", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registry.RegisterExecutor("cleanup", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { factories.Add(1) return owner, nil }) @@ -148,7 +148,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { t.Fatalf("bind = %+v", got) } env, err := proto.NewEnvelope(proto.TypeExecutionPrepare, "prepare", proto.ExecutionPreparePayload{SessionID: "cleanup", - Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "cleanup", AgentStateKey: "agents-api-cleanup", DisableExecutionEnvironment: true})}) + Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "cleanup", DisableExecutionEnvironment: true})}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/dispatch/assignment_test.go b/apps/daemon/internal/dispatch/assignment_test.go index 2aed11991..a8ca00b1c 100644 --- a/apps/daemon/internal/dispatch/assignment_test.go +++ b/apps/daemon/internal/dispatch/assignment_test.go @@ -102,7 +102,7 @@ func TestAssignmentReleaseWaitsForRacingPreparation(t *testing.T) { replyBeforeClose.Store(hasFrame(sender, proto.TypeAssignmentStatus, "release")) }} entered, cancelled, unblock := make(chan struct{}), make(chan struct{}), make(chan struct{}) - r, sender := poolRouter(t, func(ctx context.Context, _ proto.PromptRequestPayload) (agent.Executor, error) { + r, sender := poolRouter(t, func(ctx context.Context, _ agent.PrepareRequest) (agent.Executor, error) { calls.Add(1) close(entered) <-ctx.Done() diff --git a/apps/daemon/internal/dispatch/cancellation_test.go b/apps/daemon/internal/dispatch/cancellation_test.go index 61f0cd449..154547b31 100644 --- a/apps/daemon/internal/dispatch/cancellation_test.go +++ b/apps/daemon/internal/dispatch/cancellation_test.go @@ -34,7 +34,7 @@ func (s *cancelReceiptSession) Cancel(ctx context.Context) error { } func registerCancelReceiptKind(h *harness, sess *cancelReceiptSession) { - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { sess.fakeSession = &fakeSession{out: out, closeOutOnCancel: true} return sess, nil }) diff --git a/apps/daemon/internal/dispatch/capability_admission_test.go b/apps/daemon/internal/dispatch/capability_admission_test.go index 0a10601f0..090fac402 100644 --- a/apps/daemon/internal/dispatch/capability_admission_test.go +++ b/apps/daemon/internal/dispatch/capability_admission_test.go @@ -15,7 +15,7 @@ func TestSteeringDoesNotReplayUnsupportedImplementation(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) var calls atomic.Int32 - factory := func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + factory := func(_ context.Context, _ fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { return &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload, func()) error { calls.Add(1) return fmt.Errorf("%w: fixture has no active input", agent.ErrUnsupportedOperation) diff --git a/apps/daemon/internal/dispatch/environment.go b/apps/daemon/internal/dispatch/environment.go index 5156ca0f6..615100ed2 100644 --- a/apps/daemon/internal/dispatch/environment.go +++ b/apps/daemon/internal/dispatch/environment.go @@ -7,6 +7,7 @@ import ( "io" "net/url" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" ) @@ -17,12 +18,12 @@ import ( // an owner declares none of these operations, and the Router rejects each with // its typed unsupported code. docs/runtime-protocol.md defines the semantics. type Environment interface { - // Configure checks an execution configuration against the Environment and - // returns it with the Environment's workspace root. It has no effects. - Configure(proto.PromptRequestPayload) (proto.PromptRequestPayload, error) - // Prepare fills the configured execution's installed capabilities before - // the Executor factory runs. - Prepare(context.Context, proto.PromptRequestPayload) (proto.PromptRequestPayload, error) + // Configure checks an execution configuration against the Environment. It + // has no effects. + Configure(proto.PromptRequestPayload) error + // Prepare fills the Executor's workspace root and installed capabilities + // before the Executor factory runs. + Prepare(context.Context, agent.PrepareRequest) (agent.PrepareRequest, error) // ApplyRuntimePreparation applies one complete runtime_prepare transfer, // whose envelope ID is transfer, and returns only after its mutations // stop. Core never sends a transfer ID twice, so transfer may name the diff --git a/apps/daemon/internal/dispatch/environment_test.go b/apps/daemon/internal/dispatch/environment_test.go index 0a776ed5f..c2ef1cac0 100644 --- a/apps/daemon/internal/dispatch/environment_test.go +++ b/apps/daemon/internal/dispatch/environment_test.go @@ -9,8 +9,11 @@ import ( "testing" "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" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" "github.com/google/uuid" ) @@ -34,7 +37,7 @@ func TestNoEnvironmentRejectsOtherEngineBeforeFactory(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) var called atomic.Bool - registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { called.Store(true) return nil, errors.New("controlled factory stop") }) @@ -53,7 +56,7 @@ func TestNoEnvironmentUsesAvailableCapability(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) var called atomic.Bool - registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "claude_sdk", Available: available, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "claude_sdk", Available: available, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { called.Store(true) return nil, errors.New("controlled factory stop") }) @@ -74,7 +77,7 @@ func TestLocalEnvironmentRequiresAvailableCapability(t *testing.T) { var called atomic.Bool registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: mode != "unavailable", Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(mode != "unsupported")})}, - func(_ context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { + func(_ context.Context, req agent.PrepareRequest) (agent.Executor, error) { called.Store(true) if req.LocalEnvironment == nil || req.LocalEnvironment.ID != preparationEnvironmentID { t.Error("local descriptor lost before factory") @@ -96,18 +99,67 @@ func TestLocalEnvironmentRequiresAvailableCapability(t *testing.T) { } } +// installingOwner is the bound workspace with one installed MCP server +// labelled label. +type installingOwner struct { + *localworkspace.Binding + label string +} + +func (o installingOwner) Prepare(ctx context.Context, req agent.PrepareRequest) (agent.PrepareRequest, error) { + req, err := o.Binding.Prepare(ctx, req) + req.MCP = append(req.MCP, agent.EnvironmentMCP{Server: agentplugin.MCPServer{Name: o.label, Type: "http", URL: "https://mcp.example"}}) + return req, err +} + +// The owner resolves installed MCP servers during preparation, and the kind's +// declaration checks them before the factory sees them. +func TestInstalledMCPIsCheckedBeforeTheFactory(t *testing.T) { + for label, admitted := range map[string]bool{"reserved": false, "installed": true} { + t.Run(label, func(t *testing.T) { + h := newHarness(t) + if err := h.router.Shutdown(t.Context()); err != nil { + t.Fatal(err) + } + owner := installingOwner{preparationWorkspace(t), label} + var err error + h.router, err = dispatch.New(dispatch.Config{Registry: h.reg, Sender: h.sender, Environments: func(proto.AssignmentRef, proto.AssignmentBindPayload) dispatch.Environment { return owner }}) + if err != nil { + t.Fatal(err) + } + defer h.router.Shutdown(context.Background()) + assign(t, h.router, preparationSessionID, preparationEnvironmentID) + configuration := prototest.ModelConfiguration() + configuration.Declaration.ReservedMCPLabels = []string{"reserved"} + h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, configuration) + var called atomic.Bool + h.reg.RegisterExecutor("prepared", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { + called.Store(true) + return nil, errors.New("controlled factory stop") + }) + if err := h.router.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "installed", preparationRequest())); err != nil { + t.Fatal(err) + } + assertPreparationOutcome(t, h.sender, "installed", true) + if called.Load() != admitted { + t.Fatalf("factory called=%t, want %t", called.Load(), admitted) + } + }) + } +} + // A Session whose assignment resolves no Environment owner declares no // Environment operation: each gets its typed rejection before any effect. func TestSessionWithoutOwnerRejectsEnvironmentOperations(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) var called atomic.Bool - registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "local", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: "local", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { called.Store(true) return nil, errors.New("controlled factory stop") }) assign(t, h.router, preparationSessionID, preparationEnvironmentID) - execution := proto.PromptRequestPayload{AgentKind: "local", AgentStateKey: stateKey(preparationSessionID), LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID}} + execution := proto.PromptRequestPayload{AgentKind: "local", LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID}} read := execution read.WorkspaceReadOnly = true for id, test := range map[string]struct { diff --git a/apps/daemon/internal/dispatch/executor.go b/apps/daemon/internal/dispatch/executor.go index bc75c1f69..9384be70e 100644 --- a/apps/daemon/internal/dispatch/executor.go +++ b/apps/daemon/internal/dispatch/executor.go @@ -16,16 +16,16 @@ import ( const executorIdleCapacity = 16 type executorState struct { - declaration proto.Declaration - id, sessionID, environmentID, stateKey string - fingerprint [32]byte - native agent.Executor - nativeID string - ctx context.Context - cancel context.CancelFunc - admission *preparationState - run *sessionState - preparing bool + declaration proto.Declaration + id, sessionID, environmentID string + fingerprint [32]byte + native agent.Executor + nativeID string + ctx context.Context + cancel context.CancelFunc + admission *preparationState + run *sessionState + preparing bool // prepared closes once the native preparation returns. prepared chan struct{} invalid bool @@ -37,7 +37,6 @@ type executorState struct { } func executorFingerprint(req proto.PromptRequestPayload) ([32]byte, error) { - req.RunID, req.Input = "", nil req.AgentSessionID = "" req.RequireExistingNativeSession = false data, err := json.Marshal(req) @@ -46,7 +45,7 @@ func executorFingerprint(req proto.PromptRequestPayload) ([32]byte, error) { func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, input proto.ExecutionPreparePayload) error { req := input.Configuration - if strings.TrimSpace(input.SessionID) == "" || req.RunID != "" || len(req.Input) != 0 || req.AgentStateKey != "agents-api-"+input.SessionID { + if strings.TrimSpace(input.SessionID) == "" { return r.rejectPreparation(env, "invalid_configuration") } declaration, available := r.registry.Declaration(req.AgentKind) @@ -62,7 +61,7 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, code) } if environment != nil { - req, err = environment.Configure(req) + err = environment.Configure(req) } else if req.LocalEnvironment != nil { err = errors.New("the Session has no Environment owner") } @@ -81,7 +80,6 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, "invalid_configuration") } requestFingerprint := sha256.Sum256(encoded) - req.Assignment = env.Assignment r.mu.Lock() if r.closed { r.mu.Unlock() @@ -110,12 +108,6 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, "preparation_capacity") } active := 0 - for _, candidate := range r.executors { - if candidate.sessionID != input.SessionID && candidate.stateKey == req.AgentStateKey { - r.mu.Unlock() - return r.rejectPreparation(env, "session_binding_conflict") - } - } for _, owner := range r.executors { if owner.preparing || owner.admission != nil || owner.run != nil || owner.invalid { active++ @@ -155,7 +147,7 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return r.rejectPreparation(env, "executor_capacity") } ownerCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) - owner = &executorState{declaration: declaration, id: uuid.NewString(), sessionID: input.SessionID, environmentID: req.EnvironmentID(), stateKey: req.AgentStateKey, fingerprint: fingerprint, ctx: ownerCtx, cancel: cancel, preparing: true, prepared: make(chan struct{}), nativeID: req.AgentSessionID} + owner = &executorState{declaration: declaration, id: uuid.NewString(), sessionID: input.SessionID, environmentID: req.EnvironmentID(), fingerprint: fingerprint, ctx: ownerCtx, cancel: cancel, preparing: true, prepared: make(chan struct{}), nativeID: req.AgentSessionID} r.executors[input.SessionID] = owner r.log.Info("executor owner_created", "executor_id", owner.id, "session_id", owner.sessionID) } @@ -182,7 +174,7 @@ func (r *Router) handleExecutorPrepare(ctx context.Context, env proto.Envelope, return nil } -func (r *Router) prepareExecutor(p *preparationState, req proto.PromptRequestPayload, factory agent.ExecutorFactory, environment Environment) { +func (r *Router) prepareExecutor(p *preparationState, configuration proto.PromptRequestPayload, factory agent.ExecutorFactory, environment Environment) { defer r.shutdownWG.Done() owner := p.executor started := time.Now() @@ -194,10 +186,20 @@ func (r *Router) prepareExecutor(p *preparationState, req proto.PromptRequestPay } var native agent.Executor var err error + req := agent.PrepareRequest{PromptRequestPayload: configuration, StateKey: "agents-api-" + owner.sessionID, Assignment: p.request.Assignment} if owner.ctx.Err() == nil { if environment != nil { req, err = environment.Prepare(owner.ctx, req) } + if err == nil && len(req.MCP) > 0 { + // The owner resolves the Environment's installed MCP servers only + // now, so the kind's declaration checks them before the factory. + selection := req.Selection() + for _, installed := range req.MCP { + selection.MCP = append(selection.MCP, proto.SelectedMCP{Origin: "environment", Label: installed.Server.Name, Installed: true}) + } + err = proto.ValidateSelection(owner.declaration, selection) + } if err == nil && owner.ctx.Err() == nil { native, err = factory(owner.ctx, req) } diff --git a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go index 1518d3fba..fbf320f1d 100644 --- a/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go +++ b/apps/daemon/internal/dispatch/executor_cancel_receipt_test.go @@ -126,7 +126,7 @@ func TestExecutorCancellationReachesNativeBeforeDurableReceiptJoin(t *testing.T) owner := &receiptCancelExecutor{turn: make(chan *receiptCancelTurn, 2), cancelFails: mode == "cancel_failure" || mode == "close_failure_retry", closeFailsFirst: mode == "close_failure_retry", closeEntered: make(chan struct{}), closeRelease: make(chan struct{})} reg := agent.NewRegistry() reg.RegisterKind(proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration()) - reg.RegisterExecutor("reusable", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { return owner, nil }) + reg.RegisterExecutor("reusable", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { return owner, nil }) r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, IdleTimeout: time.Hour}) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/dispatch/executor_handoff_test.go b/apps/daemon/internal/dispatch/executor_handoff_test.go index 004334b55..f1e4034a6 100644 --- a/apps/daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/daemon/internal/dispatch/executor_handoff_test.go @@ -93,7 +93,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { registry := agent.NewRegistry() registry.RegisterKind(proto.SupportedAgentKind{Kind: "handoff", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration()) var creates atomic.Int32 - registry.RegisterExecutor("handoff", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registry.RegisterExecutor("handoff", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { creates.Add(1) return owner, nil }) @@ -147,7 +147,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { } } } - request := proto.ExecutionPreparePayload{SessionID: "session", Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "handoff", AgentStateKey: "agents-api-session", DisableExecutionEnvironment: true})} + request := proto.ExecutionPreparePayload{SessionID: "session", Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "handoff", DisableExecutionEnvironment: true})} admit := func(id string) proto.PreparationStatusPayload { t.Helper() handle(proto.TypeExecutionPrepare, id, request) diff --git a/apps/daemon/internal/dispatch/executor_test.go b/apps/daemon/internal/dispatch/executor_test.go index 6b97a6740..16af4da0e 100644 --- a/apps/daemon/internal/dispatch/executor_test.go +++ b/apps/daemon/internal/dispatch/executor_test.go @@ -70,7 +70,7 @@ func (t *reusableTurn) AwaitSettlement(ctx context.Context) (agent.TurnSettlemen // noEnvironmentPreparation prepares config for session without an execution // environment, with the fixture model and provider. func noEnvironmentPreparation(session string, config proto.PromptRequestPayload) proto.ExecutionPreparePayload { - config.AgentStateKey, config.DisableExecutionEnvironment = stateKey(session), true + config.DisableExecutionEnvironment = true return proto.ExecutionPreparePayload{SessionID: session, Configuration: prototest.WithModel(config)} } func executorRequest() proto.ExecutionPreparePayload { @@ -86,7 +86,7 @@ func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) ( t.Helper() calls := &atomic.Int32{} reg := agent.NewRegistry() - registerExecutorKind(reg, proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registerExecutorKind(reg, proto.SupportedAgentKind{Kind: "reusable", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { calls.Add(1) return owner, nil }) @@ -247,14 +247,13 @@ func poolRouter(t *testing.T, factory agent.ExecutorFactory) (*dispatch.Router, func poolRequest(id string) proto.ExecutionPreparePayload { req := executorRequest() req.SessionID = id - req.Configuration.AgentStateKey = "agents-api-" + id return req } func TestExecutorIdleAndActiveCapacitiesAreIndependent(t *testing.T) { var mu sync.Mutex var owners []*reusableExecutor - r, s := poolRouter(t, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + r, s := poolRouter(t, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { e := &reusableExecutor{} mu.Lock() owners = append(owners, e) diff --git a/apps/daemon/internal/dispatch/functions_test.go b/apps/daemon/internal/dispatch/functions_test.go index 6559f0400..a92d39450 100644 --- a/apps/daemon/internal/dispatch/functions_test.go +++ b/apps/daemon/internal/dispatch/functions_test.go @@ -38,7 +38,7 @@ func TestFunctionReceiptsScopeRetriesAndConflicts(t *testing.T) { reg := agent.NewRegistry() sender := &recSender{} sessions := map[string]*functionSession{} - registerSession(reg, proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported})}, func(ctx context.Context, p proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(reg, proto.SupportedAgentKind{Kind: "function-test", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported})}, func(ctx context.Context, p fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { s := &functionSession{fakeSession: &fakeSession{out: out, ctx: ctx, closeOutOnCancel: true}} sessions[p.RunID] = s return s, nil @@ -150,7 +150,7 @@ func rejectsBeforeFactory(t *testing.T, caps proto.AgentKindCapabilities, req pr t.Helper() reg := agent.NewRegistry() var called atomic.Bool - registerExecutorKind(reg, proto.SupportedAgentKind{Kind: req.AgentKind, Available: true, Capabilities: caps}, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + registerExecutorKind(reg, proto.SupportedAgentKind{Kind: req.AgentKind, Available: true, Capabilities: caps}, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { called.Store(true) return nil, errors.New("unexpected executor preparation") }) diff --git a/apps/daemon/internal/dispatch/local_directory_test.go b/apps/daemon/internal/dispatch/local_directory_test.go index c2a8e1aad..b5898d219 100644 --- a/apps/daemon/internal/dispatch/local_directory_test.go +++ b/apps/daemon/internal/dispatch/local_directory_test.go @@ -28,7 +28,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing var harnessCalls atomic.Int32 reg := agent.NewRegistry() reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, prototest.ModelConfiguration()) - reg.RegisterExecutor("native", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + reg.RegisterExecutor("native", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { harnessCalls.Add(1) return nil, errors.New("must not prepare a harness") }) @@ -39,7 +39,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing } t.Cleanup(func() { _ = r.Shutdown(context.Background()) }) assign(t, r, session, environment) - request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, AgentStateKey: "agents-api-" + session, WorkspaceReadOnly: true} + request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, WorkspaceReadOnly: true} if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "idle", proto.ExecutionPreparePayload{SessionID: session, Configuration: request})); err != nil { t.Fatal(err) } @@ -57,9 +57,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionStart, "idle", proto.ExecutionStartPayload{Handle: ready.Handle, ExecutorID: ready.ExecutorID, RunID: "forbidden", Input: proto.TextInput("work")})); err == nil { t.Fatal("read preparation admitted execution") } - bad := request - bad.AgentStateKey = "agents-api-" + uuid.NewString() - if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "wrong-session", proto.ExecutionPreparePayload{SessionID: session, Configuration: bad})); err == nil { + if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "wrong-session", proto.ExecutionPreparePayload{SessionID: uuid.NewString(), Configuration: request})); err == nil { t.Fatal("wrong Session accepted") } _ = r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionRelease, "idle", proto.ExecutionReleasePayload{Handle: ready.Handle})) @@ -100,7 +98,7 @@ func TestLocalDirectoryKeepsNotDirectorySeparateFromFailures(t *testing.T) { } reg := agent.NewRegistry() reg.RegisterKind(proto.SupportedAgentKind{Kind: "native", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported})}, prototest.ModelConfiguration()) - reg.RegisterExecutor("native", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + reg.RegisterExecutor("native", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { return nil, errors.New("must not prepare a harness") }) sender := &recSender{} @@ -110,7 +108,7 @@ func TestLocalDirectoryKeepsNotDirectorySeparateFromFailures(t *testing.T) { } t.Cleanup(func() { _ = r.Shutdown(context.Background()) }) assign(t, r, session, environment) - request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, AgentStateKey: "agents-api-" + session, WorkspaceReadOnly: true} + request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, WorkspaceReadOnly: true} if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "idle", proto.ExecutionPreparePayload{SessionID: session, Configuration: request})); err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/dispatch/mcp_http_test.go b/apps/daemon/internal/dispatch/mcp_http_test.go index 0d8b8cadd..4a05b2062 100644 --- a/apps/daemon/internal/dispatch/mcp_http_test.go +++ b/apps/daemon/internal/dispatch/mcp_http_test.go @@ -61,7 +61,7 @@ func TestMCPHTTPBearerRejectsUnsupportedRequestsBeforeFactory(t *testing.T) { } var called atomic.Bool registerExecutorKind(h.reg, proto.SupportedAgentKind{Kind: req.AgentKind, Available: mode != "unavailable", Capabilities: caps}, - func(_ context.Context, got proto.PromptRequestPayload) (agent.Executor, error) { + func(_ context.Context, got agent.PrepareRequest) (agent.Executor, error) { called.Store(true) if mode == "required" && !(*got.MCPHTTPServers)[0].Required { t.Error("required initialization lost before adapter") @@ -95,7 +95,6 @@ func TestLocalMCPOriginAndCapabilityAdmission(t *testing.T) { req.Configuration.MCPHTTPServers = &servers if strings.HasPrefix(mode, "environment") { servers[0].ConnectionOrigin = "environment" - req.Configuration.LocalEnvironment.NetworkAccess = "enabled" } if strings.Contains(mode, "bearer") { token := "synthetic-private-token" @@ -112,7 +111,7 @@ func TestLocalMCPOriginAndCapabilityAdmission(t *testing.T) { } entered := make(chan struct{}, 1) h.reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, MCPHTTPTools: proto.CapabilityFromBool(mode != "environment missing capability"), MCPHTTPBearerAuth: proto.CapabilitySupported, MCPHTTPRequired: proto.CapabilitySupported})}, prototest.ModelConfiguration()) - h.reg.RegisterExecutor("prepared", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + h.reg.RegisterExecutor("prepared", func(context.Context, agent.PrepareRequest) (agent.Executor, error) { entered <- struct{}{} return nil, errors.New("controlled stop") }) diff --git a/apps/daemon/internal/dispatch/preparation.go b/apps/daemon/internal/dispatch/preparation.go index 76ca2ab2b..ed9477416 100644 --- a/apps/daemon/internal/dispatch/preparation.go +++ b/apps/daemon/internal/dispatch/preparation.go @@ -59,11 +59,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if environment == nil || !declaration.Capabilities.LocalEnvironment.IsSupported() || !proto.ValidWorkspaceReadPreparation(req) { return r.rejectPreparation(env, "unsupported_read_preparation") } - req, err := environment.Configure(req) - if err != nil { - return r.rejectPreparation(env, "invalid_configuration") - } - if req.RunID != "" || len(req.Input) != 0 || req.EnvironmentID() == "" || strings.TrimSpace(req.AgentStateKey) == "" { + if environment.Configure(req) != nil || req.EnvironmentID() == "" { return r.rejectPreparation(env, "invalid_configuration") } if validateExecutionEnvironment(req) != nil || proto.ValidateSelection(declaration, req.Selection()) != nil { diff --git a/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go b/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go index a8c952438..c1c0741dd 100644 --- a/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go +++ b/apps/daemon/internal/dispatch/preparation_executor_fixture_test.go @@ -34,8 +34,8 @@ type preparationFactory func(context.Context, proto.PromptRequestPayload) (prepa // Old fault-injection fixtures model disposable executors, not reusable native implementations. func preparationExecutorFixture(factory preparationFactory) agent.ExecutorFactory { - return func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { - prepared, err := factory(ctx, req) + return func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) { + prepared, err := factory(ctx, req.PromptRequestPayload) if prepared == nil { return nil, err } diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index 12bd21717..bbeaba392 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -111,11 +111,8 @@ func localPreparationHarness(t *testing.T) *harness { return h } -// stateKey is the AgentStateKey Core derives for the Session. -func stateKey(session string) string { return "agents-api-" + session } - func preparationRequest() proto.ExecutionPreparePayload { - return proto.ExecutionPreparePayload{SessionID: preparationSessionID, Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "prepared", AgentStateKey: stateKey(preparationSessionID), LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID, NetworkAccess: "enabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}})} + return proto.ExecutionPreparePayload{SessionID: preparationSessionID, Configuration: prototest.WithModel(proto.PromptRequestPayload{AgentKind: "prepared", LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID, WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}})} } func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory preparationFactory) *dispatch.Router { @@ -168,9 +165,6 @@ func TestPreparationReleaseDuringBlockedFactory(t *testing.T) { p := &controlledPreparation{closed: make(chan struct{})} entered, allowReturn := make(chan context.Context, 1), make(chan struct{}) r := preparationRouter(t, sender, time.Minute, func(ctx context.Context, req proto.PromptRequestPayload) (preparedFixture, error) { - if req.RunID != "" || len(req.Input) != 0 { - t.Error("run input reached preparation") - } entered <- ctx <-allowReturn return p, nil @@ -367,28 +361,17 @@ func TestPreparationFailedReadyDeliveryAbandonsAdmission(t *testing.T) { } } -func TestPreparationRejectsInputAndProductConfiguration(t *testing.T) { - for name, change := range map[string]func(*proto.PromptRequestPayload){ - "run": func(p *proto.PromptRequestPayload) { p.RunID = "run" }, - "input": func(p *proto.PromptRequestPayload) { p.Input = proto.TextInput("input") }, - "attachment": func(p *proto.PromptRequestPayload) { - p.Input = proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image"}}}} - }, - "missing environment": func(p *proto.PromptRequestPayload) { p.LocalEnvironment = nil }, - } { - t.Run(name, func(t *testing.T) { - r := preparationRouter(t, &recSender{}, time.Minute, func(context.Context, proto.PromptRequestPayload) (preparedFixture, error) { - t.Error("invalid preparation reached native factory") - return nil, errors.New("invalid") - }) - req := preparationRequest() - change(&req.Configuration) - if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "request", req)); err == nil { - t.Fatal("invalid preparation accepted") - } - if r.ActiveRuns() != 0 { - t.Fatal("invalid configuration became a Run") - } - }) +func TestPreparationRequiresEnvironment(t *testing.T) { + r := preparationRouter(t, &recSender{}, time.Minute, func(context.Context, proto.PromptRequestPayload) (preparedFixture, error) { + t.Error("invalid preparation reached native factory") + return nil, errors.New("invalid") + }) + req := preparationRequest() + req.Configuration.LocalEnvironment = nil + if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "request", req)); err == nil { + t.Fatal("invalid preparation accepted") + } + if r.ActiveRuns() != 0 { + t.Fatal("invalid configuration became a Run") } } diff --git a/apps/daemon/internal/dispatch/receipt_order_test.go b/apps/daemon/internal/dispatch/receipt_order_test.go index c54312c64..550a0e90e 100644 --- a/apps/daemon/internal/dispatch/receipt_order_test.go +++ b/apps/daemon/internal/dispatch/receipt_order_test.go @@ -50,7 +50,7 @@ func TestDurableCompletionWaitsForSteeringReceiptSend(t *testing.T) { registry := agent.NewRegistry() var session *fakeSession var calls atomic.Int32 - registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(context.Context, proto.PromptSteerPayload, func()) error { calls.Add(1) @@ -125,7 +125,7 @@ func TestShutdownReleasesSteeringWorkerAndReceiptJoin(t *testing.T) { registry := agent.NewRegistry() entered, exited := make(chan struct{}), make(chan struct{}) var session *fakeSession - registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(ctx context.Context, _ proto.PromptSteerPayload, _ func()) error { close(entered) diff --git a/apps/daemon/internal/dispatch/receipt_shutdown_test.go b/apps/daemon/internal/dispatch/receipt_shutdown_test.go index dec26022a..8f23ca5a8 100644 --- a/apps/daemon/internal/dispatch/receipt_shutdown_test.go +++ b/apps/daemon/internal/dispatch/receipt_shutdown_test.go @@ -45,7 +45,7 @@ func TestShutdownCancelsCompletionErrorSend(t *testing.T) { sender := &shutdownAllSendsBlockSender{entered: make(chan struct{}), terminal: make(chan context.Context, 1), rescue: make(chan struct{})} registry := agent.NewRegistry() var session *fakeSession - registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(registry, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(context.Context, proto.PromptSteerPayload, func()) error { return nil }}, nil }) diff --git a/apps/daemon/internal/dispatch/router_test.go b/apps/daemon/internal/dispatch/router_test.go index 1b997a908..3702f93a5 100644 --- a/apps/daemon/internal/dispatch/router_test.go +++ b/apps/daemon/internal/dispatch/router_test.go @@ -111,7 +111,7 @@ type harness struct { router *dispatch.Router sender *recSender reg *agent.Registry - gotReq chan proto.PromptRequestPayload + gotReq chan fixtureRun gotSess chan *fakeSession } @@ -120,10 +120,10 @@ func newHarness(t *testing.T) *harness { h := &harness{ sender: &recSender{}, reg: agent.NewRegistry(), - gotReq: make(chan proto.PromptRequestPayload, 16), + gotReq: make(chan fixtureRun, 16), gotSess: make(chan *fakeSession, 16), } - registerSession(h.reg, proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "fake_alpha", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { sess := &fakeSession{out: out, ctx: ctx, closeOutOnCancel: true} h.gotReq <- req h.gotSess <- sess @@ -139,17 +139,23 @@ func newHarness(t *testing.T) *harness { // registerSession declares info, without an Environment, and starts each // Turn of the kind with factory. -func registerSession(reg *agent.Registry, info proto.SupportedAgentKind, factory func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (fixtureSession, error)) { +func registerSession(reg *agent.Registry, info proto.SupportedAgentKind, factory func(context.Context, fixtureRun, chan<- proto.Envelope) (fixtureSession, error)) { info.Capabilities.EnvironmentNone = proto.CapabilitySupported reg.RegisterKind(info, prototest.ModelConfiguration()) reg.RegisterExecutor(info.Kind, preparationExecutorFixture(func(_ context.Context, req proto.PromptRequestPayload) (preparedFixture, error) { return &controlledPreparation{start: func(ctx context.Context, id string, input proto.MessageInput, out chan<- proto.Envelope) (fixtureSession, error) { - req.RunID, req.Input = id, input - return factory(ctx, req, out) + return factory(ctx, fixtureRun{PromptRequestPayload: req, RunID: id, Input: input}, out) }}, nil })) } +// fixtureRun is the prepared request and Start a fixture Session runs. +type fixtureRun struct { + proto.PromptRequestPayload + RunID string + Input proto.MessageInput +} + // startRun binds the Session run to r, prepares its Executor of kind, starts // run and waits until it accepts operations. sender records r's frames. func startRun(t *testing.T, r *dispatch.Router, sender *recSender, kind, run string) { diff --git a/apps/daemon/internal/dispatch/runtime_preparation_execution_test.go b/apps/daemon/internal/dispatch/runtime_preparation_execution_test.go index f6bb85f2a..9c57c7112 100644 --- a/apps/daemon/internal/dispatch/runtime_preparation_execution_test.go +++ b/apps/daemon/internal/dispatch/runtime_preparation_execution_test.go @@ -38,7 +38,6 @@ func TestRuntimePreparationUnavailablePreventsNativeExecutor(t *testing.T) { t.Fatalf("capability fixture is unavailable: %v", err) } request := preparationRequest() - request.Configuration.LocalEnvironment.Capabilities = true request.Configuration.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{source}} if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "missing-capability", request)); err != nil { t.Fatalf("valid frozen selection rejected before preparation: %v", err) diff --git a/apps/daemon/internal/dispatch/steering_lifetime_test.go b/apps/daemon/internal/dispatch/steering_lifetime_test.go index 067d030a3..d7a241ff8 100644 --- a/apps/daemon/internal/dispatch/steering_lifetime_test.go +++ b/apps/daemon/internal/dispatch/steering_lifetime_test.go @@ -16,7 +16,7 @@ func TestDurableSteeringWaitsBeyondTransportDeadline(t *testing.T) { var session *fakeSession var calls atomic.Int32 release := make(chan struct{}) - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { session = &fakeSession{out: out, closeOutOnCancel: true} return &steeringSession{fakeSession: session, steer: func(ctx context.Context, input proto.PromptSteerPayload, written func()) error { calls.Add(1) @@ -67,7 +67,7 @@ func TestDurableSteeringTransportTimeoutAndShutdown(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) exited := make(chan struct{}) - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(_ context.Context, _ fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { return &steeringSession{fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(ctx context.Context, _ proto.PromptSteerPayload, written func()) error { defer close(exited) if phase == "written" { diff --git a/apps/daemon/internal/dispatch/steering_test.go b/apps/daemon/internal/dispatch/steering_test.go index d1eb361de..8bded4b25 100644 --- a/apps/daemon/internal/dispatch/steering_test.go +++ b/apps/daemon/internal/dispatch/steering_test.go @@ -31,7 +31,7 @@ func TestSteeringReceiptsAndRetries(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls, starts := 0, 0 - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { starts++ return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, @@ -87,7 +87,7 @@ func TestSteeringReadinessAndInputValidation(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload, func()) error { @@ -153,7 +153,7 @@ func TestSteeringDoesNotBlockOtherRunCancellation(t *testing.T) { defer h.router.Shutdown(context.Background()) entered, release := make(chan struct{}), make(chan struct{}) defer close(release) - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload, func()) error { @@ -211,7 +211,7 @@ func TestSteeringCapacityPreservesExistingReceipts(t *testing.T) { h := newHarness(t) defer h.router.Shutdown(context.Background()) calls := 0 - registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (fixtureSession, error) { + registerSession(h.reg, proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, func(ctx context.Context, req fixtureRun, out chan<- proto.Envelope) (fixtureSession, error) { return &steeringSession{ fakeSession: &fakeSession{out: out, closeOutOnCancel: true}, steer: func(context.Context, proto.PromptSteerPayload, func()) error { diff --git a/apps/daemon/internal/dispatch/workspace_export_test.go b/apps/daemon/internal/dispatch/workspace_export_test.go index 88ca1b55b..ab23a8414 100644 --- a/apps/daemon/internal/dispatch/workspace_export_test.go +++ b/apps/daemon/internal/dispatch/workspace_export_test.go @@ -33,10 +33,10 @@ func (s exportSender) Send(ctx context.Context, env proto.Envelope) error { // bytes, and fails after it when fail is set. type stubEnvironment struct{ fail bool } -func (stubEnvironment) Configure(r proto.PromptRequestPayload) (proto.PromptRequestPayload, error) { - return r, nil +func (stubEnvironment) Configure(proto.PromptRequestPayload) error { + return nil } -func (stubEnvironment) Prepare(_ context.Context, r proto.PromptRequestPayload) (proto.PromptRequestPayload, error) { +func (stubEnvironment) Prepare(_ context.Context, r agent.PrepareRequest) (agent.PrepareRequest, error) { return r, nil } func (stubEnvironment) ApplyRuntimePreparation(context.Context, uuid.UUID, proto.RuntimePreparePayload, []byte) error { diff --git a/apps/daemon/internal/gateway/mcp_test.go b/apps/daemon/internal/gateway/mcp_test.go index ad82809af..3778a7b94 100644 --- a/apps/daemon/internal/gateway/mcp_test.go +++ b/apps/daemon/internal/gateway/mcp_test.go @@ -28,7 +28,7 @@ func TestMCPBrokersBothOrigins(t *testing.T) { BearerToken: &token, HTTPHeaders: map[string]string{"x-tenant": " tenant-secret "}} } service := proto.PromptRequestPayload{DisableExecutionEnvironment: true} - environment := proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled"}} + environment := proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}} for _, c := range []struct { origin string diff --git a/apps/daemon/internal/localworkspace/binding.go b/apps/daemon/internal/localworkspace/binding.go index 5fbc50f56..e0ca04e05 100644 --- a/apps/daemon/internal/localworkspace/binding.go +++ b/apps/daemon/internal/localworkspace/binding.go @@ -11,7 +11,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" ) // Binding freezes operator-owned identity and paths for one Runtime lifetime. @@ -19,7 +18,7 @@ type Binding struct { environment string networkAccess string allowedDomains []string - stateKey string + session string workspace string writer *fileWriter capabilityMu sync.Mutex @@ -55,36 +54,20 @@ func Load() (*Binding, error) { return b, nil } -// Configure validates the reference before supplying the immutable local cwd. -func (b *Binding) Configure(r proto.PromptRequestPayload) (proto.PromptRequestPayload, error) { - if r.LocalEnvironment == nil || r.LocalEnvironment.ID != b.environment || r.AgentStateKey != b.stateKey || - r.DisableExecutionEnvironment { - return r, errors.New("request does not match the dedicated local Environment") - } - if !r.WorkspaceReadOnly || r.LocalEnvironment.NetworkAccess != "" || len(r.LocalEnvironment.AllowedDomains) > 0 { - requested := agentnetwork.Policy{Access: r.LocalEnvironment.NetworkAccess, AllowedDomains: r.LocalEnvironment.AllowedDomains} - if !b.NetworkPolicy().Equal(requested) { - return r, errors.New("request does not match the local Runtime network policy") - } - } - if !r.WorkspaceReadOnly { - local := *r.LocalEnvironment - if local.WorkspaceDirectory != "/workspace" && local.WorkspaceDirectory != b.workspace { - return r, errors.New("request does not match the local workspace selection") - } - if local.CapabilitySources == nil || agentcapabilities.ValidateInput(*local.CapabilitySources) != nil { - return r, agentcapabilities.ErrInvalid - } - sources := *local.CapabilitySources - present := len(sources.Skills)+len(sources.Plugins)+len(sources.Directories) > 0 - if present != local.Capabilities { - return r, agentcapabilities.ErrInvalid - } - local.Skills, local.MCP, local.CapabilityRoot = nil, nil, "" - local.WorkspaceRoot = b.workspace - r.LocalEnvironment = &local +// Configure checks the request against the bound Environment and workspace. +func (b *Binding) Configure(r proto.PromptRequestPayload) error { + local := r.LocalEnvironment + switch { + case local == nil || local.ID != b.environment || r.DisableExecutionEnvironment: + return errors.New("request does not match the dedicated local Environment") + case r.WorkspaceReadOnly: + return nil + case local.WorkspaceDirectory != "/workspace" && local.WorkspaceDirectory != b.workspace: + return errors.New("request does not match the local workspace selection") + case local.CapabilitySources == nil || agentcapabilities.ValidateInput(*local.CapabilitySources) != nil: + return agentcapabilities.ErrInvalid } - return r, nil + return nil } // Resolve is the Session's Environment owner: b for the one Session it is @@ -103,7 +86,7 @@ func (b *Binding) Support() agent.EnvironmentSupport { } func (b *Binding) Matches(environment, session string) bool { - return b != nil && b.environment == environment && b.stateKey == "agents-api-"+session + return b != nil && b.environment == environment && b.session == session } // Close keeps the workspace, which outlives each assignment of its Session. diff --git a/apps/daemon/internal/localworkspace/binding_test.go b/apps/daemon/internal/localworkspace/binding_test.go index 68a30dad6..efa036b82 100644 --- a/apps/daemon/internal/localworkspace/binding_test.go +++ b/apps/daemon/internal/localworkspace/binding_test.go @@ -1,17 +1,17 @@ package localworkspace import ( - "errors" "os" "path/filepath" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" ) -func testBinding(t *testing.T) (*Binding, proto.PromptRequestPayload) { +func testBinding(t *testing.T) (*Binding, agent.PrepareRequest) { t.Helper() private := t.TempDir() if err := os.Chmod(private, 0700); err != nil { @@ -24,28 +24,26 @@ func testBinding(t *testing.T) (*Binding, proto.PromptRequestPayload) { if err != nil { t.Fatal(err) } - b.networkAccess = "disabled" - return b, proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{ID: environment, NetworkAccess: "disabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}, AgentStateKey: "agents-api-" + session} + return b, agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}}} } func TestBindingRejectsScopeOverrides(t *testing.T) { - b, valid := testBinding(t) - configured, err := b.Configure(valid) - if err != nil || configured.LocalEnvironment.WorkspaceRoot != b.workspace { - t.Fatalf("frozen cwd: %+v %v", configured, err) + b, prepared := testBinding(t) + valid := prepared.PromptRequestPayload + if err := b.Configure(valid); err != nil { + t.Fatal(err) } for name, mutate := range map[string]func(*proto.PromptRequestPayload){ "missing reference": func(r *proto.PromptRequestPayload) { r.LocalEnvironment = nil }, "other Environment": func(r *proto.PromptRequestPayload) { r.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString()} }, - "other Session": func(r *proto.PromptRequestPayload) { r.AgentStateKey = "agents-api-" + uuid.NewString() }, - "none": func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = true }, + "none": func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = true }, } { t.Run(name, func(t *testing.T) { r := valid mutate(&r) - if _, err := b.Configure(r); err == nil { + if err := b.Configure(r); err == nil { t.Fatal("unsafe request accepted") } }) @@ -65,29 +63,6 @@ func TestDirectoryValidatesRelativePaths(t *testing.T) { } } -func TestBindingPrepareRejectsOtherWorkspaceRoot(t *testing.T) { - b, req := testBinding(t) - configured, err := b.Configure(req) - if err != nil { - t.Fatal(err) - } - for _, root := range []string{"", t.TempDir()} { - local := *configured.LocalEnvironment - local.WorkspaceRoot = root - other := configured - other.LocalEnvironment = &local - if _, err := b.Prepare(t.Context(), other); !errors.Is(err, agentcapabilities.ErrInvalid) { - t.Fatalf("workspace root %q: %v", root, err) - } - } - if _, err := os.Stat(filepath.Join(b.capabilityRoot, agentcapabilities.ManifestName)); !os.IsNotExist(err) { - t.Fatal("rejected preparation installed capabilities") - } - if _, err := b.Prepare(t.Context(), configured); err != nil { - t.Fatal("bound workspace root rejected", err) - } -} - func TestCapabilityLayoutUsesOperatorDirectories(t *testing.T) { b, _ := testBinding(t) for _, directory := range []string{filepath.Join(b.workspace, "capabilities"), filepath.Join(os.Getenv("OAC_RUNTIME_HOME"), "capabilities")} { diff --git a/apps/daemon/internal/localworkspace/capabilities.go b/apps/daemon/internal/localworkspace/capabilities.go index cd3ca2d84..a85b24143 100644 --- a/apps/daemon/internal/localworkspace/capabilities.go +++ b/apps/daemon/internal/localworkspace/capabilities.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/runtimefs" @@ -15,12 +16,11 @@ import ( // Prepare runs under the admitted executor's lifetime, before native startup. // Reconnection validates installed contents without reopening mutable sources. -func (b *Binding) Prepare(ctx context.Context, r proto.PromptRequestPayload) (proto.PromptRequestPayload, error) { +func (b *Binding) Prepare(ctx context.Context, r agent.PrepareRequest) (agent.PrepareRequest, error) { if b == nil && r.LocalEnvironment == nil || r.WorkspaceReadOnly { return r, nil } - if b == nil || r.LocalEnvironment == nil || r.LocalEnvironment.CapabilitySources == nil || - r.LocalEnvironment.ID != b.environment || r.AgentStateKey != b.stateKey || r.LocalEnvironment.WorkspaceRoot != b.workspace { + if b == nil || r.LocalEnvironment == nil || r.LocalEnvironment.CapabilitySources == nil || r.LocalEnvironment.ID != b.environment { return r, agentcapabilities.ErrInvalid } b.capabilityMu.Lock() @@ -47,12 +47,11 @@ func (b *Binding) Prepare(ctx context.Context, r proto.PromptRequestPayload) (pr if err = ctx.Err(); err != nil { return r, err } - local := *r.LocalEnvironment - local.Skills, local.MCP, local.CapabilityRoot = manifest.Skills, nil, b.capabilityRoot - for i := range local.Skills { - local.Skills[i].InstallationRoot = b.capabilityRoot + r.WorkspaceRoot, r.CapabilityRoot, r.Skills, r.MCP = b.workspace, b.capabilityRoot, manifest.Skills, nil + for i := range r.Skills { + r.Skills[i].InstallationRoot = b.capabilityRoot } - if local.ToolEnvironment { + if r.LocalEnvironment.ToolEnvironment { if _, err = ReadToolEnvironment(); err != nil { return r, err } @@ -65,16 +64,15 @@ func (b *Binding) Prepare(ctx context.Context, r proto.PromptRequestPayload) (pr if readErr != nil { return r, readErr } - local.MCP, err = resolveEnvironmentMCP(manifest.MCP, values) - for i := range local.MCP { - local.MCP[i].InstallationRoot = b.capabilityRoot - local.MCP[i].WorkspaceRoot = b.workspace + r.MCP, err = resolveEnvironmentMCP(manifest.MCP, values) + for i := range r.MCP { + r.MCP[i].InstallationRoot = b.capabilityRoot + r.MCP[i].WorkspaceRoot = b.workspace } if err != nil { return r, err } } - r.LocalEnvironment = &local return r, nil } @@ -152,7 +150,7 @@ func (b *Binding) loadCapabilitySnapshot(root *os.Root, input agentcapabilities. } func (b *Binding) capabilityIdentity() agentcapabilities.Identity { - return agentcapabilities.Identity{EnvironmentID: b.environment, SessionID: strings.TrimPrefix(b.stateKey, "agents-api-")} + return agentcapabilities.Identity{EnvironmentID: b.environment, SessionID: b.session} } // The current Linux Runtime layout is shared by user-owned and managed hosts. diff --git a/apps/daemon/internal/localworkspace/capabilities_test.go b/apps/daemon/internal/localworkspace/capabilities_test.go index 224213fca..42e1c3c94 100644 --- a/apps/daemon/internal/localworkspace/capabilities_test.go +++ b/apps/daemon/internal/localworkspace/capabilities_test.go @@ -1,32 +1,12 @@ package localworkspace -import ( - "bytes" - "encoding/json" - "testing" +import "testing" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func TestCapabilityPathsStayRuntimeOwnedAndDoNotGateReads(t *testing.T) { +func TestCapabilitiesDoNotGateReads(t *testing.T) { binding, request := testBinding(t) - request.LocalEnvironment.CapabilityRoot = "/caller/installation" - request.LocalEnvironment.Skills = []agentcapabilities.InstalledSkill{{RelativeRoot: "caller/private", PackageRoot: "caller", InstallationRoot: "/caller/installation"}} - secret := "private-mcp-marker" - request.LocalEnvironment.MCP = []proto.EnvironmentMCP{{PackageRoot: "caller/private", BearerToken: &secret}} - raw, err := json.Marshal(request.LocalEnvironment) - if err != nil || bytes.Contains(raw, []byte("caller")) || bytes.Contains(raw, []byte("skills")) || bytes.Contains(raw, []byte(secret)) { - t.Fatal("Runtime paths crossed the public daemon descriptor", err) - } - configured, err := binding.Configure(request) - if err != nil || len(configured.LocalEnvironment.Skills) != 0 || len(configured.LocalEnvironment.MCP) != 0 || configured.LocalEnvironment.CapabilityRoot != "" { - t.Fatal("execution accepted caller-supplied Skill paths", err) - } - request.LocalEnvironment.Capabilities = true - request.LocalEnvironment.Skills = nil request.WorkspaceReadOnly = true - if _, err := binding.Configure(request); err != nil { + request.LocalEnvironment.CapabilitySources = nil + if err := binding.Configure(request.PromptRequestPayload); err != nil { t.Fatal("read-only binding required a capability installation", err) } } diff --git a/apps/daemon/internal/localworkspace/capability_preparation_test.go b/apps/daemon/internal/localworkspace/capability_preparation_test.go index dc061b085..575537844 100644 --- a/apps/daemon/internal/localworkspace/capability_preparation_test.go +++ b/apps/daemon/internal/localworkspace/capability_preparation_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" @@ -26,17 +27,16 @@ func TestPreparationFreezesLocalContentsAcrossReconnect(t *testing.T) { b, req := testBinding(t) source := t.TempDir() writeSourceSkill(t, source, "first") - req.LocalEnvironment.Capabilities = true req.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{source}} - configured, err := b.Configure(req) + err := b.Configure(req.PromptRequestPayload) if err != nil { t.Fatal(err) } if _, err = os.Stat(filepath.Join(b.capabilityRoot, agentcapabilities.ManifestName)); !os.IsNotExist(err) { t.Fatal("binding installed before asynchronous admission") } - first, err := b.Prepare(t.Context(), configured) - if err != nil || len(first.LocalEnvironment.Skills) != 1 { + first, err := b.Prepare(t.Context(), req) + if err != nil || len(first.Skills) != 1 { t.Fatalf("first preparation: %v", err) } writeSourceSkill(t, source, "second") @@ -45,22 +45,20 @@ func TestPreparationFreezesLocalContentsAcrossReconnect(t *testing.T) { t.Fatal(err) } reconnect.networkAccess = b.networkAccess - again, err := reconnect.Prepare(t.Context(), configured) - if err != nil || len(again.LocalEnvironment.Skills) != 1 { + again, err := reconnect.Prepare(t.Context(), req) + if err != nil || len(again.Skills) != 1 { t.Fatalf("reconnection: %v", err) } - frozen, err := os.ReadFile(filepath.Join(b.capabilityRoot, again.LocalEnvironment.Skills[0].RelativeRoot, "SKILL.md")) + frozen, err := os.ReadFile(filepath.Join(b.capabilityRoot, again.Skills[0].RelativeRoot, "SKILL.md")) if err != nil || string(frozen[len(frozen)-5:]) != "first" { t.Fatal("reconnection recaptured source", err) } next, nextReq := testBinding(t) - nextReq.LocalEnvironment.Capabilities = true nextReq.LocalEnvironment.CapabilitySources = req.LocalEnvironment.CapabilitySources - nextConfigured, err := next.Configure(nextReq) - if err != nil { + if err := next.Configure(nextReq.PromptRequestPayload); err != nil { t.Fatal(err) } - if _, err = next.Prepare(t.Context(), nextConfigured); err != nil { + if _, err = next.Prepare(t.Context(), nextReq); err != nil { t.Fatal(err) } fresh, err := os.ReadFile(filepath.Join(next.capabilityRoot, "directories/0/SKILL.md")) @@ -68,14 +66,12 @@ func TestPreparationFreezesLocalContentsAcrossReconnect(t *testing.T) { t.Fatal("new Session did not capture new source", err) } // Reusing a snapshot under another identity or selection cannot start native work. - reconnect.stateKey = "agents-api-" + uuid.NewString() - changed := configured - changed.AgentStateKey = reconnect.stateKey - if _, err = reconnect.Prepare(t.Context(), changed); err == nil { + reconnect.session = uuid.NewString() + if _, err = reconnect.Prepare(t.Context(), req); err == nil { t.Fatal("foreign snapshot accepted") } - changed = configured - local := *configured.LocalEnvironment + changed := req + local := *req.LocalEnvironment local.CapabilitySources = &agentcapabilities.Input{} changed.LocalEnvironment = &local if _, err = b.Prepare(t.Context(), changed); err == nil { @@ -104,38 +100,37 @@ func TestPreparationUsesOperatorSourcesAndLeavesFailuresInert(t *testing.T) { t.Fatal("logical workspace source rejected", err) } root.Close() - req.LocalEnvironment.Capabilities = true req.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{source, t.TempDir()}} - configured, err := b.Configure(req) + err = b.Configure(req.PromptRequestPayload) if err != nil { t.Fatal(err) } - if _, err = b.Prepare(t.Context(), configured); err == nil { + if _, err = b.Prepare(t.Context(), req); err == nil { t.Fatal("invalid second source accepted") } if _, err = os.Stat(filepath.Join(b.capabilityRoot, agentcapabilities.ManifestName)); !os.IsNotExist(err) { t.Fatal("partial snapshot ready") } - if _, err = b.Prepare(t.Context(), configured); err == nil { + if _, err = b.Prepare(t.Context(), req); err == nil { t.Fatal("partial snapshot silently replayed") } } func TestPreparationEmptySelectionAndCancellation(t *testing.T) { b, req := testBinding(t) - configured, err := b.Configure(req) + err := b.Configure(req.PromptRequestPayload) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(t.Context()) cancel() - if _, err = b.Prepare(ctx, configured); err == nil { + if _, err = b.Prepare(ctx, req); err == nil { t.Fatal("cancelled preparation started") } - if _, err = b.Prepare(t.Context(), configured); err != nil { + if _, err = b.Prepare(t.Context(), req); err != nil { t.Fatal(err) } - read := proto.PromptRequestPayload{WorkspaceReadOnly: true} + read := agent.PrepareRequest{PromptRequestPayload: proto.PromptRequestPayload{WorkspaceReadOnly: true}} if _, err = b.Prepare(t.Context(), read); err != nil { t.Fatal("Files required capability installation", err) } @@ -146,11 +141,11 @@ func TestRuntimePreparationRejectsMissingRequiredToolEnvironment(t *testing.T) { t.Setenv("OAC_RUNTIME_INITIALIZATION_DIRECTORY", t.TempDir()) t.Setenv("OAC_RUNTIME_TOOL_ENV_FILE", "") req.LocalEnvironment.ToolEnvironment = true - configured, err := b.Configure(req) + err := b.Configure(req.PromptRequestPayload) if err != nil { t.Fatal(err) } - if _, err = b.Prepare(t.Context(), configured); err == nil { + if _, err = b.Prepare(t.Context(), req); err == nil { t.Fatal("missing required tool environment admitted") } directory, err := InitializationDirectory() @@ -160,14 +155,14 @@ func TestRuntimePreparationRejectsMissingRequiredToolEnvironment(t *testing.T) { if err = os.WriteFile(filepath.Join(directory, "tool-env.json"), []byte(`{"READY":"yes"}`), 0600); err != nil { t.Fatal(err) } - if _, err = b.Prepare(t.Context(), configured); err != nil { + if _, err = b.Prepare(t.Context(), req); err != nil { t.Fatal("prepared tool environment rejected", err) } if err = os.Remove(filepath.Join(directory, "tool-env.json")); err != nil { t.Fatal(err) } - configured.LocalEnvironment.ToolEnvironment = false - if _, err = b.Prepare(t.Context(), configured); err == nil { + req.LocalEnvironment.ToolEnvironment = false + if _, err = b.Prepare(t.Context(), req); err == nil { t.Fatal("deleted prepared tool environment was silently recreated") } } @@ -195,11 +190,10 @@ func TestPreparationFreezesToolOnlyEnvironmentAcrossReconnect(t *testing.T) { t.Fatal(err) } } - configured, err := b.Configure(req) - if err != nil { + if err := b.Configure(req.PromptRequestPayload); err != nil { t.Fatal(err) } - if _, err := b.Prepare(t.Context(), configured); err != nil { + if _, err := b.Prepare(t.Context(), req); err != nil { t.Fatal(err) } if err := os.WriteFile(source, []byte(`{"LOCAL_ONLY":"changed"}`), 0600); err != nil { @@ -210,7 +204,7 @@ func TestPreparationFreezesToolOnlyEnvironmentAcrossReconnect(t *testing.T) { t.Fatal(err) } reconnect.networkAccess = b.networkAccess - if _, err := reconnect.Prepare(t.Context(), configured); err != nil { + if _, err := reconnect.Prepare(t.Context(), req); err != nil { t.Fatal(err) } values, err := ReadOptionalToolEnvironment() diff --git a/apps/daemon/internal/localworkspace/mcp.go b/apps/daemon/internal/localworkspace/mcp.go index ec63016ae..37d371991 100644 --- a/apps/daemon/internal/localworkspace/mcp.go +++ b/apps/daemon/internal/localworkspace/mcp.go @@ -3,12 +3,12 @@ package localworkspace import ( "os" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // MCPStdioCommand resolves the common installed manifest in the daemon. -func MCPStdioCommand(server proto.EnvironmentMCP) (string, []string) { +func MCPStdioCommand(server agent.EnvironmentMCP) (string, []string) { executable, err := os.Executable() if err != nil { return "", nil @@ -16,14 +16,14 @@ func MCPStdioCommand(server proto.EnvironmentMCP) (string, []string) { return executable, []string{"runtime-mcp-exec", server.InstallationRoot, server.PackageRoot, server.Server.Name} } -func resolveEnvironmentMCP(installed []agentcapabilities.InstalledMCP, values map[string]string) ([]proto.EnvironmentMCP, error) { +func resolveEnvironmentMCP(installed []agentcapabilities.InstalledMCP, values map[string]string) ([]agent.EnvironmentMCP, error) { tokens, err := agentcapabilities.ResolveMCP(installed, values) if err != nil { return nil, err } - result := make([]proto.EnvironmentMCP, 0, len(installed)) + result := make([]agent.EnvironmentMCP, 0, len(installed)) for i, item := range installed { - result = append(result, proto.EnvironmentMCP{PackageRoot: item.PackageRoot, Server: item.Server, BearerToken: tokens[i]}) + result = append(result, agent.EnvironmentMCP{PackageRoot: item.PackageRoot, Server: item.Server, BearerToken: tokens[i]}) } return result, nil } diff --git a/apps/daemon/internal/localworkspace/mcp_test.go b/apps/daemon/internal/localworkspace/mcp_test.go index d0e52e3cc..c461be413 100644 --- a/apps/daemon/internal/localworkspace/mcp_test.go +++ b/apps/daemon/internal/localworkspace/mcp_test.go @@ -5,8 +5,8 @@ import ( "slices" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" ) @@ -29,7 +29,7 @@ func TestEnvironmentMCPCredentialsNeverFallBackToNativeEnv(t *testing.T) { } func TestMCPStdioLauncherContainsOnlyInstalledIdentity(t *testing.T) { - server := proto.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{ + server := agent.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{ Name: "package_tool", Type: "stdio", Command: "untrusted-command", Args: []string{"private-argument"}, }} command, args := MCPStdioCommand(server) diff --git a/apps/daemon/internal/localworkspace/native_binding.go b/apps/daemon/internal/localworkspace/native_binding.go index 4805dba7b..2a76761c0 100644 --- a/apps/daemon/internal/localworkspace/native_binding.go +++ b/apps/daemon/internal/localworkspace/native_binding.go @@ -25,7 +25,7 @@ func newNativeBinding(environment, session, workspace, capabilities string) (*Bi if err != nil || !info.IsDir() { return nil, errors.New("local workspace root must be an existing directory") } - return &Binding{environment: environment, stateKey: "agents-api-" + session, workspace: workspace, capabilityRoot: capabilities, writer: &fileWriter{}}, nil + return &Binding{environment: environment, session: session, workspace: workspace, capabilityRoot: capabilities, writer: &fileWriter{}}, nil } // ReadToolEnvironment reads explicit initialization values for this installation. diff --git a/apps/daemon/internal/localworkspace/network_policy_test.go b/apps/daemon/internal/localworkspace/network_policy_test.go index 96a7a9d10..6a170292c 100644 --- a/apps/daemon/internal/localworkspace/network_policy_test.go +++ b/apps/daemon/internal/localworkspace/network_policy_test.go @@ -1,56 +1,6 @@ package localworkspace -import ( - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" - "testing" -) - -func TestRuntimeNetworkPolicyMustMatchExecutionButNotReadOnly(t *testing.T) { - for _, deployed := range []string{"", "enabled", "disabled"} { - for _, requested := range []string{"", "enabled", "disabled", "restricted"} { - b, req := testBinding(t) - b.networkAccess = deployed - req.LocalEnvironment.NetworkAccess = requested - _, err := b.Configure(req) - if (err == nil) != (deployed != "" && deployed == requested) { - t.Fatalf("execution policy %q/%q: %v", deployed, requested, err) - } - req.WorkspaceReadOnly = true - req.LocalEnvironment = &proto.LocalEnvironment{ID: b.environment} - if _, err := b.Configure(req); err != nil { - t.Fatal("read-only operation requires unrelated execution policy", err) - } - } - } -} - -func TestRestrictedPolicyBindingCannotBeChangedByARequest(t *testing.T) { - b, req := testBinding(t) - b.networkAccess, b.allowedDomains = "restricted", []string{"example.com", "api.example.com"} - for _, domains := range [][]string{{"api.example.com", "EXAMPLE.com", "example.com"}, {"example.com"}, {"other.example.com"}, nil} { - req.LocalEnvironment.NetworkAccess = "restricted" - req.LocalEnvironment.AllowedDomains = domains - _, err := b.Configure(req) - if (err == nil) != (len(domains) == 3) { - t.Fatalf("binding changed by domains %v: %v", domains, err) - } - } - copy := b.NetworkPolicy() - copy.AllowedDomains[0] = "other.example.com" - if !b.NetworkPolicy().Equal(agentnetwork.Policy{Access: "restricted", AllowedDomains: []string{"example.com", "api.example.com"}}) { - t.Fatal("caller mutated frozen policy") - } - req.WorkspaceReadOnly = true - req.LocalEnvironment = &proto.LocalEnvironment{ID: b.environment} - if _, err := b.Configure(req); err != nil { - t.Fatal("read requires execution network", err) - } - req.LocalEnvironment.AllowedDomains = []string{"other.example.com"} - if _, err := b.Configure(req); err == nil { - t.Fatal("read accepted a conflicting supplied policy") - } -} +import "testing" func TestRuntimeNetworkPolicyRejectsMalformedDeploymentInput(t *testing.T) { for _, tc := range []struct { diff --git a/apps/daemon/internal/localworkspace/runtime_initialization_test.go b/apps/daemon/internal/localworkspace/runtime_initialization_test.go index dea90b121..9cdcebe22 100644 --- a/apps/daemon/internal/localworkspace/runtime_initialization_test.go +++ b/apps/daemon/internal/localworkspace/runtime_initialization_test.go @@ -342,11 +342,11 @@ func TestRuntimeInitialFileAtomicReplacement(t *testing.T) { } func TestRuntimePreparationRejectsFilesAfterFinalization(t *testing.T) { b, req := testBinding(t) - configured, err := b.Configure(req) + err := b.Configure(req.PromptRequestPayload) if err != nil { t.Fatal(err) } - if _, err = b.Prepare(t.Context(), configured); err != nil { + if _, err = b.Prepare(t.Context(), req); err != nil { t.Fatal(err) } digest := sha256.Sum256(nil) diff --git a/apps/daemon/internal/localworkspace/snapshot_marker_test.go b/apps/daemon/internal/localworkspace/snapshot_marker_test.go index 769c2ffed..4194d30af 100644 --- a/apps/daemon/internal/localworkspace/snapshot_marker_test.go +++ b/apps/daemon/internal/localworkspace/snapshot_marker_test.go @@ -8,24 +8,24 @@ import ( "runtime" "testing" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentskill" "github.com/google/uuid" ) -func markerBinding(t *testing.T) (*Binding, proto.PromptRequestPayload) { +func markerBinding(t *testing.T) (*Binding, agent.PrepareRequest) { t.Helper() b, request := testBinding(t) // This host's inherited ACL gives TempDir group permissions unless cleared. if err := os.Chmod(os.Getenv("OAC_RUNTIME_HOME"), 0700); err != nil { t.Fatal(err) } - configured, err := b.Configure(request) - if err != nil { + if err := b.Configure(request.PromptRequestPayload); err != nil { t.Fatal(err) } - return b, configured + return b, request } func markerPath(b *Binding) string { identity := b.capabilityIdentity() @@ -78,7 +78,6 @@ func TestCompletedSnapshotLossNeverRecapturesSources(t *testing.T) { source := t.TempDir() if populated { writeSourceSkill(t, source, "first") - request.LocalEnvironment.Capabilities = true request.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{source}} } if _, err := b.Prepare(t.Context(), request); err != nil { @@ -134,7 +133,6 @@ func TestSnapshotMarkerBackfillsOnlyVerifiedManifest(t *testing.T) { if err := os.RemoveAll(source); err != nil { t.Fatal(err) } - request.LocalEnvironment.Capabilities = true request.LocalEnvironment.CapabilitySources = &input if _, err := b.Prepare(t.Context(), request); err != nil { t.Fatal("valid manifest was not recovered without sources", err) diff --git a/apps/daemon/internal/wireconformance/wire_test.go b/apps/daemon/internal/wireconformance/wire_test.go index cfd2ff38e..bffdfa63e 100644 --- a/apps/daemon/internal/wireconformance/wire_test.go +++ b/apps/daemon/internal/wireconformance/wire_test.go @@ -157,7 +157,7 @@ func connectRuntime(t *testing.T, peer *corePeer, setupErr error) *runtimeSide { rt := &runtimeSide{conn: conn, executor: &controlledExecutor{turn: make(chan *controlledTurn, 1)}, stopped: make(chan struct{})} kinds := agent.NewRegistry() kinds.RegisterKind(proto.SupportedAgentKind{Kind: prototest.HarnessKind, Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{EnvironmentNone: proto.CapabilitySupported})}, prototest.ModelConfiguration()) - kinds.RegisterExecutor(prototest.HarnessKind, func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) { + kinds.RegisterExecutor(prototest.HarnessKind, func(context.Context, agent.PrepareRequest) (agent.Executor, error) { return rt.executor, setupErr }) router, err := dispatch.New(dispatch.Config{Registry: kinds, Sender: conn}) diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index f0fd3d247..c18638149 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -37,22 +37,19 @@ type harness struct { history map[string]string } -func (h *harness) prepare(_ context.Context, req proto.PromptRequestPayload) (agent.Executor, error) { - if req.RunID != "" || len(req.Input) != 0 { - return nil, errors.New("preparation submitted fixture input") - } +func (h *harness) prepare(_ context.Context, req agent.PrepareRequest) (agent.Executor, error) { if !req.DisableExecutionEnvironment || !req.DisableSubagents || len(req.FunctionTools) > 0 || req.MCPHTTPServers != nil { return nil, errors.New("unsupported fixture operation") } h.mu.Lock() defer h.mu.Unlock() - previous := h.history[req.AgentStateKey] + previous := h.history[req.StateKey] if req.AgentSessionID != previous || (req.RequireExistingNativeSession && previous == "") { return nil, errors.New("native history mismatch") } if previous == "" { - previous = "fixture-" + req.AgentStateKey - h.history[req.AgentStateKey] = previous + previous = "fixture-" + req.StateKey + h.history[req.StateKey] = previous } return &executor{native: previous}, nil } diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 696e3a614..c626ed289 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -59,6 +59,8 @@ Implement the mandatory text lifecycle and handle every extension explicitly. Qu [`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) is the interface entry point. The required lifecycle is `ExecutorFactory`, `Executor`, `Turn` and `TurnSettlement`. `Turn` is one interface: `Cancel`, `CancellationOutcome`, `SteerWithReceipt`, `SubmitFunctionResult` and `AwaitSettlement`. Required methods perform their native obligations; returning Unsupported is not an implementation of cancellation, receipts, settlement or cleanup. An operation the adapter does not support returns Unsupported, and the capability declaration, not the method, decides whether the Runtime calls it. All use the neutral protocol types. +Both factories, `ExecutorFactory` and a view's `ViewExecutorFactory`, take one `agent.PrepareRequest`: the Session's configuration as `execution_prepare` carries it, the model configuration that the Registry prepared once from the kind's declaration (`Prepared`), the Session's native state key (`StateKey`), and the Environment's workspace and installed Capabilities (`WorkspaceRoot`, `CapabilityRoot`, `Skills`, `MCP`), which its owner fills. The adapter takes its model, provider and native parameters only from `Prepared` and never parses `model` or `model_provider` itself. A Turn's Run ID and input arrive in `Executor.StartTurn`. + For example, the Codex adapter keeps its app-server and thread, the Claude adapter one streaming Query, and the MiniMax adapter its ACP connection and native session. All expose the same Executor and Turn contract. Native callbacks and resources stay inside the adapter; the Runtime owns admission, idle expiry and replacement. Cancellation targets the exact Turn through `Turn.Cancel`, and the adapter supplies native completion evidence to the Runtime. | Interface or contract | Required handling | Obligation | @@ -81,7 +83,7 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl The reason is a fixed safe string, never submitted content, a credential or raw native diagnostics. Unsupported guarantees no native side effect and is not a successful empty operation. Installation unavailability, unknown call IDs, native failures and uncertain outcomes keep their own errors and ownership. A nil `Turn` still means that no input was submitted and the output stays with the caller; never use it as an Unsupported marker. -The wire request carries no working directory. The Runtime checks `local_environment.workspace_directory` against its binding and gives the Harness its bound workspace directory in `LocalEnvironment.WorkspaceRoot`; run the native Harness there. +The wire request carries no working directory. The Runtime checks `local_environment.workspace_directory` against its binding and gives the Harness its bound workspace directory in `PrepareRequest.WorkspaceRoot`; run the native Harness there. Workspace reads, writes, output export and read-only preparation belong to the Session's [Environment owner](../../docs/runtime-protocol.md#session-assignments), not the adapter. An adapter implements none of them. Its declaration's `LocalEnvironment` and `EnvironmentNone` state what its Executors run, and `agent.Registry.Register` composes them once with what the Runtime's owner serves (`agent.EnvironmentSupport`), keeping each only where the owner serves it. The composed `LocalEnvironment` also admits the owner's workspace reads, read-only preparation and output export. One declaration holds for every Executor of the install, including its [view](#run-in-an-agent-host-view). @@ -153,8 +155,8 @@ Registration is static and requires a build. Export one `agent.Declaration` from | Order | Method | Registers | | --- | --- | --- | | 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind, availability, version, `AgentKindCapabilities` and the model configuration, whose declaration it narrows to those capabilities; it panics on a widening. It resets the other registrations, so call it first. | -| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution. Its factory runs only for a request whose selection the narrowed declaration admits. | -| 3 | `RegisterView(kind, agent.View)` | Optional: the agent-host view declaration from `Runtime.View`. It panics with `ErrInvalidView` when `View.Validate` fails. Its Executor factory validates the model configuration like `RegisterExecutor` and enforces the [gateway rule](#endpoints-and-proxy). | +| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | The Executor and Turn lifecycle used for execution. Its factory runs only for a request whose selection the narrowed declaration admits and whose model configuration prepares, and receives it with `Prepared` set. | +| 3 | `RegisterView(kind, agent.View)` | Optional: the agent-host view declaration from `Runtime.View`. It panics with `ErrInvalidView` when `View.Validate` fails. Its Executor factory receives the request that the agent host's `RegisterExecutor` prepared and enforces the [gateway rule](#endpoints-and-proxy). | `Runtime.View` declares how the Harness runs in an agent-host Session view, described in [Run in an agent-host view](#run-in-an-agent-host-view). Every adapter sets it explicitly; `View: nil` means the agent host rejects the kind, and `Registry.ResolveView` returns an error wrapping `ErrUnsupportedOperation`. `TestPublicHarnessContractDeclarations` requires the field in each declaration. @@ -170,7 +172,7 @@ Core recognizes the [built-in Harness registrations](./harness-catalog.md). Add The `Declaration` of `Configuration()` in `internal/harnessconfig/` is the Harness's support: a `proto.Declaration` with its `AgentKindCapabilities`, its message, image, MCP and output-schema limits, and in `Conflicts` the feature pairs it supports alone but not together. It states the adapter's maximum support and is the only source: Core reads it through `builtin.Registry()`, and the adapter's Runtime descriptor starts from it. Discovery and the Environment owner only clear support, and Core rejects a heartbeat that widens it. Declare only real differences between Harnesses; a rule that holds for every Harness is a common check in `proto.ValidateSelection`. -`proto.ValidateSelection` is the only check of a declaration. Core applies the static declaration when an Agent with a saved Harness is created or updated, at Session creation and at input and function-result admission, and the Runtime's narrowed declaration at device selection and before it claims a Turn. The Runtime applies it when it admits an `execution_prepare`, before any Executor factory runs. A rejection is 400 `unsupported_or_invalid_configuration` with the configuration path as `param`. Runtime facts, such as a missing binary, native history or filesystem readiness, stay adapter preparation failures. +`proto.ValidateSelection` is the only check of a declaration. Core applies the static declaration when an Agent with a saved Harness is created or updated, at Session creation and at input and function-result admission, and the Runtime's narrowed declaration at device selection and before it claims a Turn. The Runtime applies it when it admits an `execution_prepare`, and again with the Environment's installed MCP servers once the Environment owner has resolved them, before any Executor factory runs. A rejection is 400 `unsupported_or_invalid_configuration` with the configuration path as `param`. Runtime facts, such as a missing binary, native history or filesystem readiness, stay adapter preparation failures. Each Runtime declaration references the same `internal/harnessconfig/.Configuration()` and owns its native factories and probes. The catalog cannot declare a machine's availability, and there is no dynamic plugin loader. @@ -264,7 +266,7 @@ An agent host runs the Harness outside the sandbox, in a per-Session view. The v ### Capabilities -A view runs every request that the kind's declaration admits, so the adapter declares only what both its local Executor and its view run, and dispatch checks each request against that declaration. The agent host serves a local Environment and environment none, and every view runs the Environment's installed Skills and [stdio MCP](#stdio-mcp). The Environment owner fills `LocalEnvironment.Skills` and `CapabilityRoot` as sandbox paths, and the adapter hands them to its Harness as a local Executor does; only the Harness reads them, through the view, and the adapter opens none of them on the agent host. Whatever the kind declares, the agent host rejects with `ErrUnsupportedOperation` a request whose installed Capabilities no preparation resolved and one with a restricted network, because only the Provider's workload network boundary can contain a process's own sockets. It rejects a stdio binding that needs a credential with `ErrViewHandoff`. +A view runs every request that the kind's declaration admits, so the adapter declares only what both its local Executor and its view run, and dispatch checks each request against that declaration. The agent host serves a local Environment and environment none, and every view runs the Environment's installed Skills and [stdio MCP](#stdio-mcp). The Environment owner fills `PrepareRequest.Skills` and `CapabilityRoot` as sandbox paths, and the adapter hands them to its Harness as a local Executor does; only the Harness reads them, through the view, and the adapter opens none of them on the agent host. The agent host rejects a stdio binding that needs a credential with `ErrViewHandoff`. ### Environment none @@ -286,9 +288,9 @@ The agent host derives the process broker's table from the declaration: `/.oac/b ### Endpoints and proxy -Before it calls the factory, the agent host points the request's `model_provider` at the Session's [credential gateway](./model-execution.md#credential-gateway): `base_url` is `http://127.0.0.1:` with no path and `api_key` is `modelprovider.Placeholder`. It resolves the Session's MCP once, from the public declarations and the installed Environment MCP, into `ViewSession.MCP`, and removes both from the request. Only HTTP bindings go to the gateway: each points at its gateway URL and carries no bearer and no headers, and the gateway adds the declared credential and headers. A stdio binding runs under its [alias](#stdio-mcp). A view Executor takes MCP only from `ViewSession.MCP` and never resolves the request. The adapter renders the provider and the bindings as it does for a local Harness and never sees a real credential. +Before it calls the factory, the agent host points the request's model provider, `model_provider` and `Prepared.Provider`, at the Session's [credential gateway](./model-execution.md#credential-gateway): `base_url` is `http://127.0.0.1:` with no path and `api_key` is `modelprovider.Placeholder`. It resolves the Session's MCP once, from the public declarations and the installed Environment MCP, into `ViewSession.MCP`, and removes both from the request. Only HTTP bindings go to the gateway: each points at its gateway URL and carries no bearer and no headers, and the gateway adds the declared credential and headers. A stdio binding runs under its [alias](#stdio-mcp). A view Executor takes MCP only from `ViewSession.MCP` and never resolves the request. The adapter renders the provider and the bindings as it does for a local Harness and never sees a real credential. -The Registry checks each view request once, before the factory, and rejects it with `ErrViewHandoff` when its model provider is missing or is not the gateway with the placeholder, when it carries MCP outside `ViewSession.MCP`, when an HTTP binding is not a credential-free loopback endpoint, or when a stdio binding is not its alias. +The Registry checks each view request once, before the factory, and rejects it with `ErrViewHandoff` when its prepared model provider is not the gateway with the placeholder, when it carries MCP outside `ViewSession.MCP`, when an HTTP binding is not a credential-free loopback endpoint, or when a stdio binding is not its alias. With `ViewProxyEnv`, `ViewSession.Proxy` is the gateway's proxy URL. The adapter sets `HTTPS_PROXY` and `HTTP_PROXY` to it and `NO_PROXY` to `127.0.0.1,localhost`, each in upper and lower case. Declare `ViewProxyEnv` only after qualifying that every request the Harness makes locally honours these variables. A request that ignores them fails to connect, because the view has no route out. diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 6cf38129b..ad293aaef 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "添加 Harness" source: contracts/agents-api/harness-onboarding.md -source_hash: f5d10dc8f734dae33713d262079a0a0906fd881ca059b769390fc817086273c0 +source_hash: ea2a7a759e262188cab16966a558f866075544fb84724cd95ab4d5c78e96b6f3 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。 @@ -61,6 +61,8 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以 [`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) 是接口入口。必需的生命周期包括 `ExecutorFactory`、`Executor`、`Turn` 和 `TurnSettlement`。`Turn` 是一个接口:`Cancel`、`CancellationOutcome`、`SteerWithReceipt`、`SubmitFunctionResult` 和 `AwaitSettlement`。必需方法必须履行其原生义务;返回 Unsupported 并不构成对取消、回执、结算或清理的实现。适配器不支持的操作返回 Unsupported,由能力声明而不是方法决定 Runtime 是否调用它。所有接口都使用中立协议类型。 +两个工厂,`ExecutorFactory` 和视图的 `ViewExecutorFactory`,都接收一个 `agent.PrepareRequest`:`execution_prepare` 携带的 Session 配置、Registry 按 kind 的声明一次性准备好的模型配置(`Prepared`)、Session 的原生状态键(`StateKey`),以及由 Environment owner 填写的 Environment 工作区和已安装 Capabilities(`WorkspaceRoot`、`CapabilityRoot`、`Skills`、`MCP`)。适配器只从 `Prepared` 获取模型、提供商和原生参数,从不自行解析 `model` 或 `model_provider`。Turn 的 Run ID 和输入通过 `Executor.StartTurn` 传入。 + 例如,Codex 适配器保留其 app-server 和 thread,Claude 适配器保留一个流式 Query,MiniMax 适配器保留其 ACP 连接和原生 session。它们都公开相同的 Executor 和 Turn 契约。原生回调和资源保留在适配器内部;Runtime 负责准入、空闲过期和替换。取消通过 `Turn.Cancel` 精确定位到目标 Turn,适配器则向 Runtime 提供原生完成证据。 | 接口或契约 | 必需处理 | 义务 | @@ -83,7 +85,7 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl 原因必须是固定的安全字符串,绝不能是已提交内容、凭据或原始原生诊断信息。Unsupported 保证不会产生原生副作用,也不表示操作成功且为空。安装不可用、未知调用 ID、原生失败和不确定结果应保留各自的错误和所有权。nil `Turn` 仍表示没有提交任何输入,并且输出归调用方所有;绝不能将其用作 Unsupported 标记。 -线协议请求不携带工作目录。Runtime 将 `local_environment.workspace_directory` 与其绑定进行核对,并通过 `LocalEnvironment.WorkspaceRoot` 向 Harness 提供其绑定的工作区目录;必须在该目录中运行原生 Harness。 +线协议请求不携带工作目录。Runtime 将 `local_environment.workspace_directory` 与其绑定进行核对,并通过 `PrepareRequest.WorkspaceRoot` 向 Harness 提供其绑定的工作区目录;必须在该目录中运行原生 Harness。 工作区读取、写入、输出导出和只读 preparation 属于 Session 的 [Environment owner](../../../docs/zh/runtime-protocol.md#session-assignments),不属于 adapter。adapter 不实现其中任何操作。其声明中的 `LocalEnvironment` 和 `EnvironmentNone` 表示其 Executor 能运行的内容,`agent.Registry.Register` 将二者与 Runtime 的 owner 所提供的内容(`agent.EnvironmentSupport`)组合一次,仅在 owner 提供时保留。组合后的 `LocalEnvironment` 同时准入 owner 的工作区读取、只读 preparation 和输出导出。一份声明适用于该安装的每个 Executor,包括其[视图](#run-in-an-agent-host-view)。 @@ -155,8 +157,8 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 | 顺序 | 方法 | 注册内容 | | --- | --- | --- | | 1 | `RegisterKind(proto.SupportedAgentKind, harnessconfig.Configuration)` | Kind、可用性、版本、`AgentKindCapabilities` 和模型配置;它将模型配置的声明收窄到这些能力,遇到扩大时 panic。它会重置其他注册项,因此必须首先调用。 | -| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期。其工厂只为收窄后的声明所准入的请求运行。 | -| 3 | `RegisterView(kind, agent.View)` | 可选:来自 `Runtime.View` 的 agent-host 视图声明。`View.Validate` 失败时以 `ErrInvalidView` panic。其 Executor 工厂像 `RegisterExecutor` 一样验证模型配置,并执行[网关规则](#endpoints-and-proxy)。 | +| 2 | `RegisterExecutor(kind, agent.ExecutorFactory)` | 执行所用的 Executor 和 Turn 生命周期。其工厂只为收窄后的声明所准入、且模型配置能够准备的请求运行,并收到已设置 `Prepared` 的请求。 | +| 3 | `RegisterView(kind, agent.View)` | 可选:来自 `Runtime.View` 的 agent-host 视图声明。`View.Validate` 失败时以 `ErrInvalidView` panic。其 Executor 工厂接收 agent host 的 `RegisterExecutor` 已准备好的请求,并执行[网关规则](#endpoints-and-proxy)。 | `Runtime.View` 声明 Harness 如何在 agent-host Session 视图中运行,详见[在 agent-host 视图中运行](#run-in-an-agent-host-view)。每个适配器都显式设置它;`View: nil` 表示 agent host 拒绝该 kind,`Registry.ResolveView` 返回包装 `ErrUnsupportedOperation` 的错误。`TestPublicHarnessContractDeclarations` 要求每个声明都包含该字段。 @@ -172,7 +174,7 @@ Core 会识别[内置 Harness 注册项](harness-catalog.md)。向 `internal/har `internal/harnessconfig/` 中 `Configuration()` 的 `Declaration` 就是 Harness 的支持范围:一个 `proto.Declaration`,包含其 `AgentKindCapabilities`、消息、图像、MCP 和输出 schema 限制,以及 `Conflicts` 中它能单独支持但不能同时支持的功能对。它说明适配器的最大支持范围,并且是唯一来源:Core 通过 `builtin.Registry()` 读取它,适配器的 Runtime 描述符也从它开始。发现过程和 Environment owner 只能清除支持,Core 拒绝扩大该声明的心跳。只声明 Harness 之间的真实差异;对每个 Harness 都成立的规则属于 `proto.ValidateSelection` 中的通用检查。 -`proto.ValidateSelection` 是对声明的唯一检查。Core 在创建或更新已保存 Harness 的 Agent、创建 Session 以及准入输入和函数结果时应用静态声明,在设备选择和认领 Turn 之前应用 Runtime 收窄后的声明。Runtime 在准入 `execution_prepare` 时应用它,早于任何 Executor 工厂运行。拒绝返回 400 `unsupported_or_invalid_configuration`,并以配置路径作为 `param`。Runtime 事实(例如缺少二进制、原生历史或文件系统就绪状态)仍是适配器准备失败。 +`proto.ValidateSelection` 是对声明的唯一检查。Core 在创建或更新已保存 Harness 的 Agent、创建 Session 以及准入输入和函数结果时应用静态声明,在设备选择和认领 Turn 之前应用 Runtime 收窄后的声明。Runtime 在准入 `execution_prepare` 时应用它,并在 Environment owner 解析出 Environment 已安装的 MCP 服务器后连同它们再次应用,均早于任何 Executor 工厂运行。拒绝返回 400 `unsupported_or_invalid_configuration`,并以配置路径作为 `param`。Runtime 事实(例如缺少二进制、原生历史或文件系统就绪状态)仍是适配器准备失败。 每个 Runtime 声明都引用同一个 `internal/harnessconfig/.Configuration()`,并负责其原生工厂和探测。目录不能声明某台机器的可用性,也不存在动态插件加载器。 @@ -266,7 +268,7 @@ agent host 在沙箱之外、在每个 Session 一个的视图中运行 Harness ### 能力 {#capabilities} -视图运行该 kind 的声明所准入的每个请求,因此 adapter 只声明其本地 Executor 和视图都能运行的内容,dispatch 按该声明检查每个请求。agent host 提供本地 Environment 和 environment none,且每个视图都运行 Environment 已安装的 Skills 和 [stdio MCP](#stdio-mcp)。Environment owner 以沙箱路径填写 `LocalEnvironment.Skills` 和 `CapabilityRoot`,adapter 像本地 Executor 那样把它们交给 Harness;只有 Harness 通过视图读取它们,adapter 不在 agent host 上打开其中任何路径。无论 kind 如何声明,agent host 都以 `ErrUnsupportedOperation` 拒绝已安装的 Capabilities 未经任何准备解析的请求,以及带受限网络的请求,因为只有 Provider 的工作负载网络边界才能约束进程自己的 socket。它以 `ErrViewHandoff` 拒绝需要凭据的 stdio 绑定。 +视图运行该 kind 的声明所准入的每个请求,因此 adapter 只声明其本地 Executor 和视图都能运行的内容,dispatch 按该声明检查每个请求。agent host 提供本地 Environment 和 environment none,且每个视图都运行 Environment 已安装的 Skills 和 [stdio MCP](#stdio-mcp)。Environment owner 以沙箱路径填写 `PrepareRequest.Skills` 和 `CapabilityRoot`,adapter 像本地 Executor 那样把它们交给 Harness;只有 Harness 通过视图读取它们,adapter 不在 agent host 上打开其中任何路径。agent host 以 `ErrViewHandoff` 拒绝需要凭据的 stdio 绑定。 ### Environment none {#environment-none} @@ -288,9 +290,9 @@ agent host 根据声明推导进程 broker 的映射表:`/.oac/bin/` 在 ### 端点与代理 {#endpoints-and-proxy} -调用工厂之前,agent host 将请求的 `model_provider` 指向 Session 的[凭据网关](./model-execution.md#credential-gateway):`base_url` 是不带路径的 `http://127.0.0.1:`,`api_key` 是 `modelprovider.Placeholder`。它把公开声明和已安装的 Environment MCP 一次性解析为 Session 的 MCP,放入 `ViewSession.MCP`,并从请求中移除这两者。只有 HTTP 绑定进入网关:每个 HTTP 绑定指向其网关 URL,不携带 bearer,也不携带 header,网关添加声明的凭据和 header。stdio 绑定在其[别名](#stdio-mcp)下运行。视图 Executor 只从 `ViewSession.MCP` 获取 MCP,从不解析请求。适配器像对待本地 Harness 一样渲染提供商和绑定,从不接触真实凭据。 +调用工厂之前,agent host 将请求的模型提供商,即 `model_provider` 和 `Prepared.Provider`,指向 Session 的[凭据网关](./model-execution.md#credential-gateway):`base_url` 是不带路径的 `http://127.0.0.1:`,`api_key` 是 `modelprovider.Placeholder`。它把公开声明和已安装的 Environment MCP 一次性解析为 Session 的 MCP,放入 `ViewSession.MCP`,并从请求中移除这两者。只有 HTTP 绑定进入网关:每个 HTTP 绑定指向其网关 URL,不携带 bearer,也不携带 header,网关添加声明的凭据和 header。stdio 绑定在其[别名](#stdio-mcp)下运行。视图 Executor 只从 `ViewSession.MCP` 获取 MCP,从不解析请求。适配器像对待本地 Harness 一样渲染提供商和绑定,从不接触真实凭据。 -Registry 在调用工厂之前对每个视图请求检查一次,并在以下情况下以 `ErrViewHandoff` 拒绝:模型提供商缺失或不是带占位凭据的网关、请求在 `ViewSession.MCP` 之外携带 MCP、HTTP 绑定不是不含凭据的 loopback 端点,或 stdio 绑定不是其别名。 +Registry 在调用工厂之前对每个视图请求检查一次,并在以下情况下以 `ErrViewHandoff` 拒绝:准备好的模型提供商不是带占位凭据的网关、请求在 `ViewSession.MCP` 之外携带 MCP、HTTP 绑定不是不含凭据的 loopback 端点,或 stdio 绑定不是其别名。 使用 `ViewProxyEnv` 时,`ViewSession.Proxy` 是网关的代理 URL。适配器将 `HTTPS_PROXY` 和 `HTTP_PROXY` 设为该值,将 `NO_PROXY` 设为 `127.0.0.1,localhost`,每个变量都设置大写和小写两种形式。只有在确认 Harness 在本地发出的每个请求都遵循这些变量之后,才声明 `ViewProxyEnv`。忽略这些变量的请求会连接失败,因为视图没有出站路由。 diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 2582f27b8..397c0e1c3 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -57,7 +57,7 @@ The `execution_prepare` configuration carries the Session's model configuration | `execution_controls` | Always: the resolved text verbosity (default `medium`), an explicit programmatic-tool-calling disable and any `json_schema` output format. Native option names belong to the adapter | | `observe_subagent_identities`, `disable_subagents` | From the Agent's `multi_agent.enabled` | | `disable_execution_environment` | For an Environment of type `none` | -| `local_environment` | For `openai_hosted` and `self_hosted`, with the exact Environment binding. The request carries no working directory; the Runtime checks `workspace_directory` against its binding | +| `local_environment` | For `openai_hosted` and `self_hosted`, with the exact Environment binding. The request carries no working directory; the Runtime checks `workspace_directory` against its binding. It carries no network policy, because Core admits only an [enabled network](../contracts/agents-api/environments.md#restricted-network) | | `require_existing_native_session` | When a native Session must be recovered | An execution configuration requires exactly one of `local_environment` and `disable_execution_environment`; `execution_prepare` rejects neither or both with `unsupported_configuration`. @@ -130,7 +130,7 @@ An execution Turn runs in five steps: 4. Consume Run events until a native terminal outcome or loss of observation. An execution error is followed by `done`, which closes the stream; the preceding errors remain part of its outcome. 5. To abandon before start, send `execution_release`. After ownership passes to a Run, use `prompt_cancel`; releasing the old handle cannot cancel its successor. -A preparation reserves a per-Turn admission, not a new Executor. It carries an explicit Session identity and immutable configuration without model input or a Run ID. A fresh request returns a connection-local handle and the owning Executor ID; a reused healthy Executor returns `ready` without native preparation. Per-handle revisions order status observations: ignore older or repeated revisions and never apply a status to another handle. Typical transitions are `preparing → ready → starting → started`, or termination by `released`, `expired` or `failed`. A `rejected` control operation carries an `operation` and error code and does not replace the handle's current revision. Releasing an admission abandons only that admission; it does not close the Session's idle Executor or cancel a later Turn. +A preparation reserves a per-Turn admission, not a new Executor. It carries an explicit Session identity and immutable configuration without model input or a Run ID; the Runtime derives the Session's native state key from that identity. A fresh request returns a connection-local handle and the owning Executor ID; a reused healthy Executor returns `ready` without native preparation. Per-handle revisions order status observations: ignore older or repeated revisions and never apply a status to another handle. Typical transitions are `preparing → ready → starting → started`, or termination by `released`, `expired` or `failed`. A `rejected` control operation carries an `operation` and error code and does not replace the handle's current revision. Releasing an admission abandons only that admission; it does not close the Session's idle Executor or cancel a later Turn. Preparation and start run outside the receive loop and router lock. An admission expires five minutes after it is granted, and retries do not extend that deadline; expiry does not remove the Runtime's obligation to settle cleanup. The Runtime bounds active preparation and execution separately from idle retained resources and counts closing or uncertain resources until their cleanup succeeds. A definite `execution_prepare` rejection with `preparation_capacity` leaves the queued Turn unclaimed for the Worker to retry, including when cleanup holds the capacity; any other error or uncertain delivery authorizes no replay. The Runtime retains at most 64 admission records, and an old handle never consumes a replacement's admission. These records are connection-local, not durable input replay. diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 1c84a8ab4..d5b39a551 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: 6cbb3db01de6635b1e68f8639a6d8ce3b8fb022ad551a834d80c5a341b86437d +source_hash: 9ec9470b3e5cd72b1a81f83fc739a61afe2e2abd424aa646dfa366c067be1148 --- 此协议在 Runtime daemon 获取机器凭据后连接 Core 与 daemon,定义 daemon 连接上消息的含义和顺序。wire 类型、限制和验证器仅在 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 中定义一次;Core 的 [gateway](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/runtimegateway) 与参考 Runtime 的 [dispatcher](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch) 都使用它们,因此无需同步第二套 payload schema。签发凭据和打开连接的 HTTP 路由见[机器连接 API](../../contracts/agents-api/zh/machine-api.md)。 @@ -59,7 +59,7 @@ heartbeat 只能收窄 Harness 的静态声明。某个 kind 没有内置声明 | `execution_controls` | 始终设置:解析后的 text verbosity(默认 `medium`)、明确禁用 programmatic tool calling,以及任何 `json_schema` 输出格式。原生选项名称由 adapter 负责 | | `observe_subagent_identities`, `disable_subagents` | 根据 Agent 的 `multi_agent.enabled` 设置 | | `disable_execution_environment` | Environment 类型为 `none` 时设置 | -| `local_environment` | 为 `openai_hosted` 和 `self_hosted` 设置,包含精确的 Environment 绑定。请求不携带 working directory;Runtime 按自身绑定检查 `workspace_directory` | +| `local_environment` | 为 `openai_hosted` 和 `self_hosted` 设置,包含精确的 Environment 绑定。请求不携带 working directory;Runtime 按自身绑定检查 `workspace_directory`。请求不携带网络策略,因为 Core 只准入[启用的网络](../../contracts/agents-api/zh/environments.md#restricted-network) | | `require_existing_native_session` | 需要恢复原生 Session 时设置 | 执行配置必须且只能包含 `local_environment` 和 `disable_execution_environment` 之一;两者都缺失或同时存在时,`execution_prepare` 以 `unsupported_configuration` 拒绝。 @@ -132,7 +132,7 @@ Runtime 对无法路由的 Core frame 回复 `protocol_error`,回显请求 ID 4. 消费 Run 事件,直到原生终结结果或观测丢失。执行 error 后跟随 `done`,关闭流;之前的 error 仍属于结果的一部分。 5. start 前放弃时发送 `execution_release`。所有权转给 Run 后使用 `prompt_cancel`;释放旧 handle 不能取消后继 Turn。 -preparation 预约每个 Turn 的准入,而不是新 Executor。它携带明确 Session 身份和不可变配置,不包含模型输入或 Run ID。新请求返回连接本地 handle 与所属 Executor ID;复用健康 Executor 时直接返回 `ready`,无需原生 preparation。每个 handle 的 revision 决定 status 观测顺序:忽略旧的或重复的 revision,不将 status 应用于其他 handle。典型状态转移为 `preparing → ready → starting → started`,或由 `released`、`expired`、`failed` 终止。`rejected` 控制操作携带 `operation` 和 error code,不替代 handle 的当前 revision。释放 admission 仅放弃该 admission;不关闭 Session 的空闲 Executor,也不取消后续 Turn。 +preparation 预约每个 Turn 的准入,而不是新 Executor。它携带明确 Session 身份和不可变配置,不包含模型输入或 Run ID;Runtime 从该身份推导 Session 的原生状态键。新请求返回连接本地 handle 与所属 Executor ID;复用健康 Executor 时直接返回 `ready`,无需原生 preparation。每个 handle 的 revision 决定 status 观测顺序:忽略旧的或重复的 revision,不将 status 应用于其他 handle。典型状态转移为 `preparing → ready → starting → started`,或由 `released`、`expired`、`failed` 终止。`rejected` 控制操作携带 `operation` 和 error code,不替代 handle 的当前 revision。释放 admission 仅放弃该 admission;不关闭 Session 的空闲 Executor,也不取消后续 Turn。 preparation 和 start 在 receive loop 与 router lock 之外运行。admission 在授予五分钟后到期,重试不延长截止时间;到期不解除 Runtime 完成清理结算的义务。Runtime 分别限制活动 preparation、execution 和保留的空闲资源,关闭中或不确定资源持续计入限制,直到清理成功。明确的 `execution_prepare` 拒绝若为 `preparation_capacity`,会让排队 Turn 保持未领取,供 Worker 重试,包括清理占用容量的情况;其他错误或不确定交付都不授权重放。Runtime 最多保留 64 条 admission 记录,旧 handle 不会消耗替代项的 admission。这些记录仅属于连接,不是持久化输入重放。 diff --git a/internal/agentdaemon/proto/environment.go b/internal/agentdaemon/proto/environment.go index b013d9ae9..119bee8f8 100644 --- a/internal/agentdaemon/proto/environment.go +++ b/internal/agentdaemon/proto/environment.go @@ -1,42 +1,15 @@ package proto -import ( - "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentplugin" -) +import "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" // LocalEnvironment names a frozen workspace selection. Runtime must verify it // against the bound local root before resolving capabilities or native execution. type LocalEnvironment struct { - CapabilityRoot string `json:"-"` ID string `json:"id"` WorkspaceDirectory string `json:"workspace_directory"` CapabilitySources *agentcapabilities.Input `json:"capability_sources"` - // WorkspaceRoot is the bound local root the daemon supplies for execution; - // wire input cannot supply it. Read-only preparation leaves it empty. - WorkspaceRoot string `json:"-"` - // Capabilities is derived from the frozen selection for engine qualification; - // Runtime still ensures and loads the protected installation before execution. - Capabilities bool `json:"capabilities,omitempty"` - // Skills is resolved by the bound daemon; wire input cannot supply paths. - Skills []agentcapabilities.InstalledSkill `json:"-"` - // MCP is resolved from the same protected installation, never from wire input. - MCP []EnvironmentMCP `json:"-"` // ToolEnvironment consumes Core-completed confidential initialization. ToolEnvironment bool `json:"tool_environment,omitempty"` - // NetworkAccess must match the immutable Runtime policy for execution. - NetworkAccess string `json:"network_access,omitempty"` - AllowedDomains []string `json:"allowed_domains,omitempty"` -} - -// EnvironmentMCP is transient Runtime configuration. Do not log it: HTTP headers -// and the selected user bearer may be confidential. It is not agent.tools MCP. -type EnvironmentMCP struct { - InstallationRoot string - WorkspaceRoot string - PackageRoot string - Server agentplugin.MCPServer - BearerToken *string } func (r PromptRequestPayload) EnvironmentID() string { diff --git a/internal/agentdaemon/proto/mcp.go b/internal/agentdaemon/proto/mcp.go index 79af7876e..138bf8ffb 100644 --- a/internal/agentdaemon/proto/mcp.go +++ b/internal/agentdaemon/proto/mcp.go @@ -23,8 +23,8 @@ func (server MCPHTTPServer) ValidateConnectionOrigin(req PromptRequestPayload) e return errors.New("service-origin MCP requires a service execution host") } case "environment": - if req.DisableExecutionEnvironment || req.LocalEnvironment == nil || req.LocalEnvironment.NetworkAccess != "enabled" { - return errors.New("environment MCP requires an enabled workspace network") + if req.DisableExecutionEnvironment || req.LocalEnvironment == nil { + return errors.New("environment MCP requires a workspace") } default: return errors.New("MCP requires an explicit connection origin") diff --git a/internal/agentdaemon/proto/message_input_test.go b/internal/agentdaemon/proto/message_input_test.go index 215ebedb1..ff412d0dd 100644 --- a/internal/agentdaemon/proto/message_input_test.go +++ b/internal/agentdaemon/proto/message_input_test.go @@ -30,7 +30,7 @@ func TestMessageInputOrderAndTextOnlyRejection(t *testing.T) { if err != nil || actual != " before \n\n after " { t.Fatalf("text changed: %q %v", actual, err) } - for _, payload := range []any{PromptRequestPayload{Input: input}, PromptSteerPayload{Input: input}, ExecutionStartPayload{Input: input}} { + for _, payload := range []any{PromptSteerPayload{Input: input}, ExecutionStartPayload{Input: input}} { encoded, err := json.Marshal(payload) if err != nil { t.Fatal(err) diff --git a/internal/agentdaemon/proto/outbound.go b/internal/agentdaemon/proto/outbound.go index cdafb9e00..a8708eaff 100644 --- a/internal/agentdaemon/proto/outbound.go +++ b/internal/agentdaemon/proto/outbound.go @@ -22,13 +22,6 @@ type PromptRequestPayload struct { // dispatches to. AgentKind string `json:"agent_kind"` - // RunID and Input are empty in an execution_prepare configuration. - // Adapters set them from execution_start when they start a Turn. - RunID string `json:"run_id"` - - // Input preserves ordered user messages and content. - Input MessageInput `json:"input,omitempty"` - // Model, SystemPrompt, ModelProvider and HarnessConfig are the Session's // frozen model configuration. internal/harnessconfig validates them // against the selected Harness before any native effect. @@ -48,22 +41,14 @@ type PromptRequestPayload struct { // AgentSessionID is the upstream engine session id to resume. AgentSessionID string `json:"agent_session_id,omitempty"` - // AgentStateKey is the stable daemon-side state directory key. // WorkspaceReadOnly prepares temporary native state that cannot start a Run. WorkspaceReadOnly bool `json:"workspace_read_only,omitempty"` - AgentStateKey string `json:"agent_state_key,omitempty"` RequireExistingNativeSession bool `json:"require_existing_native_session,omitempty"` ObserveSubagentIdentities bool `json:"observe_subagent_identities,omitempty"` FunctionTools []FunctionTool `json:"function_tools,omitempty"` ToolSearch bool `json:"tool_search,omitempty"` DisableExecutionEnvironment bool `json:"disable_execution_environment,omitempty"` DisableSubagents bool `json:"disable_subagents,omitempty"` - - // Assignment is the assignment the request runs under: Core sends its - // frames under it, and the Runtime records the Envelope.Assignment it - // admitted the request under for the Executor factory. It is never - // encoded. - Assignment AssignmentRef `json:"-"` } // PromptCancelPayload optionally requests an application receipt; identity is on Envelope.ID. diff --git a/internal/agentdaemon/proto/prototest/wire.go b/internal/agentdaemon/proto/prototest/wire.go index b5d45b88d..451de33ac 100644 --- a/internal/agentdaemon/proto/prototest/wire.go +++ b/internal/agentdaemon/proto/prototest/wire.go @@ -27,7 +27,6 @@ const ( // Correlation values the scenarios use. const ( SessionID = "session" - StateKey = "agents-api-session" AssignmentID = "assignment" BindID = "bind" PreparationID = "prepare" @@ -116,7 +115,7 @@ func WireScenarios() []WireScenario { } prepare := send(Core, proto.TypeExecutionPrepare, PreparationID, proto.ExecutionPreparePayload{ SessionID: SessionID, - Configuration: WithModel(proto.PromptRequestPayload{AgentKind: HarnessKind, AgentStateKey: StateKey, DisableExecutionEnvironment: true}), + Configuration: WithModel(proto.PromptRequestPayload{AgentKind: HarnessKind, DisableExecutionEnvironment: true}), }) // Core binds the Session before its first Session-scoped frame. bind := []Step{ diff --git a/internal/agentdaemon/proto/runtime_prepare_test.go b/internal/agentdaemon/proto/runtime_prepare_test.go index 84519b778..d42a181b3 100644 --- a/internal/agentdaemon/proto/runtime_prepare_test.go +++ b/internal/agentdaemon/proto/runtime_prepare_test.go @@ -1,8 +1,6 @@ package proto import ( - "encoding/json" - "reflect" "strings" "testing" @@ -151,27 +149,6 @@ func TestCapabilitiesResultCannotMisrepresentUncertainty(t *testing.T) { } } -func TestLocalEnvironmentCarriesSelectionButNeverInstalledRoots(t *testing.T) { - input := &agentcapabilities.Input{Directories: []string{"/workspace/capabilities"}} - original := LocalEnvironment{ID: uuid.NewString(), WorkspaceDirectory: "/workspace", CapabilitySources: input, Capabilities: true, - Skills: []agentcapabilities.InstalledSkill{{RelativeRoot: "private-installed-root"}}, - MCP: []EnvironmentMCP{{PackageRoot: "private-mcp-root"}}} - raw, err := json.Marshal(original) - if err != nil { - t.Fatal(err) - } - if strings.Contains(string(raw), "private-") { - t.Fatal("installed roots leaked to wire") - } - var decoded LocalEnvironment - if err := json.Unmarshal(raw, &decoded); err != nil { - t.Fatal(err) - } - if decoded.ID != original.ID || decoded.WorkspaceDirectory != "/workspace" || !reflect.DeepEqual(decoded.CapabilitySources, input) || !decoded.Capabilities || len(decoded.Skills) != 0 || len(decoded.MCP) != 0 { - t.Fatal("selection round trip changed", decoded) - } -} - func TestRuntimePreparationInitialActions(t *testing.T) { base := RuntimePreparePayload{Step: "begin", EnvironmentID: uuid.NewString(), SessionID: uuid.NewString()} for _, initialization := range []RuntimeInitialization{ diff --git a/internal/agentdaemon/proto/selection.go b/internal/agentdaemon/proto/selection.go index 6f62e8589..9e995075c 100644 --- a/internal/agentdaemon/proto/selection.go +++ b/internal/agentdaemon/proto/selection.go @@ -215,10 +215,11 @@ func blankTextRune(r rune) bool { return r >= '\u2000' && r <= '\u200a' } -// Selection projects a request after its Environment owner configured or -// prepared it; installed MCP servers appear only after preparation. +// Selection projects an execution configuration as the Runtime admits it, +// without the Turn's messages and the Environment's installed MCP servers, +// which its owner resolves only when it prepares an Executor. func (r PromptRequestPayload) Selection() Selection { - s := Selection{MultiAgent: r.ObserveSubagentIdentities, Functions: len(r.FunctionTools) > 0, ToolSearch: r.ToolSearch, Messages: r.Input} + s := Selection{MultiAgent: r.ObserveSubagentIdentities, Functions: len(r.FunctionTools) > 0, ToolSearch: r.ToolSearch} for _, tool := range r.FunctionTools { s.DeferredFunctions = s.DeferredFunctions || tool.DeferLoading } @@ -226,9 +227,9 @@ func (r PromptRequestPayload) Selection() Selection { s.Environment = "none" } if local := r.LocalEnvironment; local != nil { - s.Environment, s.InstalledCapabilities = "local", local.Capabilities - for _, installed := range local.MCP { - s.MCP = append(s.MCP, SelectedMCP{Origin: "environment", Label: installed.Server.Name, Installed: true}) + s.Environment = "local" + if sources := local.CapabilitySources; sources != nil { + s.InstalledCapabilities = len(sources.Skills)+len(sources.Plugins)+len(sources.Directories) > 0 } } if c := r.ExecutionControls; c != nil { diff --git a/internal/agentdaemon/proto/workspace_read_preparation.go b/internal/agentdaemon/proto/workspace_read_preparation.go index f4992df5b..c7965443d 100644 --- a/internal/agentdaemon/proto/workspace_read_preparation.go +++ b/internal/agentdaemon/proto/workspace_read_preparation.go @@ -3,8 +3,7 @@ package proto // ValidWorkspaceReadPreparation excludes execution configuration. // The native adapter supplies temporary state; this request cannot resume or start. func ValidWorkspaceReadPreparation(r PromptRequestPayload) bool { - return r.WorkspaceReadOnly && r.LocalEnvironment != nil && r.AgentStateKey != "" && - r.RunID == "" && len(r.Input) == 0 && r.AgentSessionID == "" && + return r.WorkspaceReadOnly && r.LocalEnvironment != nil && r.AgentSessionID == "" && !r.RequireExistingNativeSession && !r.DisableExecutionEnvironment && r.Model == "" && r.SystemPrompt == "" && r.ModelProvider == nil && len(r.HarnessConfig) == 0 && r.ExecutionControls == nil && r.MCPHTTPServers == nil && diff --git a/internal/harnessconfig/preparation_test.go b/internal/harnessconfig/preparation_test.go index bea7305f2..e6fee8280 100644 --- a/internal/harnessconfig/preparation_test.go +++ b/internal/harnessconfig/preparation_test.go @@ -62,6 +62,14 @@ func TestPrepareModelConfiguration(t *testing.T) { if _, err := c.Prepare(proto.PromptRequestPayload{ModelProvider: provider()}); !errors.Is(err, ErrModel) { t.Fatal("model error was not shared") } + limited := Configuration{Providers: []Provider{{Protocol: "responses", RequiresTokenLimits: true}}} + for _, limits := range [][2]int32{{0, 0}, {64000, 0}} { + p := provider() + p.ContextWindow, p.MaxOutputTokens = limits[0], limits[1] + if _, err := limited.Prepare(proto.PromptRequestPayload{Model: "fixture", ModelProvider: p}); err == nil { + t.Fatalf("token limits %v accepted", limits) + } + } } func TestConfigurationDeclarationRejectsUnknownAndDuplicateProtocols(t *testing.T) { diff --git a/scripts/name-allowlist.json b/scripts/name-allowlist.json index 474fd61a1..190bbbf22 100644 --- a/scripts/name-allowlist.json +++ b/scripts/name-allowlist.json @@ -59,71 +59,6 @@ "regex": "parsar\\.agents-api\\.model-provider-api-key\\.v1", "reason": "This persisted model-provider key fingerprint purpose remains stable across upgrade and request replay." }, - { - "path": "apps/daemon/internal/agent/codex/preparation_router_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/dispatch/local_directory_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/dispatch/preparation_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/binding.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/native_binding.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "apps/daemon/internal/localworkspace/binding_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/internal/execution/directory_preparation.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/internal/execution/request.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/internal/execution/recovery_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/dispatch_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/environment_directory_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/environment_worker_helpers_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, - { - "path": "services/core/tests/integration/environment_worker_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "AgentStateKey is a persisted daemon/native-session resume identity carried on the existing machine wire contract; changing it would select another session." - }, { "path": "services/core/tests/integration/oac_runtime_names_migration_test.go", "regex": "parsar-core-runtime@sha256:|parsar_worker /home/runtime/\\.parsar", @@ -184,55 +119,10 @@ "regex": "example/parsar/", "reason": "The explicitly named Parsar application example retains product branding; it does not rename the Core runtime." }, - { - "path": "apps/daemon/internal/cli/connect_cleanup_test.go", - "regex": "\"agents-api-cleanup\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, { "path": "apps/daemon/internal/dispatch/executor.go", "regex": "\"agents-api-(?:session)?\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/dispatch/executor_handoff_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/dispatch/executor_test.go", - "regex": "\"agents-api-(?:session)?\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/agenthost/agenthost_linux_test.go", - "regex": "\"agents-api-\"", - "reason": "The test prepares executions through dispatch, whose admission requires the existing Core Session state-key prefix." - }, - { - "path": "apps/daemon/internal/agenthostqualify/qualify_linux_test.go", - "regex": "\"agents-api-\"", - "reason": "The test prepares executions through dispatch, whose admission requires the existing Core Session state-key prefix." - }, - { - "path": "services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go", - "regex": "\"agents-api-\"", - "reason": "Executor ownership validates the current Core and local-workspace AgentStateKey namespace; this change does not introduce an alternate identity or compatibility path." - }, - { - "path": "apps/daemon/internal/localworkspace/capabilities.go", - "regex": "\"agents-api-\"", - "reason": "AgentStateKey retains the existing daemon/native-session resume identity; capability snapshots bind to the Session UUID carried in that identity." - }, - { - "path": "apps/daemon/internal/localworkspace/capability_preparation_test.go", - "regex": "\"agents-api-\"", - "reason": "AgentStateKey retains the existing daemon/native-session resume identity; capability snapshots bind to the Session UUID carried in that identity." - }, - { - "path": "internal/agentdaemon/proto/prototest/wire.go", - "regex": "agents-api-session", - "reason": "The shared wire scenarios use the existing Core Session state-key prefix required by execution admission." + "reason": "Dispatch derives each Session's native state key in the existing agents-api- namespace; this change does not introduce an alternate identity or compatibility path." }, { "path": "apps/daemon/internal/agent/mcode/tool_environment_test.go", diff --git a/services/core/internal/execution/delivery.go b/services/core/internal/execution/delivery.go index 53b0b232b..cf17072c0 100644 --- a/services/core/internal/execution/delivery.go +++ b/services/core/internal/execution/delivery.go @@ -58,7 +58,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe defer unsubscribeChanges() status = sessions.TurnFailed result.AppliedThrough = first - subscription, err := peer.SubscribeDurable(runID, request.Assignment) + subscription, err := peer.SubscribeDurable(runID, prepared.assignment) if err != nil { result.ErrorCode = "device_disconnected" return @@ -67,7 +67,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe defer peer.Unsubscribe(runID) defer func() { if status == sessions.TurnFailed { - abort(peer, request.Assignment, runID) + abort(peer, prepared.assignment, runID) } }() journal := &journal{writer: d.sessionExecution, tenant: tenantID, session: sessionID, turn: runID, next: 1, @@ -102,7 +102,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe var pending *pendingInput var cancelSent time.Time var cancelReply <-chan cancellationResult - functions := &functionExchange{assignment: request.Assignment, kind: request.AgentKind, turns: d.SessionsReader, sessions: d.sessionExecution, tenant: tenantID, session: sessionID, turn: runID, tools: request.FunctionTools} + functions := &functionExchange{assignment: prepared.assignment, kind: request.AgentKind, turns: d.SessionsReader, sessions: d.sessionExecution, tenant: tenantID, session: sessionID, turn: runID, tools: request.FunctionTools} done := false cancelCtx, stopCancellation := context.WithCancel(ctx) defer stopCancellation() @@ -134,7 +134,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe result.ErrorCode = "device_disconnected" return } - replacement, prepareErr := d.prepareTurnExecutor(ctx, peer, tenantID, sessionID, runID, request, sessions.TurnInProgress) + replacement, prepareErr := d.prepareTurnExecutor(ctx, peer, prepared.assignment, tenantID, sessionID, runID, request, sessions.TurnInProgress) if prepareErr != nil { result.ErrorCode = "executor_recovery_failed" return @@ -285,7 +285,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe return } if !turn.CancelRequestedAt.IsZero() { - cancelReply = requestCancellation(cancelCtx, peer, request.Assignment, runID) + cancelReply = requestCancellation(cancelCtx, peer, prepared.assignment, runID) cancelSent = time.Now() continue } @@ -332,7 +332,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe result.ErrorCode = "message_input_unsupported" return } - if send(ctx, peer, request.Assignment, proto.TypePromptSteer, runID, proto.PromptSteerPayload{InputID: strconv.FormatInt(pending.sequence, 10), Input: pending.input}) != nil { + if send(ctx, peer, prepared.assignment, proto.TypePromptSteer, runID, proto.PromptSteerPayload{InputID: strconv.FormatInt(pending.sequence, 10), Input: pending.input}) != nil { result.ErrorCode = "input_outcome_unknown" return } diff --git a/services/core/internal/execution/directory_preparation.go b/services/core/internal/execution/directory_preparation.go index 52b8fc59c..548c8313c 100644 --- a/services/core/internal/execution/directory_preparation.go +++ b/services/core/internal/execution/directory_preparation.go @@ -25,7 +25,7 @@ func (d *Dispatcher) readPreparedDirectory(ctx context.Context, peer *runtimegat } func (d *Dispatcher) withPreparedWorkspace(owner context.Context, peer *runtimegateway.Session, session sessions.Session, environment sessions.Environment, bound sessions.ExecutionDevice, consume func(context.Context, string) error) error { - req := proto.PromptRequestPayload{Assignment: bound.Assignment, AgentKind: session.Engine, AgentStateKey: "agents-api-" + session.ID, WorkspaceReadOnly: true} + req := proto.PromptRequestPayload{AgentKind: session.Engine, WorkspaceReadOnly: true} if err := d.configurePreparedEnvironment(session, environment, bound, &req); err != nil { return ErrExecutionUnavailable } diff --git a/services/core/internal/execution/dispatcher.go b/services/core/internal/execution/dispatcher.go index c9b28e3fa..8dfda86ff 100644 --- a/services/core/internal/execution/dispatcher.go +++ b/services/core/internal/execution/dispatcher.go @@ -107,12 +107,11 @@ func (d *Dispatcher) Run(ctx context.Context, tenantID, sessionID, turnID string if err != nil { return sessions.Turn{}, err } - req.Assignment = bound.Device.Assignment req.DisableExecutionEnvironment = true - if err := peer.Bind(ctx, req.Assignment, bound.Device.EnvironmentID); err != nil { + if err := peer.Bind(ctx, bound.Device.Assignment, bound.Device.EnvironmentID); err != nil { return sessions.Turn{}, err } - prepared, err := d.prepareTurnExecutor(ctx, peer, tenantID, sessionID, turnID, req, sessions.TurnQueued) + prepared, err := d.prepareTurnExecutor(ctx, peer, bound.Device.Assignment, tenantID, sessionID, turnID, req, sessions.TurnQueued) if err != nil { return sessions.Turn{}, err } diff --git a/services/core/internal/execution/environment_capabilities_test.go b/services/core/internal/execution/environment_capabilities_test.go index 3529872e5..0e947b4a8 100644 --- a/services/core/internal/execution/environment_capabilities_test.go +++ b/services/core/internal/execution/environment_capabilities_test.go @@ -18,7 +18,7 @@ func TestSelfHostedCapabilitySourcesAreFrozenAndStrict(t *testing.T) { t.Fatal(err) } local := request.LocalEnvironment - if local.WorkspaceDirectory != "/home/user/project" || !local.Capabilities || local.ToolEnvironment || len(local.Skills) != 0 || + if local.WorkspaceDirectory != "/home/user/project" || local.ToolEnvironment || local.CapabilitySources == nil || !slices.Equal(local.CapabilitySources.Directories, []string{"/home/user/skills", "/opt/plugins"}) { t.Fatal("frozen source selections lost", local) } diff --git a/services/core/internal/execution/environment_placement.go b/services/core/internal/execution/environment_placement.go index fe57e0ede..7046d726f 100644 --- a/services/core/internal/execution/environment_placement.go +++ b/services/core/internal/execution/environment_placement.go @@ -95,14 +95,7 @@ func (d *Dispatcher) configurePreparedEnvironment(session sessions.Session, envi } sources.Skills = append(sources.Skills, (environmentconfig.Skill{Metadata: metadata}).InstallationMetadata()) } - req.LocalEnvironment = &proto.LocalEnvironment{ - ID: environment.ID, WorkspaceDirectory: placement.WorkspaceDirectory, - CapabilitySources: sources, - Capabilities: len(sources.Skills)+len(sources.Plugins)+len(sources.Directories) > 0, - ToolEnvironment: placement.ToolEnvironment, - } - req.LocalEnvironment.NetworkAccess = placement.NetworkAccess - req.LocalEnvironment.AllowedDomains = append([]string(nil), placement.AllowedDomains...) + req.LocalEnvironment = &proto.LocalEnvironment{ID: environment.ID, WorkspaceDirectory: placement.WorkspaceDirectory, CapabilitySources: sources, ToolEnvironment: placement.ToolEnvironment} return nil } diff --git a/services/core/internal/execution/environment_placement_test.go b/services/core/internal/execution/environment_placement_test.go index 4d980e058..01fa05091 100644 --- a/services/core/internal/execution/environment_placement_test.go +++ b/services/core/internal/execution/environment_placement_test.go @@ -3,7 +3,6 @@ package execution import ( "bytes" "encoding/json" - "slices" "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -16,7 +15,7 @@ func TestSkillReferenceIdentityStopsAtCoreBoundary(t *testing.T) { Configuration: []byte(`{"type":"openai_hosted","initialization":true,"skills":[{"type":"skill_reference","skill_id":"skill-private","version":"1","name":"proof","description":"A proof."}]}`)} var request proto.PromptRequestPayload err := (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &request) - if err != nil || request.LocalEnvironment == nil || !request.LocalEnvironment.Capabilities || len(request.LocalEnvironment.Skills) != 0 { + if err != nil || request.LocalEnvironment == nil { t.Fatal("resolved Skill did not use the common installation descriptor", err) } raw, err := json.Marshal(request.LocalEnvironment) @@ -60,23 +59,6 @@ func TestLocalEnvironmentRequiresQualifiedProfileAndExactAuthority(t *testing.T) } } -func TestNetworkPolicySurvivesPreparedBinding(t *testing.T) { - session := sessions.Session{ID: "session", TenantID: "tenant"} - for _, network := range []string{`{"access":"disabled"}`, `{"access":"restricted","allowed_domains":["Example.com","api.example.com"]}`} { - environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, - Configuration: []byte(`{"type":"openai_hosted","network":` + network + `}`)} - placement, err := parseEnvironmentPlacement(environment.Configuration) - if err != nil { - t.Fatal(err) - } - var req proto.PromptRequestPayload - err = (&Dispatcher{}).configurePreparedEnvironment(session, environment, sessions.ExecutionDevice{EnvironmentID: environment.ID}, &req) - if err != nil || req.LocalEnvironment == nil || req.LocalEnvironment.NetworkAccess != placement.NetworkAccess || !slices.Equal(req.LocalEnvironment.AllowedDomains, placement.AllowedDomains) { - t.Fatal("prepared binding lost policy", req.LocalEnvironment, err) - } - } -} - func TestToolEnvironmentRemainsInExecutionBinding(t *testing.T) { session := sessions.Session{ID: "session", TenantID: "tenant"} environment := sessions.Environment{ID: "environment", SessionID: session.ID, TenantID: session.TenantID, Configuration: []byte(`{"type":"openai_hosted","initialization":true,"packages":{"npm":["is-number"]}}`)} diff --git a/services/core/internal/execution/executor_preparation.go b/services/core/internal/execution/executor_preparation.go index 06fa77c9d..d4cb5d168 100644 --- a/services/core/internal/execution/executor_preparation.go +++ b/services/core/internal/execution/executor_preparation.go @@ -13,12 +13,12 @@ import ( // prepareTurnExecutor reserves one admission on the Runtime-owned Executor. // Core does not cache native ownership. A replacement requires the Runtime to // confirm cleanup and recover the exact Session history before returning ready. -func (d *Dispatcher) prepareTurnExecutor(ctx context.Context, peer *runtimegateway.Session, tenant, session, turn string, request proto.PromptRequestPayload, expectedStatus string) (*preparedStart, error) { - prepared, err := newPreparedStart(peer, request.Assignment) +func (d *Dispatcher) prepareTurnExecutor(ctx context.Context, peer *runtimegateway.Session, assignment proto.AssignmentRef, tenant, session, turn string, request proto.PromptRequestPayload, expectedStatus string) (*preparedStart, error) { + prepared, err := newPreparedStart(peer, assignment) if err != nil { return nil, err } - if err = send(ctx, peer, request.Assignment, proto.TypeExecutionPrepare, prepared.requestID, proto.ExecutionPreparePayload{SessionID: session, Configuration: request}); err == nil { + if err = send(ctx, peer, assignment, proto.TypeExecutionPrepare, prepared.requestID, proto.ExecutionPreparePayload{SessionID: session, Configuration: request}); err == nil { err = d.awaitTurnExecutor(ctx, tenant, session, turn, expectedStatus, prepared) } if err != nil { diff --git a/services/core/internal/execution/prepared_dispatch.go b/services/core/internal/execution/prepared_dispatch.go index 95e214c95..79943b272 100644 --- a/services/core/internal/execution/prepared_dispatch.go +++ b/services/core/internal/execution/prepared_dispatch.go @@ -70,19 +70,18 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t if err != nil { return run, err } - req.Assignment = bound.Device.Assignment if err := d.configurePreparedEnvironment(session, environment, bound.Device, &req); err != nil { return run, err } - if err := peer.Bind(owner, req.Assignment, bound.Device.EnvironmentID); err != nil { + if err := peer.Bind(owner, bound.Device.Assignment, bound.Device.EnvironmentID); err != nil { return run, err } - prepared, err := newPreparedStart(peer, req.Assignment) + prepared, err := newPreparedStart(peer, bound.Device.Assignment) if err != nil { return run, err } defer prepared.close() - if err = send(owner, peer, req.Assignment, proto.TypeExecutionPrepare, prepared.requestID, proto.ExecutionPreparePayload{SessionID: sessionID, Configuration: req}); err != nil { + if err = send(owner, peer, prepared.assignment, proto.TypeExecutionPrepare, prepared.requestID, proto.ExecutionPreparePayload{SessionID: sessionID, Configuration: req}); err != nil { return run, err } run.Reservation, err = d.awaitPreparation(owner, tenantID, sessionID, run.Reservation, prepared) diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index 17ae2ff58..443af39c4 100644 --- a/services/core/internal/execution/recovery_test.go +++ b/services/core/internal/execution/recovery_test.go @@ -20,7 +20,7 @@ func TestExistingSessionRecoveryRequiresVerifiedCapability(t *testing.T) { } continue } - if err != nil || req.RequireExistingNativeSession != wantRecovery || req.AgentSessionID != nativeID || req.AgentStateKey != "agents-api-session" { + if err != nil || req.RequireExistingNativeSession != wantRecovery || req.AgentSessionID != nativeID { t.Fatalf("engine=%s started=%v id=%s capability=%v request=%+v err=%v", engine, started, nativeID, capable, req, err) } } diff --git a/services/core/internal/execution/request.go b/services/core/internal/execution/request.go index 405c8a3c9..5387705b8 100644 --- a/services/core/internal/execution/request.go +++ b/services/core/internal/execution/request.go @@ -40,8 +40,7 @@ func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Sess } request := proto.PromptRequestPayload{AgentKind: session.Engine, FunctionTools: tools.Functions, ToolSearch: tools.Search, Model: snapshot.Agent.Model, SystemPrompt: instructions, ModelProvider: provider, HarnessConfig: harnessConfig, - ExecutionControls: controls, AgentStateKey: "agents-api-" + session.ID, - AgentSessionID: bound.NativeSessionID, RequireExistingNativeSession: recoverNativeSession, + ExecutionControls: controls, AgentSessionID: bound.NativeSessionID, RequireExistingNativeSession: recoverNativeSession, ObserveSubagentIdentities: snapshot.Agent.MultiAgent.Enabled, MaxConcurrentSubagents: snapshot.Agent.MultiAgent.MaxConcurrentSubagents, DisableSubagents: !snapshot.Agent.MultiAgent.Enabled} diff --git a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go index 7709202d2..195d3f0c1 100644 --- a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go +++ b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go @@ -114,7 +114,7 @@ func TestLiveMCPBearerGatewayColdContinuation(t *testing.T) { turn := &mcpBearerTurn{} turns = append(turns, turn) runID := uuid.NewString() - request := proto.PromptRequestPayload{AgentKind: "codex", AgentStateKey: "agents-api-" + assignment.SessionID, AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, MCPHTTPServers: &servers, Model: "MiniMax-M3", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://api.minimax.cn/v1", APIKey: provider}, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} + request := proto.PromptRequestPayload{AgentKind: "codex", AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, MCPHTTPServers: &servers, Model: "MiniMax-M3", ModelProvider: &modelprovider.Provider{Protocol: modelprovider.Responses, BaseURL: "https://api.minimax.cn/v1", APIKey: provider}, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}} sub, err := peer.SubscribeDurable(runID, assignment) if err != nil { t.Fatal("cannot subscribe before real daemon dispatch") diff --git a/services/core/tests/integration/dispatch_test.go b/services/core/tests/integration/dispatch_test.go index 99860d58f..af575f019 100644 --- a/services/core/tests/integration/dispatch_test.go +++ b/services/core/tests/integration/dispatch_test.go @@ -237,9 +237,9 @@ func TestExecutionDispatchSteeringAndNativeContinuity(t *testing.T) { first := h.message("first", "Initial input") result := h.run(ctx, first.TurnID) request := h.read(testExecutionRequest) - var prompt proto.PromptRequestPayload + var prompt testExecution _ = request.DecodePayload(&prompt) - if inputTextForTest(t, prompt.Input) != "Initial input" || prompt.AgentStateKey != "agents-api-"+h.session.ID || prompt.Model != "test-model" || prompt.SystemPrompt != "Keep this instruction." { + if inputTextForTest(t, prompt.Input) != "Initial input" || prompt.Model != "test-model" || prompt.SystemPrompt != "Keep this instruction." { t.Fatalf("wrong resolved request: %+v", prompt) } if _, err := h.bound().Run(ctx, uuid.NewString(), h.session.ID, first.TurnID); !errors.Is(err, sessions.ErrNotFound) { @@ -284,7 +284,7 @@ func TestExecutionDispatchSteeringAndNativeContinuity(t *testing.T) { result = h.run(ctx, next.TurnID) request = h.read(testExecutionRequest) _ = request.DecodePayload(&prompt) - if prompt.AgentSessionID != "native-thread-1" || prompt.AgentStateKey != "agents-api-"+h.session.ID { + if prompt.AgentSessionID != "native-thread-1" { t.Fatal("native continuity lost") } h.write(next.TurnID, proto.TypeDone, proto.DonePayload{}) diff --git a/services/core/tests/integration/environment_directory_test.go b/services/core/tests/integration/environment_directory_test.go index 0ce449113..b18fbe23c 100644 --- a/services/core/tests/integration/environment_directory_test.go +++ b/services/core/tests/integration/environment_directory_test.go @@ -73,7 +73,7 @@ func prepareDirectoryRead(t *testing.T, h *dispatchHarness, environment sessions t.Helper() frame := h.read(proto.TypeExecutionPrepare) var request proto.ExecutionPreparePayload - if frame.DecodePayload(&request) != nil || !proto.ValidWorkspaceReadPreparation(request.Configuration) || request.Configuration.LocalEnvironment == nil || request.Configuration.LocalEnvironment.ID != environment.ID || request.Configuration.AgentStateKey != "agents-api-"+h.session.ID { + if frame.DecodePayload(&request) != nil || !proto.ValidWorkspaceReadPreparation(request.Configuration) || request.Configuration.LocalEnvironment == nil || request.Configuration.LocalEnvironment.ID != environment.ID || request.SessionID != h.session.ID { t.Fatal("read did not use the closed preparation profile") } handle := acknowledgePreparation(h, frame.ID) diff --git a/services/core/tests/integration/environment_worker_helpers_test.go b/services/core/tests/integration/environment_worker_helpers_test.go index 5bfbf60c4..6cd55c03f 100644 --- a/services/core/tests/integration/environment_worker_helpers_test.go +++ b/services/core/tests/integration/environment_worker_helpers_test.go @@ -90,7 +90,7 @@ func workerRuntimeForPreparation(t *testing.T, h *dispatchHarness, frame proto.E t.Fatal("invalid worker preparation") } for _, candidate := range h.environments { - if input.Configuration.AgentStateKey == "agents-api-"+candidate.session.ID && input.Configuration.LocalEnvironment != nil && input.Configuration.LocalEnvironment.ID == candidate.device.EnvironmentID { + if input.SessionID == candidate.session.ID && input.Configuration.LocalEnvironment != nil && input.Configuration.LocalEnvironment.ID == candidate.device.EnvironmentID { return candidate } } diff --git a/services/core/tests/integration/environment_worker_test.go b/services/core/tests/integration/environment_worker_test.go index ae998860c..dc0cf75da 100644 --- a/services/core/tests/integration/environment_worker_test.go +++ b/services/core/tests/integration/environment_worker_test.go @@ -2,7 +2,6 @@ package integration import ( "encoding/json" - "strings" "testing" "time" @@ -69,7 +68,7 @@ func TestWorkerEnvironmentSharesCapacityThroughClaimAndCleanup(t *testing.T) { if second.DecodePayload(&prepare) != nil { t.Fatal("invalid Prepare") } - waiting := pending[strings.TrimPrefix(prepare.Configuration.AgentStateKey, "agents-api-")] + waiting := pending[prepare.SessionID] if waiting.ID == "" { t.Fatal("wrong waiting Session") } diff --git a/services/core/tests/integration/executor_fixture_test.go b/services/core/tests/integration/executor_fixture_test.go index ca39a966b..b6be4f202 100644 --- a/services/core/tests/integration/executor_fixture_test.go +++ b/services/core/tests/integration/executor_fixture_test.go @@ -10,6 +10,13 @@ import ( // preparation failures consume the actual control frames directly. const testExecutionRequest = "test_execution_request" +// testExecution is the payload of a testExecutionRequest. +type testExecution struct { + proto.PromptRequestPayload + RunID string `json:"run_id"` + Input proto.MessageInput `json:"input"` +} + type fixtureAdmission struct { prepare proto.ExecutionPreparePayload handle string @@ -74,8 +81,8 @@ func (h *dispatchHarness) executionFrame(env proto.Envelope) (proto.Envelope, bo if prepare.Configuration.LocalEnvironment != nil || prepare.Configuration.WorkspaceReadOnly { return env, true } - if prepare.SessionID == "" || prepare.Configuration.RunID != "" || len(prepare.Configuration.Input) != 0 { - h.t.Fatal("preparation changed Session identity or submitted input early") + if prepare.SessionID == "" { + h.t.Fatal("preparation lost Session identity") } admission := fixtureAdmission{prepare: prepare, handle: uuid.NewString(), executor: "executor-" + prepare.SessionID} h.admissions[env.ID] = admission @@ -91,9 +98,7 @@ func (h *dispatchHarness) executionFrame(env proto.Envelope) (proto.Envelope, bo h.t.Fatal("Start changed admission, Executor or input identity") } h.write(env.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: admission.handle, ExecutorID: admission.executor, Revision: 2, State: "started", RunID: start.RunID}) - request := admission.prepare.Configuration - request.RunID, request.Input = start.RunID, start.Input - projected, err := proto.NewEnvelope(testExecutionRequest, start.RunID, request) + projected, err := proto.NewEnvelope(testExecutionRequest, start.RunID, testExecution{PromptRequestPayload: admission.prepare.Configuration, RunID: start.RunID, Input: start.Input}) if err != nil { h.t.Fatal(err) } diff --git a/services/core/tests/integration/executor_recovery_test.go b/services/core/tests/integration/executor_recovery_test.go index 589055f0b..eaa64f760 100644 --- a/services/core/tests/integration/executor_recovery_test.go +++ b/services/core/tests/integration/executor_recovery_test.go @@ -12,7 +12,7 @@ func readyExecutorAttempt(t *testing.T, h *dispatchHarness) (proto.Envelope, pro t.Helper() prepare := h.read(proto.TypeExecutionPrepare) var configuration proto.ExecutionPreparePayload - if prepare.DecodePayload(&configuration) != nil || configuration.SessionID != h.session.ID || len(configuration.Configuration.Input) != 0 { + if prepare.DecodePayload(&configuration) != nil || configuration.SessionID != h.session.ID { t.Fatal("invalid Executor preparation identity") } handle, executor := uuid.NewString(), uuid.NewString() diff --git a/services/core/tests/integration/harness_onboarding_test.go b/services/core/tests/integration/harness_onboarding_test.go index 9a5996c7e..92ebd765a 100644 --- a/services/core/tests/integration/harness_onboarding_test.go +++ b/services/core/tests/integration/harness_onboarding_test.go @@ -145,18 +145,18 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } } -func awaitOnboardingPrompt(t *testing.T, c <-chan proto.PromptRequestPayload) proto.PromptRequestPayload { +func awaitOnboardingPrompt(t *testing.T, c <-chan testExecution) testExecution { t.Helper() select { case p := <-c: return p case <-time.After(10 * time.Second): t.Fatal("fixture was not dispatched") - return proto.PromptRequestPayload{} + return testExecution{} } } -func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptRequestPayload, func(proto.Envelope) error, proto.SupportedAgentKind) { +func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan testExecution, func(proto.Envelope) error, proto.SupportedAgentKind) { t.Helper() root, err := filepath.Abs("../../../..") if err != nil { @@ -182,7 +182,7 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR if err = child.Start(); err != nil { t.Fatal(err) } - started := make(chan proto.PromptRequestPayload, 4) + started := make(chan testExecution, 4) declarations := make(chan proto.SupportedAgentKind, 1) up := make(chan error, 1) down := make(chan error, 1) @@ -232,8 +232,8 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR down <- err return } - if p.SessionID == "" || p.Configuration.RunID != "" || len(p.Configuration.Input) != 0 { - down <- fmt.Errorf("fixture preparation submitted input or lost Session identity") + if p.SessionID == "" { + down <- fmt.Errorf("fixture preparation lost Session identity") return } preparations[e.ID] = p @@ -249,9 +249,7 @@ func startOnboardingPeer(t *testing.T, h *dispatchHarness) (<-chan proto.PromptR down <- fmt.Errorf("fixture Start lacks prepared Executor ownership") return } - request := p.Configuration - request.RunID, request.Input = start.RunID, start.Input - started <- request + started <- testExecution{PromptRequestPayload: p.Configuration, RunID: start.RunID, Input: start.Input} } if e.Type == proto.TypeExecutionRelease { delete(preparations, e.ID) diff --git a/services/core/tests/integration/prepared_dispatch_test.go b/services/core/tests/integration/prepared_dispatch_test.go index 86d45f84a..413db2cd6 100644 --- a/services/core/tests/integration/prepared_dispatch_test.go +++ b/services/core/tests/integration/prepared_dispatch_test.go @@ -77,7 +77,7 @@ func TestPreparedDispatchPromotesOriginalBatchAndPersistsCompletion(t *testing.T result := runPreparedDispatch(h, t.Context(), pending) frame := h.read(proto.TypeExecutionPrepare) var prepare proto.ExecutionPreparePayload - if frame.DecodePayload(&prepare) != nil || len(prepare.Configuration.Input) != 0 || prepare.Configuration.RunID != "" || prepare.Configuration.LocalEnvironment == nil || prepare.Configuration.LocalEnvironment.ID != h.device.EnvironmentID || prepare.Configuration.DisableExecutionEnvironment { + if frame.DecodePayload(&prepare) != nil || prepare.Configuration.LocalEnvironment == nil || prepare.Configuration.LocalEnvironment.ID != h.device.EnvironmentID || prepare.Configuration.DisableExecutionEnvironment { t.Fatal("invalid preparation configuration", prepare) } session, err := sessionAdapter(h.s).GetSession(t.Context(), h.tenant, h.session.ID) diff --git a/services/core/tests/integration/public_execution_test.go b/services/core/tests/integration/public_execution_test.go index 84c1cbabf..caaaaeb73 100644 --- a/services/core/tests/integration/public_execution_test.go +++ b/services/core/tests/integration/public_execution_test.go @@ -56,7 +56,7 @@ func TestExecutionWorkerAdmissionBindingAndRecovery(t *testing.T) { t.Fatal(retry, err) } request := h.read(testExecutionRequest) - var prompt proto.PromptRequestPayload + var prompt testExecution if err := request.DecodePayload(&prompt); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/worker_capacity_test.go b/services/core/tests/integration/worker_capacity_test.go index 6932380f5..b37f7ade3 100644 --- a/services/core/tests/integration/worker_capacity_test.go +++ b/services/core/tests/integration/worker_capacity_test.go @@ -56,8 +56,8 @@ func TestWorkerDefersPreparationCapacityUntilCleanupReleasesSlot(t *testing.T) { switch frame.Type { case proto.TypeExecutionPrepare: var prepare proto.ExecutionPreparePayload - if frame.DecodePayload(&prepare) != nil || turns[prepare.SessionID] == "" || len(prepare.Configuration.Input) != 0 || prepare.Configuration.RunID != "" { - t.Fatal("invalid input-free preparation", prepare) + if frame.DecodePayload(&prepare) != nil || turns[prepare.SessionID] == "" { + t.Fatal("invalid preparation", prepare) } if blocked == "" && len(admissions) == 4 { blocked = prepare.SessionID