84 lines
5.0 KiB
Markdown
84 lines
5.0 KiB
Markdown
# 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`.
|