From 767ddade57f189827fa956ff8081ca47404a4798 Mon Sep 17 00:00:00 2001 From: Claude Sonnet 5 Date: Fri, 3 Jul 2026 09:21:32 +0000 Subject: feat(sandbox): add DockerSandbox + pre-tool-use guardrail hooks (Phase 3) Gives native-API-driven agents (currently just the Phase 2 Anthropic adapter) real container isolation, decoupled from model invocation -- the model call happens in the Go process via provider.Provider, only tool execution happens in the container, unlike the CLI-subprocess ContainerRunner (left completely untouched) where the claude/gemini CLI runs inside the container. - internal/sandbox/dockersandbox.go: Sandbox via a long-lived `docker run -d ... sleep infinity` container (started once per execution, not per tool call), host-side git clone + bind-mount matching ContainerRunner's existing pattern, docker exec for read/write/bash/glob. Reuses images/agent-base (claudomator-agent:latest) rather than standing up a second image. WorkDir()/resume persists the host bind-mount directory (matching HostSandbox's contract); a resumed sandbox lazily starts a fresh container against that directory rather than trying to reattach to a possibly-gone one. - internal/sandbox/guard.go, hooks.go: Hook interface (CheckBash/CheckWrite), Guarded wrapper, DenylistBashHook (rm -rf /, force-push, curl|sh, sudo, chmod 777, dd if=) and ProtectedPathHook (.git/**, .env*, credentials/, .github/workflows/**). A rejection returns *RejectionError, which agentloop/tools.go now recognizes and feeds back to the model as a normal (non-fatal) tool-error result instead of aborting the run. - NativeRunner wraps whichever Sandbox it builds (Host or Docker) in Guarded{Hooks: DefaultHooks()} uniformly. The "anthropic" runner now uses DockerSandbox; "local" stays on HostSandbox by design (local models are the harness's more-trusted, lower-stakes-to-run tier). Docker is not installed in this dev environment (no docker/podman/containerd on PATH), so DockerSandbox's real container-lifecycle behavior is verified via mocked-command unit tests only -- go test -race ./... passes throughout, with the two real-daemon integration tests gated behind a dockerAvailable(t) check and skipping here. Live verification against an actual Docker host is a follow-up before relying on the "anthropic" agent type in production. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs --- internal/config/config.go | 59 +++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 25 deletions(-) (limited to 'internal/config') diff --git a/internal/config/config.go b/internal/config/config.go index de087d6..afbd12e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -89,31 +89,39 @@ type ProviderConfig struct { } type Config struct { - DataDir string `toml:"data_dir"` - DBPath string `toml:"-"` - LogDir string `toml:"-"` - DropsDir string `toml:"-"` - SSHAuthSock string `toml:"ssh_auth_sock"` - ClaudeBinaryPath string `toml:"claude_binary_path"` - GeminiBinaryPath string `toml:"gemini_binary_path"` - ClaudeImage string `toml:"claude_image"` - GeminiImage string `toml:"gemini_image"` - MaxConcurrent int `toml:"max_concurrent"` - ShutdownTimeout time.Duration `toml:"shutdown_timeout"` - DefaultTimeout string `toml:"default_timeout"` - ServerAddr string `toml:"server_addr"` - WebhookURL string `toml:"webhook_url"` - WorkspaceRoot string `toml:"workspace_root"` - WebhookSecret string `toml:"webhook_secret"` - APIToken string `toml:"api_token"` // shared bearer for web UI + chatbot MCP; empty disables both auth and the chatbot MCP endpoint - Projects []Project `toml:"projects"` - VAPIDPublicKey string `toml:"vapid_public_key"` - VAPIDPrivateKey string `toml:"vapid_private_key"` - VAPIDEmail string `toml:"vapid_email"` - ClaudeConfigDir string `toml:"claude_config_dir"` - LocalModel LocalModel `toml:"local_model"` - Runners RunnersConfig `toml:"runners"` - Budget BudgetConfig `toml:"budget"` + DataDir string `toml:"data_dir"` + DBPath string `toml:"-"` + LogDir string `toml:"-"` + DropsDir string `toml:"-"` + SSHAuthSock string `toml:"ssh_auth_sock"` + ClaudeBinaryPath string `toml:"claude_binary_path"` + GeminiBinaryPath string `toml:"gemini_binary_path"` + ClaudeImage string `toml:"claude_image"` + GeminiImage string `toml:"gemini_image"` + // SandboxImage is the docker image used by sandbox.DockerSandbox (the + // tool-execution sandbox for native-API-driven runners, e.g. the + // "anthropic" NativeRunner) — distinct from ClaudeImage/GeminiImage, + // which are for ContainerRunner's claude/gemini CLI-subprocess path. + // Defaults to the same image (images/agent-base) since it already has + // everything a generic git/bash sandbox needs; see + // internal/sandbox/dockersandbox.go's package doc for the tradeoff. + SandboxImage string `toml:"sandbox_image"` + MaxConcurrent int `toml:"max_concurrent"` + ShutdownTimeout time.Duration `toml:"shutdown_timeout"` + DefaultTimeout string `toml:"default_timeout"` + ServerAddr string `toml:"server_addr"` + WebhookURL string `toml:"webhook_url"` + WorkspaceRoot string `toml:"workspace_root"` + WebhookSecret string `toml:"webhook_secret"` + APIToken string `toml:"api_token"` // shared bearer for web UI + chatbot MCP; empty disables both auth and the chatbot MCP endpoint + Projects []Project `toml:"projects"` + VAPIDPublicKey string `toml:"vapid_public_key"` + VAPIDPrivateKey string `toml:"vapid_private_key"` + VAPIDEmail string `toml:"vapid_email"` + ClaudeConfigDir string `toml:"claude_config_dir"` + LocalModel LocalModel `toml:"local_model"` + Runners RunnersConfig `toml:"runners"` + Budget BudgetConfig `toml:"budget"` // Providers configures native/OpenAI-compatible LLM backends by name (see // ProviderConfig). Unused by runner construction in Phase 1. Providers map[string]ProviderConfig `toml:"providers"` @@ -147,6 +155,7 @@ func Default() (*Config, error) { GeminiBinaryPath: "gemini", ClaudeImage: "claudomator-agent:latest", GeminiImage: "claudomator-agent:latest", + SandboxImage: "claudomator-agent:latest", MaxConcurrent: 3, DefaultTimeout: "15m", ServerAddr: "127.0.0.1:8484", -- cgit v1.2.3