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/agentloop/tools.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'internal/agentloop') diff --git a/internal/agentloop/tools.go b/internal/agentloop/tools.go index e92a12f..fcb17a2 100644 --- a/internal/agentloop/tools.go +++ b/internal/agentloop/tools.go @@ -8,6 +8,7 @@ import ( "github.com/thepeterstone/claudomator/internal/agentchannel" "github.com/thepeterstone/claudomator/internal/provider" + "github.com/thepeterstone/claudomator/internal/sandbox" ) // agentToolSpecs returns the eight tools available to the loop: the four @@ -193,6 +194,15 @@ func (l *Loop) dispatchTool(ctx context.Context, name, argsJSON string) (result } _ = json.Unmarshal([]byte(argsJSON), &a) if writeErr := l.Sandbox.WriteFile(ctx, a.Path, a.Content); writeErr != nil { + // A guardrail rejection (sandbox.Guarded wrapping the real + // Sandbox) is not a fatal execution error: feed the reason back + // to the model as ordinary tool content so it can self-correct, + // instead of aborting the whole run the way a genuine sandbox + // failure (e.g. no working directory) does. + var rej *sandbox.RejectionError + if errors.As(writeErr, &rej) { + return rej.Error(), false, nil + } return "", false, writeErr } return "Written.", false, nil @@ -204,6 +214,10 @@ func (l *Loop) dispatchTool(ctx context.Context, name, argsJSON string) (result _ = json.Unmarshal([]byte(argsJSON), &a) out, errOut, exitCode, runErr := l.Sandbox.RunBash(ctx, a.Command) if runErr != nil { + var rej *sandbox.RejectionError + if errors.As(runErr, &rej) { + return rej.Error(), false, nil + } return "", false, runErr } return fmt.Sprintf("exit_code: %d\nstdout:\n%s\nstderr:\n%s", exitCode, out, errOut), false, nil -- cgit v1.2.3