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>
A saved random() roll: gold ink and steel-blue borders on a deep slate
surface. Self-registers via THEMES_DIR, so `/theme goldcrypt` resolves it
at runtime alongside the other bundled presets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agents can now run commands and build files in the shared sandbox, but
only when explicitly invoked with the `!` verb and only while the owner
has granted drive. Reuses the existing driver ACL + `_sbx:input` frames:
the Python agent emits the same input frames a human driver does, gated
by the broker's `app.drivers` check — no new transport.
Guardrails: a regex gate holds destructive commands until `/ai <name>
confirm`; blast-radius caps (20 cmds / 8KB); the agent echoes its plan to
the room before running (audit trail). Owner controls: `/grant`, `/ai
start <model> allow` to pre-grant on spawn, and a Ctrl-X panic kill
switch (revoke all non-owner drive + Ctrl-C the shell). The broker now
re-broadcasts the ACL on join so a freshly-summoned agent actually
receives its grant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reader funnels both chat and high-volume _sbx:data terminal frames
through one channel, and the UI loop redraws after handling a single frame
per turn — so on a viewer's side each chat message queued behind hundreds
of sandbox frames only surfaced one-per-redraw, making chat appear to
buffer/stall whenever a shared shell was scrolling output.
Drain a bounded burst (up to 256) of ready frames per turn via a new
drain_ready() helper, keeping chat latency bounded no matter how hard the
sandbox is streaming. Add regression tests covering FIFO/cap behavior and
chat surfacing within a few turns under flood.
Also add connect.sh: a join helper with a default port that keeps the room
password in RAM only (no-echo prompt or env var, never written to disk).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Outgoing frames were drained inline in the main select! loop with a blocking
sink.send().await. While a sandbox streams its PTY to the room, those _sbx:data
frames flood the socket; if the server backpressures (e.g. relaying to a remote
peer), each await stalled the loop, so keystrokes and incoming chat arrived in
laggy bursts. Hand the write half to a dedicated writer task; reconnect passes
it a fresh sink. Disconnects are still detected by the reader (Net::Closed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make the connect `user` arg optional. When omitted, the client prompts
"choose your handle" as the first thing on join (before the TUI opens) and
re-prompts if the server rejects the name (e.g. already taken, 409). Passing
a name on the CLI still works unchanged, so the demo script is unaffected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The help overlay now scrolls (↑/↓, PgUp/PgDn, Home/End) with a position
indicator and only Esc dismisses it, so stray keystrokes can't close a menu
that overflows the screen. Adds three bundled vestments (blush, matrix,
wraith); they're auto-discovered via Theme::available(), so they appear in
the menu and /theme list with no hardcoded entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bump from 960px/12fps to 1280px/15fps with floyd-steinberg dithering
for crisper, retina-legible terminal text — 7.4MB, under GitHub's 10MB
inline-render limit. Exceeds the upstream example.gif (800px/15fps).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.7MB looping GIF rendered from the latest demo capture (alice+bob
sharing a multipass box: summon, drive, per-user sudo).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make connecting any model a config step, not a code change:
- models.toml named profiles (api_key_env names an env var, never the key)
- providers gain available_models(); add preflight + --list-models/--check
- /ai list and /ai models in-room; client probes local Ollama for
/ai models when no agent is running, and /ai list hints to summon one
- docs/providers.md provider guide + examples/echo_provider.py
- README: command table, AI section, layout updated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keeps bootstrap.sh AI-free by default; bootstrap-ai.sh layers on the local
model runtime (transparent, opt-in install) for the /ai agent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Owner of the spawning client can summon/dismiss a local AI agent from inside
the room (default ollama/qwen2.5:3b); the agent emits encrypted typing frames
that drive a "thinking" spinner in the client.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add cmd_chat/agent: a headless client that joins a room via SRP, decrypts
broadcasts, and answers /ai <question> through a pluggable model provider
(ollama default + anthropic + openai-compatible + module:Class). Server and
zero-knowledge guarantees unchanged; the agent is just another encrypted client.
Also pin the lets-hack demo to a detached worktree of main (default) so running
it from dev still demos stable main without touching the working checkout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the stale Django CI template with a CI workflow that builds and
tests both codebases: cargo fmt/clippy/build/test for the hh client and
pytest across Python 3.10-3.12 for the server. Apply cargo fmt and fix
all clippy lints so the gates pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add brief-but-thorough sections for the shared sandbox (backends/isolation),
terminal driving, two-layer unix permission control, file/directory sharing,
live theme switching, reconnect/scrollback, and a configuration table. Lead
Quick start with bootstrap.sh and keep direnv autostart as a separate opt-in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Graceful shutdown: Ctrl+C quits in chat (interrupts PTY while driving),
RAII TermGuard + panic hook + SIGTERM/SIGHUP always restore the terminal
- Default theme is now "crypt" (neutral monochrome); theme sigil mirrored in
chat/roster/help so the pentagram only renders under the "church" theme
- Neutralize inverted-pentagram branding across CLI, scripts, docs, and Cargo
metadata (kept only in themes/church.toml + the render-time placeholder)
- Rewrite root README around hack-house; add bootstrap.sh, SECURITY.md,
CODE_OF_CONDUCT.md, CHANGELOG.md, and issue/PR templates
- .gitignore cleanup; stop tracking .venv
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add /pw (alias /password): reveal this room's password locally (never
broadcast); surfaced in the F1 help overlay and the join hint
- direnv-autostart/: cd-to-launch a single real-user session via direnv;
password is minted in memory at launch (never written to disk, matching the
RAM-only model) and scoped to the child process. setup.sh installs direnv,
hooks bash/zsh, and `direnv allow`s the dir
- lets-hack.sh: boot a FRESH server by default (replacing any live one) with a
--reuse opt-out; add -h/--help/-help; guard against killing the tmux session
you're attached to; switch-client into the coven when run inside tmux
- rename coven→clergy across rust/python/scripts; tests/test_coven.py→test_clergy.py
- snapshots in-progress hack-house client work (sandbox, themes, net, ui)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Centered modal listing every command, keybinding, and roster glyph. Opens with
F1 (desktop) or the /help command (phone-friendly, since F-keys aren't on the
Termux keyboard); any key closes it. Rendered with a Clear overlay so it floats
above the panes. Works from chat or drive mode; Ctrl-Q still quits.
9 tests pass; clean build; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Chat history: PgUp/PgDn (page), arrows (line when no sandbox), Home=oldest,
End=live. Viewport holds steady when new lines arrive while scrolled up;
sending a message jumps back to live. Backlog capped at 4000 lines.
- Sandbox terminal: vt100 parser now keeps 2000 rows of scrollback; ↑/↓ scroll
it when not driving (arrows still go to the shell while driving). Offset
applied each frame; reset on dismiss / End.
- Title indicators: 'chat ↑N (End=live)' and 'sandbox · ↑N scrollback'.
Termux's extra-keys row has arrows + PgUp/PgDn/Home/End, so it's phone-usable.
9 tests pass; clean build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The owner's keystrokes and terminal output used to round-trip through the
server, so output lagged (appeared only on the next keypress) and Ctrl-C got
queued behind a flood of outgoing output frames (e.g. 'tree') — so it never
interrupted and the command seemed to hang.
- Owner writes drive keystrokes straight to the local PTY (instant; Ctrl-C is
never starved). Remote drivers still relay via the server.
- Owner renders its sandbox locally from the PTY and ignores its own echoed
data/status frames (broker.is_none gate); others still render from echoes.
- Coalesce PTY output bursts into one frame (no flood).
- select! is biased on keyboard input; tick 120ms -> 50ms for snappier redraws.
Verified live: echo renders with no extra key; sleep+Ctrl-C interrupts cleanly.
9 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F2 is unreachable on phone/Termux keyboards, so add a /drive chat command to
enter sandbox drive mode (type into the shared shell; Esc releases). F2 still
works on desktop. Esc no longer quits from chat mode (footgun on mobile) — quit
is Ctrl-Q only. Updated on-screen hints + sandbox pane title.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Default vestments (was monochrome 'crypt') now match the Church of Malware
aesthetic: neon on black — cyan window-chrome borders, acid-green text/prompts
and your own messages, soft-cyan for others, purple system/occult lines,
hot-magenta self/owner. themes/church.toml ships the same palette; crypt.toml
(monochrome) and neon.toml remain as alternates via --theme.
Confirmed ratatui's serde accepts #rrggbb (hex --theme files load). 9 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Linux-style user permissions inside the sandbox (the original superuser ask):
- Backends are now persistent (docker run -d + exec; multipass instance) so the
broker can provision accounts and run the shell as a chosen user.
- sbx::provision(): create a real unix account per coven member at launch; the
OWNER becomes a passwordless superuser (sudo group + /etc/sudoers.d NOPASSWD
drop-in on multipass). The shared shell runs as the owner's account.
- /sudo <user> and /unsudo <user> (owner-only): real usermod + sudoers.d in the
VM — delegate/withdraw superuser. ACL frame carries sudoers; roster shows
⛧ owner · ⚡ sudoer · ◆ driver · • member.
Verified live on a real Multipass VM: shell runs as owner@vm with
'sudo -n whoami' == root; '/sudo member' gives member 'NOPASSWD: ALL';
teardown purges the instance. Docker provisions accounts + persistent
container (shell as root; sudo pkg absent so drive-grant is the delegation).
Tests: 7 cargo tests pass; clean build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire-compatible with the Python client's _ft protocol (offer/accept/reject/
chunk/done, 64KB chunks, SHA-256 verified), over the encrypted channel:
- ft.rs: read_payload (file or tar'd directory), save/extract with a zip-slip
guard (relative-only, no .. / absolute; unpack_in double-checks), SHA-256.
- /send <file> and /sendd <dir>; receiver sees an offer banner, /accept or
/reject; chunks stream in the background and the result is verified + saved
to ./downloads (directories extracted in place).
- Refactor: all outgoing ws frames now funnel through an mpsc channel drained
(batched) by the run loop, so the background chunk-sender and the PTY relay
transmit without owning the socket.
- ui.rs: pending-offer banner on the input bar.
Tests: 7 cargo tests (file + dir tar round-trip, traversal guard, + crypto/sbx).
Verified live: two TUIs, file and directory transfer, SHA-256 verified, saved.
Note: dropping accepted files into the active sandbox VM dir is a future add-on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
App-level RBAC over the single shared PTY, enforced by the broker:
- The sandbox launcher becomes owner (superuser) and first driver; broadcasts
an encrypted {"_perm":"acl",owner,drivers} frame all clients track.
- /grant <user> and /revoke <user> (owner-only) delegate/withdraw drive rights
= delegating control of the shared (root) shell — the superuser-delegation ask.
- The broker honors {"_sbx":"input"} only from permitted drivers, keyed on the
SERVER-AUTHENTICATED sender (the message username the Sanic session stamps),
not a spoofable self-asserted field — closes the spec's identity-binding gap.
- F2 is gated: non-drivers get 'ask the owner to /grant you'; revoke drops drive
live. Roster shows roles: ⛧ owner · ◆ driver · • member.
Verified live (two TUIs): member blocked pre-grant, owner /grant member, member
then drives a command in the sandbox; roster + permission messages all correct.
cargo test: 4 pass.
Note: per the single-shared-PTY decision, drive-grant *is* the permission model;
per-user unix accounts/sudo would need per-user shells (future mode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sbx.rs: prepare()/teardown() — multipass launch (idempotent, reuses an
existing instance) on /sbx launch multipass, delete --purge on stop;
Backend::default_image() per backend (24.04 / ubuntu:24.04).
- app.rs: async non-freezing launch — prepare runs in spawn_blocking and the
sandbox handle is handed to the run loop via a channel, so a ~30s multipass
VM boot never freezes the UI (status: "summoning…"). Sandbox is sized to the
actual pane (not fixed 24x80); broker resizes the PTY and broadcasts
{"_sbx":"resize"} on terminal-size changes; clients set their vt100 size to
match. Teardown on /sbx stop and on exit.
- net.rs: parse status rows/cols + resize frames.
Verified: cargo test (4 pass), clean build, live local sandbox via the async
path with dynamic full-width sizing. multipass 1.16.3 present; VM-boot path
implemented (live VM verify is slow, runs the same async flow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collaborative sandbox over the same zero-knowledge encrypted channel:
- sbx.rs: SandboxBackend (Local / Docker / Multipass) spawning a shell in a PTY
(portable-pty); reader thread pumps output to the broker.
- Broker (owner's client): /sbx launch [backend] [image] boots the sandbox and
relays PTY output as encrypted {"_sbx":"data"} frames; /sbx stop tears down.
PTY input arrives as {"_sbx":"input"} frames and is written back.
- All clients render the shared terminal from data frames via a vt100 parser;
F2 toggles drive mode (keystrokes -> input frames, incl. Ctrl-C); esc releases.
- ui.rs: sandbox pane (split below chat) with drive indicator.
- Server stays zero-knowledge: PTY bytes are Fernet-encrypted like chat/files;
the VM runs on the initiator's client, never the server.
Tests (cargo test, 4 pass): PTY I/O round-trip + headless end-to-end relay
(PTY -> _sbx frame encode -> decode -> vt100 screen shows command output).
Note: Multipass assumes the instance is launched separately (lifecycle = P3b);
per-user unix accounts + sudo delegation = P4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pin the cross-language fernet regression test to a fixed key+token (server-
independent) instead of a session token. Confirms rust decrypts python-encrypted
fernet; the live-chat path was verified end-to-end in the TUI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Connect subcommand: SRP auth then a ratatui UI over tokio + crossterm.
- Async ws (tokio-tungstenite); reader task decrypts/parses frames into events.
- Panes: top bar (e2e + house N/cap), chat scrollback, roster (self marked ⛧),
input box. Undecryptable frames surface as a system line, not a silent drop.
- Themes (vestments) via TOML --theme; default occult-monochrome + neon.
- Verified live in tmux: render, chat round-trip, roster, join/leave.
- Adds fernet python->rust interop regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Begin the coven evolution of cmd-chat (see docs/spec-collaborative-sandbox.md):
a Rust/ratatui client for the unchanged Python Sanic server, plus the
multi-user + zero-knowledge groundwork.
P0 — crypto parity (the spec's #1 risk), proven three ways:
- Hand-rolled SRP-6a (NG_2048, SHA-256, rfc5054 padding) matching pysrp
byte-for-byte, incl. the fixed b"chat" SRP identity and minimal-vs-256B
width quirks. Golden-vector unit test + offline selftest.
- Live handshake against the running server (H_AMK verified).
- Cross-language E2E: Python client decrypts a Rust-encrypted Fernet message.
P2 — multi-user coven (server):
- CMD_CHAT_MAX_USERS capacity cap (default 4, infra-for-more).
- Authoritative roster + user_joined broadcasts.
- Free the slot/username on ws disconnect (was held until 1h stale sweep).
Also: fix requirements.txt (was UTF-16, unparseable by pip).
coven/ : Rust crate (crypto.rs proven; main.rs spike CLI: selftest/handshake/srpm)
docs/ : full feature spec for the 6 requested features.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New commands: /send <filepath>, /accept, /reject
Protocol:
- Sender proposes file (name, size, SHA-256 hash)
- Recipient sees offer and chooses /accept or /reject
- On accept: file chunked (64KB), encrypted with room key, sent over WebSocket
- On receive: chunks reassembled, SHA-256 verified, saved to ./downloads/
- Server never sees file content (E2E encrypted, same as messages)
Limits: 50MB max file size. Files saved with collision-safe naming.
No server changes — server remains a dumb encrypted relay.
All 79 existing tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detects all available IPs (Tailscale, LAN, public), prints connect
command for friends to copy, prompts for password securely via getpass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- lab/setup-lab.sh: automated tmux setup with server + 2 chat clients
Supports --no-tls, --password, --port, --user1/--user2, --teardown
Auto-installs missing pip dependencies, verifies port availability,
waits for server health before connecting clients
- lab/README.md: usage docs and keyboard shortcuts
- requirements.txt: fixed UTF-16 encoding to UTF-8, cleaned pinned versions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CRITICAL fixes:
- Auto-generated self-signed TLS certs (HTTPS/WSS by default)
- Removed session_key from /srp/verify response (was sent in plaintext)
- Replaced with HMAC-SHA256 ws_token for WebSocket authentication
HIGH fixes:
- WebSocket auth now validates ws_token via hmac.compare_digest()
- /clear endpoint requires Bearer admin_token (printed at server start)
- Password no longer required as CLI arg — supports env var + getpass prompt
- Removed user_ip from Message model (no longer broadcast to clients)
MEDIUM fixes:
- Rate limiter on /srp/init and /srp/verify (10 req/min/IP)
- MessageStore capped at 1000 messages (prevents RAM DoS)
- access_log disabled (was leaking request metadata)
LOW fixes:
- Username sanitization against rich markup injection
- Dead code removed from helpers.py
All 79 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>