Admin
Admin — reached at /v1/admin; not in the public REST contract.
Admin ships in every Hanzo cloud. It carries no operations in the public API document, so it has no generated REST reference — it is reached another way.
| Reached at | /v1/admin — the operator surface, SuperAdmin-only and not in the public contract |
| In the public contract | no — see below |
Why there is no reference here
This reference is generated from the public API document, one page per operation. This capability contributes no operation to that document — which is a fact about its door, not about whether it is finished. It is GA and it runs in every deployment. It is reached at /v1/admin — the operator surface, SuperAdmin-only and not in the public contract.
Specification
HIP-1320 · Admin — The Operator Console — Draft · read the specification →
/v1/admin is the operator's view of the fleet: the tenant directory, what each
org is spending and owes, the credit an operator grants, the flags and caps it
sets, the infrastructure it watches, and what each host is running. It is
hanzoai/cloud apps/admin, and it is the one capability whose audience is not
a customer.
It owns no store. Every panel is a read fanned into IAM, commerce, the ledger, o11y and the cluster; every mutation is somebody else's write, recorded first in the tamper-evident trail. What admin owns is the ADMISSION and the AGGREGATION — one predicate at the door, and the fold that turns a hundred upstream reads into one board.
This HIP states that surface, the two-tier gate it admits through, and why a
capability nobody outside the reserved org can call is nonetheless ga.
Motivation
Every other capability in the fleet answers to a customer, and the shape of a capability HIP assumes one: the store it owns, the tenant a request becomes, the unit it meters. Admin answers to the operator and inverts each of those — no store, a caller who is deliberately NOT pinned to one tenant, and nothing metered at all. A specification that only describes the customer-facing shape leaves the one surface that can grant credit, set a cap and reload a binary as the only surface with no spec.
The address family is already specified: HIP-0139 §3.2 fixes /v1/admin/<name>
as the operator's view of <name>, served by <name>, dropped from the public
document by address. What was never written down is the capability at the ROOT
of that family — the console itself, which serves /v1/admin and is not the
operator view of anything.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 One root, two kinds of address under it
Every route admin serves MUST be under /v1/admin (manifest/apps.go:
{Name: "admin", Prefixes: []string{"/v1/admin"}}). The surface is 55 paths and
60 operations, in 33 groups (plugin/admin/openapi.json, measured
python3 -c "import json; d=json.load(open('plugin/admin/openapi.json'))",
2026-08-20).
Two kinds of address share that root and MUST NOT be confused:
- Admin's own nouns — the console.
me,overview,orgs,users,customers,usage,analytics,revenue,money,finance,invoices,subscriptions,grants,promos,caps,flags,roles,applications,products,providers,services,subsystems,infra,volumes,compute,bases,metrics,aimetrics,o11y,audit,sync,waitlist,plugins. The second segment names a thing the OPERATOR works with, and admin is the capability that answers for it. - Another capability's operator view —
/v1/admin/<name>/*, served by<name>and not by admin (HIP-0139 §3.2).referralsis the worked example:/v1/admin/referrals/bonusesis the referrals capability's row in the manifest, not admin's.
A reader tells them apart by the manifest, which is the only place that decides:
the app whose row carries the prefix is the app that serves it. Admin MUST NOT
claim /v1/admin/<name> for a <name> that is itself a capability, and a
capability MUST NOT serve /v1/admin/<noun> for a noun that is admin's.
/v1/admin/plugins is the case that looks like the second and is the first.
What it changes is not a capability's state but the BINARY a host is running,
and a binary belongs to the fleet rather than to any one capability, so the
console that watches every capability is the capability it belongs to (§7).
§2 The gate: one predicate, two tiers
Admission is decided at the top of every handler by one of two functions, and by
nothing else (apps/admin/core/typed.go).
core.Admit — platform tier. Admits if and only if principal.IsSuperAdmin
holds, which is HIP-0118's predicate and no second term. Everything that reads
across tenants or changes fleet state goes through it.
core.AdmitScoped — org tier. Admits a SuperAdmin unconditionally, or a
caller for whom THREE facts all hold: the caller administers their own org
(principal.IsOrgAdmin), the caller is a validated principal pinned to that org
(principal.Org), and that org is an enabled white-label tenant
(State.IsWhiteLabelTenant). The allowlist is seeded from
ADMIN_WL_TENANT_ORGS and is fail-closed: unset means SuperAdmins only.
Passing the gate is not the end of the scoping — the handler then folds every
read through ResolveScope/ScopedOrgs, which hard-limits a non-super caller to
their own org subtree whatever the request says.
Three rules follow, and each is a refusal:
- The predicate is EVALUATED ONCE, at the identity boundary, and thereafter
READ.
SanitizeIdentitydeletes every authority header a client sent, then mintsX-User-IsAdminonly from a validated credential whose signedorgsset contains the reserved org (middleware_identity.go). It is never restored from client input on any path, including the anonymous one. A capability MUST NOT re-derive the predicate from a claim it reads itself. - Membership is the predicate, at ANY position in the signed set. Reading it
positionally —
Orgs[0], the home org — refuses every operator who holds admin-org membership without their user row living there, which is the defectmiddleware_identity.gorecords against its own earlier form. - The gate is CALLED, not wrapped. Each handler calls
AdmitorAdmitScopedon its first line, so the tier is read where the handler is read and applies to the MCP and CLI projections too, which never touch the router (apps/admin/admin.go,routes). A middleware gate protects one projection of four.
The reserved org's slug is IAM_ADMIN_ORG, default admin
(apps/admin/admin.go, adminOrgOf).
§3 Refusal is 403, and that is deliberate
A refused caller gets 403 with SuperAdmin required or admin required, not
404. This is the opposite of the stage rule (HIP-0139 §8.2), where a
capability a customer has not been let into answers 404 so the refusal is not an
existence oracle — and the difference is the point. /v1/admin is documented,
addressed and known to exist; there is nothing for a probe to learn. Hiding it
would buy no secrecy and would cost the operator a diagnosable error.
§4 The store it owns: none
Admin opens no store. There is no cek.Open under apps/admin, and there is no
admin datastore anywhere in the fleet. Every board is a fan-in over planes that
own their own data:
| upstream | what admin reads |
|---|---|
IAM (apps/admin/iam) | the org and user directory, on the management surface, replaying the caller's own credential |
commerce (apps/admin/commerce) | balances, spend, credits, invoices, subscriptions |
the AI ledger (apps/admin/ledger.go) | what was served: 30-day AI spend and tokens |
o11y / health (apps/admin/health) | service health and the incident board |
the cluster (apps/admin/infra) | nodes, pods, volumes, services — read through the k8s API |
DigitalOcean (apps/admin/digitalocean) | the droplets and volumes behind the fleet |
the audit trail (deps.Audit) | the grant history, projected — never a second copy |
This is a REQUIREMENT and not an accident. A console that kept its own copy of a
number would be a second source of truth for that number, and the operator would
have no way to tell which one was wrong. Every figure a board shows MUST be
attributable to the plane that owns it, and a plane that fails MUST degrade its
own row honestly rather than let a fleet total read healthy — the overview
answers 200 always and reports each upstream in sources[] as ok, degraded or
not-configured.
§5 Mutations, and the trail they are written to first
Admin's writes land in other planes: a credit grant in commerce's ledger, a cap
or promo in the usage plane, a flag in flags, a suspend in IAM, a reload in the
host. Every one of them MUST be recorded in the tamper-evident, hash-chained
audit trail (HIP-1103) BEFORE it is reported as done, and a deployment with no
durable audit store MUST refuse to mutate rather than mutate unrecorded:
> refused: no durable audit store is configured on this deployment; a plugin
> lifecycle change must be recorded before it is made
> — apps/admin/plugins/fleet.go:155
Recording FIRST is what makes the trail complete: a write recorded afterwards
loses exactly the attempts that crashed between the act and the record, which
are the ones worth having. Failed attempts are recorded too, which is why the
grant history is a projection of the trail rather than a table of successes —
GET /v1/admin/grants reads back admin.customer.credit entries, so the view
cannot drift from what happened.
§6 Tenant, price, events, telemetry
Tenant. Admin is the one capability whose caller is deliberately NOT pinned
to a tenant. A SuperAdmin's window is the fleet; a white-label admin's window is
their own org subtree, computed by ResolveScope from the validated principal
and never from a request field.
Price. Free, in those words: Price: cloud.Free
(plugin/admin/main.go), and admin is absent from meteredApps (spend.go).
It meters nothing and charges nobody, because the operator is not a customer.
A capability whose audience is the operator MUST NOT be metered — a meter on
this surface would bill the house for looking at its own books.
Events. Admin publishes no admin.<noun>.<verb> events on the platform bus,
so a customer's webhooks (HIP-1310) receive nothing from it. That is correct and
MUST stay so: an operator's action is not a customer's event, and a fan-out that
delivered one would tell a tenant what the operator is doing. The audit trail is
where an operator action is durably visible, and its audience is the operator.
Telemetry. Beyond the request span every route already gets, admin emits
logs and no metrics of its own. The one publication it makes is in-process — the
per-provider funding split, handed to the ai module's funding state
(apps/admin/finance/finance.go) — and reaches no bus and no tenant.
§7 The reload door
/v1/admin/plugins is four operations: the fleet's per-host account of what it
is running, and reload, enable and disable for one plugin. Each is core.Admit
(platform tier) and each mutation is audited under §5. They are the most
dangerous routes in the fleet — any of them can take production down — and they
are specified here rather than anywhere else because they change a BINARY, which
is the fleet's and not a capability's.
The identity of what is running is the artifact's SHA-256 and MUST NOT be a version string, because the digest is the only identifier that cannot drift from the bits actually serving.
Three rules bind the door:
- The version-to-digest mapping has one author. A version resolves through
CLOUD_PLUGIN_ORIGINto<origin>/<version>/binaries.json— the index CI already writes. Admin MUST NOT compute a digest and MUST NOT accept one from the caller alone. - Verification precedes execution. The digest is handed to
zip, which verifies the download against it BEFORE the file is made executable or given its final name, and uses it as the cache key, so a version this host has already run needs no network. - Hosts are reported, never merged. The listing answers per host. During a rollout the hosts disagree BY DESIGN, and a merged view hides exactly the state an operator is watching for. A peer that could not be reached reports its error and an empty plugin set, which is NOT the same as none, and nothing downstream may conclude drift from it.
A pin rolls back as readily as it rolls forward: the same route with an older version is the rollback, so there is one mechanism and not two.
§8 Withheld, not hidden
Admin is ga — its manifest row declares no stage, and absent is ga
(HIP-0139 §8). It is core: the fleet cannot be operated without it. It is simply
not customer-facing, and those are different properties that a single "is it
finished" axis cannot carry.
The whole capability is dropped from the public document BY ADDRESS, in one
place: openapi/public.go reserves the product admin for the operator
(const Operator = "admin") and refuses any path whose first segment after
/v1 is that word. Everything downstream follows without a second decision —
no generated client class, no MCP tool, no CLI command group, no public page.
This MUST remain an address rule and MUST NOT become a per-route annotation. A list of operations to withhold is a list somebody maintains, and the operation added next week is the one that is not on it. The address is a property of the route itself and cannot be forgotten.
The consequence is deliberate and stated so it is not mistaken for an omission:
admin is a ga capability with no SDK method, no tool and no public page, and
the operator reaches it through the console at admin.hanzo.ai (HIP-0118 §6)
and through the internal document.
§9 Upstream
Admin derives from no OSS project: it forks, embeds and mirrors nothing. It is
cloud's own aggregation over planes that have their own upstreams — IAM
(HIP-0026), commerce (HIP-1220), the audit trail (HIP-1103). The infrastructure
board reads the cluster through k8s.io/client-go (Apache-2.0), which is a
client library for an API and not a project this capability derives from.
Rationale
The alternative considered was for each plane to serve its own operator surface and for the console to be a client that calls thirty of them. It was rejected for the reason §4 gives: the fold is the product. An operator asking "what is this org costing us" needs IAM, commerce and the ledger reconciled onto one row with an honest freshness flag per source, and a client that calls three services and renders three spinners has moved that reconciliation into a browser where it cannot be tested and each failure reads as a blank.
The second alternative was to gate admin at the edge alone — a proxy in front of
admin.hanzo.ai — and to leave the handlers ungated. That is one projection of
four. The MCP door and the CLI reach the same typed operations without passing
any edge, so a gate that lives at the edge is a gate two callers walk around;
this is why §2.3 puts the call in the handler.
Security Considerations
This is the capability an attacker wants. It reads every tenant, grants money, sets caps, suspends users and replaces running binaries. The whole design is one predicate, evaluated once, plus a trail that is written before the act.
Escalation by claim confusion. Trusting a per-org isAdmin for
platform-level gating is HIP-0118's named privilege-escalation bug, and it is
the one this surface would suffer first: a customer administering their own org
is one boolean away from the fleet. AdmitScoped admits that caller only with
the white-label allowlist ALSO satisfied, and even then narrows every read to
their own subtree. Admit does not admit them at all.
Escalation by header. X-User-IsAdmin is an ordinary header on the wire.
Every authority header is deleted on ingress before anything reads one, and the
admin bit is re-minted only from a validated credential — never restored from
client input, on any path, including the one where the data plane still passes
a client X-Org-Id through. If the validator cannot verify a token, the request
resolves anonymous and admin fails closed.
Escalation by position. Reading membership positionally silently refuses legitimate operators, and the fix for that class of bug is where the danger is: a "make it work" patch that widens the test to any org in the set, or to the isAdmin bit, converts a refusal bug into an escalation. Membership of the reserved org, at any position, is the predicate; nothing else is.
The reload door as the shortest path to code execution. An attacker who reaches it does not need a vulnerability in any handler — they need a URL. This is why the origin is deployment config and not a request field, why the digest comes from the index CI writes rather than from the caller, and why zip verifies before the artifact is executable. An unset origin refuses the request rather than falling back to anything.
The trail as the last line. Everything above can fail and still leave the operator able to say what happened, provided the record precedes the act and the chain is hash-linked. A deployment with no durable audit store therefore refuses to mutate at all — an unrecorded grant is worse than a refused one.
Tenant isolation on a surface built to cross it. Admin exists to read across
orgs, so the isolation cannot come from the address; it comes from the scope
resolved on the caller. Every non-super read folds through ResolveScope, and a
request field that names an org is not a scope — it is a filter within one.
How is this guide?