# Capability Matrix Every host function exposed to a plugin sits behind a capability. The manifest must declare a capability before the runtime binds the corresponding host functions; calling an undeclared host-fn throws `CapabilityDeniedError` and audits as `outcome=denied`. ## Closed enum The runtime maintains a closed enum of capability names in `packages/plugin-runtime/src/capabilities.ts`. Anything outside this list is rejected at manifest parse time. | Capability | Risk | Host binding availability | Unlocks / intended binding | Scope shape | | ---------------- | -------- | ------------------------- | --------------------------------------------------------------- | ----------------------------------------- | | `log` | none | available | `ctx.log.{info,warn,error}` - structured logging into host pino | none | | `audit` | none | available | `ctx.audit.record({ action, target?, meta? })` | none | | `tenant.info` | low | reserved / fail-closed | Reserved tenant metadata binding; not bound today | none | | `secret.read` | medium | reserved / fail-closed | Reserved per-tenant secret reads; not bound today | `{ keyPattern: "^slack-" }` planned | | `storage.read` | medium | reserved / fail-closed | Reserved tenant-scoped key/value reads; not bound today | per-prefix locking planned | | `storage.write` | medium | reserved / fail-closed | Reserved tenant-scoped key/value writes; not bound today | per-prefix locking planned | | `http.outbound` | high | reserved / fail-closed | Reserved outbound fetch binding; not bound today | `{ allowedHosts: ["hooks.slack.com"] }` | | `db.read` | high | reserved / fail-closed | Reserved DB binding for marketplace work; not bound today | `{ tablePrefix: "tx_" }` planned | | `db.write` | critical | reserved / fail-closed | Reserved DB binding for marketplace work; not bound today | `{ tablePrefix: "tx_" }` planned | `PLUGIN_CAPABILITY_NAMES` is intentionally broader than the production host binding set. As of this foundation, `PLUGIN_AVAILABLE_HOST_CAPABILITY_NAMES` contains only `log` and `audit`; declaring higher-risk capabilities parses, but production install/grant/enable paths reject them until their bindings, tests, and security review ship. ## Risk legend - **none** — no tenant data, no outbound — safe for any plugin. - **low** — tenant identity only; no payload exposure. - **medium** — touches per-tenant data inside the host; operator review recommended. - **high** — issues network requests OR reads tenant business data; audit-trail required and operator must explicitly approve at install. - **critical** — mutates tenant business data; install requires out-of-band operator sign-off; auto-disable on the slightest anomaly. ## Common pitfalls - **Declaring `http.outbound` without `allowedHosts`** is the most common authoring mistake. The runtime denies every request unless the host appears in the allowlist. The lint command warns on this shape. - **Assuming `db.read` is bound today** — the DB capability is reserved-only in the foundation. A plugin that declares it parses fine but the host returns `no_host_implementation` on call. - **Using `audit` as a payload queue** — see the async-task example; the audit body has a cap. Pass identifiers, not blobs. ## How to request a new capability 1. Propose the name (lowercase, dot-separated). 2. Write a one-paragraph threat model: what data can leak, what privileges are gained, what reverse-direction trust does the capability imply? 3. Add to `PLUGIN_CAPABILITY_NAMES` in `capabilities.ts`. 4. Add the host-fn implementation in `capability-gateway.ts` via the provided `hostFns` map; never bypass the gateway. 5. Update this table. 6. Operator-side: external security review when the new capability ships in production (Phase 06 #23 sign-off — see audit-docs.md). ## Audit trail mapping Every successful or failed call to a host-fn produces one row in `PluginCapabilityUse`: | Column | Meaning | | ------------------- | ---------------------------------------------------- | | `capability` | Capability name (e.g. `http.outbound`) | | `outcome` | `success` / `denied` / `error` | | `payloadHash` | SHA-256 of stable call descriptor; never the body | | `durationMs` | Time spent in the host-fn (excluding gateway frame) | | `errorCode` | Short code on `denied` / `error` (e.g. `not_declared_in_manifest`) | Tenant admins see this list under `/app/:tenant/settings/plugins/:id/audit`.