2026-09-23·by Sijie Wang#standmeet#architecture#design

capability-owns-quota-config-and-store

A capability declares its own quota, config, and store

Parent: capabilities

mcp-capability-plugins explains that a builtin is a manifest plus an MCP process in a sandbox. This page is about the part the host used to hand-write for each one: how many times a code may use it, which settings the owner can tune, where its rows live, and what it may ask the host for. Each of those is now a declaration in the capability's own manifest.yaml, executed by a generic sub-package that contains no business word — no "booking", no "working_hours". The host reads three lines and enforces them; the capability owns the meaning. Every claim below cites the tree at 36789537d (v0.1.31, 2026-09-07). This is external-means-fully-external made concrete: an externalized capability owns its storage too, or it was only relocated.

The manifest is data

The on-disk shape is backend/capabilities/<id>/manifest.yaml, parsed into a descriptor (backend/capabilities/descriptor.go:19-35) and translated once, in loader.go:50-76, into the host's mcpplugin.Manifest (backend/internal/capabilities/mcpplugin/manifest.go). The fields that matter here:

  • quotaManifest.Quota *QuotaDecl (manifest.go:210; quota.go:33-49): three strings — config_key (which field on the subject holds the cap, e.g. max_bookings), collection (which collection in the capability's own store holds usage, e.g. bookings), subject_field (which document field names the subject, subject_id). Incomplete → nil → no gating: when the host cannot count, "don't gate" beats "gate blindly" (Usable, quota.go:54-56).
  • claim_gateClaimGate *ClaimGateDecl (manifest.go:215; claimgate.go:29-41): tool + phrases. If the answer asserts the action is done, this turn must carry that tool's success receipt (F-A-37: four "Booked" lines with no tool call). Judged by the kernel at turn end; the manifest only declares.
  • config / code_config / role_config → three []ConfigField mounted on owner / code / role (manifest.go:183-206; config_field.go:32-62): key, label, type (string|int|bool|time|string_list), default as a JSON literal, min/max. Defaults and validation live in exactly one place — the declaration. A role value freezes into the RoleSnapshot with the session (role-snapshot-frozen); notify_owner used to be a kernel column touching nine generated files.
  • visitor_toolsVisitorTools []string + VisitorToolRequires map[string][]string (manifest.go:161,176): the tool-to-capability answer that must exist before dialing, plus per-tool action-qualified deps (calendar_book requires calendar:events.insert) so a read-only calendar hides only the write tools (F-B-8). Both YAML spellings are accepted (descriptor.go:52-81).
  • owner_toolsOwnerTools []OwnerTool (manifest.go:138-143,179): name / internal tool / description / input_schema as literal JSON, validated at load (loader.go:139-152) — the owner-MCP table must be enumerable at assembly without spinning up the sandbox.
  • transport.sandbox.host_opsTransport.Sandbox.HostOps []string (manifest.go:64-81): the names of host operations this capability wants opened. Names, not socket paths — the path is derived by the host (/run/standmeet/<id>.sock, hostop/op.go:29-34) and injected as STANDMEET_HOST_SOCKET only when the list is non-empty (loader.go:94-96).
  • aclACL string (the ACL (access-control list) gate, manifest.go:113-122,226): always or role_granted; the loader maps anything else, including absent, to role_granted — a missing declaration never opens a capability to everyone (loader.go:100-107).

The sub-packages — each one a mechanism the host used to hand-write

  • capstore — its own schema. One Postgres schema per (kind, id)mcp_<id> / connector_<id> / microsite_<id> — holding a generic records(id, collection, doc jsonb, created_at) table plus a claims(collection, key, expires_at) table with a primary key on (collection, key) (backend/internal/capabilities/capstore/store.go:40-68). The document is opaque; queries are collection + JSON containment. The schema name is always derived from the host-trusted pair and must pass assertDroppable (reserved prefix, [a-z0-9_] suffix, core-schema blocklist) before it is spliced into DDL (data definition language); Drop is the only DROP SCHEMA in the codebase (schema.go:59-89, store.go:70-92). Claim is a single-winner insert-or-take-over-expired with a 5-minute cap — the F-B-15 double-booking fix, a primary-key conflict rather than code ordering (claim.go:22-60).
  • capconfig — settings by declaration. A Store is bound to (kind, capID) at construction, so a handle can only read its own capability (capconfig.go:32-48). Four attachment points, four collections: capconfig (owner), capconfig_code, capconfig_role, capconfig_key (scope.go:29-76). The declaration is authoritative both ways: a stored key no longer declared is never returned; an undeclared key on write is rejected (capconfig.go:83-103,183-206). A field with no declared default reads as JSON null, never "" — the empty string once failed every read of booker's unset per-code cap and hid calendar_book for authorized visitors (defaultOf, :105-122). This closed the settings twin: before cc5c1db47 (2026-07-31) the host carried its own copy of booking policy and had already drifted from the sandbox (18:00/15 vs 17:00/0); aab8abe90 (2026-08-01) made the per-code field a declaration and deleted booker_code_config.go, booker_code_store.go, booker_quota.go from the composition root.
  • capquota — live counters in the capability's own store. Counter reads the limit from the subject's config by ConfigKey and counts rows in the capability's own Collection where SubjectField equals the subject id (backend/internal/capabilities/capquota/capquota.go:73-139). Allow and Remaining are one count with two outputs — the #135 externalization once restored only the gate and left quota_remaining nil for weeks (:12-16). Unset / null / ≤ 0 → unlimited (nil, never 0). The kernel's own per-code booking count (CountBookingsByCode in db/queries/access_codes.sql, removed at f376b0432, 2026-07-25) and the access_codes.max_bookings column (removed at b73c932bb) are the ledger this retired: the count now lives where the rows live.
  • sandbox / sandboxws — two runners, one workspace manager. sandbox/sandbox.go is the one-shot docker run for owner-curated skill scripts (--network=none --read-only --tmpfs /tmp --memory --cpus --rm; SANDBOX_DRIVER=docker else disabled, :96-125,205-214). sandbox/stdio.go builds the bubblewrap (bwrap) argv for long-running MCP servers: /plugin read-only, /workspace read-write when provisioned, --unshare-net unless allow_net, --die-with-parent, host sockets bind-mounted by path (:37-61,78-126). sandboxws owns the per-session workspace directories keyed by conversation id, lazily provisioned, swept by TTL (time to live) with a re-stat before removal (sandboxws/manager.go:55-69,106-153). Guards: real-third-party-mcp-sandboxed.spec.ts, sandbox-workspace-ttl-cron.spec.ts.
  • hostdesk — the inbound convergence. A capability has no network; it reaches the host only through the named ops it declared. hostdesk.Collect gathers hostop.Op{Name, Description, Invoke} from each domain's facade plus the two per-capability sources (its bound store, its bound config) (backend/internal/routes/hostdesk/hostdesk.go:74-85); Serve opens exactly the requested names on the derived socket, and an unknown name is an error (:134-150) that the composition root turns into a startup panic (backend/cmd/server/wire/hostdesk.go:48-62). A capability that names nothing gets no socket at all — ask_visitor is fully offline. The mirror-image of the outbound dispatcher and the socket mechanics are on convergence-inbound-and-outbound and capsocket; not repeated here.

ACL = always — the trio

Three manifests say acl: always: ask_visitor, corpus.retrieval, summarize_conversation (backend/capabilities/*/manifest.yaml, line 5–6 of each). calendar.book and mail.send are role_granted. At registration capload hands the always-list to the registry (SetAlwaysGranted, capreg/registry.go:160-172; capload/capreg_mcp_app_register.go:59), and the frozen snapshot answers exposure with RoleSnapshot.AllowsCapability(capID, aclAlways)aclAlways || allowedTools contains it (access/entity/role_snapshot.go:198-203). The same list feeds DockableCapabilityIDs, so the admin panel can no longer offer a dock button a visitor could never see (F-D-13).

How a Binding is assembled per session

  • The input is per-session only; a capability holds its own deps as a closure. AssembleInput{RoleSnapshot, OwnerID, Mode, Subject, Visitor, ConversationID} (backend/internal/capabilities/capreg/types.go:59-72). Subject{Kind, ID} is code or api_key (subject.go:15-38) — it used to be CodeID, so an outward key booked with zero gating (F-B-11).
  • Two gates before any dial. exposable checks the ACL, then the optional SessionGate (capload/capreg_mcp_app.go:275-289). The quota gate is built per manifest from the declared Quota and translates capreg.Subject into a capconfig.Scope at the composition root — the only place that sees both packages, with no fallback for an unknown kind (backend/cmd/server/axiscap/code_config.go:125-201). Exhaustion returns ErrQuotaExhausted, which wraps ErrHidden so the chat face still hides the tool while the HTTP face can say why (types.go:28-43).
  • The result. Binding{Tools, State, ClaimGate, Close} (types.go:94-103): State.QuotaRemaining is filled by the same counter's Remaining; ClaimGate is carried as data and judged once by the kernel at turn end (capreg/claimgate.go). dialAndList folds a failed dial into ErrHidden but logs the real cause first — prod once ran with tools:0 and nothing in the logs (F-A-1, capreg_mcp_app.go:242-273).

Standing points and the honest ceiling

  • Declaration is allowed to go stale, never silently. VisitorTools is reconciled against the real tools/list on every dial; a mismatch is logged and the real list wins (manifest.go:148-161).
  • Quota is per subject, not per session. Anonymous public/BYOAI (bring-your-own-AI) sessions have no subject and are never usage-gated (subject.go:35-38); the cap on those tiers is gas, not count (feature-floor lists both).
  • The gate fails closed on a read error, and says so. A quota read failure hides the tool with a warn line naming cap + subject (code_config.go:182-190); before that line, a hidden tool looked identical to "unauthorized" and "no connector".
  • Claims still need a manifest. capstore.Claim exists, but only calendar.book orders capstore.claim / capstore.release; any new peek-then-act capability must ask for them by name.
  • Only the builtins are on this path. The job-loop trio, skill runner, ext-mcp and openapi agent-tools are still MustRegistered in-process (capload/capreg_register.go:39-50) and declare none of this; that migration is the open structural work named on capabilities.

Built 2026-07-25 → 2026-08-01, extended through 2026-08-20. f376b0432 drained the kernel's booking ledger and gave capabilities their own store; cc5c1db47 (2026-07-31) added capconfig and deleted the host's settings twin; aab8abe90 (2026-08-01) added capquota + per-code declarations; 5cb8d8464 (2026-08-01) landed hostdesk + hostop.Op; the subject rename for API keys followed F-B-11 (2026-08-20). Guards: chat-book-quota-exhausted.spec.ts, api-key-booking-quota.spec.ts (an outward key is capped and the real calendar gains nothing), claim-needs-a-receipt.spec.ts, admin-gcal-policy-edit.spec.ts (declared defaults 09:00–18:00 / Mon–Fri / lead 2 / buffer 15 survive an edit), code-member-quota-concurrent.spec.ts, sandbox-workspace-ttl-cron.spec.ts, real-third-party-mcp-sandboxed.spec.ts.

Origin: #135 externalization (2026-07) and the F-B-8 / F-B-11 / F-B-15 / F-A-37 findings; verified against standmeet-new main 36789537d on 2026-09-07. docs/design/hostdesk.md and capability-acl-hierarchy.md are seeds, not cited as evidence.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →