From a7ae01d531d02c182591b54f9d72a7b0665564bb Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Tue, 20 Oct 2026 05:41:45 +0000 Subject: [PATCH] Accept the hosted workspace alias as Claude stdio cwd Core admits a Claude Code stdio MCP cwd against the Environment's declared workspace directory, which is /workspace for hosted Environments. The daemon compared it with the bound root (/environment/workspace in microsandbox), so every hosted stdio server failed runtime preparation. The adapter now checks the same declared directory; the bound root and its /workspace alias are one directory, and native still starts the server there. --- .../internal/agent/claudesdk/mcp_environment.go | 7 ++++--- .../agent/claudesdk/mcp_environment_test.go | 13 +++++++------ apps/daemon/internal/agent/claudesdk/options.go | 2 +- apps/daemon/internal/agent/claudesdk/workspace.go | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment.go b/apps/daemon/internal/agent/claudesdk/mcp_environment.go index 704d71f6a..06562bb78 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment.go @@ -20,8 +20,7 @@ type environmentMCPServer struct { Declared bool `json:"declared,omitempty"` } -// cwd is the directory the native process starts in; native starts stdio servers there. -func prepareRuntimeMCP(req proto.PromptRequestPayload, cwd string) ([]environmentMCPServer, []string, error) { +func prepareRuntimeMCP(req proto.PromptRequestPayload) ([]environmentMCPServer, []string, error) { bindings, err := agent.ResolveMCPBindings(req) if err != nil { return nil, nil, err @@ -33,7 +32,9 @@ func prepareRuntimeMCP(req proto.PromptRequestPayload, cwd string) ([]environmen return nil, nil, fmt.Errorf("claudesdk: unsupported MCP identity") } if process := binding.Process; process != nil { - if filepath.Clean(process.Cwd) != cwd { + // Native starts stdio servers in its workspace, which the declared + // directory names directly or through the hosted /workspace alias. + if req.LocalEnvironment == nil || filepath.Clean(process.Cwd) != req.LocalEnvironment.WorkspaceDirectory { return nil, nil, fmt.Errorf("claudesdk: MCP stdio cwd must be the workspace directory") } args := append([]string{}, process.Args...) diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go index b3620bb53..d1c87211e 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -65,7 +65,7 @@ func TestEnvironmentMCPRejectsUnqualifiedCombinations(t *testing.T) { } { environment := &proto.LocalEnvironment{MCP: []proto.EnvironmentMCP{{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}}}} mutate(environment) - if _, _, err := prepareRuntimeMCP(proto.PromptRequestPayload{LocalEnvironment: environment}, ""); err == nil { + if _, _, err := prepareRuntimeMCP(proto.PromptRequestPayload{LocalEnvironment: environment}); err == nil { t.Fatal("unsupported declaration accepted") } } @@ -91,17 +91,18 @@ func TestEnvironmentMCPObservationsUseInstalledDeclarations(t *testing.T) { } // Native starts a stdio server in the directory of its own process, so the -// declared cwd must be that workspace directory. +// declared cwd must be the declared workspace directory. func TestPublicMCPStdioStartsInTheWorkspaceDirectory(t *testing.T) { servers := []proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "local", - Stdio: &proto.MCPStdio{Command: "run", Cwd: "/work", EnvVars: []string{"TOKEN"}}}} - req := proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{}, MCPHTTPServers: &servers} - projected, _, err := prepareRuntimeMCP(req, "/work") + Stdio: &proto.MCPStdio{Command: "run", Cwd: "/workspace", EnvVars: []string{"TOKEN"}}}} + req := proto.PromptRequestPayload{LocalEnvironment: &proto.LocalEnvironment{WorkspaceDirectory: "/workspace", WorkspaceRoot: "/environment/workspace"}, MCPHTTPServers: &servers} + projected, _, err := prepareRuntimeMCP(req) raw, _ := json.Marshal(projected) if err != nil || string(raw) != `[{"server_label":"local","allowed_tools":null,"command":"run","args":[],"declared":true}]` { t.Fatal("public stdio server was not projected", string(raw), err) } - if _, _, err := prepareRuntimeMCP(req, "/elsewhere"); err == nil { + req.LocalEnvironment.WorkspaceDirectory = "/elsewhere" + if _, _, err := prepareRuntimeMCP(req); err == nil { t.Fatal("stdio cwd outside the workspace directory was accepted") } } diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index 73b798eab..3a50a1a28 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -164,7 +164,7 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR } env := withProvider(append(append([]string{}, os.Environ()...), config.Env...), provider) env = append(env, "CLAUDE_CONFIG_DIR="+config.StateDir, "TMPDIR="+filepath.Join(config.StateDir, "tmp")) - projectedMCP, mcpEnv, err := prepareRuntimeMCP(req, "") + projectedMCP, mcpEnv, err := prepareRuntimeMCP(req) if err != nil { return startRequest{}, nil, err } diff --git a/apps/daemon/internal/agent/claudesdk/workspace.go b/apps/daemon/internal/agent/claudesdk/workspace.go index 66a2486ca..640e5cbc2 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace.go +++ b/apps/daemon/internal/agent/claudesdk/workspace.go @@ -57,7 +57,7 @@ func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspace profile.Skills = req.LocalEnvironment.Skills profile.CapabilityRoot = req.LocalEnvironment.CapabilityRoot } - servers, credentials, err := prepareRuntimeMCP(req, workspaceCwd(config.Workspace)) + servers, credentials, err := prepareRuntimeMCP(req) if err != nil { return nil, nil, err }