10 Commits
Author SHA1 Message Date
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 c80140810a kms: migrate hanzo-kms off Infisical paths onto the luxfi/kms surface
kms.hanzo.ai runs luxfi/kms, which has never served an /api/* route. This
SDK was sending Infisical's: /api/v3/secrets/raw for every secret operation,
/api/v3/auth/login for user auth, /api/v1/auth/kubernetes-auth/login for k8s.
Only one call in the whole package — /v1/kms/auth/login — was ever real.

It stayed invisible because older luxfi/kms builds embedded a console SPA
behind a root catch-all that answered every unmatched path with 200
text/html. A wrong URL came back as a JSON decode error, which reads like a
parsing bug in the client rather than "this endpoint does not exist." That
catch-all is gone (cmd/kms/main.go ends in notFoundJSON), so those paths now
return honest JSON 404s and the SDK fails outright.

The route table is not the only thing that was wrong. The data model was
Infisical's too — project_id, workspace, secret version, secret comment,
shared-vs-personal type — and luxfi/kms has none of those concepts. It keys
one value by (org, path, name, env) in ZapDB at kms/secrets/{path}/{env}/{name}
and a write upserts it in place. Half-migrating would have left response
models that cannot validate what the server sends, so the vocabulary moves
with the paths:

  list_secrets(path, env) -> names   GET  /v1/kms/orgs/{org}/secrets
  get_secret(path, name, env)        GET  /v1/kms/orgs/{org}/secrets/{path}/{name}
  put_secret(path, name, value, env) POST /v1/kms/orgs/{org}/secrets   (create AND replace)
  delete_secret(path, name, env)     DELETE .../secrets/{path}/{name}
  health()                           GET  /v1/kms/healthz

Two server behaviors now have one home, hanzo_kms/routes.py, shared by the
sync and async clients so they stay mirror images:

  - The server splits the trailing path at its LAST slash into (path, name),
    so each segment is escaped individually. Escaping the joined string
    encodes the separators away and the server reads one long name. A name
    containing "/" is rejected outright: it would be written under one key
    and read back under another, so the write looks like it succeeded and
    the read never finds it.
  - There is no versioned read. get_secret(version=N) raises
    VersionUnsupportedError rather than quietly returning the current value.

org is new and required — it scopes both the URL and the JWT owner claim.
Constructor field, HANZO_KMS_ORG, defaults to "hanzo".

Auth collapses to what the server actually offers: client credentials
exchanged at /v1/kms/auth/login, or a pre-issued IAM bearer token. The AWS,
Azure, GCP, Kubernetes and SRP methods were Infisical's and none were served.
That also fixes a divergence where the async client silently ignored
HANZO_KMS_TOKEN — env parsing is now one function.

Callers migrated with it: hanzo-cli's `hanzo kms`, the hanzo-tools-kms MCP
tool, and hanzo-tools-auth's session client, all of which were building the
removed auth models and calling the removed create/update pair. Each now
constructs KMSClient() and lets it read the environment.

Tests: pkg/hanzo-kms/tests pins the wire shape — no request URL may contain
"/api/", per-segment escaping survives the server's last-slash split, sync
and async emit byte-identical request lines, and a decoder rejects the
200-HTML/JSON-404 shapes instead of reading them as empty results. Wired into
hanzo-packages-ci without `|| true`, so a regression turns CI red instead of
waiting for a decode error in production.

hanzo-kms 1.1.0 -> 1.1.1.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 08:29:59 -07:00
zeekay c3dd9e752a release: bump auth-fixed packages for publish
Ship the canonical hanzo-app client_id + /v1/iam/get-account fixes:
- hanzoai 2.2.2 → 2.2.3
- hanzo-cli 0.2.1 → 0.2.2
- hanzo 0.4.2 → 0.4.3
- hanzo-tools-auth 0.1.0 → 0.1.1
2026-06-30 22:21:53 -07:00
zeekay 6837c96a23 auth: align all SDK packages to canonical <org>-<app> client ids
IAM seeds apps as <org>-<app> (iam cmd/iam/cli/init_apps.go:57-59):
hanzo-app, zoo-app, lux-app — AppName == ClientID. Several packages used the
inverted 'app-hanzo'/'app-hanzobot' or placeholder '*-client-id', none of which
IAM knows — device/PKCE login would fail. One value, everywhere:

- hanzoai/auth.py: login_with_pkce default -> IAM_CLIENT_ID (hanzo-app)
- hanzo-cli/auth.py: DEFAULT_APP, DEFAULT_CLIENT_ID -> hanzo-app
- hanzo/commands/auth.py: IAM_CLIENT_ID -> hanzo-app
- hanzo-tools-auth/session.py: DEFAULT_APP, DEFAULT_CLIENT_ID -> hanzo-app
- hanzo/commands/iam.py: --app default + doc example -> hanzo-app
- hanzo-cli/bot/commands.py: BOT_IAM_APP/CLIENT_ID app-hanzobot -> hanzo-bot
2026-06-30 17:23:10 -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 32aea4054b fix(ci): resolve root lint failures across workspace packages 2026-03-04 12:15:14 -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
zeekay 0f3ad2483c feat(mcp): add auth, kms, and platform MCP tools
Add 3 new hanzo-tools-* packages exposing Hanzo platform services via MCP:

- hanzo-tools-auth: HanzoSession singleton + LoginTool (status/whoami/logout/refresh)
- hanzo-tools-kms: KMSTool (list/get/set/delete/inject secrets via KMS)
- hanzo-tools-platform: PlatformTool (PaaS deployments, IAM users/orgs, cloud services)

Wire into hanzo-mcp as dependencies and register in entrypoint loader.
2026-02-24 21:56:50 -08:00