Minting is issuing authority. IAM mints — it signs the token, and that
signature is where the authority is created. tasksd verifies one and
restates it as headers a transport that cannot carry a token can read.
Nothing here creates authority, so naming both operations "mint" put the
issuer's job and this service's under one word, and a word that covers
two operations is a word that stops distinguishing them.
Every place that named the header write "mint" now names it "write",
matching the leaf it already cites: hanzoai/authz names its header list
"once by the party that WRITES their values". The four ctx accessors said
"minted" for a value that is neither issued nor a header — they return
what the validated claims RESOLVED to, which is the word the org scoping
already used for Claims.EffectiveOrg.
"mint" stays where it is the right word: the test helper signing a JWT,
the `owner` claim IAM stamps at sign time, the HMAC task tokens dispatch
issues, and money mint/burn in CONTRACT.md.
LLM.md also cited edge.Inject, which no longer exists. The call is
edge.Apply — the name it took when the four readers collapsed onto one
leaf, precisely because writing headers is not injecting authority.
Co-authored-by: Hanzo Dev <[email protected]>
authz v1.10.26: the `owner` claim carries the org of the APPLICATION a token was
minted through, so reading it as the subject's org made a person's durable state —
engine.As(Org(...)) scopes the store by it — follow whichever app they signed in
through. It is the first entry of the signed membership set now.
A token stating no org gets none, with no fallback, so tasksd refuses it when
identity is required rather than scoping a namespace to an app-selected value.
Also edge.Inject→Apply and edge.Mint→Render: minting is issuing authority and only
IAM does that.
Co-authored-by: Hanzo Dev <[email protected]>
TASKSD_WHITELABEL_ISSUERS adds the brands this tasksd accepts tokens from beyond
the primary, each of which signs its own. Adding one is configuration.
The sharp edge is the empty entry: splitting an UNSET env var yields one empty
string, and an empty trusted issuer would match a token that carries no `iss` at
all. Entries are trimmed and dropped, pinned by
TestIssuerAllowlistDropsEmptyEntries — which builds the verifier through the same
code path production uses, so the guard is tested where it runs rather than in
isolation.
Co-authored-by: Hanzo Dev <[email protected]>
pkg/auth held 190 lines of JWKS cache and JWT verification — a fourth independent
reading of what an IAM token means — and it had two defects the shared reader does
not:
it verified against EVERY key in the published set and took the first that
worked, so a token naming kid A was accepted on a signature from key B. Naming a
key is what `kid` is for.
its issuer comparison was CONDITIONAL on the configured issuer being non-empty,
so an unset TASKSD_JWT_ISSUER did not fail closed — it accepted tokens from any
issuer.
It also stripped only the four names it reads. Every other minted header —
X-User-IsAdmin and X-User-Permissions (the platform and money signals),
X-User-Owner, X-Billing-Account-Id, X-Scope, X-Workspace-Id, and the retired names
— passed through from the client untouched. tasksd terminates identity itself (its
ingress routes straight to the Service), so nothing upstream was deleting them
either. Restoring the four-name strip turns
TestRequireIdentity_StripsTheWholeEstateSet red on seven forged headers.
And it had NO machine predicate at all, so an admin-org client_credentials token
arrived as a person. It now reads the membership set, like the rest of the estate.
What remains is the part that is genuinely tasksd's: which environment variables
the policy comes from. The audience stays optional and that is deliberate rather
than an oversight — IAM sets `aud` to the requesting CLIENT (RFC 8707), and tasksd
is not a client id, so it pins the issuer and the signing keys instead.
Both transports now answer "which org does this act in" with the same function,
Claims.EffectiveOrg(""), rather than the HTTP path reading `owner` and the ZAP path
reading it separately.
Cost, measured: 342 linked packages and an 18.3 MB binary, DOWN from 345 and
18.6 MB — dropping go-jose more than paid for the leaf, and tasks links ZERO zip
packages because the edge names no transport. go-jose is gone from the module
entirely; the tests now mint with golang-jwt, the library IAM signs with.
Rebased onto 12 upstream commits: kept their ServeGated(addr string) signature,
their engine.As(Org(...)) rename, their dependency versions, and the LLM.md fact
about explicit project/user isolation. Their only change to the deleted jwt.go was
a comment naming hanzo.id, which the replacement already says.
Co-authored-by: Hanzo Dev <[email protected]>
The claims struct comment credited a third-party identity server. Hanzo IAM
is original work; the vendor-derived server was hanzoai/iam-v1, retired.
The claim set described (owner/sub/email) is unchanged.
The bare /.well-known/jwks form is only answered by the hanzo.id-worker
shim, which is being retired; IAM's discovery document advertises
/v1/iam/.well-known/jwks on both the public host and in-cluster.
The IAM identity model is org/project/user; tasks carried only org+user.
IAMClaims gains `project`; RequireIdentity mints X-Project-Id (strip-list
⊇ mint-list unchanged — the header is stripped on every inbound request
and set only from a validated JWT); WithIdentity gains the project param
(in-process embedders pass their validated project through); ProjectID(ctx)
accessor. Convention: a project maps onto a tasks NAMESPACE inside the
org's shard.
Co-authored-by: hanzo-dev <[email protected]>
Enables mounting the Tasks server natively inside the unified hanzoai/cloud
binary (HIP-0106) without a JWT re-validation hop or a duplicate sqlite driver.
- auth.WithIdentity injects an already-validated identity (org/user/email)
into the handler context — the in-process twin of RequireIdentity's mint
step, for an embedder that terminates the IAM trust boundary at its gateway
before the request reaches Tasks.
- store opens through github.com/hanzoai/sqlite (the one Hanzo sqlite driver)
instead of blank-importing modernc directly, so a Tasks store embedded in
the cloud binary shares the single "sqlite" driver registration rather than
double-registering it and panicking at init. WAL/pragmas move to explicit
schemaSQL so they apply on both cgo and !cgo backends.
tasksd is now self-sufficient multi-tenant: no gateway dependency
required. Every inbound request has client-supplied X-Org-Id /
X-User-Id / X-User-Email stripped unconditionally; if Authorization:
Bearer <jwt> is present, the token is validated against the configured
JWKS (TASKSD_JWKS_URL), checked against issuer/audience, and identity
headers are minted from validated claims.
Closes the spoofable-identity hole on tasks-api.hanzo.ai: any request
with a self-set X-Org-Id previously passed through and got that org's
scoped engine.
Env:
TASKSD_JWKS_URL e.g. https://hanzo.id/.well-known/jwks
TASKSD_JWT_ISSUER e.g. https://hanzo.id
TASKSD_JWT_AUDIENCE optional
TASKSD_REQUIRE_IDENTITY=true rejects unauthenticated requests
hanzoai/gateway validates the IAM JWT and forwards X-Org-Id, X-User-Id,
X-User-Email. tasksd reads them via auth.RequireIdentity and exposes
typed accessors. TASKSD_REQUIRE_IDENTITY=true rejects unauthenticated
requests in production; default false keeps embedded/dev usable.