standard-plugin-sdk 0.1.1

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
// 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);
}