ADR-0047: Verb-Scoped Endpoint Vend — A Lane Credential Holds Only What Its Lane Does
Status · proposed · Date · 2026-10-21 · Deciders · Proposed by joestump-agent, pending Joe
Status note: this ADR is proposed. It ships with an implementation so the choice can be argued against working code rather than prose; nothing here is settled until Joe accepts it. The requirement text it implies for SPEC-0006 and SPEC-0007 is listed under Consequences rather than applied: accepted records stay as accepted until this ADR is accepted.
Context and Problem Statement
switchboard endpoint vend NAME --queue Q — and the operator API behind it — handed every vended
endpoint mcp.AllVerbs(): the eight todo verbs, and also the webhook lifecycle, routing, rule,
trusted-actor and event-replay verbs. The only flags were --queue and --json, so an operator
scripting a deployment could not ask for less, and 29 verbs is what a lane credential got for a job
that uses eight of them (issue #589).
The blast radius is the problem, not the count. A worker or merge agent that is prompt-injected, or whose env file leaks, currently holds the ability to rotate or delete the webhook that feeds every stage, rewrite the routing rules that decide which lane a delivery lands in, and replay events. None of that is part of draining a queue.
The capability to vend narrowly already exists: the web wizard ticks verbs per endpoint, and the store
already carries verbs and a webhook ceiling per endpoint. What was missing was the CLI surface, the
default posture, and any way to see at a glance what a lane holds.
Decision Drivers
- Least privilege for a credential that runs unattended code written by other people.
- A scripted vend must stay one command; a new mandatory flag would be answered by pasting the flag into every script and change nothing.
- No silent narrowing: an operator who means to grant admin verbs must be able to say so, and a mistake must fail loudly rather than mint a credential quieter than they asked for.
- One source of truth for the verb families, so the CLI, the API, the wizard and the consent screen
cannot drift (the pattern ADR-0023 and
internal/mcp/verbs.goalready establish).
Considered Options
- (A) Warn-and-opt-in. Keep the broad default; print the verb count and a warning, and require
--allow-webhook-adminfor the admin verbs. - (B) Narrow by default, explicit grant. Default to the worker surface (the todo verbs and the
endpoint's own lease verbs);
--verbstakes verb names and/or presets (todo,webhook-admin,all);--allow-webhook-adminis sugar for adding the admin preset;endpoint listshows what each endpoint holds. - (C) Force a choice. Refuse to vend until the operator names a verb set, on every call.
Decision Outcome
Chosen: (B), narrow by default.
endpoint vendwith no verb flags sends noverbsfield, and the server applies its default: the eight todo verbs plus the two verbs that read and set that endpoint's own default lease. The default lives in one function (mcp.DefaultVendVerbs) that the CLI does not duplicate, so the two cannot disagree about what "nothing named" means.--verbstakes verb names and presets together, comma-separated and case-insensitive. A name that is neither a verb nor a preset is refused before the deployment mints anything, because a typo that silently grants less than the operator meant is the one failure this surface must not have. The server resolves the same way, so a scriptedPOST /api/v1/endpointsbehaves identically.--allow-webhook-adminadds the admin preset. Naming admin verbs in--verbsis itself the opt-in; either way the reveal then prints the count and one line on what those verbs can do, so the broad grant stays a decision somebody made rather than a default that slipped through.endpoint listgains a VERBS column: a count, with the admin verbs called out —29 (19 webhook-admin)— and the verb names under-v.
Mechanics
internal/mcp/verbs.goowns the families, the presets, the canonical order, the resolver and the admin-family test used by both the reveal warning and the audit column.WebhookAdminVerbs()is a contiguous slice ofAllVerbs(), so a new verb cannot fall outside the audit flag by accident.POST /api/v1/endpointsgained an optionalverbsarray. Empty or absent is the default grant. The webhook ceiling still comes frommcp.BasicWebhookCeiling(verbs, queues), so a narrow grant carrieswebhook_max = 0: self-managed webhooks are off, exactly as when the wizard's webhook step is left blank.- The vended ingestion webhook is unaffected. It is minted by the API on the operator's behalf with its own ceiling of one, not through the endpoint's self-management allowance, so a narrow lane still receives deliveries and the webhook→todo→doorbell loop still closes. What a narrow lane loses is the ability to create, rotate or delete webhooks and to rewrite routing.
- The web wizard is unchanged: a human ticking chips there has already made the choice, and its verbs travel as they always did.
Consequences
- Breaking for scripts. A deployment that vended a router or a webhook manager with a bare
switchboard endpoint vendnow gets a worker credential, and the verbs it wanted surface later as refused MCP calls. The fix is one flag:--allow-webhook-admin, or--verbsnaming what that lane actually needs. TheChangedentry in the CHANGELOG says so. get_default_lease/set_default_lease(ADR-0043) stay in the default grant, as SPEC-0006 requires, and they widen nothing: they act only on the calling endpoint, and a claim can already ask for the same lease throughlease_ttl_seconds, under the same ceiling. The verbs the default drops are the ones that reach past one endpoint — webhooks, routing, rules, trusted actors, event replay.- Notify-hook verbs (ADR-0029) stay outside every preset, including
all: they make Switchboard dial URLs a tenant chose, so they remain name-only. - Implied requirement text, for SPEC-0006 "Todo Drain Verbs" and SPEC-0007 once this ADR is accepted: a vend that names no verbs MUST grant the drain verbs and the endpoint's own lease verbs, and no others; a vend that names an unknown verb or preset MUST mint nothing; and the endpoint listing MUST reveal each endpoint's grant without revealing its credential.
Confirmation
internal/mcp/verbs_vend_test.go pins the default grant, preset resolution and the admin partition;
internal/server/api_vend_verbs_test.go pins the API default, the resolution, and that a refused verb
mints no agent and no endpoint; cmd/switchboard/endpoint_verbs_test.go pins that the default vend
sends no verbs field, that an unknown verb exits before the deployment is called, that the admin
grant prints the warning, and that endpoint list counts verbs and names them under -v.