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
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/claudesdk/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d
}
out := &agent.Runtime{Info: descriptor, Session: func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) {
return nil, fmt.Errorf("claude_sdk: configured runtime is unavailable")
}, View: nil}
}}
var config Config

fail := func(err error) *agent.Runtime {
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/codex/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.Su
return discoverWithCheck(ctx, options, info, CheckCLIAvailable)
}
func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime {
runtime := &agent.Runtime{Info: info, Session: Factory, SessionCapabilityContext: true, ExecutorCapabilityContext: true, View: nil}
runtime := &agent.Runtime{Info: info, Session: Factory, SessionCapabilityContext: true, ExecutorCapabilityContext: true}
ctx, cancel := context.WithTimeout(parent, 15*time.Second)
defer cancel()
version, err := check(ctx, "")
Expand Down
28 changes: 0 additions & 28 deletions apps/daemon/internal/agent/contract_declarations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package agent_test

import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
Expand Down Expand Up @@ -124,33 +123,6 @@ func TestPublicHarnessContractDeclarations(t *testing.T) {
t.Errorf("%s needs explicit compile assertions on %v; got %v", name, want, assertions[name])
}
}
// Each discovered Runtime decides its agent-host view explicitly, even when it declares none.
declaration, err := parser.ParseFile(token.NewFileSet(), filepath.Join(entry.Configuration, "declaration.go"), nil, 0)
if err != nil {
t.Fatal(err)
}
runtimes := 0
ast.Inspect(declaration, func(node ast.Node) bool {
literal, ok := node.(*ast.CompositeLit)
if !ok {
return true
}
selector, ok := literal.Type.(*ast.SelectorExpr)
if !ok || selector.Sel.Name != "Runtime" {
return true
}
runtimes++
for _, element := range literal.Elts {
if field, ok := element.(*ast.KeyValueExpr); ok && fmt.Sprint(field.Key) == "View" {
return true
}
}
t.Error("agent.Runtime literal must set View explicitly")
return true
})
if runtimes == 0 {
t.Error("declaration.go constructs no agent.Runtime")
}
})
}
}
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/mcode/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.Su
return discoverWithCheck(ctx, options, info, CheckCLIAvailable)
}
func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, result proto.SupportedAgentKind, check func(context.Context, string) (string, error)) *agent.Runtime {
runtime := &agent.Runtime{Info: result, Session: Factory, SessionCapabilityContext: true, ExecutorCapabilityContext: true, View: nil}
runtime := &agent.Runtime{Info: result, Session: Factory, SessionCapabilityContext: true, ExecutorCapabilityContext: true}

ctx, cancel := context.WithTimeout(parent, 15*time.Second)
defer cancel()
Expand Down
46 changes: 39 additions & 7 deletions apps/daemon/internal/agent/mcode/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ type launchOptions struct {
}

func prepareOptions(ctx context.Context, req proto.PromptRequestPayload) (launchOptions, error) {
return prepareOptionsWithSkills(ctx, req, true)
return prepareOptionsWithTools(ctx, req, nil)
}

func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayload, managedSkills bool) (launchOptions, error) {
// prepareOptionsWithTools installs the managed Skills unless the workspace
// bridge's tools present the Environment's.
func prepareOptionsWithTools(ctx context.Context, req proto.PromptRequestPayload, tools *workspaceTools) (launchOptions, error) {
var result launchOptions
if err := validateOptions(req); err != nil {
return result, err
Expand All @@ -67,7 +69,7 @@ func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayloa
if err := os.MkdirAll(result.Dir, 0o700); err != nil {
return result, err
}
if managedSkills {
if tools == nil {
installed, err := managedskills.InstallManagedSkills(ctx, log.With("component", "mcode"), root, req.AgentOptions["skills"])
if err != nil {
return result, err
Expand All @@ -81,7 +83,7 @@ func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayloa
return result, err
}
defer data.Close()
if result.Model, err = writeNativeConfig(req, data); err != nil {
if result.Model, err = writeNativeConfig(req, data, result.DataDir, tools); err != nil {
return result, err
}
opts := req.AgentOptions
Expand Down Expand Up @@ -123,8 +125,10 @@ func validateOptions(req proto.PromptRequestPayload) error {
}

// writeNativeConfig writes the instructions and native configuration into the
// data directory and returns the model.
func writeNativeConfig(req proto.PromptRequestPayload, data *os.Root) (string, error) {
// data directory, which the native process sees at dataDir, and returns the
// model. With tools, the workspace bridge replaces native permissions and
// sandbox, and Subagents use it too.
func writeNativeConfig(req proto.PromptRequestPayload, data *os.Root, dataDir string, tools *workspaceTools) (string, error) {
opts := req.AgentOptions
prompt := optionString(opts, "system_prompt")
if override := optionString(opts, "override_system_prompt"); override != "" {
Expand Down Expand Up @@ -164,6 +168,31 @@ func writeNativeConfig(req proto.PromptRequestPayload, data *os.Root) (string, e
return "", fmt.Errorf("mcode: unsupported permission mode")
}
config["permissionMode"] = mode
servers := map[string]any{}
if tools != nil {
config["permissionMode"] = "bypassPermissions"
config["sandbox"] = map[string]bool{"enabled": false}
if len(tools.skills) > 0 {
selected := config["agents"].(map[string]any)["default"].(map[string]any)
selected["skills"] = tools.skills
for _, key := range []string{"tools", "builtinTools"} {
selected[key] = append(selected[key].([]string), "skill")
}
}
raw, err := json.Marshal(tools.profile)
if err != nil {
return "", err
}
if err := data.WriteFile("workspace-profile.json", raw, 0o600); err != nil {
return "", err
}
if !req.DisableSubagents {
// This native data directory belongs to one public Session and its
// descendants. ACP's ephemeral server map otherwise covers only root.
server := tools.server(dataDir)
servers["oac_workspace"] = map[string]any{"type": "stdio", "command": server["command"], "args": server["args"], "env": map[string]string{}, "enabled": true}
}
}
raw, err := json.Marshal(config)
if err != nil {
return "", err
Expand All @@ -172,7 +201,10 @@ func writeNativeConfig(req proto.PromptRequestPayload, data *os.Root) (string, e
return "", err
}
if req.StrictResume {
if err := data.WriteFile("mcp.json", []byte(`{"mcpServers":{}}`), 0o600); err != nil {
if raw, err = json.Marshal(map[string]any{"mcpServers": servers}); err != nil {
return "", err
}
if err := data.WriteFile("mcp.json", raw, 0o600); err != nil {
return "", err
}
}
Expand Down
10 changes: 5 additions & 5 deletions apps/daemon/internal/agent/mcode/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,12 @@ func (i viewInstall) prepare(_ context.Context, req proto.PromptRequestPayload,
defer data.Close()
opts := launchOptions{Dir: workspace, DataDir: filepath.Join(session.Home.Host, viewDataName), bindings: session.MCP,
start: session.Launch, script: i.cli, home: session.Home.Host}
if opts.Model, err = writeNativeConfig(private, data); err != nil {
dataDir, tempDir := path.Join(session.Home.View, viewDataName), path.Join(session.Home.View, viewTempName)
tools := workspaceTools{node: i.node, bridge: i.bridge, profile: map[string]any{"workspace": workspace, "scratch": tempDir, "network": "enabled"}}
if opts.Model, err = writeNativeConfig(private, data, dataDir, &tools); err != nil {
return opts, err
}
dataDir, tempDir := path.Join(session.Home.View, viewDataName), path.Join(session.Home.View, viewTempName)
opts.MCP = append([]map[string]any{tools.server(dataDir)}, servers...)
opts.Env = []string{
"PATH=" + path.Join(agent.ViewPrivateRoot, agent.ViewShimName),
"TMPDIR=" + tempDir,
Expand All @@ -208,7 +210,5 @@ func (i viewInstall) prepare(_ context.Context, req proto.PromptRequestPayload,
opts.Env = append(opts.Env, nativeEnvironment(private, dataDir)...)
opts.spawn = session.Spawn
opts.reader = clirunner.StartOptions{Binary: i.node, Args: []string{path.Join(path.Dir(i.bridge), "subagent-snapshot.mjs"), dataDir}, Dir: dataDir, Env: opts.Env, OwnProcessGroup: true}
profile := map[string]any{"workspace": workspace, "scratch": tempDir, "network": "enabled"}
tools := workspaceTools{node: i.node, bridge: i.bridge, profile: path.Join(dataDir, "workspace-profile.json")}
return opts, writeWorkspaceTools(&opts, data, req, tools, profile, nil, servers)
return opts, nil
}
98 changes: 25 additions & 73 deletions apps/daemon/internal/agent/mcode/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package mcode

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -61,18 +60,29 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P
if err != nil {
return launchOptions{}, err
}
tools := workspaceTools{node: c.Node, bridge: c.Bridge, profile: map[string]any{"capabilityRoot": req.LocalEnvironment.CapabilityRoot, "workspace": c.Directory, "scratch": c.Scratch, "network": c.Network, "allowedDomains": (agentnetwork.Policy{Access: c.Network, AllowedDomains: c.AllowedDomains}).Hosts(), "skills": len(req.LocalEnvironment.Skills) > 0}}
file, err := localworkspace.ToolEnvironmentFile()
if err != nil {
return launchOptions{}, err
}
if file != "" {
tools.profile["toolEnvFile"] = file
}
for _, skill := range req.LocalEnvironment.Skills {
tools.skills = append(tools.skills, skill.Metadata.Name)
}
// 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.DisableExecutionEnvironment = nil, true
// Public declarations have already been resolved into the transient ACP map.
private.MCPHTTPServers = nil
opts, err := prepareOptionsWithSkills(ctx, private, false)
opts, err := prepareOptionsWithTools(ctx, private, &tools)
if err != nil {
return opts, err
}
opts.Dir, opts.bindings = c.Directory, bindings
var skills []string
opts.MCP = append([]map[string]any{tools.server(opts.DataDir)}, servers...)
if len(req.LocalEnvironment.Skills) > 0 {
root := filepath.Join(opts.DataDir, "skills")
if err := os.MkdirAll(root, 0700); err != nil {
Expand All @@ -90,79 +100,21 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P
} else if err := os.Symlink(target, link); err != nil {
return opts, err
}
skills = append(skills, skill.Metadata.Name)
}
}
profile := map[string]any{"capabilityRoot": req.LocalEnvironment.CapabilityRoot, "workspace": c.Directory, "scratch": c.Scratch, "network": c.Network, "allowedDomains": (agentnetwork.Policy{Access: c.Network, AllowedDomains: c.AllowedDomains}).Hosts(), "skills": len(skills) > 0}
file, err := localworkspace.ToolEnvironmentFile()
if err != nil {
return opts, err
}
if file != "" {
profile["toolEnvFile"] = file
}
data, err := os.OpenRoot(opts.DataDir)
if err != nil {
return opts, err
}
defer data.Close()
return opts, writeWorkspaceTools(&opts, data, req, workspaceTools{node: c.Node, bridge: c.Bridge, profile: filepath.Join(opts.DataDir, "workspace-profile.json")}, profile, skills, servers)
return opts, nil
}

// workspaceTools are the workspace bridge as the native process runs it, and
// the bridge's profile path.
type workspaceTools struct{ node, bridge, profile string }
// workspaceTools is the workspace bridge as the native process runs it, the
// bridge's profile and the Skills it presents.
type workspaceTools struct {
node, bridge string
profile map[string]any
skills []string
}

// writeWorkspaceTools turns the native configuration in data over to the
// workspace bridge: native permissions and sandbox are off, the bridge's
// profile is written, and oac_workspace precedes the Session's servers.
func writeWorkspaceTools(opts *launchOptions, data *os.Root, req proto.PromptRequestPayload, tools workspaceTools, profile map[string]any, skills []string, servers []map[string]any) error {
raw, err := data.ReadFile("config.yaml")
if err != nil {
return err
}
var config map[string]any
if err = json.Unmarshal(raw, &config); err != nil {
return err
}
config["permissionMode"] = "bypassPermissions"
config["sandbox"] = map[string]bool{"enabled": false}
if len(skills) > 0 {
selected := config["agents"].(map[string]any)["default"].(map[string]any)
selected["skills"] = skills
for _, key := range []string{"tools", "builtinTools"} {
selected[key] = append(selected[key].([]any), "skill")
}
}
if raw, err = json.Marshal(config); err != nil {
return err
}
if err = data.WriteFile("config.yaml", raw, 0600); err != nil {
return err
}
if raw, err = json.Marshal(profile); err != nil {
return err
}
if err = data.WriteFile("workspace-profile.json", raw, 0600); err != nil {
return err
}
opts.MCP = []map[string]any{{"name": "oac_workspace", "command": tools.node, "args": []string{tools.bridge, tools.profile}, "env": []map[string]string{}}}
opts.MCP = append(opts.MCP, servers...)
if !req.DisableSubagents {
// This native data directory belongs to one public Session and its
// descendants. ACP's ephemeral server map otherwise covers only root.
configured := map[string]any{}
for _, server := range opts.MCP[:1] {
name, _ := server["name"].(string)
configured[name] = map[string]any{"type": "stdio", "command": server["command"], "args": server["args"], "env": map[string]string{}, "enabled": true}
}
raw, err := json.Marshal(map[string]any{"mcpServers": configured})
if err != nil {
return err
}
if err := data.WriteFile("mcp.json", raw, 0o600); err != nil {
return err
}
}
return nil
// server is the bridge's ACP MCP server, with its profile in dataDir as the
// native process sees it.
func (t workspaceTools) server(dataDir string) map[string]any {
return map[string]any{"name": "oac_workspace", "command": t.node, "args": []string{t.bridge, filepath.Join(dataDir, "workspace-profile.json")}, "env": []map[string]string{}}
}
Loading
Loading