Files
synapbus/image-build
Algis DumbrisandClaude Opus 4.6 f1e2b1fa38 feat(doc-gardener): MCP-native + docker-isolated multi-agent demo
Replace the legacy cmd/docgardener orchestration (~2400 LOC of Go
spawning subprocess workers via local_command + admin socket) with
three Docker-isolated agents that all reach SynapBus through MCP:

  doc-coordinator   — Gemini Pro, triages goal, calls create_goal +
                      propose_task_tree + send_message via MCP
  docs-inspector    — Gemini Flash, fetches docs, installs mcpproxy,
                      shells out to verify, reports findings via MCP
  docs-critic       — Gemini Flash, independent reviewer with its
                      own MCP API key + config_hash, audits the
                      inspector's evidence and DMs the owner

Every agent runs inside synapbus-agent:latest with --cap-drop=ALL,
--security-opt=no-new-privileges, --read-only root + tmpfs /tmp,
--pids-limit, memory + CPU quotas. The container reaches the
SynapBus MCP server on the host at host.docker.internal:18089
because the docker harness rewrites .gemini/settings.json URLs
from 127.0.0.1 automatically.

Wrapper baked into the image at /usr/local/bin/synapbus-agent-wrapper.sh
so configs don't need to mount or template a per-example wrapper.
The harness's default no longer overrides docker CMD — the image's
baked entry script is used unless docker.command is set explicitly.

start.sh changes:
  - Preflight: docker daemon, GEMINI_API_KEY (or ~/.gemini/oauth_creds.json)
  - Builds synapbus-agent image lazily on first run
  - Mints one MCP API key per agent via `agent revoke-key`
  - Templates each config with __PORT__, __*_APIKEY__, __MODEL__,
    __GEMINI_API_KEY__, __EXTRA_MOUNTS__
  - With OAuth fallback: copies host ~/.gemini → data/agent-home/.gemini
    once and bind-mounts the whole agent-home rw at /home/agent so
    in-container gemini has a writable HOME without polluting the host
  - SYNAPBUS_KEEP_WORKDIR=1 preserves per-run docker workdirs for
    debugging
  - Sets harness_name=docker explicitly so the resolver picks the
    right backend even with empty local_command

stop.sh: best-effort cleanup of lingering synapbus-* containers so a
killed parent doesn't leave bind-mount holders that block the next
start.sh from re-mounting the same paths.

run_task.sh: snapshot-baseline pattern (only watches replies newer
than the max msg id at send time), 600s deadline, treats any reply
from doc-coordinator that isn't DELEGATED:/REVISING: as terminal,
plus FINAL:/CANNOT: from any sender.

cmd/docgardener slimmed from 7 files / 2580 LOC to 3 files / ~370 LOC.
The remaining binary only renders the HTML report (queries goals +
goal_tasks + traces + harness_runs from the SynapBus DB read-only).
agent.go, channels.go, flow.go, gemini_tree.go all deleted.

Verified end-to-end against gemini-2.5-pro coordinator + gemini-2.5-flash
workers (with OAuth fallback mount):

  ./run_task.sh "what does this demo do?"
    → coordinator TRIVIAL: replies directly via MCP send_message

  ./run_task.sh "Verify the CLI commands on docs.mcpproxy.app/cli/command-reference"
    → coordinator calls create_goal (slug verify-mcpproxy-cli-...),
      propose_task_tree (3-node tree: coordinator/plan,
      doc-gardener/scan, doc-gardener/audit) and send_message to
      docs-inspector
    → inspector container runs ~10 minutes inside the sandbox:
      installs mcpproxy from real release URL (linux-arm64), curls
      the docs page, falls back from BeautifulSoup → grep when
      python3-venv is missing, debugs its own f-string syntax, writes
      extract_flags.py, runs `mcpproxy --help` for ground truth
    → real multi-agent iteration loop: critic REVISE: → inspector
      retry → critic REVISE: with new feedback

The agents discovered real environment quirks (tmpfs noexec on /tmp,
externally-managed Python, missing python3-venv) and worked around
them inside the sandbox without touching the host.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:28:05 +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).