# A plugin grants and revokes file access through the files family Status: accepted (WO-FL-009, 2026-09-17) The CMS File library gates a published file and hands it to one person at a time through a File access grant (cms ADRs 0181, 0186, 0187). ADR 0181 named the storefront case explicitly: selling a single file belongs to a plugin, which creates a grant after payment and revokes it on a refund. Until now a plugin had no way to say either thing, because the capability surface carried no file family at all. Decision: the ABI gains `files.grant_access`, `files.revoke_access` and `files.list_grants`, mirrored by `plugin.FileGrants` on `CoreServices`. A grant names a file by its attachment kind and id and exactly one subject: a public user, or an email address that receives a signed unlock link instead. The response carries the stored grant, including that link, so a plugin that sells a file to somebody with no account can deliver it in its own receipt. **The source is the host's to write, and so is the revoke rule.** Nothing in these messages names the calling plugin: the host already authenticates the caller at the capability boundary, stamps `plugin:` on every grant this family writes, and refuses a revoke of a grant with any other source. A plugin therefore cannot attribute a grant to somebody else, and cannot take away access that an administrator, a workflow or another plugin gave. Passing the plugin name on the wire was rejected for the same reason the bridge does not: a value a guest supplies is a value a guest can change. A grant with no expiry is the ordinary case, because revocation is the control the CMS relies on; `expires_at` is optional and absent means "until revoked". Alternatives rejected: a general "write a row in a core table" capability, which would put the CHECK constraints and the unique indexes of a security table behind a generic escape hatch; and returning only a grant id, which would force a second call for the link every seller needs. Consequences: - New: `abi/proto/v1/capability.proto` messages `FileAccessGrant`, `FilesGrantAccessRequest`/`Response`, `FilesRevokeAccessRequest`/`Response`, `FilesListGrantsRequest`/`Response`; `plugin/wasmguest/caps/files.go` and its round-trip goldens; `plugin.FileGrants`, `plugin.FileGrantParams` and `plugin.FileGrant` in `plugin/deps.go`. - `CoreServices.FileGrants` is nil on a host that does not wire it, exactly like the other optional members, and the guest stub then fails the call rather than pretending. - The host half, the plugin attribution and the revoke guard live in the cms repo (`backend/plugin/wasmhost/caps/files.go`, ADR 0190 there). Keywords: files.grant_access, files.revoke_access, files.list_grants, FileGrants, FileGrantParams, FileGrant, FileAccessGrant, FilesGrantAccessRequest, FilesRevokeAccessRequest, FilesListGrantsRequest, filesStub, capability.proto, File access grant, unlock link, unlock_url, plugin grant, grant source, storefront plugin, File purchase, revoke own grants, WO-FL-009, ADR 0181, CoreServices, wasm ABI, host_call