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>
48 lines
1.3 KiB
Bash
Executable File
48 lines
1.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# stop.sh — shut down the synapbus instance started by ./start.sh.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PID_FILE="$SCRIPT_DIR/.synapbus.pid"
|
|
|
|
say() { printf '\033[1;36m[stop]\033[0m %s\n' "$*"; }
|
|
|
|
if [ ! -f "$PID_FILE" ]; then
|
|
say "no pid file — nothing to stop"
|
|
exit 0
|
|
fi
|
|
PID=$(cat "$PID_FILE")
|
|
if ! kill -0 "$PID" 2>/dev/null; then
|
|
say "process $PID already gone"
|
|
rm -f "$PID_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
say "signaling synapbus (pid $PID)"
|
|
kill "$PID"
|
|
for i in $(seq 1 50); do
|
|
if ! kill -0 "$PID" 2>/dev/null; then break; fi
|
|
sleep 0.1
|
|
done
|
|
if kill -0 "$PID" 2>/dev/null; then
|
|
say "process did not exit gracefully — sending SIGKILL"
|
|
kill -9 "$PID" 2>/dev/null || true
|
|
fi
|
|
rm -f "$PID_FILE"
|
|
|
|
# Best-effort cleanup of any lingering agent containers. `--rm` should
|
|
# have removed them when the wrapper exited, but if SynapBus was killed
|
|
# mid-run those containers can outlive the parent and hold bind-mount
|
|
# references that prevent the next start.sh from re-mounting the same
|
|
# workdir paths.
|
|
if command -v docker >/dev/null 2>&1; then
|
|
STALE=$(docker ps -aq --filter "name=synapbus-" 2>/dev/null || true)
|
|
if [ -n "$STALE" ]; then
|
|
say "removing stale agent containers"
|
|
docker rm -f $STALE >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
say "stopped"
|