Commit Graph
21 Commits
Author SHA1 Message Date
zeekayandHanzo Dev d6ecee069c iam: pass the scope through, and let the server's refusal speak
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]>
2026-08-24 03:42:08 -07:00
zeekayandHanzo Dev 95e2025681 iam: address the rows IAM serves, and read the shape each route answers
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]>
2026-08-24 03:36:48 -07:00
zeekay 53cd11b0a5 hanzo-iam: one decoder, so a wrong path names itself
hanzo.id serves its sign-in SPA on every unmatched path, so a path IAM
does not serve answers 200 text/html. Forty-four call sites went straight
to .json() on the strength of the status code and died inside it with a
byte offset naming neither the path nor the reason. oauth was the one
module that asked, in a guard of its own.

routes.decode is that question, once, beside the unwrap that answers the
envelope one: the wire decision, then the envelope decision, each with a
name. A test walks the syntax tree and fails if any module decodes a body
on its own.

141 pass across the package and the two consumers that build on it.
2026-08-23 12:57:04 -07:00
zeekay 28035b6721 hanzo-iam: one client, one env reader, one verifier
The credential rode in the query string on AsyncIAMClient too — the
exported one — across seventeen call sites. Same fix as the sync client:
Authorization, Basic for a confidential client.

AsyncIAMClient was DEFINED TWICE, in client.py and async_client.py, and
the copies had drifted to nineteen methods against thirty-eight. __init__
exports the one in async_client.py, so the other was unreachable; it is
gone.

Three readers of IAM_* disagreed about the tenant: IAMConfig.from_env and
fastapi.configure both defaulted the org to the literal "hanzo", so a Lux
or Zoo process that forgot IAM_ORG did not fail — it addressed the hanzo
tenant and reported hanzo's users as its own. Now one reader,
IAMConfig.from_env(org), which seeds the endpoint and the tenant from the
org it is given; organization has no default at all. A test walks the
package and asserts config.py is the only module that reads the
environment.

IAMClient.validate_token and validate_token_with_cert were a second,
weaker verifier — no issuer check, no require:["exp"]. tokens.verify does
both and fails closed, so the strict one is what survives.

118 pass, and the seventeen tests of the two consumers that build on it.
2026-08-23 12:51:30 -07:00
zeekay 5bc4b34731 iam: the credential rides in Authorization, not the query string
hanzo-iam's admin calls sent clientId/clientSecret as query parameters.
IAM's app() reads httpx.Basic(c) — the Authorization header and nothing
else (internal/authz/authz.go) — so the pair authenticated nothing and
landed in every access log, proxy log and Referer on the path.

_admin_headers now renders it as HTTP Basic (RFC 6749 2.3.1), a bearer
still wins where one is held, and _admin_params is gone rather than left
returning an empty dict. The async class gets the same one helper.

test_credential_location pins it on both arms: put the pair back in the
query and the test fails on the string it leaks.
2026-08-23 12:23:26 -07:00
zooqueenandHanzo Dev 7ef22afa72 fix(iam): call the native routes, not the verbs the server is deleting
23 call sites across hanzo-iam and hanzo-tools-iam reached IAM over the legacy
verb surface — get-user, get-users, get-application(s), get-organization(s),
get-providers, get-role(s), get-user-count, get-user-roles, and the
oauth/access_token spelling. Those routes are being removed: HIP-0111 forbids
them, and a capability with an RFC uses its RFC.

Two things had to change together, which is why this adds routes.py rather than
editing the literals in place. The verb surface answered {status, msg, data} at
HTTP 200 even for a miss; the native surface returns the object at the top level,
uses real status codes, and NAMES its lists — {"users": [...]}, not
{"data": [...]}.

Swapping only the path is the dangerous half-migration, and hanzo-tools-iam shows
why: it did `users = data if isinstance(data, list) else []`, so against a native
response it would have reported ZERO users for a healthy org. Not an error — an
empty list. Both halves changed there.

routes.unwrap reads either shape so a fleet mid-rollout keeps working, and still
RAISES on the legacy error envelope because callers depended on that rather than
on a falsy return. It is deliberately temporary: once every server serves native
only, the envelope branch is dead and goes with it. Only a body whose keys are
exactly the envelope's is unwrapped, so a native row with its own `data` column
survives.

These admin methods had no coverage while they called the verb surface — the
reason migrating them blind was the risk. tests/test_routes.py pins it: no route
may contain "/get-", the token endpoint must be the one discovery advertises,
every list route must declare its key (a missing one unwraps to [] and reads as
empty), and neither client may carry a path literal. Falsified — restore a verb
in the table or in a client and the suite names it.

99 pass. tests/test_fastapi.py cannot collect in this environment
(starlette/httpx version clash, unrelated to this change); hanzo-tools-iam's
suite cannot either (`from mcp.server import FastMCP` against the installed MCP
SDK) — both verified pre-existing by reproducing them with these changes stashed.

Rebased onto upstream's IAM_ROUTE_PREFIX refactor rather than merged: that
refactor centralised the prefix but kept the verbs, so the two changes compose.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 15:41:22 -07:00
zeekayandHanzo Dev d08a7d5599 auth: verify tokens instead of asserting they exist
is_authenticated() was `load_token() is not None` — a string-presence check
that returned True for the literal "fake.not.a.real.jwt". Every permission
decision downstream of it was therefore a lie. This makes the client side of
Hanzo auth actually work, end to end. Builds on the /v1/iam prefix already on
main; the endpoint paths were necessary but not sufficient.

hanzo_iam.tokens — the ONE credential judge. Verifies the signature against
the issuer's published JWKS plus exp/iss/aud, and fails CLOSED: an unreachable
JWKS is not a pass, because a client that cannot check a signature does not
know the token is good. Reason codes distinguish "expired" from "offline".
`alg: none` is absent from the accepted list by construction. An opaque API
key cannot be judged offline, so it is confirmed against userinfo rather than
assumed valid for being a non-empty string.

hanzo_iam.oauth — a real login: authorization code + PKCE (S256) over a
loopback redirect, with a deadline. Not the device grant, which would be the
better CLI UX: iam implements RFC 8628 fully and advertises it, but no PUBLIC
client is registered, so POST /v1/iam/oauth/device answers 401 invalid_client
for hanzo-cli, hanzo-app, hanzo-cloud and hanzo-console alike. Registering one
public app is the whole server-side fix; shipping a device path that always
401s would be a lie in code. The password grant is out for the same reason
(401 invalid_client without a secret), so `--no-browser` now prints the URL
instead of prompting for a password it cannot use.

The loopback listener binds a REGISTERED redirect_uri. iam compares
redirect_uri by exact string and does not apply RFC 8252 §7.3 port-agnostic
loopback matching, so a CLI cannot pick a free ephemeral port — the previous
code bound 8399 (cli) and 8398 (bot), neither registered, and /authorize
refused both with a bare 400 before the user saw a login page. It listens on
127.0.0.1 and ::1 because the registered URIs spell "localhost", and sets
SO_REUSEADDR because the previous callback sits in TIME_WAIT for ~60s and
would otherwise EADDRINUSE a second login on a port nothing is using.

hanzo_iam.store — keyring first, else an atomic 0600 file. The old path did
write_text() then chmod(0600), so with umask 022 a bearer token sat at 0644
between the two calls. Now it is created private and renamed into place. The
PaaS session cache goes through the same writer.

Two definitions of IAMConfig existed; models.py's was the one every client
imported, so the endpoint properties main had just fixed on config.py's were
dead code. One now, in config.py, plus jwks_uri and device_endpoint.

The bot and CLI login copies are gone; both call the one flow. whoami and
`hanzo bot login` no longer print claims from an unverified decode — that
rendered an attacker-chosen identity as fact.

The Team tool refuses instead of pretending: api.hanzo.ai/team is the
marketing SPA (200 text/html), so raise_for_status() passed and .json() blew
up. There is no Team API; HANZO_TEAM_URL stays as the seam for when one ships.

PaaS token exchange reports its real diagnosis: POST /v1/auth/login 404s while
/v1/org and /v1/user 401, so the API is up and gated but the exchange route is
not deployed at that edge. No client-side workaround exists, so it says so.

Tests fail against the old behaviour, which is the point: 5 session tests fail
against the shipped is_authenticated and 4 store tests fail against
write-then-chmod. 134 pass alongside main's KMS suite.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:55:24 -07:00
zeekayandHanzo Dev 6a0898b300 fix(sdk): single-source every package version; patch-bump the two changed pkgs
Same drift as the CLI, in all three packages: a hardcoded __version__ that no
longer matched pyproject.

  hanzo_iam/__init__.py  1.1.1  vs pyproject 1.30.0   (29 minor versions apart)
  hanzo_cli/__init__.py  0.1.0  vs pyproject 0.2.2

The hanzo_cli one was load-bearing: click's version_option reads it, so it is
the reason `hanzo --version` answered "0.1.0" — a release that never existed.
All three now resolve from the installed distribution, so there is one answer.

Patch bumps for the two packages changed in 68877409 (never a lazy major):
  hanzo-cli  0.2.2 -> 0.2.3   entry point removed, __main__ added, PKCE, token path
  hanzo-iam  1.30.0 -> 1.30.1 endpoint properties on the exported IAMConfig

Verified on a clean venv: __version__ == importlib.metadata.version for all
three, `hanzo --version` 0.4.4, `python -m hanzo_cli --version` 0.2.3, 58
commands. hanzo-iam 19/19; hanzo-cli's failures are unchanged live-service auth.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 14:54:28 -07:00
zeekayandHanzo Dev 9a8b4b5c46 fix(cli): pip install hanzo shipped a 10-command CLI, not the 56-command one
`hanzo` depends on `hanzo-cli`, and BOTH declared `[project.scripts] hanzo`.
Entry-point resolution is install-order dependent, so which CLI a user got was
a coin flip — and hanzo-cli kept winning. The result:

  $ hanzo auth login
  Error: No such command 'auth'.

So the "auth login is broken by Cloudflare 1010" story was real but unreachable:
users never got as far as the HTTP call, because the binary they had did not
have an `auth` command at all. Publishing that fix would have changed nothing.

One command, one owner:
- hanzo-cli drops its console script. `hanzo` is the ONE command.
- `paas` and `bot` are mounted into it — the only two groups with no equivalent.
  `kms` is NOT mounted: `hanzo secrets` is a strict superset (audit/grant/
  revoke/rollback/rotate/versions on top of the same get/list/set/delete), and
  two commands for one concern is what we are removing.
- hanzo_cli grows a __main__ so it stays invocable as `python -m hanzo_cli`,
  and its e2e suite targets that instead of whatever `hanzo` resolves to.

Endpoints, one definition:
- models.IAMConfig (the class the package EXPORTS) gains the HIP-0111 endpoint
  properties. config.IAMConfig had them, but nothing imports that one — a
  second same-named class nobody uses is how the legacy path survived.
- config.py + fastapi.py stop hand-assembling paths and read the constants.
- password_login posted to `/oauth/token`, which is not a 404: IAM serves a
  200 text/html SPA catch-all for unregistered paths, so it received a login
  PAGE and json() blew up on HTML. Verified: legacy 200 text/html vs canonical
  401 application/json.

PKCE, because this is a public client:
- browser_login ships no client_secret, so the authorization code was the only
  secret in the flow and it arrives over a plaintext loopback redirect. The SDK
  already accepted code_challenge/code_verifier; the CLI simply never passed
  them. RFC 8252 §8.1 / RFC 7636 S256.

Version, one source:
- pyproject said 0.4.4, __init__ said 0.3.47, cli.py said 0.3.48, and the
  installed binary reported 0.1.0 — four answers, none right. Now resolved
  from the installed distribution.

Verified on a clean venv: entry-point providers 2 -> 1, commands 10 -> 58,
`hanzo --version` 0.1.0 -> 0.4.4, all four endpoints canonical. hanzo-iam
19/19 pass; hanzo-cli's 20 failures are unchanged before and after and are all
live-service auth (no stored token in this environment).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 14:51:42 -07:00
zeekay c53d6a7c81 refactor(iam): route every OIDC call through the canonical path constants
The client had OIDC paths written inline at each call site, so the canonical
route existed in as many places as it was used and drifted per method. Moves
them to named constants in models.py — OIDC_DISCOVERY_PATH, OIDC_JWKS_PATH,
OIDC_TOKEN_PATH, OIDC_INTROSPECT_PATH — and has both the sync and async
clients read those.

Values match the canonical IAM surface (HIP-0111): discovery
/.well-known/openid-configuration, jwks /v1/iam/.well-known/jwks, token
/v1/iam/oauth/token, introspect /v1/iam/oauth/introspect, authorize
/v1/iam/oauth/authorize. No legacy /oauth/* and no /api/ prefix.

One definition per route, so sync and async can no longer disagree.
2026-07-26 01:55:28 -07:00
bd0307b62b refactor(iam): drop Casdoor branding — Hanzo IAM only (white-label) (#41)
Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 14:56:32 -07:00
zeekayandGitHub 7f932fc17d fix(iam): migrate userinfo/get-account to canonical /v1/iam/oauth/userinfo (HIP-0111) (#38)
Co-authored-by: Zach Kelling <[email protected]>
2026-06-24 19:16:18 -07:00
zeekay 1aed790688 deps: drop legacy HANZO_IAM_/HANZO_CLIENT_ID env-var fallback in consumer pkgs
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.
2026-05-14 23:39:31 -07:00
zeekay 346484d664 feat(hanzo-iam): canonical IAM_ env contract + FastAPI integration (1.30.0)
Bumps hanzo-iam from 1.29.0 (older Casdoor SDK port) to 1.30.0 with a
purpose-built IAM SDK aligned to the canonical IAM_ env contract.

- ENV_PREFIX="IAM_" only — no HANZO_IAM_ / {ORG}_IAM_ fallback chain.
- IAMConfig.from_env reads IAM_ENDPOINT, IAM_CLIENT_ID, IAM_CLIENT_SECRET,
  IAM_ORG, IAM_APP, IAM_CERT, IAM_USERINFO_URL.
- New FastAPI middleware module wraps Casdoor sign-in flow with
  request-state user injection.
- 19 unit tests covering env-read, validation, and FastAPI integration.

Consumer packages (hanzo-cli, hanzo-tools-auth, hanzo-tools-paas) bump
their hanzo-iam dep to >=1.30.0.

Tag: hanzo-iam-1.30.0
2026-05-14 20:56:32 -07:00
zeekay 4ebf26df3e feat(mcp): unify tool surfaces, rename iching→zen, fix test infrastructure
- Consolidate memory tools into single unified surface
- Rename IChingTool → ZenTool across agent tools
- Rename tool files: proc_tool→exec_tool, net_tool→fetch_tool,
  vcs_tool→git_tool, todo_tool→tasks_tool
- Add hanzo_tool.py for unified Hanzo API surface
- Add workspace_tool.py for config management
- Add hanzo-tools-ui package with GitHub API integration
- Add unified.py core tool dispatcher
- Update IAM client to RFC-compliant OAuth endpoints
- Fix pytest-asyncio plugin conflict: pin <1.0.0, fix addopts
  double-registration, and align test APIs to current signatures
- Fix integration tests: PermissionManager API, ReviewProtocol
  focus values, AgentTool parameter names, mock llm module for CI
2026-03-06 03:13:54 -08:00
zeekay 8c05bf99d6 fix(ci): pin black <26.1 to avoid internal error, lower requires-python to 3.12
Black 26.1.0 crashes on multiple exception types in parentheses.
The requires-python bump to 3.14 broke CI which runs Python 3.12.
2026-03-01 16:01:16 -08:00
zeekay 6c376a4d13 [chore] bump all packages to Python 3.14+
Update requires-python to >=3.14 in all 46 pkg/ pyproject.toml files
and the root pyproject.toml (47 files total).
2026-02-28 12:43:10 -08:00
zooqueenandHanzo Dev b144973bfc fix(lint): resolve all ruff + black errors across all packages
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310,
S306, S110, TID251, UP042, F401) and run black on 526 files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-21 23:32:32 -08:00
zooqueenandHanzo Dev 920f767417 style: auto-fix ruff lint and format for Python 3.12
Run ruff check --fix and ruff format across the repo to fix ~495
auto-fixable lint errors (mostly I001 import sorting) and reformat
225 files to match the py312 target.

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-20 21:29:31 -08:00
zooqueenandHanzo Dev 5276a58d19 feat: add hanzo-cli unified CLI with IAM, KMS, PaaS management
- New hanzo-cli package (0.1.0) providing the `hanzo` command
- Auth: browser OAuth + ROPC password login against hanzo.id (Casdoor)
- IAM subcommands: users, user, set-password, orgs, apps, sync-app
- KMS subcommands: list, get, set, delete, inject
- PaaS subcommands: orgs, projects, envs, deploy (create/status/logs/redeploy/env/delete)
- PaaS deploy supports both Docker images (--image) and git repos (--repo)
- Sticky org/project/env context for PaaS commands
- PaaS client with IAM→PaaS session exchange and auto-refresh
- hanzo-iam SDK v1.1.0: bearer_token auth, admin helpers, set_password/get_applications/update_application

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-18 20:58:43 -08:00
zeekay 8e0f575a1c feat: auth/IAM refactor + add hanzo-iam and hanzo-web3 packages
- Rewrite auth.py with full OAuth token management flow
- Rewrite iam.py with comprehensive IAM commands (users, orgs, apps)
- Add hanzo-iam package for IAM SDK
- Add hanzo-web3 package for blockchain SDK
- Update API providers for new auth flow
2026-02-12 20:47:40 -08:00