ADR-093: Manifest-driven project tools — auto-rendered admin affordances per project
Status: Accepted Date: 2026-04-25
Context
While bringing up warp.gyrum.ai end-to-end through the new ADR-092 pipeline, the operator hit a recurring shape problem: every gyrum project that exposes an admin API needs a "mint a token" affordance somewhere. Tonight that meant me running a six-line curl against https://warp.gyrum.ai/api/v1/keys with the bootstrap admin token to issue an operator-scoped key. The next project (distill, then buzzy, then anything else with an admin surface) is going to want exactly the same affordance, with the same shape: pick a label, pick a scope, click submit, get back a one-time-show secret.
The naïve path is to hand-write that affordance per project — a Svelte component for warp's /keys, another for distill's /admin/tokens, another for buzzy's whatever. That's the conditional-shape ADR-092 set out to kill. Same complaint: per-project UI code locks in cosmetic differences for things that have a single underlying contract, and the cost compounds with every new project.
A second instance of the same pattern was already on the operator's mind during the conversation that produced this ADR: the /platform/<project>/docs tab is generic over every project (each one declares its docsRoot in the manifest, the UI mirrors the resulting subtree), but the rest of the project page still hard-codes affordances. The operator's framing — "is minting a tool which we can have auto installed generically so any tools into somewhere in here?" — is asking for the same generic-over-manifest treatment for arbitrary admin actions, not just docs.
This is the third time in two days a manifest-driven affordance has paid off (ADR-092's deploy manifest, ADR-092's db.mode/persistence flexing, the docs aggregator). Codifying the pattern before the second project implements its admin-tool surface is the cheap way to prevent a divergent shape settling in.
Decision
The platform UI auto-renders project-specific admin affordances ("tools") from a tools: block in the project's deploy manifest (the schema gyrum-catalog ships per ADR-092). No per-project Svelte. Adding a new tool kind to the fleet is one PR to ai-frontend (the kind handler) plus a one-line addition to the project's manifest YAML — never new project-specific code in either place separately.
The shape:
project: warp
hostname: warp.gyrum.ai
# … existing ADR-092 fields …
tools:
- kind: mint-token
label: Mint API token
api: POST /api/v1/keys
scope_via: bearer # how the UI authenticates the call (bearer | basic | cookie | none)
fields:
- name: label
type: string
required: true
- name: scope
type: select
choices: [user, admin]
default: user
response:
reveal_field: token # the one-time-show secret in the JSON response
preserve_fields: [id, label, scope] # fields safe to keep displayed after reveal
The platform UI reads the tools: array on every project page (it already loads the manifest per SWARM-H's work) and renders one card per entry. Each card lives under a new "Tools" affordance on the project page. The exact location (sibling tab versus Overview section) is a UI-side decision, not part of this ADR.
A kind is the contract: the Svelte handler for kind: mint-token knows how to render the form, POST the API call (via the project's manifest-declared admin auth), display the one-time secret, and offer Copy. New kinds (rotate-secret, run-migration, dump-data, deploy-trigger, log-tail, etc.) get added to the registry as the fleet needs them. Each new kind is a single Svelte component plus a contract entry in the typed manifest schema (the same gyrum-catalog/pkg/deploy SWARM-E shipped).
A project that does not need admin tools omits the tools: block and gets no Tools affordance. A project that has multiple tools lists multiple entries; the UI renders them in declared order.
Consequences
What becomes easier:
- Adding a new project's admin tools is a manifest edit, not a UI PR. Distill ships its
/api/v1/tokensendpoint, adds atools: [{kind: mint-token, ...}]entry to its deploy manifest, and the affordance appears on/platform/distill/toolsautomatically. Zero ai-frontend code per project from there on. - Adding a new tool kind is a single Svelte component plus a schema entry, used by every project that opts in. The cost of "we need a rotate-secret affordance" amortizes across the fleet instead of being paid per project.
- The cognitive load for the operator drops. Every project page surfaces the same set of tool kinds in the same way — same form pattern, same Copy button, same one-time-secret reveal. No remembering which project has which custom admin URL.
- Validation happens once. The typed manifest schema (per SWARM-E) catches malformed tool entries at write time via
gyrum-validate-playbook, not at click time in the UI. - Token-handling rules apply uniformly. The reveal-once-then-hide pattern, never-log-the-token rule, copy-then-clear behaviour — all live in the kind handler's Svelte component, not in N copies across N projects.
What becomes harder:
- Adding a tool kind that doesn't fit any existing project's API is harder than just hand-writing one. The ADR pushes operators to find the generic shape; sometimes a one-off is genuinely the right answer. Mitigation: an
kind: external-linkescape hatch for the "just open this URL" case, which keeps the registry-of-kinds aligned with the design grain rather than being polluted by one-off escape valves. - Cross-project security model has to be uniform. Every
kind: mint-tokeninstance assumes the same trust model: the operator's session token is admin-scoped enough to call the project's admin API. If one project requires a separate authentication ceremony before minting, the manifest needs to declare that and the kind handler needs a branch — small additional cost. - The kind registry becomes a contract surface that needs versioning. Adding required fields to an existing kind breaks every project's manifest. Mitigation: kinds are append-only and additive within a major version; breaking changes ship a new kind name (e.g.
mint-token-v2) and projects migrate at their own pace. - Surface area for "admin operations on production" expands. A Tools tab is by definition a button to call admin APIs. Authentication, audit logging, and rate-limiting need to be solid before the UI offers convenient buttons for sensitive operations. This is a real risk; mitigation lives in the per-kind handler (rate-limit per-kind, audit-log every call, require explicit admin-scope check on any kind that mutates state).
What we have signed up to operate:
- An extension of the
gyrum-catalog/pkg/deploy.Manifestschema with thetools:block + JSON Schema mirror + validator-CLI fixtures. - A registry of
kindhandlers in ai-frontend (one Svelte component per kind), starting withmint-tokenfor warp and growing as the fleet's admin surface matures. - A small backend proxy in ai-research (or wherever the platform UI's API layer lives) so the browser doesn't make CORS-direct calls to project admin endpoints — the UI calls
/api/v1/projects/<slug>/tools/<kind>and the proxy forwards to the project's declared admin URL with the operator's auth attached. - An update to
gyrum-create-repo(the new-project scaffolding CLI) so any newly-bootstrapped project ships with a placeholdertools:block in its deploy manifest — empty by default, with commented examples for the common kinds (mint-token,rotate-secret,dump-data). New projects opt out of admin tools by leaving the block empty; the affordance grows as the project gains admin endpoints. Rationale: catching this at scaffold time prevents the year-from-now situation where every project re-invents its admin flow because nobody remembered the manifest field existed.
What we revisit:
- If the registry of kinds grows past ~10 entries without consolidation, the schema is too coarse and individual kinds need to factor — split into
kind: form-submitwith atarget: …shape, for example. Watch for the consolidation moment; don't pre-emptively refactor. - If a project genuinely needs a tool that no other project will ever want, this ADR is the wrong frame and the project should ship its own UI route instead. The point is not to force universality; it's to prevent accidental divergence on shapes that ARE common.
Alternatives considered
- Per-project Svelte route for each project's admin affordances. The naïve path tonight would have been a
WarpKeysCard.sveltefor warp, then aDistillTokensCard.sveltefor distill, then aBuzzyAdminCard.sveltefor buzzy. Three copies of nearly the same form. Rejected — exactly the conditional-shape pattern ADR-092 exists to prevent. One project's "use a select for scope" becomes another's "use radio buttons" becomes a UI-consistency review every quarter. - External admin tool per project (CLI-only, no UI affordance). The operator already has
curlagainst the project's admin API; ADR-093's UI affordance is convenience. Rejected — convenience matters at scale. The whole reason "minting a token" feels heavyweight is that today it requires recovering a bootstrap admin token fromdocker logs, then crafting acurlwith the right JSON shape. A UI button removes both burdens; the operator's stated constraint ("super simple, nothing to remember") points squarely at this. - Declare the tool in the project's repo (e.g. a
docs/tools.yaml) rather than the deploy manifest. Has the merit of co-locating the tool declaration with the API it calls. Rejected — would mean the platform UI needs to clone every project repo to read its tools, which violates the ADR-092 cut (the platform is project-agnostic; it reads gyrum-catalog manifests, not project source). The deploy manifest is already the platform's source of truth for everything else about the project; tools belong in the same place. - Wait until distill (or another second project) actually needs a mint-token affordance, then generalize. The "you ain't gonna need it" framing. Rejected for two reasons: (a) the cost of writing the ADR now is ~30 minutes of doc time; the cost of converging two divergent project-specific implementations later is several PRs across multiple repos plus a UI consistency review; (b) the second project is queued (distill, then buzzy), and waiting until they ship their admin surfaces means tonight's warp-specific implementation locks the wrong shape in by being the precedent everyone else copies from.
Supersedes: none.
Extends ADR-092 (host/project separation) by adding tools: to the deploy manifest schema. Implementation depends on SWARM-E's typed manifest library + SWARM-H's manifest API endpoint already shipped tonight.
Superseded by: {{leave blank until a later ADR reverses this one}}