From 3784379728d79413e6ab3bb5c84b2587775830b9 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 15:27:19 +0000 Subject: [PATCH 1/2] Carry the bound workspace root on LocalEnvironment The prompt request's work_dir field only carried the local binding's root from localworkspace.Binding.Configure to the Harness adapters; Core never set it and the binding rejected it on the wire. Move the root to LocalEnvironment as a daemon-supplied json:"-" field and delete the wire field with the guards that existed only to reject it: the Binding.Configure check, the read-only preparation term and the Core test assertions. Codex, MiniMax Code and Claude read LocalEnvironment.WorkspaceRoot for Environment execution. The adapters no longer expand ~/, reject relative paths or create directories for a wire path; the binding validates its root at construction. environment:none requests keep the working directory they had, since Core never sent work_dir. Adapter tests that set a work directory on an environment:none request moved to real LocalEnvironment requests where the cwd mattered, and the Runtime protocol and Harness onboarding docs describe where the root comes from. --- .../internal/agent/claudesdk/local_test.go | 4 +- .../agent/claudesdk/mcp_environment_test.go | 2 +- .../internal/agent/claudesdk/options.go | 15 +---- .../internal/agent/claudesdk/session_test.go | 4 +- .../agent/claudesdk/tool_environment_test.go | 2 +- .../internal/agent/claudesdk/workspace.go | 4 +- .../agent/claudesdk/workspace_test.go | 9 +-- .../agent/codex/execution_controls_test.go | 4 +- .../agent/codex/executor_native_test.go | 3 +- .../agent/codex/harness_config_test.go | 4 +- .../agent/codex/mcp_http_preflight_test.go | 6 +- .../internal/agent/codex/model_route_test.go | 4 +- .../agent/codex/model_verbosity_test.go | 4 +- apps/daemon/internal/agent/codex/options.go | 39 +------------ .../internal/agent/codex/options_test.go | 55 ++++--------------- .../agent/codex/permission_profile_test.go | 5 +- .../agent/codex/preparation_helpers_test.go | 2 +- .../agent/codex/preparation_router_test.go | 7 +-- .../internal/agent/codex/prepared_test.go | 1 - .../agent/codex/provider_config_test.go | 4 +- .../internal/agent/codex/recovery_test.go | 10 ++++ .../internal/agent/codex/session_plan.go | 3 +- .../internal/agent/codex/verbosity_test.go | 4 +- .../internal/agent/codex/web_search_test.go | 4 +- apps/daemon/internal/agent/harness.go | 4 +- .../agent/mcode/environment_mcp_test.go | 4 +- apps/daemon/internal/agent/mcode/execution.go | 2 +- .../internal/agent/mcode/execution_test.go | 1 - apps/daemon/internal/agent/mcode/options.go | 28 +--------- .../internal/agent/mcode/options_test.go | 1 - .../internal/agent/mcode/preparation_test.go | 7 +-- .../agent/mcode/tool_environment_test.go | 2 +- apps/daemon/internal/agent/mcode/workspace.go | 4 +- apps/daemon/internal/dispatch/prompt.go | 2 +- .../daemon/internal/localworkspace/binding.go | 4 +- .../internal/localworkspace/binding_test.go | 5 +- .../internal/localworkspace/capabilities.go | 2 +- contracts/agents-api/harness-onboarding.md | 2 + docs/runtime-protocol.md | 2 +- internal/agentdaemon/proto/environment.go | 3 + internal/agentdaemon/proto/outbound.go | 6 -- .../proto/workspace_read_preparation.go | 4 +- .../execution/environment_placement_test.go | 2 +- .../store/local_environment_worker_test.go | 2 +- 44 files changed, 99 insertions(+), 187 deletions(-) diff --git a/apps/daemon/internal/agent/claudesdk/local_test.go b/apps/daemon/internal/agent/claudesdk/local_test.go index 05a1729ce..2d6408645 100644 --- a/apps/daemon/internal/agent/claudesdk/local_test.go +++ b/apps/daemon/internal/agent/claudesdk/local_test.go @@ -16,7 +16,7 @@ func TestLocalWorkspaceBindingNetworkAndRequiredHistory(t *testing.T) { config.Workspace.PublicDirectory = config.Workspace.Directory config.Workspace.NetworkAccess = "enabled" req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled"} + req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory} req.RequireExistingNativeSession = true start, _, err := prepare(config, req) if err != nil || !start.RequireHistory || start.Workspace.NetworkAccess != "enabled" { @@ -47,7 +47,7 @@ func TestRestrictedWorkspacePolicyUsesExactBoundAuthority(t *testing.T) { config.Workspace.NetworkAccess = "restricted" config.Workspace.AllowedDomains = []string{"Example.com", "api.example.com", "example.com"} req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "restricted", AllowedDomains: []string{"api.example.com", "EXAMPLE.COM"}} + req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "restricted", AllowedDomains: []string{"api.example.com", "EXAMPLE.COM"}, WorkspaceRoot: config.Workspace.Directory} if _, _, err := prepare(config, req); err == nil { t.Fatal("Runtime must not promise inner network isolation") } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go index 8cae72bf1..328674950 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -16,7 +16,7 @@ func TestEnvironmentMCPUsesInstalledLauncherAndSelectedCredential(t *testing.T) req := workspaceRequest() token := "selected-user-token" t.Setenv("MCP_TOKEN", "unselected-native-token") - req.LocalEnvironment = &proto.LocalEnvironment{CapabilityRoot: "/private/runtime/capabilities", NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{ + req.LocalEnvironment = &proto.LocalEnvironment{CapabilityRoot: "/private/runtime/capabilities", NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory, MCP: []proto.EnvironmentMCP{ {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/local", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "untrusted-package-command", Args: []string{"package-argument"}, EnvVars: []string{"MCP_TOKEN"}}}, {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp", BearerTokenEnvVar: "MCP_TOKEN"}, BearerToken: &token}, }} diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index a7a3e13be..5f9bae245 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -166,20 +166,7 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR if err != nil || !filepath.IsAbs(root) || !filepath.IsAbs(config.StateDir) || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { return fail("SDK state must be in a managed runtime subdirectory") } - start.Cwd = req.WorkDir - if start.Cwd == "" { - start.Cwd = filepath.Join(config.StateDir, "work") - } - if strings.HasPrefix(start.Cwd, "~/") { - homeDir, err := os.UserHomeDir() - if err != nil { - return startRequest{}, nil, err - } - start.Cwd = filepath.Join(homeDir, strings.TrimPrefix(start.Cwd, "~/")) - } - if !filepath.IsAbs(start.Cwd) { - return fail("work_dir must be absolute or start with ~/") - } + start.Cwd = filepath.Join(config.StateDir, "work") for _, dir := range []string{config.StateDir, filepath.Join(config.StateDir, "tmp"), start.Cwd} { if err := os.MkdirAll(dir, 0o700); err != nil { return startRequest{}, nil, err diff --git a/apps/daemon/internal/agent/claudesdk/session_test.go b/apps/daemon/internal/agent/claudesdk/session_test.go index 3dd21789d..c1808a754 100644 --- a/apps/daemon/internal/agent/claudesdk/session_test.go +++ b/apps/daemon/internal/agent/claudesdk/session_test.go @@ -74,7 +74,7 @@ func TestTextFactoryCompletionAndFailures(t *testing.T) { } func TestTextFactoryRejectsUnsupportedInput(t *testing.T) { - for _, kind := range []string{"execution-controls", "tool", "option", "relative", "outside"} { + for _, kind := range []string{"execution-controls", "tool", "option", "outside"} { t.Run(kind, func(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) @@ -87,8 +87,6 @@ func TestTextFactoryRejectsUnsupportedInput(t *testing.T) { request.FunctionTools = []proto.FunctionTool{{}} case "option": request.AgentOptions["allowed_tools"] = "anything" - case "relative": - request.WorkDir = "relative" case "outside": config.StateDir = filepath.Dir(root) } diff --git a/apps/daemon/internal/agent/claudesdk/tool_environment_test.go b/apps/daemon/internal/agent/claudesdk/tool_environment_test.go index 50a1046d6..7b53ca139 100644 --- a/apps/daemon/internal/agent/claudesdk/tool_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/tool_environment_test.go @@ -17,7 +17,7 @@ func TestSelfHostedToolEnvironment(t *testing.T) { } t.Setenv("OAC_RUNTIME_TOOL_ENV_FILE", file) req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{NetworkAccess: "enabled"} + req.LocalEnvironment = &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory} profile, _, err := prepareWorkspace(config, req) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/claudesdk/workspace.go b/apps/daemon/internal/agent/claudesdk/workspace.go index 3e8586679..ba63e73de 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace.go +++ b/apps/daemon/internal/agent/claudesdk/workspace.go @@ -43,8 +43,8 @@ func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspace if req.DisableExecutionEnvironment { return nil, nil, fmt.Errorf("claudesdk: workspace profile does not support the requested execution combination") } - if req.WorkDir != "" && req.WorkDir != config.Workspace.Directory { - return nil, nil, fmt.Errorf("claudesdk: work_dir conflicts with the trusted workspace binding") + if req.LocalEnvironment != nil && req.LocalEnvironment.WorkspaceRoot != config.Workspace.Directory { + return nil, nil, fmt.Errorf("claudesdk: workspace root conflicts with the trusted workspace binding") } if req.LocalEnvironment != nil && !(agentnetwork.Policy{Access: config.Workspace.NetworkAccess, AllowedDomains: config.Workspace.AllowedDomains}).Equal(agentnetwork.Policy{Access: req.LocalEnvironment.NetworkAccess, AllowedDomains: req.LocalEnvironment.AllowedDomains}) { return nil, nil, fmt.Errorf("claudesdk: local Runtime network policy mismatch") diff --git a/apps/daemon/internal/agent/claudesdk/workspace_test.go b/apps/daemon/internal/agent/claudesdk/workspace_test.go index 01577bf43..b84b767a7 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_test.go @@ -76,15 +76,16 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) { } func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) { - for _, name := range []string{"none", "work-dir", "mcp", "caller-policy", "relative", "missing", "ambient-setting", "duplicate-env", "bad-env"} { + for _, name := range []string{"none", "workspace-root", "mcp", "caller-policy", "relative", "missing", "ambient-setting", "duplicate-env", "bad-env"} { t.Run(name, func(t *testing.T) { config := workspaceFixture(t) req := workspaceRequest() switch name { case "none": req.DisableExecutionEnvironment = true - case "work-dir": - req.WorkDir = config.Workspace.ScratchDir + case "workspace-root": + config.Workspace.NetworkAccess = "enabled" + req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.ScratchDir} case "mcp": req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} case "caller-policy": @@ -142,7 +143,7 @@ func TestPublicMCPUsesWorkspaceProjectionWithoutCredentialCopy(t *testing.T) { config := workspaceFixture(t) config.Workspace.NetworkAccess = "enabled" req := workspaceRequest() - req.LocalEnvironment = &proto.LocalEnvironment{NetworkAccess: "enabled"} + req.LocalEnvironment = &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: config.Workspace.Directory} token := "vault-selected-canary" tools := []string{"prove"} req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "remote", ServerURL: "https://example.test/mcp", AllowedTools: &tools, Required: true, BearerToken: &token}} diff --git a/apps/daemon/internal/agent/codex/execution_controls_test.go b/apps/daemon/internal/agent/codex/execution_controls_test.go index 2ffed69d6..b1cd9eac9 100644 --- a/apps/daemon/internal/agent/codex/execution_controls_test.go +++ b/apps/daemon/internal/agent/codex/execution_controls_test.go @@ -21,7 +21,7 @@ func TestExecutionControlsOverrideWithoutMutatingNativeOptions(t *testing.T) { if options["model"] != "test-model" || original["web_search"] != "live" || original["model_verbosity"] != "high" { t.Fatal("operator options mutated") } - plan, err := BuildSessionPlan("run", "state", "", options) + plan, err := BuildSessionPlan("run", "state", options) if err != nil { t.Fatal(err) } @@ -49,7 +49,7 @@ func TestExecutionControlsRejectIncompleteOrInvalidValues(t *testing.T) { {WebSearch: "invalid", TextVerbosity: "medium"}, {WebSearch: "disabled", TextVerbosity: "invalid"}, } { options := executionOptions(proto.PromptRequestPayload{ExecutionControls: &controls}) - if plan, err := BuildSessionPlan("run", "state", "", options); err == nil { + if plan, err := BuildSessionPlan("run", "state", options); err == nil { plan.Cleanup() t.Fatal("invalid controls accepted", controls) } diff --git a/apps/daemon/internal/agent/codex/executor_native_test.go b/apps/daemon/internal/agent/codex/executor_native_test.go index 49e96bc91..227ad311c 100644 --- a/apps/daemon/internal/agent/codex/executor_native_test.go +++ b/apps/daemon/internal/agent/codex/executor_native_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "os" "os/exec" - "path/filepath" "strings" "testing" "time" @@ -50,7 +49,7 @@ func TestExecutorNativeReuse(t *testing.T) { cfg.codexBinary = binary cfg.logger = obslog.Discard() req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "executor-native", WorkDir: filepath.Join(isolated, "workspace"), + AgentKind: "codex", AgentStateKey: "executor-native", StrictResume: 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/harness_config_test.go b/apps/daemon/internal/agent/codex/harness_config_test.go index 43f4d9cb2..1efa79cc5 100644 --- a/apps/daemon/internal/agent/codex/harness_config_test.go +++ b/apps/daemon/internal/agent/codex/harness_config_test.go @@ -13,7 +13,7 @@ import ( func TestHarnessConfigAppliedWithoutChangingProvider(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) - plan, err := BuildSessionPlan("run", "native-config", "", map[string]any{ + plan, err := BuildSessionPlan("run", "native-config", map[string]any{ "model": "fixture", "harness_config": map[string]any{"model_reasoning_effort": "high"}, "model_provider": map[string]any{"base_url": "https://provider.invalid/v1", "protocol": "responses", "api_key": "test-key"}, }) @@ -27,7 +27,7 @@ func TestHarnessConfigAppliedWithoutChangingProvider(t *testing.T) { } func TestHarnessConfigConflictFailsBeforePreparation(t *testing.T) { - _, err := BuildSessionPlan("run", "", "", map[string]any{"harness_config": map[string]any{"model_provider": "bypass"}}) + _, err := BuildSessionPlan("run", "", map[string]any{"harness_config": map[string]any{"model_provider": "bypass"}}) if err != harnessconfig.ErrHarnessConfig { t.Fatalf("configuration must fail before filesystem preparation: %v", err) } diff --git a/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go b/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go index aa3c87918..40c09abdd 100644 --- a/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go +++ b/apps/daemon/internal/agent/codex/mcp_http_preflight_test.go @@ -169,6 +169,10 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) { } defer p.Close() assertPreparationOnly(t, root) + home, err := allocCodexHome(req.AgentStateKey) + if err != nil { + t.Fatal(err) + } s, err := p.start(t.Context(), "actual-run", proto.TextInput("actual prompt"), make(chan proto.Envelope, 8)) if err != nil { t.Fatal(err) @@ -191,7 +195,7 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) { if err := json.Unmarshal(frame.Params, ¶ms); err != nil { t.Fatal(err) } - if !checked || params["cwd"] != req.WorkDir || (frame.Method == "thread/resume") != (mode == "resume") { + if !checked || params["cwd"] != home || (frame.Method == "thread/resume") != (mode == "resume") { t.Fatal("thread started before the check or with another cwd") } } diff --git a/apps/daemon/internal/agent/codex/model_route_test.go b/apps/daemon/internal/agent/codex/model_route_test.go index a77451574..2e4a716cc 100644 --- a/apps/daemon/internal/agent/codex/model_route_test.go +++ b/apps/daemon/internal/agent/codex/model_route_test.go @@ -11,7 +11,7 @@ func TestPlanRejectsNonNativeFrozenProvider(t *testing.T) { for _, protocol := range []string{"anthropic", "chat_completions"} { t.Run(protocol, func(t *testing.T) { provider := map[string]any{"protocol": protocol, "base_url": "https://model.invalid/v1", "api_key": "private-sentinel"} - plan, err := BuildSessionPlan("recovered", "frozen-state", t.TempDir(), map[string]any{"model": "frozen-model", "model_provider": provider}) + plan, err := BuildSessionPlan("recovered", "frozen-state", map[string]any{"model": "frozen-model", "model_provider": provider}) if plan.Cleanup != nil { plan.Cleanup() } @@ -30,7 +30,7 @@ func TestPlanRejectsIncompleteExplicitProvider(t *testing.T) { {"model": "chosen", "model_provider": nil}, {"model_provider": map[string]any{"protocol": "responses", "base_url": "https://model.example/v1", "api_key": "fixture"}}, } { - if _, err := BuildSessionPlan("frozen", "state", "", options); err == nil { + if _, err := BuildSessionPlan("frozen", "state", options); err == nil { t.Fatal("explicit provider fell back to native defaults") } } diff --git a/apps/daemon/internal/agent/codex/model_verbosity_test.go b/apps/daemon/internal/agent/codex/model_verbosity_test.go index be995dc8a..957a70132 100644 --- a/apps/daemon/internal/agent/codex/model_verbosity_test.go +++ b/apps/daemon/internal/agent/codex/model_verbosity_test.go @@ -37,7 +37,7 @@ func TestPrepareModelVerbosity(t *testing.T) { if err := os.WriteFile(binary, []byte("#!/bin/sh\nprintf '%s' '"+catalog+"'\n"), 0700); err != nil { t.Fatal(err) } - plan, err := BuildSessionPlan("run", "state", "", map[string]any{"model": "known-model", "model_verbosity": "high"}) + plan, err := BuildSessionPlan("run", "state", map[string]any{"model": "known-model", "model_verbosity": "high"}) if err != nil { t.Fatal(err) } @@ -80,7 +80,7 @@ func TestPrepareDefaultModelVerbosity(t *testing.T) { for _, model := range []string{"supported", "unsupported", "unknown-provider-model"} { for _, level := range []string{"low", "medium", "high"} { t.Run(model+"/"+level, func(t *testing.T) { - plan, err := BuildSessionPlan("run", "state", "", executionOptions(proto.PromptRequestPayload{AgentOptions: map[string]any{"model": model}, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: level}})) + plan, err := BuildSessionPlan("run", "state", executionOptions(proto.PromptRequestPayload{AgentOptions: map[string]any{"model": model}, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: level}})) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 147a3d3dc..a8f1e6fd1 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -16,8 +16,8 @@ import ( // SessionPlan holds the resolved per-prompt launch plan derived from // the daemon's PromptRequestPayload. type SessionPlan struct { - // Cwd is the validated working directory passed to codex (and to - // the spawned app-server). Empty when the caller provided no work_dir. + // Cwd is the working directory passed to codex and the spawned + // app-server: the bound workspace root for an Environment request. Cwd string // Env is the full environment slice (KEY=value) to layer onto @@ -93,7 +93,7 @@ type SessionPlan struct { // codexBinary in sessionConfig) rather than per-call. // // Daemon-managed Codex sessions bypass approvals and the engine sandbox. -func BuildSessionPlan(runID, agentStateKey, workDir string, opts map[string]any) (SessionPlan, error) { +func BuildSessionPlan(runID, agentStateKey string, opts map[string]any) (SessionPlan, error) { cleanup := func() {} plan := SessionPlan{ CollaborationMode: CollaborationModeDefault, @@ -127,12 +127,6 @@ func BuildSessionPlan(runID, agentStateKey, workDir string, opts map[string]any) } } - resolvedCwd, err := resolveWorkDirCodex(workDir) - if err != nil { - return plan, err - } - plan.Cwd = resolvedCwd - plan.Model = stringOpt(opts, "model") plan.SystemPrompt = stringOpt(opts, "system_prompt") if override := stringOpt(opts, "override_system_prompt"); override != "" { @@ -212,33 +206,6 @@ func BuildSessionPlan(runID, agentStateKey, workDir string, opts map[string]any) // helpers // --------------------------------------------------------------------------- -func resolveWorkDirCodex(input string) (string, error) { - trimmed := strings.TrimSpace(input) - if trimmed == "" { - return "", nil - } - var abs string - switch { - case strings.HasPrefix(trimmed, "~/"): - home, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("codex: resolve home dir: %w", err) - } - abs = filepath.Join(home, strings.TrimPrefix(trimmed, "~/")) - case filepath.IsAbs(trimmed): - abs = trimmed - default: - return "", fmt.Errorf("codex: work_dir must be absolute or start with ~/, got %q", trimmed) - } - // Create the working directory so a user - // naming a fresh project root in the agent wizard works on first - // run instead of erroring with "does not exist". - if err := os.MkdirAll(abs, 0o755); err != nil { - return "", fmt.Errorf("codex: mkdir work_dir %s: %w", abs, err) - } - return abs, nil -} - func allocCodexHome(agentStateKey string) (string, error) { if strings.TrimSpace(agentStateKey) == "" { return "", fmt.Errorf("codex: agentStateKey required for CODEX_HOME allocation") diff --git a/apps/daemon/internal/agent/codex/options_test.go b/apps/daemon/internal/agent/codex/options_test.go index 613926e55..b8b8587ac 100644 --- a/apps/daemon/internal/agent/codex/options_test.go +++ b/apps/daemon/internal/agent/codex/options_test.go @@ -3,13 +3,12 @@ package codex import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "os" - "path/filepath" "strings" "testing" ) func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", nil) + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", nil) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } @@ -26,7 +25,7 @@ func TestBuildSessionPlan_DefaultsToBypass(t *testing.T) { } func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "env": map[string]any{ "OPENAI_API_KEY": "sk-test", }, @@ -61,11 +60,11 @@ func TestBuildSessionPlan_AllocsCodexHomeAndEnv(t *testing.T) { func TestBuildSessionPlan_StableCodexHomeByStateKey(t *testing.T) { stateKey := "conv-stable/agent-stable/codex" - planA, err := BuildSessionPlan("run-a", stateKey, "", nil) + planA, err := BuildSessionPlan("run-a", stateKey, nil) if err != nil { t.Fatalf("BuildSessionPlan A: %v", err) } - planB, err := BuildSessionPlan("run-b", stateKey, "", nil) + planB, err := BuildSessionPlan("run-b", stateKey, nil) if err != nil { t.Fatalf("BuildSessionPlan B: %v", err) } @@ -75,7 +74,7 @@ func TestBuildSessionPlan_StableCodexHomeByStateKey(t *testing.T) { } func TestBuildSessionPlan_RoutesReasoningSummary(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "reasoning_summary": "detailed", }) if err != nil { @@ -104,7 +103,7 @@ func codexHomeFromEnv(env []string) string { } func TestBuildSessionPlan_OverrideSystemPromptReplacesAppend(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "system_prompt": "user base", "override_system_prompt": "you are pirate", }) @@ -118,7 +117,7 @@ func TestBuildSessionPlan_OverrideSystemPromptReplacesAppend(t *testing.T) { } func TestBuildSessionPlan_EmptyOverrideKeepsSystemPrompt(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "system_prompt": "user base", "override_system_prompt": "", }) @@ -132,7 +131,7 @@ func TestBuildSessionPlan_EmptyOverrideKeepsSystemPrompt(t *testing.T) { } func TestBuildSessionPlan_ParsesCollaborationMode(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "mode": "plan", }) if err != nil { @@ -151,7 +150,7 @@ func TestBuildSessionPlan_OmittedModeRetainsCurrentInstructions(t *testing.T) { if mode != "" { opts["mode"] = mode } - plan, err := BuildSessionPlan("run", "conv/agent/codex", "", opts) + plan, err := BuildSessionPlan("run", "conv/agent/codex", opts) if err != nil { t.Fatal(err) } @@ -163,7 +162,7 @@ func TestBuildSessionPlan_OmittedModeRetainsCurrentInstructions(t *testing.T) { } func TestBuildSessionPlan_RejectsUnknownCollaborationMode(t *testing.T) { - _, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + _, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "mode": "autopilot", }) if err == nil || !strings.Contains(err.Error(), "unsupported collaboration mode") { @@ -171,38 +170,8 @@ func TestBuildSessionPlan_RejectsUnknownCollaborationMode(t *testing.T) { } } -func TestBuildSessionPlan_RejectsRelativeWorkDir(t *testing.T) { - _, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "relative/dir", nil) - if err == nil { - t.Fatal("relative work_dir must error") - } -} - -// TestBuildSessionPlan_CreatesMissingWorkDir: -// a non-existent absolute path is mkdir -p'd so a user can pin a fresh -// project root in the agent wizard. Without this, codex agents would -// hard-fail the first turn instead of running. -func TestBuildSessionPlan_CreatesMissingWorkDir(t *testing.T) { - target := filepath.Join(t.TempDir(), "missing", "parents", "leaf") - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", target, nil) - if err != nil { - t.Fatalf("BuildSessionPlan: %v", err) - } - defer plan.Cleanup() - if plan.Cwd != target { - t.Fatalf("plan.Cwd = %q, want %q", plan.Cwd, target) - } - info, err := os.Stat(target) - if err != nil { - t.Fatalf("stat target: %v", err) - } - if !info.IsDir() { - t.Fatalf("target %q is not a directory", target) - } -} - func TestBuildSessionPlan_WritesMCPConfig(t *testing.T) { - plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "mcp_servers": map[string]any{ "docs": map[string]any{ "command": "docs-server", @@ -242,7 +211,7 @@ func TestBuildSessionPlan_WritesMCPConfig(t *testing.T) { } func TestBuildSessionPlan_MissingMCPCommandErrors(t *testing.T) { - _, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", "", map[string]any{ + _, err := BuildSessionPlan("run-1", "conv-1/agent-1/codex", map[string]any{ "mcp_servers": map[string]any{ "broken": map[string]any{ "args": []any{"--x"}, diff --git a/apps/daemon/internal/agent/codex/permission_profile_test.go b/apps/daemon/internal/agent/codex/permission_profile_test.go index 58a08c3d7..dc250c7be 100644 --- a/apps/daemon/internal/agent/codex/permission_profile_test.go +++ b/apps/daemon/internal/agent/codex/permission_profile_test.go @@ -11,7 +11,7 @@ import ( func TestRuntimeUsesHostPermissions(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", t.TempDir()) { - req := proto.PromptRequestPayload{AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled"}} + req := proto.PromptRequestPayload{AgentStateKey: "session", DisableSubagents: true, LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()}} plan, _, err := prepareSessionPlan(t.Context(), req, sessionConfig{}) if err != nil { t.Fatal(err) @@ -19,6 +19,9 @@ func TestRuntimeUsesHostPermissions(t *testing.T) { if plan.Sandbox != "danger-full-access" || plan.Permissions != "" || plan.ApprovalPolicy.String != "never" { t.Fatal("Runtime must bypass inner sandbox", plan) } + if plan.Cwd != req.LocalEnvironment.WorkspaceRoot { + t.Fatal("native cwd is not the bound workspace root", plan.Cwd) + } for _, kv := range plan.ExtraConfig { if kv[0] == "default_permissions" { t.Fatal("obsolete permission wrapper", kv) diff --git a/apps/daemon/internal/agent/codex/preparation_helpers_test.go b/apps/daemon/internal/agent/codex/preparation_helpers_test.go index b07f5c04e..d56acef77 100644 --- a/apps/daemon/internal/agent/codex/preparation_helpers_test.go +++ b/apps/daemon/internal/agent/codex/preparation_helpers_test.go @@ -41,7 +41,7 @@ func preparationFixture(t *testing.T) (proto.PromptRequestPayload, sessionConfig cfg := defaultSessionConfig() cfg.codexBinary = binary req := proto.PromptRequestPayload{ - AgentKind: "codex", AgentStateKey: "prepared-session", WorkDir: filepath.Join(root, "harness"), + AgentKind: "codex", AgentStateKey: "prepared-session", ReleaseOnCompletion: true, StrictResume: true, AgentOptions: map[string]any{"model": "fixture-model", "model_verbosity": "medium"}, DisableExecutionEnvironment: true, diff --git a/apps/daemon/internal/agent/codex/preparation_router_test.go b/apps/daemon/internal/agent/codex/preparation_router_test.go index a55759523..660c94504 100644 --- a/apps/daemon/internal/agent/codex/preparation_router_test.go +++ b/apps/daemon/internal/agent/codex/preparation_router_test.go @@ -39,14 +39,14 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { } t.Setenv("OAC_RUNTIME_CAPABILITY_DIRECTORY", filepath.Join(t.TempDir(), "capabilities")) t.Setenv("OAC_TEST_EXECUTOR_MODE", "complete") - environment, session := uuid.NewString(), uuid.NewString() - if err := os.MkdirAll(req.WorkDir, 0700); err != nil { + environment, session, workspace := uuid.NewString(), uuid.NewString(), filepath.Join(root, "harness") + if err := os.MkdirAll(workspace, 0700); err != nil { t.Fatal(err) } for key, value := range map[string]string{ "OAC_RUNTIME_ENVIRONMENT_ID": environment, "OAC_RUNTIME_SESSION_ID": session, - "OAC_RUNTIME_WORKSPACE": req.WorkDir, + "OAC_RUNTIME_WORKSPACE": workspace, "OAC_RUNTIME_NETWORK_ACCESS": "enabled", } { t.Setenv(key, value) @@ -55,7 +55,6 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) { if err != nil { t.Fatal(err) } - req.WorkDir = "" req.AgentStateKey = "agents-api-" + session req.DisableExecutionEnvironment = false req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}} diff --git a/apps/daemon/internal/agent/codex/prepared_test.go b/apps/daemon/internal/agent/codex/prepared_test.go index 8d73a17c9..e729c9613 100644 --- a/apps/daemon/internal/agent/codex/prepared_test.go +++ b/apps/daemon/internal/agent/codex/prepared_test.go @@ -32,7 +32,6 @@ func TestPreparedSessionTransfersSameResourceOnce(t *testing.T) { // Caller-owned data cannot revise the prepared native configuration. req.AgentOptions["model"] = "different-model" req.AgentSessionID = "different-thread" - req.WorkDir = "/different-workspace" copy(req.FunctionTools[0].Parameters, strings.ReplaceAll(string(req.FunctionTools[0].Parameters), "integer", "boolean")) out := make(chan proto.Envelope, 8) startCtx, stopStart := context.WithCancel(t.Context()) diff --git a/apps/daemon/internal/agent/codex/provider_config_test.go b/apps/daemon/internal/agent/codex/provider_config_test.go index 133716715..345a86378 100644 --- a/apps/daemon/internal/agent/codex/provider_config_test.go +++ b/apps/daemon/internal/agent/codex/provider_config_test.go @@ -156,7 +156,7 @@ func TestNormaliseProviderConfig_Nil(t *testing.T) { } func TestBuildSessionPlan_PinsModelProviderWhenProviderSet(t *testing.T) { - plan, err := BuildSessionPlan("run-x", "conv-1/agent-1/codex", "", map[string]any{ + plan, err := BuildSessionPlan("run-x", "conv-1/agent-1/codex", map[string]any{ "model": "fixture-model", "model_provider": map[string]any{"protocol": "responses", "base_url": "https://x/v1", @@ -194,7 +194,7 @@ func TestBuildSessionPlan_PinsModelProviderWhenProviderSet(t *testing.T) { } func TestBuildSessionPlan_NoProviderLeavesBuiltinDefault(t *testing.T) { - plan, err := BuildSessionPlan("run-y", "conv-1/agent-1/codex", "", nil) + plan, err := BuildSessionPlan("run-y", "conv-1/agent-1/codex", nil) if err != nil { t.Fatalf("BuildSessionPlan: %v", err) } diff --git a/apps/daemon/internal/agent/codex/recovery_test.go b/apps/daemon/internal/agent/codex/recovery_test.go index b08156784..e880e8dfb 100644 --- a/apps/daemon/internal/agent/codex/recovery_test.go +++ b/apps/daemon/internal/agent/codex/recovery_test.go @@ -3,12 +3,15 @@ package codex import ( "context" "encoding/json" + "os" "path/filepath" "testing" "time" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" + "github.com/google/uuid" ) func TestNativeSessionRecoveryRequiresPinnedNative(t *testing.T) { @@ -141,6 +144,13 @@ func TestRequiredHistoryResolution(t *testing.T) { func TestPreparedRecoveryCannotStartWithoutExistingHistory(t *testing.T) { req, cfg, root := preparationFixture(t) req.RequireExistingNativeSession = true + // Recovery searches history for the bound workspace root. + workspace := filepath.Join(root, "workspace") + if err := os.Mkdir(workspace, 0o700); err != nil { + t.Fatal(err) + } + req.DisableExecutionEnvironment = false + req.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString(), WorkspaceDirectory: "/workspace", NetworkAccess: "enabled", CapabilitySources: &agentcapabilities.Input{}, WorkspaceRoot: workspace} p, err := newPreparation(t.Context(), req, cfg) if err != nil { t.Fatal(err) diff --git a/apps/daemon/internal/agent/codex/session_plan.go b/apps/daemon/internal/agent/codex/session_plan.go index 1ab1cb76c..ded2f3147 100644 --- a/apps/daemon/internal/agent/codex/session_plan.go +++ b/apps/daemon/internal/agent/codex/session_plan.go @@ -21,7 +21,7 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg if err != nil { return SessionPlan{}, nil, err } - plan, err := BuildSessionPlan(req.RunID, req.AgentStateKey, req.WorkDir, req.AgentOptions) + plan, err := BuildSessionPlan(req.RunID, req.AgentStateKey, req.AgentOptions) if err != nil { return SessionPlan{}, nil, fmt.Errorf("codex: build session plan: %w", err) } @@ -31,6 +31,7 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg } disableProgrammaticTools(&plan, req.ExecutionControls) if req.LocalEnvironment != nil { + plan.Cwd = req.LocalEnvironment.WorkspaceRoot plan.Sandbox = "danger-full-access" plan.Permissions = "" plan.ApprovalPolicy = AskForApproval{String: "never"} diff --git a/apps/daemon/internal/agent/codex/verbosity_test.go b/apps/daemon/internal/agent/codex/verbosity_test.go index 3b85964f1..924cc52e3 100644 --- a/apps/daemon/internal/agent/codex/verbosity_test.go +++ b/apps/daemon/internal/agent/codex/verbosity_test.go @@ -15,7 +15,7 @@ func TestVerbosityConfiguration(t *testing.T) { opts["model_verbosity"] = mode want = [][2]string{{"model_verbosity", `"` + mode + `"`}} } - plan, err := BuildSessionPlan("run", "state", "", opts) + plan, err := BuildSessionPlan("run", "state", opts) if err != nil { t.Fatal(err) } @@ -26,7 +26,7 @@ func TestVerbosityConfiguration(t *testing.T) { }) } for _, value := range []any{nil, "", "enabled", true, 1, map[string]any{}, []any{}} { - if _, err := BuildSessionPlan("run", "state", "", map[string]any{"model_verbosity": value}); err == nil { + if _, err := BuildSessionPlan("run", "state", map[string]any{"model_verbosity": value}); err == nil { t.Fatalf("accepted invalid model_verbosity %T", value) } } diff --git a/apps/daemon/internal/agent/codex/web_search_test.go b/apps/daemon/internal/agent/codex/web_search_test.go index fc1958e6f..43fc00de9 100644 --- a/apps/daemon/internal/agent/codex/web_search_test.go +++ b/apps/daemon/internal/agent/codex/web_search_test.go @@ -15,7 +15,7 @@ func TestWebSearchConfiguration(t *testing.T) { opts["web_search"] = mode want = [][2]string{{"web_search", `"` + mode + `"`}} } - plan, err := BuildSessionPlan("run", "state", "", opts) + plan, err := BuildSessionPlan("run", "state", opts) if err != nil { t.Fatal(err) } @@ -26,7 +26,7 @@ func TestWebSearchConfiguration(t *testing.T) { }) } for _, value := range []any{nil, "", "enabled", true, 1, map[string]any{}, []any{}} { - if _, err := BuildSessionPlan("run", "state", "", map[string]any{"web_search": value}); err == nil { + if _, err := BuildSessionPlan("run", "state", map[string]any{"web_search": value}); err == nil { t.Fatalf("accepted invalid web_search %T", value) } } diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 51f05a4ca..7d0f715dd 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -20,7 +20,9 @@ // Runtime registration and Core service qualification remain separate. A public // Harness also needs a profile in services/core/internal/engine; advertising // a capability cannot authorize it. Requests, events and capability descriptors -// use the existing internal/agentdaemon/proto types. +// use the existing internal/agentdaemon/proto types. An Environment execution +// request carries the Runtime's bound workspace directory in +// LocalEnvironment.WorkspaceRoot; the native Harness runs there. package agent import ( diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index efcd5b5e7..1e2c745cb 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -54,8 +54,8 @@ func TestEnvironmentMCPUsesFixedLauncherForNewAndLoadedSessions(t *testing.T) { t.Fatal("environment MCP displaced workspace tools") } cwd, err := os.ReadFile(record + ".cwd") - workspace, pathErr := filepath.EvalSymlinks(req.WorkDir) - if err != nil || pathErr != nil || string(cwd) != workspace || params.Cwd != req.WorkDir { + workspace, pathErr := filepath.EvalSymlinks(req.LocalEnvironment.WorkspaceRoot) + if err != nil || pathErr != nil || string(cwd) != workspace || params.Cwd != req.LocalEnvironment.WorkspaceRoot { t.Fatalf("native process and ACP Session must use the declared workspace: process=%q ACP=%q", cwd, params.Cwd) } server := params.MCP[1] diff --git a/apps/daemon/internal/agent/mcode/execution.go b/apps/daemon/internal/agent/mcode/execution.go index 6ad299a23..a54bfc742 100644 --- a/apps/daemon/internal/agent/mcode/execution.go +++ b/apps/daemon/internal/agent/mcode/execution.go @@ -13,7 +13,7 @@ func SupportsExecution(version string) bool { } func validateExecutionRequest(req proto.PromptRequestPayload) error { - if !req.DisableExecutionEnvironment || req.WorkDir != "" || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || len(req.FunctionTools) != 0 || (req.MCPHTTPServers != nil && len(*req.MCPHTTPServers) != 0) { + if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || len(req.FunctionTools) != 0 || (req.MCPHTTPServers != nil && len(*req.MCPHTTPServers) != 0) { return fmt.Errorf("mcode: unsupported execution configuration") } if req.ExecutionControls == nil || req.ExecutionControls.OutputFormat != nil || req.ExecutionControls.WebSearch != "disabled" || (req.ExecutionControls.TextVerbosity != "" && req.ExecutionControls.TextVerbosity != "medium") { diff --git a/apps/daemon/internal/agent/mcode/execution_test.go b/apps/daemon/internal/agent/mcode/execution_test.go index d26b5e5e5..ac7bcd53b 100644 --- a/apps/daemon/internal/agent/mcode/execution_test.go +++ b/apps/daemon/internal/agent/mcode/execution_test.go @@ -50,7 +50,6 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = false }, func(r *proto.PromptRequestPayload) { r.DisableSubagents = false }, func(r *proto.PromptRequestPayload) { r.RequireExistingNativeSession = true }, - func(r *proto.PromptRequestPayload) { r.WorkDir = "/tmp" }, 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"} }, diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index d2cbc5192..86305f17d 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -45,17 +45,12 @@ func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayloa return result, err } result.DataDir = filepath.Dir(root) - result.Dir, err = workDir(req.WorkDir, filepath.Join(result.DataDir, "workspace")) - if err != nil { - return result, err - } + result.Dir = filepath.Join(result.DataDir, "workspace") if err := os.MkdirAll(result.DataDir, 0o700); err != nil { return result, err } - if req.WorkDir == "" { - if err := os.MkdirAll(result.Dir, 0o700); err != nil { - return result, err - } + if err := os.MkdirAll(result.Dir, 0o700); err != nil { + return result, err } if managedSkills { installed, err := managedskills.InstallManagedSkills(ctx, log.With("component", "mcode"), root, req.AgentOptions["skills"]) @@ -146,23 +141,6 @@ func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayloa return result, err } -func workDir(raw, fallback string) (string, error) { - if raw == "" { - return fallback, nil - } - if strings.HasPrefix(raw, "~/") { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - raw = filepath.Join(home, raw[2:]) - } - if !filepath.IsAbs(raw) { - return "", fmt.Errorf("mcode: working directory must be absolute or start with ~/") - } - return filepath.Clean(raw), nil -} - func optionString(options map[string]any, key string) string { value, _ := options[key].(string) return value diff --git a/apps/daemon/internal/agent/mcode/options_test.go b/apps/daemon/internal/agent/mcode/options_test.go index 9e2db80bf..dde7201c9 100644 --- a/apps/daemon/internal/agent/mcode/options_test.go +++ b/apps/daemon/internal/agent/mcode/options_test.go @@ -63,7 +63,6 @@ func TestOptionsRejectDroppedContext(t *testing.T) { name string edit func(*proto.PromptRequestPayload) }{ - {"relative workdir", func(r *proto.PromptRequestPayload) { r.WorkDir = "relative" }}, {"oversized instructions", func(r *proto.PromptRequestPayload) { r.AgentOptions["system_prompt"] = strings.Repeat("x", 32*1024+1) }}, {"attachment", func(r *proto.PromptRequestPayload) { r.Input = proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image"}}}} diff --git a/apps/daemon/internal/agent/mcode/preparation_test.go b/apps/daemon/internal/agent/mcode/preparation_test.go index a75861bf3..93b095ee4 100644 --- a/apps/daemon/internal/agent/mcode/preparation_test.go +++ b/apps/daemon/internal/agent/mcode/preparation_test.go @@ -18,8 +18,7 @@ func workspaceFixture(t *testing.T) (WorkspaceConfig, proto.PromptRequestPayload r := executionRequest(t) r.RunID, r.Input, r.ConversationID = "", nil, "" r.DisableExecutionEnvironment = false - r.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled"} - r.WorkDir = t.TempDir() + r.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "enabled", WorkspaceRoot: t.TempDir()} record := filepath.Join(t.TempDir(), "calls") exe, err := os.Executable() if err != nil { @@ -31,7 +30,7 @@ func workspaceFixture(t *testing.T) (WorkspaceConfig, proto.PromptRequestPayload if err := os.WriteFile(binary, []byte(script), 0700); err != nil { t.Fatal(err) } - return WorkspaceConfig{Binary: binary, Node: "/usr/bin/node", Bridge: "/opt/bridge.mjs", Directory: r.WorkDir, Network: "enabled", Scratch: t.TempDir()}, r, record + return WorkspaceConfig{Binary: binary, Node: "/usr/bin/node", Bridge: "/opt/bridge.mjs", Directory: r.LocalEnvironment.WorkspaceRoot, Network: "enabled", Scratch: t.TempDir()}, r, record } func TestPreparedWorkspaceHasOneInputAndOutputOwner(t *testing.T) { @@ -48,7 +47,7 @@ func TestPreparedWorkspaceHasOneInputAndOutputOwner(t *testing.T) { if err != nil || strings.Contains(string(raw), "session/prompt") { t.Fatalf("preparation consumed input: %q %v", raw, err) } - if p.session.opts.Dir != r.WorkDir || p.session.opts.DataDir == r.WorkDir { + if p.session.opts.Dir != r.LocalEnvironment.WorkspaceRoot || p.session.opts.DataDir == r.LocalEnvironment.WorkspaceRoot { t.Fatal("native cwd must use the workspace without moving Session state") } out := make(chan proto.Envelope) diff --git a/apps/daemon/internal/agent/mcode/tool_environment_test.go b/apps/daemon/internal/agent/mcode/tool_environment_test.go index 789b8f1ab..557ff5dfa 100644 --- a/apps/daemon/internal/agent/mcode/tool_environment_test.go +++ b/apps/daemon/internal/agent/mcode/tool_environment_test.go @@ -39,7 +39,7 @@ func TestWorkspaceCredentialsRemainInRuntimeSnapshotAcrossReconnect(t *testing.T t.Setenv("OAC_RUNTIME_CAPABILITY_DIRECTORY", t.TempDir()) t.Setenv("OAC_RUNTIME_NETWORK_ACCESS", "enabled") t.Setenv("OAC_RUNTIME_ALLOWED_DOMAINS", "") - req.WorkDir, req.AgentStateKey = "", "agents-api-"+session + req.AgentStateKey = "agents-api-" + session req.LocalEnvironment.ID, req.LocalEnvironment.WorkspaceDirectory = environment, config.Directory req.LocalEnvironment.Capabilities = true req.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{plugin}} diff --git a/apps/daemon/internal/agent/mcode/workspace.go b/apps/daemon/internal/agent/mcode/workspace.go index 293445ccc..5c8f5aa6d 100644 --- a/apps/daemon/internal/agent/mcode/workspace.go +++ b/apps/daemon/internal/agent/mcode/workspace.go @@ -54,7 +54,7 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P 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.WorkDir != 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.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 { return launchOptions{}, fmt.Errorf("mcode: execution does not match the dedicated workspace") } servers, err := runtimeMCP(req) @@ -64,7 +64,7 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P // Reuse public option validation and private Session state provisioning. // The native process, ACP Session and workspace tools share the declared cwd. private := req - private.LocalEnvironment, private.WorkDir, private.DisableExecutionEnvironment = nil, "", true + private.LocalEnvironment, private.DisableExecutionEnvironment = nil, true // Public declarations have already been resolved into the transient ACP map. private.MCPHTTPServers = nil opts, err := prepareOptionsWithSkills(ctx, private, false) diff --git a/apps/daemon/internal/dispatch/prompt.go b/apps/daemon/internal/dispatch/prompt.go index f2b0dbbb3..2a8d45908 100644 --- a/apps/daemon/internal/dispatch/prompt.go +++ b/apps/daemon/internal/dispatch/prompt.go @@ -61,7 +61,7 @@ func (r *Router) handlePromptRequest(callerCtx context.Context, env proto.Envelo } r.log.InfoContext(callerCtx, "handlePromptRequest: decoded", "run_id", runID, "agent_kind", req.AgentKind, - "work_dir", req.WorkDir, "message_count", len(req.Input), + "message_count", len(req.Input), "has_agent_options", req.AgentOptions != nil, "agent_session_id", req.AgentSessionID, "agent_state_key", req.AgentStateKey) diff --git a/apps/daemon/internal/localworkspace/binding.go b/apps/daemon/internal/localworkspace/binding.go index 94abe0744..c56610270 100644 --- a/apps/daemon/internal/localworkspace/binding.go +++ b/apps/daemon/internal/localworkspace/binding.go @@ -69,7 +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.WorkDir != "" || + r.DisableExecutionEnvironment || r.ConversationID != "" || r.WorkspaceAuthoring || !r.StrictResume { return r, errors.New("request does not match the dedicated local Environment") } @@ -93,8 +93,8 @@ func (b *Binding) Configure(r proto.PromptRequestPayload) (proto.PromptRequestPa return r, agentcapabilities.ErrInvalid } local.Skills, local.MCP, local.CapabilityRoot = nil, nil, "" + local.WorkspaceRoot = b.workspace r.LocalEnvironment = &local - r.WorkDir = b.workspace } return r, nil } diff --git a/apps/daemon/internal/localworkspace/binding_test.go b/apps/daemon/internal/localworkspace/binding_test.go index ff243918c..42e9c565f 100644 --- a/apps/daemon/internal/localworkspace/binding_test.go +++ b/apps/daemon/internal/localworkspace/binding_test.go @@ -28,10 +28,10 @@ func testBinding(t *testing.T) (*Binding, proto.PromptRequestPayload) { return b, proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{ID: environment, NetworkAccess: "disabled", WorkspaceDirectory: "/workspace", CapabilitySources: &agentcapabilities.Input{}}, AgentStateKey: "agents-api-" + session, StrictResume: true, ReleaseOnCompletion: true} } -func TestBindingRejectsScopeAndPathOverrides(t *testing.T) { +func TestBindingRejectsScopeOverrides(t *testing.T) { b, valid := testBinding(t) configured, err := b.Configure(valid) - if err != nil || configured.WorkDir != b.workspace { + if err != nil || configured.LocalEnvironment.WorkspaceRoot != b.workspace { t.Fatalf("frozen cwd: %+v %v", configured, err) } for name, mutate := range map[string]func(*proto.PromptRequestPayload){ @@ -40,7 +40,6 @@ func TestBindingRejectsScopeAndPathOverrides(t *testing.T) { r.LocalEnvironment = &proto.LocalEnvironment{ID: uuid.NewString()} }, "other Session": func(r *proto.PromptRequestPayload) { r.AgentStateKey = "agents-api-" + uuid.NewString() }, - "path override": func(r *proto.PromptRequestPayload) { r.WorkDir = b.workspace }, "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 }, diff --git a/apps/daemon/internal/localworkspace/capabilities.go b/apps/daemon/internal/localworkspace/capabilities.go index 1dae83a6d..3dd410248 100644 --- a/apps/daemon/internal/localworkspace/capabilities.go +++ b/apps/daemon/internal/localworkspace/capabilities.go @@ -19,7 +19,7 @@ func (b *Binding) Prepare(ctx context.Context, r proto.PromptRequestPayload) (pr return r, nil } if b == nil || r.LocalEnvironment == nil || r.LocalEnvironment.CapabilitySources == nil || - r.LocalEnvironment.ID != b.environment || r.AgentStateKey != b.stateKey || r.WorkDir != b.workspace { + r.LocalEnvironment.ID != b.environment || r.AgentStateKey != b.stateKey || r.LocalEnvironment.WorkspaceRoot != b.workspace { return r, agentcapabilities.ErrInvalid } b.capabilityMu.Lock() diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 027d2b126..039fec5aa 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -84,6 +84,8 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl The reason is a fixed safe string, never submitted content, a credential or raw native diagnostics. Unsupported guarantees no native side effect and is not a successful empty operation. Installation unavailability, unknown interaction IDs, native failures and uncertain outcomes keep their own errors and ownership. A nil `Turn` still means that no input was submitted and the output stays with the caller; never use it as an Unsupported marker. +An Environment execution request carries `LocalEnvironment.WorkspaceRoot`, the Runtime's bound workspace directory; run the native Harness there. The wire request carries no path. + Workspace capability describes the actual Runtime and resource-owner combination. The Codex and MiniMax resource objects reject native workspace access while the common authorized `localworkspace` owner provides it; Claude can expose native read and list access, and the common owner provides writes. Interface presence alone never selects a resource or advertises support. The service profile qualifies public combinations and the Runtime advertises the installed combination; neither replaces schema validation or Project authorization. Native behavior tests must agree with the declarations. An advertised operation that returns Unsupported is a contract violation, never success or grounds for replay. diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index f11e5413b..0d663256b 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -60,7 +60,7 @@ The prompt request (`prompt_request`, or the configuration of `execution_prepare | `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 | +| `local_environment` | For `openai_hosted` and `self_hosted`, with the exact Environment binding. The request carries no local path; the Runtime gives the Harness its bound workspace directory | | `strict_resume`, `require_existing_native_session` | Always strict; the second when a native Session must be recovered | | `durable_receipt` on `prompt_steer` | For every active input Core delivers | diff --git a/internal/agentdaemon/proto/environment.go b/internal/agentdaemon/proto/environment.go index 041a62115..61f5d204a 100644 --- a/internal/agentdaemon/proto/environment.go +++ b/internal/agentdaemon/proto/environment.go @@ -12,6 +12,9 @@ type LocalEnvironment struct { ID string `json:"id"` WorkspaceDirectory string `json:"workspace_directory"` CapabilitySources *agentcapabilities.Input `json:"capability_sources"` + // WorkspaceRoot is the bound local root the daemon supplies for execution; + // wire input cannot supply paths. Read-only preparation leaves it empty. + WorkspaceRoot string `json:"-"` // Capabilities is derived from the frozen selection for engine qualification; // Runtime still ensures and loads the protected installation before execution. Capabilities bool `json:"capabilities,omitempty"` diff --git a/internal/agentdaemon/proto/outbound.go b/internal/agentdaemon/proto/outbound.go index 5ceda0530..041492308 100644 --- a/internal/agentdaemon/proto/outbound.go +++ b/internal/agentdaemon/proto/outbound.go @@ -53,12 +53,6 @@ type PromptRequestPayload struct { // Input preserves ordered user messages and content. Input MessageInput `json:"input,omitempty"` - // WorkDir is the cwd for the agent subprocess. Local mode: user's - // chosen project root. Sandbox mode: empty — the daemon falls - // back to a per-conversation scratch dir so plugin installs and - // the subprocess cwd stay on the same tree. - WorkDir string `json:"work_dir,omitempty"` - // AgentOptions carries agent-specific overrides (model, mode, // allowed_tools, system_prompt, mcp_servers, plugin_dirs, env, // ...). The daemon's agent interprets these; the gateway never diff --git a/internal/agentdaemon/proto/workspace_read_preparation.go b/internal/agentdaemon/proto/workspace_read_preparation.go index 7d8719c93..f732a329b 100644 --- a/internal/agentdaemon/proto/workspace_read_preparation.go +++ b/internal/agentdaemon/proto/workspace_read_preparation.go @@ -1,11 +1,11 @@ package proto -// ValidWorkspaceReadPreparation excludes execution configuration and local paths. +// ValidWorkspaceReadPreparation excludes execution configuration. // The native adapter supplies temporary state; this request cannot resume or start. func ValidWorkspaceReadPreparation(r PromptRequestPayload) bool { return r.WorkspaceReadOnly && r.LocalEnvironment != nil && r.AgentStateKey != "" && r.StrictResume && r.ReleaseOnCompletion && r.RunID == "" && len(r.Input) == 0 && - r.ConversationID == "" && r.AgentSessionID == "" && r.WorkDir == "" && + r.ConversationID == "" && r.AgentSessionID == "" && !r.RequireExistingNativeSession && !r.WorkspaceAuthoring && !r.DisableExecutionEnvironment && len(r.AgentOptions) == 0 && r.ExecutionControls == nil && r.MCPHTTPServers == nil && len(r.FunctionTools) == 0 && !r.ToolSearch && !r.ObserveMessages && diff --git a/services/core/internal/execution/environment_placement_test.go b/services/core/internal/execution/environment_placement_test.go index 060d32b82..227cff346 100644 --- a/services/core/internal/execution/environment_placement_test.go +++ b/services/core/internal/execution/environment_placement_test.go @@ -51,7 +51,7 @@ func TestLocalEnvironmentRequiresQualifiedProfileAndExactAuthority(t *testing.T) var req proto.PromptRequestPayload err := d.configurePreparedEnvironment(session, environment, store.ExecutionDevice{EnvironmentID: scope}, &req) if scope == environment.ID { - if err != nil || req.LocalEnvironment == nil || req.LocalEnvironment.ID != environment.ID || req.WorkDir != "" { + if err != nil || req.LocalEnvironment == nil || req.LocalEnvironment.ID != environment.ID { t.Fatal("local identity was not preserved", err) } } else if err == nil || req.LocalEnvironment != nil { diff --git a/services/core/internal/store/local_environment_worker_test.go b/services/core/internal/store/local_environment_worker_test.go index 11350d100..971d405ac 100644 --- a/services/core/internal/store/local_environment_worker_test.go +++ b/services/core/internal/store/local_environment_worker_test.go @@ -125,7 +125,7 @@ func TestLocalEnvironmentWorkerSchedulesPreparationWithoutRemoteResolver(t *test } } var prepare proto.ExecutionPreparePayload - if frame.DecodePayload(&prepare) != nil || prepare.Configuration.LocalEnvironment == nil || prepare.Configuration.LocalEnvironment.ID != environment.ID || prepare.Configuration.WorkDir != "" { + if frame.DecodePayload(&prepare) != nil || prepare.Configuration.LocalEnvironment == nil || prepare.Configuration.LocalEnvironment.ID != environment.ID { t.Fatal("local preparation lost identity") } before, err := h.s.GetSession(t.Context(), h.tenant, h.session.ID) From 6c15fbd46879d2d4480cb71ae0f2b44f5bef6c15 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 15:38:04 +0000 Subject: [PATCH 2/2] Describe the workspace binding check and test Prepare's root check A self_hosted request still carries local_environment.workspace_directory and capability source directories, so the docs say the request carries no working directory: the Runtime checks workspace_directory against its binding. Harness onboarding owns how the Harness receives the bound root. The SessionPlan.Cwd comment covers environment:none, and a test checks that Binding.Prepare rejects a mismatched WorkspaceRoot. --- apps/daemon/internal/agent/codex/options.go | 3 ++- .../internal/localworkspace/binding_test.go | 24 +++++++++++++++++++ contracts/agents-api/harness-onboarding.md | 2 +- docs/runtime-protocol.md | 2 +- internal/agentdaemon/proto/environment.go | 2 +- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index a8f1e6fd1..46b280b55 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -17,7 +17,8 @@ import ( // the daemon's PromptRequestPayload. type SessionPlan struct { // Cwd is the working directory passed to codex and the spawned - // app-server: the bound workspace root for an Environment request. + // app-server: the bound workspace root for an Environment request. For + // environment:none it is empty, or CODEX_HOME when MCP is configured. Cwd string // Env is the full environment slice (KEY=value) to layer onto diff --git a/apps/daemon/internal/localworkspace/binding_test.go b/apps/daemon/internal/localworkspace/binding_test.go index 42e9c565f..372cc8ec7 100644 --- a/apps/daemon/internal/localworkspace/binding_test.go +++ b/apps/daemon/internal/localworkspace/binding_test.go @@ -1,6 +1,7 @@ package localworkspace import ( + "errors" "os" "path/filepath" "testing" @@ -73,6 +74,29 @@ func TestDirectoryValidatesRelativePaths(t *testing.T) { } } +func TestBindingPrepareRejectsOtherWorkspaceRoot(t *testing.T) { + b, req := testBinding(t) + configured, err := b.Configure(req) + if err != nil { + t.Fatal(err) + } + for _, root := range []string{"", t.TempDir()} { + local := *configured.LocalEnvironment + local.WorkspaceRoot = root + other := configured + other.LocalEnvironment = &local + if _, err := b.Prepare(t.Context(), other); !errors.Is(err, agentcapabilities.ErrInvalid) { + t.Fatalf("workspace root %q: %v", root, err) + } + } + if _, err := os.Stat(filepath.Join(b.capabilityRoot, agentcapabilities.ManifestName)); !os.IsNotExist(err) { + t.Fatal("rejected preparation installed capabilities") + } + if _, err := b.Prepare(t.Context(), configured); err != nil { + t.Fatal("bound workspace root rejected", err) + } +} + func TestBindingAllowsRetainedExecutor(t *testing.T) { b, req := testBinding(t) req.ReleaseOnCompletion = false diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 039fec5aa..c49f9e1c1 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -84,7 +84,7 @@ func (s *Session) SubmitFunctionResult(context.Context, proto.FunctionResultPayl The reason is a fixed safe string, never submitted content, a credential or raw native diagnostics. Unsupported guarantees no native side effect and is not a successful empty operation. Installation unavailability, unknown interaction IDs, native failures and uncertain outcomes keep their own errors and ownership. A nil `Turn` still means that no input was submitted and the output stays with the caller; never use it as an Unsupported marker. -An Environment execution request carries `LocalEnvironment.WorkspaceRoot`, the Runtime's bound workspace directory; run the native Harness there. The wire request carries no path. +The wire request carries no working directory. The Runtime checks `local_environment.workspace_directory` against its binding and gives the Harness its bound workspace directory in `LocalEnvironment.WorkspaceRoot`; run the native Harness there. Workspace capability describes the actual Runtime and resource-owner combination. The Codex and MiniMax resource objects reject native workspace access while the common authorized `localworkspace` owner provides it; Claude can expose native read and list access, and the common owner provides writes. Interface presence alone never selects a resource or advertises support. diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 0d663256b..25450faab 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -60,7 +60,7 @@ The prompt request (`prompt_request`, or the configuration of `execution_prepare | `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 local path; the Runtime gives the Harness its bound workspace directory | +| `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 | | `durable_receipt` on `prompt_steer` | For every active input Core delivers | diff --git a/internal/agentdaemon/proto/environment.go b/internal/agentdaemon/proto/environment.go index 61f5d204a..b013d9ae9 100644 --- a/internal/agentdaemon/proto/environment.go +++ b/internal/agentdaemon/proto/environment.go @@ -13,7 +13,7 @@ type LocalEnvironment struct { WorkspaceDirectory string `json:"workspace_directory"` CapabilitySources *agentcapabilities.Input `json:"capability_sources"` // WorkspaceRoot is the bound local root the daemon supplies for execution; - // wire input cannot supply paths. Read-only preparation leaves it empty. + // wire input cannot supply it. Read-only preparation leaves it empty. WorkspaceRoot string `json:"-"` // Capabilities is derived from the frozen selection for engine qualification; // Runtime still ensures and loads the protected installation before execution.