Commit Graph

9 Commits

Author SHA1 Message Date
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
d8acadd68b test: use-case coverage + end-to-end smoke test
- tests/test_coven.py: capacity cap (5th rejected, configurable), duplicate
  username, roster frame contents, slot/username freed on disconnect.
- tests/conftest.py: set app.ctx.max_users (fixes fixture vs new server code).
- hh/smoke.sh: one-command e2e — rust unit tests, SRP self-test, boot server,
  rust handshake round-trip, cross-language python decrypt of a rust message.
- hh: drop unused Session.user_id (clean build).

pytest: 85 passed. smoke: PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:15:04 -07:00
leetcrypt
651e7210b2 test(hh): stable python->rust fernet interop vector
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>
2026-05-30 14:01:03 -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
leetcrypt
bb1d662ee1 chore: rename project coven → hack-house ⛧
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>
2026-05-30 13:29:14 -07:00