Skip to main content
Status: partially implemented. Both the identity ops (list/get agents; issue, revoke, rotate, suspend, reinstate) and the data-lifecycle ops (delete node, erase, reembed, job status) are live over the REST /admin/api router today, behind an operator session. The Mycelium web console (web/admin/) ships the identity screens — an agents list and an agent-detail view with the four mutations, including reveal-once credential rotation. Deferred: the console’s data-lifecycle screens (erase/reembed/jobs are reachable over REST and gRPC but have no UI yet).The gRPC HyphaeAdmin service and the hyphaectl CLI are LIVE. The control plane is no longer REST-only, and that matters most during an incident: the REST adapter authenticates ONLY via the OIDC browser session and mounts only when admin.console_enabled, so revoking a compromised credential over REST begins with standing up an identity provider. hyphaectl dials the gRPC service with the same API key the data plane uses.
Every mature data system separates a data plane — store, recall, gossip, the hot path served on /v1 — from a control plane: provisioning, revocation, deletion, and migration, the operator path. HyphaeDB built the control-plane logic (the Registry identity lifecycle and the LifecycleManager data lifecycle) long before it had a plane to invoke it on. This page documents that plane: the /admin/api surface, who may call it, and the safety envelope wrapped around its irreversible operations. The control plane is deliberately a separate surface from the data plane. It is an axum router merged in outside the /v1 OpenAPI coverage, so it can be authorized independently, omitted from public ingress, and reasoned about on its own. The two planes even authenticate differently — which is the first thing to get right.

Authentication: an operator session, not a data key

Two operator paths, and which you can use depends on the transport:
  • gRPC / hyphaectl — an API key. x-hyphae-key, the same metadata the data plane uses. The facade still authorizes every operation, so a non-admin key is PermissionDenied exactly as over REST — the transport adds no authority. This is the incident path: no identity provider required.
  • REST /admin/api (what the web console consumes) — an OIDC session. The REST adapter does not accept an API key. An operator authenticates with an OIDC Authorization-Code login that establishes a browser session:
1

Begin

GET /admin/auth/login redirects (302) to your IdP’s authorize URL and sets a transient, SameSite=Lax CSRF state cookie that the callback double-submits.
2

Callback

GET /admin/auth/callback exchanges the code, resolves the operator’s federated subject to a local Principal, and sets the session cookie — then redirects to /admin.
3

Act

Every /admin/api/* request carries the session cookie. POST /admin/auth/logout drops the session server-side and clears the cookie.
The session cookie (hyphae_admin_session) is an opaque server-side id, never a token — the IdP token stays in the server’s OidcLogin relying party. It is set HttpOnly (so XSS cannot read it) and SameSite=Lax, plus Secure when served over TLS. Because operator login rides the same pluggable-authn machinery as any other credential, an operator SSO produces an AuthSuccess/AuthFailure audit row on the same sink the data plane uses.
The console is off by default and gated at boot. Set admin.console_enabled to serve it, and supply admin.bootstrap_oidc_subject (the first operator’s OIDC subject, bound idempotently to a ServiceAdmin federated identity on startup). In the Production deployment profile the server refuses to boot the console over a plaintext listener — the OIDC login and session cookie must not cross cleartext — so TLS is required there.

Authorization: ServiceAdmin and ProjectAdmin

Every operation resolves the session to a Principal and runs it through the §8 authz matrix. Two admin roles matter:
  • ServiceAdmin — service-wide authority. Crosses every tenant freely; the only role that may run corpus-wide or agent-wide operations (reembed, erase Agent).
  • ProjectAdmin — confined to its own tenant. May list/read/mutate agents and delete/erase data within that tenant, and is denied — with a Forbidden and an AuthzDenied audit, no dispatch — on any cross-tenant target.
A non-admin principal (e.g. a Writer) is denied at a coarse role gate before any dispatch. See Authorization for the full role model.
Honest scoping caveat. Today ProjectAdmin authority is tenant-granular, not project-granular — a ProjectAdmin may act on any agent or node in its own tenant, not only its project. Project-granular authz rides a planned RbacAuthorizer unification. Read the matrix’s “of the target’s project” wording as tenant-scoped until then.
Reads are anti-oracle: a ProjectAdmin reading a cross-tenant agent gets NotFound, identical to a truly-missing id, so the read cannot confirm an agent’s existence in another tenant.

The identity surface

Managing agent credentials — the operator’s answer to “a key leaked, cut it off now.” Every route is session-gated; every mutation is charged RateOp::Admin and emits a synchronous audit. A revoke or rotate is immediate: the dispatch invalidates the principal cache synchronously, so the old credential fails authentication on the very next request — there is no TTL window. The rotated plaintext_secret is surfaced to the caller exactly once and is never logged or written to an audit event; the Mycelium console reveals it in a copy-once dialog.

Agent descriptors

The three descriptor routes attach a descriptor — a card-shaped description of what an agent is and can do — to an agent identity. Descriptors are inert: setting or importing one changes nothing about authentication, authorization, or trust; it is recorded metadata, never a permission input. The import route accepts two formats: an A2A AgentCard — both the current v1.0.1 supportedInterfaces shape and the pre-1.0 shape (url / preferredTransport), with which shape was ingested recorded in the import origin (card_shape) — or an ACP agentInfo document. A signed card is verified against the supplied JWKS and the result recorded, fail-closed: a signature that should verify but does not rejects the import. Imports are idempotent, and every set/import writes a synchronous DescriptorSet/DescriptorImport audit row.

The data-lifecycle surface

Destroying or migrating stored memory — high blast radius, so each route carries an extra guard. These dispatch to the LifecycleManager; the facade owns the authz, tenant confinement, rate charge, and audit. See Data lifecycle for the underlying tombstone and erasure mechanics. Delete takes an optional body { "reason": "user_delete" | "retention" | "superseded" } (absent ⇒ user_delete); a second delete of an already-tombstoned node is a success no-op. Erase requires a two-field confirmation. The body is { "scope": { "kind": "node"|"project"|"agent", "id": "<string>" }, "confirm": "<label>", "dry_run": false }, and confirm must equal the scope’s canonical labelagent:<id>, project:<id>, or node:<uuid>. A mismatch is a 400 that destroys nothing and still writes a Denied audit of the refused attempt. dry_run: true returns the would-remove counts without deleting. On a real erase, the response is an erasure certificate: the per-tier removal counts (nodes_removed, diffs_removed, deliveries_removed, sessions_removed, aggregates_recomputed, tombstones_propagated) plus a digest — a non-repudiable receipt that the compliance deletion happened.
Erasure is irreversible, and erase Agent is ServiceAdmin-only. The confirm label is your seatbelt — it must be typed to match the exact scope, so an erase cannot fire on a fat-fingered id.
Reembed is a long-running maintenance job, not a blocking call: the request returns a job_id immediately and at most one reembed runs service-wide at a time — a second request while one is running returns 409 Conflict. The new embedding engine is selected server-side from cfg.embedding (the operator changes the configured model, then triggers a reembed to migrate the corpus), so the request carries no body. Poll GET /admin/api/jobs/{id} for { status: "running", nodes_processed, nodes_total }succeeded or failed. Jobs are in-memory in V1: a crash loses the job record but not corpus consistency (the reembed commits its watermark last), and the operator simply re-triggers.

The audit and rate-limit envelope

Because control-plane operations are high-privilege and rare, every one runs the same envelope: authenticate → authorize → rate-limit (RateOp::Admin) → validate → dispatch → synchronous audit. Two properties follow:
  • Fail-closed audit. A mutating operation reports success only once its audit row is durable. If the audit write fails, the operation fails — there is no unaudited erase or revoke.
  • Consistent error contract. /admin/api reuses the data plane’s Error→status mapping: Validation→400, Unauthorized→401, Forbidden→403, NotFound→404, Conflict→409, RateLimited→429 (+ Retry-After). The session gate runs first, so an unauthenticated request is a 401 even with a malformed body, and a non-admin session is a 403, never a 401.

The Mycelium web console

web/admin/ is the React + React Aria SPA — “Mycelium” — served over this REST surface. It boots by calling GET /admin/api/me; a 401 routes to an OIDC login screen. Today it ships the identity workflow end-to-end: an agents list (keyset paging, filters, loading/empty/error states) and an agent-detail view whose actions are the four mutations above, each behind a confirm dialog, with rotation revealing the new secret exactly once. The data-lifecycle routes are live on the server but do not yet have console screens — drive them with curl or an ops script in the meantime.

hyphaectl — the incident path

The CLI over the gRPC HyphaeAdmin service. Authenticates with an operator API key, so it needs no identity provider and no browser.
erase will not fill in --confirm for you. The confirmation must equal the scope’s canonical label (agent:<id> / project:<id> / node:<id>) and the server checks it. A tool that derived the label would turn a two-field confirmation into a formality it satisfies on the operator’s behalf, which is the opposite of why the second field exists. Run with --dry-run first: it reports what would be removed and deletes nothing (a dry run is a preview, so it carries no certificate digest — only a real erasure is issued one). Authorization is unchanged by the transport: every command dispatches to the same facade as REST, so a non-admin key is refused identically and every mutation emits the same synchronous audit row.

Source

This page is a teaching restatement of the admin-control-plane spec; that spec (its §7.1 REST transport and §8 authz matrix) is authoritative for any detail here. The spec’s §6 trait issue operation is now live on the facade + POST /admin/api/agents, and the gRPC HyphaeAdmin service + hyphaectl CLI are live alongside it (identity, data-lifecycle, and audit chain verification). Follow the code.