-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagents.go
More file actions
215 lines (202 loc) · 7.43 KB
/
Copy pathagents.go
File metadata and controls
215 lines (202 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package acpruntime
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
const (
CodexACPRegistryID = "codex-acp"
ClaudeCodeACPRegistryID = "claude-acp"
GeminiCLIACPRegistryID = "gemini"
GitHubCopilotACPRegistryID = "github-copilot-cli"
OpenCodeACPRegistryID = "opencode"
PiACPRegistryID = "pi-acp"
CursorACPRegistryID = "cursor"
SimulatorAgentACPRegistryID = "simulator-agent-acp"
LocalSimulatorAgentACPRegistryID = "local-simulator-agent-acp"
)
// CreateCodexAgent builds an Agent that launches the Codex ACP wrapper via npx.
// The package is unpinned so npm resolves the latest published version each
// spawn, keeping the wrapper in sync with upstream without a code change.
func CreateCodexAgent(overrides Agent) Agent {
return mergeAgent(Agent{Type: CodexACPRegistryID, Command: "npm", Args: []string{"exec", "--yes", "@agentclientprotocol/codex-acp", "--"}}, overrides)
}
// CreateClaudeCodeAgent builds an Agent that launches the Claude Code ACP
// wrapper via npx. The package is unpinned so npm resolves the latest published
// version each spawn, keeping the wrapper in sync with upstream without a code
// change.
func CreateClaudeCodeAgent(overrides Agent) Agent {
return mergeAgent(Agent{Type: ClaudeCodeACPRegistryID, Command: "npm", Args: []string{"exec", "--yes", "@agentclientprotocol/claude-agent-acp", "--"}}, overrides)
}
func CreateGeminiAgent(overrides Agent) Agent {
return mergeAgent(Agent{Type: GeminiCLIACPRegistryID, Command: "gemini", Args: []string{"--experimental-acp"}}, overrides)
}
func CreateGitHubCopilotAgent(overrides Agent) Agent {
return mergeAgent(Agent{Type: GitHubCopilotACPRegistryID, Command: "copilot", Args: []string{"acp"}}, overrides)
}
func CreateOpenCodeAgent(overrides Agent) Agent {
return mergeAgent(Agent{Type: OpenCodeACPRegistryID, Command: "opencode", Args: []string{"acp"}}, overrides)
}
func CreatePiAgent(overrides Agent) Agent {
return mergeAgent(Agent{Type: PiACPRegistryID, Command: "pi", Args: []string{"acp"}}, overrides)
}
func mergeAgent(base Agent, overrides Agent) Agent {
if overrides.Type != "" {
base.Type = overrides.Type
}
if overrides.Command != "" {
base.Command = overrides.Command
}
if overrides.Args != nil {
base.Args = overrides.Args
}
if overrides.Env != nil {
base.Env = overrides.Env
}
// ExtraArgs from both base and overrides are appended to the final Args,
// after any Args override. This lets callers add CLI flags (e.g.
// --disallowedTools) without clobbering the agent's launch preamble.
// Order: base.ExtraArgs first, then overrides.ExtraArgs.
extra := append([]string(nil), base.ExtraArgs...)
extra = append(extra, overrides.ExtraArgs...)
if len(extra) > 0 {
base.Args = append(append([]string(nil), base.Args...), extra...)
}
base.ExtraArgs = nil
return base
}
// CreateClaudeCodeOptions builds the _meta.claudeCode.options object for the
// Claude Code ACP agent from a typed ClaudeCodeOptions value. Pass the returned
// map as StartSessionOptions.Meta to apply the configuration at session
// creation. Only non-empty/nil fields are emitted, so an empty ClaudeCodeOptions
// produces {"claudeCode":{"options":{}}}.
//
// Example — disable web tools:
//
// meta := acp.CreateClaudeCodeOptions(acp.ClaudeCodeOptions{
// DisallowedTools: []string{"WebFetch", "WebSearch"},
// })
// runtime.StartSession(ctx, acp.StartSessionOptions{Agent: agent, Meta: meta})
//
// Example — strip all built-in tools (MCP-only):
//
// meta := acp.CreateClaudeCodeOptions(acp.ClaudeCodeOptions{Tools: []string{}})
func CreateClaudeCodeOptions(opts ClaudeCodeOptions) map[string]any {
options := map[string]any{}
// Tools is emitted even when empty (len 0), because an empty array is the
// signal to disable all built-in tools. Only omit when nil (unset).
if opts.Tools != nil {
if len(opts.Tools) == 0 {
options["tools"] = []string{}
} else {
options["tools"] = opts.Tools
}
}
if len(opts.DisallowedTools) > 0 {
options["disallowedTools"] = opts.DisallowedTools
}
if len(opts.AllowedTools) > 0 {
options["allowedTools"] = opts.AllowedTools
}
if opts.Settings != nil {
options["settings"] = opts.Settings
}
return map[string]any{"claudeCode": map[string]any{"options": options}}
}
// CreateCodexConfig builds the CODEX_CONFIG env value (JSON) from a typed
// CodexConfig. Returns a map suitable for Agent.Env. If agentEnv already has a
// CODEX_CONFIG, the values are deep-merged (new values win).
//
// Example:
//
// env, _ := acp.CreateCodexConfig(acp.CodexConfig{SandboxMode: "read-only"})
// agent := acp.CreateCodexAgent(acp.Agent{Env: env})
func CreateCodexConfig(opts CodexConfig) (map[string]string, error) {
return buildCodexEnv(opts, nil)
}
// buildCodexEnv constructs or merges the CODEX_CONFIG env map from CodexConfig
// options. existingEnv provides the base env (may contain a prior CODEX_CONFIG
// that gets deep-merged).
func buildCodexEnv(opts CodexConfig, existingEnv map[string]string) (map[string]string, error) {
env := map[string]string{}
for k, v := range existingEnv {
env[k] = v
}
config := map[string]any{}
// Parse existing CODEX_CONFIG if present, so we merge rather than clobber.
if existing := env["CODEX_CONFIG"]; existing != "" {
_ = json.Unmarshal([]byte(existing), &config)
}
if opts.Model != "" {
config["model"] = opts.Model
}
if opts.SandboxMode != "" {
config["sandbox_mode"] = opts.SandboxMode
}
if opts.ApprovalPolicy != "" {
config["approval_policy"] = opts.ApprovalPolicy
}
if len(opts.WritableRoots) > 0 {
config["writable_roots"] = opts.WritableRoots
}
if opts.NetworkAccess != nil {
config["sandbox_workspace_write"] = map[string]any{"network_access": *opts.NetworkAccess}
}
for k, v := range opts.Extra {
config[k] = v
}
data, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("marshal CODEX_CONFIG: %w", err)
}
env["CODEX_CONFIG"] = string(data)
return env, nil
}
// WriteOpenCodeConfig writes an opencode.json file to the given directory. Because
// OpenCode does not read _meta or env for permission/tool configuration, the
// file is the only reliable way to restrict tools. Call this before StartSession
// with the same CWD.
//
// Example:
//
// acp.WriteOpenCodeConfig(cwd, acp.OpenCodeConfig{
// Permission: acp.OpenCodePermission{Deny: []string{"bash"}},
// })
// session, _ := runtime.StartSession(ctx, acp.StartSessionOptions{Agent: agent, CWD: cwd})
func WriteOpenCodeConfig(cwd string, opts OpenCodeConfig) error {
config := map[string]any{}
// Preserve existing opencode.json if present.
existingPath := filepath.Join(cwd, "opencode.json")
if data, err := os.ReadFile(existingPath); err == nil {
_ = json.Unmarshal(data, &config)
}
if opts.Model != "" {
config["model"] = opts.Model
}
if opts.Provider != "" {
config["provider"] = opts.Provider
}
if len(opts.Permission.Allow) > 0 || len(opts.Permission.Deny) > 0 || len(opts.Permission.Ask) > 0 {
perm := map[string]any{}
if len(opts.Permission.Allow) > 0 {
perm["allow"] = opts.Permission.Allow
}
if len(opts.Permission.Deny) > 0 {
perm["deny"] = opts.Permission.Deny
}
if len(opts.Permission.Ask) > 0 {
perm["ask"] = opts.Permission.Ask
}
config["permission"] = perm
}
for k, v := range opts.Extra {
config[k] = v
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshal opencode.json: %w", err)
}
data = append(data, '\n')
return os.WriteFile(existingPath, data, 0o644)
}