readline repaints its prompt in place with a lone \r (…└─# \r└─# cmd); the
browser turned that \r into a newline, stacking the pre-redraw prompt as a
phantom extra line (the mobile-only "extra --#"). Add a collapseCR() mini-vt
pass that resolves \r as a column-0 overwrite (ANSI escapes treated as
zero-width so colours survive; honours \x1b[K erase-to-EOL) before ansiToHtml,
so the real prompt stays and only the duplicate is dropped. TUI was already
correct via its real vt emulator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
termux-bootstrap.sh: one-shot on-device installer (pkg cryptography to avoid a
rust build, pure/wheeled pip deps, no srp C-ext, wake-lock, hh alias, Phase-0
preflight) — verified green on the Fairphone.
phone-capture.sh: auto-detect the adb device serial instead of assuming :5555,
since Wireless Debugging uses a dynamic port; target that serial for
screencap/screenrecord/pull. Clarified the pairing help to use the Tailscale IP.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User-facing how-to distinct from the design spec: the port-probe reachability
gate, one-time Termux setup + tar-over-ssh sync, the hh launcher grammar,
per-directory direnv auto-host, the mobile web console, phone-capture.sh routes
(non-root file/term/webshot vs ADB shot/screen), and the Fairphone skill
ship-list. Linked from termux-operator.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Laptop-side capture tool that SSHes into the phone. Non-root routes work from
the Termux app uid (file pull via tar-over-ssh, tmux capture-pane, headless
chromium webshot of the hh web console); shot/screen use ADB (Wireless
Debugging) since screencap/screenrecord can't reach SurfaceFlinger as an app
uid. --tg [target] ships any artifact to Telegram via video-toolkit tg-send.sh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a single `hh` launcher (host/join/web/status/stop + operator
passthrough) and a direnv snippet so `cd`-ing into a project dir
auto-hosts a room named after it. Verified live on the Fairphone:
two rooms coexist on port-scoped tmux windows (manual :8799 +
direnv :8801), status health-checks each via curl (ss/netstat are
unreliable on Android), stop-all tears down cleanly.
Termux notes baked in: no /usr/bin/env (invoke via the `hh` alias,
not the shebang); status enumerates operator daemons under
$TMPDIR/hh-bridge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Encode the robust way to reach + drive the phone in CLAUDE.md and the
hh-operator skill (which had no mobile section at all):
- One canonical hardened ssh prefix (ConnectTimeout/BatchMode/accept-new)
that never hangs and dodges the wrong-alias trap.
- Reachability gate probes the TCP port, NOT `tailscale status` — the
tailnet shows fairphone-6 "active" even when Android has killed
backgrounded Termux and sshd is dead (observed live).
- Fix-is-on-device guidance (termux-wake-lock) instead of retrying a dead
port; Termux env quirks (no /tmp, no rustc, skip srp).
- Status advanced: phone can both JOIN and HOST rooms (server-side SRP
shim) + the stdlib mobile web console mirrors a real room.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The srp package's C-extension has no aarch64/bionic wheel, so a phone in
Termux couldn't run the room server. Add the server side of SRP-6a
(create_salted_verification_key + Verifier) to the pure-Python shim and
fall srp_auth.py back to it on ImportError.
Proven live: a laptop (C-ext srp client) authenticated against a room
HOSTED ON THE PHONE (pure Verifier) over Tailscale — cross-implementation
handshake with matching session keys. Locked in with interop tests
covering real-client-vs-pure-Verifier, pure-vs-pure, and a wrong-password
negative control.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rebuild the web UI into a room mirror that looks/feels like the TUI:
tabbed Terminal + Chat, a live shared-sandbox terminal pane (polls
/api/screen, client-side ANSI/SGR render), keystroke drive with quick
keys + a command line (POST /api/keys), roster and a driver/watching
badge. Read-only until the room /grants this operator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `operator web` — a stdlib-only http.server shim over the control
socket so a phone browser can drive a room: message bubbles, live
long-poll updates, roster, and a real text box (GET /, /api/events,
/api/status, POST /api/say). Solves the Termux input gap where the
tmux `read --wait` loop is read-only. Binds 127.0.0.1 by default since
the socket grants room-send.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The phone is a live SSH host on the tailnet, so on-device Phase-0 work can be
driven directly — not "paste it back". Document the verified reachability
(fairphone-6 100.95.202.68:8022, user u0_a203, ~/.ssh/phone-deploy), the
wrong-alias trap (`phone` points at an offline device), Termux env facts, the
rust-free dep install, and the tar-over-ssh staging so future sessions check
reachability first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
scripts/termux-preflight.py runs the docs/termux-operator.md §0.1-0.5 checks on
the phone and prints a PASS/FAIL report: python>=3.11, requests/rich/websockets
import, cryptography+Fernet round-trip (version floor treated as soft), srp
C-ext OR the pure shim, that client/operator import without the server stack,
and an AF_UNIX bind under the $TMPDIR runtime root. Makes the one remaining
on-device Phase-0 step a single command. Exits non-zero on any hard failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
runtime_root() fell back to /tmp, which does not exist on Android/Termux, so the
daemon's control.sock would land in an unwritable path. Fall back
XDG_RUNTIME_DIR -> TMPDIR -> /tmp; Termux sets TMPDIR to $PREFIX/tmp. No change
on a normal box (TMPDIR is typically unset there). Enables the Phase 1
join/read/say socket to work on-device.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cmd_chat/__init__.py eagerly imported run_server (sanic/pydantic) and Client,
so any `cmd_chat.*` import — including `python -m cmd_chat.operator` — pulled
the server stack at package init. On a phone/Termux operator that installs only
the operator deps (and where srp may be unbuildable), this failed before the
client's pure-SRP fallback could help.
Make both imports lazy inside main()'s serve/connect branches. The operator
path now imports cleanly with srp/sanic/pydantic all absent (client falls back
to _srp_pure). Add a subprocess regression test to keep it that way.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Operator-only dependency subset (requests, rich, websockets, cryptography>=41,
srp) so the C-heavy cryptography>=46 server pin can be satisfied by Termux's
packaged build. Server requirements.txt is left unchanged. srp is best-effort:
the client falls back to the pure-Python shim when it can't be built.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SRP fails silently on any constant/hash mismatch, so gate the shim with a live
handshake: (1) pure User vs the real srp.Verifier, (2) a negative wrong-password
control, and (3) the full /srp/init -> /srp/verify HTTP flow against the real
server routes (backed by the real srp lib). All assert the phone operator
authenticates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap `import srp` so a missing C-extension (e.g. Termux/aarch64) transparently
loads _srp_pure as srp. No call-site changes — the shim mirrors the srp API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `srp` PyPI package is a C extension with no reliable aarch64 wheel and
often fails to build under Termux. Vendor _srp_pure.py, a stdlib-only port of
srp._pysrp with rfc5054_enable() active, exposing the exact client surface
client.py uses (rfc5054_enable, SHA256, User with start_authentication /
process_challenge / verify_session / authenticated).
Constants (N/g/k), x/u derivation, M1 and H_AMK match srp byte-for-byte —
verified by a live handshake of this pure User against the real srp Verifier
(the same backend the server runs).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phased plan (P0 dependency spike → P1 read-only presence → P2 sandbox drive →
P3 remote-provider autonomy → P4 persistence) for joining a room as a user from
Termux. Grounded in the real SRP/Fernet/ws client and the operate provider path.
Covers the srp aarch64 build risk + pure-Python SRP fallback, the cryptography
pin workaround, and Tailscale / reverse-SSH / port-forward reachability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rarity was inflating to 65% Legendary: the loop mass-produces ~70 near-clone
VMs that look near-identical on every axis, so no absolute PowerScore threshold
can create scarcity within that cluster. Fix (option C, hybrid):
- grade_curve() is now the primary rarity mechanism — a population quota
(Leg 8 / Epic 15 / Rare 25 / Uncommon 27 / Common 25%) ranked by PowerScore
with a deterministic label-seed tiebreak. Yields a clean 8/15/26/27/24%
pyramid. Rebuilds each card's rarity-dependent name + flavor via _apply_rarity.
- Absolute RARITY_TIERS kept as a single-card (--label) fallback, recalibrated.
- Dead axes repaired: _pedigree now derives from author lineage (human/hh-loop/
pulled/smoke) instead of a flat 2.0; _heft rescaled to a 512MB log reference
so it spreads instead of pinning mid-size VMs at the cap.
cardimg: apply grade_curve to the gallery so rendered cards match hh-cardex,
plus the interactive 3D flip-card gallery (--flip: front dossier / back stats).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Grow the hh card-art library so common VM types stop reusing one portrait.
- scripts/imgharvest.py: keyless DuckDuckGo image-search harvester — downloads
many stills per character and runs them through charvest's crop/quality/dedup
pipeline (static art crops far cleaner than motion-blurred video frames).
- scripts/harvest_roster.sh: batch roster (Pokemon/Digimon/Yu-Gi-Oh) sized to
the live VM card types. Library now 37 characters / 99 portraits.
- charvest: top-anchor the head+shoulders crop for tall full-body art so faces
are no longer clipped.
chardex matcher — stop same-type VMs collapsing onto a single face:
- fold a per-(VM,character) hash jitter INTO the score, not just a tie-break
- diminishing returns on extra type matches so a strong single-type face can
compete with a dual-type one
- halve tier affinity and cap the keyword bonus
- stop-word rarity/boilerplate words that were polluting keyword overlap
Result: 20 distinct faces across the library. The one remaining cluster
(Sigilyph on the Psychic/Flying recon-scanner VMs) is coherent purpose-
matching — a scanner-themed bird on the scanner VMs — not a bug.
NOTE: assets/characters/ are third-party franchise IP with NO established reuse
license — quarantined, internal WIP only, do not distribute/merge/publish
(licensing record in docs/character-art-licensing.md).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Map the loop's "high value" guidance onto the measurable cardex model
(completeness/reusability/richness/pedigree/heft) and target Epic≥650 /
Legendary≥825. Stamp usage/setup/entrypoint + ≥4 provenance notes in the
manifest, drop --todo at done, and score each card before moving on.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a status field to Card (from the registry entry) and render it as a
done/in-progress pill on the card art. Add `cardimg --gallery`: render every
VM as an inline-SVG card and lay them out in one self-contained index.html
grid (sorted by rarity, with a spread tally) for browser viewing / screenshot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Retune cardex against the live 24-VM library: the old rubric tied 11 VMs at
615 Rare (completeness/reusability maxed for any done+shareable VM, only heft
varied). Rebalance the five sub-score caps (richness 20→30 becomes the
discriminator via per-tag CAPABILITY_WEIGHT; heft log2-scaled) and recalibrate
the rarity thresholds. Library now spreads 2 Legendary / 3 Epic / 16 Rare /
2 Uncommon / 2 Common instead of a single fat Rare band.
Add cmd_chat/cardimg.py: render a Card as a collectible trading-card image.
Free + local-first — a hand-built SVG frame (rarity holo, type badges, 6 stat
bars, dex#, flavor) plus a deterministic procedural creature sigil that needs
no network, rasterized to PNG via local cairosvg/ImageMagick. Pluggable art
backends are opt-in: free hosted Pollinations flux, and key-gated paid
Stability / OpenAI / RunwayML. Every backend only fills the portrait window,
so a card always renders fully offline; failures fall back to the sigil.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Curated 10-brief queue (crypto-ctf, osint-recon, web-fuzz, log-ir-triage,
pcap-forensics, password-audit, threat-intel, vuln-flask, net-scan,
yara-triage) used by hh-loop to build + publish 10 in-depth, verified,
self-describing security VMs to the host registry.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each brief encodes a meticulous-value contract (real software install + real
baked dataset + offline self-test + status:done manifest) so loop output scores
high on the cardex rubric instead of being shallow stubs. Security-focused and
distinct from the existing library.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the autonomous /loop foundation: a headless `hack-house sbx save|publish`
subcommand so a non-TUI operator can persist a built VM to the host library
through the same canonical registry path the room UI uses (no schema drift).
- snapshot.rs: hoist register_saved_snapshot/publish_snapshot/oci_image_size out
of app.rs into a shared module used by both the TUI and the new CLI.
- registry.rs: advisory cross-process lockfile (~/.hh/registry.lock, O_EXCL spin
+ stale-reclaim) around every read-modify-write, plus atomic temp+rename store,
so concurrent /loop wave members can't clobber each other.
- main.rs: `Sbx { Save, Publish }` subcommand wired to the shared snapshot logic.
- skills/hh-loop: the loop doctrine — value rubric, adaptive 1-3 operator
topology, visible-tmux-by-default run flow, --record logs/film, wave scaling,
and safe `tmux -L hh-loop` teardown.
Proven end-to-end: built+verified a stdlib VM in a sandbox, headless
save+publish -> registry entry shareable:true with a portable tar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deterministic value model over the host VM registry: a weighted five-axis
rubric (completeness/reusability/richness/pedigree/heft) yields a PowerScore
0-1000, an absolute-threshold rarity tier, a 6-stat block, an elemental type
from tags, and a stable procedural name + dex number. Pure stdlib, no model,
no network — same VM always mints the same card. Image pipeline lands next.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The operator brain was Claude-only: spawn launched the claude CLI and let its
own loop drive the room. This adds the other first-class brain — a native
tool-calling loop (cmd_chat/operator/harness.py) that runs ANY function-calling
Provider (cmd_chat.ai) against an OPERATOR_TOOLS schema wired to the existing
bridge control verbs (say/exec/write/get/keys/screen/watch/manifest). No CLI
install, no creds carry.
The harness is a driver, not a new side-effect surface: every tool handler sends
the same control-socket request the hh-bridge CLI already sends, so the bridge
keeps enforcing grant-before-drive, the sandbox blast radius, and the recursion
budget. Layer-1 capabilities come from CAPABILITIES.md (the same portable
contract), and a per-agent token ceiling is enforced as a hard stop. Providers
that can't function-call are refused with a clear message rather than degrading
to prose.
New `operate` verb joins via the existing daemon socket and runs the loop with a
--profile or --provider/--model brain. Chat's native loop is left untouched
(self-contained harness over the shared Provider core, not a risky extraction).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The operator's capabilities contract was Claude-only — delivered via the
hh-operator skill, which non-Claude runners cannot load. Extract it into a
portable CAPABILITIES.md and make compose_directive runner-aware: the claude
runner still loads its skill, every other runner gets the capabilities prompt
inlined. Same contract, no skill machinery required.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generalize the three Claude-hardcoded spots in bootstrap (launch argv,
install plan, creds/config-dir) behind a Runner registry so a nested
operator can be spawned with any agent CLI. The Claude path stays the
default and byte-identical.
- bootstrap.Runner + RUNNERS{claude,codex,gemini,cmd} + get_runner/runner_present
- build_run_argv / install_plan / creds_source / plan_creds / child_env all
take an optional `runner` (default claude → unchanged behaviour)
- claude validated end-to-end; codex/gemini are best-effort defaults
overridable via $HH_<RUNNER>_INSTALL etc.; generic `cmd` runner reads a
full launch template from $HH_OPERATOR_CMD ({directive} placeholder)
- bridge _op_spawn resolves req["runner"], rejects unknown; plan now carries
runner/runner_present (was claude_present)
- CLI: `spawn --runner {claude|codex|gemini|cmd}`
- 7 new unit tests; full suite 132 passed
Note: compose_directive still emits the Claude-flavoured ("hh-operator
skill") directive — the portable Layer-1 prompt is P4.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move providers.py + profiles.py from cmd_chat/agent/ to a shared
cmd_chat/ai/ package so the operator bridge and the /ai chat agent can
consume one model-agnostic Provider core (groundwork for harness-mode
operators). Pure refactor — no behaviour change.
- cmd_chat/ai/{providers,profiles}.py: the canonical modules (moved verbatim)
- cmd_chat/ai/__init__.py: re-exports the public API
- cmd_chat/agent/{providers,profiles}.py: thin back-compat shims re-exporting
from cmd_chat.ai (keeps `from cmd_chat.agent.providers import …` working,
e.g. hh/scripts/bench-native-harness.py)
- internal agent consumers (memory/bridge/__main__/__init__) point at cmd_chat.ai
125 tests pass; shim identity verified (re-exports are the same objects).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CLAUDE.md operating guide (room/operator/manifest/VM-registry map +
demo-toolkit pointers) and docs/model-agnostic-operator-plan.md — the
phased spec to let any function-calling model operate a room, not just
Claude (hoist Provider core, harness-mode operator, runner registry,
portable CAPABILITIES, role presets, MCP).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
First functional bench run crashed 0/3 on two latent bench-only bugs:
(1) the Phase-4 token-count change made complete_with_tools return
(text, calls, usage), but the bench preflight still unpacked 2 values;
(2) make_bridge never stubbed _send_sbx_input, so the PTY-mirror
visibility feature hit ws=None and raised AttributeError on every tool
call. Stub it to capture mirrored lines for --verbose.
After the fix: 2/3 pass (write+read, script+run). mkdir+list still
fails — local-backend run_shell CWD divergence + 3B model churn, not a
working-memory regression.
Sprint: native-harness-working-memory (bench) — see also 46e5620, dc6317f, c83abbe
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>
Reusable SKILL.md any Claude Code session can invoke to drive a hack-house
room via the hh-bridge CLI: join, the read --wait → decide → say operator loop,
the permission-gated sandbox drive (exec/write/get + shared-PTY keystrokes),
and an embedded keystroke cheat-sheet centred on the stop-vocabulary
(ctrl-c/ctrl-d/ctrl-z/ctrl-\, esc, pager q, vim :q) so every session knows how
to end a stuck program up-front.
/loop autonomy is folded in as the "indefinite watch" pattern with an explicit
stop-condition vocabulary — the long-poll primitive + persistent daemon from
Phase 1 already support it, so no extra machinery. Symlinked into
~/.claude/skills/ for global reuse.
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>
Phase 4 (final) of the native-harness working-memory sprint. OllamaProvider
.complete_with_tools now returns (text, calls, usage), surfacing the response's
real prompt_eval_count/eval_count (free — already in the payload). The native loop
EMA-smooths real/estimate into self._tok_ratio (clamped [0.5,3.0]) and prunes
against native_token_budget / ratio, so context budgeting tracks the TRUE window
instead of the systematic bias of the len//4 char estimate. Providers that omit
counts leave the ratio at 1.0, so behaviour is unchanged where unavailable — a
free correctness win, no regression. RAM-only, no disk, no new network frames.
Scope note: touches only cmd_chat/agent/ + providers — disjoint from the parallel
feat(operator) work on this branch, so it merges/reverts independently by path.
Sprint: native-harness-working-memory (Phase 4/4) — see also 46e5620, dc6317f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 3 of the working-memory sprint. _prune_native_messages now compacts in two
stages instead of only evicting whole turns: Stage 1 digests OLD tool-role outputs
to a one-line summary (exit marker + first error line, else first line) via the new
_digest_tool_output; Stage 2 falls back to oldest-first whole-message eviction only
if still over budget. Tool outputs are the biggest context hog, and digesting keeps
the action->result causal chain intact, so whole-turn eviction (which severs it)
becomes a last resort. The pinned head/TASK and the recent keep_recent window
(including the most recent tool output, verbatim) are still never touched.
Return is now (messages, dropped, digested); the sole caller logs both. Clean-room
counterpart to Goose's tool-output condensation track. RAM-only, no disk.
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>
Phase 1+2 of the native-harness working-memory sprint. All per-task state is
process RAM only (dies with the task, same lifecycle as MemoryIndex) — no disk,
consistent with the agent's encrypted-transmission / nothing-saved posture.
Phase 1 — _WorkSet dataclass holds what the loop kept re-deriving: sandbox
cwd/shell, files written/read, a failure ledger (cmd -> exit+category), and the
last good command. Discovered cwd/shell carry across tasks in-process via
self._sbx_known (RAM fallback grounding). _render_workset re-surfaces this into
the repair-turn system prompt so it survives context pruning without a NOTES.md
on disk. Folds the old reads_seen set into wset.files_read.
Phase 2 — semantic stuck/loop detection via _action_signature (run_shell keys on
the command, write_file on path+content-hash so real edits aren't repeats,
read_file on path). Aborts honestly when an action fails >=2x verbatim (model
ignoring REPAIR_STANCE) or the same (action,outcome) repeats >=3x, instead of
burning the turn cap re-running a dead action. Verified fix-and-retry does not
false-trip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bench/ native-harness benchmark suite is dev-internal test tooling tightly
coupled to the local tmux + podman test rig, so untrack it and ignore /bench/
entirely (harness code AND result artifacts) — it stays on disk for local use
but no longer ships to origin. Also keep ignoring /docs/plans/ (local planning).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorder _CODER_MODELS to prefer the 3b coder build over 1.5b. The 3b roughly
doubles the ground-truth pass rate on the verify-then-repair native harness
(bench: 7/9 vs ~4/9 over the 9 non-net tasks) at a modest CPU-latency cost,
so it is auto-selected ahead of 1.5b when present. 7b was evaluated and
rejected: too slow to first-token inside the engage window on the CPU-only box.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two clean-room reimplementations layered onto the native `!task` loop
(`_run_native`), aimed at lifting a weak CPU-bound local model's autonomous
pass rate. No code copied from the GPL sources studied; MIT throughout.
NightShift-derived verify-then-repair gate:
- `_classify_failure` maps a failing tool result to a (category, fix-hint) so
the repair nudge names a concrete cause/next-action instead of "exited N".
- `_relevant_excerpt` keeps the error-relevant tail of a FAILING run_shell
result within the byte budget (the real error is usually at the tail).
- read-dedupe guard short-circuits repeated idempotent `read_file` of a path
already read this task.
Exoshell-derived context discipline:
- `_prune_native_messages` budgets the whole message list (~chars/4) and
evicts oldest removable turns first once over `native_token_budget`,
pinning index 0, the TASK_MARKER goal, and the most-recent turns — the
native loop previously grew unbounded, silently pushing the goal out of a
small model's window on long repair runs.
- TASK_MARKER labels the goal so it is never pruned and re-anchors the model.
- REPAIR_STANCE is appended to the turn system prompt after the first failure
to swap the whole turn into a diagnose-then-act posture.
Validated on qwen2.5-coder:3b: clean unstitched 7/9 (the local ceiling), no
regression vs baseline; unit-tested pruning (pin survival, oldest-first
eviction, under-budget no-op) and stance trigger. The two remaining fails are
exact-match correctness tasks (a count, a fibonacci string), not harness gaps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The runner cleared the input with only 6 backspaces and fired one unverified
Enter, so a dropped keystroke left a half-typed prompt that corrupted the next
send and lingered after exit. Worse, online/grant detection counted chat events
via `capture-pane -S` — but this is a full-screen alt-screen app whose scrollback
returns stale/empty frames, so detection was unreliable and the restart loop kept
dismissing healthy-but-slow spawns into a churn cycle.
New bench/tui.py exposes verified primitives shared by the runner and a restart
CLID:
* clear_input / submit — backspace-clear and Enter until the input box reads
empty (the box has no line-editing; Ctrl-A/U/K arrive as literal letters)
* capture() now reads only the VISIBLE viewport (no -S) — the live screen is
the only trustworthy source
* agent_online() reads the present-tense clergy roster, not scrolled-away chat
* restart_agent() stops/starts/grants with a generous 180s online wait (cold
/ai start reloads the model and takes 60-90s on CPU) and retries only a
genuinely hung spawn
run.py now delegates send/clear/online-check to tui and clears the box on exit.
python bench/tui.py restart <model> # one-shot reliable restart+grant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pulled and benchmarked three more tool-capable CPU models looking for a better
default. All score 0/12 (vs qwen2.5:3b at 2/12): in the multi-turn agent loop
they leak the positional-in-tags dialect (<tools>run_shell 'cmd'</tools>) the
parser can't recover, even when they emit clean structured tool_calls on a
single-turn probe; smollm2 and mistral also wedge into repeating summaries.
qwen3:4b could not be pulled — Ollama 0.3.9 is too old (HTTP 412), same as
granite3.1-dense:2b. Upgrading Ollama is the highest-leverage next step to test
the qwen3/granite3.x generation. qwen2.5:3b remains the default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tracked baselines for two additional CPU models under the optimized
harness (split-tag recovery + greedy decode). Both land at 1/12 — they emit
proper structured calls and fail on capability/content, not parse, confirming
the parser lift is concentrated on the weakest model (0.5b). granite3.1-dense:2b
is incompatible with the installed Ollama version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The first harness changes to move the benchmark off the 1/12 noise floor, on
the model that needs it most (qwen2.5:0.5b: 1/12 -> 4/12, 3/12).
- complete_with_tools now decodes the tool loop at temperature 0 (scoped; chat
keeps default sampling). At Ollama's default 0.8 the weak model sampled away
from the tool-call format into prose/fabrication; the nudge prompt changes
between turns so temp 0 still escapes a failed state on retry.
- Greedy decode made 0.5b's leak deterministic, exposing its real shape: not a
JSON object with a name key, but the name in a <tools> tag and the args in a
SEPARATE object — <tools>write_file</tools>{"path":…} — ~5 of 12 tasks/run.
_NAMED_TAG pairs the tag-name with the following args object, gated on the
known tool set so it still can't fabricate an action.
- Bridge recovers a ```bash block narrated in prose as a run_shell call,
non-destructive only (FENCE_DESTRUCTIVE guard); fires on prose-leak turns,
no-op where the model emits structured calls.
Ablation on 0.5b: structured-JSON-only 0/0 -> fenced+temp0 2/0 -> +split-tag
4/3. The lift is concentrated on the weakest model by design — a 3B emits
proper calls and fails on capability/content (unchanged at 1/12), which no
parser can fix. All recovery paths unit-checked for the positive shapes and
the negatives (prose / unknown tool / destructive block) they must ignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generalise OllamaProvider._extract_text_tool_calls to recover a tool-call
JSON object regardless of how a small/quantized model wraps it — qwen's
<tool_call> tags, bare JSON, ```json fences, alternate tags (<tools>,
<function_call>), OpenAI {"function":{…}} nesting, and parameters-vs-arguments.
A new _coerce_call gates recovery on the known tool-name set from the tools
schema, so a stray JSON blob in prose (or a hallucinated make_dir) can never
be coerced into an action. 11-case unit check: 8 leak shapes recover, 3
negatives (prose / unknown tool / random config JSON) ignored.
Benchmark verdict (honest): this does NOT move the weak-CPU-model pass rate
— 3b went 2/1/0 of 12 across three passes (baseline 1/12, noise), 0.5b went
0/0 (baseline 1/12). A direct /api/chat probe shows the hypothesis was wrong
about the FORM of the leak: the weak models emit either malformed structured
tool_calls (write_file content:null) or a fenced bash block in prose with no
tool call at all — not JSON-as-text. The structured-JSON recovery is still a
correct, safe hardening for any model that does leak JSON; the real
weak-model lever (parse ```bash fences -> run_shell) is documented as an
explicit safety decision, not folded in here.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Capture native-harness benchmark baselines for qwen2.5 0.5b/1.5b/3b and
qwen2.5-coder:7b (all probed tool-capable; deepseek-r1 and NL2SH reject the
tools field). All cluster at 1-2/12 with high variance; the 7B buys no
pass-rate gain at ~3x latency, so qwen2.5:3b stays the default. The single
biggest score sink across every model is bare tool-call-as-text leaks — a
harness parse gap, the clear next improvement.
Also drop "in your home directory" from the shell prompts: it made literal-
minded models create a home/ subdir (/root/home/a/b/c/...) or use ~/, which
the benchmark itself surfaced. Findings doc + bench README carry the model
comparison table and recommendations (llama3.2:3b / llama3.1:8b for a non-qwen
data point).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add bench/ — a 4-category × easy/medium/hard task matrix (shell, code, git,
multi) and a runner that drives the live TUI over tmux and grades each task by
a `podman exec` verify snippet (exit 0 == PASS), never by the model's
self-reported summary (which the weak CPU model fabricates). Tasks run in the
agent's real cwd with bare filenames so the suite measures task completion, not
the model's absolute-path discipline. Completion is detected off the viewport-
independent `is thinking…` footer (the TUI is full-screen, so capture-pane
scrollback is not chat history).
First baseline (qwen2.5:3b): 2/12 PASS, high variance. Surfaces the next
harness-addressable improvements — `<native>` tag leakage and bare
tool-call-as-text — now measurable against this suite. Findings doc updated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the overloaded "text + no tool call = done" terminator that made the
weak CPU model stall mid-task or give up after a failing command. Termination
is now a structural `DONE:` text sentinel; a text-only turn is resolved by an
output-aware verdict (DONE: marker / unresolved non-zero exit / no action /
filler language) and re-prompted with an exit-code-aware nudge, bounded by
MAX_NUDGES on top of max_turns. On exhaustion the summary is honest rather than
echoing the model's false "run successfully" — it reports when no tool ran or a
command exited non-zero. Live-validated on qwen2.5:3b: the multi-step stall is
fixed (proj3 completes end-to-end, ground-truth confirmed); the nudge fires on
a 126; residual give-up is model-bound. 23 offline unit assertions pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Live 3B-vs-7B command-entry results, observed failure modes (early stall,
give-up-on-error, tool hallucination), Goose/opencode loop-termination
research, and the output-aware dynamic-nudge-loop design to implement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two native-harness improvements, both live-validated against qwen2.5:3b
and qwen2.5-coder:7b on a podman/Kali sandbox:
- §3 PTY-sentinel: run_shell now runs in the REAL shared terminal via
_run_shell_in_pty (stage cmd out-of-band to a hex-token temp file, type
a `{ sh CMDF; echo $? >RCF; } 2>&1 | tee OUTF` wrapper into the live PTY,
poll the rc sentinel out-of-band, then read OUTF). The whole room now
watches commands execute live instead of an inert `# ▸` comment, while
output + exit code are still captured for the loop. tee+poll (not
stream-sentinel) avoids deadlocking the serve loop; the wrapper line
carries only our own temp paths so room text never reaches the shell
parser. _exec_tool takes ws to reach the PTY.
- NATIVE_CONTEXT=4: action tasks now get a tight, RAG-free window (last few
transcript turns only, no semantic recall). A weak model fed prior chat
chatter latched onto nearby noise (wrote a "grant permissions" script for
"write a bash script"); feeding just the instruction fixes it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
So the owner never has to name each model. Tracks an ai_agents set
(populated from `_ai` typing/stream frames and the "(ai) online" announce,
pruned on leave); `/grant ai` intersects it with the live roster and grants
all in one ACL broadcast. Help text gains a /grant ai row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Optimize the native tool-calling loop for qwen2.5:3b on CPU, where it
previously invented paths (/ai/bin/bash), ran scripts it never wrote, and
silently dropped valid actions. Three changes:
- NATIVE_SYSTEM rewritten directive: explicit write→chmod→run workflow,
relative paths only, never run an uncreated file, never guess interpreter
paths, fix the cause on non-zero exit.
- New _sandbox_facts() probe injects LIVE SANDBOX STATE (real cwd, bash
path, current files) into the system prompt so the model anchors to
ground truth instead of guessing.
- OllamaProvider recovers tool calls qwen emits as <tool_call>{json}</…>
TEXT in content (brace-balanced JSON scan), so a correct action isn't lost.
- Bump Ollama timeout 120→240s: the tool turn is non-streaming and a long
write_file can exceed a tighter cap on a contended CPU box.
Live-validated (podman/Kali): 0/3 incoherent → reliable write/run with
self-correction on exit=126 for both single- and multi-script tasks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reflect current backends (local/docker/podman/multipass — Docker=Parrot
OS Security, Podman=Kali rootless no-sudo), the backend-first /sbx
grammar, and the AI agent's ability to drive the sandbox via
/ai <name> !<task> (advisory when ungranted, acting once granted).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display-mirror hybrid (docs/plan-harness-visibility.md §2): native tool
calls now show up in the shared sandbox terminal again via inert `# `-
prefixed comment lines (comment-prefix = anti-double-run/anti-escape),
mirroring only each command. Chat de-flooded to opener + final summary.
write_file mkdir -p parent dir so relative/absolute paths both work
(fixes the regression where script creation silently failed). ui.rs
fmt_line returns Vec<Line> splitting on \n so multi-line agent output
renders as an indented block instead of one garbled row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the bounded native harness from docs/spec-native-harness.md §1.3 and
make it the default granted-!task path. The model runs host-side (no container→
host Ollama hop); only its tool calls exec in the sandbox.
providers.py:
- OllamaProvider.complete_with_tools(system, messages, tools) -> (text, calls):
one non-streaming /api/chat turn with a `tools` schema; parses message.tool_calls
(dict or JSON-string arguments). Caches tool capability (_tools_ok / supports_tools).
- ToolsUnsupported raised when the model rejects `tools` ("does not support tools").
bridge.py:
- NATIVE_SYSTEM + a 3-tool schema (run_shell / write_file / read_file), turn/byte caps.
- _run_native: seed transcript window + task → loop up to max_turns; exec each tool
call in the sandbox, feed captured output back as a `tool` message; stop on a plain
answer or the cap; stream per-call progress to chat. Degrades to _run_simple when the
provider has no complete_with_tools or the model rejects tools.
- _exec_prefix/_exec_capture/_exec_tool: <engine> exec into docker/podman/multipass/local;
paths passed as positional args + content via stdin (no shell interpolation); combined
stdout+stderr byte-capped + time-bounded. run_shell is the only intentional shell.
- Guards: DESTRUCTIVE run_shell commands are blocked (not run — no human in the loop;
use simple + /ai confirm for destructive intent); MAX_COMMANDS budget per task.
- _run_in_sandbox dispatches native|simple; default harness flipped to native.
__main__.py: default harness native (self-degrades to simple, so safe).
Offline-tested: full write/run/read loop on the local backend; destructive block
(rm -rf never executed); ToolsUnsupported → simple fallback. Live Ollama wire
validation deferred to Phase 3 bench (daemon was down). py_compile clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the Goose agentic harness across the codebase per
docs/spec-native-harness.md §3. Goose made N sequential model calls inside the
sandbox (slow on CPU-only hardware) and forced an in-container→host Ollama
gateway that tripped the rootless-Podman slirp4netns loopback bug.
- bridge.py: delete _run_goose/_goose_argv/_goose_present + GOOSE_* consts and
the present-cache; __init__ now takes harness="simple"/max_turns=5; granted
!task runs _run_simple until the native loop lands (Phase 2).
- __main__.py: --harness {native,simple} (was {goose,simple}); drop
--goose-max-turns, add --max-turns; default harness simple.
- app.rs: /ai start accepts native|simple (plain aliases simple) instead of a
bare plain flag; refresh harness comments.
- sbx.rs: remove the in-container Ollama gateway (Docker host-gateway / Podman
slirp4netns host-loopback) and the dk_bootstrap OLLAMA_HOST env — kills the
slirp4netns loopback bug; drop Goose comments.
- bootstrap.sh: drop goose from the prereq probe.
- bootstrap-ai.sh: remove the entire Goose install block, --no-goose flag,
GOOSE_INSTALLER_URL, host config writer, and goose_bin helper.
- sandbox-bootstrap.sh: remove the in-sandbox Goose binary install + config.
- spec-goose-harness.md: banner — harness portion superseded; Podman stays.
cargo check + py_compile clean. No Goose refs remain (headroom/ untouched).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /ai agent held a single, un-shielded websocket with no retry. Any
close — server restart, ping/idle reap, laptop sleep, a transient blip —
ended the serve loop, so run_async returned and the process exited
silently: the agent dropped from the roster with no /ai stop and no
goodbye.
- run_async now wraps the connection in a backoff-reconnect loop (1s→30s,
resets after a healthy ≥30s session). The server frees our session+name
on drop, so each attempt re-runs SRP to mint a fresh token. Only Ctrl-C
/ process kill (KeyboardInterrupt / CancelledError, how /ai stop ends
us) breaks the loop.
- _serve shields each frame via _handle_frame so one malformed/poisoned
frame — or a handler error — can't unwind the loop; ConnectionClosed
and cancellation propagate up to the reconnect loop.
- Forgiving keepalive (ping_interval=20, ping_timeout=60) so a heavy
CPU-only Ollama generation doesn't trip a false drop.
Also adds docs/spec-native-harness.md: replace the heavyweight Goose
harness with a lightweight host-side Ollama-native tool-calling loop
(model runs host-side, only commands exec in the sandbox — the
slirp4netns→host-Ollama bug disappears), and a file-by-file plan to strip
all Goose integration. Supersedes the harness portion of
spec-goose-harness.md (Podman backend stays).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add an installable-VM catalog that ships pointers only (no multi-GB images
in the repo). When a VM is chosen it is BUILT LOCALLY on the caller's own
machine — nothing is relayed to the room.
- scripts/vbox-library.json: 7-entry manifest (Windows 11, macOS Sonoma,
Kali, Parrot, Ubuntu 24.04, Fedora 41, Debian 12) with download
pointers, ostype, cpu/mem/disk, and build kind.
- scripts/vbox-library.sh: --list / --info / --plan / --install. Build
kinds: iso (download/--iso + createvm + boot installer; EFI+TPM for
Win11), cloudimg (delegate to vbox-new.sh, unattended), ova (import),
manual (pointer-only, e.g. macOS per Apple licensing). Detect-first:
--plan changes nothing, install refuses to clobber and rolls back
half-built VMs, direct URLs fall back to the page + --iso <path>.
- sbx.rs: vbox_library()/library_vm()/vbox_library_install() loaders +
running_vms() for live-state markers.
- app.rs: /sbx vmlib (catalog ✓installed/↓available), /sbx vmlib <id>
(pointer/notes), /sbx vmlib <id> install [--iso path] (local build);
/sbx vms now flags running VMs (▶). Registered in SBX_SUBCOMMANDS +
usage.
- ui.rs: help entry for the VM library.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The docker/podman headless-container desktop (XFCE + TigerVNC + websockify
+ noVNC published on host 127.0.0.1:6080) is removed. It was unreliable: the
desktop stack installed asynchronously during provisioning with every step
wrapped in `|| true` (silent failures), while the host port mapping existed
the moment the container ran — so opening the browser before websockify bound
6080 inside the container reset the connection (ERR_CONNECTION_RESET), with no
readiness signal and no loud failure. Containers also can't be rendered as a
real desktop by VirtualBox (no framebuffer/display), so the only desktop path
that stays is the native VirtualBox VM GUI.
Removed:
- sbx.rs: GUI_PORT const; PortHolder/port_holder()/kill_port_holder() (the
port-consent gate existed only for the noVNC publish); the `gui` param +
`-p 127.0.0.1:6080:6080` block in prepare(); HH_SBX_GUI env in dk_bootstrap();
the `gui` param on provision().
- app.rs: PendingGuiLaunch + PortPrompt structs; the port_prompt App field; the
`gui` field on PendingSudoLaunch; the port-consent modal + the sudo-submit
port check; want_gui/gui parsing + the container-GUI launch message; `gui`
threaded through spawn_launch and all call sites.
- sandbox-bootstrap.sh: the entire HH_SBX_GUI=1 desktop block.
- ui.rs / usage strings: dropped "[gui]"/"noVNC desktop" from the docker/podman
help and the /sbx usage line.
Kept (unchanged): the VirtualBox VM GUI — gui_launch() (VBoxManage startvm
--type gui), launch_vbox_gui, `/sbx vbox gui <vm>` and the `/sbx gui` alias. The
`gui` keyword is still stripped from positionals so `/sbx vbox gui <vm>` parses.
cargo check passes clean (no warnings).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The VBox GUI launch path previously had no in-TUI password capture — it
relied on cached creds (`sudo -n`) and otherwise aborted with guidance.
Generalize the existing masked sudo modal to cover it: `SudoPrompt.pending`
becomes a `PendingPrivileged` enum (container `Launch` | `VboxInstall`), so
the same local-only, never-logged password buffer now authorizes a VBox
install too. On submit the captured password is fed to `ensure_vbox_install`
via `sudo -S`, then the VM boots. Empty/Esc still cancel cleanly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GUI launch (docker/podman) now pre-flights the noVNC port (6080): if it's
already bound, a modal names the holding pid and asks before killing it and
proceeding — never a blind `bind: address already in use` collision.
Sudo capture is now uniform across every backend installer. Previously only
Docker fed the masked-modal password through `--stdin-pass`/`sudo -S`; the
captured password was silently dropped for Podman/Multipass and never wired
for VirtualBox, and those scripts used bare `sudo` (which hangs/corrupts a
raw-mode tty). Now:
- shared `run_ensure()` feeds the password to any ensure-*.sh via stdin
- podman/multipass/vbox scripts gain the docker sudo ladder
(interactive `sudo` / `--yes` `sudo -n` / `--stdin-pass` `sudo -S -p ''`)
- podman's apt path wrapped in `sh -c` so one sudo covers update+install
- vbox GUI path preflights `sudo_ready()` with actionable guidance, falling
back to fail-fast `sudo -n` instead of a tty hang
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the goose-harness and sandbox-distros-GUI design specs, a consolidated
command reference and demo-reels plan, the AI-harness planning note, and
two new client themes (blue-orange, pink-red-gray).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Podman as a rootless/daemonless sandbox backend alongside Docker,
Multipass and Local, and wire Goose in as the default agentic harness
for the granted `!task` path (bridge execs `<engine> exec <name> goose
run` and streams output to chat; auto-degrades to the simple one-shot
injector when goose is absent).
Add an optional GUI sandbox track (XFCE + TigerVNC + websockify/noVNC on
:6080) summoned via `/sbx <engine> gui`, plus container-side provisioning
in sandbox-bootstrap.sh and a host-side ensure-podman.sh prereq helper.
Refresh the in-app command help to the backend-led `/sbx <engine> [gui]`
grammar and minor ui tweaks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make the message-input bar part of the F5 resize cycle so its height is
adjustable without a sandbox (multi-line/wrapped inputs are now readable),
and let chat/clergy borrow height from the compose box when no terminal is
present. Add Option C masked sudo prompt that feeds `sudo -S` over stdin to
install/start Docker — the password never reaches chat, the PTY, or outbound
frames, and Docker Desktop on Linux is detected so no sudo is requested.
Fix a freeze where clicking the compose box entered layout-edit mode and
silently swallowed every keystroke: clicking the input bar no longer enters
edit mode, and typing any printable char now drops out of edit mode and types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design for replacing the two-scalar layout (pty_pct + roster_width) with a
binary space-partition pane tree so every pane is resizable on both axes,
fixing the "only roster width adjusts" limitation. Rolls a small in-repo tree
(no new dep) and drives the PTY grid from the terminal pane's actual Rect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
README command table claimed `/send <path>` sends to the room; actual code
(app.rs) makes `/send <user> <path>` a targeted send and `/sendroom <path>`
the room-wide offer. Add the missing `/sendroom` and `/clear` rows (both
already in the in-app help) and drop the inaccurate "save to disk" claim on
Ctrl+Alt+P — saving is `/theme save`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verified anonymous clone from git.churchofmalware.org succeeds; only the
GitHub mirror is private. Restore the plain public clone instructions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The repo is private, so anonymous clone 404s. Add token + SSH clone
instructions under the quick-start so invited users can install while we
promote the Gitea instance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update the git clone URL in README and CONTRIBUTING from the old
GitHub remote to the project's Gitea instance
(git.churchofmalware.org/trilltechnician/hack-house.git). Verified the
clone succeeds over HTTPS against that path.
- CONTRIBUTING: fork step now points at the Gitea instance, not GitHub.
- Refresh the /sbx command table to match the client: `/sbx launch` now
documents the `install` consent token and `--start` daemon boot, and a
new row covers `/sbx launch vbox [new [name]]` (VM picker / cloud-init
build).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add a -h/--help guard (prints the usage header) to the five scripts
that lacked one: smoke.sh, smoke-e2e.sh, test-features.sh,
demo-save-load.sh, sandbox-bootstrap.sh. All 15 hh/scripts/*.sh now
respond to --help.
- README: new "Scripts" section detailing every script (setup, hosting,
sandbox provisioning, tests) and noting each takes --help; plus an
archive note for the film-* recording scripts.
- README: new "Window layout" section documenting live pane resizing
(F4 fullscreen, click/F5 to select + arrows to resize) and the
/layout save/load/list/rm/reset presets, with matching keybinding-table
rows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Trim newcomer-facing clutter in hh/scripts/ without changing the running
product or CI.
- Move the one-off demo-recording scripts (film-save-load.sh,
film-virtualbox.sh) to scripts/archive/ — they depend on external
personal tooling (asciinema, video-toolkit, edge-tts, xdotool) and were
never wired into the README or CI.
- Delete join.sh: it duplicated connect.sh's join with fewer options
(hardcoded port, plaintext-only). Its only unique behaviour — a
pre-join git pull (gitea→origin, ff-only) + rebuild — is now an opt-in
`--sync` flag on connect.sh, leaving one canonical joiner.
No scripts or docs referenced the removed/moved files, so nothing else
needed updating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a window-management layer so the chat, roster and sandbox-terminal
panes can be resized live and fullscreened, with named presets for
recall.
- New layout.rs: single source of truth for the body split (pty_pct,
roster_width) and a Zoom state (Normal/Term/Chat), persisted to
layouts/<slug>.toml like themes. Both ui::draw and app::sbx_dims read
from it, so resizing just mutates state and clears announced_dims —
the per-tick loop re-syncs and broadcasts the new PTY grid.
- F4 cycles terminal/chat fullscreen (F-keys aren't forwarded to the
shell, so nothing is stolen from in-shell apps).
- Interactive editing: click a pane (or F5 to cycle terminal -> chat ->
roster) to select it; arrows then resize it live, Esc/Enter finishes.
The selected pane gets a bold accent border and an ✎ title marker.
ui::pane_at hit-tests against the same body_areas rects draw() paints.
- /layout slimmed to presets only (save/load/list/rm/reset); the old
numeric pty/chat/roster and full/chatfull/normal verbs are replaced by
the interactive flow.
- Help menu updated: LAYOUT cluster + KEYS line document F4, click/F5,
arrows and Esc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, launching a sandbox on a machine without the backend binary
died with a raw "not installed" error and no path forward. Now the
missing backend can be installed in-line, gated on explicit consent, so
a fresh checkout can go from zero to a running sandbox without leaving
the TUI.
Folded into the existing `/sbx launch` verb rather than a new command
(menu is already dense): `/sbx launch docker|multipass [image] install`.
The `install` token opts in; without it the user gets an actionable
error naming the exact retry. The token is filtered out of positional
image parsing so it never shadows a custom image.
The install runs off-thread inside the existing spawn_launch task,
before provisioning, so the TUI never blocks and the *launching guard is
cleared via BrokerMsg on failure. A fresh Docker install also leaves its
daemon up, so launch proceeds straight through.
Scripts (detect-first, never silent, --plan dry-run, idempotent if
already present):
- ensure-multipass.sh (new): Linux→snap (clear failure if snapd absent),
macOS→brew cask, Windows→winget.
- ensure-docker.sh: new --install mode using Docker's OFFICIAL,
GPG-verified apt repo (docker-ce) on Debian/Ubuntu, with dnf
(Fedora/RHEL) and pacman (Arch) fallbacks. Deliberately avoids piping
get.docker.com into a root shell. Existing daemon-start path intact.
sbx.rs: docker_installed()/multipass_installed() detectors and
ensure_docker_install()/ensure_multipass_install() wrappers; ENSURE_MULTIPASS const.
ui.rs: help text documents the [install] option on both backends.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two interactive shell apps were unusable in the shared sandbox because the
TUI swallowed their core keys:
- Esc released drive mode and was never mapped in key_to_pty, so vim could
never leave insert mode. Esc now forwards 0x1b to the PTY; F2 (already a
drive toggle) is the release key.
- Ctrl-X (owner kill switch) was intercepted globally, so nano could never
quit. It's now gated on !app.driving — while you hold the shell Ctrl-X
reaches the PTY (0x18, nano's quit); release with F2 to arm the kill
switch.
Updated the on-screen hints and /help KEYS cluster to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multipass can only snapshot a powered-off instance, so `/sbx save` on a
running multipass sandbox previously just surfaced multipass's "instance
must be stopped" error — making the snapshot that `/sbx load` needs
impossible to create through the UI.
Now multipass save powers the instance down before snapshotting, and the
handler tears the shared session down first (same as `/sbx stop`, but
WITHOUT purging so the instance — and thus the snapshot — survives for a
later `/sbx load`). Docker is unchanged: `docker commit` captures a live
container, so its save stays non-disruptive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The existing vbox paths only open pre-made images; there was no way to
build one from scratch. Add `/sbx launch vbox new [name]` backed by
scripts/vbox-new.sh: download an Ubuntu cloud image, convert it to a VDI,
build a cloud-init NoCloud seed that creates a sudo login user and installs
the sandbox-tools.json toolchain on first boot, then create + boot the VM.
VirtualBox guests can't be exec'd into like docker/multipass, so cloud-init
is the provisioning channel. Generates a one-time login password (or takes
--pass) and authorizes a local SSH key if present. Runs off-thread; rolls
back a half-built VM on failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorganize the help menu into one VIRTUAL MACHINES cluster covering all
backends, and bring docker/multipass/vbox to save+load parity:
- Launch-time dev toolchain: sandbox-bootstrap.sh + editable
sandbox-tools.json (vim/curl guaranteed), installed in docker AND
multipass sandboxes at provision time.
- Vbox load: vm_restore + `/sbx vmload <vm> [label]` (restore snapshot
then boot the GUI).
- Multipass load: `/sbx load` is now backend-aware (locate_snapshot +
SnapKind), mp_restore re-attaches the shared shell; teardown stops
(not purges) an instance that still has snapshots so they survive
`/sbx stop`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base ubuntu images ship without /var/lib/apt/lists, so apt-get install
failed with "Unable to locate package" until the user ran update by hand.
Refresh the index once during Docker provisioning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app.rs: F1 now closes the help overlay (toggle), not just opens it
- host-house.sh: add --user/--name flag to override the $USER default seat
- connect.sh: incremental cargo build before launch so the UI is never a
stale release binary; --no-build escape hatch for toolchain-less joiners
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a BRANCH guard (default: main) so launching the house never demos a
stale checkout. ensure_branch() switches to $BRANCH before the cargo build,
refuses when the tree is dirty (won't clobber uncommitted work), and no-ops
when already on-branch or when BRANCH= is passed to keep the current checkout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- host-house.sh: one tmux session hosting a room (server window) plus your own
GUI client window — builds, frees the port, waits for /health, then attaches.
- host-room.sh: free_port() kills any stale LISTENer squatting on the port so
the bind can't fail with "address already in use".
- join.sh: cap each remote pull with `timeout` + GIT_TERMINAL_PROMPT=0 so an
unreachable remote fails fast instead of hanging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unify sandbox summoning under `/sbx launch <docker|multipass|vbox>`, each
running on the invoker's own machine. For vbox:
- `/sbx launch vbox` opens an arrow-navigable VM picker (↑↓/Enter/Tab/Esc).
- `/sbx launch vbox [gui] <vm>` boots frictionlessly for a host that already
has VirtualBox + the VM imported; a non-host appends `yes` to install
VirtualBox, import the shared appliance, and/or free VT-x first.
- `/sbx gui <vm> [yes]` kept as an alias.
Add `vm_registered()` + `import_appliance()` (VBoxManage import) in sbx.rs.
Make file transfer stream disk-to-disk so multi-GB VM images can be shared
(the old 50 MB in-memory cap blocked them): `STREAM_MAX` = 16 GiB for `/send`
(`MAX_SIZE` now guards only the in-memory tar_path for sandbox injection).
`prepare_send` stat+stream-hashes off the UI thread; `Sink`/`commit` write
incoming chunks straight to a temp `.part` file and verify SHA before moving
into place. Wire frames (offer/accept/chunk/done, 64 KB, base64) unchanged.
A received `.ova`/`.ovf` auto-imports so the recipient can immediately
`/sbx launch vbox gui <vm>`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the 14 hack-house scripts (bootstrap, lets-hack, host-room, smoke,
demo/film harnesses, connect/join, ensure-docker/vbox) into hh/scripts/
and fix their path resolution for the deeper location. Update cross-refs:
sbx.rs script consts, app.rs hints, CI, direnv .envrc, README, and docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-06 17:49:32 -07:00
201 changed files with 20659 additions and 1318 deletions
@@ -32,11 +32,12 @@ Encrypted chat that runs in your terminal. You host the server, you control the
- **SRP authentication** — the password is never sent over the network (zero-knowledge proof)
- **Zero-knowledge server** — relays only ciphertext; cannot read messages, files, or terminal output
- **RAM only** — nothing persisted on the server; close it and history is gone
- **Shared sandbox** — summon a disposable `local` / `docker` / `multipass` box the whole room can watch and drive
- **Shared sandbox** — summon a disposable `local` / `docker` /`podman` /`multipass` box the whole room can watch and drive. Docker defaults to **Parrot OS Security** (`parrotsec/core`) and Podman to **Kali** (`kalilinux/kali-rolling`) — pentest distros out of the box; Podman is rootless & daemonless, so **no sudo** to launch
- **Snapshot save/load** — freeze a sandbox to a named snapshot and restore it later (`/sbx save` · `/sbx load` · `/sbx snaps`)
- **Local VirtualBox VMs** — `/sbx vms` detects VirtualBox and lists your VMs; `/sbx gui <vm>` opens a desktop VM locally for the room to gather around — per-user consent gate, with automatic resolution of VT-x conflicts (Docker Desktop / multipass)
- **Real permissions** — the host grants/revokes *drive* (keyboard) and *sudo* (VM superuser) per user; **stacking roster badges** show exactly who holds what, both in the clergy panel and inline on every chat message
- **Local-first AI agent** — `/ai start` summons an in-room AI that runs against *your own* [Ollama](https://ollama.com) (no API key, nothing leaves your machine); replies **stream token-by-token** with **in-RAM semantic recall** of the conversation for context; model-agnostic, addressed-only, end-to-end encrypted like every other client
- **AI that acts in the sandbox** — grant an agent *drive* and address it with `/ai <name> !<task>`; it works the shared box through a bounded, host-side **tool-calling loop** (run shell, write/read files, inspecting each result before the next step) and you watch its commands land live in the shared terminal. Ungranted, it stays **advisory-only** (tells you the commands, runs nothing); destructive commands are gated behind an explicit `/ai <name> confirm`
./scripts/lets-hack.sh --reuse # keep a live server (reconnect tests)
./scripts/lets-hack.sh --kill # tear it all down
```
### 3. Manual setup
@@ -150,17 +150,22 @@ Type to chat. Slash commands and keys:
| `/help` · `F1` | Help overlay |
| `/pw` | Show this room's password (local only — never broadcast) |
| `/theme [name]` | Switch vestments, or list them |
| `/send <path>` | Offer a file (or directory) to the room |
| `/send <user> <path>` | Offer a file (or directory) directly to one member |
| `/sendroom <path>` | Offer a file (or directory) to the whole room |
| `/accept` · `/reject` | Respond to a pending file offer |
| `/ai start [model\|profile]` | Summon a local AI agent (default `ollama/qwen2.5:3b`; a bare name is a `models.toml` profile) |
| `/clear` | Wipe your chat scrollback (local only) |
| `/ai start [model\|profile] [allow]` | Summon a local AI agent (default `ollama/qwen2.5:3b`; a bare name is a `models.toml` profile). `allow` auto-grants it sandbox drive at spawn |
| `/ai stop` | Dismiss the agent you summoned |
| `/ai <question>` | Ask the agent (`/ai <name> <question>` if several present) |
| `/ai <name> !<task>` | Have a *granted* agent act in the shared sandbox (advisory-only if it has no drive) |
| `/ai <name> confirm` | Approve a gated (destructive) command the agent proposed |
| `/ai list` | List the agents present (or hint to `/ai start` if none) |
| `/ai models` | Models the active agent can serve — or, with no agent, your local Ollama tags |
| `F4` · `F5` · click | Layout: fullscreen terminal · select a pane to resize (then arrows · `Esc`) — see [Window layout](#window-layout) |
| `/layout save \| load \| list \| rm \| reset` | Save / recall named pane arrangements |
### The shared sandbox
Anyone in the room can summon a disposable Linux box with `/sbx launch`. The
Anyone in the room can summon a disposable Linux box with `/sbx <backend>`. The
person who summons it is the **owner/host**: their client runs the real PTY
locally and relays its output to everyone else as encrypted frames, so the
server only ever sees ciphertext (same trust model as chat).
@@ -180,10 +187,15 @@ server only ever sees ciphertext (same trust model as chat).
| Backend | Isolation | Notes |
|---|---|---|
| `local` | none | a `bash` shell on the host — fast, for dev/testing only |
| `docker` | container | `ubuntu:24.04` by default; `/sbx launch docker --start` boots the daemon (or run `./ensure-docker.sh`) |
| `docker` | container | **Parrot OS Security** (`parrotsec/core`) by default — swap `parrotsec/security` per-launch for the full pentest set; `/sbx docker --start` boots the daemon (or run `hh/scripts/ensure-docker.sh`) |
| `podman` | container | **Kali rolling** (`kalilinux/kali-rolling`) by default — **rootless & daemonless, no sudo to launch** (add `kali-linux-headless` for the toolset); `hh/scripts/ensure-podman.sh` installs it |
| `multipass` | full VM | `24.04` by default; strongest isolation, ~30 s to boot, the choice for real use |
Tear it down with `/sbx stop` (purges the VM/container).
The backend leads the command — `/sbx podman`, `/sbx docker`, `/sbx multipass`,
`/sbx local` (the older `/sbx launch <backend>` still works). Override the image
positionally, e.g. `/sbx docker parrotsec/security` or `/sbx podman ubuntu:24.04`.
Both container engines are Debian/apt-based, so the dev-toolchain bootstrap runs
unchanged. Tear it down with `/sbx stop` (purges the VM/container).
**Snapshots.** Freeze the current sandbox to a named checkpoint with `/sbx save
[label]`, list what you've stored with `/sbx snaps`, and restore one later with
@@ -233,8 +245,9 @@ they can never advertise a power the room won't honour.
### Sharing files & directories
`/send <path>` proposes a transfer; recipients `/accept` or `/reject`. A whole
directory works too (it's packed before sending). Files are chunked (64 KB),
`/send <user> <path>` proposes a transfer to one member; `/sendroom <path>`
offers it to everyone. Recipients `/accept` or `/reject`. A whole directory
works too (it's packed into a `.tar` before sending). Files are chunked (64 KB),
encrypted with the room key, relayed as opaque ciphertext, and **SHA-256
verified** on arrival before landing in `./downloads/`. Max size is 50 MB.
@@ -247,10 +260,19 @@ when you quit). Pick a model at summon time with `/ai start <model>`.
- **Runs on *your* machine.** The default provider is [Ollama](https://ollama.com)
— a local model (default `qwen2.5:3b`), no API key, nothing leaves your host.
Run `./bootstrap-ai.sh` once to install it and pull the model.
Run `hh/scripts/bootstrap-ai.sh` once to install it and pull the model.
- **Addressed-only.** The agent reads room traffic like any client but forwards
to the model *only* the messages that trigger it (`/ai …`) — no passive
surveillance, no cost or noise when idle.
- **Can drive the sandbox.** Grant an agent *drive* (`/grant <name>`, or summon it
pre-granted with `/ai start <name> allow`) and ask it to act with
`/ai <name> !<task>`. It works the shared box through a bounded **host-side
tool-calling loop** — run shell commands, write and read files — inspecting each
result before the next step, and you watch its commands appear live in the
shared terminal. Every command runs *inside the sandbox* (the container/VM is the
blast radius), capped in count and time. Without drive it stays **advisory-only**
(it spells out the commands, runs nothing). Destructive commands are blocked
pending an explicit `/ai <name> confirm`.
- **Model-agnostic.** Swap the backend without touching the client: bundled
adapters for `ollama` (default), `anthropic`, and any OpenAI-compatible
endpoint (OpenAI, Groq, Together, local vLLM…), plus a `module:Class` hook for
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.