17 Commits
Author SHA1 Message Date
zeekay 25c6f62cff feat(rust): hanzo-shell reads exec's command and says allow, ask or deny
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.
2026-10-14 16:32:13 -07:00
zeekay fe2e8c2a02 feat(rust): hanzo-trigram reads tgrep's index, and a limit bounds the work
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.
2026-10-14 14:41:07 -07:00
zeekay 630ec216d3 feat(rust): tools/list in one order, and --tools to serve fewer
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.
2026-10-14 13:40:31 -07:00
zeekay d717c2da73 feat(rust): exec asks a classifier before it runs a command
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.
2026-10-14 13:37:51 -07:00
zeekay e9e6096ce4 feat(rust): fs search_text and code search_symbol on tgrep, in process
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.
2026-10-14 13:37:01 -07:00
zeekay e3067ebc8e feat(rust): llm limits, catalog search, served and paid-by, plan refusals 2026-10-04 16:28:31 -07:00
zooqueenandHanzo Dev eb0f29fc85 Give the Rust runtime kai_decide and Enso through llm
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Skipped
publish / publish (push) Successful in 59s
Hanzo CI/CD / test-amd64 (push) Successful in 3m22s
Hanzo CI/CD / cicd (push) Successful in 2m11s
CI/CD / cicd (push) Successful in 5m37s
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]>
2026-10-04 13:46:12 -07:00
zeekay 1ef1ce34a5 Add npx, uvx and jq to the Rust runtime
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.
2026-09-30 15:17:29 -07:00
zeekay ed2a23156d Let presence arrive before WELCOME in the native host test
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.
2026-09-30 15:17:25 -07:00
zeekay 03b7fd3edf Give the Rust fs tool Python's contract
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Skipped
Hanzo CI/CD / test-amd64 (push) Successful in 1m46s
publish / publish (push) Failing after 1m10s
Hanzo CI/CD / cicd (push) Successful in 1m53s
CI/CD / cicd (push) Successful in 3m43s
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.
2026-09-30 15:03:49 -07:00
zeekay 6dd2293590 Serve the extension's native messaging host from the Rust binary
Hanzo CI/CD / test (linux-amd64) (push) Skipped
Hanzo CI/CD / test (linux-arm64) (push) Skipped
Hanzo CI/CD / test-advisory (push) Skipped
publish / publish (push) Failing after 1m15s
Hanzo CI/CD / test-amd64 (push) Successful in 1m56s
Hanzo CI/CD / cicd (push) Successful in 2m33s
CI/CD / cicd (push) Successful in 4m33s
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.
2026-09-30 14:59:17 -07:00
zeekay d9f8034726 Route the Rust browser and cdp tools to the extension over the embedded ZAP router
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.
2026-09-30 12:52:05 -07:00
zooqueenandHanzo Dev cf98e8777a auth: the two key shapes are pk- and sk-, and one sentence says so
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]>
2026-08-08 14:59:50 -07:00
zooqueenandHanzo Dev b3c34fa7c3 feat(lsp): repo answers from /v1/code/lsp, file stays local
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]>
2026-08-06 03:14:53 -07:00
zeekayandHanzo Dev c36a1eb1c8 rust/tests: repair the two suites that stopped compiling at HIP-0300
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]>
2026-08-05 14:23:33 -07:00
zooqueenandHanzo Dev d0e89b9bf7 feat(rust): research — the /v1/ask answer engine as one tool
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]>
2026-08-04 22:50:50 -07:00
zeekay 028c53ed6d chore: sync uncommitted changes 2026-02-13 22:15:53 -08:00