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>
Two causes made a shared shell's PTY stream lag for non-host viewers and
then arrive "all at once":
1. Nagle's algorithm — PTY echo/keystrokes/chat are tiny frames; Nagle +
delayed-ACK (amplified by Tailscale RTT) coalesced them into bursts.
Set TCP_NODELAY on the client websocket (Plain/--no-tls path) and on
each server connection's socket (via ws.io_proto.transport).
2. Head-of-line blocking — ConnectionManager.broadcast awaited each
send() serially under a global lock, so the slowest viewer gated
delivery to everyone AND stalled the server's read of the broker's
next frame, backing the stream up. Give each connection its own
bounded outbound queue + writer task; broadcast now only enqueues and
never waits on a socket. A peer that overflows its backlog (4096) is
evicted (close 1013) and resyncs on reconnect rather than buffering
without bound or being fed a gapped terminal stream. Init snapshot is
enqueued as the guaranteed-first frame (send_state -> state_frame).
Adds tests/test_manager.py (FIFO, slow-client isolation, wedged
eviction, reconnect replacement). Also fmt-clean sbx.rs/theme.rs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add /pw (alias /password): reveal this room's password locally (never
broadcast); surfaced in the F1 help overlay and the join hint
- direnv-autostart/: cd-to-launch a single real-user session via direnv;
password is minted in memory at launch (never written to disk, matching the
RAM-only model) and scoped to the child process. setup.sh installs direnv,
hooks bash/zsh, and `direnv allow`s the dir
- lets-hack.sh: boot a FRESH server by default (replacing any live one) with a
--reuse opt-out; add -h/--help/-help; guard against killing the tmux session
you're attached to; switch-client into the coven when run inside tmux
- rename coven→clergy across rust/python/scripts; tests/test_coven.py→test_clergy.py
- snapshots in-progress hack-house client work (sandbox, themes, net, ui)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CRITICAL fixes:
- Auto-generated self-signed TLS certs (HTTPS/WSS by default)
- Removed session_key from /srp/verify response (was sent in plaintext)
- Replaced with HMAC-SHA256 ws_token for WebSocket authentication
HIGH fixes:
- WebSocket auth now validates ws_token via hmac.compare_digest()
- /clear endpoint requires Bearer admin_token (printed at server start)
- Password no longer required as CLI arg — supports env var + getpass prompt
- Removed user_ip from Message model (no longer broadcast to clients)
MEDIUM fixes:
- Rate limiter on /srp/init and /srp/verify (10 req/min/IP)
- MessageStore capped at 1000 messages (prevents RAM DoS)
- access_log disabled (was leaking request metadata)
LOW fixes:
- Username sanitization against rich markup injection
- Dead code removed from helpers.py
All 79 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>