Nine per-service packages registered a hanzo.tools entry point and sat in
hanzo-mcp's default dependencies, so installing it put them in a model's tool
list beside the unified tool and without its filter.
hanzo-tools-kms offered inject, which lists a path and reads every secret at it.
hanzo-tools-iam offered create_user, delete_user and the token and session reads.
cloud withholds all of them from an agent; these were a second way to the same
API, so the rule held on one path and not the other.
The auto-registration is withdrawn, not the packages: they stay importable for a
human at a CLI, which is where reading a secret is a legitimate act. An agent
reaches all eight through the unified tool, verified against the live document.
mpc keeps its registration because the fleet has no equivalent, and auth keeps
its because it reads the caller's own claims rather than another identity's.
Which list routes insist on an owner is the server's rule, and it moves. The
deployed binary requires one on users, sessions and applications and not on
roles, permissions, providers, workspaces, invitations, tokens or audit-logs;
the change that resolves a listing's tenant centrally is written but unreleased.
So nothing here keeps a copy of that rule. The IAM tool no longer refuses a
missing owner on applications and sessions, get_applications takes an owner
rather than demanding one, and no docstring or parameter description promises
that leaving it off makes the credential decide — today that answers 400.
A refusal now reaches the caller as the reason IAM gave rather than as a
transcribed body: the RFC 9457 detail, or the envelope's msg where the route
answers the envelope, which is how memberships refuses.
Co-authored-by: Hanzo Dev <[email protected]>
A row is named by owner and name — /v1/iam/users/hanzo/z — read with GET,
replaced with PUT, removed with DELETE. The hand-written clients reached for
verb-noun addresses, and the CLI and route table reached for a verb hanging off
the collection; both spellings are gone from the server.
A collection GET answers a wrapper keyed by the plural, and the key is not
always the word in the path — audit-logs answers auditLogs. Every reader now
takes the key from the table and raises when it is absent, because reading a
missing key as [] reports "none" for a healthy org. A row answers the record
itself, absence is 404, and a refusal is an RFC 9457 problem document carrying
the reason. account, memberships, password and login keep the {status, msg,
data} envelope and have their own reader.
The credential goes in Authorization as client_secret_basic, which is where IAM
reads one; the pair used to ride the query string, where it authenticated
nothing and landed in every log on the path. A password rides beside the user
row rather than inside it, so IAM hashes it and nothing writes it in the clear.
An update reads the row before replacing it, since PUT replaces the whole
record.
Role membership is the role's own users list, so adding and removing a member
reads the role, edits the list and writes it back.
The IAM tool drops enforce, system_info and health; the team tool reaches
IAM's workspaces, memberships, invitations and account instead of deferring to
an address it never had. Both name organizations only when the caller does —
IAM scopes the read, and a hardcoded owner is a refusal for everyone outside it.
The CLI route table is checked against IAM's own registered routes, and every
address here was verified against the running server.
Co-authored-by: Hanzo Dev <[email protected]>
_users() called /v1/iam/get-users and then did
users = data if isinstance(data, list) else []
The verb answered a bare JSON list. The native /v1/iam/users answers
{"users": [...]}. So swapping only the path would have left that isinstance check
False and reported ZERO users for a healthy org — an empty result, not an error,
surfaced through an MCP tool an agent then reasons over. Both halves change here.
The dict branch also accepts "data" so a server still on the legacy envelope
keeps working during rollout; it goes when the compat surface does.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo-tools-iam built URLs as {base}/api/{path} — forbidden per HIP-0111
(no /api/ prefix; one way). Now {base}/v1/iam/{path}; health probe is
/v1/iam/healthz. Asserts JSON shape, not bare 200, to dodge the IAM SPA
catch-all that serves 200 text/html for unregistered paths. Adds a test
for _iam_url + IAM_BASE_URL. (Test harness wiring for this non-member
package is a separate packaging task — code verified: 0 /api/ remain.)
Follow-up to 9c377be (hanzo-iam 1.30.0). Consumer packages now read only
the canonical IAM_ prefix — no upstream-brand alias chain, matching the
new IAMConfig.ENV_PREFIX contract enforced by test_env.py.
Touched modules:
- hanzo-cli: hanzo_cli/auth.py, hanzo_cli/bot/commands.py
- hanzo-tools-auth: hanzo_tools/auth/session.py
- hanzo-tools-iam: hanzo_tools/iam/iam_tool.py
- hanzo: src/hanzo/commands/{auth,iam}.py
- hanzoai: auth.py, llm_client.py
- tests: test_auth_pkce.py (import IAM_CLIENT_ID)
Also adds the generated pkg/hanzo-iam/uv.lock — matches the convention
of the other 17 axis pkgs that track their lockfiles.