README
¶
slicc-cli
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).
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, 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.
Commands (planned)
| command | what it does |
|---|---|
slicc |
pick browsers in a small TUI; Enter launches or attaches to the best match, opens seven and links it. With no SLICC profile and no CDP endpoint, it links seven in the default browser instead (works now; see The default) |
slicc attach --cdp <url> |
link a seven that is open in an already-running CDP browser (works now; see Local setup) |
slicc open, slicc --default-browser, slicc pair |
link a seven in your everyday browser, without CDP: open or print a pairing URL (works now; see Linking seven in your everyday browser) |
slicc unpair [<fingerprint> | --all] |
list or forget the sevens paired with open or pair |
slicc launch |
launch a browser with CDP in a separate SLICC profile and print its CDP URL |
slicc <join-url> follow [--exit] [--runner …] [--cdp <url>] [--mount <path>] |
follow a seven's tray from another machine and offer its network, compute (over SSH), browsers or folders (--exit, --runner, --cdp and --mount work now; see Remote setup) |
slicc <join-url> prompt <text>, watch, abort |
send a prompt to seven's agent and stream its answer, follow the agent as it works, or abort its run; --cdp <url> instead of a join URL links locally (works now; see Remote control) |
slicc browsers --json |
list running CDP endpoints and installed browsers, ranked |
slicc update [--check] |
replace this binary with the latest release (works now; see Updates) |
slicc install jerk |
install slicc-jerk into slicc's data folder (works now; see Companion tools) |
Install
curl -fsSL https://get.sliccy.ai/slicc | sh
This installs slicc on macOS and Linux; see Install with curl. Options pass through to the script: curl -fsSL https://get.sliccy.ai/slicc | sh -s -- --help. If get.sliccy.ai can't be reached, use the GitHub release URL directly:
curl -fsSL https://github.com/ai-ecoverse/slicc-cli/releases/latest/download/install.sh | sh
The alternatives:
- npm:
npm install -g @ai-ecoverse/slicc, ornpx @ai-ecoverse/slicc. The package is a small Node launcher; npm picks one of the platform packages@ai-ecoverse/slicc-<os>-<arch>(darwin,linux,windows×amd64,arm64) throughoptionalDependenciesand theirosandcpufields. The launcher runs that binary with the same arguments, stdio and environment plusSLICC_LAUNCHER=npm, forwardsSIGTERM,SIGHUP,SIGQUITandSIGUSR2to it, andSIGINTtoo when stdin is not a terminal (a terminal's Ctrl-C already reaches the binary; on Windows the console delivers Ctrl-C and Ctrl-Break to both). It exits with the binary's exit code, or dies of the same signal. An install with--omit=optionalhas no binary, and the launcher says so. slicc-jerk comes the same way; fnox doesn't (see Companion tools). - GitHub releases: every release of
ai-ecoverse/slicc-clicarriesslicc-<os>-<arch>(.exeon Windows) for all six targets, plusinstall.shandSHA256SUMS(which listsinstall.shtoo). See Verifying a download. - Go:
go install github.com/ai-ecoverse/slicc-cli/cmd/slicc@latest. See Install with Go.
Install with curl
install.sh is a POSIX sh script, attached to every release and kept in the repo root. https://get.sliccy.ai/slicc redirects (302) to the latest release's copy, https://github.com/ai-ecoverse/slicc-cli/releases/latest/download/install.sh, so both one-liners run the same script:
curl -fsSL https://get.sliccy.ai/slicc | sh
curl -fsSL https://github.com/ai-ecoverse/slicc-cli/releases/latest/download/install.sh | sh
Options go after sh -s --, as in curl -fsSL https://get.sliccy.ai/slicc | sh -s -- --help. The script:
- takes
VERSION(default: the latest release;1.2.3orv1.2.3) andINSTALL_DIR(default:$HOME/.local/bin) from the environment, or--version Xand--dir Das flags (curl -fsSL https://get.sliccy.ai/slicc | sh -s -- --version 1.2.3 --dir ~/bin), and--help; - supports
DarwinandLinuxonx86_64/amd64andarm64/aarch64(an x86_64 shell under Rosetta getsarm64); anything else exits 1 and points to npm, or on Windows to the release's.exe; - downloads
slicc-<os>-<arch>andSHA256SUMSfrom the release withcurlorwget, checks the hash withsha256sumorshasum -a 256, and exits 1 without installing anything when the hash differs orSHA256SUMShas no line for the asset; - copies the binary to a temporary file in
INSTALL_DIRand sets mode755; it exits 1 and leaves any installedsliccalone when that file's--versionfails, doesn't printslicc <version>, or, with a version asked for, names another version; - renames it to
slicc, so a re-run upgrades or downgrades in place; - prints
installed slicc <version> at <path>from that--version, and, whenINSTALL_DIRisn't onPATH, one line with theexport PATH=…to add.
It never uses sudo, never edits shell startup files and sends no telemetry. The Linux binaries are static (CGO_ENABLED=0), so the libc doesn't matter. The macOS binaries are the signed and notarized ones; curl sets no quarantine flag. SLICC_INSTALL_BASE_URL replaces https://github.com/ai-ecoverse/slicc-cli/releases (assets from <base>/latest/download/ or <base>/download/v<version>/), for tests only. A curl-installed slicc updates itself in place with slicc update (see Updates), or by running the one-liner again.
Install with Go
go install github.com/ai-ecoverse/slicc-cli/cmd/slicc@latest puts slicc in $GOBIN (default $(go env GOPATH)/bin). It needs Go 1.26.5 or newer, the go line of go.mod. An older Go from 1.21 on fetches that toolchain itself unless GOTOOLCHAIN=local is set. @v<version> picks a release instead of the latest.
The binary is built from source on your machine. It has no Developer ID signature, no notarization and no build attestation. Its integrity comes from the Go checksum database (sum.golang.org), which pins the hash of every release's source. On macOS a binary you build yourself isn't quarantined, so Gatekeeper doesn't stop it. Signed binaries come from the GitHub releases, from npm (npx sliccy@next, npx @ai-ecoverse/slicc), and from slicc update.
A go-installed slicc reports its module version (slicc 1.25.2 for @v1.25.2). It counts as a release build for telemetry and the update notice, but slicc update doesn't replace it; see Updates.
Coming from SLICC six
npx sliccy (sliccy@latest) is still SLICC six: its node-server, with six's UI on www.sliccy.ai. npx sliccy@next runs this binary, which links browsers to seven on seven.sliccy.ai. Six stays available as npx sliccy@6.
Six's flags. slicc with no command stops at a flag of six's node-server before it discovers or launches anything. So does an unknown command followed by one. It prints this line on stderr and exits 2:
slicc: <flag> is a SLICC six option. This is the new SLICC (seven). For SLICC six, run: npx sliccy@6 <your args>
<your args>is the command line as given, with arguments quoted where needed, for a POSIX shell or, on Windows, for PowerShell. A join URL is shown as<join-url>, since it is never printed.--flag valueand--flag=valueboth count.- With no command, Go's flag syntax applies, so
-electroncounts as well. - The first six flag is the one named.
The flags come from six's packages/node-server/src/runtime-flags.ts, at slicc e44ee9d2c (6.247.17):
| six flag | six's parser |
|---|---|
-v |
runtime-flags.ts:159 |
--serve-only |
runtime-flags.ts:163 |
--hosted |
runtime-flags.ts:167 |
--kill |
runtime-flags.ts:171 |
--install-cli |
runtime-flags.ts:175 |
--computer-demo |
runtime-flags.ts:179 |
--cdp-port |
runtime-flags.ts:188 |
--log-level |
runtime-flags.ts:196 |
--log-dir |
runtime-flags.ts:203 |
--prompt |
runtime-flags.ts:207, :256 |
--env-file |
runtime-flags.ts:211, :258 |
--install-dir |
runtime-flags.ts:219, :260 |
--lead |
runtime-flags.ts:223, :333 |
--join |
runtime-flags.ts:228, :333 |
--electron-app |
runtime-flags.ts:237, :336 |
--electron |
runtime-flags.ts:336 |
--cloud |
cloud/dispatch.ts:108 (next to runtime-flags.ts) |
Six's --version, version, --profile and --mount are seven flags too, and keep their seven meaning. Six's --mount <os-path>:<vfs-path> is refused with a hint; see Host folders.
What changed:
| six | seven's slicc |
|---|---|
npx sliccy (six's UI on www.sliccy.ai) |
npx sliccy@next (seven on seven.sliccy.ai); six stays at npx sliccy@6 |
a global slicc from npm i -g sliccy |
npm i -g sliccy@next installs sliccy and slicc, and @ai-ecoverse/slicc installs slicc. Install only one of them globally, since both provide slicc |
six's Go follower, also called slicc (--install-cli, install-cli | sh) |
whichever slicc comes first on PATH runs; slicc --version prints 6.x for six's |
--serve-only --cdp-port=9222 |
slicc --cdp http://127.0.0.1:9222, or slicc attach --cdp … |
--mount=~/proj:/workspace/proj |
--mount ~/proj[:proj][:ro], a name rather than a path; the folder is then mounted in seven |
--profile leader (QA), SLICC_USER_DATA_DIR |
--profile <name>, a SLICC profile (see slicc launch); --profile six uses six's own Chrome profile in place, SLICC_USER_DATA_DIR included (see Using SLICC six's Chrome profile) |
--prompt "…" |
slicc prompt --cdp <url> "…" once the link is up |
--lead [url], WORKER_BASE_URL |
--page <url> and --origin <origin> |
--electron <app>, --kill |
slicc --browser chrome --browser electron:<slug>; quit the app first |
--log-level, --log-dir |
SLICC_DEBUG=1, on stderr |
-v |
-V or --version |
slicc <url> follow bash -c |
slicc <url> follow --runner bash -c |
slicc <url> follow --eval python -i |
slicc <url> follow --runner sh -c --eval python3 -i |
follow --no-banner, --plain |
no banner; --plain exists on slicc, prompt and watch |
watch <scoop>, exec, wait, model, thinking, new-session, *-cloud, list-sessions, --cloud, --hosted, --install-cli, --computer-demo |
not available |
slicc update (six's follower) |
six's follower updates from ai-ecoverse/slicc, and this one from ai-ecoverse/slicc-cli; moving over means reinstalling |
Environment variables.
- Gone:
PORT,CHROME_PATH,SLICC_USER_DATA_DIR,SLICC_CDP_LAUNCH_TIMEOUT_MS,WORKER_BASE_URL,SLICC_TRAY_WORKER_BASE_URL,SLICC_SECRETS_FILE,E2B_API_KEY,SLICC_JOIN_FILE,CHROME_USER_DATA_DIR,SLICC_CHROME_GPU,SLICC_CHROME_MOCK_KEYCHAIN,SLICC_BRIDGE_TOKEN,SLICC_PROMPT_SETTLE,SLICC_ABORT_CONFIRM,SLICC_NO_TUI(use--plain),SLICC_LOG_LEVEL,SLICC_LOG_FORMAT,COLUMNS,SLICC_INSTALL_DIR. - Kept:
SLICC_DEBUG,SLICC_NO_TELEMETRY,SLICC_NO_UPDATE_CHECK,SLICC_UPDATE_API_BASE,OPTEL_RATE,OPTEL_DEBUG,NO_COLOR. - New:
SLICC_TELEMETRY_URL,SLICC_LAUNCHER(set by npm),SLICC_DISCOVERY(tests).
Files.
- Not read:
~/.slicc/secrets.env,~/.slicc/session-id,~/.slicc/logs/and~/.slicc/cloud-sessions.json. - Moved into seven:
~/.slicc/crontaband~/.slicc/webhooks/now live inside seven's kernel, not on the host. - Update cache: six's Go follower caches its update check in
<user cache dir>/slicc/update-check.json, and this binary usesupdate-check-seven.jsonnext to it. Both files have the same shape, so with one shared file, each binary would show the other's version and hold back the other's check. - Kept by this binary:
<config>/slicc/holdsssh_host_ed25519_key,tray-certs.json,tray-join.json,loopback-identity.json,loopback-peers.jsonandseven-profiles/. - Profiles: six's Chrome profiles are
~/Library/Application Support/Slicc/profiles/browser-coding-agent-chrome[-<port>]on macOS,$XDG_STATE_HOME/slicc/profileson Linux and%LOCALAPPDATA%\Slicc\profileson Windows (chrome-launch.ts:119-148). seven's live in<config>/slicc/seven-profiles, so a SLICC--profile <name>never opens six's logins. Only--profile sixdoes, in place; see Using SLICC six's Chrome profile. Discovery lists six's Chrome markedsix: true, last, and never chooses it.
Coming from slicc-node
npx @ai-ecoverse/slicc-node and slicc-swift ran a local HTTP proxy, which seven reached on loopback with the URL and key from the launch URL's #proxy=…&key=… fragment. slicc links seven over WebRTC instead (see Link). On this machine the link is set up over CDP, or with slicc open over a loopback pairing listener (see Linking seven in your everyday browser), and from another machine through the tray hub. There is no proxy port, and no key: slicc open's #slicc-pair=… fragment carries a one-time token and the node's certificate fingerprint, and its listener only sets up links. The other listener is the kernel services one, which carries the browser's own connections to *.kernel.localhost into the page (--no-kernel turns it off).
| slicc-node | slicc |
|---|---|
npx @ai-ecoverse/slicc-node, which opens seven in the default browser |
slicc open (or slicc --default-browser) does that, in any browser and without CDP (see Linking seven in your everyday browser). npx sliccy@next, or slicc from curl -fsSL https://get.sliccy.ai/slicc | sh (see Install with curl). It launches or attaches to a Chromium browser over CDP and opens seven there (see The default) |
--page <url>, --origin <url> |
--page <url>, --origin <origin> |
--port, --rotate-key, --ephemeral, --no-open |
not needed: there is no proxy port and no key. slicc open listens on 17117 or the next free port up to 17126, slicc unpair forgets paired sevens, and slicc pair doesn't open a browser |
--mount <path>[:<name>][:ro] |
the same --mount (see Host folders). In seven the folder is <device>/<name> (mount -t hostfs lars-mbp/project /mnt/p) rather than <name> |
--kernel-port <n>, --no-kernel |
the same flags (see Kernel services) |
--cdp <url>, the /cdp relay to a running browser |
the cdp capability. The browser seven runs in is always listed. More come from --browser (with --cdp <url> for an endpoint) on slicc, --cdp-extra <url> on attach, and --cdp <url> on follow. --cdp-extra and follow --cdp must answer when they start |
--quiet |
no request log. --plain prints status lines, and SLICC_DEBUG=1 logs each http stream's method, scheme and host |
/api/fetch-proxy |
http, tcp, resolve and ws streams under the local policy: the internet, this machine and the LAN, but not the browsers' debugging ports or slicc's own ports |
/api/hostfs/*, tokens re-granted after a 403 |
fs streams, with no tokens. In seven, a mount has no medium while the link is down, and fills in when the link is back |
/api/kernel-tunnel |
kernel streams into the page |
/auth/callback, /api/oauth-state, /api/oauth-result |
not needed on https://<label>.sliccy.ai, where seven signs in through the relay itself. A seven on another origin needs slicc-extension to sign in |
startProxy, launchUrl, persistentKey (JS) |
no JS API |
Which browser. slicc-node opened seven in the default browser, in any profile. slicc does that too when it finds no SLICC profile and no CDP endpoint: plain slicc then falls back to the first way below by itself, and --no-fallback turns that off (see The default). slicc links seven in one of three ways:
- Your everyday browser, any vendor:
slicc open(orslicc --default-browser) opens seven in the default browser and pairs it over loopback, with no CDP and no remote debugging.slicc pairprints the pairing URL for a seven that's already open. Once paired, that seven links again by itself after a reload or a restart ofslicc. The link has the local policy, folders, kernel services and compute, but nocdp, since slicc controls no browser there (see Linking seven in your everyday browser). - A browser
slicccontrols over CDP, withsliccitself orslicc attach, which also offers sevencdpfor that browser:- by default, a browser in a SLICC profile (see
slicc launch). Its seven is separate from the one in your everyday profile, since seven keeps its files per browser profile. - or a browser that is already running with CDP (
--cdp). That includes your everyday Chrome once its remote debugging is turned on atchrome://inspect/#remote-debugging. It is then listed aswsOnly(see Browsers).
- by default, a browser in a SLICC profile (see
- A seven on another machine, in any browser, through the tray hub: in seven's Network panel, Link a device, then run
slicc <join-url> followon this machine (see Remote setup). That link has the remote policy (public addresses only, unless--allow) and no kernel services.
Restarts. slicc-node kept its key and port, so an open page reconnected after a restart. slicc sets the link up again instead:
- A browser that
slicclaunched runs in its own process group. It outlives asliccthat crashes or is killed. - The next
sliccfinds that browser's seven tab and links it again, withattachedand nolaunched. It finds the tab through the profile'sDevToolsActivePort, or through--cdp. Given the same--mountand--kernel-port, the folders and kernel services come back too. - Quitting
sliccnormally (q, Ctrl-C,SIGTERM) closes the browsers that run launched. - Every new
sliccprocess has a newnodeid, so seven sees a new link from the same machine. attachsets up again on every new seven context, andfollowreconnects by itself, with the same device certificate.slicc openandslicc pairkeep their certificate and port, so a paired page signals again after a restart, without a new pairing.
Using SLICC six's Chrome profile
slicc --profile six and slicc launch --profile six use SLICC six's own Chrome profile, with its logins, cookies and extensions, in place: it is never copied, moved or migrated. --profile six:<port> picks the profile six uses for serve port <port>. Nothing else ever chooses six's profile: not the default, --yes, --json, --browser or a --cdp URL.
Where it is, as six resolves it (ai-ecoverse/slicc at 65c5a79b4, v6.247.17-8; the data is in slicc-browsers' catalog.json, profiles.six):
| where | six's code | |
|---|---|---|
| macOS | ~/Library/Application Support/Slicc/profiles/<name> |
packages/node-server/src/chrome-launch.ts:124-126; packages/swift-server/Sources/Browser/ChromeLauncher.swift:314-322 |
| Linux | $XDG_STATE_HOME/slicc/profiles/<name>, else ~/.local/state/slicc/profiles/<name> |
chrome-launch.ts:127-130 |
| Windows | %LOCALAPPDATA%\Slicc\profiles\<name>, else ~\AppData\Local\Slicc\profiles\<name> |
chrome-launch.ts:131-134 |
<name> |
browser-coding-agent-chrome for six and six:5710, browser-coding-agent-chrome-<port> for six:<port> |
chrome-launch.ts:27, :147; ChromeLauncher.swift:5-6, :323 |
SLICC_USER_DATA_DIR |
when set and not blank, trimmed, it is six's profile for every port | chrome-launch.ts:143-146 (node-server only) |
| six's own launch | --remote-debugging-port=9222 by default, on that profile |
chrome-launch.ts:283, packages/node-server/src/runtime-flags.ts:47; ChromeLauncher.swift:213 |
A profile that doesn't exist is an error (SLICC six's Chrome profile doesn't exist; start SLICC six once, or pick another --profile); slicc never creates it. A path is six's when it is one of these dirs, compared case-insensitively and after resolving symlinks, so a symlink to six's profile, or a symlinked root, counts too.
The lock comes first. Chrome's lock is SingletonLock on macOS and Linux (a symlink to <host>-<pid>; live when that process is), and lockfile on Windows (live when it can't be opened exclusively; slicc opens it for reading and closes it at once).
- Live and debuggable, when the profile's
DevToolsActivePortanswers/json/versionwith its websocket path (usually six's 9222): slicc attaches to the running Chrome.sliccopens a seven tab in it (or uses one that is open) and links it, while six's node-server stays attached alongside.slicc launchprints its CDP URL, opens--urlin a new tab when given, and waits. - Live and not debuggable: slicc stops with exit 1:
six's Chrome is using this profile; close it, or use --browser/--profile to pick another. - Free (no lock, or a stale one): slicc launches the browser six used. That is the installed Chromium browser whose version is the profile's
Last Version(or, without one, the version inLocal State'suser_experience_metrics.stability.stats_version), the first inslicc browsers' id order when several match; otherwise Chrome stable.--browser <id>overrides it (forslicc, the first--browser; the others are extras as usual).
Nothing is written to six's profile. No seven prefs, no clean-exit marking, no removal of DevToolsActivePort, Singleton*, the session snapshot or any other file, no extensions added or removed, and no migration. The launch passes only runtime flags, which don't persist: --remote-debugging-port=0 --user-data-dir=<six's profile> --no-first-run --no-default-browser-check, the throwaway flags only when six's profile is in the temp dir (see slicc launch), the seven flags (left out with slicc launch --no-seven-flags), --headless=new with --headless, and the start page. A DevToolsActivePort that was there before the launch is ignored until Chrome writes a new one.
What does persist is only what Chrome itself keeps: browsing data from the session, and the seven tab in session restore, so six may reopen that tab the next time it starts Chrome.
Going back: quit (q, Ctrl-C, or slicc launch's Ctrl-C), then start six as usual. A Chrome slicc launched is closed as for any launch; one slicc attached to keeps running. Nothing needs undoing: slicc keeps no record that points into six's profile.
Output. slicc --profile six skips the picker and prints plain lines (or NDJSON with --json). When it attaches, the foreign-browser notice and event follow attached, with profile six (or six:<port>): slicc: using Chrome (profile six) at http://127.0.0.1:9222, which slicc didn't launch; opened a new seven tab. …. A launch reports launched with browser six, profile six and profileDir. slicc launch --profile six prints attached and detached events when it attaches (see slicc launch). Errors about six's profile go to telemetry as fixed messages, never with a path.
In the picker a Chrome · SLICC six profile row (… six profile <port> for a port variant) appears when that profile exists, with SLICC six's own profile, used in place, below every other row. It is never the best match and never preselected: Enter on it needs the cursor moved there, or the row selected with space. Picking it does the same as --profile six. A six endpoint in that profile is shown only through its row.
Verifying a download
Put the binary and the release's SHA256SUMS in one folder, then check:
- Checksum on macOS and Linux:
shasum -a 256 -c SHA256SUMS --ignore-missing. On Linuxsha256sum -c SHA256SUMS --ignore-missingdoes the same. - Checksum on Windows (PowerShell):
(Get-FileHash slicc-windows-amd64.exe -Algorithm SHA256).Hash.ToLower()must equal the hash on that file's line inSHA256SUMS(Select-String slicc-windows-amd64.exe SHA256SUMS). - Provenance:
gh attestation verify <file> --repo ai-ecoverse/slicc-clichecks the GitHub build-provenance attestation (Sigstore) that the release workflow made for that exact file.slicc updatechecks the same attestation itself (see Attestations).
Signing:
- macOS: the binaries are signed with a Developer ID, with the hardened runtime, and notarized. A bare binary can't carry a stapled ticket (only apps, disk images and installer packages can), so Gatekeeper checks the notarization online the first time it runs. Inside Sliccstart.app the binary is stapled with the app.
- Windows: Windows binaries are unsigned; SmartScreen may warn on first run. AppLocker, WDAC or Intune policies can block them.
Companion tools
slicc works with slicc-jerk (jerk), which runs on this machine and gets tokens for seven's vault. slicc finds it on PATH or in bin of its data folder: ~/Library/Application Support/slicc on macOS, $XDG_DATA_HOME/slicc (default ~/.local/share/slicc) on Linux, %LOCALAPPDATA%\slicc on Windows. That folder is never shared over fs (see Host folders). When its bin exists, slicc appends it to its own PATH at start, so its children (the SSH runner, for one) find jerk too; a copy already on PATH wins.
fnox (secrets on a real OS) is upstream jdx/fnox: install it from https://fnox.jdx.dev. slicc doesn't install it.
With npm. @ai-ecoverse/slicc and sliccy list @ai-ecoverse/slicc-jerk as an optionalDependency (^ the latest release), but only when it is on the registry when the release is staged (npm-stage.mjs --companions); otherwise it is left out with a notice. The publish check that waits for the platform packages doesn't wait for it. When the package is installed, the launcher puts its node_modules/.bin first on PATH for the binary, so jerk resolves for slicc and its children.
slicc install jerk installs jerk into the data folder's bin (any other name, or none, exits 2 with slicc install: name jerk and the usage):
jerkfrom the latest release ofai-ecoverse/slicc-jerk(0.6.0 or later, the first with the--via-linkcalls the link needs; an older latest release is refused withslicc install: ai-ecoverse/slicc-jerk <version> is older than 0.6.0, the first release slicc works with);- the release contract (the same as slicc-cli's own): a plain binary
<name>-<os>-<arch>(.exeon Windows;darwin,linux,windows×amd64,arm64), andSHA256SUMSwith a line for it, with a build provenance attestation by the repo'srelease.ymlover the binary or overSHA256SUMS; - slicc checks the download against
SHA256SUMSand verifies its attestation (see Attestations), then installs it only once its--versionnames the release's version, through a rename next to the old one (asslicc updatedoes); - it says
installed <name> <version> at <path> (SHA256SUMS and the build provenance attestation by <identity>), or<name> <version> is already installed at <path>; --checksum-onlyskips the attestation, with a warning, and saysSHA256SUMS only; the attestation wasn't checked;- the GitHub API base follows
SLICC_UPDATE_API_BASE, as forupdate.
Only a user runs slicc install. Nothing on a link starts an install.
Attestations
slicc update and slicc install verify the GitHub build provenance attestation of every binary they download, after its SHA256SUMS check, and refuse it when that fails. A release without an attestation fails too. --checksum-only is the only way around it, and it warns.
Where the attestation comes from. GET /repos/<repo>/attestations/sha256:<digest> on the GitHub API, first for the binary's digest. When the binary has none, slicc looks up the digest of the release's SHA256SUMS instead: an attestation over SHA256SUMS covers the binary through its line there, which slicc has already checked. slicc accepts the binary when one of the bundles verifies.
What is verified (internal/attest, standard library only), for a Sigstore bundle v0.3 with one DSSE envelope, one Fulcio certificate and one Rekor (v1) entry:
- the envelope is an in-toto statement whose predicate type is
https://slsa.dev/provenance/v1, with a subject whose SHA-256 is the file's; - its single DSSE signature verifies with the certificate's key (ECDSA P-256 or P-384) over the DSSE pre-authentication encoding;
- the certificate chains, as a code-signing certificate, to a Fulcio root in the trusted root that is valid at the entry's integrated time, and the certificate itself was valid then;
- one of the certificate's embedded SCTs (
1.3.6.1.4.1.11129.2.4.2) verifies with a certificate transparency log key in the trusted root, valid at the SCT's time, over the precertificate (the certificate body without its SCT list) and its issuer's key hash (RFC 6962); - the certificate's identity: issuer (Fulcio extension
1.3.6.1.4.1.57264.1.8)https://token.actions.githubusercontent.com, and exactly one subject alternative name, the URIhttps://github.com/<repo>/.github/workflows/release.yml@refs/heads/main, pinned per repo:ai-ecoverse/slicc-cliforupdate,ai-ecoverse/slicc-jerkforinstall; - the Rekor entry is a
dsse0.0.1 entry whose payload hash, signature and verifier are the envelope's and the certificate's; - its signed entry timestamp verifies with the key of a Rekor log in the trusted root, valid at the integrated time;
- its inclusion proof leads from the entry to the proof's root hash (RFC 6962), and the checkpoint carries that tree size and root hash and is signed by the same log.
Errors name the step that failed (attestation identity: …). A bundle from a Rekor v2 log, or any other shape, fails closed.
Unsupported: Rekor v2. Bundles from Rekor v2 logs (Ed25519 log keys, dsse 0.0.2 entries, RFC 3161 timestamps instead of signed entry timestamps) aren't supported. They fail closed, so update and install refuse such a release until the verifier learns them; --checksum-only is the way around it meanwhile.
The trusted root. internal/attest/trusted_root.json is Sigstore's public-good trusted root (Fulcio CAs, Rekor and CT log keys) (the trusted_root.json target of https://tuf-repo-cdn.sigstore.dev), embedded in the binary. SLICC_SIGSTORE_TRUSTED_ROOT replaces it, for tests.
Rotating the trusted root. The Sigstore workflow runs daily. Its trusted-root job runs node scripts/sigstore-root.mjs, which follows the TUF repository's timestamp.json, snapshot and targets to the current trusted_root.json and compares its SHA-256 with the embedded file; when they differ, it opens an issue. To rotate:
- run
node scripts/sigstore-root.mjs --write, which downloads the new file and checks it against the targets hash; - review the diff (new Fulcio certificates or Rekor keys,
validForranges) against Sigstore's announcement; the script doesn't check TUF signatures, so this review is the trust decision; - open a
fix:PR, so a release ships it. Binaries released before it keep the old root, and fail closed on attestations that need the new one until they update with--checksum-onlyor by hand.
Parity. The workflow's parity job runs TestAttestParityWithGH: for the latest release, gh attestation verify --repo ai-ecoverse/slicc-cli --signer-workflow …/release.yml and slicc's verifier must both accept the binary, and both refuse it after one byte is appended.
Updates
slicc update [--check] [--checksum-only] replaces the running binary (symlinks resolved), wherever it is, such as ~/.local/bin/slicc from install.sh, with the latest GitHub release of ai-ecoverse/slicc-cli. It downloads the asset for this OS and CPU, checks it against the release's SHA256SUMS, and verifies its build provenance attestation (see Attestations); --checksum-only skips the attestation, with a warning. The new binary has to answer --version with the release's version before anything is replaced. The swap is a rename next to the old binary; on Windows the running binary is first moved aside to <name>.old, which the next run deletes. slicc update --check only reports.
The version. A release build has its version linked in (-X main.version=<version>). Without one, slicc reads its Go build info: a binary built by go install …@<version> (or go run) from a downloaded module, with a checksum in the build info, reports that module version without the leading v. Anything else built from a checkout reports dev.
update refuses, with exit code 1:
- a go-installed binary (no linked version, and a downloaded module with a checksum in its build info): it prints
update it with `go install github.com/ai-ecoverse/slicc-cli/cmd/slicc@latest`; - a development build (any version that isn't plain
X.Y.Z);--checkstill prints the latest release; - an npm install (
SLICC_LAUNCHER=npm, or a path throughnode_modules), which updates through npm. The message names the commands:npm install -g @ai-ecoverse/slicc@latestandnpx @ai-ecoverse/slicc@latest, orsliccy@nextfor both when the binary came withsliccy(anode_modules/sliccyon its path, or next to itsnode_modules). Under npx (an_npxdir on the path) it names only the npx command; - a binary inside an app bundle (
*.app/Contents/…), which the app updates.
These checks come first, so --check refuses the same way and nothing asks GitHub.
Release builds outside npm and app bundles print a notice on stderr when a newer release exists: slicc X is available (you have Y); run `slicc update` . A go-installed release says run `go install github.com/ai-ecoverse/slicc-cli/cmd/slicc@latest` instead. The notice comes from a cache (<user cache dir>/slicc/update-check-seven.json; six's Go follower uses update-check.json, see Coming from SLICC six), and a background check refreshes it at most once every 24 hours, so no run waits on the check for more than 2 seconds. --version, update, and any command line with --help, -h or -help print no notice.
Telemetry
Release builds (a plain X.Y.Z version, linked in or, for go install, from the module version) send operational telemetry through go-optel with the app id slicc-cli, sampled once per process (1 in 100 by default):
enter, with the subcommand assource, from a fixed list (abort,attach,browsers,follow,install,launch,open,pair,prompt,unpair,update,watch,defaultfor no command (no arguments, or only flags),joinfor a bare join URL, otherwiseunknown). No other argument is ever sent.error, for operational failures, from a fixed set of sources (update,signal,link,browser,ssh,cdp). Messages pass through go-optel's sanitizer, which strips URLs and paths.
--version, and any command line with --help, -h or -help (such as slicc update --help), send nothing, and development builds never configure a client.
Environment
| variable | effect |
|---|---|
SLICC_NO_TELEMETRY=1 |
send no telemetry |
SLICC_NO_UPDATE_CHECK=1 |
no update notice and no background check |
SLICC_UPDATE_API_BASE |
GitHub API base for update, install and the notice (default https://api.github.com), for mirrors and tests |
SLICC_SIGSTORE_TRUSTED_ROOT |
a Sigstore trusted_root.json to verify attestations against instead of the embedded one, for tests (see Attestations) |
SLICC_TELEMETRY_URL |
telemetry collector base (default https://rum.hlx.page/) |
OPTEL_RATE, OPTEL_DEBUG |
go-optel's sampling rate and beacon logging |
SLICC_LAUNCHER |
set to npm by the npm launcher; removed from the environment that slicc passes on |
SLICC_DISCOVERY |
test-only: limits browser discovery, see Development |
SLICC_OPEN_COMMAND |
test-only: slicc open runs this program with the pairing URL as its only argument instead of opening the default browser; the integration tests always set it |
SLICC_LOOPBACK_PORTS |
test-only: <first>-<last>, the port range of the loopback listener instead of 17117-17126 |
SLICC_USER_DATA_DIR, XDG_STATE_HOME |
read, as SLICC six reads them, only to find six's Chrome profile (see Using SLICC six's Chrome profile) |
The default: slicc
slicc [--browser <id>]... [--cdp <url>]... [--page <url>] [--origin <origin>]... [--profile <name> | --profile six[:<port>]] [--headless]
[--name <name>] [--kernel-port <n>] [--no-kernel] [--allow <rule>]... [--deny <rule>]... [--mount <path>[:<name>][:ro]]...
[--yes] [--plain] [--json] [--no-fallback] [--open] [--default-browser]
[--runner <argv…> [--]] [--shell <argv…> [--]] [--eval <argv…> [--]] [--eval-quiet <dur>] [--sftp | --no-sftp]
slicc with no command (npx sliccy) finds browsers, launches or attaches to the best one, opens seven in it and links it. It serves the link until it is told to stop: q or Ctrl-C in the dashboard, or SIGINT or SIGTERM. Then it says bye to seven, closes the browsers this run launched (as slicc launch does), leaves the browsers it attached to running, and exits 0. If the browser that hosts seven goes away, slicc closes the rest and exits 0 too. Setup failures (no browser, a launch or attach that fails) exit 1, and bad flags exit 2. With --no-fallback, a run where nothing can host seven says no browser to control: run `slicc open` to link seven in your everyday browser and exits 1.
The fallback to your default browser. Plain slicc with no SLICC profile and no CDP endpoint falls back to linking seven in your everyday browser, as slicc pair does:
- When: no
--browser,--cdpor--profilewas given and--no-fallbackwasn't, and discovery found no running CDP endpoint (SLICC six's Chrome doesn't count) and no installed Chromium browser with a profile in the SLICC profiles dir. A profile or endpoint, found or given, always wins, so the flow above runs instead. - What it says:
No SLICC browser or debugging endpoint found, so seven opens in your default browser for pairing. Use --profile or --cdp to choose.on stderr, in place of the picker and the dashboard. With--jsonit adds{"schema":1,"event":"mode","mode":"loopback","reason":"no-browser"}afterdiscovered. - The pairing URL: when stdin and stdout are both terminals, slicc opens it in the default browser. Otherwise it only prints it, unless
--openis given. Without--jsonit goes to stdout. With--jsonit comes as{"schema":1,"event":"pairing","url":"<pairing URL>"}, and so does each new one. --jsonevents in this mode, with the shapes of the other modes plusmode: "loopback"anddevice(the page's certificate fingerprint):linked:peer?,capsandpolicy, for each link that comes up;link-lost:reason?ormessage?, for each one that goes;closed, withbrowsers: [], last, when slicc exits.
- Then it serves loopback links with this run's
--page,--origin,--name,--allow,--deny,--kernel-port,--no-kernel,--no-jerk,--mountand compute flags, untilSIGINTorSIGTERM.
Discovery is slicc browsers' discovery and ranking (see Browsers), with each --cdp URL added and the --page origin counted as a seven origin.
Going with a browser. The first browser hosts seven:
- a running endpoint with a seven tab open: slicc attaches to it;
- a running endpoint without one: slicc opens
--pagein a new tab (Target.createTarget) and attaches; - an installed browser: slicc launches it as
slicc launch --url <page>does, in its SLICC profile (--profile, or the one picked withp; default: the browser id), with the seven flags and prefs, and--headless=newwith--headless, then attaches.
Without --browser, the first row of the ranking that can host seven is chosen, an endpoint slicc didn't launch included, but never one in SLICC six's profile. When that happens without the picker, slicc says so on stderr, for an endpoint that isn't in a SLICC profile and that no --cdp named: slicc: using Google Chrome (profile default) at http://127.0.0.1:9222, which slicc didn't launch; opened a new seven tab. Use --browser or --cdp to choose another. It says found a seven tab when one was open already. The profile is default for a browser's default user-data dir, the base name of the user-data dir otherwise, and left out (with its parentheses) when unknown. With --json the line comes as the foreign-browser event too. Picking a row in the picker, --browser, or a --cdp for that endpoint means the user chose it, and there is no notice. An Electron app can't host seven. Each further browser is an extra: an installed one is launched without a start page, and an endpoint is used as it is. Then the link is set up as for slicc attach (see Local setup): over CDP with the browser that hosts seven, the first page tab on an allowed origin.
What the link offers: net (tcp, resolve), http, ws, and cdp with the browser that hosts seven (seven: true) and every extra, under the local policy with all of their debugging ports protected; and kernel services on --kernel-port (default 80; --no-kernel turns them off) when seven offers kernel-in (see Kernel services); and ssh only with --runner (see SSH server); and fs with each --mount folder (see Host folders). seven gets full control of every listed browser, the one it runs in too. The cdp list (HELLO browsers) has only the browser that hosts seven and the extras picked with --browser or in the picker; the other browsers discovery found are never offered.
The cdp list follows the browsers: an extra that stops answering is taken off the list (browser-gone), and listed again with the same id when it answers on the same endpoint again (browser-back).
Flags.
--browser <id>: an id fromslicc browsers(an installed id such aschrome, orcdp:<host>:<port>), or a name, case-insensitively. Repeatable: the first hosts seven, the others are extras. Without it, the best match that can host seven. An endpoint in SLICC six's profile is refused (<id> is SLICC six's Chrome; only --profile six uses it).--cdp <url>: also consider this CDP endpoint, asslicc browsers --cdp.--page <url>: the seven page, defaulthttps://seven.sliccy.ai/. It must behttporhttps, and its origin is accepted as a seven origin.--origin,--name,--allow,--deny,--kernel-port,--no-kernel,--no-jerk,--mount, and the compute flags--runner,--shell,--eval,--eval-quiet,--sftp,--no-sftp: as forslicc attach. Without--runnerthere is nossh.--profile <name>: the SLICC profile for a launched browser that hosts seven.--profile sixand--profile six:<port>use SLICC six's own Chrome profile instead, without the picker (see Using SLICC six's Chrome profile).--headless: launch browsers without a window.--yes,--plain: no picker.--json: NDJSON events, below.--no-fallback: never fall back to the default browser. slicc then behaves as it did before the fallback: it launches an installed browser in a new SLICC profile, or exits 1 withno browser to controlwhen nothing can host seven (see above).--open: when falling back without a terminal, open the pairing URL in the default browser too.--default-browser: don't look for browsers; link a seven in the default browser instead, exactly asslicc opendoes (see Linking seven in your everyday browser).- A flag of SLICC six's points back to six and exits 2 (see Coming from SLICC six). Any other flag, or an argument, is an error.
The picker comes up when stdin and stdout are both terminals and none of --yes, --browser, --plain or --json is given. It lists the ranking best first, one row each, with its state: CDP :<port> and seven open for a running endpoint, installed · profile <name> (or new SLICC profile <name> when the profile doesn't exist yet) for a browser slicc would launch, and installed · Electron for an Electron app. The first row is marked best match. A list longer than the terminal scrolls with the cursor.
| key | what it does |
|---|---|
↑ ↓ (k, j) |
move |
| space | select or unselect the row; the first selected hosts seven, and the rest are extra cdp browsers, in the order selected |
| Enter | go: with the selected rows, or with the highlighted row when nothing is selected |
p |
choose the highlighted browser's SLICC profile: its default, an existing profile in the profiles dir, or new profile… with a name (letters, digits, ., _, -) |
| Esc | back, in the profile chooser |
q, Ctrl-C |
quit; in the dashboard, close down as described above |
The dashboard follows: the linked browsers (launched or attached, their CDP port, hosts seven or extra, and gone while one doesn't answer), the seven tab, the link state (waiting for seven, connecting, linked with its uptime, or reconnecting after a link is lost, while slicc waits for seven to offer again), the capabilities offered, the policy summary, compute ssh via <runner> with --runner, the kernel services URL, stream counts per kind (open and total, never contents), the last status lines, and the update notice, which then isn't printed on stderr. NO_COLOR turns colors off.
Plain mode (no terminal, --yes, --browser or --plain): no picker and no dashboard, just the default, with status lines on stderr (slicc: launched …, slicc: attached to …, slicc: offering …; policy: …; compute: ssh via <runner>, slicc: linked with "<name>" in <tab>, slicc: link lost: …, slicc: <name> quit …, slicc: <name> is back …, slicc: closed <ids>). It never writes ANSI escapes or cursor codes. SLICC_DEBUG=1 adds diagnostics.
--json prints NDJSON events on stdout and nothing else there, for Sliccstart (slicc --json --browser <id>). Errors are also printed on stderr. Every event has "schema": 1 and event. Fields marked ? are left out when empty; new fields and events may be added, and existing ones keep their meaning.
discovered:entries, the ranked list in the shape ofslicc browsers --json(always a list).launched:browser(the id),name,version?,role(sevenorextra),pid,profile?,profileDir?,cdp,webSocketDebuggerUrl.attached:browser,name,version?,role,launched?(truewhen this run launched it),cdp,webSocketDebuggerUrl?. Forsevenslicc is connected to it over CDP; anextrais listed forcdp.foreign-browser:browser,name,profile?,cdp,openedTab(truewhen slicc opened the seven tab,falsewhen one was open). It followsattachedwhen the browser hosting seven is an endpoint slicc didn't launch and the user didn't choose it (see above), and always when it is SLICC six's Chrome, withprofilesixorsix:<port>. The same line goes to stderr.linked:peer?(the name in seven's HELLO),tab?(the seven tab's URL),caps,policy(the summary).link-lost:reason?when slicc or seven closed the link (slicc is exiting, seven'sbyereason,replaced by a new offer), ormessage?for a failure. seven sets the link up again after a reload, and a newlinkedfollows.browser-gone,browser-back:browser,name,role. A browser stopped answering and left the cdp list, or answers again and is back on it.closed:browsers, the ids slicc closed (always a list). It is the last event of every run that got past choosing its browsers.error:message, andbrowser?when closing that browser failed. A setup error ends the run with exit 1.mode:mode(loopback) andreason(no-browser), when the run falls back to the default browser. Then comepairing,linkedandlink-lostevents (eachlinkedandlink-lostwithmodeanddevice, the page's fingerprint), andclosedwithbrowsers: [].pairing:url, a pairing URL, aftermode.
{"schema":1,"event":"discovered","entries":[…]}
{"schema":1,"event":"launched","browser":"chrome","name":"Google Chrome","version":"154.0.8037.98","role":"seven","pid":4242,"profile":"chrome","profileDir":"…/slicc/seven-profiles/chrome","cdp":"http://127.0.0.1:53211","webSocketDebuggerUrl":"ws://127.0.0.1:53211/devtools/browser/…"}
{"schema":1,"event":"attached","browser":"chrome","name":"Google Chrome","version":"154.0.8037.98","role":"seven","launched":true,"cdp":"http://127.0.0.1:53211","webSocketDebuggerUrl":"ws://127.0.0.1:53211/devtools/browser/…"}
{"schema":1,"event":"linked","tab":"https://seven.sliccy.ai/","peer":"seven","caps":["net","http","credentialed","ws","cdp"],"policy":"every public address, this machine, and the local network"}
{"schema":1,"event":"link-lost","reason":"slicc is exiting"}
{"schema":1,"event":"closed","browsers":["chrome"]}
Browsers
slicc browsers [--json] [--cdp <url>]... [--no-bundle-scan]
Lists running CDP endpoints and installed browsers, best match first. The browser tables (ids, names, bundle ids, install and user-data paths, Electron apps), the ranking tiers, the seven flags and prefs and the process-scan rules come from slicc-browsers' catalog.json, which slicc-cdp's playwright-cli reads too. Its conformance corpus runs in test/integration/corpus_test.go.
Running endpoints come from four places:
DevToolsActivePortfiles in the default user-data dirs of the browsers below, in each SLICC profile under the profiles dir (seeslicc launch), in SLICC six's profiles (see Using SLICC six's Chrome profile), and on Linux in the flatpak (~/.var/app/<id>/config/…) and snap (~/snap/chromium/common/chromium) dirs. The file's port must answer/json/versionwith the websocket path on the file's second line, otherwise the file is stale and ignored. A file without a second line starting with/is ignored too.GET /json/versionon127.0.0.1:9222to9229.- The process list (
ps -ax -o pid= -o command=; on WindowsGet-CimInstance Win32_ProcesswithProcessIdandCommandLine, as JSON): each process started with--remote-debugging-port=<n>or--remote-debugging-port <n>. Pid 0,sliccitself, and commands run by a wrapper (node,npx,tsx,npm,open,bash,zsh,sh,csh,fish,dash,timeout,env,sudo,caffeinate,cmd,powershell,pwsh) are skipped. Port 0 means the port is in theDevToolsActivePortof the process's--user-data-dir(its value runs to the next whitespace followed by--or to the end, so paths with spaces work, and a quoted value runs to the matching quote), checked as above; without one, or with one under aslicc/profilesfolder that isn't one of six's profiles, the process is skipped. Any other port is asked for/json/versionon127.0.0.1. The exact rules are in slicc-browsers. - Each
--cdpURL:http(s)://host:port,ws(s)://host:port/devtools/browser/<id>,host:portor a bare port. One that doesn't answer is reported on stderr and left out; the exit status stays 0.
Endpoints are merged by host and port. A DevToolsActivePort file wins over --cdp, which wins over the process list, which wins over the port probe. For each endpoint, /json/version gives the name, version and webSocketDebuggerUrl, and /json/list gives the seven tabs: page targets whose origin is https://<label>.sliccy.ai, other than www (:443 is the same origin, any other port isn't). An endpoint's cdp has its host in lower case, with localhost and ::1 written as 127.0.0.1. A DevToolsActivePort whose port is open but serves no /json/version (Chrome's opt-in remote debugging of a running browser) is listed as wsOnly, with the websocket URL from the file and no tab check.
Discovery only reads. Every source, the port probe included, sends each endpoint GET /json/version and GET /json/list and nothing else: no WebSocket, no navigation, no new tab. Its HTTP client never follows a redirect (a 3xx counts as no answer) and never uses a proxy from the environment. The same client does every other read of a CDP endpoint: /json/version and the WebSocket handshake when slicc and attach connect, and the cdp capability's /json/version and WebSocket for each stream. A DevToolsActivePort or ws:// --cdp URL whose port serves no /json/version gets a TCP connect, closed at once, to tell wsOnly from stale. The targets in /json/list that aren't seven tabs are dropped as soon as the list is parsed: their URLs, titles and ids are never logged (SLICC_DEBUG=1 included), cached, written to disk, printed (--json, the picker, plain mode), sent over the link, or put in telemetry or error messages. Only the seven tabs' id, url and title are kept. Attaching to a browser, and anything else that talks to it over CDP, happens only once a browser has been chosen to host seven (see The default).
Installed browsers, by id: chrome, edge, brave, opera, vivaldi, arc, atlas, dia, chrome-beta, chrome-dev, chrome-canary, brave-beta, brave-nightly, edge-beta, edge-dev, edge-canary, vivaldi-snapshot, chrome-for-testing, chromium. Electron apps get electron:<slug of the name>, such as electron:slack. When a browser is found twice, the first find keeps the id.
- macOS:
/Applicationsand~/Applications, plusmdfindby bundle id. Name, version and executable come from each bundle'sInfo.plist. An app is an Electron app when it hasContents/Frameworks/Electron Framework.frameworkorMSWebView2.framework.~/Applications/<Name> Debug.app(a Sliccstart debug build) replaces/Applications/<Name>.app. Claude and 1Password block remote debugging and are listed withdebugSupport: "disabled". With--no-bundle-scan, nothing inside an app bundle is read except for debug builds, which avoids macOS App Management prompts: browsers are matched by bundle id or app name, Electron apps by a list of known bundle ids, and versions come from Spotlight (mdls). Those Electron apps havedebugSupport: "unknown". - Windows:
StartMenuInternetandApp Pathsin the registry (current user, then machine), then the known paths under%ProgramFiles%,%ProgramFiles(x86)%and%LOCALAPPDATA%. The version is the highest version-named directory next to the executable. Electron apps are directories under%LOCALAPPDATA%\Programs,%ProgramFiles%and%ProgramFiles(x86)%withresources\app.asar(orresources\app) and Chromium's.pak/.binfiles, and Squirrel installs (%LOCALAPPDATA%\<app>\Update.exewithapp-<version>dirs). - Linux: snaps (
/snap/bin/<name>), names onPATH(such asgoogle-chrome-stable,chromium,microsoft-edge-stable,brave-browser), flatpaks (~/.local/share/flatpak/app/<id>,/var/lib/flatpak/app/<id>) and.desktopfiles in the XDG, flatpak and snap application dirs. Versions come from/var/lib/dpkg/status, a snap'smeta/snap.yaml, or else<browser> --version. A.desktopprogram whose directory hasresources/app.asar(orresources/app) and Chromium's data files is an Electron app.
SLICC six's Chrome. An endpoint that a source found in one of six's profiles is marked six: true: from its DevToolsActivePort, from a process whose --user-data-dir is six's (with port 0 its DevToolsActivePort is read), or from a port probe or --cdp URL whose port the process list maps to six's profile. A stale file marks nothing. It ranks last and is never chosen by itself (see Using SLICC six's Chrome profile).
Ranking. Tiers, in order: 1 an endpoint with seven open, 2 any other running endpoint, 3 Chrome stable, 4 other Chromium browsers, 5 Electron apps, 6 an endpoint in SLICC six's profile, with or without seven open. Endpoints sort by host, then port. Browsers sort in the id order above, Electron apps by name, then path. Versions below MinChromiumMajor (116) are left out: for endpoints by the Chrome/<major> in the user agent, and for installed Chrome, Edge and Chromium by their version, which is the Chromium version. Other browsers' versions are not Chromium versions and are not filtered.
--json prints one document. Fields marked ? are left out when empty; new fields may be added, existing ones keep their meaning. playwright-cli list --json prints the same shape. This section is its spec; slicc-browsers' schema/browsers.schema.json is a JSON Schema copy of it.
{
"schema": 1,
"minChromium": 116,
"entries": [
{
"id": "cdp:127.0.0.1:9222",
"kind": "endpoint",
"tier": 1,
"name": "Google Chrome",
"version": "154.0.8037.98",
"endpoint": {
"cdp": "http://127.0.0.1:9222",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/…",
"name": "Google Chrome",
"version": "154.0.8037.98",
"product": "Chrome/154.0.8037.98",
"userAgent": "Mozilla/5.0 …",
"source": "devtools-active-port",
"browser": "chrome",
"userDataDir": "/Users/me/Library/Application Support/Google/Chrome",
"sevenOpen": true,
"sevenTabs": [{ "id": "…", "url": "https://lars.sliccy.ai/", "title": "seven" }]
}
},
{
"id": "chrome",
"kind": "installed",
"tier": 3,
"name": "Google Chrome",
"version": "154.0.8037.98",
"installed": {
"id": "chrome",
"name": "Google Chrome",
"engine": "chromium",
"channel": "stable",
"version": "154.0.8037.98",
"path": "/Applications/Google Chrome.app",
"executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"bundleId": "com.google.Chrome",
"userDataDir": "/Users/me/Library/Application Support/Google/Chrome",
"debugSupport": "supported"
}
}
]
}
- Entry:
id(an installed id, orcdp:<host>:<port>),kind(endpointorinstalled),tier(1–6),name,version?, and eitherendpointorinstalled. endpoint:cdp(the HTTP base URL),webSocketDebuggerUrl,name,version?,product?anduserAgent?(from/json/version),source(devtools-active-port,flag,processorprobe),browser?(the installed id whose user-data dir held the file),userDataDir?(the dir that held the file, or a process's--user-data-dir),profile?(the SLICC profile name),wsOnly?,six?(truefor SLICC six's Chrome),sevenOpen,sevenTabs(always a list).installed:id,name,engine(chromiumorelectron),channel?,version?,path(the.app, executable or flatpak dir),executable(for a flatpak, theflatpakbinary),bundleId?,via?(snaporflatpak),package?(the snap or flatpak id),userDataDir?(the browser's default),debugSupport(supported,disabledorunknown),debugBuild?.
Without --json, the same list is a table: id, name, version, state and where. The state of six's Chrome ends with SLICC six's profile.
slicc launch [--browser <id>] [--profile <name>] [--url <url>] [--headless] [--no-seven-flags] [--json]
Launches a browser with CDP and keeps running until interrupted (Ctrl-C, SIGINT or SIGTERM; on Windows Ctrl-C or Ctrl-Break). Then it closes the browser it started and exits 0. If the browser exits by itself, slicc launch exits 0 too.
--browsertakes an installed id fromslicc browsers(or its name, case-insensitively). The default is the best installed Chromium browser in the ranking above.--profile sixand--profile six:<port>use SLICC six's own Chrome profile in place:slicc launchattaches to six's Chrome when it is running with CDP, refuses when it runs without, and otherwise launches the browser six used (or--browser) in it, writing nothing to the profile. None of the profile steps below apply. See Using SLICC six's Chrome profile.- Chromium browsers always get a separate SLICC profile,
<config dir>/slicc/seven-profiles/<name>, because Chrome 136 and later ignore CDP flags on the default profile. The config dir is~/Library/Application Supporton macOS,%APPDATA%on Windows and$XDG_CONFIG_HOME(or~/.config) on Linux. A snap's profiles live in~/snap/<snap>/common/slicc/seven-profiles, the only place it can write. The profiles dir is shared with slicc-cdp'splaywright-cli, whoseopenlaunches into the same<name>dirs. Both tools reuse a browser that is still running there, through itsDevToolsActivePort.--profileis the name (letters, digits,.,_,-; default: the browser id). A profile another browser process still holds is refused, and so is one that is SLICC six's (whenSLICC_USER_DATA_DIRpoints at it, say), before anything in it is touched. - Older profiles. Earlier versions kept profiles in
<config dir>/slicc/profiles. On macOS's default case-insensitive volume, that is the same folder as SLICC six's~/Library/Application Support/Slicc/profiles.slicc,slicc browsersandslicc launchmove them once, and do the same for each snap'sslicc/profiles.- Each profile dir is renamed into
seven-profiles, which is created when missing.playwright-climay already have created it. - Once every profile has been looked at, the move writes
seven-profiles/.migrated-from-profilesand never runs again. A run that is cut short picks up where it stopped next time. - A name that already exists in
seven-profilesstays in the old folder, with the noticeslicc: profile <name> stays in <old>, since <new>/<name> already exists. - Left alone: six's profiles (
browser-coding-agent-chromeandbrowser-coding-agent-chrome-<port>, in any case, and theSLICC_USER_DATA_DIRdir; nothing moves at all when the old folder is in one of them), dotfiles (six's.profile-migration-complete), and anything that isn't a valid profile name. - A profile whose
SingletonLockpoints at a live process stays where it is, with a one-line notice on stderr:slicc: profile <name> is in use by process <pid>, so it stays in <old>; quit that browser, then move it to <new>. slicc/profilesis never scanned or launched into afterwards.
- Each profile dir is renamed into
- Before the launch, a stale
DevToolsActivePort, Chrome'sSingleton*files and the session-restore snapshot are removed, and the last exit is marked clean, so only the start page opens. - Before each launch, the seven prefs are merged into the profile's
Default/Preferences, which is created if missing:tab_freezing_enabled: false,performance_tuning.tab_freezing.enabled: false, Memory Saver off (performance_tuning.high_efficiency_mode.state: 0), and[*.]sliccy.aiplus each extra allowed origin's host as tab-discard exceptions. The exceptions go into the listperformance_tuning.tab_discarding.exceptions(six's form) and into the mapperformance_tuning.tab_discarding.exceptions_with_time, which Chrome 154 reads and shows under "Always keep these sites active". Existing keys, exceptions and timestamps are kept, and a file that already has everything isn't rewritten. A Preferences file that isn't a JSON object is left alone.--no-seven-flagsskips these prefs too. - Chromium flags:
--remote-debugging-port=0 --user-data-dir=<profile> --no-first-run --no-default-browser-check, the throwaway flags for a throwaway profile, the seven flags below,--headless=newwith--headless, then--url. - A throwaway profile is one inside the system temp dir (Go's
os.TempDir():TMPDIRon macOS and Linux,TMPorTEMPon Windows; symlinks resolved, case ignored on macOS and Windows). It gets--use-mock-keychain --password-store=basic(browser.ThrowawaySwitches), so Chrome never asks for the macOS keychain or a Linux password store; without a default keychain, as under a temporaryHOME, macOS would show "Keychain Not Found". Every other profile, SLICC's and six's included, headless or not, keeps the real keychain, so its saved cookies and passwords stay readable.browser.Throwaway(dir)tells which is which. - The seven flags keep a seven tab's kernel running while the tab is in the background:
--disable-background-timer-throttling,--disable-backgrounding-occluded-windows,--disable-renderer-backgrounding,--disable-background-tracing, and--disable-features=LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSockets,IntensiveWakeUpThrottling,HighEfficiencyModeAvailable,InfiniteTabsFreezing,InfiniteTabsFreezingOnMemoryPressure,CPUMeasurementInFreezingPolicy,MemoryMeasurementInFreezingPolicy,AllowDevtoolsConnectedDiscard. Chromium ignores feature names it doesn't know.--no-seven-flags(LaunchOptions.NoSevenFlags) leaves them all out, along with the seven prefs above. Chromium keeps only one--disable-featuresvalue, so every--disable-featuresfrom the seven flags and fromLaunchOptions.ExtraArgsis merged into one switch, in order and without duplicates. The CDP port is read from the profile'sDevToolsActivePortonce/json/versionanswers with the same websocket path (30 s limit). On macOS a browser with a window starts throughopen -n -W -a <app> --args …, so camera and microphone permissions belong to the browser, not the terminal;--headlessruns the executable directly. A flatpak starts withflatpak run --filesystem=<profile> <id>. - Electron apps keep their own data and get
--remote-debugging-port=<port>, the first free port from 9333 up, so they never take 9222–9229. An app that is already running is refused, since a second instance would hand off to it; quit it first. Apps withdebugSupport: "disabled"are refused. - Shutdown:
Browser.closeover CDP; if the browser is still running 5 s later, a signal (SIGTERMto the browser's process group and main process;taskkill /Ton Windows), then after 3 s a kill (SIGKILL;taskkill /F /T). The browser runs in its own process group (CREATE_NEW_PROCESS_GROUPon Windows), so a terminal Ctrl-C reaches onlyslicc.
Plain output: the CDP HTTP URL on stdout, status lines on stderr. With --json, NDJSON events on stdout:
{"event":"launched","id","name","version","engine","pid","profile","profileDir","cdp","webSocketDebuggerUrl"}(version,profileandprofileDirare left out when empty);{"event":"closed"}afterslicc launchclosed the browser;{"event":"exited"}when the browser exited by itself;- with
--profile six, when six's Chrome was already running:{"event":"attached","id","name","version","engine","profile","profileDir","cdp","webSocketDebuggerUrl"}instead oflaunched, then{"event":"detached"}on Ctrl-C (six's Chrome keeps running) orexitedwhen it quits; {"event":"error","message"}, then exit 1.
Local setup: slicc attach
slicc attach --cdp <url> [--cdp-extra <url>]... [--origin <origin>]... [--signal cdp|stdio] [--name <name>] [--allow <rule>]... [--deny <rule>]...
[--runner <argv…> [--]] [--shell <argv…> [--]] [--eval <argv…> [--]] [--eval-quiet <dur>] [--sftp | --no-sftp] [--kernel-port <n>] [--no-kernel]
[--mount <path>[:<name>][:ro]]...
attach links a seven that is open in a running CDP browser and serves the link until it is interrupted (SIGINT, SIGTERM), or until stdin closes with --signal stdio. Status lines go to stderr, and SLICC_DEBUG=1 adds diagnostics, pion's included. pion never writes to stderr itself.
Flags.
--cdp: the browser's endpoint.http://host:portis looked up throughGET /json/version, andws://…is used as is. slicc talks to the browser WebSocket only and attaches to the tab withTarget.attachToTarget { flatten: true }. It names one browser; a second--cdpis an error. This browser is also listed for thecdpcapability, withseven: true.--cdp-extra: another browser seven may drive over the link (repeatable), in any formslicc browsers --cdptakes. Each must answer whenattachstarts.--origin: also accept seven on this origin (scheme://host[:port]), repeatable.https://<label>.sliccy.aiis always accepted, exceptwww.--signal:cdp(default) orstdio.--name: the link'snameandhostin HELLO. The default name isslicc on <host>, with this machine's short host name, lower-cased.--allow,--deny: policy rules on top of the local default (see Network policy). The--cdpand--cdp-extraports and endpoint addresses are denied.--runner <argv…>: offer compute over SSH (capssh, see SSH server). Each command runs as<argv…> <command>. The argv is every word after--runnerup to a lone--or the end of the command line, so--runner bash -cor--runner docker exec -i box sh -c -- --name box. An argv can't contain a lone--itself.--runner=…is refused. At start, slicc resolves the argv's first word as exec does (onPATH, or a path to an executable; a relative path is made absolute, since commands run in the home directory) and exits 2 before linking when it doesn't:slicc: --runner <x>: program not found, or, when the word holds whitespace (--runner "bash -c"),slicc: --runner "bash -c": no program "bash -c"; did you mean --runner bash -c?. Without--runnerthe link has nosshcapability, and the flags below are refused.--shell <argv…>: the interactive form forsshwithout a command, in the same argv syntax. The default is derived from the runner.--eval <argv…>and--eval-quiet <duration>: the REPL behind theslicc-evalsubsystem (for example--eval python3 -i), and the silence that ends one response (default500ms).--sftp,--no-sftp: serve thesftpsubsystem or not. The default is on only for a plain local shell as the runner.--mount <path>[:<name>][:ro]: share this folder with seven's kernel (capfs, repeatable); see Host folders.--kernel-port: the local port for kernel services (1–65535, default80).--no-kernel: no kernel services; nothing listens.--no-jerk: don't offer this machine's slicc-jerk (capsjerkandjerk-seal, seejerk).
Setup over CDP.
- slicc picks the first page tab whose URL has an allowed origin. Until there is one it says
waiting for a seven taband watches for new tabs. - It attaches, grants the tab's origin
local-network-access,local-networkandloopback-networkwithBrowser.setPermission(best effort), callsRuntime.addBinding { name: "sliccLinkSend" }, thenRuntime.enable. - For each new default execution context of the main frame whose origin is allowed, slicc mints a nonce (16 random bytes, base64url). It then calls
globalThis.sliccLinkReceive({ "t": "hello", "v": 1, "nonce", "node" })in that context.nodeis a random id persliccprocess (16 random bytes, base64url), the same in every hello and HELLO of that process, so seven can tell a secondsliccon the same browser (aslicc prompt --cdp …next to the bridge) from a restart of the same one. The call is retried every 100 ms to 1 s untilsliccLinkReceiveis a function. - seven answers through
sliccLinkSend(JSON.stringify(message)):{ "t": "offer", "nonce", "sdp" };{ "t": "candidate", "nonce", "candidate" }, wherecandidateisRTCIceCandidate.toJSON(), ornullat the end of gathering;{ "t": "bye", "nonce", "reason" }.
- slicc sends
answer, its owncandidates and, when it exits,byein the same shapes throughglobalThis.sliccLinkReceive?.(message), in the same context.
What setup logs. Status lines and SLICC_DEBUG=1 diagnostics name only allowed origins. A context on another origin (an iframe, an extension) is logged as ignored a context on another origin, and a seven tab that navigates to another origin as the seven tab left seven, without the new origin.
Rules for incoming setup messages.
- A binding call counts only when it comes from that main-frame context and carries its nonce. Calls from iframes, workers, other contexts and other origins, and stale nonces, are ignored.
- Each offer starts a new link and replaces the current one.
- A navigation or reload ends the link, and the next allowed context starts the setup again with a new nonce.
- A closed tab ends the link, and slicc waits for another seven tab.
- A tab that closes while slicc attaches to it doesn't stop slicc: each setup call has 10 s, and when the tab is gone slicc looks for another seven tab.
Setup over stdio (--signal stdio), for a test harness that owns CDP itself:
- The same messages go as NDJSON: slicc writes
hello,answer,candidateandbyelines to stdout, and readsoffer,candidateandbyelines from stdin. Messages with another nonce are ignored. - The harness writes
{"t":"hello"}to stdin to get a freshhellowith a new nonce, for example after it reloaded the page. --cdpis optional here. When given, it names the debugging port to deny and theseven: truebrowser forcdp.
ICE.
- No STUN or TURN, so only host candidates are used, loopback included.
- Chrome's
<uuid>.localcandidates are resolved with mDNS in query-only mode. - The link is declared failed after 15 s without connectivity.
Linking seven in your everyday browser
slicc open [--page <url>] [--origin <origin>]... [--name <name>] [--allow <rule>]... [--deny <rule>]...
[--kernel-port <n>] [--no-kernel] [--no-jerk] [--mount <path>[:<name>][:ro]]...
[--runner <argv…> [--]] [--shell <argv…> [--]] [--eval <argv…> [--]] [--eval-quiet <dur>] [--sftp | --no-sftp]
slicc pair [the same flags]
slicc --default-browser [the same flags]
slicc unpair [<fingerprint> | --all]
Link spec §1.3. These commands link a seven that runs in the browser you use every day, any vendor's, without CDP or remote debugging, as slicc-node could. Only the setup is new: once the peer connection is up, the link is the same as for slicc attach (see Link).
slicc open, andslicc --default-browser(the same command), print the pairing URL on stdout and open it in the default browser:openon macOS,xdg-openon Linux, andrundll32 url.dll,FileProtocolHandleron Windows. If that fails, slicc sayscould not open the default browser (…); open the URL above yourselfand goes on.slicc paironly prints the pairing URL, for a seven that's already open. Paste it into seven's address bar, or into its Link this computer field.- Plain
sliccfalls back to this mode by itself when it finds no SLICC profile and no CDP endpoint (see The default). - Both serve links until
SIGINTorSIGTERM, then exit 0. A listener that can't bind, or a missing config directory, exits 1, and bad flags exit 2. Status lines go to stderr, andSLICC_DEBUG=1adds diagnostics. - Flags are as for
slicc attach:--origin,--name,--allow,--deny,--kernel-port,--no-kernel,--no-jerk,--mountand the compute flags.--page <url>is the seven page to pair (defaulthttps://seven.sliccy.ai/), and its origin is accepted as a seven origin.--default-browsertakes these flags too. The flags ofsliccthat pick or launch browsers (--browser,--cdp,--profile,--headless,--yes,--plain,--json) are errors here. A flag of SLICC six's points back to six, as forslicc.
Identity. The node's DTLS certificate (ECDSA P-256, valid for 20 years) lives in <config>/slicc/loopback-identity.json (mode 0600, directory 0700). The same file holds the port the node last listened on. It is minted on first use, and slicc says new loopback identity for this computer: <name> (ab12…ef), fingerprint sha-256 AB:12:…, or this computer: … on later runs. It is separate from the tray hub's certificates and survives restarts and updates. A file that exists but can't be read as an identity (another version, broken JSON or PEM) is left alone: slicc says <path> is not a loopback identity slicc can read; … and exits 1. Only a certificate within a day of expiring is replaced. Whenever slicc mints a new identity (none yet, or a replacement), it forgets every pin in loopback-peers.json right after writing it, since every seven has to pair with the new certificate anyway. A loopback-peers.json that can't be read is left alone the same way: pairings and slicc unpair fail with <path> is not a list of paired sevens slicc can read; …, and signals are refused. <name> is --name, or this machine's short host name in lower case.
The pairing URL is <page>#slicc-pair=<port>.<token>.<node-fp>:
<port>is the listener's port;<token>is 128 random bits in base64url (22 characters). It is good once, for 10 minutes;<node-fp>is the SHA-256 fingerprint of the node's certificate, in lower-case hex without colons.
The URL is the secret, and it carries the node's fingerprint, so a process that squats on the port can't pass for the node. slicc prints a new pairing URL each time a line arrives on stdin, so press Enter for a new one. When the newest URL expires unused, slicc says the pairing URL expired; press Enter for a new one. Older URLs stay good until their own 10 minutes are up. Pairing URLs only go to stdout; telemetry and logs never get them.
The listener binds 127.0.0.1:<port>, plus [::1]:<port> when this machine has IPv6 loopback, and never 0.0.0.0. The port is the one it used last time if that's still free, otherwise the first port in 17117–17126 where both addresses bind. The kernel port (--kernel-port, unless --no-kernel) is skipped. With none free, it says no free port in 17117-17126 on 127.0.0.1 and exits 1. It serves exactly two requests:
| request | body | answer |
|---|---|---|
POST /slicc/v1/pair |
{ "token", "offer": "<sdp>" } |
200 { "answer": "<sdp>", "node": "<per-process id>", "name": "<name>" } |
POST /slicc/v1/signal |
{ "offer": "<sdp>" } |
the same |
The gate, on every request, in this order. Anything that fails it gets 403 with an empty body:
Hostis127.0.0.1:<port>,[::1]:<port>orlocalhost:<port>, ignoring case, so DNS rebinding can't reach it.- The request target is exactly
/slicc/v1/pairor/slicc/v1/signal: no query, no trailing slash, no absolute form. - There is exactly one
Origin, and it is a seven origin under the same rule as CDP setup (browser.IsSevenOrigin):https://<label>.sliccy.ai, unless slicc-browsers' catalog lists the label inseven.excludedLabelsor it starts with an entry ofseven.excludedLabelPrefixes. The--pageorigin and each--origincount as seven origins too.nulland missing origins fail. - The method is
OPTIONS(the preflight) orPOST. - A
POSTbody is a JSON object of at most 64 KiB (65 536 bytes), and itsofferholds exactly one SHA-256 fingerprint (one value, at the session or media level).
- Preflight: an
OPTIONSthat passes 1–3, asks forAccess-Control-Request-Method: POST, and asks for no request header other thanContent-Typegets204withAccess-Control-Allow-Origin: <that origin>(never*),Access-Control-Allow-Methods: POST,Access-Control-Allow-Headers: Content-Type,Access-Control-Allow-Private-Network: true,Access-Control-Max-Age: 600andVary: Origin. It never sendsAccess-Control-Allow-Credentials. - CORS on refusals: a refusal after step 3 (a wrong method, body, token or pin) carries
Access-Control-Allow-OriginandVary: Originas well, so seven can tell a403from a closed port. Earlier refusals carry no CORS headers. Every answer hasCache-Control: no-store. Origincan be forged by a process that isn't a browser, so the gate only filters browsers. Trust comes from the token or the pinned fingerprint.
Pairing.
- Rate limit: at most 10 pairing attempts a minute pass the gate. Past that,
/pairis refused before the body is read, and slicc logsrefused a pairing: more than 10 a minute(debug). Signals don't count. - The token: the node checks it in constant time. A wrong, used or expired token burns nothing, still counts toward the limit, and gets
refused a pairing request from <origin>: the token is wrong, used or expired. A matching token is burned at once, before the answer, even when the answer then fails. - The pin: the node pins the offer's fingerprint with the request's
Originin<config>/slicc/loopback-peers.json(mode0600), as{ "version": 1, "peers": { "<page-fp>": { "origin", "node", "firstSeen" } } }, wherenodeis the fingerprint of the node identity it was paired with. A new pairing of the same fingerprint replaces the entry. slicc sayspaired with seven on <origin> (ab12…ef). Every change to the file, from this process or fromslicc unpair, holds an exclusive OS file lock (flock, orLockFileExon Windows) onloopback-peers.json.locknext to it for its read and write. Concurrent changes never overwrite each other, and since the OS releases the lock when a process exits, a crash leaves no stale lock. A change that can't get the lock within 5 s fails withthe loopback state is being changed by another slicc; try again. The identity file is created underloopback-identity.json.lockthe same way, so two first runs at once end up with one certificate. - Signals:
/signalis answered only for a pinned fingerprint whose origin equals the request'sOrigin(both normalized) and whosenodeis this node's current identity, so a pin that anothersliccwrote under an older identity never counts. Asliccwhose identity another one has since replaced can't change the pins either: its pairings and unlinks fail withthis slicc's loopback identity was replaced by another slicc; restart it. - The answer: non-trickled. The node answers through its link stack, waits until ICE gathering is complete (10 s at most, after which it refuses), and returns its SDP with every host candidate and
a=end-of-candidates. The page checks that the answer's fingerprint is the one it pinned before it uses the answer.
The link in this mode is local (HELLO mode: "local"), with the node's loopback certificate:
- Caps:
net,http,credentialed,ws,unlink-loopback,fswith--mount,sshwith--runner,jerkandjerk-sealunless--no-jerk, and kernel services on--kernel-portfor a page that offerskernel-in(see Kernel services). There is nocdp, since the node controls no browser, so acdpOPEN getsEOPNOTSUPP.jerk's--seven-originis the pinned origin. - Policy: the local default (see Network policy). The protected ports are 9222–9229, every port in a
DevToolsActivePortfile that browser discovery would read (default user-data dirs, SLICC profiles and six's profiles; files are only read, nothing is probed), the listener's port and the kernel port. Those files are read again every 5 s, and a new port is protected from then on as these are: on loopback and on every address of this machine. It isn't added to HELLO'spolicy.deny, which was sent already. So a kernel program can't reach a debugging port the user turned on. - ICE: host candidates, loopback included, as for
slicc attach. - Several sevens: the node keeps one link per paired page fingerprint. A new offer for a fingerprint replaces that fingerprint's link only. Each link is an exit of its own, and kernel connections go to the page that linked last, as for
slicc attach. - Restarts: the node keeps its certificate and its port, and the page keeps its certificate and its pin. A restarted
slicc openorslicc pairis reachable again at once, and a paired page signals again without a new token. Each process has a newnodeid, so seven keys the device on the fingerprint.
Unlinking.
- From seven: on a live link, seven opens
OPEN { "kind": "unlink" }(capunlink-loopback). The node answers OPENED{}and END, deletes that page's pin, closes that link only, and saysunlinked by seven on <origin> (ab12…ef); it must pair again to link. It keeps running and keeps its certificate. - On the node:
slicc unpairlists the pins, one line each:<page-fp> <origin> paired <time>, orno paired sevens.slicc unpair <fingerprint>forgets one. A unique prefix works, ignoring colons and case. It saysforgot ab12…ef (<origin>), and exits 1 when nothing matches or more than one pin does.slicc unpair --allforgets every pin (forgot <n> paired sevens). A link that is up stays up until it drops, and a forgotten page gets403on its next signal. Once a link is down and its page is no longer pinned, the node lets go of everything it kept for that page.
The page side, as the test harness implements it (test/integration/testdata/link/loopback.js, the reference for seven):
RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256" })once per origin, kept in IndexedDB (slicc-loopback, storekeys, keycertificate) and passed ascertificatesto every loopbackRTCPeerConnection.- On load, the page reads
#slicc-pair=<port>.<token>.<node-fp>, strips it withhistory.replaceState, and asks the user to confirm. Only the confirmation sends the first loopback request. - It creates the
slicc.link.v1channel, sets the local offer, and waits for ICE gathering to complete. - It posts
{ token, offer }tohttp://127.0.0.1:<port>/slicc/v1/pairwithContent-Type: application/json,mode: "cors"andcredentials: "omit". - It accepts the answer only if its SHA-256 fingerprint is
<node-fp>; otherwise it refuses the link (a squatter). Then it pins{ fingerprint: <node-fp>, port, name }. - To link again, it posts
{ offer }to/slicc/v1/signal, first on the remembered port, then on 17117–17126, and accepts only an answer with the pinned fingerprint. After a link loss it retries with backoff, from 1 s up to 30 s. - To unlink, it opens
unlinkon the live link when HELLO listsunlink-loopback, then forgets its pin.
Link
One WebRTC peer connection. seven offers and the node answers. The node:
- Control channel: expects
slicc.link.v1from the page. It sends its HELLO as the first (text) message, and closes the link when the peer's first message isn't a valid HELLO withrole: "page"andv≥ 1, when no HELLO arrives within 10 s, or on any text message after HELLO. - Streams: accepts streams on their own
slicc.link.v1.streamchannel or on the control channel. A stream lives on the channel its OPEN came on, and frames for that id on other channels are ignored. - Its own streams: opens them on a channel of their own, or on the control channel once it has 256 stream channels open. It closes a stream's channel once both sides sent END, or after a RESET. A stream whose channel closes early is reset locally with
ECONNRESET. - Reading: reads every channel all the time and buffers up to the window. Stream frames that arrive before the peer's HELLO are held until it arrives. The SCTP receive buffer is 4 MiB.
- Frames:
type u8 | stream id u32 BE | payload. OPEN, OPENED, DATA, END, RESET (ERRNOorERRNO: message) and CREDIT (u32bytes consumed). DATA carries at most 65 536 bytes. - Credit: at most 256 KiB of uncredited DATA per stream and direction. The node credits bytes once it has written them on, for
tcponce the socket write returned. - Resets the stream with
EPROTO: DATA or END before OPENED or after the peer's END, empty DATA, DATA past the window, CREDIT of 0 or past what was sent, an unexpected OPENED, or an OPEN without a JSONkind. - Resets with
EOPNOTSUPP: an OPEN for a kind it doesn't serve. - Closes the link: an unknown frame type, a short or malformed frame (END with a payload, CREDIT that isn't 4 bytes), stream id 0, a reused id, an id with the wrong parity (page streams are odd, node streams even), or a second OPEN on a stream channel.
HELLO from attach: { "v": 1, "role": "node", "name", "host", "version", "mode": "local", "node", "caps": ["net", "http", "credentialed", "ws", "cdp"], "routes": { "prefixes": [], "exit": true }, "policy": { "allow", "deny", "summary" }, "browsers": [ … ] }. cdp and browsers are there when at least one browser is listed (see cdp). With --runner, caps adds "ssh" and HELLO adds "sshHostKey": "ssh-ed25519 AAAA…", the host key in authorized_keys form, so the page can write <host>.slicc.internal <key> into the kernel's known_hosts. With --mount, caps adds "fs" and HELLO adds "mounts" (see Host folders). With host jerk, caps adds "jerk" and "jerk-seal" and HELLO adds "jerk": { "version" } (see jerk).
ssh (cap ssh), page → node, with --runner only:
- OPEN
{ "kind": "ssh" }, OPENED{}at once. The stream's bytes are one SSH connection to the node's SSH server. - The node ends the stream with END when the SSH connection ends. If the page hasn't sent END 10 s later, the node resets it with
ECONNRESET. - Without
--runnerthe OPEN is reset withEOPNOTSUPP.
tcp (cap net), page → node:
- OPEN
{ "kind": "tcp", "host", "port" }.hostis an IP literal or a name for the system resolver. The dial goes through the policy and gives up after 30 s. - OPENED
{ "localAddr", "remoteAddr" }. - END half-closes the socket (
CloseWrite), and the socket's EOF becomes END. - RESET codes:
ECONNREFUSED,ENETUNREACH,EHOSTUNREACH,ETIMEDOUT,ECONNRESET(also for a socket error mid-stream),EACCESfor a policy refusal,ENOTFOUNDfor a name that doesn't resolve, andEPROTOfor an OPEN without a host or with a port outside 1–65535.
resolve (cap net), page → node, usually on the control channel:
- OPEN
{ "kind": "resolve", "name", "family" }, wherefamilyis4,6or0(or missing) for both. - OPENED
{ "addresses": [...], "ttl": 30 }, then END from the node. The page sends END too. - A name goes to the system resolver. An IP literal (
[::1]too) is answered as itself, and::ffff:a.b.c.dasa.b.c.d. Addresses are deduplicated, and those of the other family are left out. - Addresses the policy refuses on every port are dropped (see Network policy).
addresses: []means "not mine": the name doesn't exist (NXDOMAIN, or any other failure but a timeout), has no address of that family, or every address was dropped.- RESET codes:
ETIMEDOUTwhen the lookup times out (30 s at most), andEPROTOfor an OPEN without a name or with anotherfamily.
http (cap http), page → node. One HTTP request, fetched by the node without CORS, carried on a stream:
- OPEN
{ "kind": "http" }, answered OPENED{}at once. - The page sends a
u32BE length, that many bytes of UTF-8 JSON{ "url", "method", "headers": [[name, value], …] }(at most 1 MiB), then the request body, then END. - The node sends a
u32BE length, the JSON{ "status", "statusText", "headers": [[name, value], …], "url" }, then the body, then END.urlis the request's. - Request head:
urlmust behttp:orhttps:without credentials, andmethodan HTTP token other thanCONNECT,TRACEandTRACK.DELETE,GET,HEAD,OPTIONS,POSTandPUTare upper-cased in any case. Header names must be tokens, and values Latin-1 without control characters other than tab once leading and trailing whitespace is trimmed; each character is sent as one byte. - Request headers: dropped are the hop-by-hop headers (
Connection, the fieldsConnectionnames,Keep-Alive,Proxy-Connection,TE,Trailer,Transfer-Encoding,Upgrade), the ones the node owns (Host,Content-Length,Accept-Encoding,Expect), andProxy-Authorization. Repeated names are joined with,, exceptCookie, which joins with;. The node addsAccept-Encoding: gzip, deflate, br, or nothing when the request hasRangeorIf-Range, so ranged bodies stay unencoded. It adds noUser-Agent. - Request body: streamed under the window, with no size cap, and not sent for
GETandHEAD(it is read and dropped). A body that ends within its first 1 MiB goes with its exactContent-Length. A longer one goes with the head'sContent-Lengthwhen that is one valid number, otherwise chunked (HTTP/1.1) or as an unsized HTTP/2 body. - Upstream: a client that never follows redirects and never uses a proxy from the environment. It dials through the policy (connect timeout 30 s), verifies TLS with the system roots, and speaks HTTP/1.1 or HTTP/2. A body has no overall timeout.
- Response headers: in name order, lower-cased, repeated names joined with
,, then eachSet-Cookieas its own entry. Hop-by-hop headers and the fieldsConnectionnames are dropped. Values are read as Latin-1, one character per byte. - Decoding:
gzip,x-gzip,deflate(zlib or raw) andbr, stacked in any order and undone from the last listed, withidentityignored. When every listed coding was undone,Content-EncodingandContent-Lengthare dropped; a list with any other coding is passed on as it came, with both headers.Content-Encoding: identityalone is dropped too. A response without a body (HEAD,1xx,204,205,304) keeps both headers and has no bytes after the head. - RESET before the response head:
EPROTOfor a malformed head or one of the refusals above, and for a206with a coding the node would decode, since itsContent-Rangewouldn't match. A failed fetch is<code>: fetch failed: <reason>, withtcp's codes:EACCESfor a policy refusal,ENOTFOUNDfor a name that doesn't resolve,ETIMEDOUT,ECONNRESET, andECONNREFUSEDfor anything else, TLS failures included. - After the head: an upstream error or a broken coding mid-body is
RESET ECONNRESET.
credentialed (node cap, no kind of its own), on every node that serves http. An http OPEN with "credentialed": true carries a credential the vault injected for a transit exit (link spec §5.13). For such a stream the node:
- refuses a URL that isn't
https:on port 443 withRESET EACCES: credentialed requests go only to https on port 443, before anything is dialled; - logs the request only as its method, scheme and host, as for every
httpstream, and never its path, query, headers or body,SLICC_DEBUGincluded; - scrubs RESET messages: they carry the error's reason, never the URL's path or query, a header or a body byte;
- follows no redirects and keeps nothing: no cache, no cookies, nothing on disk.
Any value of credentialed other than false or null counts as true, so a malformed flag never sends a credential out as an ordinary request.
ws (cap ws), page → node. A WebSocket for a kernel program (link spec §5.15), which seven never opens as a browser WebSocket, because that would carry the user's cookies:
- OPEN
{ "kind": "ws", "url", "protocols": [ … ], "headers": [[name, value], …] }.urlisws:orwss:without credentials (a fragment is dropped).protocols(optional) are tokens, none listed twice, and go out asSec-WebSocket-Protocol.headers(optional) followhttp's rules for names, values (Latin-1, one byte per character), joining and the hop-by-hop drop. The node also dropsHost,Accept-Encodingand everySec-WebSocket-*header the handshake owns (Key,Version,Extensions,Protocol), and sends the rest,Origin,AuthorizationandCookieincluded. It offers no extensions (no permessage-deflate) and adds noUser-Agent. Anything else isRESET EPROTO. - Dial: through the policy, so the browsers' debugging ports and the node's own ports are
EACCES, with the system TLS roots, no proxy, HTTP/1.1, and 30 s for the connection and handshake. Redirects aren't followed. - OPENED
{ "protocol" }, the subprotocol the far end chose, or"". - RESET instead of OPENED:
EACCES: <reason>for a policy refusal,ECONNREFUSED: HTTP <status>for any answer but101(a redirect included),ECONNREFUSED: x509: …for a certificate the system doesn't trust, andECONNREFUSED: <reason>when the dial or the handshake fails otherwise (a subprotocol or an extension that wasn't offered included). - Bytes, both ways: records of a
u8type, au32BE length, then the payload, independent of DATA boundaries.1is a text message (UTF-8),2a binary one,8a close: au16code, then a UTF-8 reason of at most 123 bytes, or an empty payload for 1005. Each record is one whole message. The node reassembles the far end's fragments before it sends a record, and passes a page record on as one message while its bytes arrive, in as many frames as that takes. - Limits: a message is at most 64 MiB. A larger one from the far end, or a record header from the page announcing one, makes the node close the WebSocket with 1009. A record of another type, or a close record that is 1 byte long, has a code outside 1000–1003, 1007–1014 and 3000–4999, or a reason over 123 bytes or not UTF-8, makes it close with 1002. In both cases the node sends the page a close record with that code, then END, and ignores what the page sends after.
- Pings: the node answers the far end's pings itself and never relays them.
- Page closes first: the page sends a close record, then END. The node sends that close to the far end and relays the far end's answering close as a close record, then END. Without an answer within 5 s, it drops the connection and sends END alone.
- Far end closes first: the node answers the close with the same code, relays it as a close record, then END. The page's close record that follows is ignored.
- No clean close: END alone. The node sends it when the far end's connection drops, and drops the far end's connection when the page sends END without a close record, or inside a record. A RESET from either side drops it too.
- Flow: the stream window, nothing more. The node reads the far end one message at a time, and holds a message until the page's window takes it. It holds nothing of a page message but the window, so a record header alone reserves nothing.
- Privacy: the node logs a stream only as
ws <scheme>://<host><path>, without the query, and never a handshake header, a message, or a close reason,SLICC_DEBUGincluded.wsstreams are nevercredentialed.
kernel (page cap kernel-in), node → page:
- OPEN
{ "kind": "kernel", "port" }, sent only to a page whose HELLO listskernel-in. The page dials that kernel port and answers OPENED or RESET (ECONNREFUSEDwhen nothing listens). See Kernel services. - Then raw bytes both ways: the node's first DATA is everything it has read from the browser's connection so far, request head included. END half-closes, as for
tcp.
jerk: slicc-jerk over the link
Link spec §5.14 (J1). slicc-jerk (jerk, Companion tools) gets tokens on this machine. Over the link seven can ask it for status, start sign-ins a human completes, and seal a lease into its vault. No token crosses the link except in seal's lease event, which only seven's link layer reads and hands to the vault.
Offered when host jerk is found: of the first jerk on PATH and jerk in the data folder's bin, in that order, the first whose jerk --version names 0.6.0 or later. 0.6.0 is the first slicc-jerk with the --via-link calls below. An older jerk counts as absent: slicc says jerk: host jerk <version> is older than 0.6.0; run slicc install jerk, and HELLO has neither cap.
sliccandslicc attach: capsjerkandjerk-seal, unless--no-jerk.follow: only with--jerk(capjerk) or--jerk-seal(both).prompt,watchandabort: never.- slicc says
jerk: offering slicc-jerk <version> (jerk, jerk-seal), and HELLO adds"jerk": { "version" }. Without the cap an OPEN getsEOPNOTSUPP.
OPEN { "kind": "jerk", "op", "provider"?, "scope"?, "lease_id"?, "duration"?, "params"? }. The page sends END right after it. slicc runs jerk as the user, with no terminal and its stderr dropped:
| op | cap | runs |
|---|---|---|
list |
jerk |
jerk list --json |
status |
jerk |
jerk <provider> get --json (never --reveal) |
accounts |
jerk |
jerk <provider> list --json |
create |
jerk |
jerk <provider> create --via-link [--seven-origin <origin>] [--scope S] [-p k=v]… |
seal |
jerk-seal |
FNOX_LEASE_DURATION=<duration> jerk <provider> lease --via-link (duration 1 to 86 400 whole seconds, default 3600) |
unseal |
jerk-seal |
FNOX_LEASE_ID=<lease_id> jerk <provider> drop --lease --via-link |
--seven-originis the origin of the seven tab slicc set up with over CDP. A link set up over stdio or the tray hub has none, andcreateruns without it.- OPENED
{}once jerk has started. Then NDJSON events, then END:{ "event": "progress", "message" }forcreate, from each{"event":"progress","message"}line jerk writes;{ "event": "lease", "credentials", "expires_at", "lease_id" }, once, forseal, only when jerk exited 0 with string credentials and an RFC 3339expires_at; otherwise the result is exit 1,jerk gave no lease with an expiry;- exactly one
{ "event": "result", "exit", "provider"?, "user"?, "scopes"?, "expires_at"?, "env"?, "message", "items"? }.itemsholds the rows oflistandaccounts. Results and rows keep only these status fields:provider,name,source,requires,cli,user,scopes,expires_at,envandid, as strings, booleans or string lists. Anything else jerk prints (a masked token, say) is dropped.
- jerk's exits 0 to 5 are results. slicc never logs a lease or a result's contents, only
jerk: <op> <provider>: exit <n>.
RESETs:
EPROTO: an unknown op, a missing or malformedprovider(^[a-z0-9][a-z0-9-]{0,63}$) orlease_id, adurationout of range, ascopethat isn't printable words, or aparamwhose key isn't^[a-z][a-z0-9_]{0,31}$or whose value isn't printable (at most 16, 256 bytes each). Host jerk checks the keys against the provider's ownparamsand exits 2 for unknown ones.EACCES:drop(there's no such op;unsealdrops what jerk minted), andsealorunsealwithoutjerk-seal.EBUSY: a secondcreateon a link while one runs.ETIMEDOUT: acreatestill running after 2 min 30 s, which is then killed. Other ops get 60 s and then answer exit 1,jerk timed out.ECONNRESET: jerk couldn't start, died of a signal, or exited with a code above 5.
A page RESET or a closed link kills the jerk it started.
cdp (browser control)
Warning: a seven with the
cdpcapability has full control of every listed browser: its tabs, cookies, stored passwords and logged-in sessions, files it can download, and anything a page in it can reach. That includes the browser seven runs in (seven: true), whether slicc launched it or not: withsliccorslicc attachon your everyday browser, seven can drive all of that browser, without a further prompt. List only browsers you would let that seven use as you. Forfollowthat is a seven on another machine. The one exception is seven's own origin, which the node keeps out of reach (see below).
Only browsers the user named are listed: the one seven runs in, --cdp and --cdp-extra for attach, the extras picked with --browser or in the picker for slicc, and --cdp for follow. A browser discovery found but nobody picked is not.
HELLO lists the browsers: "browsers": [{ "id": "chrome", "name": "Chrome (headless)", "version": "154.0.8037.98", "seven": true }]. Every entry has all four fields.
idis the browser kind, then-2,-3… for a second or third of the same kind. The kind comes from/json/version:electronfor an Electron user agent, else by productchrome(alsoHeadlessChrome, and browsers such as Brave that reportChrome),edge(Edg),opera(OPR), else the product lower-cased, elsechromium.- An id keeps its meaning for the life of the node. A removed id is never given to another browser, and the same endpoint (CDP host and port) added again gets its old id back.
nameandversioncome from the endpoint's/json/version, as inslicc browsers.seven: truemarks the browser that hosts this seven tab (attach --cdp).
OPEN { "kind": "cdp", "browser": "<id>" }, page → node. For each stream the node reads the browser's /json/version again and opens a fresh WebSocket to its webSocketDebuggerUrl, so each stream is its own CDP client with its own sessions. A wsOnly browser is dialled at its websocket URL directly. Dial and handshake together get 10 s.
- OPENED:
{ "product", "protocolVersion" }, theBrowserandProtocol-Versionfields of/json/version(fromBrowser.getVersionfor awsOnlybrowser). - RESET:
ENOENTfor an id that isn't listed,ECONNREFUSEDwhen the browser doesn't answer,EPROTOfor an OPEN withoutbrowser.
Bytes, both directions: a sequence of messages, each a u32 BE length plus one UTF-8 CDP JSON message of at most 64 MiB (67 108 864 bytes). The node sends each page message as one WebSocket text message, and each text message from the browser as one stream message, after the filter below. A length over 64 MiB from the page, or a browser message over 64 MiB, is RESET EMSGSIZE, and a page END in the middle of a message is RESET EPROTO. Browser messages that start with {"method":"Network.webSocketFrameReceived" or {"method":"Network.webSocketFrameSent" are dropped: seven runs inside a browser it may debug, so those events would carry the CDP traffic itself and grow without bound.
Closing.
- The browser closes the socket with a normal close (1000): the node sends END, then discards whatever the page still sends until its END.
- The browser socket fails or closes any other way:
RESET ECONNRESET. - END from the page: the node closes the socket with a normal close, then sends END.
- RESET from the page, or the link going down: the socket is closed at once.
Seven's origin is off limits. The node relays CDP through a filter, so seven's browser control never navigates to, opens or attaches to a target on seven's own origin, and never reads or writes its cookies or storage. That holds for every listed browser, not only the one seven runs in. What is protected comes from slicc-browsers' protectedOrigins: every sliccy.ai host (the apex and www included, any port, https and http), vault.sliccy.ai by name (the vault is never a CDP target for seven's tools), blob:, filesystem: and view-source: URLs by the URL inside them, devtools: URLs, and the extra origins: --origin for attach, and --origin and the --page origin for slicc. follow has none. *.kernel.localhost is not protected. URLs are parsed the way the browser parses them (WHATWG), and the node forwards its own re-encoding of each command, so the browser runs exactly what was checked: a key given twice counts once, as its last value, and a message that isn't a JSON object with an integer id and a string method is refused (or dropped, without an id).
- Targets. Before the stream opens, the node attaches its own session to the browser target and calls
Target.setDiscoverTargetsandTarget.getTargetson it with ids of its own. Neither the session nor its messages ever reach the page. From then on it knows every target's URL and opener. A target on an inheriting URL (about:blank,about:srcdoc, none) is protected when it was protected before or when its opener is protected or unknown, so a blank popup that seven opens counts as seven. On a page session, the main frame'ssecurityOrigin(Page.frameNavigated) and the default context'sorigin(Runtime.executionContextCreated) count too. - Refused, with
{"id":…,"sessionId"?:…,"error":{"code":-32000,"message":"slicc: refused: target is on seven's origin"}}, never forwarded:Target.createTarget,Page.navigateand anything else whoseurlis protected;Page.navigateToHistoryEntryto an entry whose URL is protected (the node looks it up withPage.getNavigationHistory);Target.attachToTarget,activateTarget,closeTarget,autoAttachRelated,Browser.getWindowForTargetand any other command whosetargetIdorframeIdis a protected target; and every command on a session whose target is protected, or became so.Target.getTargetInfoandTarget.detachFromTargetstay allowed. - Origins and storage. A command whose
origin,securityOrigin,ownerOrigin,issuerOrigin,topLevelOrigin,topLevelSite,embeddedOrigin,requestingOrigin,scopeURLormanifestIdis protected, or whosestorageKeyorcacheIdnames seven's storage, is refused the same way. This coversStorage.*,IndexedDB.*,CacheStorage.*,DOMStorage.*,FileSystem.*,ServiceWorker.*,PWA.*,Browser.setPermissionandBrowser.grantPermissions. Free-form values (expression,arguments,value,headers,postDataand the like) aren't searched. - Cookies.
Network.getCookies,Network.getAllCookies,Storage.getCookiesandPage.getCookiesanswer without the cookies whose domain is protected (sliccy.aiand every subdomain, and each extra origin's host and its parents: cookies ignore ports), and without partitioned cookies whosepartitionKey.topLevelSiteis such a site.Network.setCookie,setCookies,deleteCookies,Storage.setCookiesandPage.deleteCookiefor such a domain, a URL on such a host, or a partition key under such a site, are refused withslicc: refused: cookies on seven's origin. So areNetwork.clearBrowserCookiesandStorage.clearCookies, except in a browser context this stream created withTarget.createBrowserContext. - Requests a page makes to seven. For a request whose URL is protected,
Network.*andFetch.*events lose theirCookieandSet-Cookieheaders, their raw header texts and their cookie lists, andrequestWillBeSentExtraInfodrops seven's cookies fromassociatedCookiesfor any request. Chrome can sendrequestWillBeSentExtraInfoandresponseReceivedExtraInfobefore the event that names the request's URL, so the node holds them until the request is found to be protected, gets its response, finishes or fails (a WebSocket or WebTransport once it is created); one still held after 2 seconds is passed on stripped, as for a protected request. For a WebSocket, WebTransport or EventSource on a protected origin (ws:andwss:count ashttp:andhttps:),webSocketWillSendHandshakeRequest,webSocketHandshakeResponseReceived,webSocketFrameError,eventSourceMessageReceivedanddataReceivedare dropped. Reading such a request's body (Network.getResponseBody,Fetch.getResponseBody,takeResponseBody…AsStream,Audits.getEncodedResponseand the like) is refused. The node also checks the answer, because the page may ask before the node has seen the request. WhenNetwork.getResponseBody,searchInResponseBody,streamResourceContent,getResponseBodyForInterception,takeResponseBodyForInterceptionAsStream,getRequestPostData,Fetch.getResponseBody,Fetch.takeResponseBodyAsStreamorAudits.getEncodedResponseanswers, the node holds the answer until the request has a response (forgetRequestPostData, until it is sent; for an intercepted request, until it is paused). If the request is protected by then, the answer is refused, and a stream it handed out is closed. If the node still hasn't seen the request after 2 seconds, the answer is refused withslicc: refused: slicc hasn't seen this request, so it could be seven's.Network.replayXHRis refused with that message too, unless the node has already seen the request; for a protected request it is refused like the body getters.IO.readis refused, withslicc: refused: slicc didn't hand out this stream, so it could be seven's, unless the node passed that handle to the page in an allowed answer (take…AsStream,Page.printToPDF,Network.loadNetworkResource, orIO.resolveBlobasblob:<uuid>). Continuing, failing or fulfilling it is allowed, except for a document request, which the node fails itself (see the next items). - Browser-wide commands, refused with their own reason after
slicc: refused::Target.exposeDevToolsProtocol(it would give a page the whole browser) andTarget.sendMessageToTarget(attach withflatten: trueinstead), on any target;Fetch.enableon the browser itself (a page session is fine);Tracing.start;Extensions.loadUnpacked; andBrowser.setPermission,Browser.grantPermissionswithout anoriginandBrowser.resetPermissions, except in a browser context this stream created. - Auto-attach. With
Target.setAutoAttach, a session to a protected target is detached by the node at once (Target.detachFromTargeton its parent session), and the page never sees itsattachedToTargetordetachedFromTarget. - Navigation by other means. A script's
location = …, a link,window.openor a server redirect isn't a command the node can refuse, so the node guards the network instead.- A target the stream attached to (
Target.attachToTarget, or auto-attach that reached the page) or created (Target.createTarget) is touched, for as long as the stream is open, even after the page detached. That includes every tab the browser opens while the stream auto-attaches to new tabs. - On each touched page, iframe, background page or webview target the node attaches a session of its own, which the page never sees. On it, the node enables
Fetchfor document requests to protected hosts (resourceType: Document, request stage), andNetworkwithNetwork.setBypassServiceWorker, so a service worker can't hand out a document without a request. - The node fails each such request into a protected origin with
Fetch.failRequest(BlockedByClient), and the target shows Chrome's error page instead. Every other request continues untouched. So a touched target never loads a protected document, and nothing the stream installed on it (Page.addScriptToEvaluateOnNewDocument,Fetch,Network,Page.setBypassCSP, emulation, overrides) ever runs there. - Seven's own tab and its vault frame are never touched, because attaching to them is refused.
- Page commands on a touched target's session wait until the node's own session there is ready. That takes at most 10 s; after that, or when the node's session can't be set up or is lost, they are refused with
slicc: refused: slicc can't keep seven's origin out of this target. - In touched targets, service workers are bypassed for every request, not only for protected ones.
- The guard covers every frame that shares the touched target's process. A cross-site iframe's navigation pauses in its parent's target, before the iframe becomes a target of its own, with the frame's id as
frameId(the id the iframe target gets later). A popup's request pauses in the popup's target, with itstargetIdasframeId. A redirect into a protected origin pauses again at the request stage, withredirectedRequestId, even when the first URL didn't match. - The page's own
Fetch.enableon a page session still works. Chrome chains the interceptors of a target's sessions, the most recently enabled first, so the page's interceptor could see a protected document's request before the node's does, or after the node failed it, at the response stage. The node fails such aFetch.requestPaused(resourceType: Document, protected URL, either stage) itself, and the page never sees it. - When a target commits a protected origin all the same (see the limits below), the node sees it arrive (
Target.targetInfoChanged) only after the browser committed it. A request the node failed also leaves the target on the protected URL, showing the error page. Either way the node detaches every page session on the target, drops their events and results, and refuses further commands on them; the page getsTarget.detachedFromTarget.
- A target the stream attached to (
- Auto-attach of targets without a URL. A cross-site iframe can attach before its URL is known (
url: "", which stays empty while it waits for the debugger), and so can awindow.openpopup. So when an auto-attached session (one that doesn't answer the page's ownTarget.attachToTarget) is to a page or iframe target with an empty URL, the node holds it:- The page doesn't get its
attachedToTargetyet, its events are dropped, and page commands on it are refused. - If the page asked for
waitForDebuggerOnStart, the node runsRuntime.runIfWaitingForDebuggeron it itself. - Once the target has a URL (
Target.targetInfoChanged), a protected target's session is detached without the page seeing it. Otherwise the page getsattachedToTargetwith the currenttargetInfoandwaitingForDebugger: false. The target has already started, so a script the page adds for new documents in such a frame or popup misses its first one. - A target that has no URL after 10 s is treated as protected. A session that the node does forward makes the target touched (see above).
- The page doesn't get its
- Listing.
Target.getTargets,Target.getTargetInfo,Target.targetCreatedandTarget.targetInfoChangedstill show protected targets, so seven knows its tab exists, but theirurlandtitleare only the origin (https://seven.sliccy.ai, orabout:blank). Service worker registrations and versions on a protected origin are left out ofServiceWorkerevents, andStorage,DOMStorageandBackgroundServiceevents that name a protected origin are dropped. - Limits.
- The filter relies on Chrome's site isolation. A seven frame inside a page of another site is a target of its own (
iframe), which the node protects. A page the stream never touched can still frame seven, though, and once the stream touches that page, a screenshot of it shows the frame. A same-site frame shares its parent's process and isn't a target. Everysliccy.aihost is protected, so this only matters for an extra origin, such as one port of127.0.0.1framed by a page on another. - A document that arrives without a request isn't failed: a page restored from the back/forward cache, and a
blob:orfilesystem:URL of a protected origin (which only that origin can make). The node detaches after the commit, as above, and commands that were already on their way may still have run in the new page. - If the node loses its own session (
ECONNRESET: slicc lost track of the browser's targets), it refuses every command and resets the stream unless the browser closes within 2 s. - The node's own setup connection (see Local setup) is not a
cdpstream and isn't filtered.
- The filter relies on Chrome's site isolation. A seven frame inside a page of another site is a target of its own (
List changes. The node checks every listed browser's debugging port every 2 s. One that fails twice in a row is removed: its open streams get RESET ECONNRESET: browser quit, and a later OPEN for it gets ENOENT. Once its port answers again it is listed again under the same id. A browser removed through the API (Registry.Remove, for the TUI) has its streams reset with ECONNRESET: browser removed.
browsers updates (page cap browsers), node → page. When the list changes while a link is up and the page's HELLO has browsers in caps, the node opens OPEN { "kind": "browsers", "browsers": [ … ] } on the control channel with the full current list in HELLO's shape. It replaces the previous list; there are no deltas. The page answers OPENED {} and END, and the node sends END. Updates are sent one at a time per link, a change that comes while one is in flight is sent after it, with the list as it is then, and an update the page doesn't finish within 10 s is reset with ETIMEDOUT. A change between the HELLO and the link coming up is sent as an update right away.
test/integration/testdata/link/link.js is a page-side implementation of the link (framing, credit, both channel layouts, the violation rules, and resolve and raw-fetch helpers) that the integration tests run in Chrome against slicc attach. cdp.js next to it is a CDP client over cdp streams, protect.js probes the filter that keeps seven's origin out of reach, fs.js an op-shaped client for fs streams (call, list, read, write, watch), loopback.js is the page side of loopback signalling, and page.js answers browsers and mounts updates. Through linkTest.loopback, page.js pairs from the fragment (confirm), signals again (signal, reconnect), unlinks in-band (unlink) and posts raw requests (post), with an optional ?ports=<first>-<last> scan range. page.js keeps one link per node id from the hello. For tray links (no hello node) it replaces an older link with the same fingerprint only when HELLO's node matches, as seven does. agent.js is a fake agent behind the page's agent cap: the tests queue scripted runs, emit transcript events, hold and release a run, reset watch streams, and turn the cap off. Loaded with ?kernel, the test page lists kernel-in and answers kernel OPENs with kernel.js, a small HTTP and WebSocket echo server in the page.
Kernel services
seven's kernel runs servers (vite, python -m http.server), and kernel port N is http://N.kernel.localhost/. seven's service worker carries subresources itself, but not top-level navigations or WebSockets. *.kernel.localhost resolves to loopback, so attach runs a listener that carries those connections into the page as kernel streams.
Listener.
- It binds
127.0.0.1:<kernel port>(default80, which macOS lets any user bind) when the first page whose HELLO listskernel-inlinks up, and keeps it untilattachexits. With--kernel-port 8080the URLs carry the port:http://5173.kernel.localhost:8080/.--no-kernelturns it off, and remote nodes never listen. - If the port can't be bound (something else uses it, or Linux refuses an unprivileged bind on
80),attachprintskernel services are off: cannot listen on 127.0.0.1:<port> (<reason>); try --kernel-portand runs on without it. It tries once. - The kernel port is denied in the network policy from startup, like the browser's debugging port, unless
--no-kernelis given.
Each connection.
- The node reads the first request head, at most 64 KiB and within 30 s. A connection that sends no complete head in time is closed without an answer.
- The request line must be
<method token> <target> HTTP/1.0orHTTP/1.1, and the head must hold exactly oneHost. - The
Hostmust be<1–65535>.kernel.localhost, in any case, with no port or the listener's own port, no leading zeros and no trailing dot. - Port
9222(seven's CDP facade) is refused here, before anything is sent to the page. - The node opens a
kernelstream to the page that linked most recently and is still linked. When that page goes, the one linked before it takes new connections. Open connections stay where they are. - It sends what it has read, then pipes bytes both ways with half-close, without parsing anything else. Keep-alive, chunked bodies and WebSocket upgrades pass through as they are, and a connection belongs to the host of its first request.
Errors are plain text (Content-Type: text/plain; charset=utf-8) with X-Proxy-Error: 1, Cache-Control: no-store and Connection: close:
| case | answer |
|---|---|
Host not <port>.kernel.localhost |
421 only <port>.kernel.localhost is served here |
malformed request line, or not exactly one Host |
400 malformed request head |
| head over 64 KiB | 431 request head too large |
port 9222 |
502 kernel port 9222: EACCES |
no page with kernel-in linked, or the link closes before OPENED |
502 no seven page connected |
the page answers RESET ECONNREFUSED |
502 nothing listening on kernel port N |
the page answers another RESET |
502 kernel port N: <reason> (printable ASCII, at most 200 characters) |
no OPENED within 10 s |
504 kernel port N did not answer |
The browser's connection waits unread for OPENED.
Security. The Host check defends against DNS rebinding. Kernel services are reachable like any dev server on localhost: by any local process, and by any web page Chrome lets reach loopback. Use --no-kernel to keep them inside the page.
Host folders
--mount <path>[:<name>][:ro] shares a folder of this machine with seven's kernel, which mounts it with its hostfs driver. It is repeatable and works on slicc, slicc attach and slicc <join-url> follow. Each operation is an fs link stream (below). There are no tokens: the link is the trust boundary, as for ssh, and seven decides which kernel mounts it creates. With follow, the folders go to the seven on the other machine.
Export.
- A trailing
:romakes the folder read-only. After that, the text after the last:is the name if it isn't empty and has no/or\, soC:\workis a path andC:\work:projis namedproj. The default name is the folder's own. - SLICC six's form,
<os-path>:<vfs-path>, is refused with exit 2 before anything starts:--mount <spec> is SLICC six's form, <os-path>:<vfs-path>. This is the new SLICC (seven): use --mount <path>[:<name>][:ro], such as --mount <path>:<name>, then mount the folder in seven., with the last component of the VFS path as the name. A spec counts as six's when, without:ro, it has no name and the text after its last:starts with/, unless the text before it is a drive letter (C:/work) or the whole spec is an existing folder. - The folder is resolved with
realpathat start. A missing path or a file is skipped with--mount <spec>: not an existing folder, skipping, a name of.or..withinvalid name, skipping, and a second folder under a name already used withthe name <name> is taken, skipping. - Names never reveal host paths, and neither does anything else on the link.
- Without a folder, the node doesn't advertise
fs, and anfsOPEN getsEOPNOTSUPP.
Private paths. Some folders are never shared, even inside a folder that is:
- slicc's own config, data and cache folders (
<config>/slicc, which holds the DTLS certificates, the SSH host key, the tray state and the vault pairing keys); - browser profiles: slicc's
seven-profilesandprofiles, SLICC six's profiles andSLICC_USER_DATA_DIR, and the user data folders of the browsers slicc knows, since the vault's storage lives there; - slicc-jerk's files:
$XDG_CONFIG_HOME/slicc-jerkand$XDG_STATE_HOME/slicc-jerk(~/.configand~/.local/statewhen unset, macOS included),/etc/slicc-jerk, andSLICC_JERK_CONFIG; and on Windowsslicc-jerkunder%APPDATA%,%LOCALAPPDATA%and%ProgramData%; - host fnox's files (upstream jdx/fnox's defaults):
FNOX_CONFIG_DIR,FNOX_STATE_DIRandFNOX_DATA_DIR($XDG_CONFIG_HOME/fnox,$XDG_STATE_HOME/fnoxand$XDG_DATA_HOME/fnox, else~/.config/fnox,~/.local/state/fnoxand~/.local/share/fnox, macOS included, or~\AppData\Local\fnoxon Windows),FNOX_PLUGINS_DIR(default<data>/plugins),FNOX_AGE_KEY_FILE, and, off Windows, the daemon's socket folders$XDG_RUNTIME_DIR/fnox,$TMPDIR/fnox-<euid>(on macOS withoutTMPDIR, the user temp folder thatgetconf DARWIN_USER_TEMP_DIRnames) and/tmp/fnox-<euid>.
The paths are read from slicc's own environment at start. A --mount of one of them, or of a folder inside one, is skipped with --mount <spec>: slicc never shares this folder, which holds its own keys or a credential tool's files, skipping. In a folder that contains one, every operation on a private path or below it is EACCES with slicc never shares this path. That holds when the path is reached through a symlink, through a Windows 8.3 short name, or, on macOS and Windows, typed with a different case. A rename, rmdir, unlink or symlink of a folder that holds a private path (.config, for example) is refused too, so a private folder can't be moved out of the fence or replaced. list still shows a private folder's name. watch drops a change inside a private folder before it is batched, so it adds no line, no parent folder and no all.
HELLO. caps lists fs, and "mounts" lists the folders:
"mounts": [{ "name": "project", "readonly": false,
"capabilities": { "maxIo": 16777216, "symlinks": true, "chmod": true,
"caseInsensitive": true, "normalization": "nfd-insensitive", "ranges": true } }]
capabilitiesis in the shape slicc-kernel'shostfsdriver takes, so seven hands it to the driver unchanged.symlinksandchmodarefalseon Windows andtrueelsewhere.caseInsensitiveis probed per folder on its own volume. The last component of the folder's path that has letters is looked up again with its case swapped, and the probe istruewhen that finds the same file. When no component has letters, it istrueon macOS and Windows.normalizationisnfd-insensitiveon macOS andnoneelsewhere.ranges: truesayssetattrtakessize, so the kernel reads and writes in pages.
Streams. Each operation is one stream, OPEN { "kind": "fs", "mount", "readonly"?, "op", …arguments }. An OPEN is one frame, so at most 64 KiB.
- The node answers OPENED (followed by DATA for
listandread), then END. The page sends END once it has OPENED. - An error is
RESET <ERRNO>: <message>, before OPENED or, forlist,read,writeandwatch, after it. readonly: trueon an OPEN makes that callEROFSif it writes, opens for writing or changes the namespace, even on a writable folder. A:rofolder is that way for every call.readandlistmay go on the control channel or on their own stream channel. Pages should give reads over 64 KiB their own channel.writeandwatchbelong on their own channel.
Paths are relative to the folder, /-separated, with "" (or a missing or null path) for the root. Empty and . components are ignored.
..and a leading/areEACCES, and NUL isEINVAL. On Windows\and:areEINVALtoo, which also rules out alternate data streams and drive letters.- Every operation resolves the parent directory with
realpath, and refuses it withEACCESwhen it lies outside the folder. - The last component gets lstat semantics. The kernel follows symlinks itself, so
statreports a symlink as one,liston a symlink isENOTDIR, andopenon a symlink isELOOP. - Operations that change the namespace run one at a time, and no other operation runs alongside them. The lock covers every link and every folder of the node, so no page can swap a directory for a symlink between the check and the use.
Operations, with their arguments and answers:
| op | arguments | answer |
|---|---|---|
stat |
path |
OPENED attr |
list |
path |
OPENED {}, then NDJSON lines {"name","attr"} as DATA; an entry that vanishes meanwhile is left out |
mkdir |
path |
{}; not recursive, mode 0777 minus the umask |
rmdir |
path |
{} |
unlink |
path |
{}; EISDIR for a directory |
rename |
from, to |
{}, rename(2); it replaces a file or an empty directory, a directory onto a non-empty one is ENOTEMPTY, a directory onto a file ENOTDIR, a file onto a directory EISDIR |
symlink |
target, path |
{}; target is stored as given; EPERM on Windows |
readlink |
path |
{ "target" }; EINVAL for anything but a symlink |
setattr |
path, size?, mode?, mtime? (ms) |
{}; size truncates or extends with zeros, like truncate(2), then mode and mtime apply. size on a directory is EISDIR, size or mode on a symlink is EINVAL, and mtime doesn't follow a symlink |
statfs |
{ "bsize", "blocks", "bfree", "bavail" } of the folder's volume |
|
open |
path, write?, create?, truncate?, exclusive?, mode? |
{ "fh", "attr" } |
read |
fh, offset, size (≤ maxIo), ifMatch? |
OPENED { "etag", "size", "offset", "length" }, then length bytes as DATA |
write |
fh, offset |
OPENED {}; the page sends the bytes, then END, and the node ENDs once they are written |
release |
fh |
{ "attr" } for a write handle, {} otherwise |
watch |
mounts (no mount) |
OPENED {}, then NDJSON lines as DATA |
attris{ "kind": "file"|"directory"|"symlink", "size", "mtime" (ms, fractional), "mode" (permission bits), "ino", "etag" }.etagis"<size>-<mtimeNs>-<ino>", quotes included, soattr.etagandifMatchcompare as plain strings.- The folder's root can't be removed or renamed, or replaced by a rename (
EBUSY). - Arguments:
write,create,truncate,exclusiveandreadonlycount when they are truthy, as in JavaScript.- Offsets and sizes are integers from 0 to 2^53−1,
modean integer up to0o7777, andmtimea number.fhis a positive integer, and any otherfhisEBADF. - A
path,from,toortargetthat isn't a string isEINVAL, and so is any other argument of the wrong type,nullincluded. - An unknown
op, or noop, isEINVAL. Amountthat isn't exported isENOENTwithno such folder.
Handles. open without write, create, truncate or exclusive gives a name handle, which holds nothing open between calls. Any of those flags gives a write handle, which keeps the file open (read and write) until release.
create,exclusive(withcreate),truncateandmode(default0666minus the umask) apply atopen, and their errors come back there. A directory isEISDIR(EEXISTwithcreateandexclusive), anything but a regular fileEINVAL.- Handles are numbered per link from 1. A link holds at most 4096 per folder, and the next
openisEMFILE. - An
fhsent with anothermountthan the one it was opened on isEBADF, and so is one already released. - When the link closes, its handles close. What was written stays, as with a crashed local process.
Reads. A read on a name handle opens the file again, through the path rules. When ifMatch is given and differs from the file's current etag, it is ESTALE with file changed since it was opened, so the kernel starts over instead of mixing two versions. A read on a write handle reads its descriptor and ignores ifMatch.
- OPENED carries the file's
etagandsizewith theoffsetasked for and thelengththat follows. The read is short at EOF, andlengthis 0 past it. - If the file shrinks while the bytes are being sent, the stream is
RESET ESTALE.
Writes happen in place, like open(2) and pwrite(2), in any order and with holes. Writing in place keeps hard links, extended attributes, ACLs and ownership. Programs that want atomic replacement write a temporary file and rename it, and those are operations too.
- Before OPENED, the node checks the call:
EROFSfor a read-only folder orreadonly: true,EINVALfor a badoffset,EBADFfor an unknown handle, one from another folder, or a name handle. - Each DATA is written at its place before it is credited, so the stream window bounds the memory used. More than
maxIobytes in one call isRESET EINVAL, and what came before stays written. - After the page's END and the last write, the node sends END. A page RESET leaves what was written.
Watch. OPEN { "kind": "fs", "op": "watch", "mounts": ["project", …] } streams NDJSON lines until the page sends END or the link closes:
{"mount":"project","paths":["src/a.ts","src"]}
{"mount":"project","all":true}
{"ping":1}
- A missing or empty
mounts, or one that isn't a list of strings, isEINVAL, and a name that isn't exported isENOENT. pathsname what changed and its parent directory (""for the root), coalesced over 50 ms. Past 256 paths the line isall, and so is a change the watcher can't name (the root itself, an event-queue overflow).- A watcher that fails or can't start is retried every second. It sends one
allwhen it is lost and one when it is back. A folder that is renamed away or removed is lost until it comes back. - A ping goes out every 15 s.
- Changes seven made itself come back too.
- One watcher per folder serves every watch stream on every link, and it lingers 5 s after the last stream ends, so replacing a watch is cheap.
- A page that doesn't read gets its backlog folded into one
allper folder instead of a growing queue.
The watcher per OS:
- Linux: inotify through fsnotify, one watch per directory, so the node walks the folder at start and adds every new directory. Symlinks aren't followed. A tree past
fs.inotify.max_user_watchesmakes the watcher fail, which is then retried every second. - macOS: one FSEvents stream on the folder (
FSEventStreamCreatewith file events, through purego, so the binary stays free of cgo). It holds no descriptor on anything in the tree and never follows symlinks, so tree size doesn't matter. A dropped or coalesced batch (MustScanSubDirs) is anall, and a folder that is moved or removed (RootChanged) is lost until it comes back. - Windows: one
ReadDirectoryChangesWon the folder withbWatchSubtree, which is what libuv does. A handle on every directory would stop Windows from renaming any of their parents.
Mount updates (page cap mounts). When folders are added or removed while a link is up (fs.Server.Add and Remove, for the TUI), the node opens OPEN { "kind": "mounts", "mounts": [ … ] } on the control channel, with the full list in HELLO's shape. It replaces the previous list, and there are no deltas. The page answers OPENED {} and END, and the node sends END. Updates go one at a time per link, and one the page doesn't finish within 10 s is reset with ETIMEDOUT. A removed folder's handles close (EBADF if the name comes back), its operations are ENOENT, and a watch stream covering it ends with RESET ENOENT: no such folder. --mount alone never changes the set.
Errors. The code is the POSIX name, EIO when there is none. The message is libuv's lower-case text for it (no such file or directory), or slicc's own for the cases it names (path escapes the folder, read-only folder, file changed since it was opened, the folder root cannot be moved or removed). It never contains a host path. Windows errors are mapped the way libuv maps them, for example ERROR_ACCESS_DENIED to EPERM and ERROR_SHARING_VIOLATION to EBUSY.
Windows.
- Paths go through
\\?\andGetFinalPathNameByHandle, so device names (CON,NUL) and 8.3 short names are plain names inside the folder. - Files are opened with
FILE_SHARE_DELETEand without following reparse points. - Symlinks and junctions both show as
kind: "symlink",readlinkworks on both, andunlinkremoves the link itself. inois the file index, andmodeis0666, or0444for a read-only file, plus0111for a directory.setattrmodesets or clears the read-only attribute.unlinkclears the read-only attribute first, asDeleteFilewould refuse otherwise.renamegives the POSIX answers above, replacing an empty directory itself.
Network policy
A node checks every address it connects to for the link's tcp, http, ws and ssh forwarding, and drops refused addresses from resolve answers. The check runs on each address actually connected, after DNS and for every Happy Eyeballs attempt, so a name can't rebind past it. A refusal is EACCES.
Rules. --allow and --deny take CIDR or CIDR:port, repeated or comma-separated: 10.0.0.0/8, 127.0.0.1/32:9222, [::1]/128:9222. A bare address is a single-host rule (127.0.0.1, [::1]:22). Host bits are masked off, and ::ffff:a.b.c.d rules become IPv4 rules. A rule that can never match is rejected: one inside 64:ff9b::/96, 64:ff9b:1::/48, 2002::/16, 2001::/32, 0.0.0.0/8 or ::/128.
Precedence. The longest prefix wins. At the same prefix, a rule with a port wins over one without. At a full tie, a user flag wins over a default rule, so --allow 192.168.0.0/16 opens exactly that range. Between two user flags, deny wins. An address no rule matches is allowed.
Normalization, before matching:
- IPv4-mapped IPv6 (
::ffff:a.b.c.d) is checked as IPv4. - NAT64 (
64:ff9b::/96) is checked as the IPv4 in its last 32 bits, and 6to4 (2002::/16) as the IPv4 in bits 16–47. - Local-use NAT64 (
64:ff9b:1::/48), Teredo (2001::/32),0.0.0.0/8and::are always denied, whatever the rules say.
Remote default (a node following a tray from another machine). Every public address is allowed. Denied:
- loopback:
127.0.0.0/8,::1/128; - private:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16; - CGNAT and tailnets:
100.64.0.0/10; - link-local and cloud metadata:
169.254.0.0/16,fe80::/10; - ULA:
fc00::/7; - multicast:
224.0.0.0/4,ff00::/8; - reserved and documentation:
240.0.0.0/4,255.255.255.255/32,192.0.0.0/24,192.0.2.0/24,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,2001:db8::/32,100::/64; - IPv4-compatible
::/96(deprecated) and SIIT::ffff:0:0:0/96.
Local default (the bridge to the browser slicc controls). The remote default, with loopback and the private ranges (127.0.0.0/8, ::1/128, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) allowed instead of denied, so this machine and the LAN are reachable. --deny 127.0.0.0/8 and --deny 192.168.0.0/16 take loopback and the LAN back out.
Protected ports, in both defaults. The debugging port of every browser the node serves over cdp (attach --cdp and --cdp-extra, follow --cdp) and any port the node itself listens on are denied, so a tcp stream can't get around the cdp capability:
- on loopback, each as
127.0.0.0/8:port,127.0.0.1/32:portand[::1]/128:port, so--allow 127.0.0.1/32doesn't reopen them; - on every address of this machine, as
<address>/32:portor<address>/128:port. The interfaces are enumerated at check time and cached for 5 seconds, so a new interface counts. If they can't be enumerated, those ports are denied on every address.
Browser endpoints. Each listed cdp browser's own endpoints are denied too, so a browser on another machine (--cdp http://10.0.0.5:9222 --allow 10.0.0.0/8) is reachable only through cdp:
- the host and port of its CDP URL, and of the
webSocketDebuggerUrlit reports; - a host that is a name is resolved when the browser is added (the
--hostsfile first, then the system resolver), and every address it resolves to is denied on that port; - while the browser is listed, its names are resolved again on every 2 s check, and new addresses are added to the old ones, so nothing is dropped while it's listed. IP literals aren't looked up again;
- when the browser is listed again after it stopped answering, its endpoints are resolved again and replace the old ones. If resolving fails, the old ones stay.
They are listed in HELLO's policy.deny as <address>/32:port or [<address>]/128:port.
Only a user allow for exactly the same address and port reopens one of these.
resolve filtering. An address is kept when the policy allows it on at least one port. When every address is dropped, the answer is an empty list.
HELLO. policy.summary names the default and lists the --allow and --deny rules on top of it. policy.allow and policy.deny list the --allow and --deny rules plus the protected ports' loopback denies and the browser endpoints, not the default ranges or the per-interface denies.
SSH server
A node started with a runner serves SSH (golang.org/x/crypto/ssh) on each ssh link stream. It never listens on a port.
Auth. none is accepted, because the link is the trust boundary. The user name is ignored. Everything runs as the user who started slicc, in their home directory.
Host key. One ed25519 key per machine, slicc/ssh_host_ed25519_key in the user config directory (~/Library/Application Support on macOS, $XDG_CONFIG_HOME or ~/.config on Linux, %AppData% on Windows). It is created on first use with mode 0600, and a looser mode is tightened to 0600 when it is loaded. HELLO carries its public key in authorized_keys form as sshHostKey.
No runner. Every session channel is refused as administratively prohibited, with the reason.
exec runs <runner argv…> <command>, the command as one last argument. shell runs the runner's interactive form:
- a runner ending in
-cdrops it, and adds-lwhen the word before-cissh,bash,zsh,dash,ksh,mksh,ashorfish:bash -cgivesbash -l,docker exec -i box sh -cgivesdocker exec -i box sh -l,python3 -cgivespython3; - a runner ending in
/c,/Cor-Commanddrops it:cmd.exe /cgivescmd.exe; - for any other runner,
shellprints why on stderr and exits 1.
A command that can't start exits 127 with the reason on stderr.
Sessions.
pty-reqandwindow-change: a PTY fromcreack/ptyon Unix (the command gets its own session and controlling terminal) and ConPTY on Windows.TERMcomes frompty-req.env: any variable with a valid name is accepted and added toslicc's environment.signal: on UnixABRT,ALRM,FPE,HUP,ILL,INT,KILL,PIPE,QUIT,SEGV,TERM,USR1andUSR2go to the command's process group, or with a PTY to the terminal's foreground process group. On Windows,INTon a PTY is^C, andABRT,HUP,INT,KILL,QUITandTERMotherwise end the process.- The end:
exit-status, orexit-signalfor a Unix command killed by one of the signals above (exit-status128+n for any other). After the command exits, its output is read until EOF or 2 s of silence, so a background job that keeps stdout open doesn't hold the session. A session closed while its command runs sends itHUP, and stopping the server sendsKILLto every running command. On Windows each command runs in its own job object, so ending it ends everything it started, and anything it left running ends with the session.
sftp subsystem (pkg/sftp): this machine's files as the user who started slicc, starting in the home directory. A runner that sandboxes commands (docker exec …) doesn't sandbox sftp, so sftp is on by default only when the runner's executable is a plain local shell: bash, sh, zsh, fish, pwsh, powershell or cmd, by basename, ignoring case and .exe. When it's off, the subsystem request is refused.
slicc-eval subsystem: one persistent REPL per node (for example python3 -i), started on first use and shared by every session, so state survives between them. Input is read in lines. The lines available at once are written to the REPL together, and its output is forwarded (stdout and stderr apart) until it has been quiet for 500 ms. At EOF the session exits 0. INT interrupts the REPL's current computation on Unix (Windows ignores it), and other signals go to the REPL as for exec. If the REPL exits, the session prints its exit code on stderr and exits 1, and the next session starts a new REPL. Without a REPL the subsystem exits 1 with the reason.
Forwarding. direct-tcpip dials through the node's policy (see Network policy). A refused address is administratively prohibited with the policy's message, and a failed connect is connect failed. Without a policy every target is refused. Remote forwarding (tcpip-forward), agent and X11 forwarding are refused.
Remote setup: slicc <join-url> follow
slicc <join-url> follow [--exit] [--allow <rule>]... [--deny <rule>]... [--name <name>] [--cdp <url>]...
[--runner <argv…> [--]] [--shell <argv…> [--]] [--eval <argv…> [--]] [--eval-quiet <dur>] [--sftp | --no-sftp]
[--mount <path>[:<name>][:ro]]... [--jerk] [--jerk-seal]
follow links this machine to a seven on another machine through the tray hub (see Tray hub signaling). It serves the link until interrupted (SIGINT, SIGTERM) and reconnects when the link drops. Status lines go to stderr, and SLICC_DEBUG=1 adds diagnostics. The join URL is never printed.
Flags.
--exit: offer this machine's network to seven, astcpandresolvestreams (capnet),httpstreams (caphttp) andwsstreams (capws) under the remote policy, so public addresses only by default. Without--exit, all four are refused withEOPNOTSUPP.--allow,--deny: policy rules on top of the remote default (see Network policy).--name: the link'snameandhostin HELLO, as forattach.--runner,--shell,--eval,--eval-quiet,--sftp,--no-sftp: compute over SSH (capssh), exactly as forattach.direct-tcpipforwarding goes through the remote policy, so it reaches public addresses only unless--allowsays otherwise.--runnerworks with or without--exit.--cdp <url>: let seven drive this browser over the link (repeatable, in any formslicc browsers --cdptakes; each must answer whenfollowstarts). Seecdp, and its warning: the seven on the other machine gets full control of every browser listed. No browser isseven: true. Without--cdp,followdoesn't offercdp, and acdpOPEN is refused withEOPNOTSUPP. Each browser's debugging port and its own endpoint addresses are denied totcp,http,wsandsshforwarding.--mount <path>[:<name>][:ro]: share this folder with the seven on the other machine (capfs, repeatable); see Host folders. It can read the folder, and change it unless:ro.--jerk: offer this machine's slicc-jerk for status and sign-ins (capjerk).--jerk-sealalso lets the seven on the other machine seal this machine's tokens into its vault (capjerk-seal) and implies--jerk. Without host jerk,followsaysjerk: no slicc-jerk 0.6.0 or later on PATH or in slicc's data folder; not offering it(or, for an older jerk,jerk: host jerk <version> is older than 0.6.0; run slicc install jerk) and runs on without it. Seejerk.
HELLO from follow: { "v": 1, "role": "node", "name", "host", "version", "mode": "remote", "node", "caps", "routes": { "prefixes": [], "exit" }, "policy" }. caps holds net, http, credentialed and ws with --exit, ssh with --runner (plus sshHostKey), cdp (and HELLO browsers) with --cdp, fs (and HELLO mounts) with --mount, jerk (and HELLO jerk) with --jerk, jerk-seal with --jerk-seal, and always join.
Identity. The device is its DTLS certificate. follow keeps one ECDSA P-256 certificate per hub origin (scheme://host:port), valid for 20 years, in <config>/slicc/tray-certs.json (mode 0600). Every link to that hub uses it, so the fingerprint stays the same across reconnects, restarts and updates. The first run on a hub mints it and says new device, fingerprint sha-256 …; later runs say device, fingerprint …. Every dial attaches with a fresh controllerId, which is only a routing handle.
join (cap join), page → node. OPEN { "kind": "join", "url" } carries a rotated join URL (see Rotation below). The node answers OPENED {} then END, or RESET EACCES when the URL is not a join URL on the hub it follows.
unlink (cap join), page → node. OPEN { "kind": "unlink" }. The node answers OPENED {} then END, deletes the join state and the certificate for that hub, closes the link, says unlinked by <name> with the name from seven's HELLO, and exits 0. A non-retryable bootstrap.failed with code UNLINKED does the same, with the name from the last link to that hub. seven sends it after the node's answer and then drops the peer connection. The next follow mints a new certificate, so seven sees a new device.
Reconnects. A link that drops, a link not up within 45 s of the offer, a failed bootstrap and any other signaling error lead to a new dial. Dials are spaced 1 s apart at first, doubling up to 30 s, each wait randomized by ±25 %, and back to 1 s after a link was up. A bootstrap the hub restarts (a retryable bootstrap.failed) starts a new peer connection without a new dial. The node keeps polling the bootstrap after its answer until seven's HELLO arrives, then stops. A link that closes before HELLO waits up to 3 s for a failure code from the hub, and without one it is an ordinary reconnect.
Stops with exit 1 and says why on INVALID_JOIN_CAPABILITY, TRAY_EXPIRED or TRAY_SUPERSEDED_MISSING_JOIN_URL from the hub, and on UNTRUSTED, UNSUPPORTED_RUNTIME or NO_FINGERPRINT (an internal error: the answer had no SHA-256 fingerprint) from seven. The device's fingerprint is always SHA-256.
Remote control: slicc prompt, watch and abort
slicc <join-url> prompt [--steer | --reject] [--thinking] [--json] [--plain] [--abort-on-interrupt] <text>|@file|-
slicc <join-url> watch [--thinking] [--json] [--plain]
slicc <join-url> abort
slicc prompt --cdp <url> [--origin <origin>]... [flags] <text>|@file|-
slicc watch --cdp <url> [--origin <origin>]... [flags]
slicc abort --cdp <url> [--origin <origin>]...
slicc prompt|watch|abort --signal stdio [flags] …
These commands talk to seven's agent over a link that offers nothing to seven (caps: []).
- With a join URL: slicc dials the tray as
followdoes (see Remote setup), with the same device certificate, so seven sees the same device. Afollowalready running on this machine keeps its link, because the two processes have differentnodeids. These commands never handlejoinorunlink. When seven has unlinked the device they sayseven unlinked this deviceand exit 1, and they leave the join state and the certificate alone. - With
--cdp: slicc links to the seven tab in that browser asattachdoes (see Local setup), with--originas there. A runningsliccorattachon that browser keeps its link. - With
--signal stdio(for a test harness that owns CDP itself): the local setup messages go as NDJSON on stdin and stdout, exactly as forattach --signal stdio, and--cdpisn't needed.- stdout also carries the events, as with
--json. A line with anonceis a setup message, and every other line is an event. promptcan't read its text from stdin (-exits 2).
- stdout also carries the events, as with
- Setup: slicc waits up to 1 minute for seven's HELLO, then gives up with exit 1.
- No permission: without
agentin seven's HELLO, slicc saysthis seven doesn't allow remote control from this device; turn it on in seven's Linked devicesand exits 3. A local link getsagentby default. A remote one gets it once Allow remote control is on for this device. - Privacy: prompt text and event contents never go to telemetry or logs, and the join URL is never printed.
prompt sends one prompt and streams the run it belongs to.
- Prompt text: the words after the flags, joined with spaces.
@filereads a file, and-or@-reads stdin. Trailing newlines are dropped.- An empty prompt, text that isn't UTF-8, or a prompt over 16 MiB exits 2.
- A prompt up to 60 KiB goes in the OPEN as
text. A longer one goes as DATA after it (seeagentbelow).
- When seven is busy: by default the prompt is queued as a follow-up (
whenBusy: "followUp").--steersteers the running turn, and--rejectdoesn't send it at all: slicc saysseven is busyand exits 1.--steerand--rejectcan't be combined. - Output: the assistant's text goes to stdout as it streams. Separate text entries are separated by a blank line, and stdout ends with a newline.
- Tool calls, tool results, notices and errors go to stderr, and so does thinking with
--thinking. - The prompt's own
userevent isn't shown.
- Tool calls, tool results, notices and errors go to stderr, and so does thinking with
--json: the events are printed on stdout instead, one NDJSON line each, exactly as seven sent them, includingdeliveredand the finalend. Nothing is rendered.- Exit status:
0when the run ends withendstop;1forenderror(the run failed: <message>), forendaborted(the run was aborted), and for a stream or link that ends beforeend(lost the link to seven; the run goes on there);2for bad usage;3without permission;130on Ctrl-C orSIGTERM.
- Ctrl-C leaves the run going in seven. With
--abort-on-interrupt, slicc first opensabort.
watch follows the agent until Ctrl-C or SIGTERM, then exits 0.
- Output:
userevents go to stdout as> <text>, on one line. Assistant text goes to stdout as forprompt, and a line ends at each run's end. Everything else goes to stderr, as forprompt, plusrun ended: <reason>for a run that didn't end withstop.--jsonprints the events instead. - Status lines on stderr:
slicc: watching <name>,slicc: lost the link to <name>; reconnecting, and for the tray<reason>; trying again in <wait>. - Reconnects: when the stream ends or is reset, slicc opens a new
watchon the same link. When the link goes, it sets up a new one, through the tray again or by waiting for seven over CDP. Waits start at 1 s, double up to 30 s with ±25 % jitter, and go back to 1 s once a stream opens. - Resuming: each new stream asks for
since, the id of the last event slicc saw, so nothing still in the transcript is lost. Nothing is shown twice either. Fortextandthinking, slicc counts the bytes it has shown per entry and skips that many from the entry's replay. Any other entry is skipped when its id was already seen.runevents have no id and aren't replayed. When seven answersgap: true, slicc printssome events were missed. - Exit status:
1when the link can't be set up for good (a terminal tray failure, or the CDP connection closing),2for bad usage,3without permission.
abort aborts the agent's current run. It prints slicc: aborted the run, or slicc: nothing was running, and exits 0. Otherwise it exits 1 on errors, 3 without permission and 130 on Ctrl-C.
Styling. When a stream is a terminal (and TERM isn't dumb), its lines are styled:
- tool calls
⚙ <name> <summary>; - results
↳ <summary>, or✗ <summary>for a failure; - notices
• <text>, and errors✗ <message> <detail>; - thinking dim and italic, and prompts in
watchbold.
NO_COLOR keeps the layout without colors. When stdout and stderr are the same terminal, a line on one first ends an unfinished line on the other.
Otherwise, or with --plain, there are no escape codes:
tool <name>: <summary>,tool <name> ok: <summary>,tool <name> failed: <summary>;notice: <text>,error: <message> (<detail>);thinking: <text>.
Summaries are one line of at most 200 characters. Escape sequences and control characters other than newline and tab are removed from everything seven sends before it's shown.
agent (page cap agent), node → page
- Who may: the node opens
agentstreams only to a page whose HELLO listsagent, and the page refuses them withEACCESotherwise. - Several processes, one device: the hello and HELLO carry the process's
nodeid. seven replaces an older link of the same device (any local link, or the same fingerprint over the tray) only whennodematches. Over the tray it decides once HELLO arrives.
prompt: OPEN { "kind": "agent", "op": "prompt", "text", "whenBusy": "followUp" | "steer" | "reject", "thinking"?: true } on its own channel.
- Answer: OPENED
{ "delivered": "run" | "steer" | "follow-up" }once the agent took it. Arejectwhile busy isRESET EBUSY. - Then DATA: NDJSON events, starting at this prompt's own
userevent, until the run that includes it ends. The last line is{"t":"end","reason":"stop"|"aborted"|"error","message"?}, then END. The node sends END right after OPENED. - Long prompts: a prompt over 60 KiB goes as
"length": <bytes>instead oftext.- The page answers OPENED
{}at once. - The node sends exactly that many bytes of UTF-8 as DATA, then END. A different count is
RESET EPROTO. - The page then delivers it. Its first line is
{"t":"delivered","delivered"}, or it resets withEBUSYinstead.
- The page answers OPENED
- Aborting: a RESET from the node, or a closed link, doesn't abort the run. Only
abortdoes.
watch: OPEN { "kind": "agent", "op": "watch", "since"?: "<entry id>", "thinking"?: true } on its own channel.
- Answer: OPENED
{ "head": "<entry id>", "gap"?: true }, then NDJSON events until either side resets. - Without
since: the events start at the next one. - With
since: the page first replayssince's own entry when it'stextorthinking(its whole text so far, as onedelta), then every entry after it.gap: truemeans the transcript no longer holdssince, and the events start at the next one.
abort: OPEN { "kind": "agent", "op": "abort" }, answered OPENED { "aborted": true | false } (false when nothing was running), then END.
Events, one JSON object per line:
| event | meaning |
|---|---|
{"t":"user","id","text","delivered"} |
a prompt |
{"t":"text","id","delta"} |
assistant text as it streams |
{"t":"thinking","id","delta"} |
thinking, only on streams opened with thinking |
{"t":"tool","id","call","name","summary"} |
a tool call, with a one-line summary such as the bash command |
{"t":"tool-result","id","call","ok","summary"} |
its result |
{"t":"run","state":"start"|"end","reason"?} |
a run starts or ends |
{"t":"notice","id","text"} |
retries, compactions, rewinds |
{"t":"error","id","message","detail"?} |
an error |
{"t":"delivered","delivered"} |
a long prompt was taken (prompt only) |
{"t":"end","reason","message"?} |
the last line of a prompt stream |
- Ids:
idis the transcript entry id, anddeltalines for oneidarrive in order. - Limits:
summaryis at most 200 characters. Credentials and tool output beyond the summary are never sent. - Unknown events: the node ignores events and fields it doesn't know.
Tray hub signaling (remote setup)
A node on another machine sets up its link through the tray hub, the slicc-tray-hub worker on www.sliccy.ai. seven is the tray leader and the node is a follower. The wire contract is six's packages/shared-ts/src/tray-signaling.ts. The node only signals over it and never speaks six's tray-sync messages.
Join URL. https://<host>/join/<token>. Plain http is accepted only for loopback hosts. The join URL is the secret: the node never prints it, its token, or the token's secret part, in output, logs or errors.
Attach. POST <join-url> with { "controllerId", "runtime": "slicc-link/1" }. The runtime tag lets seven tell link nodes from six's followers; the hub passes it on in follower.join_requested, and the node sends it again on retry. wait is retried after retryAfterMs (clamped to 50 ms–30 s) with the same controller id. signal starts a bootstrap and carries the TURN and STUN iceServers for the peer connection. fail (INVALID_JOIN_CAPABILITY, TRAY_EXPIRED) is terminal.
Supersede. The node does not follow HTTP redirects itself. A superseded tray's replacement is read from the Link header (rel="successor-version"), then a 3xx Location (with json dropped from its query), then the body's result.joinUrl when code is TRAY_SUPERSEDED. The 308 redirect and the older 409 fail shapes are both accepted. A replacement must itself be a valid join URL. Each hop uses a fresh controller id, and a dial follows at most five hops before it fails with TRAY_SUPERSEDED_CHAIN_EXHAUSTED. A supersede seen during a bootstrap ends it with TRAY_SUPERSEDED, and the next dial starts at the replacement.
Bootstrap. The node polls { "action": "poll", "controllerId", "bootstrapId", "cursor" } once a second, and takes the leader's bootstrap.offer and bootstrap.ice_candidate events in order. It posts answer, then its own ice-candidates. Candidates gathered before the answer is posted are held and sent right after it. A bootstrap.failed that is retryable, with retriesRemaining above 0, is retried with { "action": "retry" } after its retryAfterMs. That mints a new bootstrap, so the peer connection starts over. Any other failure ends the bootstrap. When no offer arrives within 30 s, the bootstrap ends with OFFER_TIMEOUT.
Rotation. When seven creates a new join URL it doesn't supersede the old tray, so a follower learns the new URL from seven over its link. The node accepts it only on the hub origin it currently follows (same scheme, host and port), and only as a valid join URL. It stores the URL as current for the join URL it was given, keeps the running link, and dials the new URL on its next reconnect. It never prints the URL.
Requests. Each HTTP attempt has a 30 s timeout. A logical request gets 8 s and up to four attempts. Network errors, HTTP 5xx and non-JSON bodies are retried with a backoff of 1 s, 2 s, then 4 s. JSON refusals below 500 are not retried.
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.
Development
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. 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.
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), 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.
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.
Test isolation: SLICC_DISCOVERY (test-only). When it is set, browser discovery (slicc browsers, the default flow, and browser.Discover in the same process) never probes ports 9222–9229, never reads the process list, and finds only --cdp URLs, plus:
isolated, or any value not listed here: nothing else;installed: installed browsers too;home: installed browsers, and theDevToolsActivePortfiles 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).
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.
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).
Port guard. On CI (CI set), or with SLICC_TEST_PORT_GUARD=1, TestMain holds 127.0.0.1:9222 to 9229 (and [::1] where it can) for the whole run, and the run fails if anything connects to them, other than the one test that lends a port to its own fake browser. On CI a port it can't hold fails the run. Off CI it is off by default, so a developer's own Chrome on those ports is never touched; the guarded tests then skip.
Real browsers in tests. The Chrome the harness starts itself is SLICC_TEST_CHROME (test-only: the path of a Chrome executable), else the machine's Chrome or Chromium, with the real environment, a temporary profile and --use-mock-keychain --password-store=basic. The tests that have slicc itself launch a real Chrome run it under a temporary home, where its profiles are throwaway ones. Off CI they never launch the machine's own Chrome: they need SLICC_TEST_CHROME set to a Chrome for Testing inside its .app, which they link into the temporary home's Applications folder on macOS and launch by its id (chrome-for-testing), and they skip with a notice without it, or on other systems. On CI they launch the runner's Google Chrome. An older Chrome for Testing can fail the tests that read Chrome's own prefs.
On GitHub's Windows runners the system drive C: is slow: with Chrome profiles there, Chrome sometimes waited seconds before a new tab's first request (up to past the 15 s page-load limit) or before exiting (did not exit). With them on D:, the runner's temp drive, those stalls weren't seen and the suite ran about a quarter faster. So CI points TEMP and TMP at the runner's temp dir, and the tests' temporary homes and Chrome profiles live there. When a test page doesn't load, the failure says where it stopped: the tab's URL and readyState, the resources it fetched, and every request the page server saw.
The real-Chrome tests in test/integration/ launch headless, so they run the executable directly. A windowed launch on macOS goes through open -n -W -a, which CI doesn't cover: on GitHub's macOS runners, Chrome started that way never writes DevToolsActivePort.
Releases
semantic-release cuts releases from main in .github/workflows/release.yml, on a macOS runner:
scripts/release-plan.mjsasks semantic-release (dry run) for the next version, and stops when there is none.scripts/package.sh <version>buildsdarwin,linuxandwindows×amd64,arm64withCGO_ENABLED=0,-trimpathand-X main.version=<version>. It signs and notarizes the darwin binaries and writesSHA256SUMS. The windows and linux binaries are not signed. Without the Apple signing secrets (forks, pull requests) it warns and leaves the darwin binaries unsigned.scripts/npm-stage.mjsstages@ai-ecoverse/sliccand the six platform packages intodist/npm/. It also stages the same launcher assliccy(with two bins,sliccyandslicc, and a short preview README) intodist/npm-next/, as version7.0.0-next.<version>, with exactoptionalDependencieson this release's platform packages.npx sliccy@nextruns the bin named after the package,sliccy, and both bins run the same launcher.--bin <name>(repeatable) overrides the bins of any staged launcher.actions/attest-build-provenanceattests every binary.- semantic-release checks the version again (
dist/VERSION, andscripts/go-major.mjs, see below), tags, creates the GitHub release with every binary andSHA256SUMS, thenscripts/npm-publish.mjspublishes the platform packages and the launcher through npm trusted publishing (OIDC, with provenance), and thensliccywith--tag next. Versions already on npm are skipped. Before publishingsliccy, the script checks that every exact-pinnedoptionalDependenciesversion is on the registry, waits up toSLICC_REGISTRY_WAIT_SECONDS(default 900) for freshly published ones to appear, and fails otherwise, so asliccyversion never points at missing platform packages. scripts/go-warm.sh <version>, once the tag is onorigin, asksproxy.golang.orgfor the module atv<version>(go list -mwith a scratch module cache) and askspkg.go.devto fetch it. The proxy fetch is retried briefly; thepkg.go.devfetch is one attempt of at most 10 s, since the proxy fetch already gets the version indexed. Both only warn, and the step can't fail the release.
Go module versions. In Go a pushed tag is the release: proxy.golang.org caches it, sum.golang.org records its hash for good, and pkg.go.dev indexes it.
- Never move or delete a pushed tag. The checksum database would then reject that version for every user. A bad version gets a
retractdirective ingo.mod, and a new release. - Major versions. A module at v2 or above needs
/vNat the end of its module path, orgo install …@latestnever sees it.scripts/go-major.mjs <version>fails the release when the version's major doesn't fit the module path ingo.mod(no suffix allows v0 and v1,/vNallows only vN).release-plan.mjsruns the same check, so it fails before the build. ABREAKING CHANGEcommit therefore needs a deliberate module-path change first.
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.
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.
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:
- macOS signing:
APPLE_CERTIFICATE_BASE64,APPLE_DEVELOPER_CERTIFICATE_PASSWORD; - notarization:
APPLE_API_KEY_ID,APPLE_API_KEY_ISSUER_ID,APPLE_API_KEY_P8_BASE64.
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, and sliccy stays off until SLICCY_NEXT_PUBLISH is true; until then a release skips them with a notice.
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.