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>
Friendly wrapper around `cmd_chat.py serve`: uses the project venv, mints a
random room password passed via $CMD_CHAT_PASSWORD (kept out of the process
list), prints the join command preferring the Tailscale IP, defaults to
--no-tls with an opt-in --tls (cert/key) path, and runs in the foreground.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agents now join under their model tag (e.g. "qwen2.5:3b" — model name +
parameter size) or profile label, derived at /ai start and tracked on
App.agent_name, so the roster shows what's actually answering. Updates the
broker re-grant + /ai stop revoke paths, the Python --name default
(falls back to provider.model), and the demo/bootstrap/README/docs refs.
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>
Derive each rolled palette's surface from its ink via legible_bg so the
background is as expressive as it can be while keeping chat text above a
contrast floor. Adds the oc-demo theme and reaction images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- /sbx save [--local] and new /sbx vmsave/vmsnaps: snapshot Docker,
Multipass, and VirtualBox; --local also writes a portable artifact
(docker .tar / VBox .ova) under hh-snapshots/ that survives pruning.
- sandbox reappears for anyone who leaves and rejoins (host replays
status + screen snapshot + ACL on Joined); SbxStatus ready handler is
now idempotent so it never wipes scrollback.
- received files auto-bridge into the hosted sandbox (ft::tar_path).
- AI agent: translate Ollama's cryptic 404 into model/host/fix guidance.
- bootstrap installs the AI layer (Ollama + default model) by default,
with consent gates; --no-ai opts out, --yes skips prompts.
- help menu lists the new save/vm flags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- lets-hack.sh: default PW is now `openssl rand -hex 12` (printed in the
summary for sharing) instead of the well-known hardcoded `malware-bless`
- join.sh: drop the hardcoded password (uses $HH_PASSWORD or a no-echo
prompt), and fast-forward pull from gitea+origin then `cargo build`
before connecting so joiners run the latest code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
M1: enforce the declared transfer size (clamped to MAX_SIZE) on chunk
receipt in both the Rust and Python clients — a malicious sender can no
longer grow the receive buffer unboundedly.
M2: only honor X-Forwarded-For when TRUST_PROXY is set, so a direct
client can't spoof a source IP to dodge the per-IP rate limiter.
M3: evict unverified SRP sessions after a 60s TTL on each new handshake,
preventing half-finished auths from exhausting memory.
M4: drop WS frames larger than 256 KB before they hit the store or
broadcast, bounding per-message memory and flood blast radius.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- introduce a Role model + App::host()/roles_of(): the host is the first
occupant of the roster (shown the moment they join, no sandbox required),
and roles are additive — a host who summoned a sandbox and can drive reads
✝⚡◆. Badges read the same ACL the broker enforces, so they can never
advertise a power the room won't honour
- render the stacked badge in the clergy panel and inline next to the author
on every chat message; split VirtualBox commands into their own help cluster
- default styling: the startup handle prompt now prints ✝ (crypt sigil)
instead of the inverted pentagram
- README: document VirtualBox VMs, snapshot save/load, AI streaming + recall,
the badge system, and the expanded theme set
- gitignore out-of-tree experiments, the heavy demo-build kit, and logs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- proptest-fuzz the untrusted frame parsers (sbx/ai/perm/users/decode_msg) so
a hostile relay/peer can never panic a client; fixes a decode_msg timestamp
byte-slice that panicked on a non-ASCII stamp (now char-boundary safe)
- extract a pure classify_vtx_holders() out of vtx_holders() and unit-test the
KVM/QEMU/multipass detection and stoppability rules
- headless cross-stack smoke test (smoke-e2e.sh): real relay + two TUI clients
in tmux, asserting SRP join, Fernet chat round-trip, and command dispatch
- CI: macOS matrix for the Rust client, cargo-audit + pip-audit, gitleaks
secret scan, llvm-cov/pytest-cov coverage, and a smoke-test job
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Python client saved an incoming transfer under the offerer-controlled
`name` field verbatim, so a peer could supply `../../…` or an absolute path
and write a file anywhere the user can (arbitrary write → RCE). Reduce the
name to a bare basename before joining it to the download dir, matching the
Rust client's existing behaviour.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate VirtualBox as a local facility rather than a shared-PTY backend: a
Windows guest has no shell to relay, so the honest fit is launching the VM's
GUI on the caller's own machine (the "share a VM, run it locally" path) — no
display is relayed to the room, so zero-knowledge is untouched.
- ensure-vbox.sh: detect-first installer mirroring ensure-docker.sh; --check,
--plan (real apt --simulate download plan, no changes), --yes; apt/dnf/
pacman/brew/winget; Secure Boot MOK warning. HH_VBOX_FORCE_MISSING lets a
demo exercise the missing->install path without uninstalling.
- sbx.rs: vbox_installed/vbox_version/list_vms/vm_running/gui_launch +
ensure_vbox_install.
- app.rs: /sbx vms (detect + list) and /sbx gui <vm> [--install] (detect-first
then startvm --type gui); /sbx launch virtualbox steers to /sbx gui.
- ui.rs help: /sbx vms and /sbx gui entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document /ai start <model> allow, /ai <name> !<task>, and the agent-aware
/grant|/revoke variants in the clustered help menu.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the VirtualBox sandbox design spec (headless 4th backend + share-an-
appliance GUI mode with detect-first install), the crypto pay-to-join gate
design, and the save/load PoC writeup with its demo/film driver scripts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- /sbx save|load|snaps: docker commit → hh-snap:<label> image that
survives /sbx stop; load relaunches a fresh sandbox from it; multipass
delegates to `multipass snapshot`. Local backend unsupported.
- Help overlay redesigned into topical clusters (SANDBOX, AI AGENTS,
PERMISSIONS, FILES, APPEARANCE, KEYS, ROSTER GLYPHS), collapsed by
default; up/down highlight a cluster, left/right/Enter expand-collapse
it (tmux-style), PgUp/PgDn scroll overflow, Esc closes.
- docstring: example uses --model qwen2.5:3b (the locally-pulled model),
not llama3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes the cross-language half of token streaming (perf-plan A3). On the
CPU-only box perceived latency is time-to-first-token, so showing the reply
as it generates makes a slow model feel live.
- Agent: OllamaProvider.stream() runs on a worker thread; bridge relays
cumulative previews as throttled (~5/sec) `_ai:"stream"` control frames,
then a `done` frame clears the preview as the final persisted chat message
is posted. Providers without stream() fall back to blocking complete().
- Rust client: new Net::AiStream variant + parse_ai branch; App.ai_stream
map holds the in-progress text per agent; draw_chat renders it as a dim,
italic preview bubble below history. Cleared on done and on agent leave.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tier A/B/C wins for the CPU-only Ollama box (no GPU → optimize TTFT and
tokens/sec, not VRAM):
- Separate qwen2.5-coder provider for the sandbox `!task` path; chat keeps
the general model. Auto-selected when chat is Ollama and a coder build is
present, override with --code-model.
- OllamaProvider num_ctx default 8192→4096 (8192 was a GPU-mindset default
that inflates prefill/TTFT on CPU); expose num_thread; add --num-ctx,
--num-thread, --num-predict. token_budget default 3000→2000 to fit.
- OllamaProvider.stream() generator over Ollama's stream=True chat endpoint
(provider half of token streaming; agent/Rust rendering is a follow-up).
- Few-shot request→shell exemplars in SANDBOX_SYSTEM to anchor the small
model's fenced-command output.
- Matryoshka embedding truncation: OllamaEmbedder truncate_dim=256 (--embed-dim)
for faster pure-Python cosine and less RAM; query+stored share the dim.
- docs/ai-perf-plan.md records all 8 items with status and the server-side
env (OLLAMA_NUM_PARALLEL=1, keep_alive) that must be set where ollama serve runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Give the agent recall of things said beyond the verbatim window, without
breaking the RAM-only philosophy — nothing is persisted to disk.
- MemoryIndex: a capped, in-memory pool of embedded messages with pure-Python
cosine search (no numpy). Retains far more than the rolling transcript so old
lines can be surfaced on demand; oldest evicted past the cap to bound RAM.
- OllamaEmbedder: local embeddings via nomic-embed-text, on by default and
independent of the chat provider (reuses the Ollama host when chat is Ollama).
- Bridge: captured room messages (live + backfilled) are embedded on a
background worker so a slow embedder can't stall frame draining. On a /ai
question the agent retrieves top-k relevant lines, drops weak (<min_score) and
windowed-duplicate hits, and prepends them as a clearly-fenced "recalled
context" preamble — kept at user role, never elevated to system, so untrusted
room text informs without instructing. Falls back to recency-only if the
embedder is unreachable.
- CLI: --no-rag, --embed-model, --embed-host, --rag-top-k.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OllamaProvider now sends keep_alive (default 30m) so the model stays resident
in VRAM between /ai calls instead of cold-reloading, and sets explicit options
(num_ctx 8192, num_predict 512) — Ollama otherwise caps context at 2048, which
would silently truncate the larger backfilled window.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The server already ships the full RAM message backlog in the init frame; the
agent was discarding it. _seed_transcript now decrypts that history with the
room key (skipping our own lines, control frames, and undecryptable blobs) so
the agent has context the moment it joins instead of starting amnesiac.
_window() replaces the fixed last-12 slice on both the answer and sandbox
paths: it walks newest-to-oldest and keeps messages up to --token-budget
(approx, ~4 chars/token), still capped at --context-window count. Keeps small
local models inside their effective context. Nothing touches disk.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Roadmap for deepening the /ai agent's conversational context while keeping
the RAM-only philosophy, plus Ollama latency wins. Marks Tier 1 (backfill,
token-budget window) and the perf tuning as in-scope now; RAG and in-RAM
compaction staged next. Grounded in public Anthropic docs, not leaked source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Theme::random() conjures a fresh procedural vestment — a coherent HSV
palette (dark tinted surface, one bright accent, legible ink), a random
sigil, and a generated arcane name. Bound to Ctrl+Alt+P and `/theme random`.
Theme::save() persists the vestment you're wearing to themes/<slug>.toml
(via `/theme save [name]`), so a roll you like can be re-donned later with
`/theme <name>`. Theme now derives Serialize and slugify() sanitizes the
filename. Help text and the /theme usage line advertise both verbs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>