Skip to main content
Three things must be true before a call does anything, and they are recorded in three places owned by different parties: 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 in crates/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.
flags.write.dynamic is permanently unimplemented: flags.setDynamic always answers flags.setDynamic is not implemented yet. A live write would need the in-process access ADR-001 and ADR-003 rule out. Plan around it. lifecycle.read is partial: it gates five core events, of which two are published by nothing (events). Everything else in the table works.
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.json is read for every enabled plugin with no capability check. flags.write gates a running plugin rewriting that file through flags.set.
  • A plugin’s own overlay/ directory is registered for every enabled plugin with no capability check. assets.override gates 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.
A denial is also recorded: 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:
Plugin code is installed once for the machine. What a plugin may do belongs to the account, so approving something in a profile you made to try it out does not approve it in the profile you play on (ADR-013).
  • A pre-existing global ~/.config/cordial/plugin-grants.json is moved into whichever profile first looks for one, in practice default. 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_GRANTS overrides 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": [].
Disabling is not revoking. Grants survive a disable untouched. Settings shows “Off. What you allowed it to do is kept.” for a disabled plugin that holds at least one grant, and a bare “Off” otherwise. Two states look alike and are reported differently: 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.