feat: add docker_enabled option for agents to enable Docker-in-Docker… - #417
Conversation
… sidecar - Introduced a new boolean field `docker_enabled` in the Agent model to allow agents to opt-in for a Docker-in-Docker sidecar during conversations. - Updated the frontend to include a toggle for Docker access in the agent detail view. - Enhanced localization files to support new labels and hints for Docker access. - Modified the agent API and database schema to accommodate the new field. - Implemented necessary changes in the agent runner service to manage Docker sidecar lifecycle based on the new configuration. - Added migration script to safely add the `docker_enabled` column to the agents table.
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
Reviewed changes — a clean, well-documented feature thread. docker_enabled is correctly wired through the full stack, and I specifically verified the two things that could have been subtle bugs:
- Sandbox concurrency refactor (
sandbox.go): movingwaitForDindReadyinto a cancellable goroutine is sound — no goroutine leak (buffered chan, 90s internal deadline,dindCtxcancelled on every return path viadefer cancelDindWait()), no nil-dind panic on teardown (Stopguards withif h.dind != nil), and no double-cleanup on the not-ready error path (cleanup()removes only the sandbox container; the deferredstopDindSidecarremoves the sidecar container + network). - ACP handling: the UI toggle is correctly inside the
!isAcpbranch, matching the backend's LLM-only semantics. - Migration
000041is additive withDEFAULT FALSE, consistent with repo convention.
Inline comment summary
The only actionable point is a coverage gap on the disabled path (see inline comment) and a cross-service deploy-ordering note below.
ℹ️ Cross-service migration ordering
agent-runner's FindByID now SELECTs docker_enabled, so a cold start fails — every execution, not just Docker ones — if the new agent-runner binary is live before migration 000041 is applied to the shared database. Since api and agent-runner are separate deployments sharing one schema, the migration must land before the runner rolls out.
Technical details
# Deploy ordering: agent-runner reads docker_enabled
## Affected sites
- services/agent-runner/internal/repository/postgres/agent_repository.go:85 — SELECT now references `agents.docker_enabled`
## Required outcome
- Ensure migration 000041 is applied (api-side migrate step) before the new agent-runner binary serves traffic, or cold starts fail on a missing-column error.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…isabled This commit introduces a new end-to-end test that verifies the behavior of the sandbox when Docker is disabled. The test checks that no dind sidecar container or associated network is created for a conversation where Docker is not enabled. It also ensures that the sandbox container does not have the DOCKER_HOST environment variable set, confirming that the system behaves correctly in this scenario.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the new commit e615afff adds TestSandboxDindSidecarNotStartedWhenDisabled, the negative-path e2e test that closes the coverage gap this PR's previous review flagged:
- Added
TestSandboxDindSidecarNotStartedWhenDisabled: starts aDockerEnabled: false(default) conversation and asserts thepaca-dind-<conv>sidecar container andpaca-sbx-net-<conv>private network do not exist, and that the sandbox container's env carries noDOCKER_HOST. Gated consistently with its siblings (testing.Short+PACA_E2E=1+checkDockerAvailable), reusesnewSandboxManager/rawUpstreamGooseImage, and cleans up viamgr.Stop. - Verified the black-box container/network names match
dindContainerName/conversationNetworkNameindind.go(the replication is intentional — the test is inpackage e2e_testand can't reach the unexported helpers). - Brought in
github.com/containerd/errdefs(direct) plus indirect dep bumps viago mod tidy;go vet ./test/e2e/...andgo build ./...are clean.
All three assertions requested in the prior review thread (no container, no network, no DOCKER_HOST) are covered, and no new issues surfaced in the delta.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Summary
New conversations were slow to start. Digging into
sandbox.Manager.Start, every cold start paid for two containers back to back and unconditionally:docker:dindsidecar (network create + container create/start + polldocker infountil dockerd answers) — run to full completion...And that sidecar ran for every conversation, whether or not the agent ever executes a Docker command.
This PR fixes both:
startDindSidecarnow only creates the network and starts the container; thedocker inforeadiness wait runs in a goroutine alongside the sandbox container's create/start/health-poll, joined right beforeStartreturns aHandle. Cuts cold-start latency fromdind_boot + sandbox_bootto roughlymax(dind_boot, sandbox_boot).docker_enabledcolumn onagents(defaultfalse). Agents that don't set it skip the sidecar, the private network, and theDOCKER_HOSTenv var entirely — no privileged container spun up for conversations that never touch Docker.Details
services/api/migrations/000041_add_agent_docker_enabled.sql—docker_enabled BOOLEAN NOT NULL DEFAULT FALSE.services/api: threadedDockerEnabledthrough the full agent CRUD path — domain entity, repo (all 3 INSERTs, the UPDATE, and both row↔entity mappers), service (CreateAgent/UpdateAgent/CreateGlobalAgent/UpdateGlobalAgent), DTOs, and handlers. LLM-only, same asSystemPrompt/GitCommitterName— ACP agents don't run through agent-runner's sandbox at all.services/agent-runner:agent.Config, the repo'sagentRecord/SELECT, andexecutor.coldStartcarryDockerEnableddown tosandbox.Config.sandbox.Manager.StartskipsstartDindSidecar, theNetworkConnectto its network, and theDOCKER_HOSTenv line whenDockerEnabledis false.context.WithCancel(ctx)that's cancelled on every return path (not just the happy one) — otherwise a sandbox-container failure after the goroutine starts would leave it pollingdocker infoagainst a container about to be force-removed for up todindReadyTimeout(90s), with nothing left reading the result.test/e2e/dind_test.go's two Docker-access tests to passDockerEnabled: trueexplicitly, since the sidecar they exercise is no longer on by default.docker_enabledadded to theAgenttype and all four create/update payload shapes inagent-api.ts; a "Docker access" toggle in the agent Overview tab (next to git committer identity, LLM-type agents only); i18n strings added to all 9 locales.Deliberately not included: pre-pulling the
docker:dindimage at install/upgrade time. Since the setting defaults off, most installs will never start that container at all, so warming it unconditionally would undercut the point of making it opt-in.Test plan
go build ./... && go vet ./... && go test ./internal/...— clean on bothservices/apiandservices/agent-runnernpx tsc -b --noEmitandnpx biome check— clean onapps/webtest/e2e/dind_test.go'sTestSandboxRunsAsRootWithDockerAccess/TestSandboxDindSidecarsAreIsolatedPerConversation(needsPACA_E2E=1+ real Docker — not run in this environment)dockercommands work inside the sandbox; confirm it's off by default for existing/new agents