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:
quota→Manifest.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_gate→ClaimGate *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[]ConfigFieldmounted on owner / code / role (manifest.go:183-206;config_field.go:32-62):key,label,type(string|int|bool|time|string_list),defaultas 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_ownerused to be a kernel column touching nine generated files.visitor_tools→VisitorTools []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_bookrequirescalendar: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_tools→OwnerTools []OwnerTool(manifest.go:138-143,179): name / internal tool / description /input_schemaas 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_ops→Transport.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 asSTANDMEET_HOST_SOCKETonly when the list is non-empty (loader.go:94-96).acl→ACL string(the ACL (access-control list) gate,manifest.go:113-122,226):alwaysorrole_granted; the loader maps anything else, including absent, torole_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 genericrecords(id, collection, doc jsonb, created_at)table plus aclaims(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 passassertDroppable(reserved prefix,[a-z0-9_]suffix, core-schema blocklist) before it is spliced into DDL (data definition language);Dropis the onlyDROP SCHEMAin the codebase (schema.go:59-89,store.go:70-92).Claimis 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. AStoreis 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 JSONnull, never""— the empty string once failed every read of booker's unset per-code cap and hidcalendar_bookfor authorized visitors (defaultOf,:105-122). This closed the settings twin: beforecc5c1db47(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 deletedbooker_code_config.go,booker_code_store.go,booker_quota.gofrom the composition root.capquota— live counters in the capability's own store.Counterreads the limit from the subject's config byConfigKeyand counts rows in the capability's ownCollectionwhereSubjectFieldequals the subject id (backend/internal/capabilities/capquota/capquota.go:73-139).AllowandRemainingare one count with two outputs — the #135 externalization once restored only the gate and leftquota_remainingnil for weeks (:12-16). Unset / null / ≤ 0 → unlimited (nil, never 0). The kernel's own per-code booking count (CountBookingsByCodeindb/queries/access_codes.sql, removed atf376b0432, 2026-07-25) and theaccess_codes.max_bookingscolumn (removed atb73c932bb) are the ledger this retired: the count now lives where the rows live.sandbox/sandboxws— two runners, one workspace manager.sandbox/sandbox.gois the one-shotdocker runfor owner-curated skill scripts (--network=none --read-only --tmpfs /tmp --memory --cpus --rm;SANDBOX_DRIVER=dockerelse disabled,:96-125,205-214).sandbox/stdio.gobuilds the bubblewrap (bwrap) argv for long-running MCP servers:/pluginread-only,/workspaceread-write when provisioned,--unshare-netunlessallow_net,--die-with-parent, host sockets bind-mounted by path (:37-61,78-126).sandboxwsowns 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.Collectgathershostop.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);Serveopens 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_visitoris 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}iscodeorapi_key(subject.go:15-38) — it used to beCodeID, so an outward key booked with zero gating (F-B-11). - Two gates before any dial.
exposablechecks the ACL, then the optionalSessionGate(capload/capreg_mcp_app.go:275-289). The quota gate is built per manifest from the declaredQuotaand translatescapreg.Subjectinto acapconfig.Scopeat 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 returnsErrQuotaExhausted, which wrapsErrHiddenso 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.QuotaRemainingis filled by the same counter'sRemaining;ClaimGateis carried as data and judged once by the kernel at turn end (capreg/claimgate.go).dialAndListfolds a failed dial intoErrHiddenbut logs the real cause first — prod once ran withtools:0and 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.
VisitorToolsis reconciled against the realtools/liston 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.Claimexists, but onlycalendar.bookorderscapstore.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.