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 asError::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.*coversvalues.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.
| Grant | Worlds | Covers |
|---|---|---|
values.read:<prefix> | ui, daemon | values.get, keys, watch outside the own namespace |
values.write:<prefix> | ui, daemon | values.set, delete outside the own namespace |
live.publish:<prefix> | ui, daemon | live.publish outside the own namespace |
live.subscribe:<prefix> | ui, daemon | live.subscribe, unsubscribe outside the own namespace |
events.emit:<namespace> | ui, daemon | events.emit into global.* or another plugin’s namespace |
events.on:<namespace> | ui, daemon | events.on, off for global.*, system.* or another plugin’s events |
call:<plugin id> | ui, daemon | calls.call to the companion (the host checks call:<own id>) |
secret:<NAME> | ui, daemon | secrets.get(NAME) |
account.read | ui, daemon | account.state, account.watch: machines, projects, pane metadata; never pane contents |
fetch:<host> | ui, daemon | HTTP requests to a host (daemon: wasi:http, daemon::http; also fetch:<scheme>://<host>[:<port>]; no UI interface yet) |
url.open:<host> | ui | url.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> | daemon | daemon-process.spawn/run of that program, exactly as spawned (/tmp/git is not git) |
fs.read:<path> | daemon | daemon-watch under the path, and the directory preopened read-only for WASI files |
fs.write:<path> | daemon | The directory preopened read-write for WASI files (reading included) |
panes.read | daemon | daemon-panes.panes, subscribe, wait |
panes.write | daemon | daemon-panes.create, input, close |
panes.create | daemon | Claims the New Pane handler: the companion answers standard.pane.create (handlers) |
machine.full | daemon | Every program, every path (/ and the home directory preopened read-write), and the network |
socket.connect:<host>:<port> | daemon | A WebSocket the host holds to that server (wss:// only, daemon::net::WebSocket, daemon-net); frames arrive as the plugin event system.net.websocket |
network.full | daemon | WASI 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_TOKENIn 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:
- the URL is plain
https://(no credentials, a host of letters, digits, dots and dashes, no whitespace or control characters, at most 2048 bytes), elseInvalid; - a
url.open:grant covers its host, elseGrantDenied { grant: "url.open:<host>" }; - 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:
| Warning | Why |
|---|---|
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 imports | Ask for what you use; the user reads every grant |
| A grant on the plugin’s own namespace | It 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:
- Move the effect to a companion daemon, which runs once (working together).
- 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.