A pure-Rust classifier for the `Classify` seam. brush-parser (MIT) gives the bash AST
and rustpython-parser (MIT) the Python one; every simple command, redirection,
substitution and nested script (bash -c, here-documents, a script the command wrote and
runs, python -c, ssh host '...') is read in the order the shell runs them, and the policy
over paths decides what each costs. rust/shell is its own crate with a wasm-bindgen build,
so kai-gateway can replace safety.ts with it.
exec: an ask is refused until the call carries approve: true (never lifts a deny); the
interpreter must be a shell the classifier reads; the env is read beside the command;
the git tool asks the same classifier and runs with core.fsmonitor=false; ui install no
longer hands the component name to sh -c.
On 408,231 Bash commands: safety.ts flags 20.69% (15.27% unparsed); this answers
allow 88.2 / ask 9.6 / deny 2.2 (roots set), cannot read 0.22%, p99 0.6 ms.
A search with a limit now runs in two passes: every file is weighed
(matching lines counted, definitions as 2^32) without holding a hit,
then hits are read from the heaviest files until the limit is met. The
answer is the head of the unlimited list, checked for limits from 0 to
the whole list over text, symbol, context, ties and documents. Before,
every hit of every file was built, sorted and deduplicated before the
limit applied: a pattern matching 1.28 million lines of a 112 MB tree
took 15.6 s where it now takes 0.14 s, and rg's --max-count had bounded
that case before this crate replaced it.
When there is a tgrep index at or above the searched directory the
search asks it which files can match. Candidates are matched against
current bytes, and files whose content or name changed at or after the
build are read from disk whether or not the index lists them, so the
answer is the scan's. Index::Skip scans, Index::Only refuses to; the
tests ask every query all three ways on the same tree, before and after
edits, creations, deletions and renames, and fail if the freshness walk
is disabled. hanzo-mcp index [DIR] builds one.
Measured on 112 MB / 9,026 files, cores 0-4: rg 43-55 ms, scan 75-93,
indexed 28-50. Without a watcher the index costs a metadata walk.
tools/list appended the registered tools in HashMap order, which is
different in every process: two starts of the same binary listed
different orders (checked: three runs, three orders), so a client's
prompt cache over the tool schemas could not hold between sessions. The
registered tools now follow the built-ins sorted by name.
--tools fs,code,exec lists and serves only those. A client is charged
for every schema it is shown, each turn: the whole list is 34 tools and
38 KB, about 9.5K tokens; fs, code and exec together are 4.6 KB, about
1.1K, and byte for byte the same on every start. A name the server does
not carry fails at startup and lists what it does; a call outside the
list answers Unknown tool, like any other name.
ToolsConfig's flags were never read and are left alone.
Classify is the contract between exec and the shell parser and
classifier: command and cwd in, a Verdict (allow, ask or deny, with
reasons) out. ExecTool::with_classifier installs one; ToolRegistry::
with_classifier does the same for the MCP server; ExecTool::verdict is
the call Dev makes to ask the same classifier without running anything.
A deny is refused before a process is spawned or registered. An ask runs:
a server cannot ask the person, and the client's own permission prompt
already stood between the model and the call. Every answer then carries
the verdict. refusal() is the one place that policy lives.
With none installed, which is every build until the classifier module
lands, exec answers byte for byte as before and its input schema is
unchanged: the verdict is an output field, so the TypeScript runtime's
schema stays identical.
hanzo-trigram is the one search engine behind both actions: tgrep-core
(the hanzoai fork, MIT, pinned by commit, feature docs) linked as a
library, so a search is a call, not an rg spawn. It is its own crate so
Dev can link it without this crate's browser and parser dependencies;
hanzo_mcp::trigram re-exports it.
Results are ranked (matching lines a file, or definitions before uses
for a symbol), then path, then line, so a page is an offset into one
fixed list and the cursor really pages: before, rg stopped at limit and
a cursor repeated the first page. .gitignore, hidden and binary files
are skipped as rg skips them. A glob that names a document type (*.pdf,
*.docx, *.zip) reads documents too, and a hit inside one carries the
archive members and its page, slide, paragraph or cell.
fs and code keep their names, schemas and answer shapes; entry and place
appear on a document hit only. search_symbol now reads definitions by
the engine's declaration patterns, not the first-keyword guess.
Linking tgrep-core brings libsqlite3-sys (its managed-index code), which
needs a newer cc than the tree-sitter grammars allow, so Cargo resolves
tree-sitter-{c,cpp,javascript,python,rust,typescript} to their previous
patch releases. 241 library tests and the code tool's pass on them.
Gating managed/ behind a feature in the fork removes both.
rust-v1.1.26 and rust-v1.1.27 shipped .sha256 sidecars holding a bare
hash. install.sh, which hanzo.sh runs for `curl hanzo.sh/mcp | sh`,
verifies with `sha256sum -c`, which needs "<hash> <name>", so every
Linux install of those releases stopped at "checksum MISMATCH" and
installed nothing. The sidecar is sha256sum's own line again, as it was
through rust-v1.1.23.
Release 1.1.28.
Co-authored-by: Hanzo Dev <[email protected]>
The Rust hanzo-mcp could not reach Kai at all, and its llm tool defaulted
to an upstream model id, sent no routing bound, dropped the completion id
and the model that served, and returned a 402 or 429 as ok with empty
content because the client never read the status.
- kai_decide: POST /v1/decisions with the TypeScript tool's name, schema
and shape checks (choice, score, noul; 1 to 100 questions).
- llm query: model enso-auto unless named, "auto" for Enso's pick;
max_cost and max_latency_ms ride as X-Max-Cost and X-Max-Latency-Ms;
the answer carries id and the model from X-Routed-Model or the body.
- llm models: one row per model with family, class and the catalog's
per-million prices; family narrows it.
- llm feedback: POST /v1/ai/feedback with request_id, signal, rating.
- HanzoApi::call holds an answer to its status: a non-2xx, or an
envelope that says error, is an error with the server's sentence.
Release 1.1.27.
Co-authored-by: Hanzo Dev <[email protected]>
The runner image carries no gh CLI, so the open job died on its first
command and nothing was built. The release action talks to the API
itself, and the toolchain action installs Rust and the target rather
than trusting the image to have them. hanzo-mcp 1.1.26.
npx and uvx build Python's argv (package, whitespace-split args, npx -y,
uvx --python) and run it as an exec with a two-minute auto-background. jq
passes its filter as one argv word, as Python's jq tool does.
A node registering between ours entering the registry and our WELCOME
being queued is announced to us first; the extension ignores it, and so
does the test now.
A command past its timeout had its pipes dropped with the wait, so it lost
its output (or died of SIGPIPE) and stayed running forever in ps. Its
output now drains to a log that logs reads, and its exit is recorded; a
command that finishes in time returns inline and leaves no log.
uri (path accepted), the unified envelope, sha256 hashes, write that only
creates, apply_patch guarded by the hash a read returned, list with depth,
pattern and cursor, search_text, and patch taking input. The search alias
tool goes: fs action=search_text is the one search.
Run as hanzo-zap-host, hanzo-mcp relays ai.hanzo.zap messages to the ZAP
router's socket, as Python's native_host.py does, so a machine with only the
Rust server still lets Chrome and Firefox join without a door or pairing. Each
start registers it with the installed browsers, leaving any manifest whose
program exists alone.
hanzo-mcp links zapd v1.1.5, stands for this login's router at startup and
takes the seat mcp/hanzo-<pid>, as Python hanzo-mcp does. browser takes
Python's action names and parameters (the core typed, the rest in args) and
sends each action with a wire method to the registered browser node; headless
Playwright runs only when none is registered. cdp sends raw methods to the
same node. hanzo-mcp pair prints the door's pairing code. The integration test
embeds the router on a private XDG_RUNTIME_DIR with a fake extension.
The catalog is regenerated from cloud (plugin/gen-mcp-catalog): the graph tool
now carries all thirteen operations, answer, communities, diff, erase, extract,
ingest and path among them, and the Rust crate's copy is synced from it.
The hanzo tool sent {resource, action, ...} to a subsystem handler that reads
{op, input}, so every call from the default surface reached the fleet with no
op. It now sends {op: action, input: args}, the shape the Rust runtime sends,
lists a resource's actions when none is named, and offers only subsystems.
Co-authored-by: Hanzo Dev <[email protected]>
The registry fallback asked ui.hanzo.ai/api/registry, which 301s to a 404 — so a
dead address stood in for a second chance nobody had. The primary serves; the
fallback is gone rather than moved, because /v1/registry is not served either and
pointing a client at an address the estate does not answer is the same defect
with a better prefix.
The mode tests raced a process-wide singleton: one activates minimal while
another activates guido and reads it back, and cargo runs them in threads of one
process. Three runs gave ok, ok, FAILED. They share a lock now.
The runtimes are said to mirror one another, and this one listed ten services
against the fleet's 114. Two of the ten had been left behind, and the commerce
base was api.hanzo.ai/api/v1 — an /api/ prefix nothing serves.
Nine routers over four hosts become one dispatch to the single endpoint, and the
service list becomes a read of the file the TypeScript runtime already embeds.
The aliases are removed rather than repointed: three of the four named the old
surface, so they mapped a caller off the API. Spelling is still normalised.
Cloud's APIKeyPrefixes admits pk- and sk- only (auth_identity.go, 4b030b77).
Every tool here still told the user to set an hk- key, which cloud resolves to
no principal — following the message produced a key that could never
authenticate.
The message is now a single const beside the resolver that mints it, so the
seven tools that report it cannot drift apart again. Resolution order is
untouched: HANZO_API_KEY, then ~/.hanzo/config.json .apiKey.
Co-authored-by: Hanzo Dev <[email protected]>
The server replied "2024-11-05" to every initialize, whatever the client asked
for. `hanzo dev` asks for 2025-06-18 and refuses a server that answers anything
else, so it reported `MCP server hanzo failed to start` — in an image where the
binary was on PATH, `dev mcp list` showed it registered, and a hand-written
handshake got 29 tools back. Everything that looked at the server passed;
nothing that PAIRED it with an agent worked.
Negotiation is the whole fix: echo the requested revision when it is one we
serve, answer our newest when it is not and let the client decide. The three
listed revisions are the ones this server can actually serve — initialize,
tools/list and tools/call are unchanged across them, and what the later ones add
is optional and unadvertised. A client pinned to the original revision keeps
getting it, which is the point of negotiating rather than moving the constant.
One tool, two planes, picked by data the caller already has: `file` names the
file, `repo` (a git.hanzo.ai slug, optional `rev`) says which world it lives
in. Without `repo` nothing changes — the same language server on the same
tree. With it, the question goes to the indexed corpus behind /v1/code/lsp,
which reaches across a repo's dependencies without checking anything out.
Actions map onto ops: locate carries a relation (definition|reference|type|
implementation) because "where is X" is one question with four answers, not
four routes; hover, symbols, diagnostics and complete stand on their own. The
body is {repo, rev?, path, line, character, relation?} in LSP's own frame —
0-based line, UTF-16 character — shifted from the tool's 1-based `line` at the
same boundary the local plane shifts it.
Credentials and base URL come from the one HanzoApi client every cloud-backed
tool already composes; there is no second client and no second lsp tool. The
actions a plane cannot serve say so up front rather than spawning a server or
calling out — rename/code_action/organize_imports/status need a working tree,
type/implementation/symbols are the index's to answer.
Co-authored-by: Hanzo Dev <[email protected]>
test_shell_tools and test_think_tools have not built since the shell->exec
consolidation, so `cargo test` was red and 46 tests were silently not running.
Nothing here is a new assertion — each change follows the tool's live contract,
read off the published 1.1.22 binary rather than guessed:
shell ShellTool -> ExecTool, ProcToolArgs -> ExecToolArgs. Pure rename;
all 16 pass untouched.
think ThinkTool::execute returns Value, not String, and HIP-0300 moved the
payload under "data". Help reports the tool as "tool", not "name".
Tests whose subject no longer exists are deleted, not rewritten to pass:
critic/review with focus, code_snippets, file_paths and work_description (11);
the timestamp and examples keys (2); action-less auto-detect, which now falls
through to help (1).
browser: playwright_available -> driver_running, its successor since the tool
moved to a CDP driver. That file compiled, so this one was failing, not skipped.
351 pass, 0 fail: lib 223, browser 28, fs 27, search 28, memory 22, shell 16,
think 5, research 2. tests/ is excluded from the package, so 1.1.22's contents
are unchanged and no version bump is warranted.
Co-authored-by: Hanzo Dev <[email protected]>
The name has changed hands four times across three codebases:
1.1.10, 1.1.11 rmcp client shim
0.1.0, 1.1.21 rust-sdk re-export facade (ships zero tools)
1.1.12 this repo, the only lineage with working tools
1.1.21 is what cargo add resolves today: no binary, and its
ToolRegistry::with_defaults() returns [] with the server feature on.
Publish this tree instead, above it, so the name carries the server.
Description said 'blockchain'; there is no blockchain tool. It now
names the 15 tools the registry actually serves.
Co-authored-by: Hanzo Dev <[email protected]>
Deep research is a MODE of the one door (POST /v1/ask, mode=research), not a
second endpoint and not a client-side loop: the plan → search → read → rank →
synthesize → cite pass runs server-side where it is bounded and billed. The
tool sends `{q,mode}` with `Accept: text/event-stream` and folds the stream
into one cited answer plus the sources behind it.
HanzoApi gains `events()` — POST over SSE — so streaming lives with the other
credentials-and-base-URL knowledge instead of beside a tool, and `frames()`
decodes a body into its `data:` payloads. Both are the shared `@hanzo/ai`
SearchEvent contract: data-only JSON that self-describes via `type`, with the
terminal `[DONE]` a marker rather than an event. Bearer attachment collapses
into one `auth()` now that two request paths need it.
`research` sits beside `web_search` and `web_read` because they are ONE web
capability at three depths — a snippet, a page, a report — so the existing
`ToolsConfig::web_search` flag governs all three and no second flag appears.
A mode this door does not open (`search`, `news`) is refused rather than
silently answered: a wrong answer is worse than an error.
Co-authored-by: Hanzo Dev <[email protected]>
1.1.14 published only 4 of 5 targets: the linux-amd64 job gates the
matrix on the host test run, and three tests failed on the ARC runner.
The search tools shell out to `rg` at runtime, which the runner image
does not carry, so test_search and test_unified_search asserted against
a missing binary. Install ripgrep rather than skip them — it is a real
runtime dependency and the coverage is real.
test_screen_size_action reads the display through X11 and cannot pass
headless, same as the already-skipped test_position_action. Skip it by
name so the rest of computer_tool still gates.
Reproduced both causes locally by running the test binary with `rg`
off PATH and DISPLAY unset: identical panics. With ripgrep present and
both display tests skipped, 214 pass / 0 fail.
1.1.13 never published: the release build failed on a cross-repo
include_str! and produced no artifacts. Rather than move a pushed tag,
take the next patch.
builtin_email_recipe_yaml include_str!'d
../../../../bot/extensions/recipes-brain/recipes/email.yaml — a path
that climbs out of this repo into a sibling checkout of hanzoai/bot.
That resolves only on a workstation with both repos side by side, so
every CI target failed to compile the lib:
couldn't read `src/brain/../../../../bot/.../email.yaml`
Copy the file to rust/recipes/email.yaml (byte-identical, sha256
f4577ec92d696a426d8ec230af55747e81a505c79f9d02266f9e2e0538ed38ae) and
include it from there. This is also what the module doc already claimed:
recipes live at <crate_dir>/recipes/*.yaml.
Verified by building all five targets from a copy containing only this
crate's tracked files, with no sibling repo present.
Patch bump from 1.1.12, the last published version of this crate.
Cargo.toml and Cargo.lock both carry 1.1.13 so the release builds
under --locked; a stale lock package entry is what broke the last
hanzoai/cli release.
reqwest ran on default features, which selects native-tls and pulls
openssl-sys. openssl-sys needs per-arch OpenSSL headers, so every
non-host target failed in its build script before compiling a line of
our code. Move to rustls, which is pure Rust and needs no C headers.
Same 0.11 line, only the TLS backend changes.
GetDC and ReleaseDC live in winapi::um::winuser, not wingdi; only
GetPixel is a GDI call. The windows target had never been compiled, so
the wrong module went unnoticed. Enable the wingdi, windef and
minwindef features the file actually uses.
All five targets now build from one amd64 host via cargo-zigbuild.
Swapped sha2 stand-in for blake3 crate. Wallet address output now byte-
equivalent across all five brain runtimes:
encode_address([0u8;32], None)
→ hanzo:UFC8qCW8LRUmpfyRq2qnAvYi11cqftY3b
38 brain tests pass.
- Update doc comment to point at hanzoai/node canonical home and drop
the stale `hanzoai/brain/PARITY.md` reference (PARITY contract is now
superseded by the byte-equivalent mirror policy).
- Drop spurious `mut` on the `flush` closure inside `estimate_tokens`
to match node — closure mutates through &mut params, no rebind.
Diff vs `hanzoai/node/hanzo-libs/hanzo-brain/src/algorithms.rs` is now
limited to the three expected divergences:
* doc comment (different host context)
* SearchHit: imported from super::store here, locally defined in node
* test-module use vs comment for the same reason
38 brain::algorithms tests passing.
Adds rust/src/brain/ module with three components mirroring the TS
(bot/extensions/{memory,graph-links,recipes-brain}) and Python
(python-sdk/pkg/hanzo-memory/{graph_links,recipes}.py) implementations:
graph_links.rs — Edge struct + EdgeType enum, extract_edges(),
reconcile(), slugify(). Same six edge types, same
regex + role inference, same code-fence stripping
as the other two runtimes. Pure — no I/O, no LLM.
store.rs — Pluggable BrainStore trait (async). Same surface as
TS BrainStore + Python BaseVectorDB so a single
~/.hanzo/brain/brain.db is read/written by all three.
recipes.rs — YAML recipe loader. Reads HANZO_HOME/recipes and
HANZO_BRAIN_RECIPES; ships the email recipe at
compile-time via include_str! from the canonical
bot/extensions/recipes-brain/recipes/email.yaml.
Tests: 6/6 pass via `cargo test --lib brain` covering slugify, every
edge type, code-fence stripping, reconcile, recipe parse.
Dep added: serde_yaml = "0.9".
- Rewrite zap-server.ts with inline ZAP encode/decode (no @hanzo/zap dep)
- Clean up Rust main.rs: remove dead hanzo-node check, use CARGO_PKG_VERSION
- Remove accidentally tracked rust/target/ from git
- Created complete landing page with @hanzo/ui components
- Demonstrates ui tool's ability to compose complex UIs
- Includes hero, features, testimonials, pricing, and CTA sections
- Production-ready code with animations and dark theme
- Validates dev agent can use ui tool effectively
- Comprehensive testing suite for all UI tool methods
- Matrix test report showing 80% success rate