Read the fleet from the same catalog the other runtimes do

Python addressed the cloud through ten hand-written packages while TypeScript and
Rust read a generated catalog, and the hand-written ones had drifted: paas is
platform, storage is s3, auth folded into iam, and commerce asked for plurals the
fleet does not publish.

hanzo-tools-cloud embeds the file those two already embed, so the three cannot
disagree about what the API offers — a test compares them by digest. One routing
tool over 114 subsystems rather than 114 tools, for the reason the fleet groups
its own.

Naming a subsystem the fleet does not serve is answered before the credential
check: demanding a key for something that does not exist sends the caller to fix
the wrong thing.
This commit is contained in:
2026-08-30 17:05:32 -07:00
parent b4b0f3bf67
commit badff3db49
8 changed files with 2207 additions and 0 deletions
+35
View File
@@ -281,6 +281,41 @@ metadata contradicted the file inside it. Metadata now matches the file: `Apache
emitted any more, so the suite had a failing test that named a module the client does not
have. It reads `analytics_api` now, off the client.
## The cloud tool surface is GENERATED, and ten packages duplicate it
`pkg/hanzo-tools-cloud` offers the fleet the way the fleet offers itself: one
routing tool over 114 subsystems and 1,416 operations, read from
`catalog.json` — generated out of cloud's own typed operations
(`plugin/gen-mcp-catalog`). It is the SAME file `@hanzo/mcp` and the Rust
`hanzo-mcp` crate embed, and `tests/test_cloud.py` compares the three by digest,
because three copies of one value is how they come to disagree.
It is a catalog and not a fetch because a tool list is answered before any
request has been made. Refresh every runtime's copy at once with
`pnpm sync:catalog` in `hanzoai/mcp`; it refuses to write a smaller catalog than
it replaces without `--shrink`, since a fleet answering partially and a fleet
that lost capabilities look identical.
**The withholding rule is applied once, in cloud.** The endpoint keeps an
operation off the agent surface when its name discloses a bearer secret at any
verb, or when a mutating verb acts on identity or authority — 124 of 1,540. The
generator asks `fleet.Withheld`, the same predicate the endpoint asks, so a
client cannot offer what the fleet refuses. `get_iam_users` survives;
`post_iam_users` does not.
**Ten hand-written packages duplicate this, and they duplicate `hanzoai.cloud`
too.** `hanzo-tools-{auth,billing,commerce,iam,ingress,kms,mpc,paas,s3,team}` are
4,893 lines addressing the fleet by hand, and every one of them predates both the
generated tool surface and the generated typed client. They have already drifted:
`paas` is the name the fleet left behind (it is `platform`), `storage` is `s3`,
`auth` folded into `iam`, and the commerce tool asked for plural resources
(`/products`, `/orders`) where the fleet publishes singulars under
`/v1/commerce/product/` — with no order or user collection at all, because an
order is reached through its store and a user is IAM's.
`hanzo-tools-api` is NOT in that set: 8,028 of its 14,653 lines are a vendored
APIs.guru directory, so it is a generic any-API tool rather than ours.
## Key entry points
- `pkg/hanzoai/` — the client (`ApiClient`, `Configuration`, `*Api`) under `cloud/`, plus
seven hand-written modules beside it (`config`, `mcp`, `protocols`, `session`, `zap`, …).
+23
View File
@@ -0,0 +1,23 @@
# hanzo-tools-cloud
The Hanzo Cloud surface, offered as the fleet publishes it: one entry per
subsystem carrying that subsystem's operation names, plus `describe`.
Nothing here is hand-written. `catalog.json` is generated from cloud's own typed
operations, and it is the same file the TypeScript (`@hanzo/mcp`) and Rust
(`hanzo-mcp`) runtimes embed — so the three cannot disagree about what the API
offers.
```python
from hanzo_tools.cloud import call, services, operations
services() # every subsystem the fleet serves
operations("iam") # that subsystem's operations
call("iam", "get_iam_users")
```
Every call goes to `api.hanzo.ai/v1/mcp`. Set `HANZO_API_KEY`; point elsewhere
with `HANZO_API_URL`.
Refresh the catalog from the repo root: `pnpm sync:catalog` in `hanzoai/mcp`, or
`go run ./plugin/gen-mcp-catalog .` in `hanzoai/cloud`.
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,7 @@
"""The Hanzo Cloud surface, generated from the fleet's own typed operations."""
from .cloud_tool import CloudTool, call, operations, reach, services
TOOLS = [CloudTool]
__all__ = ["CloudTool", "TOOLS", "call", "operations", "reach", "services"]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
"""The Hanzo Cloud surface, offered as the fleet publishes it.
One tool per subsystem, carrying that subsystem's operation names, plus
``describe`` — which answers one operation's prose and schema. That is the shape
the fleet serves at ``POST /v1/mcp``, and it is why this file is short: a flat
projection is 1,416 tools and roughly a megabyte to enumerate, which a model pays
for on every turn and a client with a tool cap truncates.
The names come from ``catalog.json``, generated out of cloud's own typed
operations (``plugin/gen-mcp-catalog``). It is the same file the TypeScript and
Rust runtimes embed, so the three cannot come to disagree about what the API
offers — which is what "the runtimes mirror one another" has to mean to be worth
saying.
It is a catalog rather than a fetch because a tool list is answered before any
request has been made, and a client that needs the network to say what it can do
has nothing to say when the network is what failed.
Every call goes to ``api.hanzo.ai`` — the one endpoint. The ten hand-written
packages this replaces dialled four hosts, named resources the fleet had renamed,
and asked for plurals where it publishes singulars.
"""
from __future__ import annotations
import json
import logging
import os
from functools import lru_cache
from pathlib import Path
from typing import Any
import httpx
logger = logging.getLogger(__name__)
_CATALOG = Path(__file__).with_name("catalog.json")
@lru_cache(maxsize=1)
def _fleet() -> dict[str, dict[str, list[str]]]:
return json.loads(_CATALOG.read_text())
def services() -> list[str]:
"""Every subsystem the fleet offers, in a stable order."""
return sorted(_fleet())
def operations(service: str) -> list[str]:
"""One subsystem's operations, empty for a name the fleet does not serve."""
return list(_fleet().get(service, {}).get("ops", []))
def reach() -> int:
"""What this client can address, for anything reporting coverage."""
return sum(len(e["ops"]) for e in _fleet().values())
def _endpoint() -> str:
return os.getenv("HANZO_API_URL", "https://api.hanzo.ai").rstrip("/")
def _token() -> str:
for name in ("HANZO_API_KEY", "API_KEY", "API_TOKEN", "HANZO_TOKEN"):
value = os.getenv(name)
if value:
return value
return ""
def call(service: str, op: str, params: dict[str, Any] | None = None) -> Any:
"""Hand one operation to the fleet and return what it answered.
A refusal is returned as a refusal. Folding it into a success whose body
happens to say it failed is a refusal the caller cannot see, which is worse
than no answer because it is acted on.
"""
# The name is checked first because it is a fact held here, needing no
# credential. Demanding a key for a subsystem that does not exist sends the
# caller to fix a credential when the request is what was wrong.
if service not in _fleet():
raise ValueError(f"the fleet does not serve {service!r}; it serves {', '.join(services())}")
key = _token()
if not key:
raise RuntimeError("HANZO_API_KEY required")
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": service, "arguments": {"op": op, "input": params or {}}},
}
with httpx.Client(timeout=30.0) as client:
answer = client.post(
f"{_endpoint()}/v1/mcp",
json=body,
headers={"Authorization": f"Bearer {key}"},
)
answer.raise_for_status()
payload = answer.json()
if "error" in payload:
raise RuntimeError(payload["error"].get("message", "refused"))
result = payload.get("result", payload)
if isinstance(result, dict) and result.get("isError"):
content = result.get("content") or [{}]
raise RuntimeError(content[0].get("text", "refused"))
return result
class CloudTool:
"""The fleet behind one tool, routed by subsystem and operation.
One tool rather than 114, for the reason the fleet groups its own: a flat
projection is roughly a megabyte to enumerate, which a model pays for on
every turn and a client with a tool cap truncates. The subsystem names and
their operations are read from the generated catalog, so a subsystem the
fleet gains is reachable here the day the catalog is regenerated — there is
no list in this file to fall behind.
"""
@property
def name(self) -> str:
return "cloud"
@property
def description(self) -> str:
return (
f"The Hanzo Cloud API: {len(services())} subsystems, {reach()} operations. "
"Name a subsystem in `service` and one of its operations in `op`, and pass "
"that operation's own arguments in `input`. Omit `op` to list a subsystem's "
"operations; omit `service` to list the subsystems."
)
async def call(self, ctx: Any = None, **params: Any) -> Any:
service = (params.get("service") or "").strip()
if not service:
return {"services": services()}
if service not in _fleet():
return {"error": f"the fleet does not serve {service!r}", "services": services()}
op = (params.get("op") or "").strip()
if not op:
return {"service": service, "operations": operations(service)}
try:
return call(service, op, params.get("input") or {})
except Exception as exc: # a refusal is reported as one, never as an empty result
logger.debug("cloud %s.%s refused", service, op, exc_info=True)
return {"error": str(exc)}
def register(self, mcp_server: Any) -> None:
@mcp_server.tool(name=self.name, description=self.description)
async def cloud( # type: ignore[no-untyped-def]
service: str = "", op: str = "", input: dict[str, Any] | None = None
):
return await self.call(service=service, op=op, input=input)
+29
View File
@@ -0,0 +1,29 @@
[project]
name = "hanzo-tools-cloud"
version = "0.1.0"
description = "The Hanzo Cloud surface, generated from the fleet's own typed operations"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "cloud", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
]
dependencies = ["httpx>=0.27"]
[project.entry-points."hanzo.tools"]
cloud = "hanzo_tools.cloud:TOOLS"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
[tool.hatch.build.targets.wheel.force-include]
"hanzo_tools/cloud/catalog.json" = "hanzo_tools/cloud/catalog.json"
+81
View File
@@ -0,0 +1,81 @@
"""The cloud surface is GENERATED, so what is asserted is the generation.
The valuable assertion is the last one: the catalog this package ships must be
byte-identical to the one the TypeScript and Rust runtimes ship. Three copies of
one value is how they come to disagree, and nothing else here would notice.
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import pytest
from hanzo_tools.cloud import operations, reach, services
from hanzo_tools.cloud.cloud_tool import _CATALOG
def test_the_fleet_is_whole() -> None:
assert len(services()) > 100, f"expected the fleet, got {len(services())}"
assert reach() > 1000, f"expected the fleet's operations, got {reach()}"
def test_a_subsystem_carries_its_operations() -> None:
assert "iam" in services()
assert "get_iam_users" in operations("iam")
def test_what_the_fleet_withholds_is_absent() -> None:
# The rule is applied once, in cloud, and reaches here through the generated
# file: reading who holds a role is not the same act as granting one.
assert "post_iam_users" not in operations("iam")
assert "delete_iam_users" not in operations("iam")
def test_a_name_the_fleet_dropped_is_not_offered() -> None:
# paas became platform and storage became s3. A client still naming the old
# ones describes an API that has moved.
assert "paas" not in services()
assert "storage" not in services()
assert "platform" in services()
def test_an_unserved_subsystem_is_refused_rather_than_called() -> None:
from hanzo_tools.cloud import call
with pytest.raises(ValueError, match="does not serve"):
call("paas", "anything")
def test_every_runtime_ships_the_same_catalog() -> None:
"""The three runtimes must not drift.
Skipped rather than failed when a sibling is not checked out — absence of a
checkout is not evidence of disagreement, and a test that fails on a partial
workspace is a test people learn to ignore.
"""
mine = hashlib.sha256(_CATALOG.read_bytes()).hexdigest()
root = Path(__file__).resolve().parents[4]
siblings = {
"@hanzo/mcp": root / "mcp" / "src" / "tools" / "catalog.json",
"cloud": root / "cloud" / "fleet" / "mcp.json",
}
checked = 0
for name, path in siblings.items():
if not path.exists():
continue
checked += 1
assert hashlib.sha256(path.read_bytes()).hexdigest() == mine, (
f"{name} ships a different catalog than this package. "
f"Regenerate them together: `pnpm sync:catalog` in hanzoai/mcp."
)
if checked == 0:
pytest.skip("no sibling runtime checked out beside this one")
def test_the_catalog_is_the_shape_the_runtimes_read() -> None:
raw = json.loads(_CATALOG.read_text())
assert all(sorted(entry) == ["ops"] for entry in raw.values())
assert all(entry["ops"] for entry in raw.values()), "an empty enum is a tool nobody can call"