Hanzo AI

Authz

The access decision behind an org-scoped call: who may do what, where, answered allow or deny.

Also for this capability: API · CLI · MCP · SDKs

The access decision behind an org-scoped call: who may do what, where, answered allow or deny.

Base URLhttps://api.hanzo.ai
Operations3
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1041 · Authz — Draft · read the specification →

/v1/authz is the access decision as a service: one question — may this subject take this action at this path — answered yes or no. The decision itself is github.com/hanzoai/authz, a pure library every Hanzo Go service links; the HTTP surface is that library's serve package, mounted by hanzoai/cloud.

The surface is STATELESS. It holds no policy store, and a check carries the grants it is to be decided against. IAM signs the grant set, so IAM owns it, and a second writable copy behind this door would be a second source of truth for who may do what (serve/mount.go:23-28).

Motivation

Two services that each decide access their own way disagree eventually, and the disagreement is silent until it is a breach. The decision is therefore one function in one library, and this capability exists for the callers that cannot link it — a service in another language, or a caller that wants the answer without the grants leaving the process that already holds them.

Making it a service is the small part. Keeping it from accumulating a policy store is the whole design.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

The calculus

A Path locates a resource as ordered segments, the first of which is the org. A Grant is one access fact: a subject holds a role everywhere at or below a scope path, until an expiry. A Verb is read or write, derived from the HTTP method by one mapping so no caller can disagree with IAM about whether a method writes (grant.go:106).

Roles are exactly three — member, admin, owner — because IAM's membership vocabulary is exactly three. member admits read; admin and owner admit both, folded because IAM assigns owner to whoever creates an org and a table matching only admin would refuse every founder their own org (grant.go:128-129). An unknown role admits nothing.

The predicate: subject may act iff some grant it holds has a scope covering the target and a role admitting the verb, evaluated at the current instant (grant.go:177). It FAILS CLOSED — no subject, no target, or an empty grant set is a denial.

Narrower authority is expressed as a NARROWER GRANT, never a new role. A delegation to an agent and a credential that expires are this same value with a smaller scope and a set expiry; neither is a separate mechanism (grant.go:149).

The org is the frame, and it is verbatim

A check MUST carry the gateway-minted org. A request with no org MUST be REFUSED rather than decided against an empty or default scope: collapsing tenants together is the one failure a decision service must not have.

Both the target path and EVERY grant scope MUST have that org as their first segment, and the comparison is verbatim (serve/mount.go). An org is a tenant-chosen identifier, so it is never case-folded — unlike a role, which is a closed vocabulary IAM controls.

What this surface does not do

It MUST NOT store policy, mint grants, or read a grant set of its own. Everything it decides against arrives in the request, which is what makes the answer a pure function of its inputs and the service safe to run anywhere.

Two artifacts around the door disagree with that today, and both are the door's problem rather than the library's. The fleet manifest routes a policies prefix here that nothing registers — a name resolving to no handler. And the prose published for the check describes a stored per-org policy set and a three-field body, where the mounted handler takes the grants in the request (serve/mount.go:49-58, hanzoai/authz v1.10.31). A description of state this surface does not hold is worse than no description: it tells an integrator to look for a policy API that was deliberately not built.

Liveness reads no tenant

The liveness and readiness reads are unauthenticated and never org-scoped. They report a property of THIS PROCESS, and a probe that needed a tenant would fail for reasons that have nothing to do with the process being alive (plugin/authz/main.go, the Describe prose).

The surface, whole

Three routes, and no others: POST /v1/authz/check, GET /v1/authz/health, GET /v1/authz/readyz (serve/mount.go:31-44). None is a typed operation, and none can be: authz is a leaf module that must never import cloud, so both seams a subsystem normally types through are closed to it, and each operation is instead DECLARED — prose registered beside the route (plugin/authz/main.go:28-49). §3 records that the check's declared prose is currently wrong; the defect is in the prose, not the contract.

The capability owns no store (§3 is why). It is FREE — no meter, no debit through any plane (plugin/authz/main.go, Price: cloud.Free). It publishes no event, so a customer's webhooks receive nothing from it. It emits nothing to observability beyond the request span every route already gets. Its stage is ga. It derives from no outside project: github.com/hanzoai/authz is our own leaf, and nothing in it forks, embeds or mirrors an upstream.

The registry predicate is a different question

Authority over IAM's own identity rows — who may mint a credential, who may administer a user — is a SECOND predicate over entities addressed by (owner, name), with a reserved-owner gate and a per-client capability allowlist (entity.go). It is not expressible as a location and MUST NOT be folded into the grant calculus. Two questions, two predicates, no overlap.

Rationale

The alternative is the ordinary one: a policy engine with stored policies per tenant, and a CRUD surface to edit them. It puts the authority to grant authority behind this door, which means this door is now identity infrastructure and must be protected as such — while IAM, which signs the memberships, still holds its own copy. Keeping the store in one place and passing the grants costs a slightly larger request body and removes the whole class.

The pure decision stays in a leaf module that links no HTTP stack and no driver, so adding a caller costs nothing (serve/mount.go:1-6). Speaking HTTP is I/O and lives in its own package.

Security Considerations

The caller supplies the grants, so the ONLY thing standing between a caller and a forged authority is the org frame: every scope is verified to be at or below the caller's own validated org before it can admit anything. A decision service that accepted grants scoped anywhere would be a "may I?" oracle that answers whatever the asker prefers.

Failing closed matters more here than usual. An empty grant set, an expired grant and a malformed path all deny, so a caller cannot produce an allow by sending less.

Liveness answering while every tenant's state is cold is deliberate and is not an authorization surface: it discloses that a process is running and nothing about who may do what.

Four surfaces

SurfaceReaches this capability asCoverage
RESTauthz at its own prefix3 operations
CLIhanzo authz …3 of 3
SDKAuthzApi in every published client3 methods
MCPtool authz on https://api.hanzo.ai/v1/mcp2 operations

Quickstart

export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys

Then the first call — a read that needs nothing but the key. GET /v1/authz/health, operation get_authz_health:

hanzo authz health

Endpoints

EndpointWhat it does
POST /v1/authz/checkAsk whether a subject may act on an object
GET /v1/authz/healthLiveness of the policy engine
GET /v1/authz/readyzReadiness of the policy engine

All Hanzo APIs · Interactive reference

How is this guide?