diff options
| author | Claude <noreply@anthropic.com> | 2026-05-25 19:02:19 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-25 19:02:19 +0000 |
| commit | 65cd7ea65d9c6fe0fad39bb2c5cac70d61153444 (patch) | |
| tree | 693dc265879bc39872e490989ece62ef07aef9eb /internal/executor/container.go | |
| parent | c0fabf5a2f4ca371403571b82e29c5073bed24fb (diff) | |
feat(executor): wire the agent MCP back-channel for gemini containers (Phase 4)
ContainerRunner previously skipped the MCP back-channel for gemini agents, so
they ran tool-less. It now mints a per-task token for gemini too and registers
the agent MCP server in the gemini CLI's user settings
(agentHome/.gemini/settings.json → $HOME/.gemini in-container) using the
gemini-cli mcpServers/httpUrl schema with a bearer header. With MCP enabled,
gemini also receives the planning preamble that points at the ask_user/
report_summary/spawn_subtask/record_progress tools.
Config generation is unit-tested (TestWriteGeminiMCPSettings) and the write is
in the run setup path. CAVEAT: whether the gemini CLI actually invokes these
tools in non-interactive (-p) mode — and how it handles tool auto-approval —
is NOT verified against a live gemini binary in this environment; this lands
the plumbing for a follow-up spike. No regression risk for gemini runs: a
config issue degrades to the prior tool-less behavior.
https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
Diffstat (limited to 'internal/executor/container.go')
| -rw-r--r-- | internal/executor/container.go | 42 |
1 files changed, 39 insertions, 3 deletions
diff --git a/internal/executor/container.go b/internal/executor/container.go index 78c3ed7..4f8fa06 100644 --- a/internal/executor/container.go +++ b/internal/executor/container.go @@ -306,16 +306,22 @@ func (r *ContainerRunner) runContainer(ctx context.Context, t *task.Task, e *sto } // Per-task MCP back-channel: mint a scoped token bound to this run's channel - // and write an mcp-config pointing the agent at the host agent MCP server. + // and point the agent at the host agent MCP server. Claude reads an + // --mcp-config file; the gemini CLI auto-discovers servers from its + // ~/.gemini/settings.json (agentHome is the container's $HOME). mcpEnabled := false - if r.Registry != nil && t.Agent.Type != "gemini" { + if r.Registry != nil { token, mintErr := r.Registry.Mint(ch) if mintErr != nil { return fmt.Errorf("minting agent mcp token: %w", mintErr) } defer r.Registry.Revoke(token) mcpURL := strings.TrimRight(strings.ReplaceAll(r.APIURL, "localhost", "host.docker.internal"), "/") + "/mcp" - if err := writeMCPConfig(workspace, mcpURL, token); err != nil { + if t.Agent.Type == "gemini" { + if err := writeGeminiMCPSettings(agentHome, mcpURL, token); err != nil { + return fmt.Errorf("writing gemini mcp settings: %w", err) + } + } else if err := writeMCPConfig(workspace, mcpURL, token); err != nil { return fmt.Errorf("writing mcp config: %w", err) } mcpEnabled = true @@ -534,6 +540,36 @@ func writeMCPConfig(workspace, mcpURL, token string) error { return os.WriteFile(filepath.Join(workspace, ".claudomator-mcp.json"), data, 0600) } +// writeGeminiMCPSettings registers the per-task agent MCP server in the gemini +// CLI's user settings (agentHome/.gemini/settings.json, which is $HOME/.gemini +// in-container). The gemini CLI discovers MCP servers from this file rather than +// a command-line flag; httpUrl selects the streamable-HTTP transport and headers +// carry the bearer token. +// +// NOTE: the on-disk schema follows the gemini-cli mcpServers format, but whether +// the CLI actually invokes these tools in non-interactive (-p) mode has not been +// verified against a live gemini binary — confirm with a spike before relying on +// gemini tool-use in production. +func writeGeminiMCPSettings(agentHome, mcpURL, token string) error { + cfg := map[string]any{ + "mcpServers": map[string]any{ + "claudomator": map[string]any{ + "httpUrl": mcpURL, + "headers": map[string]string{"Authorization": "Bearer " + token}, + }, + }, + } + data, err := json.Marshal(cfg) + if err != nil { + return err + } + dir := filepath.Join(agentHome, ".gemini") + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "settings.json"), data, 0600) +} + func (r *ContainerRunner) buildInnerCmd(t *task.Task, e *storage.Execution, isResume, mcpEnabled bool) []string { // Claude CLI uses -p for prompt text. To pass a file, we use a shell to cat it. // We use a shell variable to capture the expansion to avoid quoting issues with instructions contents. |
