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.
exec gains run(): an argv with no shell, waited on up to a deadline, then
left running in exec's process table. npx and uvx use it with Python's argv
and a two-minute deadline; jq passes its filter as one argv word.
@hanzo/mcp 2.4.14
read returns numbered lines and the full sha256 hash, write only creates,
apply_patch takes old_text and new_text under base_hash, list walks with
depth, pattern and cursor, patch takes the Rust grammar, and search_text
takes a pattern and glob. rg now runs without a shell, so a pattern is never
a shell word.
@hanzo/mcp 2.4.13
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.
The two lines had diverged on what this client offers: a hand-written tool
naming seven resources against four hosts, or the surface cloud generates
from its own typed operations. The generated one wins, and not by
preference — cloud's gen-mcp-catalog writes src/tools/catalog.json into
this repo, so the fleet already treats it as the destination.
hanzo-cloud.ts stays deleted and the Rust tool keeps reading the catalog.
The iam commits improved a `call` helper that dialled a host per service;
the catalog-driven path takes every address from the document and answers
through the error envelope, so what those commits were correcting is gone
rather than reverted. The hero card's font stack came across untouched.
384 JS tests, 51 Rust tests, typecheck clean.
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.
The seven resources this offered had drifted from the fleet it names: paas is
platform, storage is s3, auth folded into iam, and iam alone carries 97
operations against the handful the hand-written tool reached. It also dialled
four hosts for one API.
The names come from cloud's typed operations now, so this client cannot come to
disagree with the API about what exists. The withholding rule is asked of cloud
rather than restated here — a client deriving its set from the raw catalog offers
what the fleet refuses.
The default surface is unchanged at 22 tools; the fleet sits behind hanzo, whose
resource enum is derived rather than listed.
Users, organizations, roles, applications, providers, permissions,
invitations, tokens, sessions and audit rows are addressed as
/v1/iam/{plural} for a list and /v1/iam/{plural}/{owner}/{name} for a row,
in both the Rust `hanzo` router and the TypeScript iam/auth tools. `id`
splits into its owner and name path segments and a malformed one is refused
rather than sent. Deleting a user is a DELETE on the row; updating one reads
the row and PUTs {"user": merged}.
`owner` is scope: it goes on the wire only when the caller names one, and the
server resolves the rest from the credential. /v1/iam/applications is the one
list that requires it, and asks.
A non-2xx answer is now an error carrying its status and reason — RFC 9457
`detail`, else the envelope `msg`, else `title` — so KMS, PaaS and IAM
refusals surface instead of reading as data. Tests cover the row address, the
id split, and a problem+json refusal against a local listener.
Identity questions belong to the iam service alone, so paas no longer
duplicates users/orgs/roles. Actions with no address behind them (enforce,
system_info, health, mfa status) are gone.
Co-authored-by: Hanzo Dev <[email protected]>
This repo is the only publisher of the package; the copy vendored in
hanzoai/extension is a build input and stays private. Records the flat KMS read
and that NPM_TOKEN sits at the org root, so the next reader does not rediscover
it against a 404.
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]>
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]>
An agent could already keep a todo list (`tasks`), but only on its own disk.
Nothing here reached the board a human looks at, so "what is this agent doing"
had no answer outside the agent. These four tools are the cloud /v1/tracker
surface — the ONE work-item primitive — as MCP tools.
They are deliberately NOT called tasks_*. Three planes are easy to braid and
cloud apps/tracker/contract.go is law about it: a tracker Issue is a work item,
hanzoai/tasks is durable async execution, and the local `tasks` tool is a
private file at ~/.hanzo/todos.json. Naming these tasks_* would have collided
with that third one and pointed callers at the plane nobody can see.
Linking a run needed no new field. An agent's run is a SESSION — "mission" is
not modeled anywhere in the fleet — and the tracker contract already defines
extRef as "a link INTO another plane", so `session` writes the anchor
`session:<id>` and hanzo.app renders that session's live status on the row.
tracker_create defaults source to agent, because the board's ?source=agent
filter is only true if agents say so.
Cloud's own refusal reaches the caller verbatim — "unknown status filter" is
worth more to an agent than a generic failure — and the org is never sent: it
is minted server-side from the bearer, and a tool that sends one is a tool
asking to be trusted about tenancy.
NOTE: the jest suite cannot RUN on main right now — TypeScript 7.0.2 is
installed and ts-jest 29.4.6 cannot read its compiler API, so all 18 suites
fail to load. That break predates this commit and is orthogonal to it (verified
on a clean origin/main checkout). These tools were verified twice: 20 jest tests
green under the TS 5 toolchain, and 11 runtime checks through the shipped
esbuild bundle under main's current dependency state.
Co-authored-by: Hanzo Dev <[email protected]>
The runner needs ripgrep at test time and cannot run the two display-gated
computer tests. Tag pushes do not start a run today, so the workflow is
dispatched against the tag ref and verified by listing assets.
Add code_search / code_context / code_ask / code_index — one MCP tool per
api.hanzo.ai/v1/code endpoint — so @hanzo/dev and any MCP agent gain per-org
code search, budget-packed context, and cited RAG answers.
Auth mirrors hanzo-cloud.ts exactly: the caller's Hanzo bearer from
HANZO_API_KEY (or API_KEY/API_TOKEN/HANZO_TOKEN). The org is minted
server-side from that identity (principal gate) — a client never passes an
org. Output is requested as markdown (?format=md) so the LLM reads a
token-thrifty table; a non-2xx surfaces as a clear isError string. The tools
join the canonical HIP-0300 surface (allUnifiedTools), so tools/list exposes
all four over stdio.
test: hermetic local-server suite (10 tests) asserts method/path/format=md/
Bearer, no-org-leak, and arg + auth failures. Update the two tool-list
registry tests for gimp + the four code tools, and fix a pre-existing
ts-jest rootDir error that blocked every suite importing test/setup.ts.
Co-authored-by: Hanzo Dev <[email protected]>
- LLM.md is the canonical AI context file (tracked in git)
- CLAUDE.md is a local symlink to LLM.md (gitignored)
- Updated LLM.md content for accuracy and conciseness
## Core Implementation
- Unified search engine with strategy pattern for extensible search types
- Full OpenAI MCP specification compliance for search and fetch tools
- Multi-modal search strategies: Symbol, AST, Vector, Text, and File search
- Automatic strategy selection based on query characteristics
## Security & Remote Access
- Secure ngrok tunnel integration with automatic detection
- Multi-method authentication (Bearer token, API keys, JWT)
- Post-quantum TLS configuration support
- Rate limiting and CORS protection
- Access logging and monitoring
- Only exposes files when ngrok credentials are present
## Testing & CI
- 102 tests with 85% coverage on critical paths
- GitHub Actions CI workflow for automated testing
- Node.js 18.x and 20.x compatibility testing
- Coverage reporting to Codecov
- Security audit checks
## Documentation
- Comprehensive documentation in LLM.md
- Security guide in docs/SECURITY.md
- .npmrc excluded from repository for security
This implementation provides a production-ready search system for AI tools like ChatGPT and Claude Desktop.