Commit Graph

17 Commits

Author SHA1 Message Date
leetcrypt 750692b6aa test(client): interop-gate the pure SRP shim against a real server
SRP fails silently on any constant/hash mismatch, so gate the shim with a live
handshake: (1) pure User vs the real srp.Verifier, (2) a negative wrong-password
control, and (3) the full /srp/init -> /srp/verify HTTP flow against the real
server routes (backed by the real srp lib). All assert the phone operator
authenticates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-07 15:06:15 -07:00
leetcrypt 0798db7941 feat(operator): harness-mode operator — drive a room with any function-calling model (P2)
The operator brain was Claude-only: spawn launched the claude CLI and let its
own loop drive the room. This adds the other first-class brain — a native
tool-calling loop (cmd_chat/operator/harness.py) that runs ANY function-calling
Provider (cmd_chat.ai) against an OPERATOR_TOOLS schema wired to the existing
bridge control verbs (say/exec/write/get/keys/screen/watch/manifest). No CLI
install, no creds carry.

The harness is a driver, not a new side-effect surface: every tool handler sends
the same control-socket request the hh-bridge CLI already sends, so the bridge
keeps enforcing grant-before-drive, the sandbox blast radius, and the recursion
budget. Layer-1 capabilities come from CAPABILITIES.md (the same portable
contract), and a per-agent token ceiling is enforced as a hard stop. Providers
that can't function-call are refused with a clear message rather than degrading
to prose.

New `operate` verb joins via the existing daemon socket and runs the loop with a
--profile or --provider/--model brain. Chat's native loop is left untouched
(self-contained harness over the shared Provider core, not a risky extraction).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-28 23:23:17 -07:00
leetcrypt 2091bcefed feat(operator): portable CAPABILITIES.md as the single Layer-1 prompt (P4)
The operator's capabilities contract was Claude-only — delivered via the
hh-operator skill, which non-Claude runners cannot load. Extract it into a
portable CAPABILITIES.md and make compose_directive runner-aware: the claude
runner still loads its skill, every other runner gets the capabilities prompt
inlined. Same contract, no skill machinery required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-28 23:15:56 -07:00
leetcrypt 09cea40494 feat(operator): runner registry — spawn any agent CLI, not just Claude (P3)
Generalize the three Claude-hardcoded spots in bootstrap (launch argv,
install plan, creds/config-dir) behind a Runner registry so a nested
operator can be spawned with any agent CLI. The Claude path stays the
default and byte-identical.

- bootstrap.Runner + RUNNERS{claude,codex,gemini,cmd} + get_runner/runner_present
- build_run_argv / install_plan / creds_source / plan_creds / child_env all
  take an optional `runner` (default claude → unchanged behaviour)
- claude validated end-to-end; codex/gemini are best-effort defaults
  overridable via $HH_<RUNNER>_INSTALL etc.; generic `cmd` runner reads a
  full launch template from $HH_OPERATOR_CMD ({directive} placeholder)
- bridge _op_spawn resolves req["runner"], rejects unknown; plan now carries
  runner/runner_present (was claude_present)
- CLI: `spawn --runner {claude|codex|gemini|cmd}`
- 7 new unit tests; full suite 132 passed

Note: compose_directive still emits the Claude-flavoured ("hh-operator
skill") directive — the portable Layer-1 prompt is P4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-28 23:11:41 -07:00
leetcrypt 19d5b80a2c feat(operator): hh-agent manifest — VMs carry a self-describing handoff record
Add ICM-aligned agent-manifest tooling so a hack-house sandbox/VM can be
shared, traded, and resumed without a human briefing. manifest.py is a
dependency-light (stdlib + optional PyYAML) library + CLI that writes a
`.hh-agent/` bundle: manifest.yaml (canonical machine record) plus rendered
AGENT.md / last-state.md / summary.md / goals.yaml views.

Wire a `manifest` op into the operator bridge (push/pull/update) that moves the
bundle in and out of the *target sandbox* via the same exec path as write/get,
so the VM itself carries its purpose, goals, user intent, live state, and a
provenance chain — the unit of agent-to-agent work transmission. Pairs with
`spawn`: stamp → push → hand a child "load the .hh-agent manifest and continue."

Proven end-to-end against a real Kali podman container (push → on-disk verify →
update → pull-back) and covered by 6 new offline tests (34 green total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 20:28:48 -07:00
leetcrypt 338e843e85 feat(operator): Phase 6 — relay-mode read (screen) + stop-condition engine (watch)
Completes the remote-drive triad for a broker-owned sandbox the operator can't
exec into directly: keys (in) → watch (wait) → screen (out).

- _sbx:data PTY-relay frames are absorbed into a capped rolling terminal buffer
  (not surfaced as chat); `screen` returns it ansi-stripped (or raw). sandbox.
  strip_ansi handles CSI/OSC/CR noise so output greps cleanly.
- `watch` is a formal stop-condition engine: blocks until a regex matches in the
  screen buffer or chat events, or an idle-quiescence window, or a hard timeout,
  then reports which fired — the autonomy loop's principled wait.
- Skill doctrine updated with the type→wait→read relay loop.

28 offline tests green.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 20:13:08 -07:00
leetcrypt 60215bbdd4 feat(operator): Phase 5 — bootstrap + budgeted recursive spawn
Lets an operator stand up a nested Claude Code operator against another room —
a tree of operators — behind hard guardrails:

- Budget(depth, fanout, cost): depth/fanout are hard caps decremented on every
  descend(); a leaf (depth 0) or a spent level (fanout 0) refuses to spawn,
  stopping a runaway tree. Cost is carried down as a soft ceiling.
- detect_system / install_plan: decide install from what's present; prefer the
  documented npm package `@anthropic-ai/claude-code` or a $HH_CLAUDE_INSTALL
  override — never a guessed URL.
- Credentials are gated OFF by default: a child authenticates itself unless the
  operator explicitly passes allow_creds, and only its own creds, only to a path
  it was handed.
- compose_directive bakes the objective, stop conditions and inherited budget
  into the child's `claude -p` prompt so it self-limits.

`spawn` op + CLI verb default to a dry-run plan (inspect the tree before growing
it); `--go` launches detached on the host. `up`/`serve` take --depth/--fanout/
--cost. 24 offline tests green; dry-run plan verified live through the socket.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 13:38:42 -07:00
leetcrypt 9c1e2921e9 feat(operator): Phase 2 — sandbox drive (exec/write/get) + keystroke relay
Co-located mode: the operator launches its own podman/docker container and
execs into it out-of-band (argv-identical to the native harness), exposed as
`sbx launch|status|down`, `exec`, `write`, `get`. When granted, it can also
drive the room's broker-owned container directly on the same host.

Relay mode primitive: `keys` injects raw bytes into the shared PTY via the
same `_sbx:input` frame a human driver emits — full keyboard control incl. the
stop-vocabulary (ctrl-c/ctrl-d/ctrl-z/ctrl-\, esc, arrows, pager q). A compact
per-session cheat-sheet (`sandbox.KEYS_HELP`, `keys --help-keys`) documents
what each inject does and how to end a stuck program, token-efficiently.

Gating: exec/write/get refuse a host (`local`) inherited from the room; the
room sandbox is only driven when `granted`. Keystrokes are inert until granted.

17 offline tests green; e2e verified against real podman (launch→exec→write→
get round-trip→teardown) and through the CLI socket.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 12:34:24 -07:00
leetcrypt 7e81a059b9 feat(operator): Phase 1 — Claude-driven room bridge (join/read/say)
Invert the agent model: a headless OperatorBridge(Client) owns the
websocket while a Claude Code session drives it via a unix control
socket. Reuses Client SRP/Fernet and AgentBridge's reconnect/serve
shape. Ships an hh-bridge CLI (up/read/say/roster/status/down) with a
seq'd in-RAM inbox + asyncio.Condition long-poll (read --wait) for
in-turn autonomy. ACL/sandbox-status frames are recorded for later
phases. Sandbox drive, delegation and nesting are out of scope here.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 12:19:47 -07:00
leetcrypt 3a54578f0a perf(net): near-real-time sandbox for non-host viewers
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>
2026-06-06 17:11:51 -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 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 e7bacc93da fix(security): comprehensive security hardening — TLS, HMAC WS auth, rate limiting, IP leak prevention
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>
2026-05-25 20:30:40 -07:00
mirai 467d942877 New storage scheme 2026-01-06 21:42:50 +08:00
mirai 8b58bf4db3 Final notes [2] 2026-01-02 23:35:20 +03:00
mirai 0756aab53f Final notes 2026-01-02 23:25:02 +03:00
mirai 5cbe355660 feat: add SRP authentication, improve security
- Replace RSA key exchange with SRP (Secure Remote Password)
- Password never transmitted over network
- Add unit tests for endpoints
- Fix datetime.UTC compatibility for Python < 3.11
- Fix logger.exception usage
- Update README with new auth flow diagram
2026-01-02 23:09:00 +03:00