Phase A: host-global VM registry (~/.hh/registry.json) joining opaque
snapshots to their .hh-agent manifests. New src/registry.rs (serde_json),
one Entry per saved VM with cached purpose/status/todo scraped from the
live container at save time. Reconcile-on-read self-evicts pruned images.
/sbx browse TUI lister + operator-side `registry list|show` reader.
Phase B: trading/skill VMs. Entry gains shareable/tags/share_path +
publish/get/list_shareable helpers. New TUI verbs /sbx publish <label>
[tag...], /sbx catalog @user, /sbx pull @user <label>. Wire protocol adds
_sbx:catreq/catalog/pullreq frames (parse_sbx). Receiver auto-loads a
received hh-snap-*.tar, reads the in-image manifest, and self-registers
(kept shareable for re-trade). Reuses the existing E2E /send + ft.rs
streaming transport.
podman gotcha fixed: `podman load` prints registry-qualified
`localhost/hh-snap:<label>` vs docker's bare tag — parse_loaded_tag now
finds the hh-snap: marker anywhere in the line (unit-tested both shapes).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ICM-aligned agent-manifest tooling so a hack-house sandbox/VM can be
shared, traded, and resumed without a human briefing. manifest.py is a
dependency-light (stdlib + optional PyYAML) library + CLI that writes a
`.hh-agent/` bundle: manifest.yaml (canonical machine record) plus rendered
AGENT.md / last-state.md / summary.md / goals.yaml views.
Wire a `manifest` op into the operator bridge (push/pull/update) that moves the
bundle in and out of the *target sandbox* via the same exec path as write/get,
so the VM itself carries its purpose, goals, user intent, live state, and a
provenance chain — the unit of agent-to-agent work transmission. Pairs with
`spawn`: stamp → push → hand a child "load the .hh-agent manifest and continue."
Proven end-to-end against a real Kali podman container (push → on-disk verify →
update → pull-back) and covered by 6 new offline tests (34 green total).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Completes the remote-drive triad for a broker-owned sandbox the operator can't
exec into directly: keys (in) → watch (wait) → screen (out).
- _sbx:data PTY-relay frames are absorbed into a capped rolling terminal buffer
(not surfaced as chat); `screen` returns it ansi-stripped (or raw). sandbox.
strip_ansi handles CSI/OSC/CR noise so output greps cleanly.
- `watch` is a formal stop-condition engine: blocks until a regex matches in the
screen buffer or chat events, or an idle-quiescence window, or a hard timeout,
then reports which fired — the autonomy loop's principled wait.
- Skill doctrine updated with the type→wait→read relay loop.
28 offline tests green.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lets an operator stand up a nested Claude Code operator against another room —
a tree of operators — behind hard guardrails:
- Budget(depth, fanout, cost): depth/fanout are hard caps decremented on every
descend(); a leaf (depth 0) or a spent level (fanout 0) refuses to spawn,
stopping a runaway tree. Cost is carried down as a soft ceiling.
- detect_system / install_plan: decide install from what's present; prefer the
documented npm package `@anthropic-ai/claude-code` or a $HH_CLAUDE_INSTALL
override — never a guessed URL.
- Credentials are gated OFF by default: a child authenticates itself unless the
operator explicitly passes allow_creds, and only its own creds, only to a path
it was handed.
- compose_directive bakes the objective, stop conditions and inherited budget
into the child's `claude -p` prompt so it self-limits.
`spawn` op + CLI verb default to a dry-run plan (inspect the tree before growing
it); `--go` launches detached on the host. `up`/`serve` take --depth/--fanout/
--cost. 24 offline tests green; dry-run plan verified live through the socket.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-located mode: the operator launches its own podman/docker container and
execs into it out-of-band (argv-identical to the native harness), exposed as
`sbx launch|status|down`, `exec`, `write`, `get`. When granted, it can also
drive the room's broker-owned container directly on the same host.
Relay mode primitive: `keys` injects raw bytes into the shared PTY via the
same `_sbx:input` frame a human driver emits — full keyboard control incl. the
stop-vocabulary (ctrl-c/ctrl-d/ctrl-z/ctrl-\, esc, arrows, pager q). A compact
per-session cheat-sheet (`sandbox.KEYS_HELP`, `keys --help-keys`) documents
what each inject does and how to end a stuck program, token-efficiently.
Gating: exec/write/get refuse a host (`local`) inherited from the room; the
room sandbox is only driven when `granted`. Keystrokes are inert until granted.
17 offline tests green; e2e verified against real podman (launch→exec→write→
get round-trip→teardown) and through the CLI socket.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invert the agent model: a headless OperatorBridge(Client) owns the
websocket while a Claude Code session drives it via a unix control
socket. Reuses Client SRP/Fernet and AgentBridge's reconnect/serve
shape. Ships an hh-bridge CLI (up/read/say/roster/status/down) with a
seq'd in-RAM inbox + asyncio.Condition long-poll (read --wait) for
in-turn autonomy. ACL/sandbox-status frames are recorded for later
phases. Sandbox drive, delegation and nesting are out of scope here.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>