Files
synapbus/image-build
Algis DumbrisandClaude Opus 4.6 560d9d4125 feat(harness): docker isolation backend + canonical synapbus-agent image
New `internal/harness/docker` package: per-run ephemeral container
backend that runs each agent in `docker run --rm`, bind-mounts the
materialized workdir at /workspace, and captures stdout/stderr/exit
code/result.json the same way the subprocess backend does.

Inspired by scion's pkg/runtime/docker.go: shell out to the docker
CLI (zero new Go deps, zero CGO), per-task ephemeral containers with
no warm pool, host-side scratch dir bind-mounted in.

Default security posture (overridable per agent):
  --rm
  --cap-drop=ALL
  --security-opt=no-new-privileges
  --read-only with tmpfs /tmp
  --pids-limit=512
  --user=<host uid:gid>
  --network=bridge (configurable; --network=none for air-gap)
  --memory / --cpus from agent config
  --add-host host.docker.internal:host-gateway on Linux

The backend reuses subprocess.AgentConfig for gemini_md/claude_md/
mcp_servers/skills materialization so existing example configs work
unchanged. Per-agent docker tunables go under a new `docker` block
in harness_config_json: image, memory, cpus, network, extra_mounts,
cap_add, read_only_root, user, entrypoint, command, extra_args.

MCP host rewrite: `.gemini/settings.json` URLs of the form
http://127.0.0.1:<port>/mcp are rewritten to
http://host.docker.internal:<port>/mcp at materialization time so the
in-container Gemini CLI can reach the SynapBus MCP server on the host
without code changes in the example wrappers.

Wired into the reactor and Registry resolver:
- Registry.Resolve picks "docker" when harness_config_json contains a
  `"docker"` block, taking precedence over local_command so explicit
  isolation never silently downgrades.
- reactor.agentBackendKind() returns backendDocker for the same case.
- evaluateTrigger's harness-backend gate accepts backendDocker
  alongside subprocess + webhook.
- main.go registers docker.Harness with the harness registry, passing
  the SynapBus listen port so the URL rewrite uses the correct host
  port.

Smoke tests in docker_test.go (skipped when no docker daemon):
- TestExecute_Hello: env injection + bind-mount writeback + message.json
  + result.json + stdout capture using alpine:3.20
- TestExecute_NoImage: rejects agents missing docker.image
- TestExecute_TimeoutCancel: wall-clock budget kills the container

New canonical agent image at image-build/synapbus-agent/:
- Debian bookworm-slim base
- Node 22 + @google/gemini-cli + @anthropic-ai/claude-code
- jq, sqlite3, curl, git, python3, tini (PID 1 for signal forwarding)
- Non-root agent user uid/gid 1000
- ENTRYPOINT tini, CMD /workspace/wrapper.sh

No SynapBus binary inside the image — agents reach the host MCP server
over the network at host.docker.internal:<port>.

Pre-existing reactor test failures (TestReactorNoK8sImage,
TestReactorDepthExceeded, TestReactorBudgetExhausted,
TestReactorCooldownSkipped, TestReactorSequentialExecution) verified
to exist on f319290 unchanged — not introduced by this commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:59:37 +03:00
..

SynapBus container images

The docker harness backend (internal/harness/docker/) runs each agent inside an ephemeral container. This directory holds the canonical agent image SynapBus's bundled examples reference.

synapbus-agent

The default image. Debian bookworm-slim base with:

  • gemini CLI (@google/gemini-cli)
  • claude CLI (@anthropic-ai/claude-code)
  • tini as PID 1 (signal forwarding + zombie reaping)
  • Standard tooling the example wrappers use: jq, sqlite3, curl, git, python3
  • Non-root agent user (uid 1000, gid 1000) matching the typical host user

No SynapBus binary lives in the image. Agents reach the SynapBus MCP server on the host at host.docker.internal:<port> — the harness rewrites .gemini/settings.json URLs from 127.0.0.1 to the gateway hostname automatically.

Build

Local single-arch:

docker build -t synapbus-agent:latest image-build/synapbus-agent

Multi-arch via buildx (recommended for sharing the image):

docker buildx build \
    --platform linux/amd64,linux/arm64 \
    -t synapbus-agent:latest \
    --load \
    image-build/synapbus-agent

Pin specific CLI versions with build args:

docker build \
    --build-arg GEMINI_CLI_VERSION=0.37.1 \
    --build-arg CLAUDE_CODE_VERSION=1.0.0 \
    -t synapbus-agent:0.37.1 \
    image-build/synapbus-agent

Wire an agent to use it

In harness_config_json add a docker block:

{
  "gemini_md": "...",
  "mcp_servers": [...],
  "env": {...},
  "docker": {
    "image": "synapbus-agent:latest",
    "memory": "1g",
    "cpus": "1.0",
    "network": "bridge"
  }
}

The reactor will pick the docker backend automatically when it sees the docker.image field. Default security posture: --cap-drop=ALL, --security-opt=no-new-privileges, --read-only root with tmpfs /tmp, --pids-limit=512, --user=<host uid:gid>. Override via the typed fields in the docker block (memory, cpus, cap_add, extra_mounts, read_only_root, user).