If installing a plugin were enough to grant what it asked for, the manifest would be a formality.
Default deny
A plugin absent from the grants file gets nothing, and a capability it requested but was not granted is refused at the point of use, by name.
There is no capability meaning “anything” and no grants entry meaning “all”. The list is closed (ADR-003): there is no
process.spawn, filesystem path or memory access to ask for.
The fifteen capabilities
The list is closed and lives incrates/cordial-plugins/src/capability.rs. The method-to-capability mapping is a closed table in protocol.rs::required_capability, so a typo in a method name fails as unknown rather than falling through to a check that happens to pass.
Every capability also has a second-person consequence sentence, which the install dialog shows instead of the wire name. A test asserts that no sentence contains its own dotted name.
Why the capabilities are split
preferences.get sits under settings.read, and there is deliberately no preferences.set: those answers are the user’s (ADR-020).
Two things that are not capabilities
A static file a plugin ships is not a request a process is making. It is what the plugin is, and installing and enabling it is the consent.- A plugin’s own
flags.jsonis read for every enabled plugin with no capability check.flags.writegates a running plugin rewriting that file throughflags.set. - A plugin’s own
overlay/directory is registered for every enabled plugin with no capability check.assets.overridegates a running plugin registering a directory at runtime. Without this a texture pack, which has no process, could not overlay anything. See ADR-021 and ADR-010.
What a refusal looks like
host::authorise runs before dispatch and returns one of two refusals itself; the ok is the handler’s.
Broker::allows pushes a Denial { plugin, capability } onto a list each time it refuses.
Passing the broker is not the same as reaching an effect. Refusals that come from the handler, all error:
An
error is used for an unwired method rather than denied, so you are not sent looking for a permission that was never the problem. Each method’s own refusals are on its page: flags, surface.
Grants are per profile
The file is<profile>/plugin-grants.json:
- A pre-existing global
~/.config/cordial/plugin-grants.jsonis moved into whichever profile first looks for one, in practicedefault. Every other profile starts at default deny. The move is skipped if the profile already has its own file, and a failed move leaves the old file untouched. Why: ADR-013. CORDIAL_PLUGIN_GRANTSoverrides the path for every profile at once. It is a development switch.
Granting and revoking
1
Install
consent::verdict decides whether to ask. A plugin with no entry module and no capabilities installs silently. Anything else gets a dialog listing each capability’s consequence sentence, and Allow writes every requested capability into the grants file at once. Escape and “Not now” grant nothing. Why: ADR-003.2
Switch it on
Allowing is not starting. A plugin with code is written into
plugin-enabled.json as off whatever the dialog said, and the success subtitle says so: “It is switched off until you turn it on.” Data-only plugins stay absent from the file, and therefore on.3
Adjust per capability
On the plugin’s row in Settings, one switch per requested capability. Revoking a plugin’s last capability removes its key from the file rather than leaving
"id": [].
Withheld capabilities are named at startup:
plugin <id>: not granted flags.write, presence.set.
The grants file is authoritative, and it is not intersected with the manifest. start_all hands this profile’s entry straight to Broker::grant; the manifest’s list only produces the “not granted” message and decides which switches Settings draws. A capability written into the file by hand that the manifest never requested is therefore granted at runtime (INFERRED from the code path, no client run). The one exception is a hot-swap restart after an update, which intersects (ADR-038).
Effects, never channels
A plugin never receives a socket, a file descriptor or a D-Bus connection. Cordial holds the permission and performs the effect; the plugin sends a payload.presence.set takes a presence structure and Cordial owns the Discord socket. notify.send and url.open are the same shape over the portal. Why: ADR-007, ADR-018.
If you need something the capabilities do not cover, open an issue. A resource Cordial does not already broker needs a change to Cordial, not to your manifest. A broker is a payload type and an effect, which makes adding one small, and that is also the test: if a proposed broker cannot be small, the capability is too broad and wants splitting.
ADR-027 proposes ui.notify, ui.hud and ui.panel. Its status is proposed and none of those names exists in capability.rs. It is a design under discussion, not something to call.