Skip to main content

Module grants

Module grants 

Source
Expand description

§Grants

A plugin can do anything inside its own namespace (<id>.*) without asking. Everything else is a grant in the manifest, and every grant carries a reason:

"grants": {
  "values.read:git.*": "Reads the branch state the git companion records",
  "secret:GITHUB_TOKEN": "Signs in to GitHub to read pull request checks"
}
  • The reason is one sentence the install screen shows under the grant: 1 to 140 characters after trimming. A grant without one, or with a longer one, is rejected by the host and by standard-plugin check.
  • The user approves the grants when installing; a new grant in an update asks again.
  • The host checks every call. A call no grant covers returns call-error::grant-denied(<grant>), which the SDK surfaces as Error::GrantDenied { grant } naming exactly what to add, and the host counts the denial (the viewer’s frame trace shows it).

§Matching

A grant kind:subject covers:

  • itself exactly (values.read:git.main);
  • with a trailing *, every subject with that prefix (values.read:git.* covers values.read:git.main; fetch:* covers every host). * is only allowed at the end;
  • grants without a subject (account.read) cover themselves.

The plugin’s own namespace needs no grant for values, live messages and events: plugin clock reads and writes clock.*, publishes, withdraws (live.delete) and subscribes to clock.*, and emits and listens to clock.* freely. Nothing may emit into system.*.

Some interfaces need no grant at all: config, claims, health, view (the viewer’s theme, palette, tints, clock, machine and instance), and a daemon plugin’s daemon-context (its machine, account, lease epoch and environment; machine.full widens the environment to the whole login environment).

§The grants

standard-plugin-manifest’s catalogue (CATALOGUE) is the list tools accept; anything else is an unknown grant.

GrantWorldsCovers
values.read:<prefix>ui, daemonvalues.get, keys, watch outside the own namespace
values.write:<prefix>ui, daemonvalues.set, delete outside the own namespace
live.publish:<prefix>ui, daemonlive.publish outside the own namespace
live.subscribe:<prefix>ui, daemonlive.subscribe, unsubscribe outside the own namespace
events.emit:<namespace>ui, daemonevents.emit into global.* or another plugin’s namespace
events.on:<namespace>ui, daemonevents.on, off for global.*, system.* or another plugin’s events
call:<plugin id>ui, daemoncalls.call to the companion (the host checks call:<own id>)
secret:<NAME>ui, daemonsecrets.get(NAME)
account.readui, daemonaccount.state, account.watch: machines, projects, pane metadata; never pane contents
fetch:<host>ui, daemonHTTP requests to a host (daemon: wasi:http, daemon::http; also fetch:<scheme>://<host>[:<port>]; no UI interface yet)
url.open:<host>uiurl.open of an https:// URL on that host in the user’s browser, on user input (below); url.open:*.<domain> also covers the domain’s subdomains, url.open:* any host
process.exec:<program>daemondaemon-process.spawn/run of that program, exactly as spawned (/tmp/git is not git)
fs.read:<path>daemondaemon-watch under the path, and the directory preopened read-only for WASI files
fs.write:<path>daemonThe directory preopened read-write for WASI files (reading included)
panes.readdaemondaemon-panes.panes, subscribe, wait
panes.writedaemondaemon-panes.create, input, close
panes.createdaemonClaims the New Pane handler: the companion answers standard.pane.create (handlers)
machine.fulldaemonEvery program, every path (/ and the home directory preopened read-write), and the network
socket.connect:<host>:<port>daemonA WebSocket the host holds to that server (wss:// only, daemon::net::WebSocket, daemon-net); frames arrive as the plugin event system.net.websocket
network.fulldaemonWASI sockets to any address, and HTTP to any host

The account keeps a plugin’s secrets. Set one from a pipe, never an argument, and remove it the same way:

printf %s "$GITHUB_TOKEN" | standard plugin secret set my-card GITHUB_TOKEN
standard plugin secret unset my-card GITHUB_TOKEN

In a viewer, C in the Plugin settings view opens a prompt for the secrets a plugin’s secret: grants name: the value types or pastes as dots, and Enter stores it on the account. The viewer never keeps or shows it.

A path grant covers its directory and everything beneath it by whole path components, after symlinks are resolved: fs.read:/srv/data covers /srv/data/x, not /srv/database, and a link inside it that points elsewhere does not lead out.

The install and approval screens list machine.full, network.full and every process.exec: and fetch: grant first, marked high risk. The plugin store says what each grant lets the plugin do in plain language (standard_plugin_manifest::grant_text) above the reason. The grants are the plugin’s requirements, not choices: nothing is ticked one by one, and the only alternative to installing (or approving an update) is not to.

A UI plugin asking for a daemon-only grant is rejected: it could never use it.

§Opening URLs

url::open(url) (or cx.open_url(url)) opens a page in the user’s browser: natively the viewer’s machine’s system opener (open on macOS, xdg-open on Linux, the URL its one argument, never a shell), in a browser viewer a new tab (noopener). Every host checks, in order:

  1. the URL is plain https:// (no credentials, a host of letters, digits, dots and dashes, no whitespace or control characters, at most 2048 bytes), else Invalid;
  2. a url.open: grant covers its host, else GrantDenied { grant: "url.open:<host>" };
  3. the plugin is handling user input (a key, paste, pointer press or command on its surfaces) or was given some within the last second, and has not opened a URL for it yet, else Invalid.

The browser viewer checks the grant and the input again on its main thread before it opens anything. A plugin with nothing the user did can never open a page.

§What check warns about

standard-plugin check (and build, which runs it) reports errors the host would refuse and warnings for what it would accept but probably should not:

WarningWhy
An interface the component imports has no covering grant (account without account.read, secrets without a secret: grant, calls without call:<id>, the daemon interfaces without theirs; machine.full covers daemon-process and daemon-watch, as the host does)Every call will be denied
A grant whose interface the component never importsAsk for what you use; the user reads every grant
A grant on the plugin’s own namespaceIt needs none
Double play: a UI plugin with an events.on: grant and any of fetch:, values.write:, events.emit:, call:See below

§Double play

A UI plugin runs once in every open viewer. If it listens to shared events and reacts with an effect, the effect happens once per viewer: three open viewers, three writes, three requests, three emitted events (which other viewers’ instances may react to in turn). check flags the combination. The fixes, in order of preference:

  1. Move the effect to a companion daemon, which runs once (working together).
  2. Act only in the viewer the user is driving (view::is_driving()), and claim the effect first (claims().once(key, ttl, ...)) so two viewers that both believe they drive still act once.