WO-FL-009. The files family reaches the CMS File access grant store: a plugin grants one file to a member or an address, lists what a file has handed out, and revokes what it gave. The response carries the signed unlock link, so a seller can deliver a file to somebody with no account. Nothing on the wire names the calling plugin: the host stamps plugin:<name> on what this family writes and refuses a revoke of anything else. ADR 0008. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.0 KiB
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:<name> 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.protomessagesFileAccessGrant,FilesGrantAccessRequest/Response,FilesRevokeAccessRequest/Response,FilesListGrantsRequest/Response;plugin/wasmguest/caps/files.goand its round-trip goldens;plugin.FileGrants,plugin.FileGrantParamsandplugin.FileGrantinplugin/deps.go. CoreServices.FileGrantsis 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