// The Standard Code plugin contract, version 2 (docs/plugin-runtime.md).
//
// Every plugin is a wasm component against this package. A UI plugin targets
// `ui-plugin` and paints through shared buffers in its own linear memory that
// the viewer samples on its paced frame cadence. A daemon plugin targets
// `daemon-plugin` inside `standardd` and never renders.
//
// Every interface of a world is linked at instantiation. Grants are checked
// per call: a call the plugin's approved grants do not cover returns
// `call-error::grant-denied` naming the missing grant, and the host counts the
// denial. Imports outside the world fail at instantiation, before any call.
package standard:plugin@2.0.0;
/// Types shared by every interface.
interface types {
/// A JSON document as text. Values, configuration, events, live messages
/// and call payloads are all JSON.
type json = string;
/// Why a host call did not run.
variant call-error {
/// The manifest does not carry the grant the call needs. The payload
/// names the grant (`values.read:git.*`, `account.read`, ...).
grant-denied(string),
/// The per-plugin rate limit for this call kind was reached.
rate-limited,
/// The backing service is not reachable right now.
unavailable(string),
/// The arguments were malformed (an unknown surface id, an oversized
/// value, a key outside the plugin's namespace).
invalid(string),
}
/// Where an event or call is delivered.
variant target {
/// Every listener on the account.
all,
/// Every viewer.
viewers,
/// Every daemon.
daemons,
/// The daemon on one machine, by machine id.
machine(string),
/// The singleton daemon of the plugin.
singleton,
}
}
/// Durable key-value state on the account, namespaced by plugin id.
/// Grants: `values.read:<prefix>` and `values.write:<prefix>`; the plugin's
/// own id is the default prefix.
interface values {
use types.{json, call-error};
get: func(key: string) -> result<option<json>, call-error>;
set: func(key: string, value: json) -> result<_, call-error>;
delete: func(key: string) -> result<_, call-error>;
keys: func(prefix: string) -> result<list<string>, call-error>;
/// Ask for `event::value-changed` for keys under the prefix.
watch: func(prefix: string) -> result<_, call-error>;
}
/// The unstored latest-value channel. Grants: `live.publish:<prefix>` and
/// `live.subscribe:<prefix>`. Deliveries arrive as `event::live`.
interface live {
use types.{json, call-error};
publish: func(key: string, payload: json) -> result<_, call-error>;
/// Withdraws the latest value of `key`: subscribers hear
/// `event::live-deleted`, and a later subscriber sees nothing for it.
/// Grant: the same `live.publish:<prefix>` as publishing it.
delete: func(key: string) -> result<_, call-error>;
subscribe: func(prefix: string) -> result<_, call-error>;
unsubscribe: func(prefix: string) -> result<_, call-error>;
}
/// Named, unstored messages between plugins. The plugin's own namespace
/// (`<id>.*`) needs no grant; `global.*`, another plugin's namespace and
/// `system.*` need `events.emit:<ns>` or `events.on:<ns>`. Deliveries arrive
/// as `event::plugin-event`.
///
/// The host delivers one system event to a daemon plugin unasked:
/// `system.plugin.interest` with `{ "surfaces": [<surface id>, ...] }`,
/// the plugin's UI surfaces some viewer on the account shows now (the union
/// over every viewer; an instance counts as its surface), once when the
/// plugin starts and again whenever it changes. A daemon half reads outside
/// sources for its UI only while it names something.
interface events {
use types.{json, call-error, target};
emit: func(name: string, payload: json, to: target) -> result<_, call-error>;
on: func(pattern: string) -> result<_, call-error>;
off: func(pattern: string) -> result<_, call-error>;
}
/// Request/response to a companion plugin. Grant: `call:<companion>`
/// (implied for companions).
///
/// A UI plugin prefers `send` and `call-async`: neither blocks its thread,
/// so `event` and `frame` stay within their budgets while the companion
/// works. At most 32 `send`s and `call-async`s of one plugin are in flight
/// at once; past that they answer `rate-limited`.
interface calls {
use types.{json, call-error, target};
/// Calls `method` and waits for the answer: at most `timeout-ms` (10 s
/// when none, 30 s at most), then `unavailable("call_timeout")`. Blocks
/// the plugin's thread meanwhile (never the viewer's); in a browser
/// without JSPI it answers `unavailable`.
call: func(method: string, payload: json, to: target, timeout-ms: option<u32>) -> result<json, call-error>;
/// Sends `method` and returns at once: nothing answers. For commands
/// whose outcome arrives another way (a value, a live message).
send: func(method: string, payload: json, to: target) -> result<_, call-error>;
/// Starts a call and returns its id at once; the answer (or its error,
/// a timeout included) arrives later as `event::call-result` with that
/// id. Ids are unique for the life of the plugin, restarts included.
call-async: func(method: string, payload: json, to: target, timeout-ms: option<u32>) -> result<u64, call-error>;
}
/// The plugin's configuration document, as the account stores it. No grant.
interface config {
use types.{json};
get: func() -> json;
}
/// The plugin's own report of how it is doing, shown beside its runtime
/// state in the Plugins view (a UI plugin's, per viewer) and by
/// `standard plugin health` (a daemon plugin's, per machine). Each call
/// replaces the last report; a plugin that never calls is `ok`. No grant.
interface health {
enum health-state {
ok,
/// Working, with a problem the user may want to know about (a
/// provider unreachable, a watch that stopped).
degraded,
/// Not doing its job until something changes.
failed,
}
/// Reports `state` with a short message for the user (at most 200
/// characters are kept; empty for `ok`).
set: func(state: health-state, message: string);
}
/// Named secrets the user entered for this plugin. Grant: `secret:<NAME>`.
interface secrets {
use types.{call-error};
get: func(name: string) -> result<option<string>, call-error>;
}
/// Read-only account state. Grant: `account.read`. Never what is inside a
/// pane: no screen, scrollback, input or output exists in this world.
interface account {
use types.{call-error};
record machine {
id: string,
name: string,
online: bool,
}
record project {
id: string,
name: string,
machine: string,
path: string,
}
enum agent-status {
none,
working,
idle,
waiting-for-input,
}
record pane {
id: string,
/// The pane's generation: 1 at creation, advanced by every restart
/// and restore. A pane id with its generation names one run of the
/// pane (`<id>@<generation>`); 0 while unknown.
generation: u64,
machine: string,
project: option<string>,
title: string,
/// The pane's current directory when the host knows it, else its
/// project root. A daemon learns it when an event already fires
/// for the pane (its creation, a new foreground program, an agent
/// hook), and a new directory arrives as a `changed` pane.
cwd: string,
cols: u32,
rows: u32,
/// The foreground program name, when known.
program: option<string>,
/// The agent the pane runs, when one is detected.
agent: option<string>,
agent-status: agent-status,
}
record account-state {
machines: list<machine>,
projects: list<project>,
panes: list<pane>,
}
state: func() -> result<account-state, call-error>;
/// Ask for `event::account-changed` on every change.
watch: func() -> result<_, call-error>;
}
/// Exactly-once effects across the runtimes of one plugin. Every viewer on
/// the account runs its own instance of a UI plugin, and a fleet daemon
/// plugin runs on every daemon; an effect that must happen once (a
/// notification, a write another instance would repeat) is claimed first,
/// and only the instance whose claim succeeded performs it. A singleton's
/// claims carry its lease epoch like every other write. Keys live in the
/// plugin's own namespace. No grant.
interface claims {
use types.{call-error};
/// Claims `key` for `ttl-ms`. `true`: this instance holds the claim and
/// performs the effect. `false`: another instance holds it. A claim
/// held by this instance is renewed.
claim: func(key: string, ttl-ms: u32) -> result<bool, call-error>;
/// Gives up a claim this instance holds; nothing when it holds none.
release: func(key: string) -> result<_, call-error>;
}
/// Where a UI plugin paints. UI world only.
///
/// A surface is one region of the plugin's own linear memory, laid out as the
/// host describes (`layout`), double-buffered with a sequence number so a
/// half-written slot is never sampled. The plugin allocates the region at the
/// size the host reports, attaches it, paints one slot, and commits with the
/// dirty rectangles. The viewer samples committed slots only when a frame is
/// due and the surface is visible, and reads only the dirty rectangles.
interface surface {
use types.{call-error};
enum model {
/// Graphemes, colours and attributes per cell.
cells,
/// RGBA8 pixels, the surface's size in cells times the cell pixel size.
pixels,
}
/// The size a surface currently has. `cols` and `rows` are cells;
/// `px-w`/`px-h` are the pixel size of the pixels model (zero while the
/// viewer has no pixel geometry); `cell-px-w`/`cell-px-h` the pixel size
/// of one cell.
record geometry {
cols: u32,
rows: u32,
px-w: u32,
px-h: u32,
cell-px-w: u32,
cell-px-h: u32,
}
/// How the region must be laid out, in bytes.
record region-layout {
/// Total length of the region: `header-len + 2 * slot-len`.
len: u32,
/// The header at offset zero: eight little-endian u32 words
/// `seq, model, cols, rows, px-w, px-h, cell-px-w, cell-px-h`.
header-len: u32,
/// Bytes per slot. Cells: `cols * rows * 16`
/// (`grapheme: u32, fg: u32, bg: u32, attrs: u16, pad: u16`).
/// Pixels: `px-w * px-h * 4` RGBA8.
slot-len: u32,
model: model,
geometry: geometry,
}
record rect {
x: u32,
y: u32,
w: u32,
h: u32,
}
/// The layout the named surface needs right now. `invalid` for an id the
/// manifest does not declare.
layout: func(id: string) -> result<region-layout, call-error>;
/// Bind a region of the plugin's memory as the surface's buffer. The
/// region must be at least `region-layout.len` bytes and carry the header for
/// the current geometry. Attaching again after `event::resize` rebinds
/// the surface to a new region; the host keeps sampling the old region
/// until the first commit at the new size and never reads it after that
/// commit, so the plugin may free it once that commit returns.
attach: func(id: string, region: list<u8>) -> result<_, call-error>;
/// Publish the slot the plugin just painted. `slot` is 0 or 1; `dirty`
/// lists the changed rectangles in cells (cells model) or pixels (pixels
/// model). An empty list changes nothing and requests no frame. The
/// first commit after a resize is treated as fully dirty.
commit: func(id: string, slot: u8, dirty: list<rect>) -> result<_, call-error>;
detach: func(id: string) -> result<_, call-error>;
/// Opens a surface the viewer shows only on request: a `stage` or a
/// `panel.popover`. Allowed only while the plugin handles a user
/// gesture (a `key`, `paste` or pointer `down` on one of its surfaces,
/// or one of its `command`s); `invalid` otherwise, and for any other
/// anchor. The surface receives `visibility` and `focus` once the
/// viewer shows it.
open: func(id: string) -> result<_, call-error>;
/// Closes a surface `open` opened; nothing when it is not open. Needs no
/// gesture. The user closes it too (Escape, or a click outside it); the
/// plugin sees `focus(id, false)` and `visibility(id, false)` either way.
close: func(id: string) -> result<_, call-error>;
/// Asks for a size instead of the manifest's `height`/`width`: `cols`
/// and `rows` in cells, 0 for "the viewer's choice" in that dimension,
/// except the rows of a `machine.after` or `project.after` instance,
/// where 0 is no rows: the instance is not drawn, takes no space and is
/// hidden until it asks for rows again. The viewer clamps it to what
/// the anchor allows where it places the surface (a card's width is
/// the sidebar's; a popover fits the modal area) and answers with a
/// `resize` when the size changes. Stays in force until asked again;
/// `invalid` for an unknown surface. An instance id
/// (`<surface>@<owner>`) may be asked before the viewer has sized it.
request-size: func(id: string, cols: u32, rows: u32) -> result<_, call-error>;
/// Sets a short label the viewer shows at the right end of the
/// surface's chrome title: a boxed `sidebar.card`'s top border, a
/// `pane`'s header, a `column`'s title row. `none` removes it. At most
/// 48 characters with no control characters; `invalid` otherwise, and
/// for an unknown surface. Stays until set again.
set-label: func(id: string, label: option<string>) -> result<_, call-error>;
/// A cell of a surface, from its top left.
record caret {
col: u32,
row: u32,
}
/// Places the viewer's own text cursor at `caret` in the surface while
/// the surface takes keys (an open `stage`, `panel.popover` or
/// `pane.overlay`, or the surface a click or `open` gave input focus),
/// so a text field draws no cursor of its own: the viewer paints its
/// cursor there, and the cursor trail moves to it as it moves between
/// panes. `none` removes it; a caret outside the surface is not drawn.
/// `invalid` for an unknown surface. Stays until set again.
set-caret: func(id: string, caret: option<caret>) -> result<_, call-error>;
}
/// Viewer-local state and capabilities. UI world only. No grant.
interface view {
use types.{call-error};
/// Whether this viewer is the one the user is controlling.
is-driving: func() -> bool;
/// The viewer's theme colours as 0xRRGGBB.
record theme-colours {
fg: u32,
bg: u32,
/// `fg` receded toward `bg`: "grayed out" (never a fixed gray).
recede-fg: u32,
recede-bg: u32,
accent: u32,
/// The terminal's 16 ANSI colours, 0 to 15, as the viewer resolved
/// them (the xterm defaults where the terminal did not say).
palette: list<u32>,
}
theme: func() -> theme-colours;
record viewer-capabilities {
/// Whether the pixels model paints as real graphics here.
graphics: bool,
/// The pixel size of one cell of this plugin's pixels surfaces.
cell-px-w: u32,
cell-px-h: u32,
/// The frame rate this plugin gets right now: the viewer's paced
/// rate, lowered by what the terminal link sustains and by the
/// slow-plugin policy.
fps: u32,
/// Device pixels per surface pixel: 1, or 2 while the slow-plugin
/// policy has halved this plugin's pixel resolution (the viewer
/// scales the image up).
pixel-scale: u32,
}
capabilities: func() -> viewer-capabilities;
/// The focused pane's id, when one is focused.
focused-pane: func() -> option<string>;
/// Shows the pane with id `pane` in this viewer's workspace and gives
/// it focus, as a click on its sidebar row does. Allowed only while the
/// plugin handles a user gesture (a `key`, `paste` or pointer `down` on
/// one of its surfaces, or one of its `command`s); `invalid` otherwise.
/// A pane the viewer does not know is ignored.
focus-pane: func(pane: string) -> result<_, call-error>;
/// A pane and its generation (`account.pane`'s `id` and `generation`).
record pane-ref {
id: string,
/// 0 while the viewer does not know it.
generation: u64,
}
/// The pane an instance surface belongs to, with its generation now. A
/// `pane.footer` or `pane.header` surface has one instance per pane
/// the viewer shows, named `<surface-id>@<pane-id>`; it arrives as
/// `resize` and `visibility` for that id, and the plugin creates a
/// surface by it. None for any other id.
surface-pane: func(surface: string) -> option<pane-ref>;
/// The machine an instance surface belongs to: a `machine.after`
/// surface has one instance per machine the viewer shows, named
/// `<surface-id>@<machine-id>`. None for any other id.
surface-machine: func(surface: string) -> option<string>;
/// The project an instance surface belongs to: a `project.after`
/// surface has one instance per project the viewer shows, named
/// `<surface-id>@<project-id>`. None for any other id.
surface-project: func(surface: string) -> option<string>;
/// The identity tint (0xRRGGBB) of what an instance surface belongs
/// to: a `machine.after` instance's machine, a `project.after`
/// instance's project, a pane instance's project (else its machine).
/// None without one, or for any other id.
surface-tint: func(surface: string) -> option<u32>;
/// The identity tint of a machine or project, by id, as the viewer
/// paints it; none when it has none.
identity-tint: func(id: string) -> option<u32>;
/// Asks for a `frame()` on the next paced frame, from any export
/// (an `event` included). Without a request, a commit made in `event`
/// is sampled on the next frame something else causes.
request-frame: func();
/// Asks for a `frame()` at `at-ms` on the viewer's clock (the clock
/// `frame` receives as `now-ms`). The earliest request wins.
wake-at: func(at-ms: u64);
/// The machine this viewer runs on, by the account's machine id; none
/// for a viewer that is not an enrolled machine (a browser).
machine-id: func() -> option<string>;
/// This viewer instance: the same for every plugin of one running
/// viewer, different in every other viewer (another terminal on the
/// same machine included) and after a restart.
instance-id: func() -> string;
/// The wall clock where the viewer runs: milliseconds since the Unix
/// epoch. For dates and countdowns; frames keep to `now-ms`.
wall-ms: func() -> u64;
/// The viewer's local time zone's offset from UTC right now, in
/// minutes, east positive (UTC+2 is 120). Follows daylight saving.
utc-offset-minutes: func() -> s32;
/// The viewer's IANA time zone name (`Europe/Berlin`), when known.
time-zone: func() -> option<string>;
}
/// Opens web pages in the user's browser. UI world only. Grant:
/// `url.open:<host>` (`url.open:*.example.com` for its subdomains too,
/// `url.open:*` for any host).
interface url {
use types.{call-error};
/// Opens `url` in the user's browser: on the machine the viewer runs on
/// (the system opener natively, a new tab in a browser viewer). Only
/// `https://` URLs, and only as a direct result of user input: while
/// the plugin handles a key, paste, pointer press or command on its
/// surfaces, or within one second after one was delivered, and once
/// per input. `invalid` otherwise, or for a URL that is not https, has
/// no host, or carries whitespace or control characters;
/// `grant-denied(url.open:<host>)` for a host no grant names.
open: func(url: string) -> result<_, call-error>;
}
/// What a plugin is told, beyond its frame callback.
interface event-types {
use surface.{geometry};
use types.{json, call-error};
/// The answer to `calls.call-async`.
record call-result {
/// The id `call-async` returned.
id: u64,
outcome: result<json, call-error>,
}
/// Modifier bits of `key.modifiers` and `pointer.modifiers`:
/// 1 shift, 2 ctrl, 4 alt (option), 8 super (command).
enum key-phase {
press,
/// The key is held and repeats.
repeat,
/// Only where the viewer's terminal reports releases.
release,
}
/// A key on the surface with input focus.
record key {
surface: string,
/// A character key is the character it types (`a`, `A`, `1`, `?`,
/// `é`); a named key is one of `enter`, `tab`, `backtab`,
/// `backspace`, `delete`, `insert`, `space`, `left`, `right`, `up`,
/// `down`, `home`, `end`, `pageup`, `pagedown`, `f1` to `f12`.
/// Escape never arrives: it closes an open surface.
code: string,
/// The text the key types, when it types text.
text: option<string>,
modifiers: u32,
phase: key-phase,
}
enum pointer-kind {
down,
up,
/// Motion with no button held.
move,
/// Motion with a button held.
drag,
/// One wheel step; `button` says which way.
wheel,
/// The pointer moved onto the surface (before its first `move`).
enter,
/// The pointer left the surface: moved elsewhere, or the surface
/// was hidden under it. `x`/`y`, `col`/`row` are the last position
/// on it.
leave,
}
/// The pointer over one of the plugin's surfaces.
record pointer {
surface: string,
/// The position in the surface's own units: cells in the cells
/// model, surface pixels in the pixels model (sub-cell where the
/// viewer knows the pointer's pixel position, else the cell's
/// centre).
x: u32,
y: u32,
/// The cell under the pointer.
col: u32,
row: u32,
/// 0 none, 1 left, 2 middle, 3 right; for `wheel`: 4 up, 5 down,
/// 6 left, 7 right.
button: u8,
kind: pointer-kind,
modifiers: u32,
}
variant event {
/// A surface changed size or model; call `surface.layout`,
/// allocate, attach.
resize(tuple<string, geometry>),
/// A surface became visible or hidden.
visibility(tuple<string, bool>),
/// Graphics support, cell pixel size, frame rate or pixel scale
/// changed (`view.capabilities`).
capabilities-changed,
/// `view.is-driving` changed.
driving(bool),
/// A watched value changed (`values.watch`).
value-changed(tuple<string, option<json>>),
/// A live message (`live.subscribe`).
live(tuple<string, json>),
/// A plugin event (`events.on`).
plugin-event(tuple<string, json>),
/// The account snapshot changed (`account.watch`).
account-changed,
key(key),
/// Text pasted into the surface with input focus: surface, text.
paste(tuple<string, string>),
pointer(pointer),
/// The viewer's theme changed; read `view.theme`.
theme-changed,
/// A surface gained or lost input focus: surface, focused.
focus(tuple<string, bool>),
/// The user ran one of the manifest's `commands`, by id.
command(string),
/// A `calls.call-async` finished.
call-result(call-result),
/// A live key was withdrawn (`live.delete`).
live-deleted(string),
}
enum power {
mains,
save-power,
}
}
/// A UI plugin: runs in every viewer, paints into its surfaces, never has
/// side effects on the account beyond what its grants allow.
world ui-plugin {
import types;
import values;
import live;
import events;
import calls;
import config;
import secrets;
import account;
import health;
import surface;
import view;
import event-types;
import claims;
import url;
use event-types.{event, power};
/// Called once after instantiation with the configuration document.
export activate: func(config: string);
/// Called when a paced frame is due and at least one of the plugin's
/// surfaces is visible. `now-ms` is the viewer's monotonic clock,
/// `interval-ms` the paced frame interval. Returns the `now-ms` at which
/// the plugin next wants a frame callback (`now-ms` for the next frame,
/// a later instant for a clock, none to wait for an event). Nothing wakes
/// an idle viewer otherwise.
export frame: func(now-ms: u64, interval-ms: u32, power: power) -> option<u64>;
export event: func(e: event);
export deactivate: func();
}
/// A daemon plugin: runs inside `standardd`, never renders. Every interface
/// of the world is linked, and the system interfaces check the plugin's
/// grants per call. `standardd` also links WASI 0.2 beside them (a plugin
/// built with the SDK's `wasi` feature imports it): `wasi:filesystem` with
/// exactly the directories the `fs.read:<path>` and `fs.write:<path>` grants
/// name preopened (`/` under `machine.full`), `wasi:http/outgoing-handler`
/// to the hosts `fetch:<host>` grants name, `wasi:sockets` only under
/// `network.full` or `machine.full`, clocks, random, and stdout/stderr into
/// the daemon's log prefixed with the plugin id.
world daemon-plugin {
import types;
import values;
import live;
import events;
import calls;
import config;
import secrets;
import account;
import health;
import claims;
import daemon-context;
import daemon-process;
import daemon-watch;
import daemon-panes;
import daemon-net;
use event-types.{event};
export activate: func(config: string);
export event: func(e: event);
/// What happened on the daemon's machine: watched files changed, a
/// child wrote output or exited, a pane changed.
export daemon-events;
/// Answers `calls.call` from the UI plugin with the same id.
export companion;
/// Drives the plugin's pending work: called after `activate`, after
/// every `event`, `handle-event` and `handle-call`, and at the instant
/// the previous call asked for. `now-ms` is the daemon's monotonic
/// clock. Returns the `now-ms` at which the plugin next wants a call
/// (none: only on an event). (Not named `poll`: a daemon plugin may
/// link wasi-libc, which defines a `poll` symbol.)
export drive: func(now-ms: u64) -> option<u64>;
export deactivate: func();
}
/// What a daemon plugin answers: `calls.call` from the UI plugin with the
/// same id (its companion). An exported interface rather than a world-level
/// function, so the world needs no `use` of `types` and the UI world a
/// shared SDK links beside it carries none either.
interface companion {
use types.{json, call-error};
/// Who made a call, as the account routed it.
record caller {
/// The machine the calling runtime runs on.
machine-id: string,
plugin-id: string,
/// `ui` or `daemon`.
kind: string,
/// The caller's lease epoch, when a singleton daemon called.
epoch: option<u64>,
/// The account controller lease when the call was made: the machine
/// the user controls from and its fencing token. A plugin acting for
/// the user compares it with the caller's machine.
controller-machine-id: option<string>,
controller-fencing-token: option<string>,
}
/// Answers one call. `invalid` for a method the plugin does not know.
handle-call: func(method: string, payload: json, %from: caller) -> result<json, call-error>;
}
/// Spawn programs on the daemon's machine. Grant: `process.exec:<program>`
/// (the program exactly as spawned; `process.exec:*` for any), or
/// `machine.full`. Children run in their own process group; the host kills
/// every group a plugin started when the plugin stops.
interface daemon-process {
use types.{call-error};
record spawned {
pid: u32,
}
/// Where a child's standard stream goes.
enum stdio {
/// Nowhere (`/dev/null`).
null,
/// To the plugin: output arrives as `daemon-event::process-output`,
/// input goes through `write`.
piped,
/// The daemon's log, each line prefixed with the plugin id (output
/// streams only; stdin reads nothing).
log,
}
record command {
program: string,
args: list<string>,
/// Variables set on top of the plugin's environment
/// (`daemon-context.environment`), after `env-remove`.
env: list<tuple<string, string>>,
/// Variables removed from the plugin's environment before `env` is
/// applied (`GIT_DIR`, say).
env-remove: list<string>,
/// Start from an empty environment: only `env`.
clear-env: bool,
cwd: option<string>,
stdin: stdio,
stdout: stdio,
stderr: stdio,
}
/// Spawns `program` with `args` in the plugin's environment: stdin
/// empty, output to the log. `cwd` defaults to the user's home.
spawn: func(program: string, args: list<string>, cwd: option<string>) -> result<spawned, call-error>;
/// Spawns a command with its environment and streams.
run: func(command: command) -> result<spawned, call-error>;
/// Writes to a piped stdin.
write: func(pid: u32, bytes: list<u8>) -> result<_, call-error>;
/// Closes a piped stdin (the child reads end of file).
close-stdin: func(pid: u32) -> result<_, call-error>;
/// The exit status once the child has exited (a signal is its negated
/// number); none while it runs.
try-wait: func(pid: u32) -> result<option<s32>, call-error>;
/// Blocks the plugin until the child exits. Prefer awaiting
/// `daemon-event::process-exited`.
wait: func(pid: u32) -> result<s32, call-error>;
/// Kills the child's process group.
kill: func(pid: u32) -> result<_, call-error>;
}
/// Where a daemon plugin runs and the environment it runs with. Daemon
/// world only; no grant.
///
/// The environment is the daemon user's login environment reduced to a
/// safe set: `HOME`, `USER`, `LOGNAME`, `PATH` (the login shell's, with its
/// version managers), `LANG`, every `LC_*`, `TMPDIR` and `SHELL`. Under
/// `machine.full` it is the whole login environment. WASI sees the same
/// variables (and `PWD`, the home directory); children inherit them.
interface daemon-context {
/// The machine this daemon runs on, by the account's machine id.
machine-id: func() -> string;
/// The account this daemon is enrolled in, once the daemon has read it.
account-id: func() -> option<string>;
/// The plugin's environment, sorted by name.
environment: func() -> list<tuple<string, string>>;
/// The lease epoch this instance runs under: a singleton's, which
/// every write of its account session carries (a later epoch means the
/// lease moved and this instance is stopping). None for a fleet plugin.
lease-epoch: func() -> option<u64>;
/// The Standard Code build this daemon runs.
record release-info {
/// The channel it came from: `branch:<name>` for a branch build,
/// else the published channel's name (`team`, `canary`,
/// `production`).
channel: string,
version: string,
/// The commit it was built from.
git-sha: string,
}
/// The build this daemon runs; none where it is not known (a local
/// development daemon).
release: func() -> option<release-info>;
}
/// WebSockets the host holds for the plugin, to servers a
/// `socket.connect:<host>:<port>` grant names (`wss://` only). The host
/// connects, answers and sends pings, and delivers what happens as the
/// plugin event `system.net.websocket` with a JSON payload
/// `{"socket": <handle>, "kind": "open" | "message" | "closed",
/// "text": <frame>, "reason": <why>}`, so a plugin waiting for frames is
/// never woken for nothing. Frames are text, 64 KiB at most; eight sockets
/// per instance; they close when the instance ends.
interface daemon-net {
use types.{call-error};
/// Starts connecting to `url` with extra request `headers` (none that
/// the handshake owns: `Host`, `Upgrade`, `Connection`, `Sec-*`). The
/// handle's `open` or `closed` event follows.
websocket-open: func(url: string, headers: list<tuple<string, string>>) -> result<u32, call-error>;
/// Sends one text frame.
websocket-send: func(socket: u32, text: string) -> result<_, call-error>;
/// Closes the socket; its `closed` event follows.
websocket-close: func(socket: u32);
}
/// File and directory change events, debounced by the host and delivered
/// as `daemon-event::file-changed`. Grant: `fs.read:<path>` covering the
/// path, or `machine.full`. The host never polls: a watcher that fails is
/// reported as `daemon-event::watch-failed`. A plugin holds at most 256
/// watches at once (`rate-limited` past that).
interface daemon-watch {
use types.{call-error};
record watch-options {
/// A directory and everything under it (true), or its own entries
/// only. Ignored for a file.
recursive: bool,
/// Globs relative to the watched path whose changes are dropped
/// before they reach the plugin: `*` within a component, `?` one
/// character, `**` any number of components. A glob without `/`
/// matches a component at any depth (`target`, `*.log`); one with
/// `/` is anchored at the watched path (`.git/objects`), and a
/// leading `/` anchors a single name there, as in `.gitignore`
/// (`/target`). Excluding a directory excludes everything in it. At
/// most 64.
exclude: list<string>,
}
/// Watches a file or a directory.
watch: func(path: string, options: watch-options) -> result<u32, call-error>;
unwatch: func(handle: u32) -> result<_, call-error>;
}
/// Panes on the daemon's machine. Grants: `panes.read` to list, subscribe
/// and wait; `panes.write` to create, type into and close. Writes follow the
/// daemon's own controller rules: a pane is created and typed into only
/// while this machine holds the account's controller lease.
interface daemon-panes {
use types.{call-error};
use account.{pane};
/// A pane `create` opened: its id and generation (`account.pane`'s),
/// together naming this run of it (`<id>@<generation>`).
record created-pane {
id: string,
generation: u64,
}
panes: func() -> result<list<pane>, call-error>;
/// Asks for `daemon-event::pane-changed` for this machine's panes.
subscribe: func() -> result<_, call-error>;
unsubscribe: func() -> result<_, call-error>;
/// Opens a pane in `cwd`: a project root of this machine or a directory
/// inside one (by whole components, after symlinks), which names the
/// project the pane belongs to. It runs `command` through the user's
/// login shell, or the shell itself when none, with `env` set on top of
/// its login environment (at most 64 variables; a name is not empty
/// and has no `=`, neither side a NUL). `invalid` for a directory
/// outside every project root.
create: func(cwd: string, command: option<string>, env: list<tuple<string, string>>) -> result<created-pane, call-error>;
/// What `create-with` opens: `create`'s arguments and the pane's title.
record new-pane {
cwd: string,
command: option<string>,
env: list<tuple<string, string>>,
/// The pane's title (at most 128 characters, no control
/// characters); the plugin's id when none.
title: option<string>,
}
/// `create`, with a title.
create-with: func(pane: new-pane) -> result<created-pane, call-error>;
input: func(pane: string, bytes: list<u8>) -> result<_, call-error>;
close: func(pane: string) -> result<_, call-error>;
/// Blocks until the pane exits or closes, for at most `timeout-ms`
/// (capped at 30 s). Whether it did.
wait: func(pane: string, timeout-ms: u32) -> result<bool, call-error>;
}
/// What happens on the daemon's machine. An exported interface, like
/// `companion`, so a UI component built with the same SDK never imports
/// these types.
interface daemon-events {
use account.{pane};
record process-output {
pid: u32,
/// Standard error rather than standard output.
stderr: bool,
bytes: list<u8>,
}
record file-change {
/// The handle `daemon-watch.watch` returned.
watch: u32,
/// Every path that changed since the previous delivery.
paths: list<string>,
}
enum pane-change-kind {
created,
/// Title, size or agent state changed.
changed,
exited,
closed,
}
record pane-change {
kind: pane-change-kind,
pane: pane,
}
variant daemon-event {
file-changed(file-change),
/// A watch stopped delivering: its handle and the watcher's error.
watch-failed(tuple<u32, string>),
/// Output from a piped stream; at most 64 KiB per event.
process-output(process-output),
/// A child exited: its pid and status (a signal is negated). Sent
/// after the last of its output.
process-exited(tuple<u32, s32>),
pane-changed(pane-change),
}
/// Called with each event, in order; the plugin is driven afterwards.
handle-event: func(e: daemon-event);
}