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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/daemon/internal/agent/claudesdk/mcp_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@ func prepareRuntimeMCP(req proto.PromptRequestPayload) ([]environmentMCPServer,
if err != nil {
return nil, nil, err
}
return mcpServers(bindings, localworkspace.MCPStdioCommand)
}

// mcpServers renders resolved bindings, each stdio binding with the command
// and arguments stdio gives it and each credential in a private environment
// variable.
func mcpServers(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) ([]environmentMCPServer, []string, error) {
var servers []environmentMCPServer
var env []string
for _, binding := range bindings {
if !mcpLabel.MatchString(binding.ServerLabel) || binding.ServerLabel == "functions" {
return nil, nil, fmt.Errorf("claudesdk: unsupported MCP identity")
}
if binding.Stdio != nil {
command, args := localworkspace.MCPStdioCommand(*binding.Stdio)
command, args := stdio(*binding.Stdio)
servers = append(servers, environmentMCPServer{mcpHTTPServer: mcpHTTPServer{ServerLabel: binding.ServerLabel}, Command: command, Args: args})
continue
}
Expand Down
72 changes: 36 additions & 36 deletions apps/daemon/internal/agent/claudesdk/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"strings"
Expand Down Expand Up @@ -83,12 +84,12 @@ func declareView(probe Config, info RuntimeInfo, node, root, bridge string, load
ForwardEnv: []string{"CLAUDECODE", "GIT_EDITOR"},
Proxy: agent.ViewProxyEnv,
Capabilities: agent.ViewCapabilities{
EnvironmentNone: proto.CapabilityUnsupported,
EnvironmentNone: proto.CapabilitySupported,
Skills: proto.CapabilityUnsupported,
FunctionTools: proto.CapabilityFromBool(info.SupportsWorkspaceFunctions()),
FunctionResultImages: proto.CapabilityFromBool(info.SupportsFunctionResultImages()),
ToolSearch: proto.CapabilityFromBool(info.SupportsWorkspaceToolSearch()),
StdioMCP: proto.CapabilityUnsupported,
StdioMCP: proto.CapabilitySupported,
},
}
loader.AddTo(view)
Expand All @@ -115,15 +116,18 @@ func newViewExecutorFactory(probe Config, layout viewLayout) agent.ViewExecutorF
}
}

// prepareView builds the workspace profile for one Session from the request
// and the view: the workspace is the sandbox's, MCP comes only from the view,
// prepareView builds the start request for one Session from the request and
// the view: the workspace profile in the sandbox's workspace, or no workspace
// in the work directory with environment none. MCP comes only from the view,
// and the environment is closed.
func prepareView(layout viewLayout, req proto.PromptRequestPayload, view agent.ViewSession) (startRequest, []string, error) {
environment := req.LocalEnvironment
if environment == nil || !workspacePathSyntax(environment.WorkspaceRoot) || req.DisableExecutionEnvironment || view.Launch == nil || view.Proxy == "" {
return startRequest{}, nil, errors.New("claudesdk: a view Executor requires the sandbox workspace, Launch and the gateway proxy")
if (environment == nil) != req.DisableExecutionEnvironment || environment != nil && !workspacePathSyntax(environment.WorkspaceRoot) || view.Launch == nil || view.Proxy == "" {
return startRequest{}, nil, errors.New("claudesdk: a view Executor requires the sandbox workspace or environment none, Launch and the gateway proxy")
}
servers, err := viewMCP(view.MCP)
// The gateway adds each credential and header, and the Harness runs each
// stdio alias without arguments.
servers, _, err := mcpServers(view.MCP, func(stdio proto.EnvironmentMCP) (string, []string) { return stdio.Server.Command, nil })
if err != nil {
return startRequest{}, nil, err
}
Expand All @@ -134,33 +138,24 @@ func prepareView(layout viewLayout, req proto.PromptRequestPayload, view agent.V
if err := viewHome(view.Home); err != nil {
return startRequest{}, nil, err
}
profile, env := viewEnvironment(layout, view.Home.View, view.Proxy, provider)
profile, env := viewEnvironment(layout, view.Home.View, view.Proxy, provider, environment != nil)
if environment == nil {
// Environment none has no Environment MCP, so each server is HTTP.
start.Cwd = path.Join(view.Home.View, agent.ViewWorkName)
if len(servers) != 0 {
http := make([]mcpHTTPServer, 0, len(servers))
for _, server := range servers {
http = append(http, server.mcpHTTPServer)
}
start.MCPHTTPServers = &http
}
return start, env, nil
}
profile.NetworkAccess, profile.MCP = environment.NetworkAccess, servers
start.Workspace, start.Cwd = profile, environment.WorkspaceRoot
return start, env, nil
}

// viewMCP renders the gateway's HTTP endpoints. The gateway adds each
// credential and header, so none is rendered here.
func viewMCP(bindings []agent.MCPBinding) ([]environmentMCPServer, error) {
declarations := make([]proto.MCPHTTPServer, 0, len(bindings))
for _, binding := range bindings {
if binding.Transport != "http" || binding.Stdio != nil {
return nil, fmt.Errorf("%w: %s MCP in an agent-host view", agent.ErrUnsupportedOperation, binding.Transport)
}
declarations = append(declarations, proto.MCPHTTPServer{ServerLabel: binding.ServerLabel, ServerURL: binding.ServerURL, AllowedTools: binding.AllowedTools, Required: binding.Required})
}
if err := validateMCPServers(declarations); err != nil {
return nil, err
}
projected, _ := prepareMCPHTTP(&declarations)
servers := make([]environmentMCPServer, 0, len(*projected))
for _, server := range *projected {
servers = append(servers, environmentMCPServer{mcpHTTPServer: server})
}
return servers, nil
}

// viewHome lays out the native directories. A later Executor finds the tree
// the Session uid has owned, so every operation stays inside one os.Root and
// an existing entry must be a directory, not a link.
Expand All @@ -185,23 +180,28 @@ func viewHome(home agent.ViewDir) error {
}

// viewEnvironment is the complete Harness environment. home is the Session
// home's view path.
func viewEnvironment(layout viewLayout, home, proxy string, provider []string) (*workspaceProfile, []string) {
// home's view path, and workspace adds what the workspace tools need.
func viewEnvironment(layout viewLayout, home, proxy string, provider []string, workspace bool) (*workspaceProfile, []string) {
shims := agent.ViewPrivateRoot + "/" + agent.ViewShimName
profile := &workspaceProfile{Home: home + "/home", State: home + "/config", Scratch: home + "/tmp", EnvNames: []string{}, AllowedDomains: []string{}}
env := []string{
"PATH=" + shims, "HOME=" + profile.Home, "TMPDIR=" + profile.Scratch, "CLAUDE_CONFIG_DIR=" + profile.State,
// The messaging socket path stays local and short (C5).
"XDG_RUNTIME_DIR=" + home + "/xdg",
// Bash runs the sandbox shell through the shim (C3).
"SHELL=" + shims + "/bash", "CLAUDE_CODE_SHELL=" + shims + "/bash",
// The shell's cwd file must be at the same path on both sides (C4).
"CLAUDE_CODE_TMPDIR=/tmp/oac-claude-" + strings.ToLower(rand.Text()),
"CLAUDE_CODE_CERT_STORE=bundled", // C2
"USE_BUILTIN_RIPGREP=0", // C7: rg runs through its shim
"CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1", // C9
"CLAUDE_CODE_TOOL_MEMORY_LIMIT=0", // C13
}
if workspace {
env = append(env,
// Bash runs the sandbox shell through the shim (C3).
"SHELL="+shims+"/bash", "CLAUDE_CODE_SHELL="+shims+"/bash",
// The shell's cwd file must be at the same path on both sides
// (C4). An empty root has no /tmp, where Claude Code creates it.
"CLAUDE_CODE_TMPDIR=/tmp/oac-claude-"+strings.ToLower(rand.Text()),
"USE_BUILTIN_RIPGREP=0", // C7: rg runs through its shim
)
}
env = append(env, nativeFlags...)
if layout.libraries != "" {
env = append(env, "LD_LIBRARY_PATH="+layout.libraries)
Expand Down
34 changes: 31 additions & 3 deletions apps/daemon/internal/agent/claudesdk/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
Expand Down Expand Up @@ -106,10 +105,39 @@ func TestViewExecutorLaunchesAClosedGatewayEnvironment(t *testing.T) {
t.Fatal("the real key reached the view")
}

// The Harness runs a stdio binding's alias without arguments.
docs := session.MCP
session.MCP = []agent.MCPBinding{{ServerLabel: "local", Transport: "stdio", Stdio: &proto.EnvironmentMCP{
Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: agent.ViewAlias(0)}}}}
if _, err := view.Executor(t.Context(), req, session); !errors.Is(err, agent.ErrUnsupportedOperation) {
t.Fatalf("stdio MCP = %v, want ErrUnsupportedOperation", err)
stdio, err := view.Executor(t.Context(), req, session)
if err != nil {
t.Fatal(err)
}
defer stdio.Close(ctx)
var stdioStart startRequest
if err := json.Unmarshal(<-requests, &stdioStart); err != nil || stdioStart.Workspace == nil || len(stdioStart.Workspace.MCP) != 1 ||
stdioStart.Workspace.MCP[0].Command != agent.ViewAlias(0) || stdioStart.Workspace.MCP[0].Args != nil {
t.Fatalf("stdio MCP = %+v, %v", stdioStart.Workspace, err)
}

// With environment none the bridge runs without a workspace in the work directory.
none := req
none.LocalEnvironment, none.DisableExecutionEnvironment = nil, true
session.MCP = docs
noneExecutor, err := view.Executor(t.Context(), none, session)
if err != nil {
t.Fatal(err)
}
defer noneExecutor.Close(ctx)
var noneStart startRequest
if err := json.Unmarshal(<-requests, &noneStart); err != nil || launched.Dir != "/.oac/home/work" || noneStart.Cwd != launched.Dir || noneStart.Workspace != nil ||
noneStart.MCPHTTPServers == nil || len(*noneStart.MCPHTTPServers) != 1 || (*noneStart.MCPHTTPServers)[0].ServerURL != "http://127.0.0.1:17102/mcp/docs" {
t.Fatalf("environment none launched in %q with %+v, %v", launched.Dir, noneStart, err)
}
for _, entry := range launched.Env {
if strings.HasPrefix(entry, "CLAUDE_CODE_TMPDIR=") || strings.HasPrefix(entry, "SHELL=") {
t.Errorf("environment none sets the workspace tools' %s", entry)
}
}

// A link the Session uid planted in its home is never followed.
Expand Down
11 changes: 6 additions & 5 deletions apps/daemon/internal/agent/codex/mcp_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConf
if bindings == nil {
return nil, nil, nil
}
return mcpServersFromBindings(bindings)
return mcpServersFromBindings(bindings, localworkspace.MCPStdioCommand)
}

// mcpServersFromBindings renders resolved bindings, with each credential in a
// private environment variable.
func mcpServersFromBindings(bindings []agent.MCPBinding) (map[string]mcpServerConfig, []string, error) {
// mcpServersFromBindings renders resolved bindings, each stdio binding with
// the command and arguments stdio gives it and each credential in a private
// environment variable.
func mcpServersFromBindings(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) (map[string]mcpServerConfig, []string, error) {
servers := make(map[string]mcpServerConfig, len(bindings))
var env []string
for _, binding := range bindings {
Expand All @@ -35,7 +36,7 @@ func mcpServersFromBindings(bindings []agent.MCPBinding) (map[string]mcpServerCo
}
server := mcpServerConfig{Name: binding.ServerLabel, URL: binding.ServerURL, Required: binding.Required, EnabledTools: binding.AllowedTools, ApproveTools: binding.ConnectionOrigin == "environment"}
if binding.Stdio != nil {
server.Command, server.Args = localworkspace.MCPStdioCommand(*binding.Stdio)
server.Command, server.Args = stdio(*binding.Stdio)
}
if binding.BearerToken != nil {
server.BearerTokenEnvVar = "OAC_RUNTIME_MCP_BEARER_" + rand.Text()
Expand Down
8 changes: 6 additions & 2 deletions apps/daemon/internal/agent/codex/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ 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 and
// the Session's private CODEX_HOME for environment:none.
// app-server: the bound workspace root for an Environment request and,
// for environment:none, the Session's private CODEX_HOME or a view's work
// directory.
Cwd string

// Env is the environment slice (KEY=value) the plan adds. A local
Expand Down Expand Up @@ -122,6 +123,9 @@ func buildSessionPlan(req proto.PromptRequestPayload, allocHome func() (agent.Vi
}
plan.home = home
plan.Env = []string{"DISABLE_TELEMETRY=1", "CODEX_HOME=" + home.View}
if req.DisableExecutionEnvironment {
plan.Env = append(plan.Env, "CODEX_EXEC_SERVER_URL=none")
}
if prepared.Provider != nil {
if err := writeCodexProviderConfig(home.Host, nativeProvider(*prepared.Provider)); err != nil {
return plan, err
Expand Down
3 changes: 0 additions & 3 deletions apps/daemon/internal/agent/codex/session_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg
}
}

if req.DisableExecutionEnvironment {
plan.Env = append(plan.Env, "CODEX_EXEC_SERVER_URL=none")
}
var skillRoots []string
if req.LocalEnvironment != nil && len(req.LocalEnvironment.Skills) > 0 {
if err := verifyHostedSkills(req.LocalEnvironment.Skills); err != nil {
Expand Down
34 changes: 18 additions & 16 deletions apps/daemon/internal/agent/codex/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ func newView(binary string, codeModeHost bool) agent.View {
ForwardEnv: slices.Clone(viewForwardEnv),
Proxy: agent.ViewProxyEnv,
Capabilities: agent.ViewCapabilities{
EnvironmentNone: proto.CapabilityUnsupported,
EnvironmentNone: proto.CapabilitySupported,
Skills: proto.CapabilityUnsupported,
FunctionTools: proto.CapabilitySupported,
FunctionResultImages: proto.CapabilitySupported,
ToolSearch: proto.CapabilityUnsupported,
StdioMCP: proto.CapabilityUnsupported,
StdioMCP: proto.CapabilitySupported,
},
Executor: func(ctx context.Context, req proto.PromptRequestPayload, session agent.ViewSession) (agent.Executor, error) {
cfg := defaultSessionConfig()
Expand Down Expand Up @@ -129,26 +129,27 @@ func staticELF(name string) bool {
}

// prepareViewPlan builds the plan for codex in the view: the Environment's
// workspace as cwd, CODEX_HOME and TMPDIR in the Session home, MCP only from
// the Session, and a closed environment.
// workspace as cwd, or the work directory with environment none, CODEX_HOME
// and TMPDIR in the Session home, MCP only from the Session, and a closed
// environment.
func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (SessionPlan, error) {
view := cfg.view
local := req.LocalEnvironment
if local == nil || req.DisableExecutionEnvironment || !path.IsAbs(local.WorkspaceRoot) {
return SessionPlan{}, fmt.Errorf("%w: codex: a view runs in an Environment workspace", agent.ErrUnsupportedOperation)
}
if !filepath.IsAbs(view.Home.Host) || !path.IsAbs(view.Home.View) {
return SessionPlan{}, errors.New("codex: view home must be absolute")
}
cwd := path.Join(view.Home.View, agent.ViewWorkName)
if local := req.LocalEnvironment; local != nil {
cwd = local.WorkspaceRoot
}
if (req.LocalEnvironment == nil) != req.DisableExecutionEnvironment || !path.IsAbs(cwd) {
return SessionPlan{}, fmt.Errorf("%w: codex: a view runs in an Environment workspace or with environment none", agent.ErrUnsupportedOperation)
}
if _, err := runtimePermissionProfile(req); err != nil {
return SessionPlan{}, err
}
for _, binding := range view.MCP {
if binding.Transport != "http" || binding.Stdio != nil {
return SessionPlan{}, fmt.Errorf("%w: codex: stdio MCP %q in a view", agent.ErrUnsupportedOperation, binding.ServerLabel)
}
}
servers, _, err := mcpServersFromBindings(view.MCP)
// The Harness runs each stdio alias without arguments, which the native
// configuration reports as an empty list.
servers, _, err := mcpServersFromBindings(view.MCP, func(stdio proto.EnvironmentMCP) (string, []string) { return stdio.Server.Command, []string{} })
if err != nil {
return SessionPlan{}, err
}
Expand All @@ -161,7 +162,7 @@ func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg se
return SessionPlan{}, err
}
disableProgrammaticTools(&plan, req.ExecutionControls)
plan.Cwd = local.WorkspaceRoot
plan.Cwd = cwd
plan.Sandbox = SandboxDangerFullAcces
plan.Permissions = ""
plan.ApprovalPolicy = AskForApproval{String: "never"}
Expand All @@ -182,7 +183,8 @@ func prepareViewPlan(ctx context.Context, req proto.PromptRequestPayload, cfg se
}
}
// No login shell, and no ancestor walk above the workspace over the
// mount. No trust entry is written, so the project stays untrusted.
// mount. The adapter writes no trust entry; Codex keeps its native
// project trust and records its own on thread/start.
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"allow_login_shell", "false"}, [2]string{"project_root_markers", "[]"})
if req.ExecutionControls != nil {
if err := viewModelVerbosity(ctx, cfg.codexBinary, &plan, req.ModelProvider); err != nil {
Expand Down
11 changes: 9 additions & 2 deletions apps/daemon/internal/agent/codex/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,14 @@ func TestViewExecutorLaunchesInTheSessionView(t *testing.T) {

session.MCP = []agent.MCPBinding{{ServerLabel: "local", ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: "stdio", Stdio: &proto.EnvironmentMCP{
Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: agent.ViewAlias(0)}}}}
if _, err := view.Executor(t.Context(), req, session); !errors.Is(err, agent.ErrUnsupportedOperation) || len(launched) != 1 {
t.Fatalf("stdio MCP: launches %d, err %v", len(launched), err)
req.LocalEnvironment, req.DisableExecutionEnvironment = nil, true
if _, err := view.Executor(t.Context(), req, session); err == nil || len(launched) != 2 {
t.Fatalf("environment none: launches %d, err %v", len(launched), err)
}
if launch := launched[1]; launch.Dir != "/.oac/home/work" || !slices.Contains(launch.Env, "CODEX_EXEC_SERVER_URL=none") {
t.Fatalf("environment none launched in %q with %v", launch.Dir, launch.Env)
}
if config, err := os.ReadFile(planted); err != nil || !strings.Contains(string(config), "command = \"/.oac/bin/oac-mcp-0\"\n\n") {
t.Fatalf("stdio alias config.toml: %v\n%s", err, config)
}
}
18 changes: 8 additions & 10 deletions apps/daemon/internal/agent/mcode/environment_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,24 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, []agent.MCPBi
if err != nil {
return nil, nil, err
}
servers, err := workspaceMCP(bindings, func(binding agent.MCPBinding) (map[string]any, error) {
command, args := localworkspace.MCPStdioCommand(*binding.Stdio)
return map[string]any{"name": binding.ServerLabel, "command": command, "args": args, "env": []map[string]string{}}, nil
})
servers, err := workspaceMCP(bindings, localworkspace.MCPStdioCommand)
return servers, bindings, err
}

// workspaceMCP renders the Session's MCP bindings as ACP servers; stdio
// renders a stdio binding.
func workspaceMCP(bindings []agent.MCPBinding, stdio func(agent.MCPBinding) (map[string]any, error)) ([]map[string]any, error) {
// workspaceMCP renders the Session's MCP bindings as ACP servers, each stdio
// binding with the command and arguments stdio gives it.
func workspaceMCP(bindings []agent.MCPBinding, stdio func(proto.EnvironmentMCP) (string, []string)) ([]map[string]any, error) {
var servers []map[string]any
for _, binding := range bindings {
if binding.ServerLabel == "oac_workspace" || binding.ConnectionOrigin != "environment" || binding.AllowedTools != nil || binding.Required {
return nil, fmt.Errorf("mcode: unsupported MCP binding")
}
render := environmentHTTPMCP
if binding.Transport != "http" {
render = stdio
command, args := stdio(*binding.Stdio)
servers = append(servers, map[string]any{"name": binding.ServerLabel, "command": command, "args": args, "env": []map[string]string{}})
continue
}
server, err := render(binding)
server, err := environmentHTTPMCP(binding)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading