@ai-ecoverse/slicc 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +664 -8
  2. package/package.json +8 -8
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `slicc` is SLICC's command line for seven, the SLICC on `*.sliccy.ai`. It is one binary for macOS, Windows and Linux. It launches and links browsers, and shares a machine's network, compute and browsers with a running seven over a WebRTC link. `npx sliccy` and Sliccstart.app run this binary. It replaces slicc-node and slicc-swift (see [Coming from slicc-node](#coming-from-slicc-node)).
4
4
 
5
- **Status:** being built. `slicc` (the default flow), `slicc attach` (with the `cdp` capability), `slicc open`, `slicc pair` and `slicc unpair` (seven in your everyday browser, without CDP), `slicc <join-url> follow`, remote control with `prompt`, `watch` and `abort` (also from inside seven's kernel, see [Inside seven](#inside-seven)), host folders with `--mount`, `slicc browsers`, `slicc launch`, `update`, `install`, `--version` and `--help` work so far, plus a hidden diagnostic, `slicc <join-url> follow --signal-only [--timeout 2m]`. It attaches through the tray hub, prints a summary of the leader's offer (its size, media sections, candidate count and DTLS fingerprint) and the ICE server URLs (credentials are never printed), and exits without answering. The link protocol is drafted alongside seven's link exit and moves into this README once both sides pass a shared test.
5
+ **Status:** being built. `slicc` (the default flow), `slicc attach` (with the `cdp` capability), `slicc open`, `slicc pair` and `slicc unpair` (seven in your everyday browser, without CDP), `slicc <join-url> follow`, remote control with `prompt`, `watch` and `abort` (also from inside seven's kernel, see [Inside seven](#inside-seven)), host folders with `--mount`, `slicc browsers`, `slicc launch`, `update`, `install`, `--version` and `--help` work so far, plus a hidden diagnostic, `slicc <join-url> follow --signal-only [--timeout 2m]`. It attaches through the tray hub, prints a summary of the leader's offer (its size, media sections, candidate count and DTLS fingerprint) and the ICE server URLs (credentials are never printed), and exits without answering. The link protocol is specified in [Link protocol](#link-protocol).
6
6
 
7
7
  ## Commands (planned)
8
8
 
@@ -1188,11 +1188,11 @@ slicc --help
1188
1188
  - **`3`, end:** that side's END. slicc closes the connection after both ends or a reset.
1189
1189
  - **Errors:** an unknown type, a control record over 64 KiB, any record over 16 MiB, a control record that isn't JSON, or a later control record that isn't a `reset` makes slicc send `{ "reset": "EPROTO" }` and fail the stream. `ENOTCONN` (the tray can't be reached) fails `prompt` and `abort` with exit 1; `watch` says `lost the link to seven; reconnecting` and retries with its usual backoff, as it does when a connection drops.
1190
1190
 
1191
- **The host module.** Go on `wasip1` can't open sockets, so the package's `host/slicc-host.mjs` gives `slicc.wasm` the `slicc_host` imports, on the kernel's loopback sockets (the calls the JS program API's `net.connect` uses): `probe`, `connect(host, port)` (loopback only, non-blocking), `connected(fd, timeout)`, `read(fd, buffer, timeout)`, `write(fd, buffer, timeout)` (non-blocking) and `close(fd)`. Connects, reads and writes wait at most 100 ms at a time, so signals, timeouts and a reset that arrives while a long prompt uploads get through. Without the module, slicc exits 1 with `this runtime has no slicc_host module`.
1191
+ **The host module.** Go on `wasip1` can't open sockets, so the package's `host/slicc-host.mjs` gives `slicc.wasm` the `slicc_host` imports, on the kernel's loopback, through the imports-module API's `ctx.net.connect` and `ctx.net.ready` and the `fd-read`, `fd-write`, `fd-select` and `fd-close` calls (slicc-kernel 1.92.0 or later, the package's `engines.slicc-kernel`): `probe`, `connect(host, port)` (loopback only, non-blocking), `connected(fd, timeout)`, `read(fd, buffer, timeout)`, `write(fd, buffer, timeout)` (non-blocking) and `close(fd)`. Connects, reads and writes wait at most 100 ms at a time, so signals, timeouts and a reset that arrives while a long prompt uploads get through. Without the module, slicc exits 1 with `this runtime has no slicc_host module`. On a kernel older than 1.92.0, which has no `ctx.net`, `connect` answers `ENOSYS`, so slicc exits 1 with `can't reach seven's link facade`.
1192
1192
 
1193
1193
  **The package.** `@ai-ecoverse/wasi-slicc` holds `bin/slicc.wasm`, `host/slicc-host.mjs`, this section as its README, and the LICENSE. Its `package.json` has `"slicc": { "abi": "wasi", "commands": { "slicc": { "wasm": "bin/slicc.wasm", "imports": "host/slicc-host.mjs" } } }` and `engines.slicc-kernel`. The release attaches `slicc.wasm` as `slicc-wasip1-wasm.wasm`, in `SHA256SUMS` and its attestation (see [Releases](#releases)).
1194
1194
 
1195
- **Tests.** `test/integration/wasi_test.go` builds `slicc.wasm`, stages the package with `npm-stage.mjs --wasi-only`, and runs it in `@ai-ecoverse/slicc-kernel`'s Node kernel (an exact-pinned devDependency, so `npm ci` first) against a fake facade, a kernel JS program in `testdata/wasi/fakefacade/`. `cmd/slicc` builds for both: a file there that needs a native-only package carries `//go:build !wasip1`, and `wasi_test.go` fails when the `wasip1` build breaks or links one of the left-out packages.
1195
+ **Tests.** `test/integration/wasi_test.go` builds `slicc.wasm`, stages the package with `npm-stage.mjs --wasi-only`, and runs it in `@ai-ecoverse/slicc-kernel`'s Node kernel (an exact-pinned devDependency, so `npm ci` first) against a fake facade, a kernel JS program in `testdata/wasi/fakefacade/`. `wasi_realfacade_test.go` runs it against the kernel's own link facade (`createNodeKernel({ facades: { link } })`), served by a test page hook, `testdata/wasi/page.mjs`, that implements [§5.16](#516-the-link-facade-page-side-not-on-the-wire-the-in-kernel-slicc): it checks the stamp the hook receives (root and a cone in `sudo` pass, a scoop gets `EACCES`) and that the kernel sets `SLICC_LINK_URL`. `cmd/slicc` builds for both: a file there that needs a native-only package carries `//go:build !wasip1`, and `wasi_test.go` fails when the `wasip1` build breaks or links one of the left-out packages.
1196
1196
 
1197
1197
  ## Tray hub signaling (remote setup)
1198
1198
 
@@ -1212,10 +1212,664 @@ A node on another machine sets up its link through the tray hub, the `slicc-tray
1212
1212
 
1213
1213
  **Join URL state.** The node remembers the current join URL for each join URL it was given, so it reconnects after a network change or restart without walking the supersede chain again. The state lives in `<config>/slicc/tray-join.json`, where `<config>` is `~/Library/Application Support` on macOS, `$XDG_CONFIG_HOME` or `~/.config` on Linux, and `%AppData%` on Windows. The directory is mode `0700` and the file `0600`, written atomically. Entries are keyed by the SHA-256 of the given join URL, and at most 16 are kept.
1214
1214
 
1215
+ ## Link protocol
1216
+
1217
+ The wire protocol between seven (the page) and `slicc` (the node), v1. seven's side is the `link` exit in [slicc-bios](https://github.com/ai-ecoverse/slicc-bios), and both sides must stay in step with this section, the same way [slicc-node's README](https://github.com/ai-ecoverse/slicc-node#protocol) holds the local-proxy protocol.
1218
+
1219
+ **Decisions:**
1220
+ - **WebRTC is the only link layer, local and remote.** CDP only carries the setup messages for a local link ([§1.1](#11-local-setup-over-cdp)). For a remote link, the tray hub carries them, using six's normal join and follow URL ([§1.2](#12-remote-setup-over-the-tray-hub)).
1221
+ - **No Tailscale in the CLI.** It would need an account, so it feels less owned. The rendezvous and TURN are sliccy.ai's own.
1222
+ - **Go** for the CLI, in `ai-ecoverse/slicc-cli`. [`go-optel`](https://github.com/ai-ecoverse/go-optel) gets its own repo.
1223
+ - **No six compatibility.** The CLI doesn't speak six's tray-sync messages inside the data channel. It reuses only the tray hub's signaling.
1224
+ - **Compute goes over SSH** on the link ([§5.5](#55-ssh-cap-ssh-page--node)), not a hand-rolled exec protocol. The kernel has an `ssh` client.
1225
+
1226
+ ### 1. Peers and setup
1227
+
1228
+ - **page**: seven. It owns the `RTCPeerConnection` and always sends the **offer**.
1229
+ - **node**: the `slicc` binary (pion). It always sends the **answer**.
1230
+ - A **link** is one peer connection. A page can hold several links, one exit each.
1231
+
1232
+ #### 1.1 Local setup over CDP
1233
+
1234
+ Used when the node already holds a CDP connection to the browser that runs seven. No key in a URL, and nothing listening on loopback.
1235
+
1236
+ 1. The node finds the seven target. That's a page target whose main-frame origin is allowed, using the same origin rule as slicc-node's gate: `https://<label>.sliccy.ai` other than `www`, plus any `--origin`.
1237
+ 2. It attaches (`flatten: true`), calls `Runtime.enable` and `Runtime.addBinding { name: "sliccLinkSend" }`.
1238
+ 3. It sends `{ "t": "hello", "v": 1, "nonce": "<base64url 16 bytes>", "node": "<random per-process id>" }` (see [§5.11](#511-agent-page-cap-agent-node--page-remote-control) for how the page uses `node`) by calling `Runtime.evaluate` on `globalThis.sliccLinkReceive?.(<json>)`.
1239
+ 4. The page answers through the binding, `sliccLinkSend(<json>)`, with:
1240
+ - `{ "t": "offer", "nonce", "sdp" }`
1241
+ - `{ "t": "candidate", "nonce", "candidate" }` (trickled; `null` at the end)
1242
+ 5. The node sends the following through `sliccLinkReceive`:
1243
+ - `{ "t": "answer", "nonce", "sdp" }`
1244
+ - its own `candidate` messages
1245
+ 6. Either side can send `{ "t": "bye", "nonce", "reason" }`.
1246
+
1247
+ Rules:
1248
+ - **The node checks where a binding call came from.** It accepts one only when `Runtime.bindingCalled.executionContextId` is the main frame's default context and that context's origin is allowed (from `Runtime.executionContextCreated`). It ignores calls from iframes, workers, other origins, or with a stale `nonce`.
1249
+ - **Hello delivery** is retried until both `globalThis.sliccLinkReceive` and the `sliccLinkSend` binding exist in the main frame's context. The node calls `addBinding` again when the binding is missing (seen on macOS around the about:blank → page swap). The page should also check that `sliccLinkSend` exists before calling it.
1250
+ - **On exit** the node sends `bye`.
1251
+ - **Navigating or reloading seven drops the link.** The node runs the setup again when a new allowed context appears.
1252
+ - **ICE:** host candidates only. There's no STUN or TURN, since both peers are on the same machine.
1253
+ - Chrome hides host candidates behind mDNS (`<uuid>.local`), so the node resolves them (pion `MulticastDNSMode` query-only).
1254
+ - **Candidates:** a `candidate` message carries `RTCIceCandidateInit` JSON (`candidate.toJSON()`: `candidate`, `sdpMid`, `sdpMLineIndex`, `usernameFragment`), and `null` once gathering ends.
1255
+ - **Local Network Access:** Chrome 154 stable doesn't gate WebRTC with it yet. The check exists behind `--enable-features=LocalNetworkAccessChecksWebRTC` and will ship.
1256
+ - So before setup, the node grants the permission for the seven origin over CDP, best effort: `Browser.setPermission` with `local-network-access`, `local-network` and `loopback-network` set to `granted`. Errors are ignored on Chromes without them.
1257
+ - `Browser.grantPermissions` would replace the origin's other grants, so it isn't used.
1258
+ - Measured on Chrome 154 and macOS: the link comes up in 0.3–0.9 s. Loopback throughput is about 40–45 MB/s Chrome→pion and about 70–80 MB/s pion↔pion; the CPU limits it, not the window.
1259
+ - **No data fallback.** CDP carries only the setup, never link data.
1260
+ - **HELLO timeout:** each side closes the link if the peer's HELLO hasn't arrived 10 s after the control channel opens.
1261
+ - **`--signal stdio`** (tests): the same messages as NDJSON on stdin and stdout. Each `offer` starts a new link and replaces the current one. A harness may write `{"t":"hello"}` to get a fresh `hello` and nonce, for example after it reloaded the page.
1262
+
1263
+ #### 1.2 Remote setup over the tray hub
1264
+
1265
+ The scenario: `slicc` on a laptop launches Chrome there. seven runs on a desktop, in a browser no CDP client controls. After the CLI follows the desktop's tray, seven drives the laptop's Chrome and uses the laptop's network.
1266
+
1267
+ - **Rendezvous: the tray hub.** seven is a **tray leader** on the existing tray hub (six's `slicc-tray-hub` worker), and every linked CLI is a **follower**.
1268
+ - The wire contract is [`packages/shared-ts/src/tray-signaling.ts`](https://github.com/ai-ecoverse/slicc/blob/main/packages/shared-ts/src/tray-signaling.ts). The leader creates the tray with `POST /tray` (joinToken + controllerToken) and handles the bootstrap over its control WebSocket.
1269
+ - Followers attach at `/join/<token>` and run the bootstrap poll, answer and candidate exchange.
1270
+ - TURN credentials come from the hub. Media is peer-to-peer when ICE allows it, else through TURN. The hub sees only SDP and candidates.
1271
+ - **Pairing:** seven's Network panel → *Link a device* shows the normal join URL and the command, `npx sliccy <join-url> follow`, in six's grammar. One join URL serves any number of devices, as in six.
1272
+ - **The join URL is the secret**, long-lived like six's. Rotating the tray replaces it.
1273
+ - **Supersede:** the deployed hub answers with HTTP `308`, a `Location` header, `Link: <…>; rel="successor-version"` and `result.action: "redirect"`. The CLI never lets its HTTP client follow it. It reads `Link`, then `Location`, then the body, accepts the legacy `409 TRAY_SUPERSEDED` shape too, and follows at most five hops per dial.
1274
+ - **State:** the CLI keeps the current URL in `<config>/slicc/tray-join.json` (mode `0600`). Entries are keyed by the SHA-256 of the URL the user gave, so that URL itself isn't stored. That way `follow` reconnects after a network change or restart.
1275
+ - **Runtime tag:** link followers attach with `runtime: "slicc-link/1"`, so the leader can tell them from six's followers (`slicc-cli`, iOS).
1276
+ - **Identity is the DTLS certificate.** Each CLI install keeps one persistent DTLS certificate per hub origin in `<config>/slicc/` (mode `0600`). It survives restarts and updates.
1277
+ - The page identifies a device by the certificate's SHA-256 fingerprint from the follower's answer SDP (lower-case hex, no colons). The Network panel, the default-exit choice (`link:<fp>`) and the unlink list are all keyed on it.
1278
+ - A reconnect under a new `controllerId` replaces the old link and keeps its address.
1279
+ - An answer without a SHA-256 fingerprint gets `bootstrap.failed` `NO_FINGERPRINT`.
1280
+ - A new user account, machine or config dir is a new device.
1281
+ - **`controllerId` is only a per-dial routing id.** The CLI mints a fresh one for every dial, as six does. A stable one would make the hub hand back a stale `connected` bootstrap on a quick reconnect (`BootstrapCoordinator.ensure()`).
1282
+ - **Unlinked devices:** when an answer carries a fingerprint the leader has unlinked, the leader fails that bootstrap with `bootstrap.failed { code: "UNLINKED", retryable: false }` ([§5.7](#57-unlink-cap-join-or-unlink-loopback-on-13-links-page--node)), then closes the peer connection.
1283
+ - The hub allows a failure even after `connected`, so the follower keeps polling the bootstrap after posting its answer, at least until HELLO arrives. Over the tray the page drops its `null` end-of-candidates, which pion doesn't need.
1284
+
1285
+ #### 1.3 Local setup without CDP: loopback signalling
1286
+
1287
+ The scenario: seven is open in the user's **everyday browser** (any vendor, no CDP, no remote debugging), on the same machine as `slicc`, as slicc-node allowed. Only the signalling and trust setup are new. Once the peer connection is up, the link is [§2](#2-channels)–[§5](#5-stream-kinds) unchanged.
1288
+
1289
+ **Pairing and trust**
1290
+ - **What anchors trust:** two pinned certificates.
1291
+ - **The node's** persistent DTLS certificate. It gets a loopback identity of its own in `<config>/slicc/` (mode `0600`), as for the tray hub ([§1.2](#12-remote-setup-over-the-tray-hub)).
1292
+ - **The page's** persistent `RTCCertificate`: seven generates it once per origin with `RTCPeerConnection.generateCertificate`, keeps it in IndexedDB, and uses it for every loopback link.
1293
+ - Each side pins the other's SHA-256 fingerprint at pairing. After that, the node answers only offers whose SDP fingerprint it has paired, and the page accepts only an answer whose fingerprint it pinned. This replaces [§1.1](#11-local-setup-over-cdp)'s CDP binding and per-context nonce.
1294
+ - **Pairing URL:** `slicc open` (or `slicc --default-browser`) opens `https://seven.sliccy.ai/#slicc-pair=<port>.<token>.<node-fp>` in the default browser. `slicc pair` only prints it, for a seven that's already open (the user pastes it into seven's address bar, or seven's *Link this computer* field).
1295
+ - **The token:** 128 random bits, base64url, one-time, and valid for 10 minutes.
1296
+ - **The URL is the secret**, as slicc-node's fragment was. It carries the node's fingerprint, so a process squatting the port can't pass as the node.
1297
+ - **On the page:** seven reads and strips the fragment at once (`history.replaceState`), then shows a tray confirmation: "Link **<hostname>** (`ab12…ef`)? This computer's network, folders and kernel services become available to seven." [Link] [Cancel]. slicc prints the same hostname and short fingerprint. Only [Link] (a user gesture) starts the first loopback request, which also satisfies Chrome's Local Network Access prompt (below).
1298
+ - **Pairing exchange** (one request): `POST http://127.0.0.1:<port>/slicc/v1/pair` with `{ "token", "offer": "<sdp>" }`. The offer is non-trickled: the page waits for ICE gathering to complete. The node checks the token, burns it, and pins the offer's fingerprint together with the request's `Origin`. It answers `{ "answer": "<sdp>", "node": "<per-process id>", "name": "<hostname>" }`, and the page checks that the answer's fingerprint equals `<node-fp>` from the URL.
1299
+ - **Reconnect** (no token): `POST /slicc/v1/signal` with `{ "offer" }`. The node answers only for a paired page fingerprint whose pinned origin equals the request's `Origin`; otherwise it returns `403`. The page checks the answer against its pin. There's no other endpoint.
1300
+ - **Unpairing:** seven's Network panel *Unlink* forgets the pin and, if it's linked, first opens an **`unlink` stream** ([§5.7](#57-unlink-cap-join-or-unlink-loopback-on-13-links-page--node)) on the live link. A loopback node advertises cap `unlink-loopback` for this. The node answers OPENED `{}` then END, deletes that page's pin, and closes that link; unlike [§5.7](#57-unlink-cap-join-or-unlink-loopback-on-13-links-page--node) it keeps running and keeps its certificate. A page that unlinks while not linked just forgets its pin, and the node's stale pin is harmless (the page won't signal again) until `slicc unpair`. `slicc unpair [--all]` removes pins on the node. Pins live in `<config>/slicc/loopback-peers.json` (mode `0600`), keyed by page fingerprint, with origin and first-seen time.
1301
+
1302
+ **The listener**
1303
+ - **Address:** `127.0.0.1` only (and `[::1]`), never `0.0.0.0`. Port **17117** (slicc-node's), or the first free port in 17117–17126. The pairing URL carries the actual port; on reconnect the page tries the remembered port, then the range.
1304
+ - **Gate, on every request:**
1305
+ - `Host` must be `127.0.0.1:<port>`, `[::1]:<port>` or `localhost:<port>` (against DNS rebinding);
1306
+ - the method and the two paths are exact;
1307
+ - `Origin` must pass exactly the same rule as CDP setup ([§1.1](#11-local-setup-over-cdp)): slicc-cli's `IsSevenOrigin` with slicc-browsers' catalog `ExcludedLabels` (so `www`, `vault`, `mcpcat`, `get` and every other service label never pair or signal), plus `--origin`;
1308
+ - the body is JSON, at most 64 KiB;
1309
+ - **preflight:** `OPTIONS` answers only for allowed origins, with `Access-Control-Allow-Private-Network: true`, `Access-Control-Allow-Origin: <that origin>` (never `*`), no credentials, and `Vary: Origin`.
1310
+ - Everything else gets `403` with no body. The listener isn't a proxy and serves nothing else. `Origin` can be forged by non-browser processes, but the gate only filters browsers; trust is the token or the pinned fingerprint.
1311
+ - **Rate limit:** at most 10 pairing attempts a minute; a wrong token burns nothing and counts.
1312
+
1313
+ **Local Network Access**
1314
+ - Chrome's LNA prompt appears on seven's first request to `127.0.0.1`. seven only makes loopback requests after a pairing [Link] click, or on load when it has a pin, so users who never pair are never prompted.
1315
+ - If the permission is denied, the page says how to allow it in site settings.
1316
+ - **Mixed content:** `http://127.0.0.1` is potentially trustworthy, so Chrome, Edge, Firefox and Safari allow it from https pages. bios CI tests Chrome and Firefox; Safari is checked by hand before release. Where a browser blocks it, the page offers the tray path ([§1.2](#12-remote-setup-over-the-tray-hub)).
1317
+
1318
+ **The link in this mode**
1319
+ - **ICE:** host candidates on loopback only, as in [§1.1](#11-local-setup-over-cdp).
1320
+ - **Caps:** `net`, `http`, `ws`, `credentialed`, `fs` (with `--mount`), `kernel-in` services ([§5.9](#59-kernel-listener-on-the-node-local-only), listener on `--kernel-port`), `ssh` with `--runner`, `jerk`/`jerk-seal`, and `agent`. **No `cdp`:** the node controls no browser.
1321
+ - **Policy:** the local default ([§6.4](#64-local-default-slicc-the-bridge-to-the-browser-it-controls)). Its protected ports have no served browser here, so they become 9222–9229 plus every port in a `DevToolsActivePort` file the node finds read-only, plus the node's own listeners. A kernel program can't reach a debugging port the user enabled.
1322
+ - **PRE-1** ([§5.8](#58-cdp-cap-cdp-page--node)) protects seven's origin from seven's own browser control. In this mode there's none, so PRE-1 doesn't apply. The `http` cap is the node's own client and carries no browser cookies or storage, so fetching a `*.sliccy.ai` URL through it exposes nothing of the user's session.
1323
+ - **Restarts:** the node keeps its certificate and port, and the page keeps its certificate and pin. On link loss the page retries `/signal` with backoff (1 s up to 30 s, and on `online` or `visibilitychange`). A restarted `slicc` is reachable again with no new pairing. The `node` id changes, and the page keys the device on the fingerprint, so the name stays.
1324
+ - **Several sevens:** the node holds one link per paired page fingerprint, and each is an exit on its own.
1325
+
1326
+ **Entry points** (automatic fallback):
1327
+ - **Plain `slicc`** with no SLICC profile and no CDP endpoint (none launched, none found, none given) falls back to this mode by itself: it opens the pairing URL in the default browser and links over loopback.
1328
+ - The dashboard (and a plain-mode status line, plus a `--json` event `{"event":"mode","mode":"loopback","reason":"no-browser"}`, then `{"event":"pairing","url"}` for each pairing URL, and the same `linked` and `closed` events as other modes) says: "No SLICC browser or debugging endpoint found, so seven opens in your default browser for pairing. Use --profile or --cdp to choose."
1329
+ - **`--no-fallback`** keeps the pre-fallback behaviour, for scripts: launch an installed Chromium into a new SLICC profile when one exists, and exit with "no browser to control" only when nothing can host seven. Without it, non-interactive runs (no TTY) still fall back, but print the pairing URL instead of opening a browser unless `--open` is given. With it, nothing falls back, TTY or not.
1330
+ - **`slicc open`, `slicc --default-browser` and `slicc pair`** stay as the explicit forms.
1331
+ - **A found or given profile or CDP endpoint always wins;** the fallback only triggers when both are absent.
1332
+
1333
+ #### 1.4 A seven as follower: leader to leader
1334
+
1335
+ One seven (A) follows another seven's (B) tray, so that a program in A's kernel, the in-kernel `slicc` ([§5.16](#516-the-link-facade-page-side-not-on-the-wire-the-in-kernel-slicc)), can prompt, watch and abort B's agent. A's page plays the node's part of [§1.2](#12-remote-setup-over-the-tray-hub) and of the link; B needs no change.
1336
+
1337
+ - **Signalling:** A's page joins B's tray at `/join/<token>` as a follower, exactly as the CLI does ([§1.2](#12-remote-setup-over-the-tray-hub)): bootstrap poll, answer, candidates, TURN from the hub.
1338
+ - **The runtime tag:** `slicc-link/1`.
1339
+ - **The answer:** A's `RTCPeerConnection` answers B's offer.
1340
+ - **Identity:** one persistent `RTCCertificate` per (A's origin, hub origin), kept in IndexedDB and passed to every `RTCPeerConnection` A opens toward that hub.
1341
+ - **How B sees A:** B identifies and gates A by its fingerprint, as any device.
1342
+ - **On `unlink` from B ([§5.7](#57-unlink-cap-join-or-unlink-loopback-on-13-links-page--node)):** A deletes that certificate and its follower state for the hub.
1343
+ - **Follower state:** keyed by the SHA-256 of the join URL the program gave (as the CLI's `tray-join.json`). It follows hub supersedes ([§1.2](#12-remote-setup-over-the-tray-hub)). The URL itself is never stored in the clear and never logged.
1344
+ - **HELLO:** A sends `caps: []` (control-only, [§5.11](#511-agent-page-cap-agent-node--page-remote-control)), a per-link `node` id, and its name: the seven's own name, or its origin's label.
1345
+ - **Streams:** A opens only `agent` streams ([§5.11](#511-agent-page-cap-agent-node--page-remote-control)), and refuses every stream B opens with `RESET EACCES`.
1346
+ - **Lifetime:** one link per join URL, opened on demand, kept while a facade stream uses it, and closed 10 minutes after the last one ends.
1347
+ - **Only under kernel users `enforce`:** A's page follows trays and serves the facade only when its kernel runs users `enforce`. Under `off` or `report`, every process is effectively root.
1348
+
1349
+ ### 2. Channels
1350
+
1351
+ - **Control channel:** label `slicc.link.v1`. The page creates it, ordered and reliable. Each side's **first message on it is HELLO** (text, JSON, [§4](#4-hello)). Every later message on any channel is a binary frame ([§3](#3-frames)). A text message after HELLO closes the link.
1352
+ - **Stream channels:** label `slicc.link.v1.stream`, ordered and reliable. A stream's opener may create a new channel for it. That channel then carries exactly that stream and is closed once the stream has ended in both directions or been reset.
1353
+ - **Shared streams:** an opener may instead put a stream on the control channel, for short streams like `resolve` or once it has 256 stream channels open. Chrome caps SCTP stream ids at 1023, so about 512 per side.
1354
+ - **A stream lives on the channel its OPEN arrived on.** Receivers must handle both layouts. Closing a stream channel resets any stream that hasn't ended.
1355
+ - A second OPEN on a stream channel closes the link.
1356
+ - Frames for a stream that arrive on any other channel are ignored.
1357
+ - **Message size:** at most 65 541 bytes (5-byte header plus 65 536). That's below what Chrome and pion advertise for `max-message-size`.
1358
+ - **Keep reading.** A receiver reads every channel all the time and buffers up to the stream's window ([§3.3](#33-flow-control)). It never applies backpressure by not reading. SCTP has one receive window per association, so one unread channel would stall every channel. The node raises pion's SCTP receive buffer to 4 MiB.
1359
+
1360
+ ### 3. Frames
1361
+
1362
+ slicc-node's kernel-tunnel framing, unchanged on the wire:
1363
+
1364
+ | byte 0 | bytes 1–4 | rest |
1365
+ |---|---|---|
1366
+ | type (`u8`) | stream id (`u32` BE) | payload |
1367
+
1368
+ | type | name | payload |
1369
+ |---|---|---|
1370
+ | 1 | OPEN | UTF-8 JSON `{ "kind", … }` ([§5](#5-stream-kinds)) |
1371
+ | 2 | OPENED | UTF-8 JSON, `{}` when there is nothing to say |
1372
+ | 3 | DATA | 1 to 65 536 bytes |
1373
+ | 4 | END | empty: the sender half-closes (`SHUT_WR`) |
1374
+ | 5 | RESET | UTF-8 `ERRNO` or `ERRNO: message`, aborts both directions |
1375
+ | 6 | CREDIT | `u32` BE bytes consumed, at least 1 |
1376
+
1377
+ #### 3.1 Stream ids
1378
+ Page-opened streams are odd and node-opened streams even, never 0, and never reused on a link.
1379
+
1380
+ #### 3.2 Lifecycle
1381
+ - The receiver of OPEN answers OPENED or RESET. DATA and END flow only after OPENED, in both directions.
1382
+ - A stream is over after END in both directions, or a RESET from either side.
1383
+ - Frames for an unknown id are ignored, because they may cross a RESET.
1384
+
1385
+ #### 3.3 Flow control
1386
+ - **Window:** at most 256 KiB of DATA per stream and direction that hasn't been credited.
1387
+ - **Credit:** the receiver credits bytes once it has handed them on (a socket write returned, `writer.write()` resolved).
1388
+ - **Stopping:** a sender that's out of window stops reading its source.
1389
+
1390
+ #### 3.4 Violations
1391
+ - **Reset the stream with `EPROTO`:**
1392
+ - DATA or END before OPENED, or after the peer's END;
1393
+ - empty DATA;
1394
+ - DATA past the window, CREDIT of 0, or CREDIT past what was sent;
1395
+ - an OPEN whose payload isn't JSON with a `kind`.
1396
+ - **Close the link:** an unknown type, a short or malformed frame, a duplicate id, or an id with the wrong parity.
1397
+
1398
+ ### 4. HELLO
1399
+
1400
+ ```json
1401
+ {
1402
+ "v": 1,
1403
+ "role": "node",
1404
+ "name": "slicc on laptop",
1405
+ "host": "laptop",
1406
+ "version": "1.4.0",
1407
+ "mode": "local",
1408
+ "caps": ["net", "http"],
1409
+ "routes": { "prefixes": [], "exit": true },
1410
+ "policy": { "allow": [], "deny": ["127.0.0.1/32:9222"], "summary": "every public address, this machine, and the local network" }
1411
+ }
1412
+ ```
1413
+
1414
+ - **`role`:** `page` or `node`.
1415
+ - **`name`:** for display. **`host`** (node only): the machine's short hostname, lower-cased, as a hint for the DNS label. The page makes it DNS-safe and unique per link (`laptop-2`) and serves it as `<host>.slicc.internal` on a synthetic address in `198.18.57.0/24`. `--name` overrides both.
1416
+ - **`mode`:** `local` (CDP setup) or `remote` (hub).
1417
+ - **`caps`:** what this side accepts OPENs for.
1418
+ - node: `net` (`tcp` and `resolve`), `http`, `ssh`, `cdp`, `fs`, `join`, `jerk` and `jerk-seal` ([§5.14](#514-jerk-caps-jerk-and-jerk-seal-page--node-j1)).
1419
+ - node, without a kind: `credentialed` ([§5.13](#513-credentialed-node-cap-http-streams-that-carry-a-value)), a promise about how the node handles `http` streams.
1420
+ - page: `kernel-in` ([§5.4](#54-kernel-page-cap-kernel-in-node--page)).
1421
+ - **`routes`** (node only): the prefixes it offers as specific routes, plus `exit: true` when it's willing to be a default exit.
1422
+ - **`policy`:** for display only; the node enforces it ([§6](#6-policy-node)).
1423
+ - **Versions:** each side ignores fields it doesn't know. If `v` differs, the side with the lower `v` decides, or closes the link when it can't.
1424
+ - **Before HELLO:** neither side sends OPEN until it has received the peer's HELLO.
1425
+ - **Unsupported kinds:** OPEN for a kind the receiver didn't advertise gets RESET `EOPNOTSUPP`.
1426
+
1427
+ ### 5. Stream kinds
1428
+
1429
+ #### 5.1 `tcp` (cap `net`), page → node
1430
+ - **OPEN:** `{ "kind": "tcp", "host": "example.com", "port": 443 }`. `host` is an IP literal or a name, and a name is resolved by the node with the system resolver.
1431
+ - **OPENED:** `{ "localAddr": "192.168.1.20:53122", "remoteAddr": "93.184.215.14:443" }` once `connect()` succeeded.
1432
+ - **RESET codes:**
1433
+ - `ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`, `ETIMEDOUT` (connect timeout 30 s), `ECONNRESET`;
1434
+ - `EACCES` for a policy refusal;
1435
+ - `ENOTFOUND` when a name doesn't resolve (the page maps it to whatever the kernel uplink wants).
1436
+ - **During the stream:** END maps to `closeWrite()` on both sides, and a socket error mid-stream is `RESET ECONNRESET`.
1437
+
1438
+ #### 5.2 `resolve` (cap `net`), page → node
1439
+ - **OPEN:** `{ "kind": "resolve", "name": "example.com", "family": 0 }`, where `family` is `4`, `6` or `0` for both.
1440
+ - **OPENED:** `{ "addresses": ["93.184.215.14", "2606:2800:21f:cb07:6820:80da:af6b:8b2c"], "ttl": 30 }`, immediately followed by END from the node. The page then sends END too.
1441
+ - **Filtering:** addresses the node's policy would refuse are dropped before answering.
1442
+ - **Empty list:** `addresses: []` means "not mine", whether the name doesn't exist or every address was filtered, so the page can try another exit.
1443
+ - **TTL:** Go's resolver doesn't report TTLs, so v1 always sends `30`.
1444
+ - **Details:**
1445
+ - IP literals come back as themselves when allowed, and `::ffff:a.b.c.d` as `a.b.c.d`.
1446
+ - Results are deduplicated and filtered to the requested family.
1447
+ - A lookup **timeout** is `RESET ETIMEDOUT`, so the page doesn't cache a transient failure as "not mine".
1448
+ - Other failures (e.g. SERVFAIL) are `[]`.
1449
+ - A missing name or a bad family is `RESET EPROTO`.
1450
+ - **Loopback answers:** a `resolve` answer with a loopback address is useless to the page. The kernel never sends loopback out through an exit, so it would dial its own loopback. Test names in `--hosts` should map to a non-loopback address of the node's machine. For `http` by name, the node resolves it itself, so loopback works there.
1451
+
1452
+ #### 5.3 `http` (cap `http`), page → node
1453
+ - **OPEN:** `{ "kind": "http" }`. The node answers OPENED `{}` at once.
1454
+ - **Bytes page → node:** slicc-node's raw-fetch request, a `u32` BE length, the JSON head `{ "url", "method", "headers" }`, then the body, then END.
1455
+ - **Bytes node → page:** a `u32` BE length, the JSON head `{ "status", "statusText", "headers", "url" }`, then the decoded body, then END.
1456
+ - **Same rules as slicc-node's `fetch-proxy`:**
1457
+ - the header drop and fold rules, and `Accept-Encoding`;
1458
+ - redirects not followed;
1459
+ - decoding, and the `206`-with-a-decoded-coding refusal, which becomes `RESET EPROTO`.
1460
+ - **Differences from slicc-node:**
1461
+ - The request body streams under the window, so there's no 256 MiB buffer.
1462
+ - Errors before the response head are RESETs: `ECONNREFUSED: fetch failed: …`, `EACCES` for policy, `EPROTO` for a malformed head.
1463
+ - **Policy and TLS:** the node's HTTP client dials through the same policy-checked dialer as `tcp` and verifies TLS with the system roots.
1464
+ - **Details** ([slicc-cli#21](https://github.com/ai-ecoverse/slicc-cli/pull/21)):
1465
+ - Header bytes are Latin-1 both ways, as fetch's ByteString is.
1466
+ - The request head is at most 1 MiB.
1467
+ - A body that ends within 1 MiB is sent with its exact `Content-Length`. A longer one uses the page head's `Content-Length` when that is one valid number, and is otherwise sent chunked.
1468
+ - Methods are upper-cased the way fetch does it.
1469
+ - `CONNECT`, `TRACE`, `TRACK`, URLs with credentials, and header values past U+00FF or with control characters are `RESET EPROTO`.
1470
+ - No default `User-Agent` is added.
1471
+ - A TLS failure is `ECONNREFUSED: fetch failed: x509…`.
1472
+ - An upstream error mid-body, or a broken coding, is `RESET ECONNRESET`.
1473
+ - **Credentialed streams:** `{ "kind": "http", "credentialed": true }`, only to nodes with the `credentialed` cap ([§5.13](#513-credentialed-node-cap-http-streams-that-carry-a-value)).
1474
+
1475
+ #### 5.4 `kernel` (page cap `kernel-in`), node → page
1476
+ - **OPEN:** `{ "kind": "kernel", "port": 5173 }`. This is how the node carries `<port>.kernel.localhost` navigations and WebSockets into the page, as slicc-node's kernel tunnel does today.
1477
+ - **Answer:** the page calls `kernel.dial({ port })` and answers OPENED or `RESET ECONNREFUSED`.
1478
+ - **The page refuses port 9222** (the CDP facade) with `EACCES`, and so does any port the kernel reserves.
1479
+ - **Nothing else comes in.** The page refuses every other node-opened kind with `EOPNOTSUPP`.
1480
+
1481
+ #### 5.5 `ssh` (cap `ssh`), page → node
1482
+ - **OPEN:** `{ "kind": "ssh" }`. The stream is a raw byte stream into the node's embedded SSH server (`golang.org/x/crypto/ssh`). That server never listens on a real port.
1483
+ - **Addressing** (page router): each link gets `<host>.slicc.internal` and a synthetic address in `198.18.57.0/24`. A kernel connect to port 22 there becomes this stream kind. Any other port becomes `OPEN { kind: 'tcp', host: '127.0.0.1', port }` under the node's policy. So `ssh laptop.slicc.internal` and `git` over ssh work unchanged.
1484
+ - **Auth:** the link is the trust boundary, so the server accepts `none` authentication on link streams, and ignores the user name (`$USER` in the kernel). Commands run as the user who started `slicc`.
1485
+ - **Host key:** one ed25519 key per node, kept `0600` in its config directory so it survives restarts. HELLO carries it as `"sshHostKey": "ssh-ed25519 AAAA…"`. The page writes `<host>.slicc.internal <key>` into the kernel's **system-wide `/etc/ssh/ssh_known_hosts`**, because seven manages those keys and wasix-openssh's per-user file sits under the passwd home, not `$HOME`. So `ssh` never has to trust a first-seen key.
1486
+ - **Client limits** (wasix-openssh 10.6.0-1): no ControlMaster, so each `ssh` is its own stream. Interactive `^C` may stop the local client until [slicc-kernel#247](https://github.com/ai-ecoverse/slicc-kernel/issues/247); exec mode is unaffected.
1487
+ - **What a session can do:**
1488
+ - `exec` and `shell` run through the node's runner, the first `--runner` argv (for example `bash -c`) as a forced command, with PTYs: `creack/pty`, ConPTY on Windows.
1489
+ - The `sftp` subsystem serves files.
1490
+ - `direct-tcpip` forwarding is checked against the same policy as `tcp`.
1491
+ - The persistent REPL from six's `follow --eval` is the `slicc-eval` subsystem: one REPL per node, started on first use and shared by every session. Each eval ends after a quiet window (default 500 ms). Its framing is in [SSH server](#ssh-server).
1492
+ - `sftp` is on by default only when the runner is a plain local shell (`bash`, `sh`, `zsh`, `fish`, `pwsh`, `powershell`, `cmd`). Otherwise it is off, because sftp serves the host's files as the user and would get around a sandboxing runner such as `docker exec …`. `--sftp` / `--no-sftp` override.
1493
+ - A refused `direct-tcpip` is answered `administratively prohibited`, with the policy message.
1494
+ - **Off by default:** a node without `--runner` doesn't advertise `ssh`, and refuses the OPEN with `EOPNOTSUPP`. If a session reaches the server anyway, it is refused the same way as a direct-tcpip.
1495
+
1496
+ #### 5.6 `join` (cap `join`), page → node, remote links only
1497
+ - **Why:** "New join URL" in seven creates a fresh tray and does **not** supersede the old one, so a leaked URL stops working. Devices linked at that moment learn the new URL over the link.
1498
+ - **OPEN:** `{ "kind": "join", "url": "https://www.sliccy.ai/join/<token>" }`, on the control channel.
1499
+ - **The node:**
1500
+ - It accepts only a join URL on the **same hub origin** as the one it followed. Anything else gets `RESET EACCES`.
1501
+ - It stores the URL as its current one (`tray-join.json`, mode `0600`), replacing the old one.
1502
+ - It answers OPENED `{}`, then END.
1503
+ - It keeps the current link, and uses the new URL on its next reconnect.
1504
+ - It never prints the URL.
1505
+ - **Advertised:** only by nodes that set up through the tray hub. A local (CDP) node doesn't advertise `join`, and refuses the OPEN with `EOPNOTSUPP`.
1506
+ - **The page:** it sends `join` to every linked device advertising the cap, waits for OPENED (or a RESET or timeout) from each, then retires the old tray. A device that was offline at that moment must be linked again, and that is the point of revocation.
1507
+
1508
+ #### 5.7 `unlink` (cap `join`, or `unlink-loopback` on §1.3 links), page → node
1509
+ On a [§1.3](#13-local-setup-without-cdp-loopback-signalling) loopback link, `unlink` removes that page's pin and closes the link only ([§1.3](#13-local-setup-without-cdp-loopback-signalling) *Unpairing*). The rest of this section is for remote links.
1510
+ - **OPEN:** `{ "kind": "unlink" }` on the control channel. The node answers OPENED `{}`, then END.
1511
+ - **Then the node:**
1512
+ - deletes its join state and DTLS certificate for that hub origin;
1513
+ - closes the link;
1514
+ - stops following, printing `unlinked by <leader name>`, and exits 0.
1515
+ - **A node offline at the time:** it gets `bootstrap.failed` `UNLINKED` once its next answer reveals the unlinked fingerprint, and does the same.
1516
+ - **Re-linking:** running `slicc <join-url> follow` again then mints a new certificate and shows up as a new device, which is what the user asked for by running it. The leader keeps refusing the old fingerprint.
1517
+
1518
+ #### 5.8 `cdp` (cap `cdp`), page → node
1519
+ - **HELLO:** a node with `cdp` lists its browsers: `"browsers": [{ "id": "chrome", "name": "Google Chrome", "version": "154.0.8037.98", "seven": true }]`.
1520
+ - `id` is stable for the life of the node: the browser kind, with `-2`, `-3`… for a second or third of the same kind.
1521
+ - `seven: true` marks the browser that hosts this seven tab (local setup only).
1522
+ - **OPEN:** `{ "kind": "cdp", "browser": "<id>" }`. The node opens a **fresh** WebSocket to that browser's `webSocketDebuggerUrl` for each stream, so every stream is its own CDP client.
1523
+ - **OPENED:** `{ "product", "protocolVersion" }` from `/json/version`.
1524
+ - **RESET:** `ENOENT` for an unknown id; `ECONNREFUSED` when the browser doesn't answer.
1525
+ - **Bytes, both directions:** a sequence of messages. Each is a `u32` BE length plus one UTF-8 CDP JSON message, at most 64 MiB, otherwise `RESET EMSGSIZE`. The node relays each one as a WebSocket text message.
1526
+ - **Filtered:** the node doesn't relay `Network.webSocketFrameReceived` or `Network.webSocketFrameSent` (parity with slicc-node's `/cdp`).
1527
+ - **Errors:**
1528
+ - An OPEN without `browser`, or a page END in the middle of a message, is `RESET EPROTO`.
1529
+ - `EMSGSIZE` applies in both directions, including a browser message over 64 MiB.
1530
+ - **Chrome's opt-in debugging of a running browser** (`wsOnly`, no `/json/version`): OPENED comes from `Browser.getVersion`.
1531
+ - **Closing:**
1532
+ - A **clean** browser close (close frame 1000) gives END. After it, the node discards page bytes until the page's END.
1533
+ - Any other browser close gives `RESET ECONNRESET`.
1534
+ - A page END closes the socket with 1000, then the node sends END. A page RESET closes it at once.
1535
+ - **Kernel side:** seven maps its port-9222 facade to these streams. `playwright-cli --runtime <id>` (slicc-cdp) picks the browser.
1536
+ - **Which browsers:**
1537
+ - **`attach`:** the browser it set up through (`--cdp <url>`, single-valued, `seven: true`), plus every `--cdp-extra <url>` (repeatable); the TUI's multi-select passes those. A second `--cdp` is an error that points to `--cdp-extra`.
1538
+ - **`follow`:** only the browsers named with `--cdp <url>`. Without one, `follow` doesn't advertise `cdp`, because a remote seven gets full control of every browser listed. `follow` never marks a browser `seven: true`.
1539
+ - **Protected ports:** every listed browser's debugging port is protected in the policy on both commands, so a `tcp` stream can't reach the raw port even under `--allow`.
1540
+
1541
+ - **Protected origins (PRE-1):** the node filters every `cdp` stream against `protectedOrigins` from slicc-browsers (≥ 1.4.0: `*.sliccy.ai` at any depth, any port, http and https; `vault.sliccy.ai` by name; plus the setup's extra origins). It refuses navigating to, attaching to, or driving targets on those origins, and storage, cookie and permission access for them, as **CDP errors** `{"error":{"code":-32000,"message":"slicc: refused: …"}}`, not RESETs. It detaches sessions that land on them and redacts their `url`/`title` in listings to the origin.
1542
+ - **In targets the stream touched** (attached to or created): the node fails every protected document load with `net::ERR_BLOCKED_BY_CLIENT`, so instrumentation installed before a navigation can never run in a protected document. That covers redirects, service-worker-served navigations and the page's own Fetch interception.
1543
+ - **Auto-attached targets with no URL yet** (OOPIFs, popups) are held until the URL is known, then detached if protected. After 10 s with no URL they are treated as protected. The node's own setup connection ([§1.1](#11-local-setup-over-cdp)) isn't a `cdp` stream and is the one exception. [`cdp` (browser control)](#cdp-browser-control) has the details.
1544
+
1545
+ #### 5.8.1 `browsers` updates (page cap `browsers`), node → page
1546
+ - **When:** the browser list changes while a link is up (a browser quits, or the TUI adds one). The node then opens `OPEN { "kind": "browsers", "browsers": [ … ] }` on the control channel, carrying the **full** current list in HELLO's shape. It replaces the previous list; there are no deltas.
1547
+ - **Answer:** the page answers OPENED `{}`, then END, and the node sends END. A RESET is ignored, since the next update carries the full list anyway.
1548
+ - **Only to pages that advertise `browsers`.**
1549
+ - **Ids never change meaning:** a removed id is never reused for another browser, and re-adding the same endpoint (same CDP host:port) gets its old id back.
1550
+ - **Removal:** a later `OPEN cdp` for a removed id gets `ENOENT`, and the node resets that browser's open `cdp` streams with `ECONNRESET: browser removed`.
1551
+ - **Automatic removal:** the node checks each browser's debugging port every 2 s. After two misses it removes the browser (`ECONNRESET: browser quit`). Once the port answers again, it lists the browser again under the same id.
1552
+
1553
+ #### 5.9 `kernel` listener on the node (local only)
1554
+ - **When:** a local node whose peer advertises `kernel-in` listens on `127.0.0.1:<kernel port>` (default `80`, or `--kernel-port`; `--no-kernel` turns it off). It serves `http://<port>.kernel.localhost/`, with slicc-node's rules ([its README, *Kernel services*](https://github.com/ai-ecoverse/slicc-node#kernel-services)):
1555
+ - the request head is read for at most 64 KiB and 30 s;
1556
+ - exactly one `Host` of the form `<1–65535>.kernel.localhost`, with no port or the listener's own port, no leading zeros and no trailing dot;
1557
+ - errors: `421`, `400`, `431`, `502 no seven page connected`, `502 nothing listening on kernel port N`, `504`.
1558
+ - **Per connection:** the node opens `OPEN { "kind": "kernel", "port": N }` to the page, sends the bytes it has read, then pipes both ways.
1559
+ - The node refuses port 9222 itself, before any OPEN.
1560
+ - Its listener port joins the policy's protected ports.
1561
+ - **Binding:** if the port can't be bound, the node prints `kernel services are off: … try --kernel-port` and goes on without them.
1562
+ - **Remote nodes** never listen.
1563
+
1564
+ #### 5.10 `fs` (cap `fs`), page → node: host folders over the link
1565
+ slicc-node's hostfs ([its README, *Host folders*](https://github.com/ai-ecoverse/slicc-node#host-folders)), carried as link streams instead of HTTP. The kernel's `hostfs` driver keeps its operations and semantics. [Host folders](#host-folders) is the detailed spec, and this section is the summary.
1566
+
1567
+ - **Export:** `--mount <path>[:<name>][:ro]` (repeatable) on `attach`, `follow` and the default flow.
1568
+ - The root is `realpath`'d at start. A missing path, a file, or a taken name is skipped with a warning.
1569
+ - Names never reveal host paths.
1570
+ - Without `--mount`, the node doesn't advertise `fs`.
1571
+ - **HELLO:** `"mounts": [{ "name", "readonly", "capabilities": { "maxIo", "symlinks", "chmod", "caseInsensitive", "normalization", "ranges": true } }]`.
1572
+ - `capabilities` is exactly slicc-node's grant answer, probed **per mount** on that folder's volume, so seven passes it to the driver unchanged.
1573
+ - **No tokens:** the link is the trust boundary.
1574
+ - **Read-only per OPEN:** the page may set `readonly: true` on any OPEN, and the node enforces it per OPEN (`EROFS` for any write, write open or namespace change), even on a writable export.
1575
+ - **Stream shape:** one stream per op, `OPEN { "kind": "fs", "mount", "readonly"?, "op", …args }` with slicc-node's ops and arguments. The OPEN is at most one frame (64 KiB).
1576
+ - The node answers OPENED (plus DATA for `list` and `read`), then END. The page sends END once it has OPENED.
1577
+ - **Errors:** `RESET <ERRNO>: <message>`, where the message is libuv's lower-case strerror text and never contains a path. Wrongly typed or out-of-range arguments, and an unknown `op`, are `EINVAL`. An unknown `mount` is `ENOENT`.
1578
+ - **Op answers:**
1579
+ - Most ops answer in OPENED with slicc-node's answer JSON.
1580
+ - **`list`:** OPENED `{}`, then NDJSON as DATA (`{"name","attr"}` per line), then END.
1581
+ - It's streamed in batches with bounded memory, so an entry that vanishes between batches is left out.
1582
+ - If the directory itself is moved or replaced mid-list, the stream ends with `RESET ESTALE`.
1583
+ - **`read`:** OPENED `{ "etag", "size", "offset", "length" }`, then exactly `length` bytes as DATA, then END. The read is short at EOF, `length: 0` past it, and `RESET ESTALE` if the file shrinks during the read.
1584
+ - **Etags** keep slicc-node's quoting, so `attr.etag` and `ifMatch` compare as plain strings.
1585
+ - **Layout:** the page should give reads over 64 KiB their own channel, and the node accepts either layout.
1586
+ - **`write`:** `OPEN { "kind": "fs", "mount", "op": "write", "fh", "offset" }`, on its own stream channel.
1587
+ - The node first runs the checks that need no I/O, and RESETs before OPENED with `EROFS`, `EBADF` (unknown fh, or one from another mount or without write flags) or `EINVAL`.
1588
+ - Then OPENED `{}`, and DATA, credited once each `pwrite` returns.
1589
+ - More than `maxIo` in total is `RESET EINVAL`, and what came before stays written.
1590
+ - The node sends END after the page's END, once the last `pwrite` has returned. A page RESET leaves what was written.
1591
+ - **Watch:** `OPEN { "kind": "fs", "op": "watch", "mounts": [ … ] }` on its own channel. A missing or empty list is `EINVAL`, and an unknown name `ENOENT`.
1592
+ - OPENED `{}`, then slicc-node's NDJSON lines as DATA.
1593
+ - A page that doesn't read gets its backlog folded into one `{"mount","all":true}` per mount, instead of a dropped stream.
1594
+ - **Watch replacement:** a watch OPEN is cheap to replace. The page typically keeps one per link covering every mounted name, and restarts it when the list changes.
1595
+ - **Mount updates** (page cap `mounts`): when the node's export set changes, it opens `OPEN { "kind": "mounts", "mounts": [ … ] }` on the control channel, the full list in HELLO's shape, like [§5.8.1](#581-browsers-updates-page-cap-browsers-node--page). The page answers OPENED `{}` then END.
1596
+ - Handles on a removed mount get `EBADF`.
1597
+ - Its ops get `ENOENT`.
1598
+ - A watch covering it ends with `RESET ENOENT`.
1599
+ - **Handles:** numbered per link from 1, at most 4096 open per link and mount (`EMFILE`). An fh used with another mount is `EBADF`. All of them close with the link.
1600
+ - **Locking:** one node-wide lock across all links and mounts, as in slicc-node. Namespace changes and write opens are exclusive. Name opens, `read`'s re-open, `stat`, `list`, `readlink` and `statfs` are shared. `pwrite` takes no lock.
1601
+ - **Path rules and safety:** exactly slicc-node's:
1602
+ - `realpath` the parent, then lstat semantics for the last component;
1603
+ - `..`, a leading `/` and NUL refused;
1604
+ - namespace changes serialized;
1605
+ - the root can't be removed or renamed (`EBUSY`);
1606
+ - writes in place.
1607
+ - **Windows:**
1608
+ - `symlinks: false` and `chmod: false`, and the `symlink` op is `EPERM`.
1609
+ - Existing symlinks and junctions show as `kind: "symlink"`, and `readlink` works on them.
1610
+ - `\` and `:` in a path are `EINVAL`.
1611
+ - Paths go through `\\?\`, so device names and 8.3 names are plain names inside the folder.
1612
+
1613
+ #### 5.11 `agent` (page cap `agent`), node → page: remote control
1614
+ `slicc prompt`, `slicc watch` and `slicc abort` talk to seven's agent, the cone, which slicc-agent hosts. The page serves each stream through `remoteControl(connection)` from `@ai-ecoverse/slicc-agent/page`, on top of its `AgentConnection`, so the CLI never speaks pi's protocol.
1615
+
1616
+ - **Who may:** the page advertises `agent` only to links the user allowed to control it.
1617
+ - A **local** link ([§1.1](#11-local-setup-over-cdp)) gets it by default, because a CLI holding the browser's CDP can drive the page anyway.
1618
+ - A **remote** link gets it only after the user turns on *Allow remote control* for that device in Linked devices. It's off by default, and keyed on the fingerprint.
1619
+ - Seven lists every device that has linked once, even one that's offline or offered no caps, so the toggle can be found after a refused `prompt`.
1620
+ - Without the cap, the node doesn't open `agent` streams, and the page refuses them with `EACCES`.
1621
+ - **Control-only links:** `prompt`, `watch` and `abort` links advertise `caps: []`. The page gives them no `<host>.slicc.internal`, no exit entry, and opens no `kernel` streams to them.
1622
+ - **One device, several processes:** the node's HELLO ([§4](#4-hello)) and the [§1.1](#11-local-setup-over-cdp) hello both carry a random per-process `"node"` id.
1623
+ - The page replaces an older link of the same device (local: any local link; remote: the same fingerprint) only when `node` matches.
1624
+ - Over the tray, the page learns `node` at HELLO, so any replacement waits for HELLO.
1625
+ - So `slicc <join> prompt` doesn't kick that machine's running `follow`, and `slicc prompt --cdp …` doesn't kick `slicc`'s bridge.
1626
+ - **`prompt`:** `OPEN { "kind": "agent", "op": "prompt", "text": "…", "whenBusy": "steer" | "followUp" | "reject", "thinking"?: true }` on its own channel. The default `whenBusy` is `followUp`.
1627
+ - **Long prompts:** a prompt that doesn't fit in the OPEN frame sends `"length": <bytes>` instead of `text`.
1628
+ - The page answers OPENED `{}` at once. The node then sends exactly that many bytes of UTF-8 as DATA, then END. A different count is `RESET EPROTO`.
1629
+ - The page then delivers the prompt. The first NDJSON line is `{"t":"delivered","delivered":…}`, or `RESET EBUSY` for a `reject` while busy, then the events as usual.
1630
+ - The node uses this past about 60 KiB, up to 16 MiB.
1631
+ - **OPENED** (inline `text` only): `{ "delivered": "run" | "steer" | "follow-up" }`, once the agent has accepted the send. A `reject` while busy is `RESET EBUSY`. A `length` prompt gets OPENED `{}` at once instead, and its delivery comes as the first NDJSON line (above).
1632
+ - **Then DATA:** NDJSON events, **starting at this prompt's own `user` event**. Nothing from before it is sent. They run until the run that includes it ends.
1633
+ - **The last line** is `{"t":"end","reason":"stop"|"aborted"|"error","message"?}`, then END.
1634
+ - **Aborting:** a page RESET or a closed CLI doesn't abort the run. Aborting is explicit.
1635
+ - **`abort`:** `OPEN { "kind": "agent", "op": "abort" }`, answered OPENED `{ "aborted": true | false }` (false when nothing was running), then END.
1636
+ - **`watch`:** `OPEN { "kind": "agent", "op": "watch", "since"?: "<entry id>", "thinking"?: true }` on its own channel.
1637
+ - **OPENED:** `{ "head": "<entry id>", "gap"?: true }`, then NDJSON events forever.
1638
+ - **Without `since`,** the stream starts at the next event.
1639
+ - **With `since`,** the page first replays `since`'s own entry when it's `text` or `thinking`, as one `delta` with its whole text so far. Then it replays every entry after it. Clients skip what they already printed.
1640
+ - **`gap: true`** means the transcript no longer holds `since` (a new session, or a compaction). The stream then starts at the next event.
1641
+ - **Events:** one JSON object per line, mapped by slicc-agent from pi-durable's agent events (`watchEvents`) and handed to the page by `remoteControl(connection)` from `@ai-ecoverse/slicc-agent/page`.
1642
+ - `{"t":"user","id","text","delivered"}`;
1643
+ - `{"t":"text","id","delta"}`: assistant text as it streams;
1644
+ - `{"t":"thinking","id","delta"}`: only when the stream was opened with `thinking`;
1645
+ - `{"t":"tool","id","call","name","summary"}` and `{"t":"tool-result","id","call","ok","summary"}`;
1646
+ - `{"t":"run","state":"start"|"end","reason"?}`. These have no `id`, so a resume doesn't replay a missed `run end`. Clients use them only for layout and notices, and the `end` line or the next `user` event is authoritative.
1647
+ - `{"t":"notice","id","text"}`: retries, compactions, rewinds;
1648
+ - `{"t":"error","id","message","detail"?}`.
1649
+ - **Ids** are opaque strings.
1650
+ - `user`, `tool-result`, and a `notice` or `error` that is a transcript entry: the entry id, such as `"42"`.
1651
+ - An assistant message's `text`, `thinking` and `tool` blocks: `"a<ts>.<block>"`, where `<ts>` is the message's own timestamp (set when its generation starts, and kept in the committed entry) and `<block>` its content index. An assistant message isn't an entry until it's committed, so this id is stable from the first delta to the committed entry. A second message with the same timestamp gets `"a<ts>~1.<block>"`, a third `~2`, and so on.
1652
+ - Other notices and errors: an id unique to that event, such as `"retry-<at>"`, `"compaction-<task>"` or `"task-<task>"`.
1653
+ - `run` lines have no id. A watch can't replay them, because runs aren't transcript entries.
1654
+ - **Guarantees** (the CLI's tracker relies on them):
1655
+ 1. **An id is never reused for different content.** A regenerated or retried message is a new message with its own timestamp, and messages that share a timestamp get the `~n` suffix.
1656
+ 2. **Deltas only append.** `delta` lines for one id arrive in order, and their concatenation equals the text `watch` replays for that id as `since`'s own entry, byte for byte.
1657
+ 3. **Every event that isn't streamed has its own id.** A client drops a second non-streamed event with an id it has already shown, so a notice that changes gets a new id.
1658
+ 4. **A `since` that names nothing the transcript still holds** (a compacted entry, or a message an abort or reload dropped before it was committed) answers `gap: true`. It never fails the OPEN.
1659
+ - **`delivered`** on `user`: `run`; `steer` when it follows a tool result in a run; `follow-up` when it was queued behind a run.
1660
+ - **A `prompt` stream** carries only its own `user` event, not other clients' user entries, even a steer into the same run. `watch` carries them all.
1661
+ - **`run end`** comes once the agent has settled the run's inputs, so its `reason` (`stop`, `aborted` or `error`) is the agent's. A `prompt` stream's `end` line follows it.
1662
+ - **Rules:** `summary` is at most 200 characters, with secrets redacted.
1663
+ - **Never sent:** credentials, or tool output beyond the summary. A tool's summary is its name and main argument (the command, path, pattern or URL). A result's summary is its line count, or the first line of an error.
1664
+
1665
+ #### 5.12 Reserved
1666
+ `exec` (a command in seven's kernel shell, like six's `exec`) and `udp` (datagram frames, once the kernel has datagram sockets).
1667
+
1668
+ #### 5.13 `credentialed` (node cap): `http` streams that carry a value
1669
+ Values live in the vault (`vault.sliccy.ai`). A request reaches a node with a value in it only through a **transit exit**: the vault's `forward` hands bios the injected head, and bios sends it over the link.
1670
+
1671
+ - **The cap:** a node that lists `credentialed` in `caps` handles every `http` stream opened with `"credentialed": true` as below. There's no new kind.
1672
+ - **OPEN:** `{ "kind": "http", "credentialed": true }`. The bytes in both directions are [§5.3](#53-http-cap-http-page--node)'s.
1673
+ - **The page** sets the flag on every `http` OPEN whose head carries an injected value, and sends such an OPEN only to a node that advertises the cap. A node without it ignores the field ([§4](#4-hello)), so to the vault that device has no transit exit.
1674
+ - **The node:**
1675
+ - never logs the request's or the response's head or body, at any level, `SLICC_DEBUG` included;
1676
+ - names the request only by method, host and path, without the query, the fragment or userinfo. That holds for logs, the dashboard, `--json` events and RESET messages;
1677
+ - doesn't follow redirects (as for every `http` stream);
1678
+ - keeps nothing: no cache, no cookie jar, nothing on disk, and nothing of the request in memory once the stream has ended;
1679
+ - reports nothing about the request to telemetry. An error report carries its fixed source and the errno only.
1680
+ - **RESETs:** [§5.3](#53-http-cap-http-page--node)'s codes. The text after `ERRNO:` never contains a header, a body byte, the query or the full URL. A TLS failure is `ECONNREFUSED: fetch failed: x509: …` and names only the host.
1681
+ - **Refused:** a credentialed head whose URL isn't `https:` on port 443 is `RESET EACCES` before anything is dialled. The vault already refuses those, so this is defence in depth.
1682
+ - **Not the node's job:** redacting the response. The node passes it on as it came, and the vault's and bios's rules apply on the page side.
1683
+
1684
+ #### 5.14 `jerk` (caps `jerk` and `jerk-seal`), page → node: J1
1685
+ jerk ([ai-ecoverse/slicc-jerk](https://github.com/ai-ecoverse/slicc-jerk)) gets tokens on the host.
1686
+
1687
+ **The ground rule:** no token, grant, code or masked token crosses the link. The **one exception** is `seal`'s `lease` event, which only bios's link layer reads.
1688
+
1689
+ **Advertising:**
1690
+
1691
+ | node command | `jerk` (status, create) | `jerk-seal` (seal, unseal) |
1692
+ |---|---|---|
1693
+ | `slicc`, `slicc attach` (local) | yes, when `jerk` ≥ the minimum version is on the host's `PATH` or in slicc's data dir. `--no-jerk` turns both off. | yes, by default |
1694
+ | `slicc <join> follow` (remote) | only with an explicit `--jerk` | only with an explicit `--jerk-seal`, which implies `--jerk`. A remote seven pulling this machine's tokens into its browser is a bigger grant than starting a sign-in. |
1695
+ | `prompt`, `watch`, `abort` | no | no |
1696
+ | any node without host jerk | no. Host providers are missing in seven. | no |
1697
+
1698
+ HELLO adds `"jerk": { "version": "1.0.0" }` next to `caps`.
1699
+
1700
+ **OPEN:** `{ "kind": "jerk", "op": <op>, "provider"?: <id>, "scope"?: <string>, "lease_id"?: <string>, "duration"?: <seconds>, "params"?: { <k>: <string> } }`
1701
+
1702
+ | op | cap | opened by | fields | what the node runs, as the host user | limits |
1703
+ |---|---|---|---|---|---|
1704
+ | `list` | `jerk` | the kernel facade or bios | none | `jerk list --json` | |
1705
+ | `status` | `jerk` | facade or bios | `provider` | `jerk <provider> get --json` without `--reveal`; the node keeps only the status fields | |
1706
+ | `accounts` | `jerk` | facade or bios | `provider` | `jerk <provider> list --json` | |
1707
+ | `create` | `jerk` | facade or bios | `provider`, `scope?`, `params?` | `jerk <provider> create --via-link --seven-origin <origin of the linked page> [--scope S] [-p k=v]` | only when the provider's create **opens a sign-in page a human completes**: `[oauth]` modes `loopback`, `intercept` and `device`, and CLI flows marked `create.link = true`. Never `--force`. Never a create that mints a token with no UI (`npm token create`, `lease_via = "create"`). |
1708
+ | `seal` | `jerk-seal` | **bios's link layer only**; the facade refuses it | `provider`, `duration?` (default 3600) | `FNOX_LEASE_DURATION=<duration> jerk <provider> lease --via-link` | |
1709
+ | `unseal` | `jerk-seal` | **bios's link layer only** | `provider`, `lease_id` | `FNOX_LEASE_ID=<lease_id> jerk <provider> drop --lease --via-link` | revokes upstream only what jerk minted |
1710
+
1711
+ - **`--via-link`** is a host-jerk flag. It forbids a terminal, `--reveal`, `--force` and CLI logout. Its stdout is status JSON, plus the single lease object for `lease`. Host jerk enforces it again itself, so a buggy node can't widen it.
1712
+ - **There's no `drop` op.** A provider CLI logout is never possible over J1, and jerk-minted lease tokens are dropped through `unseal`.
1713
+ - **`provider`** matches `^[a-z0-9][a-z0-9-]{0,63}$`. **`params`** must be well formed (the node-side details below), otherwise `EPROTO`. Whether a key is one the provider declares is checked by host jerk, and an undeclared key arrives as `result.exit` 2.
1714
+ - **OPENED:** `{}` once the node has started host jerk.
1715
+ - **Bytes node → page:** newline-delimited JSON events, then END.
1716
+ - `{ "event": "progress", "message": "sign-in opened in Google Chrome on laptop" }`, zero or more of them, for `create` only. A device-flow user code may appear here, since it is useless without the human's approval.
1717
+ - `{ "event": "lease", "credentials": { "<ENV>": "<token>", … }, "expires_at": "<RFC3339>", "lease_id": "…" }`, exactly once, for `seal` only, before the result.
1718
+ - bios's link layer takes it out of the stream and passes it straight to the vault's `seal`, and keeps no copy. It never forwards it, logs it, or puts it in an error.
1719
+ - Without `expires_at`, bios discards it and the result becomes exit 1.
1720
+ - The node never logs it either, and keeps no copy once it's sent.
1721
+ - Exactly one `{ "event": "result", "exit": <jerk exit code>, "provider"?, "user"?, "scopes"?, "expires_at"?, "env"?: [names], "message" }`. `list` and `accounts` add `"items": [ … ]` with the same status fields per row.
1722
+ - Bytes page → node: none. The page sends END right after OPEN.
1723
+ - **Jerk outcomes are results, not RESETs.** Exits 1–5 (jerk's exit codes) arrive in `result.exit`, and in-kernel jerk exits with that code.
1724
+
1725
+ | RESET | when | in-kernel jerk exits |
1726
+ |---|---|---|
1727
+ | `EPROTO` | bad OPEN: unknown op, missing field, unknown param | 1 |
1728
+ | `EACCES` | the op isn't allowed over J1 (the table above); a remote link lacks `--jerk` or `--jerk-seal`; or `seal`/`unseal` came through the kernel facade | 2 |
1729
+ | `EBUSY` | a `create` is already in flight on this link (at most one at a time) | 1 |
1730
+ | `ETIMEDOUT` | `create` ran past the provider `timeout` plus 30 s | 3 |
1731
+ | `ECONNRESET` | host jerk died, or the link dropped | 1 |
1732
+ | (cap not advertised) | `EOPNOTSUPP` per [§4](#4-hello)'s rule | 4 |
1733
+
1734
+ **Node-side details ([slicc-cli#74](https://github.com/ai-ecoverse/slicc-cli/pull/74)):**
1735
+ - **`params`:** the node can't see a provider's declared params, so it refuses only malformed ones with `EPROTO`: keys must match `^[a-z][a-z0-9_]{0,31}$`, values must be printable, at most 16 params of at most 256 bytes each. Host jerk rejects unknown keys with exit 2, which arrives as `result.exit` 2.
1736
+ - **`--seven-origin`** is passed only on links set up over CDP ([§1.1](#11-local-setup-over-cdp)), from the seven context's origin. Tray (`follow`) and stdio links run `create` without it.
1737
+ - **Timeouts:** `create` gets 2 min 30 s (jerk's default provider timeout plus 30 s); other ops get 60 s. A `create` past its time is `RESET ETIMEDOUT`. Any other op past its time gives `result.exit` 1 with "jerk timed out", not a RESET.
1738
+ - **Lease validation:** a lease without string `credentials` or an RFC 3339 `expires_at` is never sent; the result is exit 1.
1739
+ - **"Died":** a signal, an exit code above 5, or a failed start gives `ECONNRESET`. Exits 0–5 are results.
1740
+ - **Status fields are whitelisted:**
1741
+ - results keep `provider`, `user`, `scopes`, `expires_at`, `env`, `message` (and `items`);
1742
+ - list rows keep `provider`, `name`, `source`, `requires`, `cli`, `user`, `scopes`, `expires_at`, `env`, `id`, as strings, booleans or string lists.
1743
+ - Everything else is dropped, including any masked token.
1744
+
1745
+ **The agent-vs-user rule:**
1746
+ - The caller of J1 may be the agent. So J1 only starts things a human must finish (`create`), moves tokens into a store the agent can't read (`seal`), or undoes what jerk minted (`unseal`).
1747
+ - For `create`, the **sign-in page is the confirmation**. A create that would succeed without a human on a page is refused (`EACCES`).
1748
+ - `seal` can succeed with no human present, for example when gh is already signed in on the host. That's safe because a seal carries no rules: the token is injected only under rules in seven's root-owned fnox config, and adding one takes `sudo`, whose dialog is the human's approval.
1749
+ - Ending a provider CLI's own login (`gh auth logout`, `wrangler logout`, `aio logout`) is never possible over J1. The user runs `jerk <provider> drop` on the host.
1750
+ - A provider whose sign-in needs a terminal reports `exit 5` over J1, with "run `jerk github create` on laptop".
1751
+
1752
+ #### 5.15 `ws` (cap `ws`), page → node: WebSockets for kernel programs
1753
+ From [slicc-bios#360](https://github.com/ai-ecoverse/slicc-bios/pull/360) and [slicc-kernel#215](https://github.com/ai-ecoverse/slicc-kernel/pull/215). bios never opens a browser WebSocket for a program, because the browser would attach the user's cookies. So a program's `ws:` or `wss:` connection always goes out through an exit.
1754
+
1755
+ - **OPEN:** `{ "kind": "ws", "url": "wss://host/path", "protocols": ["…"], "headers": [["name","value"], …] }`.
1756
+ - **`url`:** `ws:` or `wss:`. A URL on the link's own `<host>.slicc.internal` arrives already rewritten to `127.0.0.1`, as for `http`.
1757
+ - **`headers`:** the program's handshake headers, Latin-1, with [§5.3](#53-http-cap-http-page--node)'s header rules.
1758
+ - The node drops `Host`, `Connection`, `Upgrade`, `Sec-WebSocket-Key`, `-Version`, `-Extensions` and `-Protocol`, and sends the rest, `Origin` and `Authorization` included.
1759
+ - It offers no extensions (no permessage-deflate).
1760
+ - **Refused with `RESET EPROTO`:** URLs with credentials, other schemes, a bad OPEN.
1761
+ - **Dial:** through the same policy-checked dialer and system TLS roots as `http`. **Protected ports ([§6](#6-policy-node)) apply**, so a `ws` stream can never reach a browser's debugging port.
1762
+ - **Redirects:** not followed.
1763
+ - **Success:** OPENED `{ "protocol": "<chosen or empty>" }`.
1764
+ - **Failure:** RESET with `EACCES` (policy), `ECONNREFUSED: HTTP <status>` (a non-101 answer, 3xx included), `ECONNREFUSED: …` (dial, DNS, timeout (30 s) or TLS; for TLS, `ECONNREFUSED: x509: …` naming only the host). bios maps every RESET to 502, so no finer codes are needed.
1765
+ - **Bytes, both directions:** records of `u8 type`, then a `u32` BE length, then the payload. Records are independent of DATA frame boundaries.
1766
+ - **Types:** `1` text (UTF-8), `2` binary, `8` close (`u16` code, then a UTF-8 reason of at most 123 bytes; an empty payload means 1005).
1767
+ - **Whole messages only:** the node reassembles fragments, and the page sends whole messages.
1768
+ - **Size:** at most 64 MiB per message; over that, the receiver closes with 1009.
1769
+ - **Other types:** an unknown type, or a malformed close, makes the receiver close with 1002.
1770
+ - **Page-side violations** (1009, 1002): the node tells the page with a close record carrying that code, then END.
1771
+ - **Pings:** the node answers the far end's pings itself, and they're never relayed.
1772
+ - **Closing:**
1773
+ - **Page first:** the page sends a close record, then END. The node sends the WebSocket close, relays the far end's close record, then END.
1774
+ - **Far end first:** the node echoes the far end's close code at once (RFC 6455), relays its close record, then END. A close record from the page after that is optional and ignored.
1775
+ - **No clean close:** END alone, which the page reports as 1006.
1776
+ - **Flow control:** the stream window ([§3.3](#33-flow-control)), with nothing extra.
1777
+ - **Privacy:** the node never logs handshake headers or message payloads at any level, and names a stream only by host and path (no query). A program may hold a real value through `fnox exec`.
1778
+ - **No vault injection, v1:** `ws` streams are never `credentialed`. bios refuses (`EACCES`, "secrets in WebSocket handshakes aren't supported yet") an OPEN whose handshake contains a vault placeholder, rather than sending the placeholder.
1779
+ - **HELLO:** nodes that implement it add `"ws"` to `caps`. Without it, bios answers the program 502 "…carries no WebSockets; update its slicc CLI".
1780
+
1781
+ #### 5.16 The link facade (page side, not on the wire): the in-kernel `slicc`
1782
+
1783
+ How a kernel program in seven reaches [§1.4](#14-a-seven-as-follower-leader-to-leader). This isn't a link stream. It's seven's page serving its own kernel. The page holds the WebRTC; the program never sees the hub, the join state or the peer connection.
1784
+
1785
+ - **The program:** `@ai-ecoverse/wasi-slicc`, slicc-cli's client command layer built for `GOOS=wasip1` (published by slicc-cli, offered by bios's catalog).
1786
+ - **Commands:** `slicc <join-url> prompt|watch|abort`, with the CLI's own flags, output and `--json` events.
1787
+ - **Anything else** exits 2 with `slicc: <command> isn't available inside seven`. A prompt without a join URL is refused the same way.
1788
+ - **How it reaches the facade:** through its host module, at `SLICC_LINK_URL` (below).
1789
+ - **Where it's served:** a page-served kernel facade with the caller's stamp on each connection, mirroring the CDP facade on `127.0.0.1:9222`. The mechanism is [slicc-kernel#526](https://github.com/ai-ecoverse/slicc-kernel/issues/526), shipped in [slicc-kernel 1.90.0](https://github.com/ai-ecoverse/slicc-kernel/tree/v1.90.0#page-facades) ([#556](https://github.com/ai-ecoverse/slicc-kernel/pull/556)):
1790
+ - **The kernel side:** `createKernel({ facades: { link: { port, hook } } })`. An attached client can supply the hook later with `client.serveFacade("link", hook)`.
1791
+ - **The stamp:** `hook(stamp, { name })` gets the connector's `{ pid, ppid, uid, gid, user?, groups, users, tag? }`, taken by the kernel at `connect()` from the process's credentials. A program can't forge it.
1792
+ - **The port:** reserved from boot, so a program's `bind()` to it fails with `EADDRINUSE`, and kernel-only, so the page's `kernel.dial`, a client's `dial` and `loopbackFetch` are refused with `ECONNREFUSED`.
1793
+ - **The limit:** bytes from the page wait in the kernel until the program reads them, up to 16 MiB per connection. Past that, the kernel closes the connection, and the hook's `close(reason)` gets `slicc-kernel: more than 16777216 bytes are waiting for the program to read`.
1794
+ - **The connection:** a raw byte stream on the kernel's loopback, reached at `SLICC_LINK_URL` (`tcp://127.0.0.1:<port>`). The kernel hands the connection and the connector's stamp to the page's hook, as `SO_PEERCRED` would. The program's host module connects with the imports-module API's `ctx.net.connect` (slicc-kernel 1.92.0 or later).
1795
+ - **Framing, one connection per [§5.11](#511-agent-page-cap-agent-node--page-remote-control) stream:** records of `u8 type`, a `u32` BE length, then the payload (as [§5.15](#515-ws-cap-ws-page--node-websockets-for-kernel-programs)), in both directions.
1796
+ - **`1` control (JSON):**
1797
+ - the program's first record is `{ "v": 1, "join": "<join-url>", "open": { "kind": "agent", "op": "prompt" | "watch" | "abort", … } }`. `open` is exactly [§5.11](#511-agent-page-cap-agent-node--page-remote-control)'s OPEN, `length` for long prompts included;
1798
+ - the page answers with `{ "opened": { … } }` ([§5.11](#511-agent-page-cap-agent-node--page-remote-control)'s OPENED) or `{ "reset": "ERRNO: message" }`. After a `reset`, it closes the connection.
1799
+ - **`2` data:** the stream's DATA, prompt bytes up and NDJSON down.
1800
+ - **`3` end:** that side's END. The page closes the connection after both ends, or after a `reset`.
1801
+ - **After the first exchange, the only control record allowed is a `reset`,** at any point: it's that stream's RESET. Any other later control record gets `reset` `EPROTO`.
1802
+ - **Limits:** a control record is at most 64 KiB. A data record is at most 16 MiB, and senders keep them at 64 KiB or less. Any other type, or an oversized or malformed record, gets a `reset` with `EPROTO`.
1803
+ - **Ctrl-C inside seven** ends the program with status 130 (the kernel's default), `watch` included. Go on `wasip1` can't catch signals, so `--abort-on-interrupt` isn't offered there.
1804
+ - **Who may:**
1805
+ - **only under kernel users `enforce`;**
1806
+ - **only uid 0 and members of `sudo`** (cones), from the stamp. Scoops are refused;
1807
+ - **otherwise** `{ "reset": "EACCES: …" }`.
1808
+ - **Secrets:** a prompt ends at B's model provider.
1809
+ - **On the way out:** A's page scrubs every outgoing prompt byte with A's vault scrubber, streaming.
1810
+ - **While the vault is locked** (`scrub.ready: false`), the answer is `{ "reset": "EACCES: unlock the vault to continue" }`.
1811
+ - **On the way back:** events from B aren't scrubbed on A.
1812
+ - **Errors:** [§5.11](#511-agent-page-cap-agent-node--page-remote-control)'s, plus:
1813
+ - `ENOTCONN`: the tray can't be reached;
1814
+ - `EACCES`: B refused (`UNLINKED`, or no *Allow remote control*), with B's message.
1815
+ - **Not offered:** exits, kernel streams, CDP or anything else on B. Six's `exec` and `list` are parked.
1816
+
1817
+ ### 6. Policy (node)
1818
+
1819
+ #### 6.1 Where it's checked
1820
+ - **On the address being connected:** in the dialer's `Control` hook, for every address Happy Eyeballs tries. So a name can't rebind past it, and one check covers `tcp`, `http` and `resolve` filtering.
1821
+ - **Normalized first:**
1822
+ - IPv4-mapped IPv6 (`::ffff:a.b.c.d`) is checked as IPv4.
1823
+ - NAT64 `64:ff9b::/96` and 6to4 (`2002::/16`) are checked by their embedded IPv4. The local-use NAT64 range `64:ff9b:1::/48` is denied outright, since RFC 8215 lets operators place the IPv4 address differently.
1824
+ - Teredo (`2001::/32`) is denied.
1825
+ - `0.0.0.0/8` and `::/128` are always denied, because `connect()` to them reaches loopback.
1826
+
1827
+ #### 6.2 Rules
1828
+ - **Form:** `--allow` and `--deny` take `CIDR` or `CIDR:port` (`127.0.0.1/32:9222`, `[::1]/128:9222`).
1829
+ - **Most specific wins:** the longest prefix wins, and at the same prefix a rule with a port wins over one without.
1830
+ - **Ties:** at a full tie, a **user flag beats a default rule**, so `--allow 192.168.0.0/16` opens exactly the range it names. Among the user's own flags, deny wins.
1831
+ - **Refusal:** `EACCES`.
1832
+
1833
+ #### 6.3 Remote default (`slicc exit <join>`)
1834
+
1835
+ The node allows public unicast and denies:
1836
+ - loopback: `127.0.0.0/8`, `::1/128`;
1837
+ - private ranges: `10/8`, `172.16/12`, `192.168/16`;
1838
+ - CGNAT and tailnets: `100.64/10`;
1839
+ - link-local, including cloud metadata: `169.254/16`, `fe80::/10`;
1840
+ - ULA: `fc00::/7`, which covers `fd7a:115c:a1e0::/48`;
1841
+ - multicast: `224/4`, `ff00::/8`;
1842
+ - reserved and documentation ranges: `240/4`, `255.255.255.255/32`, `192.0.0/24`, `192.0.2/24`, `198.18/15`, `198.51.100/24`, `203.0.113/24`, `2001:db8::/32`, `100::/64`;
1843
+ - deprecated IPv4-compatible `::/96` and SIIT `::ffff:0:0:0/96`.
1844
+
1845
+ #### 6.4 Local default (`slicc`, the bridge to the browser it controls)
1846
+
1847
+ The remote default, but it **also allows loopback and the private ranges** (`10/8`, `172.16/12`, `192.168/16`, `fc00::/7`).
1848
+ - **Why:** this is parity with slicc-node, whose `fetch-proxy` reaches the LAN today (a NAS, a dev server on another box).
1849
+ - **Still denied:** link-local and metadata.
1850
+ - **Except on loopback:** the node denies the browser's debugging port and its own listening ports (the kernel listener of [§5.9](#59-kernel-listener-on-the-node-local-only) included). Otherwise a kernel program could reach raw CDP on the host and get around the `cdp` capability.
1851
+ - It denies them on **every address of this machine** too (interfaces enumerated at check time, cached briefly), in case something listens on `0.0.0.0`.
1852
+ - Browsers the node launches bind their debugging port to loopback only, and so do the node's own listeners.
1853
+ - **Opting out:** `--deny 127.0.0.0/8` and `--deny 192.168.0.0/16` restore the remote default.
1854
+
1855
+ ### 7. Reference peer for seven's CI
1856
+
1857
+ The node is the reference. Two setup modes exist for tests:
1858
+ - **`slicc attach --cdp <url> --origin <test origin> [--allow …]`** is the production local path.
1859
+ - **`--signal stdio`** writes the setup messages from [§1.1](#11-local-setup-over-cdp) as NDJSON on stdout and reads them from stdin. A test harness that already owns CDP relays them itself. slicc-shared-web's harness wants to be the only CDP client.
1860
+
1861
+ For tests, a node in either mode can add `--allow 127.0.0.1/32`, so the harness's own upstream servers count as public.
1862
+
1863
+ CI installs a pinned release: `go install github.com/ai-ecoverse/slicc-cli/cmd/slicc@v<version>`, or the release binary (see [Install](#install)).
1864
+
1865
+ §8 is retired.
1866
+
1215
1867
  ## Development
1216
1868
 
1217
1869
  Go 1.26 and Node ≥ 24 (for the lint tools). `npm run build` writes `bin/slicc`. `npm run lint` runs the slicc lint tools, `gofmt`, `go mod tidy -diff`, `go vet` and golangci-lint. `npm test` runs the integration tests in `test/integration/`, which build the binary and drive it; run `npm ci` first, since the [Inside seven](#inside-seven) tests need the pinned `@ai-ecoverse/slicc-kernel`. Unit tests stay next to the code and out of git (`npm run test:unit`). `go build` and `npm run build` link no version, so these binaries report `dev`. `npm run dist` cross-builds all six targets into `artifacts/release/` with `SHA256SUMS`, unsigned. `npm run npm:stage` then stages the npm packages into `dist/npm/`, and `npm run npm:check` packs them with `npm publish --dry-run`.
1218
1870
 
1871
+ **Test shards.** CI splits the integration tests into shards with `scripts/test-shard.mjs <index>/<count> <go test args>`. The split is greedy by measured time: `test/integration/shard-times.json` holds each top-level test's seconds per platform (`darwin`, `linux`, `win32`), each test goes to the shard with the least time so far, and a test without a time counts as the platform's median. The last shard runs everything the others don't, so a test that isn't in any list yet still runs. CI runs two shards on Linux and Windows and three on macOS, whose runners vary the most (the same shard took from 400 s to 570 s), so each stays well inside its 12 minutes. CI runs them with `-v`, so `node scripts/shard-times.mjs --run <CI run id>` refreshes the file from a finished run's logs (`<platform>=<log file>` reads local `go test -v` output instead); commit the result when tests get much slower or faster.
1872
+
1219
1873
  **Test names: `--hosts <file>`** (test-only, left out of `--help`). `slicc attach`, `slicc open`, `slicc pair` and `slicc <join-url> follow` take a hosts file, so tests get deterministic names without public DNS. Each line is `<ip> <name> [<name>…]`, and `#` starts a comment. Names match case-insensitively, with or without a trailing dot, and a name on several lines gets every address, in file order. Wherever the node resolves a name (`resolve`, `tcp` by name, `http`, and the policy's [browser endpoints](#network-policy)), a name in the file gets the file's addresses of the requested family, possibly none, and never the system resolver's. Other names go to the system resolver. The policy applies afterwards as usual: `resolve` drops refused addresses, and a dial to a refused address is `EACCES`. `tcp` and `http` try the addresses in file order. A file that can't be read or parsed exits 2. A name mapped to a loopback address only helps the node's own dials, `http` and `tcp` by name: a seven kernel that gets a loopback address from `resolve` dials its own loopback, because loopback never goes out through an exit. For end-to-end exit tests, map test names to a non-loopback address of the machine.
1220
1874
 
1221
1875
  Front ends and capabilities follow a node's links with `node.Node.Subscribe(func(node.Event)) (unsubscribe func())`. Events: `connecting` (an offer was answered), `linked` (the peer's HELLO arrived, in `Peer`), `link-down` (`Reason` when the node closed the link: seven's `bye` reason, `replaced by a new offer` or `slicc is exiting`; otherwise `Err` for a failure; a failed answer has no `Link`), and `stream-open` and `stream-close` for each page-opened stream, with its kind. Every event but a failed answer carries its `*link.Link`. A link's `link-down` comes before the next link's `connecting`, but its streams' `stream-close` can come after its `link-down`. Events are delivered synchronously on the node's and the link's goroutines, so a subscriber must return quickly and never block; one that needs to wait hands the event to its own goroutine.
@@ -1225,7 +1879,7 @@ Front ends and capabilities follow a node's links with `node.Node.Subscribe(func
1225
1879
  - `installed`: installed browsers too;
1226
1880
  - `home`: installed browsers, and the `DevToolsActivePort` files under the home and config dirs from the environment (`HOME`, `APPDATA`, `LOCALAPPDATA`, `XDG_CONFIG_HOME`), SLICC six's profiles included (`XDG_STATE_HOME`, `SLICC_USER_DATA_DIR`).
1227
1881
 
1228
- In the same process, probe ports, dirs and a process list passed in explicitly (`Options.ProbePorts`, `Options.Home`, `Options.ProfilesDir`, and `Options.Processes`, which returns the output of the process-list command) are still used. `Options.Transport` and `Options.Dial` replace discovery's HTTP transport and TCP dial; the corpus test fakes every server with them, and its process list is fake too. `test/integration`'s `TestMain` sets `SLICC_DISCOVERY=isolated` and unsets `SLICC_USER_DATA_DIR`, and points `HOME`, `APPDATA` and the XDG dirs, `XDG_STATE_HOME` included (and `USERPROFILE` and `LOCALAPPDATA` except on Windows, where a Chrome started with temporary ones never writes `DevToolsActivePort`) at a temporary directory for itself and every `slicc` it starts, so no test can reach the browsers of the machine it runs on. Tests opt in to more with a fake home or fake endpoints. The Chrome the harness starts itself gets the real environment.
1882
+ In the same process, probe ports, dirs and a process list passed in explicitly (`Options.ProbePorts`, `Options.Home`, `Options.ProfilesDir`, and `Options.Processes`, which returns the output of the process-list command) are still used. `Options.Transport` and `Options.Dial` replace discovery's HTTP transport and TCP dial; the corpus test fakes every server with them, and its process list is fake too. `test/integration`'s `TestMain` sets `SLICC_DISCOVERY=isolated` and unsets `SLICC_USER_DATA_DIR`, and points `HOME`, `APPDATA` and the XDG dirs, `XDG_STATE_HOME` included (and `LOCALAPPDATA`, and `USERPROFILE` except on Windows, where a Chrome started with a temporary one never writes `DevToolsActivePort`; the tests that have `slicc` launch a real Chrome give it the real `LOCALAPPDATA` on Windows, which they only do on CI) at a temporary directory for itself and every `slicc` it starts, so no test can reach the browsers of the machine it runs on. It also puts a stand-in `jerk` first on `PATH` that fails `--version` (on Windows one for every `PATHEXT` extension), so slicc never finds the machine's own slicc-jerk there, while every other tool keeps its own path. The tests that want a jerk put the fake jerk's dir before it. Tests opt in to more with a fake home or fake endpoints. The Chrome the harness starts itself gets the real environment.
1229
1883
 
1230
1884
  **SLICC six's profile in tests.** The `--profile six` tests build fake six profile trees in a fake home only: each OS's layout, a mixed-case name, a symlinked profile and root, `SingletonLock` (or `lockfile`) live, stale and absent, and debuggable endpoints that are fakes or the harness's own headless Chrome on a free port. They check the trees byte for byte after a launch, an attach and an exit. A fake browser (`testdata/fakechrome`) stands in for Chrome where a real one would write to the profile itself. The one real-Chrome launch into a fake six profile checks that nothing was seeded; on Windows it puts that profile at `SLICC_USER_DATA_DIR` in a temporary dir, since that Chrome keeps the real `LOCALAPPDATA` (see below).
1231
1885
 
@@ -1234,7 +1888,7 @@ In the same process, probe ports, dirs and a process list passed in explicitly (
1234
1888
  **Protected-origin vectors.** `test/integration/testdata/link/protect.js` exports `protectVectors`, the attacks the relay must stop, in a shape other guards reuse: `{ id, setup, trigger, control: { origin: 'ordinary', expected }, expected }`. Each runs three times, through the relay, against the same server under three names: an ordinary origin (`ordinary.test`, the control, which must succeed), seven (an `--origin`) and the vault (`vault.sliccy.ai`). A run passes when its observations match `control.expected` or `expected`, and a vector whose control fails fails as a whole. The only run left out is the vault in the same-site iframe cases, since no unprotected page is same-site with a `sliccy.ai` host. The observations come from the test server's log of requests and of the beacons an injected init script sends, and from what the page's own CDP session saw. The cases:
1235
1889
  - C1, an init script, then `location.href`;
1236
1890
  - C2, redirect chains through allowed hops (301, 302, 303, 307, 308, and a POST form through 307 and 308), and a chain that passes through the protected origin and back out;
1237
- - C3, an instrumented popup that navigates itself, and a popup opened on a redirect chain;
1891
+ - C3, an instrumented popup that navigates itself, a popup opened on a redirect chain, and popups opened on a redirect chain or straight on the origin that never reach the page: not through auto-attach with or without `waitForDebuggerOnStart`, `Target.attachToTarget`, `activateTarget` or `closeTarget`, and not after they navigate to another path of the origin and the opener turns auto-attach on again; `Target.getTargetInfo` shows them without their path;
1238
1892
  - C4, static and script-inserted iframes, same-site and cross-site to the page;
1239
1893
  - C5, the page's own `Fetch.enable` on every request at both stages: a document, `fetch()`, XHR, an image, an auth challenge, and fulfilling guessed request ids;
1240
1894
  - C6, cookie reads, writes and clears, and storage commands that name the origin or its storage key.
@@ -1249,7 +1903,9 @@ The real-Chrome tests in `test/integration/` launch headless, so they run the ex
1249
1903
 
1250
1904
  ## Releases
1251
1905
 
1252
- semantic-release cuts releases from `main` in `.github/workflows/release.yml`, on a macOS runner:
1906
+ PRs merge through the merge queue. CI also runs on `merge_group`, and the queue requires one check, the `ci` job, which fails unless every other CI job passed. Merge-queue runs never start or satisfy a release: the release starts only from CI's `push` run on `main`.
1907
+
1908
+ semantic-release cuts releases from `main` in `.github/workflows/release.yml`, on a macOS runner. The release doesn't run the tests again: the workflow starts when the CI workflow's run for a push to `main` completes (`workflow_run`), and the release job runs only when that run passed, sharded tests on all three OSes included, on the commit that run tested. A CI run that failed or was cancelled releases nothing; when a re-run of its failed jobs passes, the release follows by itself. When `main` has moved on by then, the checkout is behind `main`, so semantic-release (in `release-plan.mjs` and again in the release) skips without failing, and the newer commit's run releases. A pull request's dry run and a manual run instead wait in the `ci` job for the CI workflow's run on the same commit (`pull_request`, or `push` on `main`), and fail unless it passed. Only a run on `main` publishes: anywhere else (a pull request, a manual run from another branch) the release job is a dry run (`DRY_RUN` is true whenever `github.ref` isn't `refs/heads/main`), and the attestation, semantic-release (the GitHub release and the npm packages), `go-warm.sh`, `install-smoke` and the `sliccy-next` recovery each also require `refs/heads/main` themselves. The release job then runs these steps:
1253
1909
  1. `scripts/release-plan.mjs` asks semantic-release (dry run) for the next version, and stops when there is none.
1254
1910
  2. `scripts/package.sh <version>` builds `darwin`, `linux` and `windows` × `amd64`, `arm64` with `CGO_ENABLED=0`, `-trimpath` and `-X main.version=<version>`, and `slicc-wasip1-wasm.wasm` (`GOOS=wasip1 GOARCH=wasm`) the same way. It signs and notarizes the darwin binaries and writes `SHA256SUMS`. The windows and linux binaries are not signed. Without the Apple signing secrets (forks, pull requests) it warns and leaves the darwin binaries unsigned.
1255
1911
  3. `scripts/npm-stage.mjs` stages `@ai-ecoverse/slicc` and the six platform packages into `dist/npm/`. It also stages the same launcher as `sliccy` (with two bins, `sliccy` and `slicc`, and a short preview README) into `dist/npm-next/`, as version `7.0.0-next.<version>`, with exact `optionalDependencies` on this release's platform packages. `npx sliccy@next` runs the bin named after the package, `sliccy`, and both bins run the same launcher. `--bin <name>` (repeatable) overrides the bins of any staged launcher. `--wasi` also stages `@ai-ecoverse/wasi-slicc` into `dist/npm/wasi-slicc/` from `slicc-wasip1-wasm.wasm` and `npm/wasi-slicc/host/slicc-host.mjs`, with README.md's [Inside seven](#inside-seven) section as its README; `--wasi-only` stages only that package.
@@ -1263,12 +1919,12 @@ semantic-release cuts releases from `main` in `.github/workflows/release.yml`, o
1263
1919
 
1264
1920
  **Recovering `sliccy@next`.** When a release tagged and published its platform packages but `sliccy` didn't go out (a registry that lagged past the wait, say), `gh workflow run release.yml --ref main -f sliccy_next_only=true` publishes it without a new release. On `main` only, that run skips the release job and runs `scripts/sliccy-next.mjs`. The script takes the latest `vX.Y.Z` tag reachable from `HEAD`, stages `sliccy@7.0.0-next.<version>` with the launcher (`npm/slicc.js`) from that tag and exact `optionalDependencies` on that version's platform packages, and publishes it through `npm-publish.mjs` with `--tag next`. The same guards apply: the `SLICCY_NEXT_PUBLISH` gate, the tag and version checks, the registry check, and a version that is already published is skipped. Only the latest release can be recovered, so `next` never moves backwards.
1265
1921
 
1266
- On pull requests the same workflow runs as a dry run: an unsigned build of all six targets, the npm packages and `sliccy` (with `--tag next`) packed with `--dry-run`, `scripts/sliccy-next.mjs --dry-run` (the recovery path against the latest real release, registry check included), and each binary run with `--version` on its own OS and CPU.
1922
+ Every pull request's CI checks the packaging in its `installer-assets` job: an unsigned build of all six targets, the npm packages and `sliccy` (with `--tag next`) packed with `--dry-run`, and `scripts/sliccy-next.mjs --dry-run` (the recovery path against the latest real release, registry check included). A pull request that changes `release.yml`, `.releaserc.json`, or a script only the release runs (`scripts/package.sh`, `scripts/npm-*.mjs`, `scripts/release-plan.mjs`, `scripts/sliccy-next.mjs`, `scripts/go-*`) also runs the release workflow as a dry run: the same checks on the release's macOS runner, and each binary run with `--version` on its own OS and CPU.
1267
1923
 
1268
1924
  The workflow reads these repository secrets. Each group is optional: without the first the darwin binaries stay unsigned, without the second they are signed but not notarized:
1269
1925
  - macOS signing: `APPLE_CERTIFICATE_BASE64`, `APPLE_DEVELOPER_CERTIFICATE_PASSWORD`;
1270
1926
  - notarization: `APPLE_API_KEY_ID`, `APPLE_API_KEY_ISSUER_ID`, `APPLE_API_KEY_P8_BASE64`.
1271
1927
 
1272
- npm needs no token. `npm run trust` (fledgling) configures trusted publishing for the seven packages listed under `fledgling.include` in `package.json`. Publishing to npm is off until the repository variable `SLICC_NPM_PUBLISH` is `true`, `sliccy` stays off until `SLICCY_NEXT_PUBLISH` is `true`, and `@ai-ecoverse/wasi-slicc` until `WASI_SLICC_PUBLISH` is `true` (the facade's kernel side, slicc-kernel#526, comes first); until then a release skips them with a notice. `@ai-ecoverse/wasi-slicc` isn't under `fledgling.include` yet, so its trusted publishing needs setting up before that variable is turned on.
1928
+ npm needs no token. `npm run trust` (fledgling) configures trusted publishing for the eight packages listed under `fledgling.include` in `package.json`, `@ai-ecoverse/wasi-slicc` included. Publishing to npm is off until the repository variable `SLICC_NPM_PUBLISH` is `true`, `sliccy` stays off until `SLICCY_NEXT_PUBLISH` is `true`, and `@ai-ecoverse/wasi-slicc` until `WASI_SLICC_PUBLISH` is `true`; until then a release skips them with a notice. Its trusted publishing has to be set up with `npm run trust` before that variable is turned on.
1273
1929
 
1274
1930
  `sliccy` only ever goes to the `next` dist-tag, so `npx sliccy` keeps running SLICC six and `npx sliccy@next` runs this binary. `npm-publish.mjs` refuses, even in a dry run, to publish `sliccy` with any tag but `next` or with a version that doesn't match `^7\.0\.0-next\.`. Nothing here changes `sliccy`'s other dist-tags.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "SLICC's command line for seven: links a machine's network, compute and browsers to seven over WebRTC",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -18,12 +18,12 @@
18
18
  "node": ">=18"
19
19
  },
20
20
  "optionalDependencies": {
21
- "@ai-ecoverse/slicc-darwin-arm64": "1.40.0",
22
- "@ai-ecoverse/slicc-darwin-amd64": "1.40.0",
23
- "@ai-ecoverse/slicc-linux-arm64": "1.40.0",
24
- "@ai-ecoverse/slicc-linux-amd64": "1.40.0",
25
- "@ai-ecoverse/slicc-windows-arm64": "1.40.0",
26
- "@ai-ecoverse/slicc-windows-amd64": "1.40.0",
27
- "@ai-ecoverse/slicc-jerk": "^0.17.0"
21
+ "@ai-ecoverse/slicc-darwin-arm64": "1.41.0",
22
+ "@ai-ecoverse/slicc-darwin-amd64": "1.41.0",
23
+ "@ai-ecoverse/slicc-linux-arm64": "1.41.0",
24
+ "@ai-ecoverse/slicc-linux-amd64": "1.41.0",
25
+ "@ai-ecoverse/slicc-windows-arm64": "1.41.0",
26
+ "@ai-ecoverse/slicc-windows-amd64": "1.41.0",
27
+ "@ai-ecoverse/slicc-jerk": "^0.19.0"
28
28
  }
29
29
  }