cmd_chat/__init__.py eagerly imported run_server (sanic/pydantic) and Client,
so any `cmd_chat.*` import — including `python -m cmd_chat.operator` — pulled
the server stack at package init. On a phone/Termux operator that installs only
the operator deps (and where srp may be unbuildable), this failed before the
client's pure-SRP fallback could help.
Make both imports lazy inside main()'s serve/connect branches. The operator
path now imports cleanly with srp/sanic/pydantic all absent (client falls back
to _srp_pure). Add a subprocess regression test to keep it that way.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap `import srp` so a missing C-extension (e.g. Termux/aarch64) transparently
loads _srp_pure as srp. No call-site changes — the shim mirrors the srp API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `srp` PyPI package is a C extension with no reliable aarch64 wheel and
often fails to build under Termux. Vendor _srp_pure.py, a stdlib-only port of
srp._pysrp with rfc5054_enable() active, exposing the exact client surface
client.py uses (rfc5054_enable, SHA256, User with start_authentication /
process_challenge / verify_session / authenticated).
Constants (N/g/k), x/u derivation, M1 and H_AMK match srp byte-for-byte —
verified by a live handshake of this pure User against the real srp Verifier
(the same backend the server runs).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rarity was inflating to 65% Legendary: the loop mass-produces ~70 near-clone
VMs that look near-identical on every axis, so no absolute PowerScore threshold
can create scarcity within that cluster. Fix (option C, hybrid):
- grade_curve() is now the primary rarity mechanism — a population quota
(Leg 8 / Epic 15 / Rare 25 / Uncommon 27 / Common 25%) ranked by PowerScore
with a deterministic label-seed tiebreak. Yields a clean 8/15/26/27/24%
pyramid. Rebuilds each card's rarity-dependent name + flavor via _apply_rarity.
- Absolute RARITY_TIERS kept as a single-card (--label) fallback, recalibrated.
- Dead axes repaired: _pedigree now derives from author lineage (human/hh-loop/
pulled/smoke) instead of a flat 2.0; _heft rescaled to a 512MB log reference
so it spreads instead of pinning mid-size VMs at the cap.
cardimg: apply grade_curve to the gallery so rendered cards match hh-cardex,
plus the interactive 3D flip-card gallery (--flip: front dossier / back stats).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Grow the hh card-art library so common VM types stop reusing one portrait.
- scripts/imgharvest.py: keyless DuckDuckGo image-search harvester — downloads
many stills per character and runs them through charvest's crop/quality/dedup
pipeline (static art crops far cleaner than motion-blurred video frames).
- scripts/harvest_roster.sh: batch roster (Pokemon/Digimon/Yu-Gi-Oh) sized to
the live VM card types. Library now 37 characters / 99 portraits.
- charvest: top-anchor the head+shoulders crop for tall full-body art so faces
are no longer clipped.
chardex matcher — stop same-type VMs collapsing onto a single face:
- fold a per-(VM,character) hash jitter INTO the score, not just a tie-break
- diminishing returns on extra type matches so a strong single-type face can
compete with a dual-type one
- halve tier affinity and cap the keyword bonus
- stop-word rarity/boilerplate words that were polluting keyword overlap
Result: 20 distinct faces across the library. The one remaining cluster
(Sigilyph on the Psychic/Flying recon-scanner VMs) is coherent purpose-
matching — a scanner-themed bird on the scanner VMs — not a bug.
NOTE: assets/characters/ are third-party franchise IP with NO established reuse
license — quarantined, internal WIP only, do not distribute/merge/publish
(licensing record in docs/character-art-licensing.md).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a status field to Card (from the registry entry) and render it as a
done/in-progress pill on the card art. Add `cardimg --gallery`: render every
VM as an inline-SVG card and lay them out in one self-contained index.html
grid (sorted by rarity, with a spread tally) for browser viewing / screenshot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Retune cardex against the live 24-VM library: the old rubric tied 11 VMs at
615 Rare (completeness/reusability maxed for any done+shareable VM, only heft
varied). Rebalance the five sub-score caps (richness 20→30 becomes the
discriminator via per-tag CAPABILITY_WEIGHT; heft log2-scaled) and recalibrate
the rarity thresholds. Library now spreads 2 Legendary / 3 Epic / 16 Rare /
2 Uncommon / 2 Common instead of a single fat Rare band.
Add cmd_chat/cardimg.py: render a Card as a collectible trading-card image.
Free + local-first — a hand-built SVG frame (rarity holo, type badges, 6 stat
bars, dex#, flavor) plus a deterministic procedural creature sigil that needs
no network, rasterized to PNG via local cairosvg/ImageMagick. Pluggable art
backends are opt-in: free hosted Pollinations flux, and key-gated paid
Stability / OpenAI / RunwayML. Every backend only fills the portrait window,
so a card always renders fully offline; failures fall back to the sigil.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deterministic value model over the host VM registry: a weighted five-axis
rubric (completeness/reusability/richness/pedigree/heft) yields a PowerScore
0-1000, an absolute-threshold rarity tier, a 6-stat block, an elemental type
from tags, and a stable procedural name + dex number. Pure stdlib, no model,
no network — same VM always mints the same card. Image pipeline lands next.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
Move providers.py + profiles.py from cmd_chat/agent/ to a shared
cmd_chat/ai/ package so the operator bridge and the /ai chat agent can
consume one model-agnostic Provider core (groundwork for harness-mode
operators). Pure refactor — no behaviour change.
- cmd_chat/ai/{providers,profiles}.py: the canonical modules (moved verbatim)
- cmd_chat/ai/__init__.py: re-exports the public API
- cmd_chat/agent/{providers,profiles}.py: thin back-compat shims re-exporting
from cmd_chat.ai (keeps `from cmd_chat.agent.providers import …` working,
e.g. hh/scripts/bench-native-harness.py)
- internal agent consumers (memory/bridge/__main__/__init__) point at cmd_chat.ai
125 tests pass; shim identity verified (re-exports are the same objects).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase A: host-global VM registry (~/.hh/registry.json) joining opaque
snapshots to their .hh-agent manifests. New src/registry.rs (serde_json),
one Entry per saved VM with cached purpose/status/todo scraped from the
live container at save time. Reconcile-on-read self-evicts pruned images.
/sbx browse TUI lister + operator-side `registry list|show` reader.
Phase B: trading/skill VMs. Entry gains shareable/tags/share_path +
publish/get/list_shareable helpers. New TUI verbs /sbx publish <label>
[tag...], /sbx catalog @user, /sbx pull @user <label>. Wire protocol adds
_sbx:catreq/catalog/pullreq frames (parse_sbx). Receiver auto-loads a
received hh-snap-*.tar, reads the in-image manifest, and self-registers
(kept shareable for re-trade). Reuses the existing E2E /send + ft.rs
streaming transport.
podman gotcha fixed: `podman load` prints registry-qualified
`localhost/hh-snap:<label>` vs docker's bare tag — parse_loaded_tag now
finds the hh-snap: marker anywhere in the line (unit-tested both shapes).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
Phase 4 (final) of the native-harness working-memory sprint. OllamaProvider
.complete_with_tools now returns (text, calls, usage), surfacing the response's
real prompt_eval_count/eval_count (free — already in the payload). The native loop
EMA-smooths real/estimate into self._tok_ratio (clamped [0.5,3.0]) and prunes
against native_token_budget / ratio, so context budgeting tracks the TRUE window
instead of the systematic bias of the len//4 char estimate. Providers that omit
counts leave the ratio at 1.0, so behaviour is unchanged where unavailable — a
free correctness win, no regression. RAM-only, no disk, no new network frames.
Scope note: touches only cmd_chat/agent/ + providers — disjoint from the parallel
feat(operator) work on this branch, so it merges/reverts independently by path.
Sprint: native-harness-working-memory (Phase 4/4) — see also 46e5620, dc6317f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 3 of the working-memory sprint. _prune_native_messages now compacts in two
stages instead of only evicting whole turns: Stage 1 digests OLD tool-role outputs
to a one-line summary (exit marker + first error line, else first line) via the new
_digest_tool_output; Stage 2 falls back to oldest-first whole-message eviction only
if still over budget. Tool outputs are the biggest context hog, and digesting keeps
the action->result causal chain intact, so whole-turn eviction (which severs it)
becomes a last resort. The pinned head/TASK and the recent keep_recent window
(including the most recent tool output, verbatim) are still never touched.
Return is now (messages, dropped, digested); the sole caller logs both. Clean-room
counterpart to Goose's tool-output condensation track. RAM-only, no disk.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Phase 1+2 of the native-harness working-memory sprint. All per-task state is
process RAM only (dies with the task, same lifecycle as MemoryIndex) — no disk,
consistent with the agent's encrypted-transmission / nothing-saved posture.
Phase 1 — _WorkSet dataclass holds what the loop kept re-deriving: sandbox
cwd/shell, files written/read, a failure ledger (cmd -> exit+category), and the
last good command. Discovered cwd/shell carry across tasks in-process via
self._sbx_known (RAM fallback grounding). _render_workset re-surfaces this into
the repair-turn system prompt so it survives context pruning without a NOTES.md
on disk. Folds the old reads_seen set into wset.files_read.
Phase 2 — semantic stuck/loop detection via _action_signature (run_shell keys on
the command, write_file on path+content-hash so real edits aren't repeats,
read_file on path). Aborts honestly when an action fails >=2x verbatim (model
ignoring REPAIR_STANCE) or the same (action,outcome) repeats >=3x, instead of
burning the turn cap re-running a dead action. Verified fix-and-retry does not
false-trip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorder _CODER_MODELS to prefer the 3b coder build over 1.5b. The 3b roughly
doubles the ground-truth pass rate on the verify-then-repair native harness
(bench: 7/9 vs ~4/9 over the 9 non-net tasks) at a modest CPU-latency cost,
so it is auto-selected ahead of 1.5b when present. 7b was evaluated and
rejected: too slow to first-token inside the engage window on the CPU-only box.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two clean-room reimplementations layered onto the native `!task` loop
(`_run_native`), aimed at lifting a weak CPU-bound local model's autonomous
pass rate. No code copied from the GPL sources studied; MIT throughout.
NightShift-derived verify-then-repair gate:
- `_classify_failure` maps a failing tool result to a (category, fix-hint) so
the repair nudge names a concrete cause/next-action instead of "exited N".
- `_relevant_excerpt` keeps the error-relevant tail of a FAILING run_shell
result within the byte budget (the real error is usually at the tail).
- read-dedupe guard short-circuits repeated idempotent `read_file` of a path
already read this task.
Exoshell-derived context discipline:
- `_prune_native_messages` budgets the whole message list (~chars/4) and
evicts oldest removable turns first once over `native_token_budget`,
pinning index 0, the TASK_MARKER goal, and the most-recent turns — the
native loop previously grew unbounded, silently pushing the goal out of a
small model's window on long repair runs.
- TASK_MARKER labels the goal so it is never pruned and re-anchors the model.
- REPAIR_STANCE is appended to the turn system prompt after the first failure
to swap the whole turn into a diagnose-then-act posture.
Validated on qwen2.5-coder:3b: clean unstitched 7/9 (the local ceiling), no
regression vs baseline; unit-tested pruning (pin survival, oldest-first
eviction, under-budget no-op) and stance trigger. The two remaining fails are
exact-match correctness tasks (a count, a fibonacci string), not harness gaps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The first harness changes to move the benchmark off the 1/12 noise floor, on
the model that needs it most (qwen2.5:0.5b: 1/12 -> 4/12, 3/12).
- complete_with_tools now decodes the tool loop at temperature 0 (scoped; chat
keeps default sampling). At Ollama's default 0.8 the weak model sampled away
from the tool-call format into prose/fabrication; the nudge prompt changes
between turns so temp 0 still escapes a failed state on retry.
- Greedy decode made 0.5b's leak deterministic, exposing its real shape: not a
JSON object with a name key, but the name in a <tools> tag and the args in a
SEPARATE object — <tools>write_file</tools>{"path":…} — ~5 of 12 tasks/run.
_NAMED_TAG pairs the tag-name with the following args object, gated on the
known tool set so it still can't fabricate an action.
- Bridge recovers a ```bash block narrated in prose as a run_shell call,
non-destructive only (FENCE_DESTRUCTIVE guard); fires on prose-leak turns,
no-op where the model emits structured calls.
Ablation on 0.5b: structured-JSON-only 0/0 -> fenced+temp0 2/0 -> +split-tag
4/3. The lift is concentrated on the weakest model by design — a 3B emits
proper calls and fails on capability/content (unchanged at 1/12), which no
parser can fix. All recovery paths unit-checked for the positive shapes and
the negatives (prose / unknown tool / destructive block) they must ignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generalise OllamaProvider._extract_text_tool_calls to recover a tool-call
JSON object regardless of how a small/quantized model wraps it — qwen's
<tool_call> tags, bare JSON, ```json fences, alternate tags (<tools>,
<function_call>), OpenAI {"function":{…}} nesting, and parameters-vs-arguments.
A new _coerce_call gates recovery on the known tool-name set from the tools
schema, so a stray JSON blob in prose (or a hallucinated make_dir) can never
be coerced into an action. 11-case unit check: 8 leak shapes recover, 3
negatives (prose / unknown tool / random config JSON) ignored.
Benchmark verdict (honest): this does NOT move the weak-CPU-model pass rate
— 3b went 2/1/0 of 12 across three passes (baseline 1/12, noise), 0.5b went
0/0 (baseline 1/12). A direct /api/chat probe shows the hypothesis was wrong
about the FORM of the leak: the weak models emit either malformed structured
tool_calls (write_file content:null) or a fenced bash block in prose with no
tool call at all — not JSON-as-text. The structured-JSON recovery is still a
correct, safe hardening for any model that does leak JSON; the real
weak-model lever (parse ```bash fences -> run_shell) is documented as an
explicit safety decision, not folded in here.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the overloaded "text + no tool call = done" terminator that made the
weak CPU model stall mid-task or give up after a failing command. Termination
is now a structural `DONE:` text sentinel; a text-only turn is resolved by an
output-aware verdict (DONE: marker / unresolved non-zero exit / no action /
filler language) and re-prompted with an exit-code-aware nudge, bounded by
MAX_NUDGES on top of max_turns. On exhaustion the summary is honest rather than
echoing the model's false "run successfully" — it reports when no tool ran or a
command exited non-zero. Live-validated on qwen2.5:3b: the multi-step stall is
fixed (proj3 completes end-to-end, ground-truth confirmed); the nudge fires on
a 126; residual give-up is model-bound. 23 offline unit assertions pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two native-harness improvements, both live-validated against qwen2.5:3b
and qwen2.5-coder:7b on a podman/Kali sandbox:
- §3 PTY-sentinel: run_shell now runs in the REAL shared terminal via
_run_shell_in_pty (stage cmd out-of-band to a hex-token temp file, type
a `{ sh CMDF; echo $? >RCF; } 2>&1 | tee OUTF` wrapper into the live PTY,
poll the rc sentinel out-of-band, then read OUTF). The whole room now
watches commands execute live instead of an inert `# ▸` comment, while
output + exit code are still captured for the loop. tee+poll (not
stream-sentinel) avoids deadlocking the serve loop; the wrapper line
carries only our own temp paths so room text never reaches the shell
parser. _exec_tool takes ws to reach the PTY.
- NATIVE_CONTEXT=4: action tasks now get a tight, RAG-free window (last few
transcript turns only, no semantic recall). A weak model fed prior chat
chatter latched onto nearby noise (wrote a "grant permissions" script for
"write a bash script"); feeding just the instruction fixes it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Optimize the native tool-calling loop for qwen2.5:3b on CPU, where it
previously invented paths (/ai/bin/bash), ran scripts it never wrote, and
silently dropped valid actions. Three changes:
- NATIVE_SYSTEM rewritten directive: explicit write→chmod→run workflow,
relative paths only, never run an uncreated file, never guess interpreter
paths, fix the cause on non-zero exit.
- New _sandbox_facts() probe injects LIVE SANDBOX STATE (real cwd, bash
path, current files) into the system prompt so the model anchors to
ground truth instead of guessing.
- OllamaProvider recovers tool calls qwen emits as <tool_call>{json}</…>
TEXT in content (brace-balanced JSON scan), so a correct action isn't lost.
- Bump Ollama timeout 120→240s: the tool turn is non-streaming and a long
write_file can exceed a tighter cap on a contended CPU box.
Live-validated (podman/Kali): 0/3 incoherent → reliable write/run with
self-correction on exit=126 for both single- and multi-script tasks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display-mirror hybrid (docs/plan-harness-visibility.md §2): native tool
calls now show up in the shared sandbox terminal again via inert `# `-
prefixed comment lines (comment-prefix = anti-double-run/anti-escape),
mirroring only each command. Chat de-flooded to opener + final summary.
write_file mkdir -p parent dir so relative/absolute paths both work
(fixes the regression where script creation silently failed). ui.rs
fmt_line returns Vec<Line> splitting on \n so multi-line agent output
renders as an indented block instead of one garbled row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 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>
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>
- /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>
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>
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>
- /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>
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>
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>
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>
- 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>
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>