From 23d70cdabff7eae0b23bfc1eb634a7c279dad36f Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 06:55:55 +0000 Subject: [PATCH] Delete prompt request fields Core never sends Core never sets workspace_authoring and always sends strict_resume and observe_tool_observations as true, so the Runtime carried code paths that no Core request reaches. Delete them and keep only the behavior Core uses: - Workspace authoring: the proto messages and socket variable, the WorkspaceAuthoring request field and capability, the daemon authoring bridge, its CLI wiring and companion PATH helper, the env agent option that only the bridge injected, and Core's capability copy and authoring_request rejection case. - strict_resume: resume and recovery are always strict; mcode always runs its protected execution profile. - observe_tool_observations: adapters always attach the neutral observation to tool_call frames. - Codex and the shared StateDir no longer derive legacy state keys from the conversation or run; an empty agent state key is rejected. - clirunner always owns the child's process group (a Job object on Windows), because every caller now requests it. The Core-Runtime protocol doc, Harness onboarding, CONTRIBUTING and the Claude SDK adapter README drop the deleted fields and features. --- CONTRIBUTING.md | 1 - .../claudesdk/cancellation_live_linux_test.go | 2 +- .../internal/agent/claudesdk/commands.go | 10 +- .../agent/claudesdk/commands_session_test.go | 102 ++++++------- .../internal/agent/claudesdk/commands_test.go | 88 ++++++------ .../internal/agent/claudesdk/declaration.go | 1 - .../agent/claudesdk/declaration_test.go | 2 +- .../internal/agent/claudesdk/executor.go | 3 - .../claudesdk/executor_confirmation_test.go | 3 + .../claudesdk/executor_live_linux_test.go | 2 +- .../internal/agent/claudesdk/executor_turn.go | 4 +- .../internal/agent/claudesdk/functions.go | 10 +- .../agent/claudesdk/functions_test.go | 2 +- .../agent/claudesdk/live_linux_test.go | 3 +- apps/daemon/internal/agent/claudesdk/mcp.go | 10 +- .../agent/claudesdk/mcp_environment_test.go | 4 +- .../internal/agent/claudesdk/mcp_test.go | 4 +- .../internal/agent/claudesdk/options.go | 6 +- .../agent/claudesdk/preparation_test.go | 4 +- .../internal/agent/claudesdk/readiness.go | 18 ++- .../internal/agent/claudesdk/session.go | 2 +- .../agent/claudesdk/workspace_launch_test.go | 4 +- .../claudesdk/workspace_live_linux_test.go | 2 +- .../internal/agent/clirunner/process.go | 94 ++---------- .../agent/clirunner/process_group_other.go | 2 +- .../clirunner/process_group_unix_test.go | 6 +- .../clirunner/process_group_windows_test.go | 4 +- .../internal/agent/codex/declaration.go | 1 - .../internal/agent/codex/declaration_test.go | 2 +- .../internal/agent/codex/environment.go | 26 ++-- .../agent/codex/environment_retired_test.go | 47 +++--- apps/daemon/internal/agent/codex/executor.go | 5 +- .../agent/codex/executor_native_test.go | 2 +- .../internal/agent/codex/executor_test.go | 2 +- .../internal/agent/codex/mcp_required_test.go | 1 - apps/daemon/internal/agent/codex/options.go | 37 +---- .../internal/agent/codex/options_test.go | 12 +- .../internal/agent/codex/preparation.go | 16 +-- .../agent/codex/preparation_helpers_test.go | 2 +- apps/daemon/internal/agent/codex/prepared.go | 3 +- .../internal/agent/codex/recovery_test.go | 8 +- apps/daemon/internal/agent/codex/resume.go | 10 +- .../internal/agent/codex/resume_test.go | 79 +++++------ apps/daemon/internal/agent/codex/rpc.go | 2 +- .../internal/agent/codex/rpc_close_test.go | 2 +- apps/daemon/internal/agent/codex/session.go | 9 +- .../agent/codex/session_command_output.go | 3 - .../codex/session_command_output_test.go | 62 ++++---- .../agent/codex/session_notifications_test.go | 3 +- .../internal/agent/codex/session_plan.go | 2 +- .../internal/agent/codex/session_tools.go | 8 +- .../agent/codex/session_tools_test.go | 70 ++++----- apps/daemon/internal/agent/codex/skills.go | 20 +-- .../internal/agent/codex/skills_test.go | 12 -- .../internal/agent/configuration_test.go | 8 -- apps/daemon/internal/agent/harness.go | 3 +- .../internal/agent/installroot/probe.go | 2 +- .../internal/agent/mcode/declaration.go | 2 - .../internal/agent/mcode/declaration_test.go | 2 +- .../agent/mcode/environment_mcp_test.go | 1 - apps/daemon/internal/agent/mcode/events.go | 25 ++-- apps/daemon/internal/agent/mcode/execution.go | 19 +-- .../internal/agent/mcode/execution_test.go | 17 +-- .../agent/mcode/executor_native_test.go | 2 +- .../internal/agent/mcode/executor_test.go | 2 +- .../internal/agent/mcode/executor_turn.go | 6 +- .../agent/mcode/mcp_observations_test.go | 2 +- .../agent/mcode/native_history_test.go | 2 +- apps/daemon/internal/agent/mcode/options.go | 60 +++----- .../internal/agent/mcode/options_test.go | 10 +- .../internal/agent/mcode/preparation_test.go | 2 +- apps/daemon/internal/agent/mcode/session.go | 15 +- .../internal/agent/mcode/session_test.go | 13 +- .../internal/agent/mcode/steering_test.go | 2 +- .../internal/agent/mcode/tool_observations.go | 22 ++- .../agent/mcode/tool_observations_test.go | 2 +- apps/daemon/internal/agent/mcode/workspace.go | 2 +- .../agent/mcode/workspace_readiness.go | 2 +- apps/daemon/internal/agent/registry.go | 12 -- apps/daemon/internal/agent/runtime_paths.go | 20 +-- .../internal/agent/runtime_paths_test.go | 17 +-- apps/daemon/internal/authoring/bridge.go | 134 ------------------ apps/daemon/internal/authoring/bridge_test.go | 106 -------------- apps/daemon/internal/cli/authoring.go | 77 ---------- apps/daemon/internal/cli/authoring_test.go | 101 ------------- .../cli/claude_sdk_live_linux_test.go | 6 +- apps/daemon/internal/cli/companion_path.go | 18 --- .../internal/cli/companion_path_test.go | 37 ----- apps/daemon/internal/cli/connect.go | 7 - .../internal/cli/connect_cleanup_test.go | 2 +- apps/daemon/internal/cli/connect_suspend.go | 12 +- apps/daemon/internal/cli/preparation_test.go | 32 +---- apps/daemon/internal/dispatch/executor.go | 2 +- .../dispatch/executor_handoff_test.go | 2 +- .../daemon/internal/dispatch/executor_test.go | 2 +- .../dispatch/functions_native_test.go | 2 +- .../internal/dispatch/local_directory_test.go | 4 +- apps/daemon/internal/dispatch/preparation.go | 2 +- .../internal/dispatch/preparation_test.go | 4 +- .../daemon/internal/localworkspace/binding.go | 3 +- .../internal/localworkspace/binding_test.go | 8 +- .../runtime_initialization_process.go | 2 +- apps/daemon/testdata/onboarding/main.go | 2 +- contracts/agents-api/harness-onboarding.md | 6 +- contracts/agents-api/zh/harness-onboarding.md | 8 +- docs/runtime-protocol.md | 7 +- docs/zh/runtime-protocol.md | 9 +- internal/agentdaemon/proto/authoring.go | 31 ---- internal/agentdaemon/proto/inbound.go | 1 - internal/agentdaemon/proto/outbound.go | 28 ++-- internal/agentdaemon/proto/preparation.go | 6 +- .../proto/prototest/capabilities.go | 1 - internal/agentdaemon/proto/prototest/wire.go | 2 +- .../proto/workspace_read_preparation.go | 6 +- packages/claude-sdk-adapter/README.md | 8 +- scripts/name-allowlist.json | 10 -- services/core/internal/execution/delivery.go | 2 +- .../execution/directory_preparation.go | 2 +- .../core/internal/execution/recovery_test.go | 2 +- services/core/internal/execution/request.go | 5 +- services/core/internal/runtimedevice/state.go | 1 - .../mcp_bearer_live_linux_test.go | 2 +- .../core/internal/runtimegateway/session.go | 1 - .../tests/integration/execution_tools_test.go | 4 +- .../integration/harness_onboarding_test.go | 2 +- 125 files changed, 453 insertions(+), 1367 deletions(-) delete mode 100644 apps/daemon/internal/authoring/bridge.go delete mode 100644 apps/daemon/internal/authoring/bridge_test.go delete mode 100644 apps/daemon/internal/cli/authoring.go delete mode 100644 apps/daemon/internal/cli/authoring_test.go delete mode 100644 apps/daemon/internal/cli/companion_path.go delete mode 100644 apps/daemon/internal/cli/companion_path_test.go delete mode 100644 internal/agentdaemon/proto/authoring.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12405dd99..04cbec682 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,6 @@ Core must build, deploy and run independently of product services, frontends and - Parsar owns users, workspaces, business authorization, Agent/Team definitions, capabilities, product conversations, IM/sharing, approval decisions and billing. It uses Core for execution. - A product conversation may reference several execution Sessions. Core owns native engine session identities; an execution Session has its own lifetime, separate from a daemon connection, process or sandbox. - Build application orchestration on the [public Session and event contract](docs/api/public-agent-api.md). Product cursor replay must be an explicit product extension. Business Team orchestration belongs to the application; Core's pinned `multi_agent` and Subagent resources remain part of the public contract. -- Daemon Skill/SP authoring is a product operation: forward it through a scoped product callback that checks the original requester and workspace. A Runtime credential alone must not authorize business writes. ### Optional application example 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 a16a9d7ce..bc5cec8bd 100644 --- a/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/cancellation_live_linux_test.go @@ -65,7 +65,7 @@ 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, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}} + request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}} running, err := NewFactory(config)(ctx, request, out) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/claudesdk/commands.go b/apps/daemon/internal/agent/claudesdk/commands.go index 46dfa31b1..9bc4c266a 100644 --- a/apps/daemon/internal/agent/claudesdk/commands.go +++ b/apps/daemon/internal/agent/claudesdk/commands.go @@ -33,9 +33,7 @@ func (c *commandState) receive(event bridgeEvent, start startRequest, sessionID return fmt.Errorf("claudesdk: inconsistent command observation") } c.calls[event.ID] = *n - if start.observeFunctions { - emit(proto.TypeToolCall, proto.ToolCallPayload{ID: event.ID, Name: "Bash", Stage: event.Stage, Observation: n}) - } + emit(proto.TypeToolCall, proto.ToolCallPayload{ID: event.ID, Name: "Bash", Stage: event.Stage, Observation: n}) return nil } @@ -48,15 +46,13 @@ func (c *commandState) complete() bool { return true } -func (c *commandState) close(start startRequest, emit func(string, any)) { +func (c *commandState) close(emit func(string, any)) { for id, call := range c.calls { if call.Status != "in_progress" { continue } call.Status = "incomplete" c.calls[id] = call - if start.observeFunctions { - emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: "Bash", Stage: "after", Observation: &call}) - } + emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: "Bash", Stage: "after", Observation: &call}) } } diff --git a/apps/daemon/internal/agent/claudesdk/commands_session_test.go b/apps/daemon/internal/agent/claudesdk/commands_session_test.go index de9454556..bc62c8540 100644 --- a/apps/daemon/internal/agent/claudesdk/commands_session_test.go +++ b/apps/daemon/internal/agent/claudesdk/commands_session_test.go @@ -16,74 +16,56 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func TestWorkspaceCommandsRequirePackagedFeatureOnlyWhenRequested(t *testing.T) { - for _, observed := range []bool{false, true} { - config := preparationFixture(t, "old-command-runtime") - req := preparationRequest() - req.ObserveToolObservations = observed - resource, err := NewPreparationFactory(config)(t.Context(), req) - if observed { - if err == nil || !strings.Contains(err.Error(), "workspace command observations") { - t.Fatal("old bridge accepted requested command observations", err) - } - if _, err := os.Stat(filepath.Join(config.StateDir, "launched")); !os.IsNotExist(err) { - t.Fatal("old bridge started execution before rejection") - } - } else { - if err != nil { - t.Fatal("old bridge changed opt-out behavior", err) - } - if err := resource.Close(); err != nil { - t.Fatal(err) - } - } +func TestWorkspaceCommandsRequirePackagedFeature(t *testing.T) { + config := preparationFixture(t, "old-command-runtime") + if _, err := NewPreparationFactory(config)(t.Context(), 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) { + t.Fatal("old bridge started execution before rejection") } } func TestWorkspaceCommandFramesKeepStartIdentityAndObservedOutput(t *testing.T) { - for _, observed := range []bool{false, true} { - config := preparationFixture(t, "commands-success") - req := preparationRequest() - req.ObserveToolObservations = observed - resource, err := NewPreparationFactory(config)(t.Context(), req) - if err != nil { - t.Fatal(err) - } - defer resource.Close() - if _, err := os.Stat(filepath.Join(config.StateDir, "start.json")); !os.IsNotExist(err) { - t.Fatal("preparation submitted a command") - } - out := make(chan proto.Envelope, 16) - s, err := resource.Start(t.Context(), "actual-command-run", proto.TextInput("hello"), out) - if err != nil { - t.Fatal(err) + config := preparationFixture(t, "commands-success") + resource, err := NewPreparationFactory(config)(t.Context(), preparationRequest()) + if err != nil { + t.Fatal(err) + } + defer resource.Close() + if _, err := os.Stat(filepath.Join(config.StateDir, "start.json")); !os.IsNotExist(err) { + t.Fatal("preparation submitted a command") + } + out := make(chan proto.Envelope, 16) + s, err := resource.Start(t.Context(), "actual-command-run", proto.TextInput("hello"), out) + if err != nil { + t.Fatal(err) + } + defer s.Cancel(context.Background()) + var frames []proto.ToolCallPayload + done := 0 + for event := range out { + if event.ID != "actual-command-run" || event.Type == proto.TypeError || event.Type == proto.TypeCommandOutput { + t.Fatal("execution identity or final-only command behavior changed", event.Type) } - defer s.Cancel(context.Background()) - var frames []proto.ToolCallPayload - done := 0 - for event := range out { - if event.ID != "actual-command-run" || event.Type == proto.TypeError || event.Type == proto.TypeCommandOutput { - t.Fatal("execution identity or final-only command behavior changed", event.Type) - } - if event.Type == proto.TypeToolCall { - var payload proto.ToolCallPayload - if err := event.DecodePayload(&payload); err != nil { - t.Fatal(err) - } - frames = append(frames, payload) - } - if event.Type == proto.TypeDone { - done++ + if event.Type == proto.TypeToolCall { + var payload proto.ToolCallPayload + if err := event.DecodePayload(&payload); err != nil { + t.Fatal(err) } + frames = append(frames, payload) } - if done != 1 || observed && len(frames) != 2 || !observed && len(frames) != 0 { - t.Fatal("completion or observation opt-in changed", done, frames) - } - if observed && (frames[0].ID != "observed" || frames[1].ID != "observed" || frames[1].Observation.Status != "failed" || - string(frames[1].Observation.Output) != `"Exit code 7\nretained"`) { - t.Fatal("native failure output was not retained", frames) + if event.Type == proto.TypeDone { + done++ } } + if done != 1 || len(frames) != 2 { + t.Fatal("completion or observation changed", done, frames) + } + if frames[0].ID != "observed" || frames[1].ID != "observed" || frames[1].Observation.Status != "failed" || + string(frames[1].Observation.Output) != `"Exit code 7\nretained"` { + t.Fatal("native failure output was not retained", frames) + } } func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *testing.T) { @@ -93,7 +75,7 @@ func TestWorkspaceCommandCancellationAndBridgeFailuresCloseOnlyPendingCalls(t *t defer cancel() config := preparationFixture(t, mode) req := workspaceRequest() - req.AgentSessionID, req.ObserveToolObservations = "native-session", true + req.AgentSessionID = "native-session" out := make(chan proto.Envelope, 32) s, err := NewFactory(config)(ctx, req, out) if err != nil { diff --git a/apps/daemon/internal/agent/claudesdk/commands_test.go b/apps/daemon/internal/agent/claudesdk/commands_test.go index 33b923c7f..f98129c51 100644 --- a/apps/daemon/internal/agent/claudesdk/commands_test.go +++ b/apps/daemon/internal/agent/claudesdk/commands_test.go @@ -13,53 +13,45 @@ func commandEvent(id, stage, status, command string) bridgeEvent { Observation: &proto.ToolObservation{Kind: "command", Status: status, Command: command}} } -func TestCommandObservationLifecycleAndOptIn(t *testing.T) { - for _, observed := range []bool{false, true} { - state := commandState{calls: map[string]proto.ToolObservation{}} - start := startRequest{Workspace: &workspaceProfile{}, observeFunctions: observed} - var events []proto.ToolCallPayload - emit := func(kind string, payload any) { - if kind != proto.TypeToolCall { - t.Fatal(kind) - } - events = append(events, payload.(proto.ToolCallPayload)) - } - const command = " printf 'failure\\n'; exit 7 " - before := commandEvent("native-call", "before", "in_progress", command) - if err := state.receive(before, start, "native-session", emit); err != nil || state.complete() { - t.Fatal("call was not pending", err) - } - if err := state.receive(before, start, "native-session", emit); err == nil { - t.Fatal("duplicate bridge call accepted") - } - after := commandEvent("native-call", "after", "failed", command) - after.Observation.Output = json.RawMessage(`"Exit code 7\nfailure"`) - if err := state.receive(after, start, "native-session", emit); err != nil || !state.complete() { - t.Fatal("native result did not complete the call", err) - } - if err := state.receive(after, start, "native-session", emit); err == nil { - t.Fatal("duplicate bridge result accepted") - } - if err := state.receive(commandEvent("pending", "before", "in_progress", "sleep 30"), start, "native-session", emit); err != nil { - t.Fatal(err) - } - state.close(start, emit) - state.close(start, emit) - if !state.complete() || state.calls["pending"].Status != "incomplete" || state.calls["native-call"].Status != "failed" { - t.Fatal("closure changed an observed result or lost an unfinished call") - } - if !observed { - if len(events) != 0 { - t.Fatal("opt-out emitted observations") - } - continue - } - if len(events) != 4 || events[0].ID != "native-call" || events[0].Name != "Bash" || events[0].Observation.Command != command || - string(events[1].Observation.Output) != `"Exit code 7\nfailure"` || events[1].Observation.ExitCode != nil || - events[1].Observation.Cwd != nil || events[1].Observation.DurationMS != nil || events[3].ID != "pending" || - events[3].Observation.Status != "incomplete" || len(events[3].Observation.Output) != 0 { - t.Fatal("observation identity, output or unknown metadata changed", events) +func TestCommandObservationLifecycle(t *testing.T) { + state := commandState{calls: map[string]proto.ToolObservation{}} + start := startRequest{Workspace: &workspaceProfile{}} + var events []proto.ToolCallPayload + emit := func(kind string, payload any) { + if kind != proto.TypeToolCall { + t.Fatal(kind) } + events = append(events, payload.(proto.ToolCallPayload)) + } + const command = " printf 'failure\\n'; exit 7 " + before := commandEvent("native-call", "before", "in_progress", command) + if err := state.receive(before, start, "native-session", emit); err != nil || state.complete() { + t.Fatal("call was not pending", err) + } + if err := state.receive(before, start, "native-session", emit); err == nil { + t.Fatal("duplicate bridge call accepted") + } + after := commandEvent("native-call", "after", "failed", command) + after.Observation.Output = json.RawMessage(`"Exit code 7\nfailure"`) + if err := state.receive(after, start, "native-session", emit); err != nil || !state.complete() { + t.Fatal("native result did not complete the call", err) + } + if err := state.receive(after, start, "native-session", emit); err == nil { + t.Fatal("duplicate bridge result accepted") + } + if err := state.receive(commandEvent("pending", "before", "in_progress", "sleep 30"), start, "native-session", emit); err != nil { + t.Fatal(err) + } + state.close(emit) + state.close(emit) + if !state.complete() || state.calls["pending"].Status != "incomplete" || state.calls["native-call"].Status != "failed" { + t.Fatal("closure changed an observed result or lost an unfinished call") + } + if len(events) != 4 || events[0].ID != "native-call" || events[0].Name != "Bash" || events[0].Observation.Command != command || + string(events[1].Observation.Output) != `"Exit code 7\nfailure"` || events[1].Observation.ExitCode != nil || + events[1].Observation.Cwd != nil || events[1].Observation.DurationMS != nil || events[3].ID != "pending" || + events[3].Observation.Status != "incomplete" || len(events[3].Observation.Output) != 0 { + t.Fatal("observation identity, output or unknown metadata changed", events) } } @@ -67,7 +59,7 @@ func TestCommandObservationsRejectUnqualifiedOrInconsistentEvents(t *testing.T) for _, mode := range []string{"profile", "uninitialized", "session", "id", "nil", "kind", "empty-command", "name", "cwd", "exit", "duration", "arguments", "error", "output-object", "before-output", "before-status", "after-before", "changed-command", "after-status", "stage"} { t.Run(mode, func(t *testing.T) { state := commandState{calls: map[string]proto.ToolObservation{}} - start := startRequest{Workspace: &workspaceProfile{}, observeFunctions: true} + start := startRequest{Workspace: &workspaceProfile{}} sessionID := "native-session" event := commandEvent("call", "before", "in_progress", "pwd") if strings.HasPrefix(mode, "after-") || mode == "changed-command" { @@ -130,7 +122,7 @@ func TestCommandObservationUnknownAndEmptyOutputRemainDistinct(t *testing.T) { for _, output := range []json.RawMessage{nil, json.RawMessage(`null`), json.RawMessage(`""`)} { state := commandState{calls: map[string]proto.ToolObservation{}} start := startRequest{Workspace: &workspaceProfile{}} - emit := func(string, any) { t.Fatal("opt-out emitted an observation") } + emit := func(string, any) {} if err := state.receive(commandEvent("call", "before", "in_progress", "pwd"), start, "native-session", emit); err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/claudesdk/declaration.go b/apps/daemon/internal/agent/claudesdk/declaration.go index e99650049..21a643f88 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration.go +++ b/apps/daemon/internal/agent/claudesdk/declaration.go @@ -26,7 +26,6 @@ var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude Usage: proto.CapabilitySupported, Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, - WorkspaceAuthoring: proto.CapabilityUnsupported, Steering: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, diff --git a/apps/daemon/internal/agent/claudesdk/declaration_test.go b/apps/daemon/internal/agent/claudesdk/declaration_test.go index d62d77781..149ef8f65 100644 --- a/apps/daemon/internal/agent/claudesdk/declaration_test.go +++ b/apps/daemon/internal/agent/claudesdk/declaration_test.go @@ -122,7 +122,7 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) { registry := agent.NewRegistry() registry.Register(Declaration, *runtime) info := registry.SupportedAgentKinds()[0] - if info.Capabilities.WorkspaceAuthoring.IsSupported() || info.Capabilities.Preparation.IsSupported() != ready { + if info.Capabilities.Preparation.IsSupported() != ready { t.Fatal(info) } if !ready { diff --git a/apps/daemon/internal/agent/claudesdk/executor.go b/apps/daemon/internal/agent/claudesdk/executor.go index 34240d0c5..55eed30a5 100644 --- a/apps/daemon/internal/agent/claudesdk/executor.go +++ b/apps/daemon/internal/agent/claudesdk/executor.go @@ -91,9 +91,6 @@ func validateExecutorFeatures(info RuntimeInfo, start startRequest) error { if start.Subagents != nil && !info.SupportsSubagents() { return errors.New("claudesdk: subagent resources are unavailable") } - if start.Workspace != nil && start.observeFunctions && !info.supportsWorkspaceCommands() { - return errors.New("claudesdk: packaged runtime does not support workspace command observations") - } if start.Workspace != nil && len(start.Functions) > 0 && !info.SupportsWorkspaceFunctions() { return errors.New("claudesdk: workspace functions are unavailable") } diff --git a/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go b/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go index f4cb246bd..bba0b7099 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_confirmation_test.go @@ -27,6 +27,9 @@ func TestExecutorNativeConfirmationSurvivesCleanup(t *testing.T) { t.Fatal("initial output missing") } if mode == "pending_function" || mode == "pending_function_unconfirmed" { + if event := <-out; event.Type != proto.TypeToolCall { + t.Fatal("function observation missing") + } if event := <-out; event.Type != proto.TypeFunctionCall { t.Fatal("function obligation missing") } 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 25fb8fcda..669f2dce1 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/executor_live_linux_test.go @@ -80,7 +80,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) { _ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600) } defer persist() - request := proto.PromptRequestPayload{StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": model, "system_prompt": "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}} + request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": model, "system_prompt": "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) diff --git a/apps/daemon/internal/agent/claudesdk/executor_turn.go b/apps/daemon/internal/agent/claudesdk/executor_turn.go index d1b9ce48c..97c6638cf 100644 --- a/apps/daemon/internal/agent/claudesdk/executor_turn.go +++ b/apps/daemon/internal/agent/claudesdk/executor_turn.go @@ -197,8 +197,8 @@ func (s *session) runTurn(start startRequest, out chan<- proto.Envelope) { if !s.functionsComplete(cancelled && settlementConfirmed) || !s.steeringComplete() || !mcp.complete() || !commands.complete() { settlementConfirmed, reusable, reason = false, false, "unsettled_native_operations" } - mcp.close(start, emit) - commands.close(start, emit) + mcp.close(emit) + commands.close(emit) s.stopSteering() metadata := map[string]any{proto.DoneMetaAgentSessionType: "claude_session"} if id := s.inputSessionID(); id != "" { diff --git a/apps/daemon/internal/agent/claudesdk/functions.go b/apps/daemon/internal/agent/claudesdk/functions.go index 80048c70b..752d1347f 100644 --- a/apps/daemon/internal/agent/claudesdk/functions.go +++ b/apps/daemon/internal/agent/claudesdk/functions.go @@ -84,13 +84,11 @@ func (s *session) receiveFunction(event bridgeEvent, start startRequest, emit fu if err != nil { return err } - if start.observeFunctions { - id, stage := event.CallID, "after" - if call != nil { - id, stage = call.CallID, "before" - } - emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: observation.Name, Stage: stage, Observation: observation}) + id, stage := event.CallID, "after" + if call != nil { + id, stage = call.CallID, "before" } + emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: observation.Name, Stage: stage, Observation: observation}) if call != nil { emit(proto.TypeFunctionCall, call) } else { diff --git a/apps/daemon/internal/agent/claudesdk/functions_test.go b/apps/daemon/internal/agent/claudesdk/functions_test.go index 09f8b430a..a448790e3 100644 --- a/apps/daemon/internal/agent/claudesdk/functions_test.go +++ b/apps/daemon/internal/agent/claudesdk/functions_test.go @@ -22,7 +22,7 @@ func TestFunctionFactoryNativeReceipts(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{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", ObserveToolObservations: true, AgentOptions: map[string]any{"model": "fake-model", "system_prompt": "instructions"}, FunctionTools: []proto.FunctionTool{{Name: "lookup", Description: "Lookup.", Parameters: json.RawMessage(`{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"}}}}`)}}} + request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentSessionID: "native-session", AgentOptions: map[string]any{"model": "fake-model", "system_prompt": "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) diff --git a/apps/daemon/internal/agent/claudesdk/live_linux_test.go b/apps/daemon/internal/agent/claudesdk/live_linux_test.go index 335ff6ecd..ba2f7ca9d 100644 --- a/apps/daemon/internal/agent/claudesdk/live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/live_linux_test.go @@ -137,9 +137,8 @@ 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, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Answer briefly and preserve the exact verification value in the conversation. Use no tools."}} + request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": "Answer briefly and preserve the exact verification value in the conversation. Use no tools."}} if success != nil { - request.ObserveToolObservations = true request.AgentOptions["system_prompt"] = "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}`)}} } diff --git a/apps/daemon/internal/agent/claudesdk/mcp.go b/apps/daemon/internal/agent/claudesdk/mcp.go index a74da05d7..b193136d7 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp.go +++ b/apps/daemon/internal/agent/claudesdk/mcp.go @@ -109,9 +109,7 @@ func (m *mcpState) receive(event bridgeEvent, start startRequest, emit func(stri return fmt.Errorf("claudesdk: inconsistent MCP observation") } m.calls[event.ID] = *n - if start.observeFunctions { - emit(proto.TypeToolCall, proto.ToolCallPayload{ID: event.ID, Name: n.Name, Stage: event.Stage, Observation: n}) - } + emit(proto.TypeToolCall, proto.ToolCallPayload{ID: event.ID, Name: n.Name, Stage: event.Stage, Observation: n}) return nil } @@ -124,15 +122,13 @@ func (m *mcpState) complete() bool { return true } -func (m *mcpState) close(start startRequest, emit func(string, any)) { +func (m *mcpState) close(emit func(string, any)) { for id, call := range m.calls { if call.Status != "in_progress" { continue } call.Status = "incomplete" m.calls[id] = call - if start.observeFunctions { - emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: call.Name, Stage: "after", Observation: &call}) - } + emit(proto.TypeToolCall, proto.ToolCallPayload{ID: id, Name: call.Name, Stage: "after", Observation: &call}) } } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go index 328674950..ab84b3b9e 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -73,7 +73,7 @@ func TestEnvironmentMCPRejectsUnqualifiedCombinations(t *testing.T) { } func TestEnvironmentMCPObservationsUseInstalledDeclarations(t *testing.T) { - start := startRequest{Workspace: &workspaceProfile{MCP: []environmentMCPServer{{mcpHTTPServer: mcpHTTPServer{ServerLabel: "installed"}}}}, observeFunctions: true} + start := startRequest{Workspace: &workspaceProfile{MCP: []environmentMCPServer{{mcpHTTPServer: mcpHTTPServer{ServerLabel: "installed"}}}}} state := mcpState{calls: map[string]proto.ToolObservation{}} observation := proto.ToolObservation{Kind: "mcp", Name: "echo", Server: "installed", Status: "in_progress", Arguments: json.RawMessage(`{}`), Output: json.RawMessage(`null`), Error: json.RawMessage(`null`)} var emitted []proto.ToolCallPayload @@ -81,7 +81,7 @@ func TestEnvironmentMCPObservationsUseInstalledDeclarations(t *testing.T) { if err := state.receive(bridgeEvent{ID: "native-call", Stage: "before", Observation: &observation}, start, emit); err != nil { t.Fatal(err) } - state.close(start, emit) + state.close(emit) if len(emitted) != 2 || emitted[1].ID != "native-call" || emitted[1].Observation.Status != "incomplete" { t.Fatal("interrupted environment call lost identity") } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_test.go b/apps/daemon/internal/agent/claudesdk/mcp_test.go index 7d1220a01..a8ad18877 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_test.go @@ -72,7 +72,7 @@ func TestHTTPMCPDeclaration(t *testing.T) { } func TestMCPObservationLifecycle(t *testing.T) { - start := startRequest{MCPHTTPServers: &[]mcpHTTPServer{{ServerLabel: "fixture"}}, observeFunctions: true} + start := startRequest{MCPHTTPServers: &[]mcpHTTPServer{{ServerLabel: "fixture"}}} state := mcpState{calls: map[string]proto.ToolObservation{}} var observations []proto.ToolCallPayload emit := func(kind string, payload any) { @@ -107,7 +107,7 @@ func TestMCPObservationLifecycle(t *testing.T) { if err := state.receive(before, start, emit); err != nil { t.Fatal(err) } - state.close(start, emit) + state.close(emit) if !state.complete() || observations[len(observations)-1].Observation.Status != "incomplete" { t.Fatal("cancellation lost pending call") } diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index 47c1ac3e1..46ffde096 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -39,7 +39,6 @@ type startRequest struct { MCPHTTPServers *[]mcpHTTPServer `json:"mcp_http_servers,omitempty"` Workspace *workspaceProfile `json:"workspace,omitempty"` RequireHistory bool `json:"require_history,omitempty"` - observeFunctions bool } func prepare(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { @@ -55,7 +54,7 @@ func prepare(config Config, req proto.PromptRequestPayload) (startRequest, []str } func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { - start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, ObserveMessages: req.ObserveMessages, Functions: req.FunctionTools, observeFunctions: req.ObserveToolObservations} + start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, ObserveMessages: req.ObserveMessages, Functions: req.FunctionTools} fail := func(reason string) (startRequest, []string, error) { return startRequest{}, nil, fmt.Errorf("claudesdk: %s", reason) } @@ -64,9 +63,6 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR return startRequest{}, nil, err } start.NativeModelOptions = compileNativeModelOptions(modelConfiguration.HarnessConfig) - if req.WorkspaceAuthoring { - return fail("requested capability is not available in the private SDK adapter") - } if err := req.ValidateToolSearch(true); err != nil { return startRequest{}, nil, err } diff --git a/apps/daemon/internal/agent/claudesdk/preparation_test.go b/apps/daemon/internal/agent/claudesdk/preparation_test.go index 716797389..f722d4481 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_test.go @@ -107,7 +107,7 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) { } func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) { - for _, name := range []string{"run", "prompt", "conversation", "attachments", "authoring", "subagents", "workspace-missing", "none", "functions", "mcp", "controls", "old-runtime"} { + for _, name := range []string{"run", "prompt", "conversation", "attachments", "subagents", "workspace-missing", "none", "functions", "mcp", "controls", "old-runtime"} { t.Run(name, func(t *testing.T) { config := preparationFixture(t, name) req := preparationRequest() @@ -120,8 +120,6 @@ func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) req.ConversationID = "product" case "attachments": req.Input = proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image"}}}} - case "authoring": - req.WorkspaceAuthoring = true case "subagents": req.ObserveSubagentIdentities = true case "workspace-missing": diff --git a/apps/daemon/internal/agent/claudesdk/readiness.go b/apps/daemon/internal/agent/claudesdk/readiness.go index 5c9668990..a9f7f8fbf 100644 --- a/apps/daemon/internal/agent/claudesdk/readiness.go +++ b/apps/daemon/internal/agent/claudesdk/readiness.go @@ -73,16 +73,14 @@ func (info RuntimeInfo) supportsWorkspace() bool { return slices.Contains(info.Features, "workspace_tools") } +// Workspace execution always emits neutral command observations, so a bridge +// without them cannot run a prepared workspace. func (info RuntimeInfo) supportsWorkspacePreparation() bool { - return info.supportsWorkspace() && slices.Contains(info.Features, "workspace_prepare") -} - -func (info RuntimeInfo) supportsWorkspaceCommands() bool { - return info.supportsWorkspacePreparation() && slices.Contains(info.Features, "workspace_command_observations") + return info.supportsWorkspace() && slices.Contains(info.Features, "workspace_prepare") && slices.Contains(info.Features, "workspace_command_observations") } func (info RuntimeInfo) SupportsLocalRuntime() bool { - return info.supportsWorkspaceCommands() && slices.Contains(info.Features, "local_runtime_v2") + return info.supportsWorkspacePreparation() && slices.Contains(info.Features, "local_runtime_v2") } func (info RuntimeInfo) SupportsWorkspaceFunctions() bool { @@ -114,10 +112,10 @@ func CheckRuntime(ctx context.Context, config Config) (RuntimeInfo, error) { } process, err := clirunner.Start(clirunner.StartOptions{ Parent: ctx, Binary: binary, - Args: []string{filepath.Join(filepath.Dir(config.Entrypoint), "runtime_check.js"), config.Entrypoint}, - Dir: filepath.Dir(config.Entrypoint), - Env: env, - OwnProcessGroup: true, KillTimeout: 250 * time.Millisecond, + Args: []string{filepath.Join(filepath.Dir(config.Entrypoint), "runtime_check.js"), config.Entrypoint}, + Dir: filepath.Dir(config.Entrypoint), + Env: env, + KillTimeout: 250 * time.Millisecond, }) if err != nil { return RuntimeInfo{}, fmt.Errorf("claudesdk: cannot start runtime check: %w", err) diff --git a/apps/daemon/internal/agent/claudesdk/session.go b/apps/daemon/internal/agent/claudesdk/session.go index 311952092..7edaabb07 100644 --- a/apps/daemon/internal/agent/claudesdk/session.go +++ b/apps/daemon/internal/agent/claudesdk/session.go @@ -86,7 +86,7 @@ func launch(ctx context.Context, config Config, start startRequest, env []string if binary == "" { binary = "node" } - process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: []string{config.Entrypoint}, Dir: start.Cwd, Env: env, NeedStdin: true, OwnProcessGroup: true}) + process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: []string{config.Entrypoint}, Dir: start.Cwd, Env: env, NeedStdin: true}) if err != nil { return nil, err } diff --git a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go index dfaac5162..1f310776d 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_launch_test.go @@ -24,7 +24,7 @@ test "$ANTHROPIC_AUTH_TOKEN" = selected-provider-fixture || exit 23 test "$TMPDIR" != "$CLAUDE_CONFIG_DIR/tmp" || exit 24 case "$1" in */runtime_check.js) - printf '%s\n' '{"type":"runtime_ready","protocol":3,"node":"fixture","sdk":"fixture","mcp":"fixture","native":"fixture","features":["workspace_tools","workspace_prepare"]}' ;; + printf '%s\n' '{"type":"runtime_ready","protocol":3,"node":"fixture","sdk":"fixture","mcp":"fixture","native":"fixture","features":["workspace_tools","workspace_prepare","workspace_command_observations"]}' ;; *) IFS= read -r request printf '%s\n' '{"type":"executor_ready","protocol":3}' @@ -59,7 +59,7 @@ esac t.Fatal("expected one settled completion") } // Feature checking must reject an older bridge without starting execution. - script = strings.ReplaceAll(script, `"features":["workspace_tools","workspace_prepare"]`, `"features":[]`) + script = strings.ReplaceAll(script, `"features":["workspace_tools","workspace_prepare","workspace_command_observations"]`, `"features":[]`) script = strings.ReplaceAll(script, "IFS= read -r request", "touch '"+filepath.Join(config.StateDir, "unexpected-start")+"'") if err := os.WriteFile(config.Node, []byte(script), 0o700); err != nil { t.Fatal(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 6a19b6e53..ceca7f844 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_live_linux_test.go @@ -104,7 +104,7 @@ func testLiveClaudeWorkspace(t *testing.T, explicitPreparation bool) { out := make(chan proto.Envelope, 64) req := workspaceRequest() req.RunID, req.Input, req.AgentSessionID = uuid.NewString(), proto.TextInput(prompt), resume - req.StrictResume, req.ReleaseOnCompletion, req.ObserveMessages, req.ObserveToolObservations = true, true, true, true + req.ReleaseOnCompletion, req.ObserveMessages = true, true req.AgentOptions = map[string]any{"model": "MiniMax-M3", "system_prompt": "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} var running agent.Session diff --git a/apps/daemon/internal/agent/clirunner/process.go b/apps/daemon/internal/agent/clirunner/process.go index e693df7f5..a9a4465bd 100644 --- a/apps/daemon/internal/agent/clirunner/process.go +++ b/apps/daemon/internal/agent/clirunner/process.go @@ -5,9 +5,7 @@ import ( "fmt" "io" "os/exec" - "runtime" "sync" - "syscall" "time" ) @@ -19,8 +17,6 @@ type StartOptions struct { Env []string NeedStdin bool KillTimeout time.Duration - // OwnProcessGroup bounds the lifetime of subprocess descendants on Unix. - OwnProcessGroup bool } type Process struct { @@ -29,13 +25,11 @@ type Process struct { Stdout io.ReadCloser Stderr io.ReadCloser - ctx context.Context - cancel context.CancelFunc - done chan struct{} - killAfter time.Duration + ctx context.Context + cancel context.CancelFunc + done chan struct{} cancelOnce sync.Once - waitOnce sync.Once cancelProcess func() error waitProcess func() error } @@ -51,54 +45,9 @@ func Start(opts StartOptions) (*Process, error) { opts.KillTimeout = 3 * time.Second } - if opts.OwnProcessGroup || runtime.GOOS == "windows" { - return startProcessGroup(opts) - } - - ctx, cancel := context.WithCancel(opts.Parent) - cmd := exec.CommandContext(ctx, opts.Binary, opts.Args...) - cmd.Dir = opts.Dir - if len(opts.Env) > 0 { - cmd.Env = append([]string{}, opts.Env...) - } - - var stdin io.WriteCloser - var err error - if opts.NeedStdin { - stdin, err = cmd.StdinPipe() - if err != nil { - cancel() - return nil, fmt.Errorf("clirunner: stdin pipe: %w", err) - } - } - stdout, err := cmd.StdoutPipe() - if err != nil { - closePipe(stdin) - cancel() - return nil, fmt.Errorf("clirunner: stdout pipe: %w", err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - closePipe(stdin) - cancel() - return nil, fmt.Errorf("clirunner: stderr pipe: %w", err) - } - if err := cmd.Start(); err != nil { - closePipe(stdin) - cancel() - return nil, fmt.Errorf("clirunner: start %q: %w", opts.Binary, err) - } - - return &Process{ - Cmd: cmd, - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - ctx: ctx, - cancel: cancel, - done: make(chan struct{}), - killAfter: opts.KillTimeout, - }, nil + // Every child owns its process group (a Job object on Windows), bounding the + // lifetime of its descendants. + return startProcessGroup(opts) } func (p *Process) Context() context.Context { @@ -118,41 +67,20 @@ func (p *Process) Done() <-chan struct{} { } func (p *Process) Cancel() { - if p == nil { + if p == nil || p.cancelProcess == nil { return } p.cancelOnce.Do(func() { - if p.cancelProcess != nil { - _ = p.cancelProcess() - p.cancel() - return - } - if p.Cmd != nil && p.Cmd.Process != nil { - _ = p.Cmd.Process.Signal(syscall.SIGTERM) - go func() { - select { - case <-p.done: - case <-time.After(p.killAfter): - _ = p.Cmd.Process.Signal(syscall.SIGKILL) - } - }() - } - if p.cancel != nil { - p.cancel() - } + _ = p.cancelProcess() + p.cancel() }) } func (p *Process) Wait() error { - if p == nil || p.Cmd == nil { + if p == nil || p.waitProcess == nil { return nil } - if p.waitProcess != nil { - return p.waitProcess() - } - err := p.Cmd.Wait() - p.waitOnce.Do(func() { close(p.done) }) - return err + return p.waitProcess() } func closePipe(p io.Closer) { diff --git a/apps/daemon/internal/agent/clirunner/process_group_other.go b/apps/daemon/internal/agent/clirunner/process_group_other.go index d5763742b..796f8aa19 100644 --- a/apps/daemon/internal/agent/clirunner/process_group_other.go +++ b/apps/daemon/internal/agent/clirunner/process_group_other.go @@ -5,5 +5,5 @@ package clirunner import "errors" func startProcessGroup(StartOptions) (*Process, error) { - return nil, errors.New("clirunner: process-group ownership requires Unix") + return nil, errors.New("clirunner: process ownership requires Unix or Windows") } diff --git a/apps/daemon/internal/agent/clirunner/process_group_unix_test.go b/apps/daemon/internal/agent/clirunner/process_group_unix_test.go index 8a3cbf162..da3162be5 100644 --- a/apps/daemon/internal/agent/clirunner/process_group_unix_test.go +++ b/apps/daemon/internal/agent/clirunner/process_group_unix_test.go @@ -107,9 +107,9 @@ func startOwnedHelper(t *testing.T, ctx context.Context, mode, dir string) *Proc t.Helper() p, err := Start(StartOptions{ Parent: ctx, Binary: os.Args[0], - Args: []string{"-test.run=^TestOwnedGroupHelper$", "--", "leader", mode, dir}, - Env: append(os.Environ(), "GO_WANT_OWNED_GROUP=1", "GORACE=atexit_sleep_ms=0"), - OwnProcessGroup: true, KillTimeout: 300 * time.Millisecond, + Args: []string{"-test.run=^TestOwnedGroupHelper$", "--", "leader", mode, dir}, + Env: append(os.Environ(), "GO_WANT_OWNED_GROUP=1", "GORACE=atexit_sleep_ms=0"), + KillTimeout: 300 * time.Millisecond, }) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/clirunner/process_group_windows_test.go b/apps/daemon/internal/agent/clirunner/process_group_windows_test.go index b2d442b54..fee66f0f2 100644 --- a/apps/daemon/internal/agent/clirunner/process_group_windows_test.go +++ b/apps/daemon/internal/agent/clirunner/process_group_windows_test.go @@ -21,7 +21,7 @@ func TestWindowsOwnedTree(t *testing.T) { dir := t.TempDir() ctx, cancel := context.WithCancel(t.Context()) defer cancel() - p, err := Start(StartOptions{Parent: ctx, Binary: os.Args[0], Args: []string{"-test.run=^TestWindowsTreeHelper$", "--", "leader", mode, dir}, Env: append(os.Environ(), "OAC_TREE_HELPER=1"), OwnProcessGroup: true}) + p, err := Start(StartOptions{Parent: ctx, Binary: os.Args[0], Args: []string{"-test.run=^TestWindowsTreeHelper$", "--", "leader", mode, dir}, Env: append(os.Environ(), "OAC_TREE_HELPER=1")}) if err != nil { t.Fatal(err) } @@ -129,7 +129,7 @@ func TestWindowsTreeHelper(t *testing.T) { time.Sleep(10 * time.Millisecond) } case "owner": - p, err := Start(StartOptions{Parent: context.Background(), Binary: os.Args[0], Args: []string{"-test.run=^TestWindowsTreeHelper$", "--", "leader", mode, dir}, Env: os.Environ(), OwnProcessGroup: true}) + p, err := Start(StartOptions{Parent: context.Background(), Binary: os.Args[0], Args: []string{"-test.run=^TestWindowsTreeHelper$", "--", "leader", mode, dir}, Env: os.Environ()}) if err != nil { os.Exit(3) } diff --git a/apps/daemon/internal/agent/codex/declaration.go b/apps/daemon/internal/agent/codex/declaration.go index ea6bfab51..26bb61c4e 100644 --- a/apps/daemon/internal/agent/codex/declaration.go +++ b/apps/daemon/internal/agent/codex/declaration.go @@ -20,7 +20,6 @@ var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{ Usage: proto.CapabilitySupported, Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, - WorkspaceAuthoring: proto.CapabilitySupported, Steering: proto.CapabilitySupported, MessageItems: proto.CapabilitySupported, ToolObservations: proto.CapabilitySupported, diff --git a/apps/daemon/internal/agent/codex/declaration_test.go b/apps/daemon/internal/agent/codex/declaration_test.go index d3a364b97..b26acf131 100644 --- a/apps/daemon/internal/agent/codex/declaration_test.go +++ b/apps/daemon/internal/agent/codex/declaration_test.go @@ -29,7 +29,7 @@ func TestMCPRequiredDiscoveryRequiresPinnedNative(t *testing.T) { // The declaration must retain the complete baseline capability descriptor. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"SubagentObservations": true, "Streaming": true, "Permissions": true, "Usage": true, "Resume": true, "Steering": true, "MessageItems": true, "ToolObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "DurableInputReceipts": true, "DurableTurns": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true, "WorkspaceAuthoring": true} + expected := map[string]bool{"SubagentObservations": true, "Streaming": true, "Permissions": true, "Usage": true, "Resume": true, "Steering": true, "MessageItems": true, "ToolObservations": true, "EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "WebSearchControl": true, "MessageImages": true, "FunctionResultImages": true, "SubagentControl": true, "DurableInputReceipts": true, "DurableTurns": true, "FunctionTools": true, "MCPHTTPTools": true, "MCPHTTPBearerAuth": true} expected["ExecutionControls"], expected["TextVerbosity"] = SupportsTextVerbosity, SupportsTextVerbosity value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { diff --git a/apps/daemon/internal/agent/codex/environment.go b/apps/daemon/internal/agent/codex/environment.go index 51ba1f0f6..725e5ef58 100644 --- a/apps/daemon/internal/agent/codex/environment.go +++ b/apps/daemon/internal/agent/codex/environment.go @@ -7,8 +7,6 @@ import ( "fmt" "os" "strings" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // Check the native provider instead of assuming an older binary honors the flag. @@ -40,22 +38,16 @@ func nativeEnvironmentStatus(ctx context.Context, rpc *JSONRPCClient, id string) } // Native still recognizes the retired transport variables. Reject them before -// setup so inherited or operator options cannot select a separate executor. +// setup so the inherited environment cannot select a separate executor. // The explicit none selector remains part of native execution isolation. -func validateNativeTransportEnvironment(req proto.PromptRequestPayload) error { - options, err := buildSessionEnv(req.AgentOptions) - if err != nil { - return err - } - for _, environment := range [][]string{os.Environ(), options} { - for _, entry := range environment { - key, value, _ := strings.Cut(entry, "=") - if value == "" { - continue - } - if (key == "CODEX_EXEC_SERVER_URL" && value != "none") || strings.HasPrefix(key, "CODEX_EXEC_SERVER_NOISE_") { - return errors.New("codex: retired executor transport configuration is not supported") - } +func validateNativeTransportEnvironment() error { + for _, entry := range os.Environ() { + key, value, _ := strings.Cut(entry, "=") + if value == "" { + continue + } + if (key == "CODEX_EXEC_SERVER_URL" && value != "none") || strings.HasPrefix(key, "CODEX_EXEC_SERVER_NOISE_") { + return errors.New("codex: retired executor transport configuration is not supported") } } return nil diff --git a/apps/daemon/internal/agent/codex/environment_retired_test.go b/apps/daemon/internal/agent/codex/environment_retired_test.go index 08dab1f6b..f3e070843 100644 --- a/apps/daemon/internal/agent/codex/environment_retired_test.go +++ b/apps/daemon/internal/agent/codex/environment_retired_test.go @@ -24,33 +24,27 @@ func TestReadOnlyPreparationRejectedBeforeNativeSetup(t *testing.T) { } } -func TestRetiredNativeTransportOptionsRejectedBeforeState(t *testing.T) { +func TestRetiredNativeTransportEnvironmentRejectedBeforeState(t *testing.T) { for _, key := range []string{"CODEX_EXEC_SERVER_URL", "CODEX_EXEC_SERVER_NOISE_REGISTRY_URL", "CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID", "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN"} { - for _, source := range []string{"process", "options"} { - for _, none := range []bool{false, true} { - t.Run(key+"/"+source+"/"+map[bool]string{false: "local", true: "none"}[none], func(t *testing.T) { - req, cfg, root := preparationFixture(t) - req.DisableExecutionEnvironment = none - if !none { - req.LocalEnvironment = &proto.LocalEnvironment{ID: "local"} - } - if source == "process" { - t.Setenv(key, "retired-private-value") - } else { - req.AgentOptions["env"] = map[string]any{key: "retired-private-value"} - } - p, err := newPreparation(t.Context(), req, cfg) - if p != nil || err == nil || !strings.Contains(err.Error(), "retired executor transport") || strings.Contains(err.Error(), "retired-private-value") { - t.Fatal("transport override admitted or disclosed", err) - } - if len(preparationFrames(t, root)) != 0 { - t.Fatal("retired transport started native process") - } - if _, err := os.Stat(filepath.Join(root, "daemon", "agent-sessions")); !os.IsNotExist(err) { - t.Fatal("retired transport created state", err) - } - }) - } + for _, none := range []bool{false, true} { + t.Run(key+"/"+map[bool]string{false: "local", true: "none"}[none], func(t *testing.T) { + req, cfg, root := preparationFixture(t) + req.DisableExecutionEnvironment = none + if !none { + req.LocalEnvironment = &proto.LocalEnvironment{ID: "local"} + } + t.Setenv(key, "retired-private-value") + p, err := newPreparation(t.Context(), req, cfg) + if p != nil || err == nil || !strings.Contains(err.Error(), "retired executor transport") || strings.Contains(err.Error(), "retired-private-value") { + t.Fatal("transport override admitted or disclosed", err) + } + if len(preparationFrames(t, root)) != 0 { + t.Fatal("retired transport started native process") + } + if _, err := os.Stat(filepath.Join(root, "daemon", "agent-sessions")); !os.IsNotExist(err) { + t.Fatal("retired transport created state", err) + } + }) } } } @@ -58,7 +52,6 @@ func TestRetiredNativeTransportOptionsRejectedBeforeState(t *testing.T) { func TestNativeNoneSelectorRemainsSupported(t *testing.T) { req, cfg, root := preparationFixture(t) t.Setenv("CODEX_EXEC_SERVER_URL", "none") - req.AgentOptions["env"] = map[string]any{"CODEX_EXEC_SERVER_URL": "none"} p, err := newPreparation(t.Context(), req, cfg) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/codex/executor.go b/apps/daemon/internal/agent/codex/executor.go index 2f1e2eb09..75403c57d 100644 --- a/apps/daemon/internal/agent/codex/executor.go +++ b/apps/daemon/internal/agent/codex/executor.go @@ -69,8 +69,7 @@ func (e *Executor) StartTurn(ctx context.Context, runID string, input proto.Mess functions := &functionCalls{definitions: base.functions.definitions, names: base.functions.names, pending: map[string]*pendingFunction{}} turnCtx, cancel := context.WithCancel(base.cancelCtx) s := &Session{executor: e, nativeHome: base.nativeHome, - functions: functions, observeMessages: base.observeMessages, - observeToolObservations: base.observeToolObservations, observeSubagentIdentities: base.observeSubagentIdentities, + functions: functions, observeMessages: base.observeMessages, observeSubagentIdentities: base.observeSubagentIdentities, cfg: base.cfg, rpc: base.rpc, cancelCtx: turnCtx, cancelFn: cancel, waitDone: make(chan struct{}), outputDone: make(chan struct{}), cleanup: func() {}, bufs: NewItemBuffers(), resolvedModel: base.resolvedModel, interactions: newPendingCodexInteractions(), runID: runID, out: out} @@ -94,7 +93,7 @@ func (e *Executor) StartTurn(ctx context.Context, runID string, input proto.Mess } s.registerHandlers() e.mu.Unlock() - req := proto.PromptRequestPayload{RunID: runID, Input: input, AgentSessionID: e.prepared.resumeID, StrictResume: e.prepared.strictResume, RequireExistingNativeSession: e.prepared.requireExistingNativeSession} + req := proto.PromptRequestPayload{RunID: runID, Input: input, AgentSessionID: e.prepared.resumeID, RequireExistingNativeSession: e.prepared.requireExistingNativeSession} // Ownership precedes any native submission. Even an uncertain start returns the // exact Turn so its caller can await settlement without replaying the input. err := s.startNative(ctx, e.prepared.plan, req) diff --git a/apps/daemon/internal/agent/codex/executor_native_test.go b/apps/daemon/internal/agent/codex/executor_native_test.go index 227ad311c..247e6b083 100644 --- a/apps/daemon/internal/agent/codex/executor_native_test.go +++ b/apps/daemon/internal/agent/codex/executor_native_test.go @@ -50,7 +50,7 @@ func TestExecutorNativeReuse(t *testing.T) { cfg.logger = obslog.Discard() req := proto.PromptRequestPayload{ AgentKind: "codex", AgentStateKey: "executor-native", - StrictResume: true, DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, + DisableExecutionEnvironment: true, DisableSubagents: true, ObserveMessages: true, AgentOptions: map[string]any{"model": model, "model_provider": map[string]any{"base_url": endpoint, "protocol": "responses", "api_key": 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}")}}, } diff --git a/apps/daemon/internal/agent/codex/executor_test.go b/apps/daemon/internal/agent/codex/executor_test.go index 01245f944..f864d5899 100644 --- a/apps/daemon/internal/agent/codex/executor_test.go +++ b/apps/daemon/internal/agent/codex/executor_test.go @@ -165,7 +165,7 @@ func TestExecutorStartErrorsRetainExactOwnership(t *testing.T) { } func TestExecutorCloseRetainsPlanUntilReaped(t *testing.T) { - process, err := clirunner.Start(clirunner.StartOptions{Parent: t.Context(), Binary: os.Args[0], Args: []string{"-test.run=^TestJSONRPCClientFakeCodexProcess$", "--"}, Env: append(os.Environ(), "CODEX_RPC_FAKE_PROCESS=1", "GORACE=atexit_sleep_ms=0"), NeedStdin: true, OwnProcessGroup: true}) + process, err := clirunner.Start(clirunner.StartOptions{Parent: t.Context(), Binary: os.Args[0], Args: []string{"-test.run=^TestJSONRPCClientFakeCodexProcess$", "--"}, Env: append(os.Environ(), "CODEX_RPC_FAKE_PROCESS=1", "GORACE=atexit_sleep_ms=0"), NeedStdin: true}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/mcp_required_test.go b/apps/daemon/internal/agent/codex/mcp_required_test.go index bd4bbec2b..44b106a43 100644 --- a/apps/daemon/internal/agent/codex/mcp_required_test.go +++ b/apps/daemon/internal/agent/codex/mcp_required_test.go @@ -17,7 +17,6 @@ func TestRequiredMCPWaitsForNativeThreadAndNeverRestartsFailedResume(t *testing. for _, mode := range []string{"new ready", "new failed", "resume ready", "resume failed"} { t.Run(mode, func(t *testing.T) { req, cfg, root := preparationFixture(t) - req.StrictResume = true req.AgentOptions = map[string]any{"model": "fixture-model"} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "docs", ServerURL: "https://docs.example/mcp", Required: true}} req.MCPHTTPServers = &servers diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 2e0457cb2..4b7b2d2d2 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "slices" - "sort" "strings" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/paths" @@ -74,8 +73,6 @@ type SessionPlan struct { // system_prompt string forwarded as developerInstructions // model_provider object frozen upstream protocol, endpoint, credential and token limits // harness_config object native parameters accepted by harnessconfig/codex -// env object extra env vars (KEY=string-value); the Runtime's -// authoring bridge supplies it // // Execution controls select the native web_search and model_verbosity settings. // The codex binary itself is resolved via PATH only. @@ -109,11 +106,6 @@ func BuildSessionPlan(agentStateKey string, opts map[string]any, controls *proto plan.Model = stringOpt(opts, "model") plan.SystemPrompt = stringOpt(opts, "system_prompt") - env, err := buildSessionEnv(opts) - if err != nil { - return plan, err - } - codexHome, err := allocCodexHome(agentStateKey) if err != nil { return plan, err @@ -121,7 +113,7 @@ func BuildSessionPlan(agentStateKey string, opts map[string]any, controls *proto if err := resetGeneratedConfig(codexHome); err != nil { return plan, err } - env = append(env, "CODEX_HOME="+codexHome) + env := []string{"DISABLE_TELEMETRY=1", "CODEX_HOME=" + codexHome} provider, hasProvider, err := normaliseProviderConfig(opts["model_provider"]) if err != nil { @@ -203,33 +195,6 @@ func resetGeneratedConfig(codexHome string) error { return nil } -func buildSessionEnv(opts map[string]any) ([]string, error) { - env := []string{ - "DISABLE_TELEMETRY=1", - } - raw, ok := opts["env"] - if !ok || raw == nil { - return env, nil - } - envMap, ok := raw.(map[string]any) - if !ok { - return nil, fmt.Errorf("codex.BuildSessionPlan: env must be object, got %T", raw) - } - keys := make([]string, 0, len(envMap)) - for k := range envMap { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - s, ok := envMap[k].(string) - if !ok { - return nil, fmt.Errorf("codex.BuildSessionPlan: env[%q] must be string, got %T", k, envMap[k]) - } - env = append(env, k+"="+s) - } - return env, nil -} - // normaliseProviderConfig validates and renders the frozen native provider. func normaliseProviderConfig(raw any) (providerConfig, bool, error) { if raw == nil { diff --git a/apps/daemon/internal/agent/codex/options_test.go b/apps/daemon/internal/agent/codex/options_test.go index f5fec2fdb..b20123e2b 100644 --- a/apps/daemon/internal/agent/codex/options_test.go +++ b/apps/daemon/internal/agent/codex/options_test.go @@ -24,24 +24,17 @@ func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { } func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { - plan, err := BuildSessionPlan("conv-1/agent-1/codex", map[string]any{ - "env": map[string]any{ - "OPENAI_API_KEY": "sk-test", - }, - }, nil) + plan, err := BuildSessionPlan("conv-1/agent-1/codex", nil, nil) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } defer plan.Cleanup() hasCodexHome := false - hasOpenAI := false hasTelemetry := false for _, kv := range plan.Env { switch { case strings.HasPrefix(kv, "CODEX_HOME="): hasCodexHome = true - case kv == "OPENAI_API_KEY=sk-test": - hasOpenAI = true case kv == "DISABLE_TELEMETRY=1": hasTelemetry = true } @@ -49,9 +42,6 @@ func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { if !hasCodexHome { t.Fatalf("env missing CODEX_HOME: %+v", plan.Env) } - if !hasOpenAI { - t.Fatalf("env missing OPENAI_API_KEY: %+v", plan.Env) - } if !hasTelemetry { t.Fatalf("env missing DISABLE_TELEMETRY: %+v", plan.Env) } diff --git a/apps/daemon/internal/agent/codex/preparation.go b/apps/daemon/internal/agent/codex/preparation.go index 663f060d2..0d3a4bdf9 100644 --- a/apps/daemon/internal/agent/codex/preparation.go +++ b/apps/daemon/internal/agent/codex/preparation.go @@ -25,7 +25,6 @@ func newSession(parent context.Context, req proto.PromptRequestPayload, out chan return nil, errors.New("codex: nil out channel") } runID, prompt := req.RunID, req.Input - req.AgentStateKey = effectiveAgentStateKey(req) req.RunID, req.Input = "", nil prepared, err := newPreparation(parent, req, cfg) if err != nil { @@ -42,9 +41,6 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg if req.WorkspaceReadOnly { return nil, errors.New("codex: workspace reads use the local Runtime interface") } - if req.RequireExistingNativeSession && (!req.StrictResume || req.AgentStateKey == "" || req.WorkspaceReadOnly) { - return nil, errors.New("codex: native-session recovery requires strict private state") - } if req.RunID != "" || len(req.Input) != 0 { return nil, errors.New("codex: preparation does not accept a run identity or prompt") } @@ -61,8 +57,6 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg if err != nil { return nil, err } - req.AgentStateKey = effectiveAgentStateKey(req) - plan, skillRoots, err := prepareSessionPlan(parent, req, cfg) if err != nil { return nil, err @@ -86,11 +80,9 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg rpc := NewJSONRPCClient(rpcCfg) s := &Session{ - nativeHome: nativeHomeFromPlan(plan), - functions: functions, - observeMessages: req.ObserveMessages, - - observeToolObservations: req.ObserveToolObservations, + nativeHome: nativeHomeFromPlan(plan), + functions: functions, + observeMessages: req.ObserveMessages, observeSubagentIdentities: req.ObserveSubagentIdentities && !req.DisableSubagents, cfg: cfg, rpc: rpc, @@ -105,7 +97,7 @@ func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg plan.Cleanup = s.cleanup p := &Prepared{ session: s, plan: plan, - resumeID: req.AgentSessionID, strictResume: req.StrictResume, requireExistingNativeSession: req.RequireExistingNativeSession, + resumeID: req.AgentSessionID, requireExistingNativeSession: req.RequireExistingNativeSession, transferred: make(chan struct{}), } diff --git a/apps/daemon/internal/agent/codex/preparation_helpers_test.go b/apps/daemon/internal/agent/codex/preparation_helpers_test.go index 65625bcec..a04fe665c 100644 --- a/apps/daemon/internal/agent/codex/preparation_helpers_test.go +++ b/apps/daemon/internal/agent/codex/preparation_helpers_test.go @@ -42,7 +42,7 @@ func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig cfg.codexBinary = binary req := proto.PromptRequestPayload{ AgentKind: "codex", AgentStateKey: "prepared-session", - ReleaseOnCompletion: true, StrictResume: true, + ReleaseOnCompletion: true, AgentOptions: map[string]any{"model": "fixture-model"}, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, DisableExecutionEnvironment: true, diff --git a/apps/daemon/internal/agent/codex/prepared.go b/apps/daemon/internal/agent/codex/prepared.go index 454b71976..1f4e049f0 100644 --- a/apps/daemon/internal/agent/codex/prepared.go +++ b/apps/daemon/internal/agent/codex/prepared.go @@ -17,7 +17,6 @@ type Prepared struct { session *Session plan SessionPlan resumeID string - strictResume bool requireExistingNativeSession bool claimed bool closed bool @@ -70,7 +69,7 @@ func (p *Prepared) start(ctx context.Context, runID string, prompt proto.Message p.started = true close(p.transferred) transferred = true - req := proto.PromptRequestPayload{RunID: runID, Input: prompt, AgentSessionID: p.resumeID, StrictResume: p.strictResume, RequireExistingNativeSession: p.requireExistingNativeSession} + req := proto.PromptRequestPayload{RunID: runID, Input: prompt, AgentSessionID: p.resumeID, RequireExistingNativeSession: p.requireExistingNativeSession} go s.run(p.plan, req) return s, nil } diff --git a/apps/daemon/internal/agent/codex/recovery_test.go b/apps/daemon/internal/agent/codex/recovery_test.go index da32ad87e..78aad7c16 100644 --- a/apps/daemon/internal/agent/codex/recovery_test.go +++ b/apps/daemon/internal/agent/codex/recovery_test.go @@ -46,7 +46,7 @@ func TestRequiredHistoryResolution(t *testing.T) { case "wrong-home": row["path"] = "/another/sessions/rollout.jsonl" } - req := proto.PromptRequestPayload{StrictResume: true, RequireExistingNativeSession: true} + req := proto.PromptRequestPayload{RequireExistingNativeSession: true} if scenario == "fresh" { req.RequireExistingNativeSession = false } @@ -196,14 +196,12 @@ func TestPreparedRecoveryCannotStartWithoutExistingHistory(t *testing.T) { } } -func TestRecoveryRequiresStrictPrivateExecution(t *testing.T) { - for _, mode := range []string{"non-strict", "no-state", "read-only"} { +func TestRecoveryRequiresWritableAgentState(t *testing.T) { + for _, mode := range []string{"no-state", "read-only"} { t.Run(mode, func(t *testing.T) { req, cfg, root := preparationFixture(t) req.RequireExistingNativeSession = true switch mode { - case "non-strict": - req.StrictResume = false case "no-state": req.AgentStateKey = "" case "read-only": diff --git a/apps/daemon/internal/agent/codex/resume.go b/apps/daemon/internal/agent/codex/resume.go index 6ee852b1c..2bc3172e1 100644 --- a/apps/daemon/internal/agent/codex/resume.go +++ b/apps/daemon/internal/agent/codex/resume.go @@ -9,18 +9,12 @@ import ( func (s *Session) resolveThread(req proto.PromptRequestPayload, plan SessionPlan) error { if strings.TrimSpace(req.AgentSessionID) != "" { - if err := s.resumeThread(req.AgentSessionID, plan); err == nil { - return nil - } else if req.StrictResume { + if err := s.resumeThread(req.AgentSessionID, plan); err != nil { return fmt.Errorf("codex: thread/resume: %w", err) - } else { - s.cfg.logger.Warn("codex: thread/resume failed; starting fresh", "run_id", s.runID, "thread_id", req.AgentSessionID, "err", err) } + return nil } if req.RequireExistingNativeSession { - if !req.StrictResume { - return fmt.Errorf("codex: recovery requires strict resume") - } id, err := s.recoverRoot(plan) if err != nil { return err diff --git a/apps/daemon/internal/agent/codex/resume_test.go b/apps/daemon/internal/agent/codex/resume_test.go index 1da859d76..698eb2cc7 100644 --- a/apps/daemon/internal/agent/codex/resume_test.go +++ b/apps/daemon/internal/agent/codex/resume_test.go @@ -10,53 +10,38 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" ) -func TestStrictResumeDoesNotStartFresh(t *testing.T) { - for _, strict := range []bool{false, true} { - t.Run(map[bool]string{false: "legacy", true: "strict"}[strict], func(t *testing.T) { - client, server, cleanup := NewTestClient() - defer cleanup() - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - s := &Session{rpc: client.JSONRPCClient, cancelCtx: ctx, cfg: sessionConfig{logger: log.With("component", "resume-test")}} - result := make(chan error, 1) - go func() { - result <- s.resolveThread(proto.PromptRequestPayload{AgentSessionID: "existing", StrictResume: strict}, SessionPlan{}) - }() - var req struct { - ID string `json:"id"` - Method string `json:"method"` - } - decoder := json.NewDecoder(server.FromClient) - encoder := json.NewEncoder(server.ToClient) - if err := decoder.Decode(&req); err != nil { - t.Fatal(err) - } - if req.Method != "thread/resume" { - t.Fatal(req.Method) - } - if err := encoder.Encode(map[string]any{"id": req.ID, "error": map[string]any{"code": -32600, "message": "native history unavailable"}}); err != nil { - t.Fatal(err) - } - if !strict { - if err := decoder.Decode(&req); err != nil { - t.Fatal(err) - } - if req.Method != "thread/start" { - t.Fatal(req.Method) - } - if err := encoder.Encode(map[string]any{"id": req.ID, "result": map[string]any{"thread": map[string]string{"id": "fresh"}}}); err != nil { - t.Fatal(err) - } - } - select { - case err := <-result: - if (err != nil) != strict { - t.Fatalf("strict=%v err=%v", strict, err) - } - case <-ctx.Done(): - t.Fatal("thread resolution did not finish") - } - }) +func TestFailedResumeDoesNotStartFresh(t *testing.T) { + client, server, cleanup := NewTestClient() + defer cleanup() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + s := &Session{rpc: client.JSONRPCClient, cancelCtx: ctx, cfg: sessionConfig{logger: log.With("component", "resume-test")}} + result := make(chan error, 1) + go func() { + result <- s.resolveThread(proto.PromptRequestPayload{AgentSessionID: "existing"}, SessionPlan{}) + }() + var req struct { + ID string `json:"id"` + Method string `json:"method"` + } + decoder := json.NewDecoder(server.FromClient) + encoder := json.NewEncoder(server.ToClient) + if err := decoder.Decode(&req); err != nil { + t.Fatal(err) + } + if req.Method != "thread/resume" { + t.Fatal(req.Method) + } + if err := encoder.Encode(map[string]any{"id": req.ID, "error": map[string]any{"code": -32600, "message": "native history unavailable"}}); err != nil { + t.Fatal(err) + } + select { + case err := <-result: + if err == nil { + t.Fatal("failed resume started a fresh thread") + } + case <-ctx.Done(): + t.Fatal("thread resolution did not finish") } } diff --git a/apps/daemon/internal/agent/codex/rpc.go b/apps/daemon/internal/agent/codex/rpc.go index 2102eda9e..78607a12c 100644 --- a/apps/daemon/internal/agent/codex/rpc.go +++ b/apps/daemon/internal/agent/codex/rpc.go @@ -182,7 +182,7 @@ func (c *JSONRPCClient) Start(ctx context.Context, init InitializeParams) (Initi process, err := clirunner.Start(clirunner.StartOptions{ Parent: ctx, Binary: c.cfg.Binary, Args: args, Dir: c.cfg.Cwd, Env: c.cfg.Env, - NeedStdin: true, OwnProcessGroup: true, KillTimeout: 250 * time.Millisecond, + NeedStdin: true, KillTimeout: 250 * time.Millisecond, }) if err != nil { return InitializeResult{}, fmt.Errorf("codex rpc: spawn %q: %w", c.cfg.Binary, err) diff --git a/apps/daemon/internal/agent/codex/rpc_close_test.go b/apps/daemon/internal/agent/codex/rpc_close_test.go index 80e404a85..c7b345d33 100644 --- a/apps/daemon/internal/agent/codex/rpc_close_test.go +++ b/apps/daemon/internal/agent/codex/rpc_close_test.go @@ -15,7 +15,7 @@ import ( ) func TestJSONRPCClientCloseCanRetryUnreapedChild(t *testing.T) { - process, err := clirunner.Start(clirunner.StartOptions{Parent: t.Context(), Binary: os.Args[0], Args: []string{"-test.run=^TestJSONRPCClientFakeCodexProcess$", "--"}, Env: append(os.Environ(), "CODEX_RPC_FAKE_PROCESS=1", "GORACE=atexit_sleep_ms=0"), NeedStdin: true, OwnProcessGroup: true}) + process, err := clirunner.Start(clirunner.StartOptions{Parent: t.Context(), Binary: os.Args[0], Args: []string{"-test.run=^TestJSONRPCClientFakeCodexProcess$", "--"}, Env: append(os.Environ(), "CODEX_RPC_FAKE_PROCESS=1", "GORACE=atexit_sleep_ms=0"), NeedStdin: true}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/session.go b/apps/daemon/internal/agent/codex/session.go index 9b8ce23b0..f2bc6333f 100644 --- a/apps/daemon/internal/agent/codex/session.go +++ b/apps/daemon/internal/agent/codex/session.go @@ -70,11 +70,10 @@ type Session struct { functions *functionCalls observeMessages bool - observeToolObservations bool - runID string - cfg sessionConfig - out chan<- proto.Envelope - rpc *JSONRPCClient + runID string + cfg sessionConfig + out chan<- proto.Envelope + rpc *JSONRPCClient cancelCtx context.Context cancelFn context.CancelFunc diff --git a/apps/daemon/internal/agent/codex/session_command_output.go b/apps/daemon/internal/agent/codex/session_command_output.go index 600e4fdad..1d5cd9f92 100644 --- a/apps/daemon/internal/agent/codex/session_command_output.go +++ b/apps/daemon/internal/agent/codex/session_command_output.go @@ -7,9 +7,6 @@ import ( ) func (s *Session) onCommandOutput(raw json.RawMessage) { - if !s.observeToolObservations { - return - } var p AgentMessageDeltaNotification if json.Unmarshal(raw, &p) != nil || !s.isRootTurn(p.ThreadID, p.TurnID) || p.ItemID == "" || p.Delta == "" { return diff --git a/apps/daemon/internal/agent/codex/session_command_output_test.go b/apps/daemon/internal/agent/codex/session_command_output_test.go index 4f86ccbd0..a51fb6694 100644 --- a/apps/daemon/internal/agent/codex/session_command_output_test.go +++ b/apps/daemon/internal/agent/codex/session_command_output_test.go @@ -8,42 +8,34 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func TestCommandOutputRequiresOptInAndRootTurn(t *testing.T) { - for _, enabled := range []bool{false, true} { - out := make(chan proto.Envelope, 10) - s := &Session{runID: "run", out: out, cancelCtx: context.Background(), cfg: defaultSessionConfig(), rpc: NewJSONRPCClient(JSONRPCConfig{}), observeToolObservations: enabled} - s.registerHandlers() - s.setThreadID("root") - s.beginRootTurn("root", "turn") - for _, raw := range []string{ - `{"threadId":"child","turnId":"turn","itemId":"cmd","delta":"foreign"}`, - `{"threadId":"root","turnId":"old","itemId":"cmd","delta":"foreign"}`, - `{"threadId":"root","turnId":"turn","delta":"missing identity"}`, - `{"threadId":"root","turnId":"turn","itemId":"cmd","delta":null}`, - `{"threadId":42}`, `{}`, - } { - scopeNotification(t, s, "item/commandExecution/outputDelta", raw) - } - if len(out) != 0 { - t.Fatal("invalid output reached root") +func TestCommandOutputRequiresRootTurn(t *testing.T) { + out := make(chan proto.Envelope, 10) + s := &Session{runID: "run", out: out, cancelCtx: context.Background(), cfg: defaultSessionConfig(), rpc: NewJSONRPCClient(JSONRPCConfig{})} + s.registerHandlers() + s.setThreadID("root") + s.beginRootTurn("root", "turn") + for _, raw := range []string{ + `{"threadId":"child","turnId":"turn","itemId":"cmd","delta":"foreign"}`, + `{"threadId":"root","turnId":"old","itemId":"cmd","delta":"foreign"}`, + `{"threadId":"root","turnId":"turn","delta":"missing identity"}`, + `{"threadId":"root","turnId":"turn","itemId":"cmd","delta":null}`, + `{"threadId":42}`, `{}`, + } { + scopeNotification(t, s, "item/commandExecution/outputDelta", raw) + } + if len(out) != 0 { + t.Fatal("invalid output reached root") + } + for _, fragment := range []string{"same\n", "same\n", "结束\n"} { + raw, _ := json.Marshal(map[string]string{"threadId": "root", "turnId": "turn", "itemId": "cmd", "delta": fragment}) + scopeNotification(t, s, "item/commandExecution/outputDelta", string(raw)) + if len(out) != 1 { + t.Fatal("missing registered command notification") } - for _, fragment := range []string{"same\n", "same\n", "结束\n"} { - raw, _ := json.Marshal(map[string]string{"threadId": "root", "turnId": "turn", "itemId": "cmd", "delta": fragment}) - scopeNotification(t, s, "item/commandExecution/outputDelta", string(raw)) - if !enabled { - if len(out) != 0 { - t.Fatal("product frame sequence changed") - } - continue - } - if len(out) != 1 { - t.Fatal("missing registered command notification") - } - env := <-out - var p proto.CommandOutputPayload - if env.DecodePayload(&p) != nil || env.Type != proto.TypeCommandOutput || env.ID != "run" || p.ID != "cmd" || p.Delta != fragment { - t.Fatal("command fragment changed", env) - } + env := <-out + var p proto.CommandOutputPayload + if env.DecodePayload(&p) != nil || env.Type != proto.TypeCommandOutput || env.ID != "run" || p.ID != "cmd" || p.Delta != fragment { + t.Fatal("command fragment changed", env) } } } diff --git a/apps/daemon/internal/agent/codex/session_notifications_test.go b/apps/daemon/internal/agent/codex/session_notifications_test.go index 0df73e0e1..0ee2a9b94 100644 --- a/apps/daemon/internal/agent/codex/session_notifications_test.go +++ b/apps/daemon/internal/agent/codex/session_notifications_test.go @@ -13,8 +13,7 @@ func TestRootNotificationIsolation(t *testing.T) { t.Run(map[bool]string{false: "child without thread started", true: "child thread started"}[childStarted], func(t *testing.T) { out := make(chan proto.Envelope, 64) s := &Session{runID: "run", out: out, cancelCtx: context.Background(), cfg: defaultSessionConfig(), - rpc: NewJSONRPCClient(JSONRPCConfig{}), bufs: NewItemBuffers(), observeMessages: true, - observeToolObservations: true} + rpc: NewJSONRPCClient(JSONRPCConfig{}), bufs: NewItemBuffers(), observeMessages: true} s.registerHandlers() s.setThreadID("root") notify := func(method, params string) { t.Helper(); scopeNotification(t, s, method, params) } diff --git a/apps/daemon/internal/agent/codex/session_plan.go b/apps/daemon/internal/agent/codex/session_plan.go index 216b6717a..6d8ae5d67 100644 --- a/apps/daemon/internal/agent/codex/session_plan.go +++ b/apps/daemon/internal/agent/codex/session_plan.go @@ -10,7 +10,7 @@ import ( ) func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (SessionPlan, []string, error) { - if err := validateNativeTransportEnvironment(req); err != nil { + if err := validateNativeTransportEnvironment(); err != nil { return SessionPlan{}, nil, err } if err := validatePermissionProfile(req); err != nil { diff --git a/apps/daemon/internal/agent/codex/session_tools.go b/apps/daemon/internal/agent/codex/session_tools.go index 5a40b45c3..d1d4b4c94 100644 --- a/apps/daemon/internal/agent/codex/session_tools.go +++ b/apps/daemon/internal/agent/codex/session_tools.go @@ -10,13 +10,11 @@ func (s *Session) sendItemEvents(events []proto.Envelope, notification json.RawM var native struct { Item json.RawMessage `json:"item"` } - if s.observeToolObservations { - if err := json.Unmarshal(notification, &native); err != nil { - return - } + if err := json.Unmarshal(notification, &native); err != nil { + return } for _, event := range events { - if (s.observeToolObservations) && event.Type == proto.TypeToolCall { + if event.Type == proto.TypeToolCall { var tool proto.ToolCallPayload if err := event.DecodePayload(&tool); err != nil { return diff --git a/apps/daemon/internal/agent/codex/session_tools_test.go b/apps/daemon/internal/agent/codex/session_tools_test.go index 2eff526bb..8d4e06c8d 100644 --- a/apps/daemon/internal/agent/codex/session_tools_test.go +++ b/apps/daemon/internal/agent/codex/session_tools_test.go @@ -21,47 +21,39 @@ func toolSnapshotFixtures() []string { } } -func TestToolObservationsOnlyWhenRequested(t *testing.T) { - for _, enabled := range []bool{false, true} { - for _, item := range toolSnapshotFixtures() { - var source struct{ ID string } - if err := json.Unmarshal([]byte(item), &source); err != nil { - t.Fatal(err) +func TestToolObservations(t *testing.T) { + for _, item := range toolSnapshotFixtures() { + var source struct{ ID string } + if err := json.Unmarshal([]byte(item), &source); err != nil { + t.Fatal(err) + } + t.Run(source.ID, func(t *testing.T) { + out := make(chan proto.Envelope, 4) + s := &Session{runID: "run", out: out, cancelCtx: context.Background(), bufs: NewItemBuffers(), cfg: defaultSessionConfig()} + s.setThreadID("private-thread") + s.onTurnStarted(json.RawMessage(`{"threadId":"private-thread","turn":{"id":"private-turn"}}`)) + raw := json.RawMessage(`{"threadId":"private-thread","turnId":"private-turn","item":` + item + `}`) + s.onItemStarted(raw) + s.onItemCompleted(raw) + if len(out) != 2 { + t.Fatalf("tool event count changed: %d", len(out)) } - t.Run(source.ID, func(t *testing.T) { - out := make(chan proto.Envelope, 4) - s := &Session{runID: "run", observeToolObservations: enabled, out: out, cancelCtx: context.Background(), bufs: NewItemBuffers(), cfg: defaultSessionConfig()} - s.setThreadID("private-thread") - s.onTurnStarted(json.RawMessage(`{"threadId":"private-thread","turn":{"id":"private-turn"}}`)) - raw := json.RawMessage(`{"threadId":"private-thread","turnId":"private-turn","item":` + item + `}`) - s.onItemStarted(raw) - s.onItemCompleted(raw) - if len(out) != 2 { - t.Fatalf("tool event count changed: %d", len(out)) + for _, stage := range []string{"before", "after"} { + event := <-out + var tool proto.ToolCallPayload + if event.DecodePayload(&tool) != nil || event.Type != proto.TypeToolCall || tool.ID != source.ID || tool.Stage != stage { + t.Fatal(event) } - for _, stage := range []string{"before", "after"} { - event := <-out - var tool proto.ToolCallPayload - if event.DecodePayload(&tool) != nil || event.Type != proto.TypeToolCall || tool.ID != source.ID || tool.Stage != stage { - t.Fatal(event) - } - if bytes.Contains(event.Payload, []byte("native_item")) { - t.Fatal("engine-specific snapshot escaped adapter") - } - if !enabled { - if tool.Observation != nil { - t.Fatal("unrequested observation") - } - continue - } - if tool.Observation == nil || stage == "before" && tool.Observation.Status != "in_progress" { - t.Fatal(tool.Observation) - } - if source.ID == "mcp" && !bytes.Contains(tool.Observation.Output, []byte("9007199254740993")) { - t.Fatal("structured output precision lost") - } + if bytes.Contains(event.Payload, []byte("native_item")) { + t.Fatal("engine-specific snapshot escaped adapter") } - }) - } + if tool.Observation == nil || stage == "before" && tool.Observation.Status != "in_progress" { + t.Fatal(tool.Observation) + } + if source.ID == "mcp" && !bytes.Contains(tool.Observation.Output, []byte("9007199254740993")) { + t.Fatal("structured output precision lost") + } + } + }) } } diff --git a/apps/daemon/internal/agent/codex/skills.go b/apps/daemon/internal/agent/codex/skills.go index 69994f5ee..f7bdfe04a 100644 --- a/apps/daemon/internal/agent/codex/skills.go +++ b/apps/daemon/internal/agent/codex/skills.go @@ -1,24 +1,6 @@ package codex -import ( - "context" - "strings" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func effectiveAgentStateKey(req proto.PromptRequestPayload) string { - if strings.TrimSpace(req.AgentStateKey) != "" { - return req.AgentStateKey - } - if id := strings.TrimSpace(req.ConversationID); id != "" { - return "_legacy_conversation/" + id + "/codex" - } - if id := strings.TrimSpace(req.RunID); id != "" { - return "_legacy_run/" + id + "/codex" - } - return "" -} +import "context" func setSkillExtraRoots(ctx context.Context, rpc *JSONRPCClient, roots []string) error { if len(roots) == 0 { diff --git a/apps/daemon/internal/agent/codex/skills_test.go b/apps/daemon/internal/agent/codex/skills_test.go index 1f90c574b..bc316eed9 100644 --- a/apps/daemon/internal/agent/codex/skills_test.go +++ b/apps/daemon/internal/agent/codex/skills_test.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) func TestSetSkillExtraRootsUsesCodexRPC(t *testing.T) { @@ -40,13 +38,3 @@ func TestSetSkillExtraRootsUsesCodexRPC(t *testing.T) { t.Fatalf("setSkillExtraRoots: %v", err) } } - -func TestEffectiveAgentStateKeyFallsBackToConversation(t *testing.T) { - got := effectiveAgentStateKey(proto.PromptRequestPayload{ - ConversationID: "conv-legacy", - RunID: "run-ignored", - }) - if got != "_legacy_conversation/conv-legacy/codex" { - t.Fatalf("state key = %q", got) - } -} diff --git a/apps/daemon/internal/agent/configuration_test.go b/apps/daemon/internal/agent/configuration_test.go index 974f06824..a186755ce 100644 --- a/apps/daemon/internal/agent/configuration_test.go +++ b/apps/daemon/internal/agent/configuration_test.go @@ -30,11 +30,6 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { return nil, expected }) configuration.Providers[0].Protocol = "anthropic" - copy, err := registry.Configuration("fixture") - if err != nil { - t.Fatal(err) - } - copy.Providers[0].Protocol = "anthropic" factory, _ := registry.Resolve("fixture") executor, _ := registry.ResolveExecutor("fixture") preparation, _ := registry.ResolvePreparation("fixture") @@ -63,9 +58,6 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) { if calls != 3 { t.Fatal("unexpected native calls", calls) } - if _, err := registry.Configuration("missing"); err == nil { - t.Fatal("undeclared configuration inferred") - } } func TestRegistryRejectsInvalidConfigurationDeclaration(t *testing.T) { diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 5ca1f91e6..427a83a58 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -291,8 +291,7 @@ func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) { r.kinds[kind] = info } -// RegisterPreparation installs a separate execution-only path. Product factory -// wrappers must not add authoring side effects to it. +// RegisterPreparation installs a separate execution-only path. func (r *Registry) RegisterPreparation(kind string, workspaceRead bool, prepare PreparationFactory) { r.mu.Lock() defer r.mu.Unlock() diff --git a/apps/daemon/internal/agent/installroot/probe.go b/apps/daemon/internal/agent/installroot/probe.go index b185c31bc..21f50ed47 100644 --- a/apps/daemon/internal/agent/installroot/probe.go +++ b/apps/daemon/internal/agent/installroot/probe.go @@ -14,7 +14,7 @@ import ( func Probe(parent context.Context, binary string, args, env []string, dir string) (string, error) { ctx, cancel := context.WithTimeout(parent, 25*time.Second) defer cancel() - p, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: args, Env: env, Dir: dir, OwnProcessGroup: true, KillTimeout: 250 * time.Millisecond}) + p, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: args, Env: env, Dir: dir, KillTimeout: 250 * time.Millisecond}) if err != nil { return "", errors.New("native component failed to start") } diff --git a/apps/daemon/internal/agent/mcode/declaration.go b/apps/daemon/internal/agent/mcode/declaration.go index 5a6682dbb..c97e57bea 100644 --- a/apps/daemon/internal/agent/mcode/declaration.go +++ b/apps/daemon/internal/agent/mcode/declaration.go @@ -18,7 +18,6 @@ var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "mcode" Usage: proto.CapabilityUnsupported, Resume: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilityUnsupported, - WorkspaceAuthoring: proto.CapabilitySupported, Steering: proto.CapabilityUnsupported, MessageItems: proto.CapabilityUnsupported, ToolObservations: proto.CapabilityUnsupported, @@ -78,7 +77,6 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, r runtime.Executor = NewExecutorFactory(workspace) } if workspace != nil { - runtime.Info.Capabilities.WorkspaceAuthoring = proto.CapabilityUnsupported runtime.Preparation = NewPreparationFactory(*workspace) runtime.WorkspaceReadPreparation = true } diff --git a/apps/daemon/internal/agent/mcode/declaration_test.go b/apps/daemon/internal/agent/mcode/declaration_test.go index 16ec2df71..ff2f74f16 100644 --- a/apps/daemon/internal/agent/mcode/declaration_test.go +++ b/apps/daemon/internal/agent/mcode/declaration_test.go @@ -35,7 +35,7 @@ func TestMCodeExecutionOptInIsVersionBound(t *testing.T) { // The declaration must retain the complete baseline capability descriptor. func TestDeclaredCapabilityBaseline(t *testing.T) { - expected := map[string]bool{"Streaming": true, "Permissions": true, "Resume": true, "WorkspaceAuthoring": true} + expected := map[string]bool{"Streaming": true, "Permissions": true, "Resume": true} value := reflect.ValueOf(Declaration.Info.Capabilities) for i := 0; i < value.NumField(); i++ { name := value.Type().Field(i).Name diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index d4ccae14a..aa143e5aa 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -109,7 +109,6 @@ func TestEnvironmentMCPCancelSettlesPendingObservationBeforeDone(t *testing.T) { c, req, _ := workspaceFixture(t) c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" req.LocalEnvironment.MCP = []proto.EnvironmentMCP{environmentMCPFixture()} - req.ObserveToolObservations = true script, err := os.ReadFile(c.Binary) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/mcode/events.go b/apps/daemon/internal/agent/mcode/events.go index 0b04ce6dd..97bd1f8cd 100644 --- a/apps/daemon/internal/agent/mcode/events.go +++ b/apps/daemon/internal/agent/mcode/events.go @@ -59,7 +59,7 @@ func (s *Session) emitTool(update toolUpdate) error { if update.ID == "" || s.completedTools[update.ID] { return nil } - previous, started := s.tools[update.ID] + previous := s.tools[update.ID] if update.Name == "" { update.Name = previous.Name } @@ -69,21 +69,18 @@ func (s *Session) emitTool(update toolUpdate) error { if update.RawInput == nil { update.RawInput = previous.RawInput } - if s.req.ObserveToolObservations { - update.mcp = previous.mcp - if update.mcp != nil && update.Name != previous.Name { - return fmt.Errorf("mcode: native MCP call identity changed") - } - if update.mcp == nil { - var err error - update.mcp, err = s.environmentMCPIdentity(update.Name) - if err != nil { - return err - } + update.mcp = previous.mcp + if update.mcp != nil && update.Name != previous.Name { + return fmt.Errorf("mcode: native MCP call identity changed") + } + if update.mcp == nil { + var err error + update.mcp, err = s.environmentMCPIdentity(update.Name) + if err != nil { + return err } - started = previous.mcp != nil || workspaceToolObservation(previous, "before") != nil } - if !started { + if previous.mcp == nil && workspaceToolObservation(previous, "before") == nil { if err := s.emitToolStage(update, "before"); err != nil { return err } diff --git a/apps/daemon/internal/agent/mcode/execution.go b/apps/daemon/internal/agent/mcode/execution.go index 82dd3c9f8..bcb9e3902 100644 --- a/apps/daemon/internal/agent/mcode/execution.go +++ b/apps/daemon/internal/agent/mcode/execution.go @@ -7,7 +7,9 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -// SupportsExecution is an operator opt-in, separate from ordinary product availability. +// SupportsExecution reports whether the daemon advertises MiniMax Code execution: +// the operator sets OAC_RUNTIME_MCODE_AGENTS_API=1 and the native version is the +// qualified one. Otherwise discovery reports only availability. func SupportsExecution(version string) bool { return os.Getenv("OAC_RUNTIME_MCODE_AGENTS_API") == "1" && version == SupportedVersion } @@ -27,9 +29,6 @@ func validateExecutionRequest(req proto.PromptRequestPayload) error { return err } } - if req.AgentOptions["env"] != nil { - return fmt.Errorf("mcode: execution cannot import an environment") - } return nil } @@ -45,12 +44,8 @@ func configureTextExecution(config map[string]any) { // Harness children use the daemon user's ordinary environment. func executionEnvironment() []string { return os.Environ() } -// ACP commands are only recognized for a single text block. Public input must -// remain user text; ordinary product Sessions retain their native command behavior. -func promptContent(text string, public bool) []map[string]string { - blocks := []map[string]string{{"type": "text", "text": text}} - if public { - blocks = append(blocks, map[string]string{"type": "text", "text": ""}) - } - return blocks +// ACP commands are only recognized for a single text block. A second, empty +// block keeps public input as user text. +func promptContent(text string) []map[string]string { + return []map[string]string{{"type": "text", "text": text}, {"type": "text", "text": ""}} } diff --git a/apps/daemon/internal/agent/mcode/execution_test.go b/apps/daemon/internal/agent/mcode/execution_test.go index 839c594ad..9f188f97f 100644 --- a/apps/daemon/internal/agent/mcode/execution_test.go +++ b/apps/daemon/internal/agent/mcode/execution_test.go @@ -10,15 +10,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -func executionRequest(t *testing.T) proto.PromptRequestPayload { - r := testRequest(t) - r.StrictResume, r.ReleaseOnCompletion, r.DisableExecutionEnvironment, r.DisableSubagents = true, true, true, true - r.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"} - return r -} - func TestExecutionOptionsInheritUserEnvironment(t *testing.T) { - r := executionRequest(t) + r := testRequest(t) t.Setenv("OAC_TEST_SECRET_CANARY", "secret") t.Setenv("NODE_OPTIONS", "--import=untrusted") opts, err := prepareOptions(r) @@ -52,9 +45,8 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { func(r *proto.PromptRequestPayload) { r.RequireExistingNativeSession = true }, func(r *proto.PromptRequestPayload) { r.FunctionTools = []proto.FunctionTool{{Name: "f"}} }, func(r *proto.PromptRequestPayload) { r.ExecutionControls.WebSearch = "enabled" }, - func(r *proto.PromptRequestPayload) { r.AgentOptions["env"] = map[string]any{"X": "Y"} }, } { - r := executionRequest(t) + r := testRequest(t) change(&r) if _, err := prepareOptions(r); err == nil { t.Fatal("unsupported execution accepted") @@ -64,12 +56,9 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { func TestPublicTextDoesNotInvokeACPCommands(t *testing.T) { for _, text := range []string{"/model", "/compact", "hello"} { - blocks := promptContent(text, true) + blocks := promptContent(text) if len(blocks) != 2 || blocks[0]["text"] != text || blocks[1]["text"] != "" { t.Fatal(blocks) } - if blocks = promptContent(text, false); len(blocks) != 1 || blocks[0]["text"] != text { - t.Fatal("product prompt changed") - } } } diff --git a/apps/daemon/internal/agent/mcode/executor_native_test.go b/apps/daemon/internal/agent/mcode/executor_native_test.go index e9dd4d9a3..96cae1889 100644 --- a/apps/daemon/internal/agent/mcode/executor_native_test.go +++ b/apps/daemon/internal/agent/mcode/executor_native_test.go @@ -28,7 +28,7 @@ func TestNativeMCodeExecutorReuse(t *testing.T) { if err != nil { t.Fatal("private provider options unavailable") } - req := executionRequest(t) + req := testRequest(t) req.ReleaseOnCompletion = false req.RunID, req.Input, req.ConversationID = "", nil, "" if json.Unmarshal(raw, &req.AgentOptions) != nil { diff --git a/apps/daemon/internal/agent/mcode/executor_test.go b/apps/daemon/internal/agent/mcode/executor_test.go index eeac6a0ce..fc78836d0 100644 --- a/apps/daemon/internal/agent/mcode/executor_test.go +++ b/apps/daemon/internal/agent/mcode/executor_test.go @@ -28,7 +28,7 @@ func executorFixture(t *testing.T, scenario string, workspace bool) (*executor, if workspace { factory = NewExecutorFactory(&config) } else { - req = executionRequest(t) + req = testRequest(t) req.ReleaseOnCompletion = false req.RunID, req.Input, req.ConversationID = "", nil, "" t.Setenv("OAC_RUNTIME_MCODE_BIN", config.Binary) diff --git a/apps/daemon/internal/agent/mcode/executor_turn.go b/apps/daemon/internal/agent/mcode/executor_turn.go index 3bf537374..1633f5f18 100644 --- a/apps/daemon/internal/agent/mcode/executor_turn.go +++ b/apps/daemon/internal/agent/mcode/executor_turn.go @@ -25,7 +25,7 @@ func (s *Session) runExecutorTurn() { // Written steering requests retain their original receipt owner even after // prompt completion. No successor starts until all callers have settled. s.operations.Wait() - if s.req.StrictResume && !s.req.DisableSubagents && s.subagentHistoryReady { + if !s.req.DisableSubagents && s.subagentHistoryReady { childErr := s.settleSubagents() s.mu.Lock() s.subagentSettlementError = childErr @@ -82,7 +82,7 @@ func (s *Session) runExecutorTurn() { } func (s *Session) captureSubagentBaseline() error { - if !s.req.StrictResume || s.req.DisableSubagents { + if s.req.DisableSubagents { return nil } snapshot, err := s.readSubagents(s.ctx) @@ -135,7 +135,7 @@ func (s *Session) cancelTurn(ctx context.Context) error { err = s.writeContext(ctx, rpcFrame{JSONRPC: "2.0", Method: "session/cancel", Params: raw}) } if first { - if err == nil && s.req.StrictResume && !s.req.DisableSubagents { + if err == nil && !s.req.DisableSubagents { err = s.stopSubagents(ctx) } if err != nil { diff --git a/apps/daemon/internal/agent/mcode/mcp_observations_test.go b/apps/daemon/internal/agent/mcode/mcp_observations_test.go index 1c790f731..61e090a6b 100644 --- a/apps/daemon/internal/agent/mcode/mcp_observations_test.go +++ b/apps/daemon/internal/agent/mcode/mcp_observations_test.go @@ -15,7 +15,7 @@ func mcpObservationSession(t *testing.T) (*Session, chan proto.Envelope) { t.Helper() out := make(chan proto.Envelope, 16) s := &Session{ctx: context.Background(), opts: launchOptions{DataDir: t.TempDir()}, - req: proto.PromptRequestPayload{RunID: "run", ObserveToolObservations: true, + req: proto.PromptRequestPayload{RunID: "run", LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{environmentMCPFixture()}}}, out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}, active: true, sessionID: "native-session"} if err := writeMCPRegistry(s.opts.DataDir, mcpRegistryEntry("proof.server", "proof_server_2", "read.status", "read_status_2")); err != nil { diff --git a/apps/daemon/internal/agent/mcode/native_history_test.go b/apps/daemon/internal/agent/mcode/native_history_test.go index 0106570cc..d49061554 100644 --- a/apps/daemon/internal/agent/mcode/native_history_test.go +++ b/apps/daemon/internal/agent/mcode/native_history_test.go @@ -24,7 +24,7 @@ func TestNativeMCodeHistoryIsolation(t *testing.T) { } for name, id := range map[string]string{"foreign": foreign, "missing": "00000000-0000-4000-8000-000000000000"} { t.Run(name, func(t *testing.T) { - req := executionRequest(t) + req := testRequest(t) if json.Unmarshal(raw, &req.AgentOptions) != nil { t.Fatal("invalid private options") } diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index 5714e401f..50926a7c8 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "strconv" - "strings" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" @@ -24,15 +23,13 @@ func prepareOptions(req proto.PromptRequestPayload) (launchOptions, error) { if _, err := harnessconfiguration.Configuration().PrepareHarnessConfig(req.AgentOptions); err != nil { return result, err } - if req.StrictResume { - if err := validateExecutionRequest(req); err != nil { - return result, err - } + if err := validateExecutionRequest(req); err != nil { + return result, err } if req.Input.HasImages() { return result, fmt.Errorf("mcode: ACP does not support attachments") } - dataDir, err := agent.StateDir("mcode", req.AgentStateKey, req.ConversationID, req.RunID) + dataDir, err := agent.StateDir("mcode", req.AgentStateKey) if err != nil { return result, err } @@ -62,15 +59,13 @@ func prepareOptions(req proto.PromptRequestPayload) (launchOptions, error) { return result, err } config["custom_provider"] = map[string]any{"oac": provider} - if req.StrictResume { - configureTextExecution(config) - if !req.DisableSubagents { - config["agents"] = map[string]any{"default": map[string]any{ - "tools": []string{"task", "task_append", "task_query", "task_output", "task_stop"}, - "builtinTools": []string{"task", "task_append", "task_query", "task_output", "task_stop"}, "skills": []string{}, - "features": map[string]bool{"mavis": false, "delegation": true, "webSearch": false}, - }} - } + configureTextExecution(config) + if !req.DisableSubagents { + config["agents"] = map[string]any{"default": map[string]any{ + "tools": []string{"task", "task_append", "task_query", "task_output", "task_stop"}, + "builtinTools": []string{"task", "task_append", "task_query", "task_output", "task_stop"}, "skills": []string{}, + "features": map[string]bool{"mavis": false, "delegation": true, "webSearch": false}, + }} } config["permissionMode"] = "auto" data, err := json.Marshal(config) @@ -80,36 +75,17 @@ func prepareOptions(req proto.PromptRequestPayload) (launchOptions, error) { if err := os.WriteFile(filepath.Join(result.DataDir, "config.yaml"), data, 0o600); err != nil { return result, err } - result.Env = append([]string{}, os.Environ()...) - if req.StrictResume { - result.Env = append(executionEnvironment(), "OAC_RUNTIME_MCODE_TOOL_POLICY=protected-mcp-v1") - if err := os.WriteFile(filepath.Join(result.DataDir, "mcp.json"), []byte(`{"mcpServers":{}}`), 0o600); err != nil { - return result, err - } - if !req.DisableSubagents { - result.Env = append(result.Env, "OAC_RUNTIME_MCODE_MAX_SUBAGENTS="+strconv.Itoa(*req.MaxConcurrentSubagents)) - } else { - result.Env = append(result.Env, "OAC_RUNTIME_MCODE_MAX_SUBAGENTS=0") - } + result.Env = append(executionEnvironment(), "OAC_RUNTIME_MCODE_TOOL_POLICY=protected-mcp-v1") + if err := os.WriteFile(filepath.Join(result.DataDir, "mcp.json"), []byte(`{"mcpServers":{}}`), 0o600); err != nil { + return result, err } - if raw := opts["env"]; raw != nil && !req.StrictResume { - env, ok := raw.(map[string]any) - if !ok { - return result, fmt.Errorf("mcode: env must be an object") - } - for key, rawValue := range env { - value, ok := rawValue.(string) - if !ok || key == "" || strings.ContainsAny(key, "=\x00") || strings.ContainsRune(value, 0) { - return result, fmt.Errorf("mcode: invalid environment entry") - } - result.Env = append(result.Env, key+"="+value) - } + if !req.DisableSubagents { + result.Env = append(result.Env, "OAC_RUNTIME_MCODE_MAX_SUBAGENTS="+strconv.Itoa(*req.MaxConcurrentSubagents)) + } else { + result.Env = append(result.Env, "OAC_RUNTIME_MCODE_MAX_SUBAGENTS=0") } // The adapter owns the native state location, including after cold resume. - result.Env = append(result.Env, "MINIMAX_DATA_DIR="+result.DataDir) - if req.StrictResume { - result.Env = append(result.Env, "HOME="+result.DataDir, "USERPROFILE="+result.DataDir) - } + result.Env = append(result.Env, "MINIMAX_DATA_DIR="+result.DataDir, "HOME="+result.DataDir, "USERPROFILE="+result.DataDir) result.MCP = []map[string]any{} return result, nil } diff --git a/apps/daemon/internal/agent/mcode/options_test.go b/apps/daemon/internal/agent/mcode/options_test.go index fa7fa94d2..e5d399ed1 100644 --- a/apps/daemon/internal/agent/mcode/options_test.go +++ b/apps/daemon/internal/agent/mcode/options_test.go @@ -11,8 +11,8 @@ import ( ) func TestOptionsRefreshManagedState(t *testing.T) { + t.Setenv("MINIMAX_DATA_DIR", "/wrong") req := testRequest(t) - req.AgentOptions["env"] = map[string]any{"MINIMAX_DATA_DIR": "/wrong", "FIXTURE": "yes"} opts, err := prepareOptions(req) if err != nil { t.Fatal(err) @@ -20,7 +20,13 @@ func TestOptionsRefreshManagedState(t *testing.T) { if !strings.HasPrefix(opts.Dir, os.Getenv("OAC_RUNTIME_HOME")+string(os.PathSeparator)) { t.Fatalf("workdir escaped managed state: %s", opts.Dir) } - if opts.Env[len(opts.Env)-1] != "MINIMAX_DATA_DIR="+opts.DataDir { + dataDir := "" + for _, entry := range opts.Env { + if value, ok := strings.CutPrefix(entry, "MINIMAX_DATA_DIR="); ok { + dataDir = value + } + } + if dataDir != opts.DataDir { t.Fatal("state override did not win") } req.AgentOptions["system_prompt"] = "" diff --git a/apps/daemon/internal/agent/mcode/preparation_test.go b/apps/daemon/internal/agent/mcode/preparation_test.go index 93b095ee4..249129603 100644 --- a/apps/daemon/internal/agent/mcode/preparation_test.go +++ b/apps/daemon/internal/agent/mcode/preparation_test.go @@ -15,7 +15,7 @@ import ( func workspaceFixture(t *testing.T) (WorkspaceConfig, proto.PromptRequestPayload, string) { t.Helper() - r := executionRequest(t) + r := testRequest(t) r.RunID, r.Input, r.ConversationID = "", nil, "" r.DisableExecutionEnvironment = false r.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()} diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index cd32a2edf..741f9a3d9 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -80,7 +80,7 @@ func newSession(ctx context.Context, req proto.PromptRequestPayload, out chan<- } func launch(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, binary string, out chan<- proto.Envelope) (*Session, error) { - process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: []string{"acp"}, Dir: opts.Dir, Env: opts.Env, NeedStdin: true, OwnProcessGroup: req.StrictResume}) + process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: []string{"acp"}, Dir: opts.Dir, Env: opts.Env, NeedStdin: true}) if err != nil { return nil, err } @@ -104,7 +104,7 @@ func (s *Session) run() { defer close(s.finished) err := s.prepareNative() if err == nil { - if s.req.StrictResume && !s.req.DisableSubagents { + if !s.req.DisableSubagents { var snapshot nativeSubagentSnapshot snapshot, err = s.readSubagents(s.ctx) s.subagentHistoryReady = err == nil @@ -121,7 +121,7 @@ func (s *Session) run() { if err == nil { err = s.executePrompt() } - if s.req.StrictResume && !s.req.DisableSubagents && s.subagentHistoryReady && s.out != nil { + if !s.req.DisableSubagents && s.subagentHistoryReady && s.out != nil { observationErr := s.settleSubagents() s.mu.Lock() s.subagentSettlementError = observationErr @@ -174,7 +174,7 @@ func (s *Session) prepareNative() error { if initialized.ProtocolVersion != 1 { return fmt.Errorf("mcode: unsupported ACP protocol version %d", initialized.ProtocolVersion) } - if s.req.StrictResume && !s.req.DisableSubagents && (initialized.Meta.Subagents.Version != 1 || initialized.Meta.Subagents.WorkspaceTools != "protected-mcp-v1" || s.req.MaxConcurrentSubagents == nil || initialized.Meta.Subagents.MaxConcurrent != *s.req.MaxConcurrentSubagents) { + if !s.req.DisableSubagents && (initialized.Meta.Subagents.Version != 1 || initialized.Meta.Subagents.WorkspaceTools != "protected-mcp-v1" || s.req.MaxConcurrentSubagents == nil || initialized.Meta.Subagents.MaxConcurrent != *s.req.MaxConcurrentSubagents) { return fmt.Errorf("mcode: native Subagent admission is unavailable") } params := map[string]any{"cwd": s.opts.Dir, "mcpServers": s.opts.MCP} @@ -221,7 +221,7 @@ func (s *Session) executePrompt() error { var result struct { StopReason string `json:"stopReason"` } - err = s.call("session/prompt", map[string]any{"sessionId": s.sessionID, "prompt": promptContent(prompt, s.req.StrictResume)}, &result, true) + err = s.call("session/prompt", map[string]any{"sessionId": s.sessionID, "prompt": promptContent(prompt)}, &result, true) s.active = false s.mu.Lock() s.steeringReady = false @@ -360,16 +360,13 @@ func (s *Session) Cancel(ctx context.Context) error { if s.executor != nil { return s.cancelTurn(ctx) } - if s.req.StrictResume && !s.req.DisableSubagents { + if !s.req.DisableSubagents { if err := s.cancelSubagents(ctx); err != nil { s.process.Cancel() return err } } s.process.Cancel() - if !s.req.StrictResume { - return nil - } select { case <-s.exited: case <-ctx.Done(): diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index f2af87a50..ad506fedb 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -20,15 +20,12 @@ func testRequest(t *testing.T) proto.PromptRequestPayload { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) return proto.PromptRequestPayload{RunID: "run-1", ConversationID: "conversation-1", AgentStateKey: "conversation-1/agent-1/mcode", Input: proto.TextInput("Hello"), AgentOptions: map[string]any{ "model": "fixture", "model_provider": map[string]any{"protocol": "anthropic", "base_url": "https://provider.example/v1", "api_key": "fixture-key", "context_window": 64000, "max_output_tokens": 4096}, "system_prompt": "Current instructions", - }} + }, ReleaseOnCompletion: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} } func helperSession(t *testing.T, scenario string, resume bool) (*Session, <-chan proto.Envelope) { t.Helper() req := testRequest(t) - if scenario == "strict-cancel" { - req = executionRequest(t) - } if resume { req.AgentSessionID = "native-1" } @@ -272,7 +269,7 @@ func TestMCodeProcess(t *testing.T) { Prompt []map[string]string `json:"prompt"` } _ = json.Unmarshal(frame.Params, &input) - if strict := scenario == "strict-cancel" || (strings.HasPrefix(scenario, "prepared") || strings.HasPrefix(scenario, "executor")); (strict && len(input.Prompt) != 2) || (!strict && len(input.Prompt) != 1) { + if len(input.Prompt) != 2 { os.Exit(9) } if strings.HasPrefix(scenario, "executor") { @@ -286,7 +283,7 @@ func TestMCodeProcess(t *testing.T) { continue } update("agent_message_chunk", map[string]any{"content": map[string]string{"type": "text", "text": input.Prompt[0]["text"]}}) - update("tool_call", map[string]any{"toolCallId": "repeated-call", "name": "Read", "status": "in_progress", "rawInput": map[string]any{}}) + update("tool_call", map[string]any{"toolCallId": "repeated-call", "name": "mcp__oac_workspace__workspace_bash", "status": "in_progress", "rawInput": map[string]any{"command": "true"}}) update("tool_call_update", map[string]any{"toolCallId": "repeated-call", "status": "completed"}) raw, _ := json.Marshal(map[string]string{"stopReason": "end_turn"}) send(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Result: raw}) @@ -302,7 +299,7 @@ func TestMCodeProcess(t *testing.T) { update("tool_call", map[string]any{"toolCallId": "native-call", "name": "mcp__proof_server__read_status", "status": "in_progress", "rawInput": map[string]any{}}) continue } - if scenario == "steering" || scenario == "steer-rejected" || scenario == "steer-lost" || scenario == "strict-cancel" { + if scenario == "steering" || scenario == "steer-rejected" || scenario == "steer-lost" || scenario == "cancel-wait" { promptID = frame.ID update("agent_message_chunk", map[string]any{"content": map[string]string{"type": "text", "text": "ready"}}) continue @@ -325,7 +322,7 @@ func TestMCodeProcess(t *testing.T) { } update("agent_message_chunk", map[string]any{"content": map[string]string{"type": "text", "text": "Hello "}}) update("agent_message_chunk", map[string]any{"content": map[string]string{"type": "text", "text": "world"}}) - update("tool_call", map[string]any{"toolCallId": "tool-1", "name": "Read", "status": "in_progress", "rawInput": map[string]any{"path": "fixture.txt"}}) + update("tool_call", map[string]any{"toolCallId": "tool-1", "name": "mcp__oac_workspace__workspace_bash", "status": "in_progress", "rawInput": map[string]any{"command": "cat fixture.txt"}}) for range 2 { update("tool_call_update", map[string]any{"toolCallId": "tool-1", "status": "completed", "rawOutput": "fixture"}) } diff --git a/apps/daemon/internal/agent/mcode/steering_test.go b/apps/daemon/internal/agent/mcode/steering_test.go index 2c4cc517a..1ecdb5af1 100644 --- a/apps/daemon/internal/agent/mcode/steering_test.go +++ b/apps/daemon/internal/agent/mcode/steering_test.go @@ -85,7 +85,7 @@ func TestTerminalFollowsAllNativeFrames(t *testing.T) { } func TestExecutionCancellationWaitsForOutputAndProcess(t *testing.T) { - s, out := helperSession(t, "strict-cancel", false) + s, out := helperSession(t, "cancel-wait", false) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() select { diff --git a/apps/daemon/internal/agent/mcode/tool_observations.go b/apps/daemon/internal/agent/mcode/tool_observations.go index 7f6ba9f9c..1dbd6d643 100644 --- a/apps/daemon/internal/agent/mcode/tool_observations.go +++ b/apps/daemon/internal/agent/mcode/tool_observations.go @@ -11,20 +11,18 @@ func (s *Session) emitToolStage(update toolUpdate, stage string) error { if stage == "after" { payload.Result = map[string]any{"output": update.RawOutput, "status": update.Status} } - if s.req.ObserveToolObservations { - payload.Observation = workspaceToolObservation(update, stage) - if payload.Observation == nil { - var err error - payload.Observation, err = environmentMCPObservation(update, stage) - if err != nil { - return err - } - } - // Native task/skill bookkeeping has no qualified public item mapping. - if payload.Observation == nil { - return nil + payload.Observation = workspaceToolObservation(update, stage) + if payload.Observation == nil { + var err error + payload.Observation, err = environmentMCPObservation(update, stage) + if err != nil { + return err } } + // Native task/skill bookkeeping has no qualified public item mapping. + if payload.Observation == nil { + return nil + } s.emit(proto.TypeToolCall, payload) return nil } diff --git a/apps/daemon/internal/agent/mcode/tool_observations_test.go b/apps/daemon/internal/agent/mcode/tool_observations_test.go index e9011d503..c7d1fa6da 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(), req: proto.PromptRequestPayload{RunID: "run", ObserveToolObservations: true}, out: out, tools: map[string]toolUpdate{}, completedTools: map[string]bool{}} + s := &Session{ctx: context.Background(), req: proto.PromptRequestPayload{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/workspace.go b/apps/daemon/internal/agent/mcode/workspace.go index 7fdf2d3fd..5b913e4ac 100644 --- a/apps/daemon/internal/agent/mcode/workspace.go +++ b/apps/daemon/internal/agent/mcode/workspace.go @@ -53,7 +53,7 @@ func prepareWorkspaceOptions(c WorkspaceConfig, req proto.PromptRequestPayload) if c.Network != "enabled" || len(c.AllowedDomains) != 0 { return launchOptions{}, fmt.Errorf("mcode: Runtime does not implement network isolation") } - if !req.StrictResume || 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.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 { return launchOptions{}, fmt.Errorf("mcode: execution does not match the dedicated workspace") } servers, err := runtimeMCP(req) diff --git a/apps/daemon/internal/agent/mcode/workspace_readiness.go b/apps/daemon/internal/agent/mcode/workspace_readiness.go index b730745b4..7f44d672a 100644 --- a/apps/daemon/internal/agent/mcode/workspace_readiness.go +++ b/apps/daemon/internal/agent/mcode/workspace_readiness.go @@ -16,7 +16,7 @@ import ( func CheckWorkspace(ctx context.Context, c WorkspaceConfig) error { ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - p, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: c.Node, Args: []string{filepath.Join(filepath.Dir(c.Bridge), "check.mjs")}, Env: executionEnvironment(), OwnProcessGroup: true}) + p, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: c.Node, Args: []string{filepath.Join(filepath.Dir(c.Bridge), "check.mjs")}, Env: executionEnvironment()}) if err != nil { return err } diff --git a/apps/daemon/internal/agent/registry.go b/apps/daemon/internal/agent/registry.go index 4c6b5e815..6584fb751 100644 --- a/apps/daemon/internal/agent/registry.go +++ b/apps/daemon/internal/agent/registry.go @@ -107,15 +107,3 @@ func (r *Registry) ResolvePreparation(kind string) (PreparationFactory, error) { } return f, nil } - -// Configuration returns an owned declaration for registry wrappers. Wrappers -// transfer it with the factory; they must not infer configuration from kind names. -func (r *Registry) Configuration(kind string) (harnessconfig.Configuration, error) { - r.mu.RLock() - defer r.mu.RUnlock() - configuration, ok := r.configurations[kind] - if !ok { - return harnessconfig.Configuration{}, ErrUnsupportedKind - } - return configuration.Clone(), nil -} diff --git a/apps/daemon/internal/agent/runtime_paths.go b/apps/daemon/internal/agent/runtime_paths.go index 0ad41e2f0..456231282 100644 --- a/apps/daemon/internal/agent/runtime_paths.go +++ b/apps/daemon/internal/agent/runtime_paths.go @@ -10,7 +10,7 @@ import ( // StateDir returns an adapter-owned state directory scoped to one agent // state. It never derives runtime state from the subprocess cwd. -func StateDir(agentKind, agentStateKey, conversationID, runID string) (string, error) { +func StateDir(agentKind, agentStateKey string) (string, error) { root, err := paths.Root() if err != nil { return "", fmt.Errorf("agent: resolve state directory: %w", err) @@ -19,21 +19,11 @@ func StateDir(agentKind, agentStateKey, conversationID, runID string) (string, e if kind == "" { return "", fmt.Errorf("agent: invalid agent kind %q", agentKind) } - base := filepath.Join(root, "runtime", kind) - if key := strings.TrimSpace(agentStateKey); key != "" { - parts := safeRuntimePathParts(key) - if len(parts) == 0 { - return "", fmt.Errorf("agent: invalid agent state key %q", agentStateKey) - } - return filepath.Join(append([]string{base, "state"}, parts...)...), nil - } - if id := safeRuntimePathPart(conversationID); id != "" { - return filepath.Join(base, "conv-"+id), nil - } - if id := safeRuntimePathPart(runID); id != "" { - return filepath.Join(base, "run-"+id), nil + parts := safeRuntimePathParts(agentStateKey) + if len(parts) == 0 { + return "", fmt.Errorf("agent: invalid agent state key %q", agentStateKey) } - return "", fmt.Errorf("agent: agent state key, conversation id, or run id is required") + return filepath.Join(append([]string{root, "runtime", kind, "state"}, parts...)...), nil } func safeRuntimePathParts(value string) []string { diff --git a/apps/daemon/internal/agent/runtime_paths_test.go b/apps/daemon/internal/agent/runtime_paths_test.go index 2cc51981a..3e98c1792 100644 --- a/apps/daemon/internal/agent/runtime_paths_test.go +++ b/apps/daemon/internal/agent/runtime_paths_test.go @@ -8,7 +8,7 @@ import ( func TestStateDirUsesStableAgentState(t *testing.T) { home := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", home) - got, err := StateDir("codex", "conv-1/agent-1/codex", "ignored", "ignored") + got, err := StateDir("codex", "conv-1/agent-1/codex") if err != nil { t.Fatalf("StateDir: %v", err) } @@ -18,15 +18,16 @@ func TestStateDirUsesStableAgentState(t *testing.T) { } } -func TestStateDirSanitizesFallback(t *testing.T) { +func TestStateDirRequiresAgentState(t *testing.T) { home := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", home) - got, err := StateDir("fake_beta", "", "../conv name", "ignored") - if err != nil { - t.Fatalf("StateDir: %v", err) + for _, key := range []string{"", " ", "../.."} { + if _, err := StateDir("codex", key); err == nil { + t.Fatalf("state key %q accepted", key) + } } - want := filepath.Join(home, "runtime", "fake_beta", "conv-.._conv_name") - if got != want { - t.Fatalf("root = %q, want %q", got, want) + got, err := StateDir("codex", "../session-1/a b") + if want := filepath.Join(home, "runtime", "codex", "state", "session-1", "a_b"); err != nil || got != want { + t.Fatalf("sanitized state = %q, %v; want %q", got, err, want) } } diff --git a/apps/daemon/internal/authoring/bridge.go b/apps/daemon/internal/authoring/bridge.go deleted file mode 100644 index 519165fda..000000000 --- a/apps/daemon/internal/authoring/bridge.go +++ /dev/null @@ -1,134 +0,0 @@ -package authoring - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "os" - "path/filepath" - "sync" - "time" - - "github.com/google/uuid" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -type Sender interface { - Send(context.Context, proto.Envelope) error -} - -type Bridge struct { - sender Sender - mu sync.Mutex - waiters map[string]waiter -} - -type waiter struct { - runID string - response chan proto.AuthoringResponsePayload -} - -func New(sender Sender) *Bridge { - return &Bridge{sender: sender, waiters: make(map[string]waiter)} -} - -// Deliver only resolves the matching request from the same active run. -func (b *Bridge) Deliver(env proto.Envelope) { - var response proto.AuthoringResponsePayload - if env.DecodePayload(&response) != nil { - return - } - b.mu.Lock() - w, ok := b.waiters[response.RequestID] - b.mu.Unlock() - if ok && w.runID == env.ID { - select { - case w.response <- response: - default: - } - } -} - -func (b *Bridge) request(ctx context.Context, runID string, request proto.AuthoringRequestPayload) (proto.AuthoringResponsePayload, error) { - request.RequestID = uuid.NewString() - w := waiter{runID: runID, response: make(chan proto.AuthoringResponsePayload, 1)} - b.mu.Lock() - b.waiters[request.RequestID] = w - b.mu.Unlock() - defer func() { b.mu.Lock(); delete(b.waiters, request.RequestID); b.mu.Unlock() }() - env, err := proto.NewEnvelope(proto.TypeAuthoringRequest, runID, request) - if err != nil { - return proto.AuthoringResponsePayload{}, err - } - if err := b.sender.Send(ctx, env); err != nil { - return proto.AuthoringResponsePayload{}, err - } - select { - case response := <-w.response: - return response, nil - case <-ctx.Done(): - return proto.AuthoringResponsePayload{}, ctx.Err() - } -} - -func (b *Bridge) Listen(parent context.Context, runID string) (string, func(), error) { - home, err := os.UserHomeDir() - if err != nil { - return "", nil, err - } - dir := filepath.Join(home, ".oac", "authoring") - if err := os.MkdirAll(dir, 0o700); err != nil { - return "", nil, err - } - path := filepath.Join(dir, uuid.NewString()[:8]+".sock") - listener, err := net.Listen("unix", path) - if err != nil { - return "", nil, fmt.Errorf("open daemon authoring socket: %w", err) - } - if err := os.Chmod(path, 0o600); err != nil { - _ = listener.Close() - return "", nil, err - } - ctx, cancel := context.WithCancel(parent) - stop := context.AfterFunc(ctx, func() { _ = listener.Close() }) - closeListener := func() { cancel(); stop(); _ = listener.Close() } - go func() { - defer closeListener() - for { - conn, err := listener.Accept() - if err != nil { - return - } - go b.serve(ctx, runID, conn) - } - }() - return path, closeListener, nil -} - -func (b *Bridge) serve(parent context.Context, runID string, conn net.Conn) { - defer func() { _ = conn.Close() }() - ctx, cancel := context.WithTimeout(parent, 30*time.Second) - defer cancel() - stop := context.AfterFunc(ctx, func() { _ = conn.Close() }) - defer stop() - _ = conn.SetDeadline(time.Now().Add(30 * time.Second)) - var request proto.AuthoringRequestPayload - decoder := json.NewDecoder(io.LimitReader(conn, proto.AuthoringMaxBytes+1)) - decoder.DisallowUnknownFields() - err := decoder.Decode(&request) - if err == nil && decoder.InputOffset() > proto.AuthoringMaxBytes { - err = errors.New("authoring request is too large") - } - var response proto.AuthoringResponsePayload - if err == nil { - response, err = b.request(ctx, runID, request) - } - if err != nil { - response.Error = err.Error() - } - _ = json.NewEncoder(conn).Encode(response) -} diff --git a/apps/daemon/internal/authoring/bridge_test.go b/apps/daemon/internal/authoring/bridge_test.go deleted file mode 100644 index 17056693d..000000000 --- a/apps/daemon/internal/authoring/bridge_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package authoring - -import ( - "context" - "encoding/json" - "net" - "os" - "path/filepath" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -type testSender struct{ frames chan proto.Envelope } - -func (s testSender) Send(ctx context.Context, env proto.Envelope) error { - select { - case s.frames <- env: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -func TestBridgeUsesRunAttributionAndClosesAccess(t *testing.T) { - home, err := os.MkdirTemp("/tmp", "pa-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(home) - t.Setenv("HOME", home) - sender := testSender{frames: make(chan proto.Envelope, 1)} - b := New(sender) - path, release, err := b.Listen(t.Context(), "run-a") - if err != nil { - t.Fatal(err) - } - defer release() - if filepath.Dir(path) != filepath.Join(home, ".oac", "authoring") { - t.Fatal("socket outside OpenAgentCore state") - } - if info, err := os.Stat(path); err != nil || info.Mode().Perm() != 0o600 { - t.Fatalf("socket permissions: %v %v", info, err) - } - conn, err := net.Dial("unix", path) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - _ = conn.SetDeadline(time.Now().Add(3 * time.Second)) - if err := json.NewEncoder(conn).Encode(proto.AuthoringRequestPayload{RequestID: "client-supplied", Operation: proto.AuthoringContext}); err != nil { - t.Fatal(err) - } - var frame proto.Envelope - select { - case frame = <-sender.frames: - case <-time.After(time.Second): - t.Fatal("request not forwarded") - } - var request proto.AuthoringRequestPayload - if frame.DecodePayload(&request) != nil || frame.ID != "run-a" || request.RequestID == "client-supplied" || frame.Type != proto.TypeAuthoringRequest { - t.Fatalf("wrong attribution: %+v", frame) - } - wrong, _ := proto.NewEnvelope(proto.TypeAuthoringResponse, "run-b", proto.AuthoringResponsePayload{RequestID: request.RequestID, Data: json.RawMessage(`"wrong"`)}) - b.Deliver(wrong) - good, _ := proto.NewEnvelope(proto.TypeAuthoringResponse, "run-a", proto.AuthoringResponsePayload{RequestID: request.RequestID, Data: json.RawMessage(`"right"`)}) - b.Deliver(good) - var response proto.AuthoringResponsePayload - if err := json.NewDecoder(conn).Decode(&response); err != nil { - t.Fatal(err) - } - if string(response.Data) != `"right"` { - t.Fatalf("wrong response: %+v", response) - } - release() - if _, err := net.DialTimeout("unix", path, time.Second); err == nil { - t.Fatal("completed run still accepts commands") - } -} - -func TestBridgeCancellationClearsWaiters(t *testing.T) { - sender := testSender{frames: make(chan proto.Envelope, 1)} - b := New(sender) - ctx, cancel := context.WithCancel(t.Context()) - done := make(chan error, 1) - go func() { - _, err := b.request(ctx, "run", proto.AuthoringRequestPayload{Operation: proto.AuthoringSkillList}) - done <- err - }() - <-sender.frames - cancel() - select { - case err := <-done: - if err == nil { - t.Fatal("cancelled request succeeded") - } - case <-time.After(time.Second): - t.Fatal("cancelled request remained blocked") - } - b.mu.Lock() - defer b.mu.Unlock() - if len(b.waiters) != 0 { - t.Fatal("waiter retained after cancellation") - } -} diff --git a/apps/daemon/internal/cli/authoring.go b/apps/daemon/internal/cli/authoring.go deleted file mode 100644 index 6b3b26956..000000000 --- a/apps/daemon/internal/cli/authoring.go +++ /dev/null @@ -1,77 +0,0 @@ -package cli - -import ( - "context" - "maps" - "os" - "path/filepath" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/authoring" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" -) - -func withAuthoringBridge(factory agent.Factory, bridge *authoring.Bridge) agent.Factory { - return func(ctx context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { - if !req.WorkspaceAuthoring { - return factory(ctx, req, out) - } - path, release, err := bridge.Listen(ctx, req.RunID) - if err != nil { - return nil, err - } - req.AgentOptions = maps.Clone(req.AgentOptions) - if req.AgentOptions == nil { - req.AgentOptions = make(map[string]any) - } - env, _ := req.AgentOptions["env"].(map[string]any) - env = maps.Clone(env) - if env == nil { - env = make(map[string]any) - } - env[proto.AuthoringSocketEnv] = path - if executable, err := os.Executable(); err == nil { - addCompanionCLIPath(env, filepath.Dir(executable)) - } - req.AgentOptions["env"] = env - upstream := make(chan proto.Envelope, 64) - session, err := factory(ctx, req, upstream) - if err != nil { - release() - return nil, err - } - go func() { - defer close(out) - defer release() - for event := range upstream { - if event.Type == proto.TypeDone { - release() - } - out <- event - } - }() - return session, nil - } -} - -func authoringRegistry(registry *agent.Registry, bridge *authoring.Bridge) *agent.Registry { - wrapped := agent.NewRegistry() - for _, info := range registry.SupportedAgentKinds() { - factory, _ := registry.Resolve(info.Kind) - if info.Capabilities.WorkspaceAuthoring.IsSupported() { - factory = withAuthoringBridge(factory, bridge) - } - configuration, err := registry.Configuration(info.Kind) - if err != nil { - panic(err) - } - wrapped.RegisterKind(info, configuration, factory) - if executor, err := registry.ResolveExecutor(info.Kind); err == nil { - wrapped.RegisterExecutor(info.Kind, executor) - } - if prepare, err := registry.ResolvePreparation(info.Kind); err == nil { - wrapped.RegisterPreparation(info.Kind, info.Capabilities.WorkspaceReadPreparation.IsSupported(), prepare) - } - } - return wrapped -} diff --git a/apps/daemon/internal/cli/authoring_test.go b/apps/daemon/internal/cli/authoring_test.go deleted file mode 100644 index 709109256..000000000 --- a/apps/daemon/internal/cli/authoring_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package cli - -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - -import ( - "context" - "errors" - "net" - "os" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/authoring" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" -) - -func TestAuthoringSocketEndsWithTurnWhileSessionIsRetained(t *testing.T) { - home, err := os.MkdirTemp("/tmp", "pa-wrap-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(home) - t.Setenv("HOME", home) - env := map[string]any{"MODEL_KEY": "preserved"} - var path string - var events chan<- proto.Envelope - factory := withAuthoringBridge(func(_ context.Context, req proto.PromptRequestPayload, out chan<- proto.Envelope) (agent.Session, error) { - injected := req.AgentOptions["env"].(map[string]any) - path, _ = injected[proto.AuthoringSocketEnv].(string) - if path == "" || injected["MODEL_KEY"] != "preserved" { - t.Fatal("missing per-run context") - } - events = out - return nil, nil - }, authoring.New(nil)) - out := make(chan proto.Envelope, 1) - _, err = factory(t.Context(), proto.PromptRequestPayload{RunID: "run", WorkspaceAuthoring: true, AgentOptions: map[string]any{"env": env}}, out) - if err != nil { - t.Fatal(err) - } - if _, exists := env[proto.AuthoringSocketEnv]; exists { - t.Fatal("mutated caller environment") - } - if _, err := os.Stat(path); err != nil { - t.Fatal(err) - } - done, _ := proto.NewEnvelope(proto.TypeDone, "run", proto.DonePayload{}) - events <- done - select { - case <-out: - case <-time.After(time.Second): - t.Fatal("terminal event blocked") - } - if conn, err := net.DialTimeout("unix", path, time.Second); err == nil { - _ = conn.Close() - t.Fatal("authoring remained available after done") - } - close(events) -} - -func TestAuthoringRegistryRequiresExplicitCapability(t *testing.T) { - root, err := os.MkdirTemp("/tmp", "pa-opt-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(root) - t.Setenv("OAC_RUNTIME_HOME", root) - for _, optIn := range []bool{false, true} { - reg := agent.NewRegistry() - called := false - stop := errors.New("controlled factory stop") - out := make(chan proto.Envelope, 1) - reg.RegisterKind(proto.SupportedAgentKind{Kind: "engine", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{WorkspaceAuthoring: proto.CapabilityFromBool(optIn)})}, harnessconfig.Configuration{}, func(_ context.Context, req proto.PromptRequestPayload, events chan<- proto.Envelope) (agent.Session, error) { - called = true - env, _ := req.AgentOptions["env"].(map[string]any) - socket, _ := env[proto.AuthoringSocketEnv].(string) - if (socket != "") != optIn { - t.Fatal("authoring capability was not respected") - } - if optIn { - if _, err := os.Stat(socket); err != nil { - t.Fatal(err) - } - } else if events != out { - t.Fatal("non-product event channel was wrapped") - } - return nil, stop - }) - wrapped := authoringRegistry(reg, authoring.New(nil)) - factory, err := wrapped.Resolve("engine") - if err != nil { - t.Fatal(err) - } - _, err = factory(t.Context(), proto.PromptRequestPayload{RunID: "run", WorkspaceAuthoring: true}, out) - if !called || !errors.Is(err, stop) { - t.Fatal("registered factory was not preserved", err) - } - } -} 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 3b8a14647..43a162377 100644 --- a/apps/daemon/internal/cli/claude_sdk_live_linux_test.go +++ b/apps/daemon/internal/cli/claude_sdk_live_linux_test.go @@ -13,7 +13,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/authoring" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/google/uuid" @@ -73,7 +72,6 @@ func TestLiveRegisteredClaudeSDK(t *testing.T) { t.Helper() reg := agent.NewRegistry() registerAgentKinds(reg, discovery) - reg = authoringRegistry(reg, authoring.New(nil)) sender := make(registeredSDKSender, 256) router, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender}) if err != nil { @@ -89,7 +87,7 @@ func TestLiveRegisteredClaudeSDK(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second) defer cancel() id := uuid.NewString() - request := proto.PromptRequestPayload{RunID: id, AgentKind: "claude_sdk", Input: proto.TextInput(prompt), AgentStateKey: "registered-acceptance", AgentSessionID: resume, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, ObserveToolObservations: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": nil}} + request := proto.PromptRequestPayload{RunID: id, AgentKind: "claude_sdk", Input: proto.TextInput(prompt), AgentStateKey: "registered-acceptance", AgentSessionID: resume, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, AgentOptions: map[string]any{"model": "MiniMax-M3", "system_prompt": nil}} 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}`)}} } @@ -182,7 +180,7 @@ func TestLiveRegisteredClaudeSDK(t *testing.T) { if third.Outcome.Metadata[proto.DoneMetaAgentSessionID] != id || !strings.Contains(third.Outcome.Content, nonce) { t.Fatal("registered cold continuation lost identity or history") } - data, _ := json.MarshalIndent(map[string]any{"scope": "SDK-only readiness and production registration/authoring registry -> daemon router -> pinned SDK/native -> real MiniMax; function receipt, cancellation and cold continuation; public API admission remains separate", "descriptor": discovery[0].runtime.Info, "entrypoint": entrypoint, "verification_value": nonce, "executions": []execution{first, second, third}}, "", " ") + data, _ := json.MarshalIndent(map[string]any{"scope": "SDK-only readiness and production registration -> daemon router -> pinned SDK/native -> real MiniMax; function receipt, cancellation and cold continuation; public API admission remains separate", "descriptor": discovery[0].runtime.Info, "entrypoint": entrypoint, "verification_value": nonce, "executions": []execution{first, second, third}}, "", " ") if err := os.WriteFile(filepath.Join(root, "proof.json"), data, 0o600); err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/cli/companion_path.go b/apps/daemon/internal/cli/companion_path.go deleted file mode 100644 index d83814828..000000000 --- a/apps/daemon/internal/cli/companion_path.go +++ /dev/null @@ -1,18 +0,0 @@ -package cli - -import ( - "os" - "path/filepath" -) - -func addCompanionCLIPath(env map[string]any, dir string) { - info, err := os.Stat(filepath.Join(dir, "parsar")) - if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0o111 == 0 { - return - } - path, ok := env["PATH"].(string) - if !ok { - path = os.Getenv("PATH") - } - env["PATH"] = dir + string(os.PathListSeparator) + path -} diff --git a/apps/daemon/internal/cli/companion_path_test.go b/apps/daemon/internal/cli/companion_path_test.go deleted file mode 100644 index 4d8b560fa..000000000 --- a/apps/daemon/internal/cli/companion_path_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package cli - -import ( - "os" - "path/filepath" - "testing" -) - -func TestCompanionCLIPath(t *testing.T) { - for _, tc := range []struct { - name string - mode os.FileMode - want bool - }{ - {"executable", 0o700, true}, - {"download awaiting review", 0o600, false}, - {"missing", 0, false}, - } { - t.Run(tc.name, func(t *testing.T) { - dir := t.TempDir() - if tc.mode != 0 { - if err := os.WriteFile(filepath.Join(dir, "parsar"), []byte("binary"), tc.mode); err != nil { - t.Fatal(err) - } - } - env := map[string]any{"PATH": "/existing/tools", "OTHER": "retained"} - addCompanionCLIPath(env, dir) - want := "/existing/tools" - if tc.want { - want = dir + string(os.PathListSeparator) + want - } - if env["PATH"] != want || env["OTHER"] != "retained" { - t.Fatalf("unexpected child environment: %v", env) - } - }) - } -} diff --git a/apps/daemon/internal/cli/connect.go b/apps/daemon/internal/cli/connect.go index ae295f0aa..32b8e885c 100644 --- a/apps/daemon/internal/cli/connect.go +++ b/apps/daemon/internal/cli/connect.go @@ -12,7 +12,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/auth" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/authoring" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" @@ -427,8 +426,6 @@ func pumpConn(parentCtx context.Context, conn *transport.Conn, registry *agent.R if err != nil { return err } - bridge := authoring.New(conn) - registry = authoringRegistry(registry, bridge) router, err := dispatch.New(dispatch.Config{ Registry: registry, Sender: conn, @@ -471,10 +468,6 @@ func pumpConn(parentCtx context.Context, conn *transport.Conn, registry *agent.R obslog.Bg().Warn("pumpConn: recvCh closed", "err", conn.Err()) return conn.Err() } - if env.Type == proto.TypeAuthoringResponse { - bridge.Deliver(env) - continue - } obslog.Bg().Info("pumpConn: received envelope, calling router.Handle", "type", env.Type, "id", env.ID) if err := router.Handle(parentCtx, env); err != nil { obslog.Bg().Error("router.Handle failed", "type", env.Type, "id", env.ID, "err", err) diff --git a/apps/daemon/internal/cli/connect_cleanup_test.go b/apps/daemon/internal/cli/connect_cleanup_test.go index e8ef2d21e..c8f92a00e 100644 --- a/apps/daemon/internal/cli/connect_cleanup_test.go +++ b/apps/daemon/internal/cli/connect_cleanup_test.go @@ -148,7 +148,7 @@ func testDisconnectedPumpCleanup(t *testing.T, suspend bool) { } defer peer.Close() env, err := proto.NewEnvelope(proto.TypeExecutionPrepare, "prepare", proto.ExecutionPreparePayload{SessionID: "cleanup", - Configuration: proto.PromptRequestPayload{AgentKind: "cleanup", AgentStateKey: "agents-api-cleanup", StrictResume: true, DisableExecutionEnvironment: true}}) + Configuration: proto.PromptRequestPayload{AgentKind: "cleanup", AgentStateKey: "agents-api-cleanup", DisableExecutionEnvironment: true}}) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/cli/connect_suspend.go b/apps/daemon/internal/cli/connect_suspend.go index 5050d6e06..b1146b94d 100644 --- a/apps/daemon/internal/cli/connect_suspend.go +++ b/apps/daemon/internal/cli/connect_suspend.go @@ -7,7 +7,6 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/authoring" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/transport" @@ -34,7 +33,6 @@ func (s *reconnectSender) replace(conn *transport.Conn) { s.mu.Lock(); s.conn = type suspendedRouter struct { router *dispatch.Router - bridge *authoring.Bridge sender *reconnectSender registry *agent.Registry local *localworkspace.Binding @@ -46,13 +44,11 @@ func newSuspendedRouter(conn *transport.Conn, registry *agent.Registry) (*suspen return nil, err } sender := &reconnectSender{conn: conn} - bridge := authoring.New(sender) - wrapped := authoringRegistry(registry, bridge) - router, err := dispatch.New(dispatch.Config{Registry: wrapped, Sender: sender, Log: obslog.Bg(), LocalWorkspace: local}) + router, err := dispatch.New(dispatch.Config{Registry: registry, Sender: sender, Log: obslog.Bg(), LocalWorkspace: local}) if err != nil { return nil, err } - return &suspendedRouter{router: router, bridge: bridge, sender: sender, registry: wrapped, local: local}, nil + return &suspendedRouter{router: router, sender: sender, registry: registry, local: local}, nil } func (s *suspendedRouter) shutdown() { @@ -177,10 +173,6 @@ func (s *suspendedRouter) pump(ctx context.Context, conn *transport.Conn, boot * if !ok { return nil, conn.Err() } - if env.Type == proto.TypeAuthoringResponse { - s.bridge.Deliver(env) - continue - } if env.Type == proto.TypeEnvironmentResume { request := rejectedResumeRequest(env) code := "not_suspended" diff --git a/apps/daemon/internal/cli/preparation_test.go b/apps/daemon/internal/cli/preparation_test.go index df9082731..7df3223e6 100644 --- a/apps/daemon/internal/cli/preparation_test.go +++ b/apps/daemon/internal/cli/preparation_test.go @@ -1,19 +1,16 @@ package cli -import "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" - import ( "context" - "errors" "testing" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/authoring" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto/prototest" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" ) -func TestPreparationRegistrationBypassesProductWrappers(t *testing.T) { +func TestPreparationRegistrationFollowsNativeSupport(t *testing.T) { for _, supported := range []bool{false, true} { reg := agent.NewRegistry() info := proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilityFromBool(supported)})} @@ -33,30 +30,15 @@ func TestPreparationRegistrationBypassesProductWrappers(t *testing.T) { if !supported { continue } - stop := errors.New("controlled preparation stop") - reg.RegisterPreparation("codex", true, func(_ context.Context, req proto.PromptRequestPayload) (agent.Prepared, error) { - if req.RunID != "" || req.WorkspaceAuthoring || len(req.AgentOptions) != 0 { - t.Error("product wrapper injected preparation context") - } - return nil, stop - }) - wrapped := authoringRegistry(reg, authoring.New(nil)) - prepare, err := wrapped.ResolvePreparation("codex") - if err != nil { - t.Fatal(err) - } - if _, err := prepare(t.Context(), proto.PromptRequestPayload{AgentKind: "codex"}); !errors.Is(err, stop) { - t.Fatal("raw preparation was lost or wrapped", err) - } - for _, info := range wrapped.SupportedAgentKinds() { + for _, info := range reg.SupportedAgentKinds() { if info.Kind == "codex" && (!info.Capabilities.Preparation.IsSupported() || !info.Capabilities.WorkspaceReadPreparation.IsSupported()) { - t.Fatal("real heartbeat registry lost preparation") + t.Fatal("heartbeat registry lost preparation") } } - wrapped.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { - return nil, stop + reg.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) { + return nil, nil }) - if _, err := wrapped.ResolvePreparation("codex"); err == nil { + if _, err := reg.ResolvePreparation("codex"); err == nil { t.Fatal("factory replacement retained stale preparation") } } diff --git a/apps/daemon/internal/dispatch/executor.go b/apps/daemon/internal/dispatch/executor.go index 4e7389cc4..7ac4223b6 100644 --- a/apps/daemon/internal/dispatch/executor.go +++ b/apps/daemon/internal/dispatch/executor.go @@ -45,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.ConversationID != "" || req.WorkspaceAuthoring || req.AgentStateKey != "agents-api-"+input.SessionID || !req.StrictResume { + if strings.TrimSpace(input.SessionID) == "" || req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" || req.AgentStateKey != "agents-api-"+input.SessionID { return r.rejectPreparation(env, "invalid_configuration") } caps, available := r.availableCapabilities(req.AgentKind) diff --git a/apps/daemon/internal/dispatch/executor_handoff_test.go b/apps/daemon/internal/dispatch/executor_handoff_test.go index b0fc793fa..60f194351 100644 --- a/apps/daemon/internal/dispatch/executor_handoff_test.go +++ b/apps/daemon/internal/dispatch/executor_handoff_test.go @@ -150,7 +150,7 @@ func TestPreparedDonePublishesAfterExecutorHandoff(t *testing.T) { } } } - request := proto.ExecutionPreparePayload{SessionID: "session", Configuration: proto.PromptRequestPayload{AgentKind: "handoff", AgentStateKey: "agents-api-session", StrictResume: true, DisableExecutionEnvironment: true}} + request := proto.ExecutionPreparePayload{SessionID: "session", Configuration: proto.PromptRequestPayload{AgentKind: "handoff", AgentStateKey: "agents-api-session", 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 c8db7c741..55686fd2a 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 } func executorRequest() proto.ExecutionPreparePayload { - return proto.ExecutionPreparePayload{SessionID: "session", Configuration: proto.PromptRequestPayload{AgentKind: "reusable", AgentStateKey: "agents-api-session", StrictResume: true, DisableExecutionEnvironment: true}} + return proto.ExecutionPreparePayload{SessionID: "session", Configuration: proto.PromptRequestPayload{AgentKind: "reusable", AgentStateKey: "agents-api-session", DisableExecutionEnvironment: true}} } func executorRouter(t *testing.T, owner *reusableExecutor, idle time.Duration) (*dispatch.Router, *recSender, *atomic.Int32) { t.Helper() diff --git a/apps/daemon/internal/dispatch/functions_native_test.go b/apps/daemon/internal/dispatch/functions_native_test.go index 036ea1c8d..f541a43a8 100644 --- a/apps/daemon/internal/dispatch/functions_native_test.go +++ b/apps/daemon/internal/dispatch/functions_native_test.go @@ -138,7 +138,7 @@ func TestNativeFunctionBridge(t *testing.T) { nativeID := "" for index := 0; index < 3; index++ { run := fmt.Sprintf("run-%d", index) - request := proto.PromptRequestPayload{AgentKind: "codex", Input: proto.TextInput("Look up ticket 42."), RunID: run, AgentStateKey: "native-functions", AgentSessionID: nativeID, StrictResume: true, ReleaseOnCompletion: true, DisableExecutionEnvironment: true, ObserveToolObservations: true, + request := proto.PromptRequestPayload{AgentKind: "codex", Input: proto.TextInput("Look up ticket 42."), RunID: run, AgentStateKey: "native-functions", AgentSessionID: nativeID, ReleaseOnCompletion: true, DisableExecutionEnvironment: true, FunctionTools: []proto.FunctionTool{{Name: "lookup_ticket", Description: "Read a synthetic ticket", Parameters: json.RawMessage(`{"type":"object","properties":{"ticket":{"type":"string"}},"required":["ticket"],"additionalProperties":false}`)}}, AgentOptions: map[string]any{"model": "gpt-5.5", "model_provider": map[string]any{"protocol": "responses", "base_url": model.URL + "/v1", "api_key": "synthetic-local-token"}}} env, _ := proto.NewEnvelope(proto.TypePromptRequest, run, request) diff --git a/apps/daemon/internal/dispatch/local_directory_test.go b/apps/daemon/internal/dispatch/local_directory_test.go index b0363bf24..d06af72aa 100644 --- a/apps/daemon/internal/dispatch/local_directory_test.go +++ b/apps/daemon/internal/dispatch/local_directory_test.go @@ -43,7 +43,7 @@ func TestLocalDirectoryPreparationNeedsNoHarnessAndRejectsOtherOwners(t *testing t.Fatal(err) } t.Cleanup(func() { _ = r.Shutdown(context.Background()) }) - request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, AgentStateKey: "agents-api-" + session, StrictResume: true, ReleaseOnCompletion: true, WorkspaceReadOnly: true} + request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, AgentStateKey: "agents-api-" + session, ReleaseOnCompletion: true, WorkspaceReadOnly: true} if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "idle", proto.ExecutionPreparePayload{Configuration: request})); err != nil { t.Fatal(err) } @@ -123,7 +123,7 @@ func TestLocalDirectoryKeepsNotDirectorySeparateFromFailures(t *testing.T) { t.Fatal(err) } t.Cleanup(func() { _ = r.Shutdown(context.Background()) }) - request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, AgentStateKey: "agents-api-" + session, StrictResume: true, ReleaseOnCompletion: true, WorkspaceReadOnly: true} + request := proto.PromptRequestPayload{AgentKind: "native", LocalEnvironment: &proto.LocalEnvironment{ID: environment}, AgentStateKey: "agents-api-" + session, ReleaseOnCompletion: true, WorkspaceReadOnly: true} if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "idle", proto.ExecutionPreparePayload{Configuration: request})); err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/dispatch/preparation.go b/apps/daemon/internal/dispatch/preparation.go index e77761f09..36695ab7a 100644 --- a/apps/daemon/internal/dispatch/preparation.go +++ b/apps/daemon/internal/dispatch/preparation.go @@ -63,7 +63,7 @@ func (r *Router) handleExecutionPrepare(ctx context.Context, env proto.Envelope) if req, err = r.localWorkspace.Configure(req); err != nil { return r.rejectPreparation(env, "invalid_configuration") } - if req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" || req.WorkspaceAuthoring || req.EnvironmentID() == "" || strings.TrimSpace(req.AgentStateKey) == "" || !req.StrictResume || !req.ReleaseOnCompletion { + if req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" || req.EnvironmentID() == "" || strings.TrimSpace(req.AgentStateKey) == "" || !req.ReleaseOnCompletion { return r.rejectPreparation(env, "invalid_configuration") } if validateExecutionEnvironment(req, caps) != nil || (len(req.FunctionTools) > 0 && !caps.FunctionTools.IsSupported()) { diff --git a/apps/daemon/internal/dispatch/preparation_test.go b/apps/daemon/internal/dispatch/preparation_test.go index 7d4a9de4e..64f85b1bf 100644 --- a/apps/daemon/internal/dispatch/preparation_test.go +++ b/apps/daemon/internal/dispatch/preparation_test.go @@ -113,7 +113,7 @@ func localPreparationHarness(t *testing.T) *harness { } func preparationRequest() proto.ExecutionPreparePayload { - return proto.ExecutionPreparePayload{SessionID: preparationSessionID, Configuration: proto.PromptRequestPayload{AgentKind: "prepared", AgentStateKey: "agents-api-" + preparationSessionID, StrictResume: true, ReleaseOnCompletion: true, LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID, NetworkAccess: "enabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}}} + return proto.ExecutionPreparePayload{SessionID: preparationSessionID, Configuration: proto.PromptRequestPayload{AgentKind: "prepared", AgentStateKey: "agents-api-" + preparationSessionID, ReleaseOnCompletion: true, LocalEnvironment: &proto.LocalEnvironment{ID: preparationEnvironmentID, NetworkAccess: "enabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}}} } func preparationRouter(t *testing.T, sender dispatch.Sender, timeout time.Duration, factory agent.PreparationFactory) *dispatch.Router { @@ -372,12 +372,10 @@ func TestPreparationRejectsInputAndProductConfiguration(t *testing.T) { "run": func(p *proto.PromptRequestPayload) { p.RunID = "run" }, "input": func(p *proto.PromptRequestPayload) { p.Input = proto.TextInput("input") }, "conversation": func(p *proto.PromptRequestPayload) { p.ConversationID = "product" }, - "authoring": func(p *proto.PromptRequestPayload) { p.WorkspaceAuthoring = true }, "attachment": func(p *proto.PromptRequestPayload) { p.Input = proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image"}}}} }, "missing environment": func(p *proto.PromptRequestPayload) { p.LocalEnvironment = nil }, - "resume": func(p *proto.PromptRequestPayload) { p.StrictResume = false }, } { t.Run(name, func(t *testing.T) { r := preparationRouter(t, &recSender{}, time.Minute, func(context.Context, proto.PromptRequestPayload) (agent.Prepared, error) { diff --git a/apps/daemon/internal/localworkspace/binding.go b/apps/daemon/internal/localworkspace/binding.go index c56610270..fe8bca207 100644 --- a/apps/daemon/internal/localworkspace/binding.go +++ b/apps/daemon/internal/localworkspace/binding.go @@ -69,8 +69,7 @@ func (b *Binding) Configure(r proto.PromptRequestPayload) (proto.PromptRequestPa return r, nil } if b == nil || r.LocalEnvironment == nil || r.LocalEnvironment.ID != b.environment || r.AgentStateKey != b.stateKey || - r.DisableExecutionEnvironment || - r.ConversationID != "" || r.WorkspaceAuthoring || !r.StrictResume { + r.DisableExecutionEnvironment || r.ConversationID != "" { return r, errors.New("request does not match the dedicated local Environment") } if !r.WorkspaceReadOnly || r.LocalEnvironment.NetworkAccess != "" || len(r.LocalEnvironment.AllowedDomains) > 0 { diff --git a/apps/daemon/internal/localworkspace/binding_test.go b/apps/daemon/internal/localworkspace/binding_test.go index 372cc8ec7..9aad690f0 100644 --- a/apps/daemon/internal/localworkspace/binding_test.go +++ b/apps/daemon/internal/localworkspace/binding_test.go @@ -26,7 +26,7 @@ func testBinding(t *testing.T) (*Binding, proto.PromptRequestPayload) { } b.networkAccess = "disabled" b.capabilityRoot = t.TempDir() - return b, proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{ID: environment, NetworkAccess: "disabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}, AgentStateKey: "agents-api-" + session, StrictResume: true, ReleaseOnCompletion: true} + return b, proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{ID: environment, NetworkAccess: "disabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}, AgentStateKey: "agents-api-" + session, ReleaseOnCompletion: true} } func TestBindingRejectsScopeOverrides(t *testing.T) { @@ -40,10 +40,8 @@ func TestBindingRejectsScopeOverrides(t *testing.T) { "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 }, - "product authoring": func(r *proto.PromptRequestPayload) { r.WorkspaceAuthoring = true }, - "non-strict resume": func(r *proto.PromptRequestPayload) { r.StrictResume = false }, + "other Session": func(r *proto.PromptRequestPayload) { r.AgentStateKey = "agents-api-" + uuid.NewString() }, + "none": func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = true }, } { t.Run(name, func(t *testing.T) { r := valid diff --git a/apps/daemon/internal/localworkspace/runtime_initialization_process.go b/apps/daemon/internal/localworkspace/runtime_initialization_process.go index e98dc070b..68174485d 100644 --- a/apps/daemon/internal/localworkspace/runtime_initialization_process.go +++ b/apps/daemon/internal/localworkspace/runtime_initialization_process.go @@ -110,7 +110,7 @@ func runInitializationProcess(ctx context.Context, binary string, args []string, return &InitializationFailure{} } process, err := clirunner.Start(clirunner.StartOptions{Parent: operation, Binary: binary, Args: args, - Dir: directory, Env: env, OwnProcessGroup: true, KillTimeout: 250 * time.Millisecond}) + Dir: directory, Env: env, KillTimeout: 250 * time.Millisecond}) if err != nil { return ErrInitializationUnconfirmed } diff --git a/apps/daemon/testdata/onboarding/main.go b/apps/daemon/testdata/onboarding/main.go index 8ecc4dbaf..4ead4f620 100644 --- a/apps/daemon/testdata/onboarding/main.go +++ b/apps/daemon/testdata/onboarding/main.go @@ -40,7 +40,7 @@ func (h *harness) prepare(_ context.Context, req proto.PromptRequestPayload) (ag if req.RunID != "" || len(req.Input) != 0 || req.ConversationID != "" { return nil, errors.New("preparation submitted fixture input") } - if !req.StrictResume || !req.DisableExecutionEnvironment || !req.DisableSubagents || len(req.FunctionTools) > 0 || req.MCPHTTPServers != nil { + if !req.DisableExecutionEnvironment || !req.DisableSubagents || len(req.FunctionTools) > 0 || req.MCPHTTPServers != nil { return nil, errors.New("unsupported fixture operation") } h.mu.Lock() diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 0d6fba074..8d5c7459d 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -151,8 +151,6 @@ A Harness that supports the Subagent reads implements the [neutral observation c Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`, then add it to `harnessDeclarations` in [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go). The declaration contains the kind and complete capability descriptor, the shared model `Configuration` and a `Discover` function. Discovery receives the profile and diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor and session, preparation and Executor factories. Return nil when the adapter is not configured; return an unavailable descriptor with a session factory when configured prerequisites fail. Keep version gates and factory-selection conditions inside the adapter. -An adapter that supports product workspace authoring declares `WorkspaceAuthoring` itself; common registration does not grant it. - [`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) iterates the discovered runtimes and calls `Registry.Register` from `agent/harness.go`. It verifies that discovery retained the declared kind and installs factories in this order: | Order | Method | Registers | @@ -202,7 +200,7 @@ Run the `engine` and `execution` tests for omission, policy, combination and err ## Native model configuration -[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) owns the shared configuration declaration and pure preparation contract. Each adapter supplies one `Configuration`, in `internal/harnessconfig/`, to Core's composition and to the Runtime's `RegisterKind`. The direct factory, preparation and Executor paths all validate through that declaration before native side effects, and Registry wrappers keep the declaration with the factory. The wire object is `proto.HarnessConfig`. [Model execution](./model-execution.md#native-model-parameters) lists each Harness's accepted fields. +[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) owns the shared configuration declaration and pure preparation contract. Each adapter supplies one `Configuration`, in `internal/harnessconfig/`, to Core's composition and to the Runtime's `RegisterKind`. The direct factory, preparation and Executor paths all validate through that declaration before native side effects. The wire object is `proto.HarnessConfig`. [Model execution](./model-execution.md#native-model-parameters) lists each Harness's accepted fields. A supplied `model` must be a nonempty string, and an explicit `model_provider` requires it. The native-owned connection path may omit both; explicit null is invalid. An explicitly empty declaration accepts no provider or nonempty native parameters and advertises no provider support. Unknown protocol formats and duplicate protocol declarations fail at registration. @@ -247,7 +245,7 @@ An adapter may supply `agent.Installation` from `installation.go` in its own pac ## Native process ownership -The daemon's `clirunner` offers opt-in Unix process-group ownership for adapters whose SDK launches a native child; unsupported hosts reject this mode before launch. Explicit and parent-context cancellation share a TERM grace period (three seconds by default) and a bounded KILL escalation. An internal reaper also cleans remaining group members when the direct process exits, even if a descendant still holds stdout open; during cancellation, surviving descendants keep the remaining grace after the leader exits. The daemon's `stop` command waits up to ten seconds for confirmed shutdown, which covers that grace period and the pipe and owner cleanup after it. +The daemon's `clirunner` starts every native child in its own Unix process group (a Job object on Windows); other hosts reject the launch. Explicit and parent-context cancellation share a TERM grace period (three seconds by default) and a bounded KILL escalation. An internal reaper also cleans remaining group members when the direct process exits, even if a descendant still holds stdout open; during cancellation, surviving descendants keep the remaining grace after the leader exits. The daemon's `stop` command waits up to ten seconds for confirmed shutdown, which covers that grace period and the pipe and owner cleanup after it. Owned output pipes stay readable after the leader exits. Consumers drain stdout and stderr before calling `Wait`, which joins the cached process result and closes the readers. `Done` reports leader reaping and group cleanup signals; it is not a native execution receipt or proof of persisted history. SDK adapters settle each Turn and drain its observations before publishing completion, and Executor close also closes the query and awaits the native child. Process groups are lifecycle supervision, not isolation or containment of descendants that leave the group. diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index b22b086bf..e5252a97d 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "将原生 Harness 添加到 OpenAgentCore" source: contracts/agents-api/harness-onboarding.md -source_hash: 32adff66e2fb33956e96f9a453518d70a3f87056eb777e82a60e087fe4e5aa31 +source_hash: cad1a3666f3c0d4c460bb6213da4bcca89c156fc387b1355d683593b55d025a0 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、Core 资格认定和验收。[Harness capabilities](harness-capabilities.md) 记录了当前每个 Harness 支持的功能。 @@ -153,8 +153,6 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`,然后将其添加到 [`cli/agent_discovery.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_discovery.go) 的 `harnessDeclarations` 中。声明包含 kind、完整能力描述符、共享模型 `Configuration` 和 `Discover` 函数。发现过程接收 profile 和诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符、session 工厂、准备工厂和 Executor 工厂。未配置适配器时返回 nil;已配置的前置条件失败时,返回不可用描述符和 session 工厂。将版本门控和工厂选择条件保留在适配器内部。 -支持产品工作区创作功能的适配器自行声明 `WorkspaceAuthoring`;通用注册不会授予该能力。 - [`cli/agent_registration.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_registration.go) 遍历已发现的 Runtime,并调用 `agent/harness.go` 中的 `Registry.Register`。它验证发现过程是否保留了声明的 kind,并按以下顺序安装工厂: | 顺序 | 方法 | 注册内容 | @@ -204,7 +202,7 @@ profile 是纯逻辑:它使用现有的公共类型和协议类型,声明受 ## 原生模型配置 {#native-model-configuration} -[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 负责共享配置声明和纯准备契约。每个适配器在 `internal/harnessconfig/` 中提供一个 `Configuration`,供 Core 组合和 Runtime 的 `RegisterKind` 使用。直接调用工厂、准备路径和 Executor 路径都会在产生原生副作用之前通过该声明进行验证,而 Registry 包装器会将声明与工厂保留在一起。线协议对象是 `proto.HarnessConfig`。[Model execution](model-execution.md#native-model-parameters) 列出了每个 Harness 接受的字段。 +[`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 负责共享配置声明和纯准备契约。每个适配器在 `internal/harnessconfig/` 中提供一个 `Configuration`,供 Core 组合和 Runtime 的 `RegisterKind` 使用。直接调用工厂、准备路径和 Executor 路径都会在产生原生副作用之前通过该声明进行验证。线协议对象是 `proto.HarnessConfig`。[Model execution](model-execution.md#native-model-parameters) 列出了每个 Harness 接受的字段。 提供的 `model` 必须是非空字符串,并且显式指定 `model_provider` 时必须提供它。原生所有权连接路径可以省略二者;显式 null 无效。显式为空的声明不接受任何 Provider 或非空原生参数,也不宣称支持 Provider。未知协议格式和重复协议声明会导致注册失败。 @@ -249,7 +247,7 @@ Environment 验收使用 `services/core/tests/official_environment_{templates,se ## 原生进程所有权 {#native-process-ownership} -daemon 的 `clirunner` 为 SDK 会启动原生子进程的适配器提供可选的 Unix 进程组所有权;不支持的主机会在启动前拒绝此模式。显式取消和父上下文取消共享 TERM 宽限期(默认为三秒)以及有界的 KILL 升级过程。当直接进程退出时,内部回收器也会清理进程组的剩余成员,即使某个后代进程仍保持 stdout 打开;在取消过程中,主进程退出后,存活的后代进程仍会保留剩余宽限时间。daemon 的 `stop` 命令最多等待十秒以确认关闭,这涵盖该宽限期以及之后的管道和所有者清理。 +daemon 的 `clirunner` 让每个原生子进程在自己的 Unix 进程组中启动(Windows 上为 Job 对象);其他主机会拒绝启动。显式取消和父上下文取消共享 TERM 宽限期(默认为三秒)以及有界的 KILL 升级过程。当直接进程退出时,内部回收器也会清理进程组的剩余成员,即使某个后代进程仍保持 stdout 打开;在取消过程中,主进程退出后,存活的后代进程仍会保留剩余宽限时间。daemon 的 `stop` 命令最多等待十秒以确认关闭,这涵盖该宽限期以及之后的管道和所有者清理。 所属输出管道在主进程退出后仍可读取。消费者在调用 `Wait` 之前耗尽 stdout 和 stderr;`Wait` 会汇合缓存的进程结果并关闭读取器。`Done` 报告主进程回收和进程组清理信号;它不是原生执行回执,也不是历史已持久化的证据。SDK 适配器会结算每个 Turn,并在发布完成状态前耗尽其观察结果;Executor 关闭还会关闭 Query 并等待原生子进程。进程组用于生命周期监管,而不是隔离或遏制离开进程组的后代进程。 diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index a0398c22a..fdd0e943b 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -51,7 +51,7 @@ A declaration describes what the Runtime can do. Core admits a public feature on | `message_images`, `function_result_images` | A message, or a function result, carries an image | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | The Agent declares HTTP MCP servers; one is `required`; a Vault credential is selected for one | -`permissions` gates permission decisions inside the Runtime, and `workspace_authoring` gates the daemon's authoring command. Core has no admission rule for `usage` and `resume`. +`permissions` gates permission decisions inside the Runtime. Core has no admission rule for `usage` and `resume`. The prompt request (`prompt_request`, or the configuration of `execution_prepare`) carries the opt-ins Core sets for each Run: @@ -59,17 +59,16 @@ The prompt request (`prompt_request`, or the configuration of `execution_prepare | --- | --- | | `agent_options` | Always `model` and `system_prompt` from the Agent; `model_provider` when the Session froze one; `harness_config` when the Agent sets `x_agents_core.harness_config`. Core sends no other key; the Runtime rejects any other key with `unsupported_configuration` before preparation | | `execution_controls` | Always: web search `disabled`, 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_tool_observations` | Always. Tool-call frames then carry the engine-neutral `observation` | | `observe_messages` | When the Runtime declares `message_items`. Text deltas then carry the native item ID, and `output_message` frames report message start, completion, phase and the completion text | | `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 | -| `strict_resume`, `require_existing_native_session` | Always strict; the second when a native Session must be recovered | +| `require_existing_native_session` | When a native Session must be recovered | | `durable_receipt` on `prompt_steer` | For every active input Core delivers | An execution configuration requires exactly one of `local_environment` and `disable_execution_environment`; `execution_prepare` rejects neither or both with `unsupported_configuration`. -Requests without an opt-in keep the frames and fields they had without it. +Requests without an opt-in keep the frames and fields they had without it. A `tool_call` frame carries the engine-neutral `observation` whenever the adapter maps the native tool. ## Envelope and identity diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 050b36d91..0a13af57b 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: 13766041c9ed0c012ceb5051fe43dcc8a1738a198fe325aad15f1902fe373ee3 +source_hash: fd5bcb71c9234310a449fad97a30a899027133ca8df862e8a3306f6bd3dacae8 --- 此协议在 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)。 @@ -53,7 +53,7 @@ wire 上每个字段都是 JSON boolean,所有字段都必须出现,包括 ` | `message_images`, `function_result_images` | 消息或 function result 携带图像 | | `mcp_http_tools`, `mcp_http_required`, `mcp_http_bearer_auth` | Agent 声明 HTTP MCP server;其中一个为 `required`;其中一个选用了 Vault 凭据 | -`permissions` 控制 Runtime 内部权限决定,`workspace_authoring` 控制 daemon 的 authoring 命令。Core 对 `usage` 和 `resume` 没有准入规则。 +`permissions` 控制 Runtime 内部权限决定。Core 对 `usage` 和 `resume` 没有准入规则。 prompt 请求(`prompt_request` 或 `execution_prepare` 的配置)携带 Core 为各 Run 设置的显式启用项: @@ -61,17 +61,16 @@ prompt 请求(`prompt_request` 或 `execution_prepare` 的配置)携带 Core | --- | --- | | `agent_options` | 始终设置 Agent 的 `model` 和 `system_prompt`;Session 冻结了 model provider 时设置 `model_provider`;Agent 设置 `x_agents_core.harness_config` 时设置 `harness_config`。Core 不发送其他键;Runtime 在准备之前以 `unsupported_configuration` 拒绝任何其他键 | | `execution_controls` | 始终设置:web search 为 `disabled`、解析后的 text verbosity(默认 `medium`)、明确禁用 programmatic tool calling,以及任何 `json_schema` 输出格式。原生选项名称由 adapter 负责 | -| `observe_tool_observations` | 始终设置。tool-call frame 随后携带与 engine 无关的 `observation` | | `observe_messages` | Runtime 声明 `message_items` 时设置。文本 delta 随后携带原生 item ID,`output_message` frame 报告消息开始、完成、phase 和完成文本 | | `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` | -| `strict_resume`, `require_existing_native_session` | 始终严格;需要恢复原生 Session 时设置第二项 | +| `require_existing_native_session` | 需要恢复原生 Session 时设置 | | `prompt_steer` 上的 `durable_receipt` | Core 交付的每个活动输入都设置 | 执行配置必须且只能包含 `local_environment` 和 `disable_execution_environment` 之一;两者都缺失或同时存在时,`execution_prepare` 以 `unsupported_configuration` 拒绝。 -未携带显式启用项的请求保留未启用时的 frame 和字段。 +未携带显式启用项的请求保留未启用时的 frame 和字段。只要 adapter 映射了原生工具,`tool_call` frame 就携带与 engine 无关的 `observation`。 ## Envelope 与身份 {#envelope-and-identity} diff --git a/internal/agentdaemon/proto/authoring.go b/internal/agentdaemon/proto/authoring.go deleted file mode 100644 index 338d77b5d..000000000 --- a/internal/agentdaemon/proto/authoring.go +++ /dev/null @@ -1,31 +0,0 @@ -package proto - -import "encoding/json" - -const ( - TypeAuthoringRequest = "authoring_request" - TypeAuthoringResponse = "authoring_response" - AuthoringContext = "context" - AuthoringSkillList = "skill.list" - AuthoringSkillRead = "skill.read" - AuthoringSkillCreate = "skill.create" - AuthoringSkillUpdate = "skill.update" - AuthoringPromptRead = "instructions.read" - AuthoringPromptWrite = "instructions.write" - AuthoringMaxBytes = 1 << 20 - AuthoringSocketEnv = "OAC_RUNTIME_DAEMON_SOCKET" -) - -// AuthoringRequestPayload uses Envelope.ID for the active run, never a client-supplied workspace or user. -type AuthoringRequestPayload struct { - RequestID string `json:"request_id,omitempty"` - Operation string `json:"operation"` - CapabilityID string `json:"capability_id,omitempty"` - Content string `json:"content,omitempty"` -} - -type AuthoringResponsePayload struct { - RequestID string `json:"request_id,omitempty"` - Data json.RawMessage `json:"data,omitempty"` - Error string `json:"error,omitempty"` -} diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index bb41594c7..73a49f6a8 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -226,7 +226,6 @@ type AgentKindCapabilities struct { Usage CapabilitySupport `json:"usage"` Resume CapabilitySupport `json:"resume"` NativeSessionRecovery CapabilitySupport `json:"native_session_recovery"` - WorkspaceAuthoring CapabilitySupport `json:"workspace_authoring"` Steering CapabilitySupport `json:"steering"` MessageItems CapabilitySupport `json:"message_items"` diff --git a/internal/agentdaemon/proto/outbound.go b/internal/agentdaemon/proto/outbound.go index 8f5db4e41..799d09835 100644 --- a/internal/agentdaemon/proto/outbound.go +++ b/internal/agentdaemon/proto/outbound.go @@ -45,8 +45,8 @@ type PromptRequestPayload struct { // dispatches to. AgentKind string `json:"agent_kind"` - // ConversationID lets the daemon scope per-conversation state - // (Claude --resume session id, scratch dir). + // ConversationID is the Core Session ID of a prompt_request Run. Preparation + // and Executor configurations leave it empty, and no adapter reads it. ConversationID string `json:"conversation_id"` // RunID is the ID of the Core Turn this prompt executes; mirrored @@ -73,21 +73,17 @@ type PromptRequestPayload struct { // 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"` - WorkspaceAuthoring bool `json:"workspace_authoring,omitempty"` + WorkspaceReadOnly bool `json:"workspace_read_only,omitempty"` + AgentStateKey string `json:"agent_state_key,omitempty"` // ReleaseOnCompletion closes the native writer before acknowledging Done. - ReleaseOnCompletion bool `json:"release_on_completion,omitempty"` - StrictResume bool `json:"strict_resume,omitempty"` - RequireExistingNativeSession bool `json:"require_existing_native_session,omitempty"` - ObserveMessages bool `json:"observe_messages,omitempty"` - - ObserveToolObservations bool `json:"observe_tool_observations,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"` + ReleaseOnCompletion bool `json:"release_on_completion,omitempty"` + RequireExistingNativeSession bool `json:"require_existing_native_session,omitempty"` + ObserveMessages bool `json:"observe_messages,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"` } // PromptCancelPayload optionally requests an application receipt; identity is on Envelope.ID. diff --git a/internal/agentdaemon/proto/preparation.go b/internal/agentdaemon/proto/preparation.go index b03ba0905..99c1dba13 100644 --- a/internal/agentdaemon/proto/preparation.go +++ b/internal/agentdaemon/proto/preparation.go @@ -9,9 +9,9 @@ const ( TypePreparationStatus = "preparation_status" ) -// ExecutionPreparePayload reuses execution configuration without accepting input -// or product authoring. SessionID identifies the immutable configuration owner; -// each request reserves a separate Turn admission on its Runtime Executor. +// ExecutionPreparePayload reuses execution configuration without accepting +// input. SessionID identifies the immutable configuration owner; each request +// reserves a separate Turn admission on its Runtime Executor. type ExecutionPreparePayload struct { SessionID string `json:"session_id"` Configuration PromptRequestPayload `json:"configuration"` diff --git a/internal/agentdaemon/proto/prototest/capabilities.go b/internal/agentdaemon/proto/prototest/capabilities.go index f40726262..f3337682f 100644 --- a/internal/agentdaemon/proto/prototest/capabilities.go +++ b/internal/agentdaemon/proto/prototest/capabilities.go @@ -18,7 +18,6 @@ func Capabilities(overrides proto.AgentKindCapabilities) proto.AgentKindCapabili Usage: proto.CapabilityUnsupported, Resume: proto.CapabilityUnsupported, NativeSessionRecovery: proto.CapabilityUnsupported, - WorkspaceAuthoring: proto.CapabilityUnsupported, Steering: proto.CapabilityUnsupported, MessageItems: proto.CapabilityUnsupported, ToolObservations: proto.CapabilityUnsupported, diff --git a/internal/agentdaemon/proto/prototest/wire.go b/internal/agentdaemon/proto/prototest/wire.go index d27120aad..7833863c9 100644 --- a/internal/agentdaemon/proto/prototest/wire.go +++ b/internal/agentdaemon/proto/prototest/wire.go @@ -110,7 +110,7 @@ func WireScenarios() []WireScenario { } prepare := send(Core, proto.TypeExecutionPrepare, PreparationID, proto.ExecutionPreparePayload{ SessionID: SessionID, - Configuration: proto.PromptRequestPayload{AgentKind: HarnessKind, AgentStateKey: StateKey, StrictResume: true, DisableExecutionEnvironment: true}, + Configuration: proto.PromptRequestPayload{AgentKind: HarnessKind, AgentStateKey: StateKey, DisableExecutionEnvironment: true}, }) started := []Step{ prepare, diff --git a/internal/agentdaemon/proto/workspace_read_preparation.go b/internal/agentdaemon/proto/workspace_read_preparation.go index f732a329b..5869ca3b2 100644 --- a/internal/agentdaemon/proto/workspace_read_preparation.go +++ b/internal/agentdaemon/proto/workspace_read_preparation.go @@ -4,10 +4,10 @@ package proto // 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.StrictResume && r.ReleaseOnCompletion && r.RunID == "" && len(r.Input) == 0 && + r.ReleaseOnCompletion && r.RunID == "" && len(r.Input) == 0 && r.ConversationID == "" && r.AgentSessionID == "" && - !r.RequireExistingNativeSession && !r.WorkspaceAuthoring && !r.DisableExecutionEnvironment && + !r.RequireExistingNativeSession && !r.DisableExecutionEnvironment && len(r.AgentOptions) == 0 && r.ExecutionControls == nil && r.MCPHTTPServers == nil && len(r.FunctionTools) == 0 && !r.ToolSearch && !r.ObserveMessages && - !r.ObserveToolObservations && !r.ObserveSubagentIdentities + !r.ObserveSubagentIdentities } diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index feca4d537..4cba3ec4c 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -48,7 +48,7 @@ Directory requests are bounded to 8 KiB and 1,000 immediate entries, with explic ### Command observations -With `ObserveToolObservations`, private workspace execution requires the packaged `workspace_command_observations` feature and emits the neutral command snapshots. Match root, current-query native Bash call/result identities after input; ignore historical replay, synthetic and child work. Preserve exact command text and the native per-call textual result, including native rendering or truncation. This is final native output, not incremental stdout/stderr or reconstructed interleaving. Native error results are failed; unambiguous structured interruption is incomplete. Missing results close as incomplete after the observation drain; query cancellation does not overwrite an already observed native failure. Do not infer an exit code from rendered text or supply cwd/duration without qualified native fields. Preparation alone emits no command. Cold continuation must not reissue historical observations. This private translation does not enable public workspace admission, Read/Edit Items or Files ownership. +Private workspace execution requires the packaged `workspace_command_observations` feature and emits the neutral command snapshots. Match root, current-query native Bash call/result identities after input; ignore historical replay, synthetic and child work. Preserve exact command text and the native per-call textual result, including native rendering or truncation. This is final native output, not incremental stdout/stderr or reconstructed interleaving. Native error results are failed; unambiguous structured interruption is incomplete. Missing results close as incomplete after the observation drain; query cancellation does not overwrite an already observed native failure. Do not infer an exit code from rendered text or supply cwd/duration without qualified native fields. Preparation alone emits no command. Cold continuation must not reissue historical observations. This private translation does not enable public workspace admission, Read/Edit Items or Files ownership. ### HTTP MCP @@ -66,11 +66,11 @@ Workspace deferred-function discovery uses native ToolSearch alongside the norma For unmanaged bootstrap, daemon `connect` optionally registers this factory as `claude_sdk` when the operator sets `OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT` to the absolute packaged `dist/main.js`. `OAC_RUNTIME_CLAUDE_SDK_NODE` selects Node (default: `node` on PATH). Discovery resolves Node once and checks that exact configuration before pairing; the SDK's bounded runtime check is independent of CLI version probes. A ready SDK alone is sufficient to start the daemon. No configuration means no SDK probe or descriptor; failed readiness reports an unavailable descriptor with a rejecting factory. Runtime checks establish local readiness, not provider authentication. Installed daemons use `start` and their verified installation manifest for adapter selection and activation; ambient activation variables cannot extend that selection. See [the native installation contract](../../deploy/README.md#native-daemon-installer). -SDK state lives under `paths.ProfileDir(profile)/runtime/claude-sdk`, independently of the replaceable runtime bundle. Both the entrypoint and managed state root must be absolute. Background re-execution inherits operator configuration; it does not persist provider credentials in pairing profiles. The daemon registers `claude_sdk` directly, without the `WorkspaceAuthoring` wrapper of its product agent kinds. It accepts no caller-supplied environment variables or business write authority. +SDK state lives under `paths.ProfileDir(profile)/runtime/claude-sdk`, independently of the replaceable runtime bundle. Both the entrypoint and managed state root must be absolute. Background re-execution inherits operator configuration; it does not persist provider credentials in pairing profiles. It accepts no caller-supplied environment variables or business write authority. ### Descriptor and execution profile -The SDK descriptor advertises the validated daemon subset, including durable Turns/input receipts, text observations, function tools, raw usage and restrictive execution controls. It does not advertise permissions, product authoring, raw tool Items, general web-search control or text-verbosity levels. Router admission for `environment:none` uses the available engine capability, not an engine name. +The SDK descriptor advertises the validated daemon subset, including durable Turns/input receipts, text observations, function tools, raw usage and restrictive execution controls. It does not advertise permissions, raw tool Items, general web-search control or text-verbosity levels. Router admission for `environment:none` uses the available engine capability, not an engine name. Core owns public admission for Claude: [harness selection](../../contracts/agents-api/model-execution.md#harness-selection) chooses the engine for each Session; one [engine policy](../../contracts/agents-api/harness-onboarding.md#add-the-engine-to-core) serves API admission, device selection and the final claim; the [qualified operations](../../contracts/agents-api/harness-capabilities.md) table records Claude's medium-only verbosity and object-root function schemas; [function result images](../../contracts/agents-api/message-content.md#function-results) defines which placements accept image results; and [deployment defaults](../../contracts/agents-api/model-execution.md#deployment-defaults) define the model provider Core freezes for a Session. The adapter receives that provider as the adapter-owned `model_provider`, never in public Session configuration. Without one, a `none` host uses the daemon's own provider environment. The adapter alone selects the provider environment and removes credentials from native tool environments. @@ -78,7 +78,7 @@ The `none` profile accepts only text, explicit model/system instructions, manage ### Function server and results -The internal SDK function-server helper uses the maintained MCP server's public request handlers and standard Tool/CallToolResult types. It snapshots definitions and forwards JSON Schema without a JSON Schema-to-Zod conversion; supplied tools are always loaded. Native call identity comes from the pinned harness's `claudecode/toolUseId` MCP metadata, independently of request IDs, names or arrival order. Missing identities and undeclared tools fail before invoking the host. Return content/error fields unchanged over MCP and forward its per-request abort signal. The private Go factory connects declared functions through this helper and reuses the daemon function-call/result interface and opt-in neutral observations. The native function-server registry must contain exactly those functions. SDK allowlisting admits only these host callbacks; the host still owns result decisions and any business permission checks. It grants no runtime-token business authority. +The internal SDK function-server helper uses the maintained MCP server's public request handlers and standard Tool/CallToolResult types. It snapshots definitions and forwards JSON Schema without a JSON Schema-to-Zod conversion; supplied tools are always loaded. Native call identity comes from the pinned harness's `claudecode/toolUseId` MCP metadata, independently of request IDs, names or arrival order. Missing identities and undeclared tools fail before invoking the host. Return content/error fields unchanged over MCP and forward its per-request abort signal. The private Go factory connects declared functions through this helper and reuses the daemon function-call/result interface and neutral observations. The native function-server registry must contain exactly those functions. SDK allowlisting admits only these host callbacks; the host still owns result decisions and any business permission checks. It grants no runtime-token business authority. Function results remain pending after stdin/MCP delivery. A matching live, root native user tool_result confirms application only when its Session/call identity, error flag and ordered content match the submission. Text matches exactly; each submitted image position must remain a valid native base64 image. Native resizing or re-encoding may change image bytes. This acknowledges incorporation into native history, not byte/pixel fidelity or completed provider consumption. Public Items retain the original caller content; real image-dependent model responses separately qualify usability. Ignore replayed, synthetic and subagent messages. Native error text joins the submitted text parts with newlines; neutral observations retain their original order and separate failure status. Missing/mismatched receipts fail the execution; do not replay unknown delivery. Result submission waits at most ten seconds for a receipt and cancels uncertain execution on timeout. Invalid or unsupported image results fail before consuming a pending call. Function state belongs to one live Run and ends with it; the router owns receipt retry/conflict handling. This does not establish crash recovery or exactly-once effects. diff --git a/scripts/name-allowlist.json b/scripts/name-allowlist.json index 23ac631c9..0cb022c51 100644 --- a/scripts/name-allowlist.json +++ b/scripts/name-allowlist.json @@ -144,16 +144,6 @@ "regex": "AGENTS_API_PUBLIC_URL", "reason": "Historical migration commentary records the environment name used when this migration was written." }, - { - "path": "apps/daemon/internal/cli/companion_path.go", - "regex": "\\bparsar\\b", - "reason": "The authoring bridge puts the separate product CLI on PATH; that CLI keeps its own name." - }, - { - "path": "apps/daemon/internal/cli/companion_path_test.go", - "regex": "\\bparsar\\b", - "reason": "The authoring bridge puts the separate product CLI on PATH; that CLI keeps its own name." - }, { "path": "CONTRIBUTING.md", "regex": "apps/parsar/|Parsar owns", diff --git a/services/core/internal/execution/delivery.go b/services/core/internal/execution/delivery.go index b18f4f86e..3cd1e1833 100644 --- a/services/core/internal/execution/delivery.go +++ b/services/core/internal/execution/delivery.go @@ -256,7 +256,7 @@ func (d *Dispatcher) deliver(ctx context.Context, tenantID, sessionID string, pe return } } - case proto.TypePermissionRequest, proto.TypePromptForUserChoice, proto.TypeAuthoringRequest: + case proto.TypePermissionRequest, proto.TypePromptForUserChoice: result.ErrorCode = "interaction_not_supported" return } diff --git a/services/core/internal/execution/directory_preparation.go b/services/core/internal/execution/directory_preparation.go index 427e400cb..177f98571 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{AgentKind: session.Engine, AgentStateKey: "agents-api-" + session.ID, StrictResume: true, ReleaseOnCompletion: true, WorkspaceReadOnly: true} + req := proto.PromptRequestPayload{AgentKind: session.Engine, AgentStateKey: "agents-api-" + session.ID, ReleaseOnCompletion: true, WorkspaceReadOnly: true} if err := d.configurePreparedEnvironment(session, environment, bound, &req); err != nil { return ErrExecutionUnavailable } diff --git a/services/core/internal/execution/recovery_test.go b/services/core/internal/execution/recovery_test.go index e71df9b27..75e798208 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.StrictResume || req.AgentStateKey != "agents-api-session" { + if err != nil || req.RequireExistingNativeSession != wantRecovery || req.AgentSessionID != nativeID || req.AgentStateKey != "agents-api-session" { 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 82ba8ade8..99b06d1f1 100644 --- a/services/core/internal/execution/request.go +++ b/services/core/internal/execution/request.go @@ -54,9 +54,8 @@ func (d *Dispatcher) executionRequest(ctx context.Context, session sessions.Sess } request := proto.PromptRequestPayload{AgentKind: session.Engine, FunctionTools: tools.Functions, ToolSearch: tools.Search, AgentOptions: options, ExecutionControls: controls, AgentStateKey: "agents-api-" + session.ID, - AgentSessionID: bound.NativeSessionID, StrictResume: true, - RequireExistingNativeSession: recoverNativeSession, - ObserveMessages: caps.MessageItems, ObserveToolObservations: true, + AgentSessionID: bound.NativeSessionID, RequireExistingNativeSession: recoverNativeSession, + ObserveMessages: caps.MessageItems, ObserveSubagentIdentities: snapshot.Agent.MultiAgent.Enabled, MaxConcurrentSubagents: snapshot.Agent.MultiAgent.MaxConcurrentSubagents, DisableSubagents: !snapshot.Agent.MultiAgent.Enabled} diff --git a/services/core/internal/runtimedevice/state.go b/services/core/internal/runtimedevice/state.go index 6bb39ecfa..0d0cf69a0 100644 --- a/services/core/internal/runtimedevice/state.go +++ b/services/core/internal/runtimedevice/state.go @@ -91,7 +91,6 @@ type KindCapabilities struct { MCPHTTPBearerAuth bool `json:"mcp_http_bearer_auth,omitempty"` DurableInputReceipts bool `json:"durable_input_receipts,omitempty"` DurableTurns bool `json:"durable_turns,omitempty"` - WorkspaceAuthoring bool `json:"workspace_authoring,omitempty"` } // SupportedAgentKind is the sanitized runtime.config view 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 7a6982fca..8183f6cab 100644 --- a/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go +++ b/services/core/internal/runtimegateway/mcp_bearer_live_linux_test.go @@ -87,7 +87,7 @@ func TestLiveMCPBearerGatewayColdContinuation(t *testing.T) { turn := &mcpBearerTurn{} turns = append(turns, turn) runID := uuid.NewString() - request := proto.PromptRequestPayload{AgentKind: "codex", ConversationID: "mcp-bearer-acceptance", RunID: runID, Input: proto.TextInput(prompt), AgentStateKey: "mcp-bearer-acceptance", AgentSessionID: resume, StrictResume: true, ReleaseOnCompletion: true, ObserveMessages: true, ObserveToolObservations: true, DisableExecutionEnvironment: true, DisableSubagents: true, MCPHTTPServers: &servers, AgentOptions: map[string]any{"model": "MiniMax-M3"}, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} + request := proto.PromptRequestPayload{AgentKind: "codex", ConversationID: "mcp-bearer-acceptance", RunID: runID, Input: proto.TextInput(prompt), AgentStateKey: "mcp-bearer-acceptance", AgentSessionID: resume, ReleaseOnCompletion: true, ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, MCPHTTPServers: &servers, AgentOptions: map[string]any{"model": "MiniMax-M3"}, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}} sub, err := peer.SubscribeDurable(runID) if err != nil { t.Fatal("cannot subscribe before real daemon dispatch") diff --git a/services/core/internal/runtimegateway/session.go b/services/core/internal/runtimegateway/session.go index cb115fedb..7d166ce87 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -533,7 +533,6 @@ func deviceKindsFromHeartbeat(p proto.HeartbeatPayload) []runtimedevice.Supporte MCPHTTPTools: info.Capabilities.MCPHTTPTools.IsSupported(), MCPHTTPRequired: info.Capabilities.MCPHTTPRequired.IsSupported(), MCPHTTPBearerAuth: info.Capabilities.MCPHTTPBearerAuth.IsSupported(), - WorkspaceAuthoring: info.Capabilities.WorkspaceAuthoring.IsSupported(), }, }) } diff --git a/services/core/tests/integration/execution_tools_test.go b/services/core/tests/integration/execution_tools_test.go index d1cd4a85d..77cf4c88f 100644 --- a/services/core/tests/integration/execution_tools_test.go +++ b/services/core/tests/integration/execution_tools_test.go @@ -19,8 +19,8 @@ func TestExecutionNegotiatesAndPersistsToolObservations(t *testing.T) { env := h.read(testExecutionRequest) var request proto.PromptRequestPayload _ = env.DecodePayload(&request) - if !request.ObserveToolObservations || request.ObserveMessages { - t.Fatal("advertised capability was not requested") + if request.ObserveMessages { + t.Fatal("unadvertised message items were requested") } start := json.RawMessage(`{"kind":"mcp","server":"reference","name":"lookup","arguments":{"key":"value"},"status":"in_progress","output":null,"error":null}`) complete := json.RawMessage(`{"kind":"mcp","server":"reference","name":"lookup","arguments":{"key":"value"},"status":"completed","output":{"content":[{"type":"text","text":"answer"}],"structuredContent":{"version":9007199254740993}},"error":null}`) diff --git a/services/core/tests/integration/harness_onboarding_test.go b/services/core/tests/integration/harness_onboarding_test.go index b655f11cb..f60c5342a 100644 --- a/services/core/tests/integration/harness_onboarding_test.go +++ b/services/core/tests/integration/harness_onboarding_test.go @@ -117,7 +117,7 @@ func TestThirdHarnessPublicOnboarding(t *testing.T) { } request("POST", "/v1/agents/sessions/"+created.ID+"/events", `{"events":[{"type":"agent.session.input.message","input":[{"role":"user","content":[{"type":"input_text","text":"hold"}]}]}]}`, 202) next := awaitOnboardingPrompt(t, started) - if next.RunID == first.RunID || next.AgentSessionID != bound.NativeSessionID || !next.StrictResume { + if next.RunID == first.RunID || next.AgentSessionID != bound.NativeSessionID { t.Fatal(next) } request("POST", "/v1/agents/sessions/"+created.ID+"/events", `{"events":[{"type":"agent.session.input.cancel"}]}`, 202)