Commit Graph

36 Commits

Author SHA1 Message Date
leetcrypt 37d5916b2e feat(layout): fullscreen + interactive pane resizing
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
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>
2026-06-07 12:53:44 -07:00
leetcrypt c9cdd2feca feat(sbx): install Docker/Multipass on demand from /sbx launch
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
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>
2026-06-07 00:35:19 -07:00
leetcrypt 764c827d07 fix(ux): forward Esc and Ctrl-X to the sandbox shell while driving
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
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>
2026-06-06 22:53:45 -07:00
leetcrypt b8a06077a4 fix(sbx): make /sbx save work on a live multipass sandbox
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
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>
2026-06-06 22:38:55 -07:00
leetcrypt 6160d957f5 feat(sbx): create fresh VirtualBox VMs via cloud-init (/sbx launch vbox new)
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
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>
2026-06-06 22:33:37 -07:00
leetcrypt d448314e5e feat(sbx): VM dev-toolchain bootstrap + save/load parity across backends
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>
2026-06-06 22:29:24 -07:00
leetcrypt 7624cb04f7 feat(ux): F1 toggles help, --user flag, connect.sh fresh-build
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
- 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>
2026-06-06 21:51:54 -07:00
leetcrypt 98e202e0fe feat(sbx,ft): share VirtualBox VMs — vbox launch grammar, picker & streaming transfer
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>
2026-06-06 19:45:45 -07:00
leetcrypt 1fa8c332ed chore: consolidate all shell scripts into hh/scripts/
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
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
leetcrypt 9222fa8ad7 feat(ai): name agents after their model, not hardcoded "oracle"
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>
2026-06-06 17:26:17 -07:00
leetcrypt d6d44128c0 feat(sbx,ai): snapshot all backends w/ local export; robust AI + rejoin
- /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>
2026-06-06 16:47:57 -07:00
leetcrypt 2c4a4f9a22 harden(ft,auth,net): cap transfers/frames, evict stale SRP, distrust XFF
CI / rust client (hh) (macos-latest) (push) Has been cancelled
CI / rust client (hh) (ubuntu-latest) (push) Has been cancelled
CI / rust coverage (push) Has been cancelled
CI / python server (3.10) (push) Has been cancelled
CI / python server (3.11) (push) Has been cancelled
CI / python server (3.12) (push) Has been cancelled
CI / headless e2e smoke (push) Has been cancelled
CI / dependency audit (push) Has been cancelled
CI / secret scanning (push) Has been cancelled
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>
2026-06-05 06:59:16 -07:00
leetcrypt 5676216a2f feat(ui): stacking role badges in roster + chat; drop default pentagram
- 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>
2026-06-04 22:56:10 -07:00
leetcrypt 7519df1695 feat(sbx): VirtualBox detect-first install + local GUI VM launch
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>
2026-06-03 10:41:32 -07:00
leetcrypt 07e9c30846 feat(sbx,ui): VM snapshot save/load + collapsible clustered help menu
- /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>
2026-06-02 23:03:00 -07:00
leetcrypt 69bce5ead8 feat(ai): stream agent replies token-by-token to the room
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>
2026-06-02 22:42:08 -07:00
leetcrypt 00c1f1c3c9 feat(theme): add theme randomizer and save-to-disk
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>
2026-06-02 17:39:37 -07:00
leetcrypt 47019dd630 feat(ai): let agents drive the sandbox on request (/ai <name> !<task>)
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>
2026-06-02 16:42:24 -07:00
leetcrypt 9158a488f7 fix(ui): batch-drain incoming frames so a sandbox stream can't stall chat
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>
2026-06-02 14:20:40 -07:00
leetcrypt ea67796551 fix(ui): move websocket writer off the UI loop to stop input/chat lag
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>
2026-06-01 23:04:59 -07:00
leetcrypt b4c5f9a9fa feat(ui): scrollable help menu, Esc-to-close, + blush/matrix/wraith themes
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>
2026-06-01 22:09:08 -07:00
leetcrypt 65df12de9e feat(ai): model profiles, capability discovery, and agentless /ai list|models
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>
2026-06-01 15:25:07 -07:00
leetcrypt 05bdc2d802 feat(ai): /ai start|stop agent control + in-room typing indicator
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>
2026-06-01 11:38:15 -07:00
leetcrypt 8eacf4d27b ci: proper Rust+Python CI workflow; cargo fmt + clippy clean
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>
2026-06-01 00:52:20 -07:00
leetcrypt ff5186a9d3 feat(hh): graceful shutdown, crypt default theme, neutralize branding, share-prep
- 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>
2026-05-31 23:23:19 -07:00
leetcrypt 5de493e895 feat(hh): /pw command, RAM-only direnv autostart, robust lets-hack; coven→clergy
- 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>
2026-05-31 22:29:17 -07:00
leetcrypt 8e6365a649 feat(hh): help overlay (F1 / /help)
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>
2026-05-30 23:20:08 -07:00
leetcrypt 51bc85e078 feat(hh): scrollback for chat + sandbox terminal
- 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>
2026-05-30 23:10:36 -07:00
leetcrypt d6595935d3 fix(hh): instant sandbox render + working Ctrl-C for the owner
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>
2026-05-30 22:10:18 -07:00
leetcrypt 1445b1151a feat(hh): /drive command (mobile-friendly) + Esc no longer quits
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>
2026-05-30 22:03:00 -07:00
leetcrypt 0e269afce7 feat(hh): real VM unix users + sudo delegation ⛧
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>
2026-05-30 19:51:40 -07:00
leetcrypt 1dfc614cc5 feat(hh): P5 — file & directory uploads ⛧
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>
2026-05-30 19:39:55 -07:00
leetcrypt f73c23bf57 feat(hh): P4 — permissions (owner/superuser + drive delegation) ⛧
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>
2026-05-30 16:41:34 -07:00
leetcrypt d8018cbe2a feat(hh): P3b — multipass lifecycle + PTY resize sync
- 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>
2026-05-30 16:34:08 -07:00
leetcrypt 232a00cc9e feat(hh): P3 — summonable sandbox + shared PTY ⛧
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>
2026-05-30 14:26:14 -07:00
leetcrypt 14aa369fb2 feat(hh): ratatui TUI client — chat, live roster, themes
- 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>
2026-05-30 13:57:07 -07:00