@ai-ecoverse/slicc-browsers 1.18.5 → 1.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -608
- package/bfcache-restore.mjs +44 -6
- package/extension-world.mjs +3 -2
- package/guard-vectors.json +1 -1
- package/package.json +3 -3
- package/protected-error-page.mjs +1 -0
package/README.md
CHANGED
|
@@ -1,700 +1,260 @@
|
|
|
1
1
|
# slicc-browsers
|
|
2
2
|
|
|
3
|
-
The browser
|
|
4
|
-
- `slicc browsers` and `slicc launch` in [slicc-cli](https://github.com/ai-ecoverse/slicc-cli) (Go, `CGO_ENABLED=0`);
|
|
5
|
-
- `playwright-cli list` and `open` in [slicc-cdp](https://github.com/ai-ecoverse/slicc-cdp) (Rust, also built for `wasm32-wasip1`).
|
|
3
|
+
The browser knowledge that SLICC's tools share. SLICC drives a real Chromium browser over the Chrome DevTools Protocol (CDP), and several programs, written in different languages, need to agree on the same facts:
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
- which browsers exist, where they are installed, and how to launch them for SLICC;
|
|
6
|
+
- how to find a browser that is already running with a debugging port, and which one to pick;
|
|
7
|
+
- which origins browser control must **never** touch, starting with seven's own origin and the vault;
|
|
8
|
+
- how to prove that a CDP guard really keeps a client away from them.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
These programs can't share code, so they share data: one catalog, three sets of test vectors and their JSON schemas, published as a Go module, a Rust crate and an npm package at the same version.
|
|
10
11
|
|
|
11
|
-
|
|
|
12
|
+
| who | uses it for |
|
|
12
13
|
| --- | --- |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
14
|
+
| [slicc-cli](https://github.com/ai-ecoverse/slicc-cli) (Go) | `slicc browsers`, `slicc launch`, and its CDP relay's guard |
|
|
15
|
+
| [slicc-cdp](https://github.com/ai-ecoverse/slicc-cdp) (Rust, also `wasm32-wasip1`) | `playwright-cli list` and `open`, and its guard |
|
|
16
|
+
| [slicc-agent](https://github.com/ai-ecoverse/slicc-agent) | refusing protected origins in its browser tools |
|
|
17
|
+
| [slicc-bios](https://github.com/ai-ecoverse/slicc-bios), [slicc-extension](https://github.com/ai-ecoverse/slicc-extension) | their CDP guards, certified with the guard vectors |
|
|
18
|
+
|
|
19
|
+
## What's inside
|
|
16
20
|
|
|
17
21
|
| file | what it is |
|
|
18
22
|
| --- | --- |
|
|
19
|
-
| `catalog.json` |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
| [`catalog.json`](catalog.json) | every known Chromium browser and Electron app, with install paths per OS, discovery and ranking rules, seven's launch flags and prefs, profile locations and the protected origins |
|
|
24
|
+
| [`corpus/`](corpus), `corpus.json` | discovery conformance cases: a fake machine each, with the ranked browser list it must produce |
|
|
25
|
+
| [`protected.json`](protected.json), [`protected.mjs`](protected.mjs) | protected-origin test vectors, and a reference matcher in JavaScript |
|
|
26
|
+
| [`guard-vectors.json`](guard-vectors.json) | the CDP scenarios every guard must pass |
|
|
27
|
+
| [`guard-runner.mjs`](guard-runner.mjs), [`guard-driver.mjs`](guard-driver.mjs), [`fixture-server.mjs`](fixture-server.mjs) | a reference runner for the guard vectors, its helpers, and the test server they run against |
|
|
28
|
+
| [`schema/`](schema) | JSON Schema (2020-12) for each file above, and for the `--json` output of `slicc browsers` and `playwright-cli list` |
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
### Go
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
go get github.com/ai-ecoverse/slicc-browsers
|
|
36
|
+
```
|
|
31
37
|
|
|
32
38
|
```go
|
|
33
39
|
import sliccbrowsers "github.com/ai-ecoverse/slicc-browsers"
|
|
34
40
|
|
|
35
41
|
c := sliccbrowsers.Default()
|
|
36
|
-
|
|
42
|
+
chrome, ok := c.Browser("chrome")
|
|
43
|
+
rank := c.Priority("chrome")
|
|
37
44
|
```
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
- `(
|
|
41
|
-
- `
|
|
42
|
-
- `(
|
|
43
|
-
- `Guard() (*GuardVectors, error)` parses `guard-vectors.json`; `GuardVectorsJSON()` and `GuardVectorsSchemaJSON()` return copies of the files. `Guard()` parses it into `SchemaVersion`, `SetVersion`, `Targets`, `Variants`, `Capabilities`, `Placeholders` and `Vectors`. A `GuardExpected` is `Checks` (names to booleans) or `Text`, and a `GuardObservation` is `Means`, `DependsOn` and `Requires`. The `GuardTarget*`, `GuardOrdinary`, `GuardSameSite`, `GuardKind*`, `GuardVia*`, `GuardScopeAny`, `GuardControl`, `GuardGuarded`, `GuardCap*` and `GuardSession*` constants name the values.
|
|
44
|
-
- `Corpus() ([]Case, error)` returns the cases. See [Running the corpus](#running-the-corpus) for the helpers.
|
|
46
|
+
The files are embedded, so nothing is read from disk at run time.
|
|
47
|
+
- `Default()` parses the embedded catalog once and panics if it can't, which CI rules out; `Load()` returns the error instead, and `Parse(b)` reads another copy.
|
|
48
|
+
- `Corpus()`, `Protected()` and `Guard()` return the vectors as typed structs.
|
|
49
|
+
- `CatalogJSON()`, `CorpusJSON()`, `ProtectedJSON()`, `GuardVectorsJSON()` and the `…SchemaJSON()` functions return the raw files.
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
### Rust
|
|
47
52
|
|
|
48
53
|
```toml
|
|
49
54
|
[dependencies]
|
|
50
|
-
slicc-browsers = "1"
|
|
55
|
+
slicc-browsers = { version = "1", features = ["serde"] }
|
|
51
56
|
|
|
52
57
|
[dev-dependencies]
|
|
53
58
|
slicc-browsers = { version = "1", features = ["corpus"] }
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
- Feature `serde`: `Catalog` and the other types (`protected_origins` is a `ProtectedOrigins`), `Catalog::parse(&str)`, `catalog()` (parses `CATALOG`), `Catalog::browser` and `Catalog::priority`. It pulls in `serde` and `serde_json`.
|
|
58
|
-
- Feature `corpus` (implies `serde`): `CORPUS`, `PROTECTED` and `GUARD_VECTORS`, the text of `corpus.json`, `protected.json` and `guard-vectors.json`; the module `corpus` with `corpus()`, `Case`, `Layout` and the helpers below; the module `protected` with `vectors()`, `Vectors` and the `OPENER_*` constants; and the module `guard` with `vectors()`, `Vectors`, `Vector`, `Step`, `Observation`, `Expected` (`Checks` or `Text`), and the `TARGET_*`, `ORDINARY*`, `KIND_*`, `VIA_*`, `SCOPE_ANY`, `CONTROL`, `GUARDED`, `CAP_*` and `SESSION_*` constants. Use it from `[dev-dependencies]` only; with resolver 2 it stays out of normal builds.
|
|
59
|
-
|
|
60
|
-
Every feature builds for `wasm32-wasip1`.
|
|
61
|
-
|
|
62
|
-
## npm
|
|
63
|
-
|
|
64
|
-
`import catalog from '@ai-ecoverse/slicc-browsers' with { type: 'json' }`. The exports are `.` (`catalog.json`), `./catalog.json`, `./corpus.json`, `./protected.json`, `./protected.mjs`, `./guard-vectors.json`, `./guard-driver.mjs`, `./schema/catalog.schema.json`, `./schema/browsers.schema.json`, `./schema/corpus.schema.json`, `./schema/protected.schema.json` and `./schema/guard-vectors.schema.json`.
|
|
65
|
-
|
|
66
|
-
`guard-driver.mjs` is described under [Guard vectors](#guard-vectors).
|
|
67
|
-
|
|
68
|
-
`protected.mjs` is the reference matcher for [protected origins](#protected-origins): `import { protectedOrigins } from '@ai-ecoverse/slicc-browsers/protected.mjs'`, then `protectedOrigins(catalog.protectedOrigins, extraOrigins)` returns `{ url(text), target({ url, previous, opener, type, documentURL, unreachableURL, navigating }), cookieDomain(domain), storageKey(key), unparseable(text) }`, each a boolean. `url(text)` uses the load/loaded-URL fail-closed rule; `url(text, { readOnly: true })` preserves the old parse-failure result for read-only keys and value redaction, and `storageKey(key)` uses that mode internally. `unparseable(text)` identifies parse or unwrap failures without changing whether a parsed origin is protected. It needs only the WHATWG `URL`, so it runs in Node and in a browser. `protected.mjs` and `seven.mjs` import nothing and re-export nothing, so an extension or service worker can ship either file alone; `npm run validate` (through `scripts/self-contained.mjs`) evaluates each as a module in a fresh `node:vm` context that has only `URL`, with no linker resolution allowed.
|
|
69
|
-
|
|
70
|
-
The complete static import graph of `guard-runner.mjs` is browser-safe too: it uses ordinary browser globals (`URL`, timers, `fetch`, `atob`/`btoa`, `Uint8Array`, `TextEncoder` and `TextDecoder`), never `Buffer`, `process`, `require` or `node:` imports. Base64 bodies and postData are decoded with `atob` and UTF-8 bytes rather than a Node shim. `npm run validate` loads and executes that graph in a fresh `node:vm` browser-like context, including an encoded-body exercise and a drivable page row. The separate host-side `./fixture` server intentionally uses Node HTTP/TLS APIs and is not imported by the browser runner.
|
|
71
|
-
|
|
72
|
-
## `catalog.json`
|
|
73
|
-
|
|
74
|
-
`"schema": 1`. A minor release only adds fields; a field that changes meaning or goes away needs schema 2 and a major release. Relative paths are arrays of path segments, joined with the platform's separator. Top-level fields:
|
|
75
|
-
|
|
76
|
-
- `minChromiumMajor` (116): endpoints whose user agent's `Chrome/<major>` (`discovery.chromeMajorPattern`) is lower are left out, and so are installed browsers with `chromiumVersion: true` whose version's leading number is lower. A version that doesn't start with a number is kept.
|
|
77
|
-
- `browsers`: every known Chromium browser, **in rank priority order**. Each has:
|
|
78
|
-
- `id`, `name`, `channel` (`stable`, `beta`, `dev`, `canary`, `nightly`, `snapshot`, `testing`), `engine` (`chromium`), and `chromiumVersion`: whether its version is the Chromium version (Chrome, Edge, Chromium) or its own (Brave, Opera, Vivaldi, Arc).
|
|
79
|
-
- `macos`: `bundleId`, `app` (the bundle's file name), `executable` (under `Contents/MacOS`), and `dataDir?`, the default user-data dir under `~/Library/Application Support`.
|
|
80
|
-
- `windows?`: `roots` (names from `platforms.windows.roots`, searched in order) and `executables` (each tried under each root); `dataRoot` and `dataDir`, the default user-data dir; `startMenuInternet?`, the browser's key names under the `StartMenuInternet` registry keys.
|
|
81
|
-
- `linux?`: `bins` (names on `PATH`), `desktopIds` (`.desktop` file names without the suffix), `flatpak?` (the app id), `snap?` (the snap name), `snapDataDir?` (the user-data dir under `~/snap/<snap>/common`), `configDir` (the user-data dir under the config dir, also under `~/.var/app/<flatpak>/config` for a flatpak), and `debs` (dpkg package names for the version).
|
|
82
|
-
- `electron`: `macosApps` (known Electron apps by bundle id, with display names), `debugBlocked` (app names that block remote debugging; they're listed with `debugSupport: "disabled"` and never launched), and `idPrefix` (`electron:`, followed by the slug of the name: lower case, runs of other characters as one `-`).
|
|
83
|
-
- `rank`: `tiers`, 1 to 6: `sevenOpen` (an endpoint with a seven tab), `endpoint` (any other endpoint), `chrome` (installed `chromeId`), `chromium` (other installed Chromium browsers), `electron` (installed Electron apps), `six` (an endpoint in SLICC six's profile, with or without a seven tab; see [SLICC six's profiles](#slicc-sixs-profiles)).
|
|
84
|
-
- `discovery`:
|
|
85
|
-
- `activePortFile` (`DevToolsActivePort`): its first line is the port (1 to 65535) and its second the browser's websocket path, which starts with `/`. A file without both is ignored. A file is live only when its port answers `/json/version` with a `webSocketDebuggerUrl` whose path is that websocket path; otherwise it's stale and ignored. When the port accepts connections but serves no `/json/version`, the endpoint is listed as `wsOnly`, with `ws://127.0.0.1:<port><path>`;
|
|
86
|
-
- `probePorts` (9222 to 9229);
|
|
87
|
-
- every endpoint's `cdp` is `<scheme>://<host>:<port>`, with the host in lower case and `localhost` and `::1` written as `127.0.0.1`, whatever form the URL was found in. Endpoints with the same host and port, written that way, are one endpoint. A `ws://` `--cdp` URL that is listed as `wsOnly` keeps its websocket URL as given;
|
|
88
|
-
- `sourceRank`: when two sources find the same host and port, the lower rank wins, and on a tie the first found;
|
|
89
|
-
- `names`: an endpoint's name when it isn't from a known browser's user-data dir: `electron` when the user agent contains `electronUserAgent`, else `products[<product>]` for the product before the `/` in `/json/version`'s `Browser`, else that product, else `fallback`;
|
|
90
|
-
- `process`: the process scan, see [The process source](#the-process-source).
|
|
91
|
-
- `seven`:
|
|
92
|
-
- `originScheme` and `originHostPattern`: a seven origin is `https://<host>` with a host matching the pattern, except for the `excludedLabels` (`www`, `vault`, `vault-preview`, `get`, `mcpcat`, `echo`) and the `excludedLabelPrefixes` (`vault-`, which excludes `vault-feature-x` and any other vault branch preview). The bare apex (`sliccy.ai`, i.e. an empty label) never matches. An origin is compared without the scheme's default port (443 for `https`, 80 for `http`), so `https://seven.sliccy.ai:443` is seven and `https://seven.sliccy.ai:8443` is not. The same goes for extra seven origins;
|
|
93
|
-
- `sitePattern` (`[*.]sliccy.ai`);
|
|
94
|
-
- `switches` and `disabledFeatures`: the seven flags, all `--disable-features` values merged into one switch;
|
|
95
|
-
- `prefs`: values merged into a profile's `Default/Preferences` at the `path` of keys;
|
|
96
|
-
- `discardExceptions`: where the site pattern and the extra origins' hosts go, as a `list` of strings or as a `chrome-time-map`, a map from site to the time it was added (microseconds since 1601-01-01 UTC, as a decimal string);
|
|
97
|
-
- `cleanExit`: values that mark the last exit clean;
|
|
98
|
-
- `form` says where a key comes from: `six` (SLICC six's keys) or `chrome-154` (what Chrome 154 reads).
|
|
99
|
-
- `protectedOrigins`: what browser control must never reach, see [Protected origins](#protected-origins). `domains` (`sliccy.ai`), `hosts` (`vault.sliccy.ai`), `schemes` (`https`, `http`), `innerSchemes` (`blob`, `filesystem`, `view-source`), `anySchemes` (`devtools`), `protectedSchemes` (`chrome`, `chrome-untrusted`, `chrome-search`, `chrome-native`, `chrome-distiller`, `chrome-experimental-site-token-provider`, `isolated-app`, `externalfile`, `content`, `chrome-extension`, `devtools`, `edge`, `brave`, `opera`, `vivaldi`, `arc`, `file`) with `schemeExceptions` (`chrome://newtab`, `chrome://new-tab-page`, `chrome-untrusted://new-tab-page`, `edge://newtab`, `brave://newtab`, `opera://startpage`), `aboutScheme` (`chrome`), `targetTypes` (`browser_ui`), `inheriting` (`about:blank`, `about:srcdoc`), `errorPage` (`url: "chrome-error://chromewebdata/"`, `target: "previous"`, `redact: "origin"`), `unparseable` (`protected`) with `unparseableRefusal`, and `refusal`, the text of a parsed protected-origin refusal.
|
|
100
|
-
- `profiles`: SLICC profiles live in `<config dir>/<root>/<name>`, and a snap's in `~/snap/<snap>/common/<snapRoot>/<name>`. `legacyRoot` is the old root, never scanned; on macOS's case-insensitive volume it is SLICC six's root too. `sixProfilePattern` matches six's profile names, case-insensitively. `namePattern` is a valid profile name. `six` describes SLICC six's Chrome profiles (see [SLICC six's profiles](#slicc-sixs-profiles)):
|
|
101
|
-
- `roots`: six's profile root per OS (`macos`, `linux`, `windows`), a `base` location followed by the `path` segments. The base is its `env` when that is set and not empty (any value, as six takes it), else `home` under the home dir.
|
|
102
|
-
- `defaultName` (`browser-coding-agent-chrome`), `defaultPort` (5710) and `portSuffix` (`-{port}`): six's profile for its serve port `<port>` is `<root>/<defaultName>`, with `portSuffix` added for any port other than `defaultPort`.
|
|
103
|
-
- `userDataDirEnv` (`SLICC_USER_DATA_DIR`): when set, its value, trimmed of whitespace, is six's profile for every port, unless it is empty.
|
|
104
|
-
- `launch`: `chromiumArgs` (with `{profile}` for the profile dir), then the seven flags, then `headless` with `--headless`. Electron apps get `electron.portFlag` with the first free port from `electron.minPort` up that isn't in `electron.reservedPorts`.
|
|
105
|
-
- `platforms`: the config dir per OS, the Windows roots, and the Windows registry keys. A location is the environment variable `env` when it's set (on Linux only an absolute path counts), else `absolute`, else `home` under the home dir. `registry`: under each of `hives`, every subkey of each `startMenuInternet` key, with its `openCommand` default value, and the default value of `appPaths\<name>` for each of `appPathNames`.
|
|
106
|
-
|
|
107
|
-
## Protected origins
|
|
108
|
-
|
|
109
|
-
Browser control must never navigate to, open or attach to a target on seven's own origin, or read or write its cookies or storage: not slicc-cli's `cdp` relay, not slicc-cdp's `playwright-cli`, not slicc-agent. `protectedOrigins` in `catalog.json` says what is protected, and every implementation runs the vectors in `protected.json`.
|
|
110
|
-
|
|
111
|
-
**URLs.** A URL is parsed the way the WHATWG URL Standard (and so the browser) parses it, so `https:\\seven.sliccy.ai`, a tab or newline inside the URL, `%2E`, `。` and full-width letters all reach seven. In Go and Rust that takes a WHATWG parser, not `net/url`. A URL the guard’s WHATWG parser cannot parse is **protected** wherever the guard judges a URL the browser will load or has loaded: `unparseable: "protected"` in both `catalog.json.protectedOrigins` and `protected.json`. Chrome’s parser is not interchangeable with the guard’s parser; in particular it can load `http://exa%20mple.sliccy.ai/` even though the WHATWG parser rejects the decoded space. Then:
|
|
112
|
-
1. While its scheme is one of `innerSchemes`, the rest of the URL after `<scheme>:` takes its place, at most 8 times: `blob:https://seven.sliccy.ai/<uuid>` is `https://seven.sliccy.ai/<uuid>`. A failing inner parse, or a URL still wrapped after the eighth unwrap, is unparseable and protected.
|
|
113
|
-
2. A scheme in `anySchemes` is protected whatever follows: a DevTools front end (`devtools://`) can be inspecting seven.
|
|
114
|
-
3. A scheme in `protectedSchemes` is protected like seven, whatever follows, except the `scheme://host` pairs in `schemeExceptions` (the new tab pages), on any path. Match the parsed scheme and exact host, never a raw URL prefix: `chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=` is excepted, but `chrome-untrusted://new-tab-page.evil/` and `chrome-untrusted://other/` remain protected. Chrome's new tab embeds the excepted untrusted host, and `chrome://new-tab-page/` can commit as itself when opened directly:
|
|
115
|
-
- Chrome's privileged UI (`chrome://settings`, `chrome://password-manager`, `chrome-untrusted://…`, `devtools://`) and the same UI in the catalog's other Chromium browsers (`edge://settings/passwords`, `edge://wallet`, `brave://`, `opera://`, `vivaldi://`, `arc://`), since a client attaches to whatever browser answers;
|
|
116
|
-
- `chrome-extension://`, every page, frame and extension target of every type: password-manager and wallet extensions keep secrets there. There are no default exceptions. Seven's own extension origin is already covered as a seven origin. If a guard ever needs an exception, it is a catalog entry by extension id with a reason, never a default;
|
|
117
|
-
- `file://`: otherwise a tool could open `~/.ssh` or `~/.aws/credentials` in the real browser, around slicc's file-system fence. Adopters whose tests open local HTML through the guarded client move those fixtures to a local http server.
|
|
118
|
-
|
|
119
|
-
An `about:` URL other than `inheriting` is first rewritten the way Chromium rewrites it, to `aboutScheme://<path>`: `about:settings` is `chrome://settings` and protected, `about:newtab` is `chrome://newtab`, and `about:blank` stays unprotected. A wrapper is judged by what it wraps (rule 1): `view-source:chrome://settings/` and `view-source:file:///…` are protected, and `blob:` and `filesystem:` URLs by their creator's origin.
|
|
120
|
-
4. A scheme in `schemes` is protected when its host is one of `hosts`, or one of `domains` or under one, at any depth and on any port. The host is lower-cased, and one trailing dot is dropped.
|
|
121
|
-
5. Any other URL is protected when its origin is one of the program's **extra origins**: the seven origins it accepts besides `sliccy.ai` (slicc-cli's `--origin` and `--page` origins), compared as `scheme://host[:port]` without the scheme's default port, with the host as in 3.
|
|
122
|
-
|
|
123
|
-
This is wider than the rule for finding seven (`seven.originHostPattern`, which leaves out the `excludedLabels` and `excludedLabelPrefixes`—`www`, `vault`, `vault-preview`, `get`, `mcpcat`, `echo`, and any `vault-*` prefix). Every `sliccy.ai` host shares the `sliccy.ai` cookies, and cookies ignore the port and, without `Secure`, the scheme. SLICC six lives on `www.sliccy.ai` and the apex, and its cookies matter. `*.kernel.localhost` is not protected: each port is its own origin, and the agent's own servers live there. An extra origin is matched exactly, so `http://127.0.0.1:5710` doesn't protect `http://127.0.0.1:5711`.
|
|
124
|
-
|
|
125
|
-
**Chromium scheme audit.** Checked [content scheme registration](https://github.com/chromium/chromium/blob/d0bfaae35fa745d9dee525dc4ff6617b1551dbe7/content/common/url_schemes.cc) and [Chrome additional schemes](https://github.com/chromium/chromium/blob/d0bfaae35fa745d9dee525dc4ff6617b1551dbe7/chrome/common/chrome_content_client.cc), including platform-specific registrations:
|
|
126
|
-
|
|
127
|
-
| Scheme | Decision and reason |
|
|
61
|
+
| feature | adds |
|
|
128
62
|
| --- | --- |
|
|
129
|
-
|
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `chrome-native` | Protected: browser UI rendered by platform-native widgets instead of HTML. |
|
|
133
|
-
| `chrome-distiller` | Protected: browser-generated reader-mode documents, which may contain content distilled from a protected document. |
|
|
134
|
-
| `chrome-error` | Not scheme-wide protected: generic network-error documents hold no protected document content; ordinary errors must remain readable and recoverable. |
|
|
135
|
-
| `isolated-app` | Protected: installed isolated web apps have app-specific storage and privileged capabilities outside ordinary web origins. |
|
|
136
|
-
| `devtools` | Protected already: a DevTools front end can inspect a protected target. |
|
|
137
|
-
| `chrome-extension` | Protected already: extension documents and workers can contain credentials and browser privileges. |
|
|
138
|
-
| `chrome-experimental-site-token-provider` | Protected: an internal data source issues site-specific tokens. |
|
|
139
|
-
| `externalfile` | Protected: ChromeOS local/external file access must not bypass the `file` fence. |
|
|
140
|
-
| `file` | Protected already: local files must not bypass the agent's file-system fence. |
|
|
141
|
-
| `content` | Protected: Android local content-provider access must not bypass the `file` fence. |
|
|
142
|
-
| `android-app` | Not scheme-wide protected: Android app/referrer identity, not a browser UI document; extra-origin rules still apply. |
|
|
143
|
-
| `googlechrome` | Not scheme-wide protected: an external launch alias, not registered as a privileged document scheme by these entry points. |
|
|
144
|
-
| `view-source`, `blob`, `filesystem` | Not blindly scheme-wide protected: existing recursive inner-origin rules protect what they wrap. |
|
|
145
|
-
| `about` | Not blindly scheme-wide protected: existing Chromium alias rewriting protects UI while retaining inheriting blank/srcdoc URLs. |
|
|
146
|
-
| `http`, `https`, `data`, `javascript`, `ws`, `wss`, `mailto`, `webcal` | Not scheme-wide protected: ordinary web, inline content, transport or external handlers, not privileged browser UI. |
|
|
147
|
-
|
|
148
|
-
A guard must refuse navigation, creation and attach on protected schemes, including protected frames of an otherwise ordinary tab, rather than merely hiding them from discovery. `chrome-error` is deliberately not a protected scheme: ordinary DNS, connection and certificate failures produce that same generic document, so scheme-wide refusal would prevent reattaching to an ordinary error tab. Error pages hold no secrets from the failed document. This does not permit opening or attaching to a protected origin: guards still fail protected navigations before commit and enforce protected-target provenance; C1–C5 observer-only `unreachableUrl`, history and network block evidence is unchanged.
|
|
63
|
+
| none | `CATALOG` and the schemas as `&str` constants, with no dependencies |
|
|
64
|
+
| `serde` | typed `Catalog`, `catalog()`, `Catalog::browser` and `Catalog::priority` |
|
|
65
|
+
| `corpus` | the vectors (`CORPUS`, `PROTECTED`, `GUARD_VECTORS`) and the modules `corpus`, `protected` and `guard`, for tests |
|
|
149
66
|
|
|
150
|
-
|
|
67
|
+
Every feature builds for `wasm32-wasip1`. Keep `corpus` in `[dev-dependencies]` so it stays out of release builds.
|
|
151
68
|
|
|
152
|
-
|
|
69
|
+
### npm
|
|
153
70
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
**Unparseable URL boundaries.** Fail closed on command destinations: navigation and target creation, the URL and `userTypedURL` of a selected history entry, `Fetch.continueRequest` and legacy interception overrides, `Network.loadNetworkResource`, PWA install/launch, and cookie-command URLs when no domain is supplied. Also fail closed on security-relevant Chrome-reported URLs: targets, main-frame `securityOrigin`, execution-context origins, requests/redirect responses, web sockets and web transports, and service-worker scopes/scripts. A protected default execution context still cuts its session; a protected foreign isolated world stays hidden. This is not a rule to reject arbitrary strings a guard merely reads: storage-command origin keys, storage keys/cache ids, script/stack URLs, generic value redaction and storage events retain the old not-protected result for a parse failure. Cookie domains remain suffix matches, so `exa%20mple.sliccy.ai` is already covered.
|
|
159
|
-
|
|
160
|
-
The empty string is exempt (`Target.createTarget`’s blank page, an empty history `userTypedURL`, or no override); opaque-origin spellings `null` and `://` are exempt too. `about:blank` still inherits provenance, so those exemptions never erase an already protected opener or target. A failing `about:` alias is judged as the original `about:` URL, not as a parse failure. `unparseableRefusal` is exactly `slicc: refused: slicc can't parse this URL, so it could be a protected page`; a URL the guard can parse and finds protected uses its ordinary refusal instead. IDNA validation differs between WHATWG implementations: Node accepts some punycode spellings that the Go parser rejects; the canonical `.sliccy.ai` spellings remain protected by the domain rule either way.
|
|
161
|
-
|
|
162
|
-
The deliberate cost includes otherwise ordinary `http://<U+200D>.example/` and `https://exa mple.com/`: an unparseable URL is not an ordinary-origin allowlist exception. The vectors include space/percent-space, U+200D/punycode and invalid-punycode `.sliccy.ai` forms; malformed hosts, ports and IPv6; failed wrappers and unwrap-limit exhaustion; and the explicit exemptions and read-only storage-key controls.
|
|
163
|
-
|
|
164
|
-
**The vault.** `vault.sliccy.ai` is in `hosts` by name, and vault branch previews (`vault-preview.sliccy.ai`, `vault-*.sliccy.ai`) are covered by the `sliccy.ai` domain rule. `sliccy.ai` already covers them all, but vault must stay protected even if `domains` ever changes: the vault must never be a CDP target for agent tools. The one exception is slicc-cli's own pairing attach for the vault, which is the CLI's own connection and not browser control handed to the agent.
|
|
165
|
-
|
|
166
|
-
**Targets.** A target whose `TargetInfo.type` is one of `targetTypes` (`browser_ui`, Chrome's own UI such as `chrome://omnibox-popup.top-chrome/`) is always protected, whatever its URL. Otherwise a target's effective origin is its URL's (`TargetInfo.url`), except when the URL inherits the origin of whoever made the document: the empty URL, and `inheriting` with any query or fragment. Such a target is protected when it was protected before this URL (a seven tab that went to `about:blank` keeps seven's origin), or when its opener (`openerId`, else `openerFrameId`) is protected or unknown, for an opener closed before the program saw it. The URL, `openerFrameId` and `browserContextId` alone can't tell: a blank popup that seven opens has seven's origin and the URL `about:blank`. A program that holds a session to the target also has the document's real origin: the main frame's `securityOrigin` in `Page.frameNavigated`, and the default context's `origin` in `Runtime.executionContextCreated`. When one of those is protected, the target is protected too, until a URL with an origin of its own replaces it.
|
|
167
|
-
|
|
168
|
-
A same-site frame shares its parent's process and has no target of its own. Every `sliccy.ai` host is protected, and site isolation gives a seven frame inside any other site its own target, so this only matters for an extra origin whose site has other origins (another port of `127.0.0.1`, say).
|
|
169
|
-
|
|
170
|
-
**Cookies.** A cookie domain, lower-cased and without a leading or trailing dot, is protected when it is one of `domains` or under one, or when it is one of `hosts`, the host of a protected domain or an extra origin, or a parent of one: a cookie on `.example.test` reaches `https://seven.example.test`.
|
|
171
|
-
|
|
172
|
-
**Storage keys** (`Storage`, `IndexedDB`, `CacheStorage` and `FileSystem` in CDP): a key is protected when its origin (up to the first `^`) or its top-level site (a part after `^0`) is a protected URL.
|
|
173
|
-
|
|
174
|
-
**Redirect secrets.** An allowed response can name a protected destination without becoming a protected document itself. Every protected URL in a nested delivered string or object key is redacted in place to its origin, including embedded `Location`, `Content-Location`, `Refresh` and `Link` values, raw `headersText` lines, `redirectResponse`, Page URL/history/error fields, console/exception text and Audits issues. Percent-encoded representations must not retain the tail. A separate `urlFragment` alongside a protected `url` is cleared too. The event remains delivered so Network ExtraInfo pairing survives; dropping an ExtraInfo event fails these vectors. A request whose response names a protected URL has its response body refused (`slicc: refused:`), through both Fetch and Network, even though the request started at an ordinary URL. Body reads, replay and URL overrides must not follow an ordinary redirect into protected content or send its cookies. Ordinary redirect headers and bodies stay readable.
|
|
175
|
-
|
|
176
|
-
**Refusals** carry the text in `refusal`, `slicc: refused: target is on seven's origin`.
|
|
177
|
-
|
|
178
|
-
**`protected.json`** (schema `schema/protected.schema.json`): `errorPage` (the committed-error previous-decision and origin-redaction rule), `unparseable` (`protected`) and `unparseableRefusal`, in step with the catalog; `origins`, the extra origins every vector runs with; `urls` (`url`, `protected`); `targets` (`url`, `documentURL?` and `unreachableURL?` for validated main-frame error-commit evidence, `navigating?` for a main-frame navigation in flight; `type?`, the target's `TargetInfo.type`; `previous?`, whether the target was protected before; `opener?`, `protected`, `unprotected` or `unknown`, and no `opener` for a target without one; `protected`); `cookieDomains` (`domain`, `protected`); `storageKeys` (`key`, `protected`). `npm run validate` runs every vector through `protected.mjs`, and no vector uses a port from 9222 to 9229.
|
|
179
|
-
|
|
180
|
-
## Guard vectors
|
|
181
|
-
|
|
182
|
-
Every CDP guard runs the vectors in `guard-vectors.json` (schema `schema/guard-vectors.schema.json`): bios, slicc-cdp, slicc-extension and slicc-cli. The sources are listed in `sources`:
|
|
183
|
-
- slicc-cli's `protectVectors` at `2d5d981` (`test/integration/testdata/link/protect.js`), with the ids kept, and at `fe33052` for `C3.redirect-popup-stays-out` and `C3.direct-popup-stays-out`: a popup opened straight onto, or through a redirect chain to, a protected origin never reaches the client, not at open and not after the page later navigates itself and auto-attach runs again;
|
|
184
|
-
- slicc-cdp's `test/integration/guard-vectors.json` at `4907a45`, where its `E1`, `E2`, `E3`, `E5` and `B2` are renamed `E1.onload`, `E2.binding`, `E3.bypass-csp`, `E5.console` and `B2.headers`;
|
|
185
|
-
- the cert lane's E and B rows;
|
|
186
|
-
- `C5.continue-request` and `C5.continue-response`, added next to `C5.document`;
|
|
187
|
-
- the `C8` rows for privileged pages: `C8.navigate` (a page navigates to `chrome://settings`, `chrome://password-manager`, an extension page or `file:///`; refused), `C8.view-source` (the same through `view-source:`; refused), `C8.create` (`Target.createTarget` straight on those pages; refused), `C8.browser-ui-listed` (`Target.getTargets` lists no `browser_ui` target) and `C8.browser-ui-attach` (`Target.attachToTarget` by a `browser_ui` target's id; refused). All five have an unguarded control, since the guard refuses them only on the privileged target. Two cases aren't rows, because Chrome itself blocks them, so an unguarded client can't tell a guard from none: a page's `window.open('chrome://settings')`, and an `about:` alias such as `about:settings`, which `Page.navigate` and `Target.createTarget` both turn into `about:blank#blocked`. The `about:` aliases are in `protected.json` instead.
|
|
188
|
-
- slicc-cdp's owner vectors in `test/integration/guard-native.test.mjs` at `d85ac3c`: `C5.script`, a protected script subresource under the client's own `Fetch.enable` `*` at both stages (the guard neither pauses it for the client nor lets the client fulfil it, and the page runs the stand-in's real `/script`, while the control's paused request carries its headers as an object with `User-Agent` and runs the forged script), and `X1.after-exit`, the client lifecycle row below.
|
|
189
|
-
|
|
190
|
-
`X1.after-exit` (family `X`, client lifecycle) needs `clientExit`. Through the guard, the client opens an ordinary tab, registers `Page.addScriptToEvaluateOnNewDocument` with `{beacon}`, `Network.setExtraHTTPHeaders` with `x-probe: {tag}` and `Fetch.enable` `*` with a rule that fulfils the destination with a forged `FULFILLED` page, then loads the `/page?k=href&tag={tag}&to={target}/canary/{tag}` trigger whose `go()` navigates to `{target}/canary/{tag}`. Before the exit it must see its init-script beacon, its header on the trigger request and the trigger's `Fetch.requestPaused`, matched up to `&to=`, so a guard that redacts the embedded protected URL to its origin still matches: those are the positive evidence on both legs. Then the client exits, the unguarded observer calls `go()`, and the destination must be served and read as the real `canary` document, with no beacon from `{target}`, no `x-probe` on its request, no forged body and no `Fetch.requestPaused` for it reaching the client: nothing the client registered may outlive it. The control runs the same on `{plain}` through the guard, and expects the same, since an ordinary navigation after the exit must be just as clean. A guard that keeps its upstream session, or its own init scripts, headers or interception, past the client's exit fails the row.
|
|
191
|
-
|
|
192
|
-
`C4.iframe-preattach.blank` and `C4.iframe-preattach.srcdoc` read a protected frame's same-process child that committed before the client attached. The unguarded observer opens `<from>/framed` around `{target}/nest?n=<tag>&kind=blank` or `kind=srcdoc`: the protected frame holds an `about:blank` child that it fills, or an `about:srcdoc` child, each named `frame-marker-<tag>` and showing that marker, and then moves itself with `history.replaceState` to `/nest/path-secret-<tag>?query-secret=<tag>#fragment-secret-<tag>`. `from` is `{ordinary-same-site}` on the guarded leg, so both frames share the page's process under site isolation, and `{plain}` on the control. The `.shared` rows open the page from `{ordinary}` on both legs and require `siteIsolationOff`; a transport without it reports them as `skip` with `not-supported:siteIsolationOff`. The observer must find the child in the page's own frame tree and pierced document, and must see the marker or the secret through every read. Then the client attaches to the settled page and makes the same reads: `Page.getFrameTree`, `Accessibility.getFullAXTree` with the protected frame's and the child's `frameId`, `Accessibility.getPartialAXTree` of the marker element's `backendNodeId` with `fetchRelatives`, `DOM.getDocument` with `depth: -1` and `pierce: true`, `DOM.describeNode` of the child's document and `DOM.getOuterHTML` of the marker element, and `DOMSnapshot.captureSnapshot`. Each `<read>Shown` holds when a reply carries the marker or the secret path, query or fragment, and `eventsShown` when an event on the client's session does. The guarded leg expects all of them false: a refusal, an error or origin-only data passes, and a refused attach refuses every read. The control runs unguarded and expects every read to show the content. Missing evidence is `incomplete`, with `frame-read: <reason>`: no same-process child within 5000 ms (scaled by `settleScale`), an observer read that doesn't show the content (that field reads `incomplete`), a client read or attach that was never answered (an error that says `timed out` or `closed`), or a control attach without a session.
|
|
193
|
-
|
|
194
|
-
The `C9` foreign extension isolated-world family requires `extensionWorld`: a browser hosting a foreign content-script extension on the ordinary fixture page. `C9.world-hidden` requires the page default context to remain visible after `Runtime.enable`, while the extension context and Runtime/Debugger identifiers remain hidden without cutting or detaching the session. `C9.world-select` probes numeric and unique context selectors, object and argument handles, promises, properties, named bindings and DOM node resolution; each command selecting the hidden world must fail with the `slicc: refused:` prefix, never an ordinary browser error. Numeric guesses also sweep ids 1–32: an id not exposed to this client must be refused, while every exposed numeric context must remain usable; none of those guarded probes may reach the fixture content-script marker. The marker combines the content script’s global lexical `root` binding with its extension-id DOM marker and `chrome.runtime.id`, and is independently required to be absent from the page default world. `C9.world-debugger` pauses inside that content script: the control must reach its frame, while the guard must refuse or hide it. The control waits up to 5000 ms (scaled by `settleScale`) for `Debugger.paused`; when it never pauses, `reachable` records `"incomplete"`, not `false`, and the row fails with `extensionWorld debugger: no Debugger.paused within <ms>ms`. The guarded leg waits up to 1500 ms, scaled by `refusalScale`. `C9.page-drivable` evaluates `self.origin`, captures a DOM snapshot and dispatches an Input click on the same ordinary page; cutting the entire session fails this regression row. All four use an unguarded control, the default browser context and observer discovery of the real extension world, not an invented isolated world. Protected default contexts remain session-cutting evidence; hiding a foreign isolated world does not weaken any seven-origin row.
|
|
195
|
-
|
|
196
|
-
The integration launcher loads shared-web 1.25.2’s harness extension exactly once through `Extensions.loadUnpacked` (`ankmbodmgimjemnojmeddfcdlbocolob`), whose content script runs on HTTP(S) pages. shared-web 1.25.2 also loads it by default in its own harness. A transport unable to host a foreign content-script extension omits `extensionWorld` and reports `not-supported:extensionWorld`; an advertised capability with no observed world fails, rather than silently skipping.
|
|
197
|
-
|
|
198
|
-
`C2.redirect-secret` probes ordinary fixture 302s to seven’s `/auth/callback?code=…&state=…` and vault’s `/oauth/mcp-callback?code=…`, plus `#access_token=…` variants of both. It navigates with Page and scripted `location.href`, fetches a subresource and sends an XHR. After each one it waits, at least 250 ms and up to two seconds scaled by `settleScale` (stall-tolerant), for the fixture to log the redirect URL and every request seen for it to end, before it navigates away. On the guarded leg, once one action's wait times out, the remaining actions skip it, so a guard that never terminates costs that bound once per leg. The 302 carries `Location`, `Content-Location`, `Refresh: 0;url=…` and `Link: <…>; rel=preconnect`. Page, Network (including ExtraInfo), Runtime, Log, Audits and both Fetch stages are enabled. Every received raw message is scanned, including replies; only traffic proven to belong to the shared unguarded observer is excluded. The scan runs across an eight-second tail, for raw, percent-encoded, JSON-escaped and base64 marker representations. Both response-body APIs are attempted on each hop; guarded bodies need matched refusals. `Network.getResponseBody` is attempted, once per id, with every network id the client learns for a 301–308 hop: the `requestId` of a Network event carrying the redirect status and the `networkId` of the Fetch response-stage pause. For each registered hop whose id the client learned neither way, the probe reads the hop’s real id from the harness’s own unguarded tap (the observer session, which enables Network on the guarded leg) and attempts the read on the guarded session with it. The evidence’s `networkBodyCarriers` counts the ids per carrier, and `networkBodyPath` names the weakest carrier any id needed: `withheld+tap`, then `fetch-networkId`, then `network`, or `none` without any id. These reads never hold up a Fetch pause, and the probe gives them five seconds after `Fetch.disable` to settle, and again after the terminal wait for reads that late events started. A read still unanswered then is not a refusal. `networkBodyAttempted` and `networkBodyRefused` hold only when every attempt is refused. On the guarded leg a `fetch-networkId` or `withheld+tap` carrier also needs the same row’s unguarded control to have seen the redirect’s Network events. Without that proof, or with no id at all, both read `incomplete` and the probe returns `redirect-secret: network body leg <reason>`; an attempt that is not refused is measured and fails. Every request the client sees for a registered redirect URL, through its initial `Network.requestWillBeSent` or a redirect `requestWillBeSent` whose `redirectResponse.url` is that URL, must end within the step in a redirect `requestWillBeSent`, `loadingFinished` or `loadingFailed` for that `requestId`. Before the evidence is evaluated, on every leg, the probe waits up to five seconds scaled by `settleScale` (stall-tolerant) for every seen request to reach one of those events. The evidence’s `hops` records per `requestId` the `delivery` (`redacted` for an origin-only protected redirect in place, `withheld` for a terminal without the redirect, `ordinary`, `raw` or `unterminated`) and the `terminal` event method. On the guarded leg `requestsTerminated` is false on any `unterminated` request after that wait, a measured guard failure rather than missing evidence; on the unguarded control an `unterminated` request is missing evidence, so `requestsTerminated` reads `incomplete` and the probe returns `redirect-secret: control requests <ids> unterminated`; `hopsRedacted` is false on any `raw` or `ordinary` hop, so a delivered protected hop must be origin-only, never the raw or a substituted URL; the guarded leg expects it. Page frame trees, resource trees and navigation history are read before recovery to catch error-page leftovers. Recovery and each following action use a fresh ordinary tab, never a tab whose history contains the protected destination. Each request's body reads stay on its owning client session; all probe tabs and observers remain alive through the eight-second tail and terminal/body-read waits. A closed body owner leaves that read `incomplete`, not `sessionEnded`; another live owner of the same request id is still read. A closed retained tab does not cut the tail or the remaining actions short. `sessionSurvives` proves the client can evaluate the title in a fresh ordinary tab. A content-command refusal in a protected-history tab is not itself a failure; every received message still enters the leak checks. Missing recovery evidence or an unseen override pause is `incomplete`, not a pass. A measured leak takes precedence over incomplete evidence. A separate structural check requires every protected `Network.Request.urlFragment` and `Page.Frame.urlFragment` to be empty or absent, even if its value contains no marker; the ordinary `#access_token` control independently requires fragment evidence in request events, frame events, frame-tree replies and resource-tree replies. The ordinary session must survive. The unguarded ordinary redirect control preserves all four headers in `redirectResponse`, ExtraInfo’s headers and `headersText`, and Fetch’s response headers. `C2.redirect-secret-mix` repeats that matrix with 301/303/307/308. The protected leg requires in-place ExtraInfo delivery with origin-only header values, not merely the absence of a marker.
|
|
199
|
-
|
|
200
|
-
`C2.redirect-body` calls `Network.loadNetworkResource` on an ordinary redirect with `includeCredentials: true`, reads any stream with `IO.read`, replays a redirected XHR, requests Page resource content, and captures Page/MHTML and DOM snapshots after a main-frame redirect. It also captures an ordinary parent containing an iframe redirected to vault, including its error document, and overrides a paused ordinary Fetch request to the allowed redirect URL. No protected marker may appear in any message or body, and no request from those operations may send the independently seeded seven cookie. The ordinary control must return the body, resource content and snapshots, and accept the URL override. Its iframe MHTML and DOM reads are polled until both show the marker, for up to 5000 ms scaled by `settleScale`, with the stall-tolerant `waitFor` bound. A channel that never shows it reads `incomplete`, while a measured channel stays true; the control is incomplete, never a pass or a failure solely for missing iframe evidence. Guarded leak checks remain measured on every received reply. Its network-body carrier, `networkBodyAttempted`, `networkBodyRefused`, `hops`, `requestsTerminated` and `hopsRedacted` follow `C2.redirect-secret`. Each probe cleans up its redirect registrations, observer session and cookie. The focused Chrome tests use HTTPS with SPKI-pinned fixture trust and default-port protected URLs; host resolver rules route seven and vault to the fixture rather than the public service.
|
|
201
|
-
|
|
202
|
-
Every C2 redirect probe reports `sessionEnded`. It becomes true when the guarded client’s active session ends during the probe: a `Target.detachedFromTarget` for that active session unless its owner is already known closed, a non-body CDP reply that the active session is closed or not found, or that the websocket or transport is closed. `Inspected target navigated or closed` is an ordinary reply on a live session, not a session end. When an expected Fetch pause or XHR event is missing, the probe sends one `Runtime.evaluate` to check that the session is still alive. Once the session has ended, the probe sends nothing more on it, skips the eight-second tail and the termination wait, and evaluates what the client received before the end. `secretHidden`, `protectedBodyHidden`, `cookieHidden`, `fragmentsHidden` and `hopsRedacted` are measured on those messages, replies, bodies and fixture logs, so a guard that leaked before the end still fails them. A clean end passes them. `sessionSurvives` reads false. Checks that need evidence from after the end read `incomplete`, with `redirect-secret: session ended (<how>)` in the probe’s `incomplete`. If the end cuts the probe’s action matrix short, or leaves any body read pending or unanswered, then `fetchBodyAttempted`, `fetchBodyRefused` and the network-body leg read `incomplete`, even after some refused reads. Any answer that isn’t a refusal still fails its refusal check. `requestsTerminated` reads `incomplete` when requests are unterminated or the matrix was cut short. A short but origin-only `extraLocation` reads `incomplete`. So does each attempt or override check whose command never reached the client: it wasn’t sent because the session had already ended, or its reply was the session end. On the unguarded control, a session end is an error: the probe throws `redirect-secret: control session ended (<how>)`.
|
|
203
|
-
|
|
204
|
-
`C8.unparseable` probes both raw and canonical space/U+200D/invalid-punycode URLs through creation and navigation. Every guarded destination needs a matched refusal. Its unguarded controls record `document` or `native-rejection` outcomes, including Chrome builds that reject raw U+200D instead of committing it. Both raw and encoded-space forms must commit real fixture documents, because that is the proven cookie leak. An independent observer loads the encoded-space document and confirms it shares a uniquely seeded `.sliccy.ai` cookie; the guarded client must neither list nor attach to it, read its cookie, query that cookie by URL, nor cause a cookie-bearing destination request. The guard does not erase the observer’s private browser state. The row runs on both browser-session and page-session-only transports, uses the default context, and cleans up its own targets and cookie. Cookie seeding always uses the unguarded observer’s browser session, never the guarded client. Only guarded browser-session target listing requires `browserSession`; without it, `listed` records `not-supported` explicitly in probe and observation evidence, and the result lists that field in `notSupported`. Creation, navigation, attachment and `Network.getCookies` with `urls` still require matched refusals; filtering cookies without refusing fails `cookieRefused`. Fixture URLs omit the colon when the port is empty, and reported URLs are compared after canonicalisation, including Chrome’s WHATWG-unparseable encoded-space hosts. Launchers must resolve the raw/canonical space, U+200D/punycode and invalid-punycode `.sliccy.ai` fixture hosts to the fixture server, including for default-port HTTP or HTTPS origins. The HTTP fixture accepts Chrome’s encoded-space Host header without reparsing it as a WHATWG origin.
|
|
205
|
-
|
|
206
|
-
`C8.chrome-search` requests `chrome-search://most-visited/` through `Page.navigate` and requires an explicit guard refusal. Its unguarded control requires a CDP response, not a successful document commit: Chrome can answer with `errorText` when that internal data source is unavailable outside an Instant process. Browser-side load failure is not a guard refusal, and merely hiding the target must fail this row.
|
|
71
|
+
```sh
|
|
72
|
+
npm install @ai-ecoverse/slicc-browsers
|
|
73
|
+
```
|
|
207
74
|
|
|
208
|
-
|
|
75
|
+
```js
|
|
76
|
+
import catalog from '@ai-ecoverse/slicc-browsers' with { type: 'json' };
|
|
77
|
+
import { protectedOrigins } from '@ai-ecoverse/slicc-browsers/protected.mjs';
|
|
78
|
+
```
|
|
209
79
|
|
|
210
|
-
|
|
80
|
+
| export | what it is |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `.`, `./catalog.json`, `./corpus.json`, `./protected.json`, `./guard-vectors.json` | the data |
|
|
83
|
+
| `./schema/<name>.schema.json` | the schemas |
|
|
84
|
+
| `./protected.mjs` | the protected-origin matcher |
|
|
85
|
+
| `./seven.mjs` | `isSevenLabel(seven, label)`: whether a `sliccy.ai` host label can be a seven instance |
|
|
86
|
+
| `./guard-runner`, `./guard-driver.mjs` | the guard vector runner and its helpers |
|
|
87
|
+
| `./fixture`, `./fixture-client` | the test server (Node only) and a client for a remote one |
|
|
211
88
|
|
|
212
|
-
|
|
89
|
+
Everything except `./fixture` runs in a browser as well as in Node. `protected.mjs` and `seven.mjs` import nothing, so an extension or a service worker can ship either file on its own.
|
|
213
90
|
|
|
214
|
-
|
|
91
|
+
## The catalog
|
|
215
92
|
|
|
216
|
-
`
|
|
93
|
+
`catalog.json` holds everything a program needs to know about browsers:
|
|
217
94
|
|
|
218
|
-
|
|
95
|
+
- **Browsers.** Chrome, Edge, Brave, Opera, Vivaldi, Arc, Atlas, Dia and their pre-release channels, Chrome for Testing and Chromium, **in priority order**. Each has its macOS bundle, Windows install roots and registry keys, Linux binaries, `.desktop` ids, flatpak and snap names, and its default user-data dir.
|
|
96
|
+
- **Electron apps**: known apps by bundle id, and the ones that block remote debugging.
|
|
97
|
+
- **Ranking**: endpoints with a seven tab come first, then other running endpoints, then installed Chrome, other installed Chromium browsers, Electron apps, and SLICC six's profile last. Browsers older than Chromium 116 are left out.
|
|
98
|
+
- **Discovery**: `DevToolsActivePort` files, the ports to probe, the process scan, and how endpoints are named, normalized and merged.
|
|
99
|
+
- **Seven**: which `sliccy.ai` hosts are seven instances, and the switches, disabled features and profile prefs a launch for seven needs.
|
|
100
|
+
- **Profiles and platforms**: where SLICC keeps its browser profiles, and the config dirs and registry keys per OS.
|
|
101
|
+
- **Protected origins**: see [below](#protected-origins).
|
|
219
102
|
|
|
220
|
-
|
|
103
|
+
The catalog is `"schema": 1`. Minor releases only add fields; changing or removing one takes schema 2 and a major release. slicc-cli keeps no browser tables of its own: to add or change a browser, change `catalog.json`, release, and bump the dependency.
|
|
221
104
|
|
|
222
|
-
|
|
105
|
+
## Discovery
|
|
223
106
|
|
|
224
|
-
A
|
|
107
|
+
A program lists the browsers it can attach to, from four sources: `DevToolsActivePort` files in known user-data dirs and SLICC profiles, `--cdp` URLs, a probe of ports 9222 to 9229, and the process list. It ranks the endpoints, adds the installed browsers it could launch, and prints the list in the shape of [`schema/browsers.schema.json`](schema/browsers.schema.json). [slicc-cli's README](https://github.com/ai-ecoverse/slicc-cli#slicc-browsers---json---cdp-url---no-bundle-scan) describes that output. Seven's tabs are reported by origin only, never with a path, query or title.
|
|
225
108
|
|
|
226
|
-
|
|
227
|
-
- `schemaVersion` (1): guards pin it.
|
|
228
|
-
- `setVersion`: the package version. The release flow stamps it into the npm package and the crate (`scripts/set-crate-version.mjs`). In git, and so in the Go module, it reads `0.0.0`; Go code gets the version from the module.
|
|
229
|
-
- `targets`: the names a runner maps to a scheme, host and port: `seven`, `vault`, `vault-x` (a vault branch preview), `apex`, `own` (the client page's origin, `{client}`), `ordinary`, `ordinary-same-site`, `chrome-newtab`, `chrome-new-tab-page`, `chrome-settings`, `chrome-passwords`, `chrome-search`, `network-error`, `chrome-extension-page` and `file-root`. Each has a `description` and, for the protected ones, a default `host`. `chrome-newtab` (`chrome://newtab/`), `chrome-new-tab-page` (`chrome://new-tab-page/`), `chrome-settings` (`chrome://settings/`), `chrome-passwords` (`chrome://password-manager/`), `chrome-search` (`chrome-search://most-visited/`), `network-error` (`http://nonexistent.invalid/`), `chrome-extension-page` (a page of Chrome's built-in PDF viewer extension) and `file-root` (`file:///`) carry a `url`, which the runner uses unless `targets` maps the name to something else. No other URL in the file is one a runner must use.
|
|
230
|
-
- `variants`: the detach variants in ms. `prompt` and `lagging` are documented; a runner may add others, such as `lagging-8s`, and reports the name it ran.
|
|
231
|
-
- `capabilities`: what a transport may lack. `autoAttach` covers `Target.setAutoAttach`, `children` steps and child sessions; `browserSession` covers calls on the browser session; `browserContexts` covers `Target.createBrowserContext` and `Target.disposeBrowserContext`; `extensionWorld` means it can host a foreign content-script extension isolated world on the ordinary fixture; `discovery` means session-less `Target.setDiscoverTargets` and `Target.targetCreated`/`Target.targetInfoChanged` delivery; `chromeUrlsAttach` means the transport can attach to and drive `chrome://` pages. CDP over a DevTools port can; `chrome.debugger` can't, since Chrome refuses it every `chrome://` page, even with `--extensions-on-chrome-urls`. `clientExit` means the harness can close the guarded client in the middle of a row: the client has `close()`, the `clientFactory` makes a fresh guarded client for the next leg, and an unguarded observer independent of the client outlives it. `siteIsolationOff` means the browser runs without site isolation, so a cross-site iframe shares its parent's process.
|
|
232
|
-
- `placeholders`: every `{name}` used in the vectors, with what it stands for. `{client}` is the client page's origin, which the runner maps.
|
|
109
|
+
### The process source
|
|
233
110
|
|
|
234
|
-
|
|
235
|
-
- `id`, and `family` (`C`, `E`, `B` or `X`, the id's first letter);
|
|
236
|
-
- `kind`: `cdp` (runnable steps) or `text` (rows in words, which runners report as `not-run`, never `pass`);
|
|
237
|
-
- `scope`: `any`, or the one guard the vector is for;
|
|
238
|
-
- `setup` and `trigger`, both lists of steps;
|
|
239
|
-
- `targets`: the protected or reachability targets it runs against;
|
|
240
|
-
- `requires?`: the capabilities needed for the whole vector, including its steps' `requires`. Observation-only capabilities may be omitted here to keep independent legs runnable. A runner whose transport lacks one reports the vector as `skip` with the reason `not-supported:<capability>`. A row with a `page-or-browser` cookie step that would carry a `browserContextId` reports `not-supported:browserSession` without a browser session, because that parameter is only allowed on the browser target;
|
|
241
|
-
- `observe?`: each observed name, as `{ "means", "dependsOn"?, "requires"?, "eitherWhen"? }`. `requires` gates only that observation when it is not also a whole-vector requirement: unavailable evidence is recorded as `"not-supported"`, excluded from that field's expectations, and reported in the row's `notSupported` list. `dependsOn` lists the refusals the observation depends on (see Refusals). `eitherWhen` is a check: when it holds on the guarded leg, that observation's expectation becomes `"either"` there (see Design-dependent outcomes);
|
|
242
|
-
- `live`: the liveness check;
|
|
243
|
-
- `control` (`origin: "ordinary"`, `via`, `expected`) and `expected`: an object of names and booleans, or text;
|
|
244
|
-
- `skip?` (`target`, `reason`): `reason` starts with a code, as in `not-applicable: …`. A runner reports the row on `target` as `skip` with this `reason`, unless a capability is missing.
|
|
111
|
+
The process scan finds browsers that something else started with `--remote-debugging-port`. It reads `ps` on Unix and `Get-CimInstance Win32_Process` on Windows, skips wrappers such as `node`, `npx` or a shell, and takes the port and the `--user-data-dir` from the command line, even when the path has spaces. With port 0 it reads the port from that dir's `DevToolsActivePort`. The exact parsing rules are in `catalog.json`'s `discovery.process` and the corpus's `process-*` cases. Tests always fake the process list.
|
|
245
112
|
|
|
246
|
-
|
|
247
|
-
Any `cdp`, `page` or `children` step can take `requires`, the capabilities it needs: `autoAttach` for `Target.setAutoAttach`, `children` and `session: "child"`, `browserSession` for `session: "browser"`, `browserContexts` for `Target.createBrowserContext` and `Target.disposeBrowserContext`, and `chromeUrlsAttach` for a step that must attach to a `chrome://` page to pass.
|
|
248
|
-
- `{ "cdp": method, "params"?, "session"?, "as"?, "refusable"?, "refusal"? }`: a CDP call. It runs on the page session by default, on the browser with `browser`, on either with `page-or-browser` (the driver uses the page session), or on the current auto-attached target with `child`. `as` names its outcome. `refusable: true` with `refusal: { "match", "onlyProtected"? }` (a regular expression for the error message or code) marks a call that a guard may refuse at registration.
|
|
249
|
-
- `{ "page": action, … }`:
|
|
250
|
-
- `tab`: a new tab at `url`, in `context`, which becomes the page. `background` defaults to `true`;
|
|
251
|
-
- `navigate`: go to `url` and wait for the load;
|
|
252
|
-
- `href`, `replace`, `form`, `post`: navigate from the page, 50 ms later;
|
|
253
|
-
- `open`: `window.open` with a user gesture;
|
|
254
|
-
- `iframe`: add an iframe;
|
|
255
|
-
- `framed`: load the `/framed` page from `from`, with `url` in an iframe;
|
|
256
|
-
- `fetch`, `xhr`, `image`, `script`: load `url` and name the body (`width N` for an image, `String(globalThis.vectorScript)` after a `<script src>` loads, `error` when it fails, or `<status> <body>` with `status`); `background` doesn't wait;
|
|
257
|
-
- `eval`: evaluate `expression`.
|
|
258
|
-
`hops` routes `url` through `/hop` on each `origin`, with its `code`.
|
|
259
|
-
- `{ "answer": { "url", "stage"?, "cdp"?, "params"?, "hold"? } }`: answer each paused request or auth challenge for `url` with that call; `hold` leaves it paused. Every other paused request continues unchanged.
|
|
260
|
-
- `{ "children": steps, "where"? }`: run these steps on each target auto-attached from then on, then resume it. `where` scopes them to the row's own target: `{ "openerId": "page" }` (opened by the row's page) or `{ "urlPrefix": url }`; other attached targets are resumed and otherwise left alone. When a vector has a `children` step with `where`, a target that attaches before that step runs waits until it does, and is then matched against `where`. A target is only resumed when it attached waiting for the debugger, and a `browser_ui` target (Chrome's own UI, such as `chrome://omnibox-popup.top-chrome/`) is never adopted or sent anything.
|
|
261
|
-
- `{ "settle": { "beacon"?, "served"?, "title"?, "ms"?, "variant"?, "session"?, "dependsOn"?, "or"? } }`: wait for a beacon from `beacon`, for the fixture to log a request for the URL `served`, for the page `title`, or for the session to detach. When a refusal in `dependsOn` was recorded, the beacon can't come: a settle with `or` (a check) then waits until `or` holds, and one without it is skipped. Without such a refusal, `or` plays no part. Each beacon settle after an init script depends on that script's registration, and its `or` is what the row reads: the hops and the destination for the redirect chains, or the served page otherwise. It waits up to `ms` (10000 by default), then another 600 ms. `variant` waits the run's detach variant instead.
|
|
262
|
-
- `{ "wait": ms }`.
|
|
263
|
-
- `{ "paused": { "url", "ms" } }`: wait until the client has seen the pause that an `answer` rule for `url` matches (`Fetch.requestPaused` at the rule's stage, or `Fetch.authRequired` for a `Fetch.continueWithAuth` rule). It waits up to `ms`. On the control leg, a pause that never comes fails the row with `paused: no pause for <url> within <ms>ms`. On the guarded leg the guard may hide that pause, so the step waits the full `ms` and doesn't fail. The C5 guessed-id vectors and `C5.auth` use it before their second request, so the probe's interception id falls between the two ids the guess spans.
|
|
264
|
-
- `{ "guess": { "between", "cdp", "params" }, "as" }`: the call for each request id between the ids paused for the two URLs.
|
|
265
|
-
- `{ "client": "exit" }`: the guarded client exits. The runner calls its `close()`, waits up to 5000 ms (scaled by `settleScale`) for the page target to be no longer attached, noting in `notes` when it stays attached, attaches its unguarded observer to the page, and runs every later step of the leg through that observer. Observations still read the exited client's events. It runs only at the top level of `setup` or `trigger`, and the vector requires `clientExit`.
|
|
266
|
-
- `{ "text": … }`: a step in words.
|
|
113
|
+
### SLICC six's profiles
|
|
267
114
|
|
|
268
|
-
|
|
115
|
+
SLICC six, the previous generation, launches Chrome in a profile of its own (`browser-coding-agent-chrome` under six's profile root, or `SLICC_USER_DATA_DIR`). Discovery lists such an endpoint, marks it `six: true` and ranks it last, but **never picks it by itself**. Only an explicit request, such as `slicc --profile six`, uses it. The roots per OS are in `catalog.json`'s `profiles.six`.
|
|
269
116
|
|
|
270
|
-
|
|
271
|
-
- `/doc?n=`: an HTML page with the `data-canary` marker; `listener=1` adds `<script src="/listener.js?n=<n>">`; `csp=1` adds `Content-Security-Policy: script-src 'none'` and an inline script that sends a beacon tagged `inline-<n>` to the log, which the CSP blocks unless it is bypassed;
|
|
272
|
-
- `/hop?code=&to=` and `/framed?src=`;
|
|
273
|
-
- `/listener.js`: one line registering a window `load` listener, served with `SourceMap: /listener.js.map`;
|
|
274
|
-
- `/data?n=`: `real-<n>`, with CORS;
|
|
275
|
-
- `/script?n=`: a script setting `globalThis.vectorScript` to `real-<n>`;
|
|
276
|
-
- `/page?k=&to=`: a trigger page whose `go()` sends the page to `to` (`k=href`, `replace`, `popup` or `form`; `deferred` runs `href` itself after `defer` ms, default 3000), and `/canary/<tag>`: a document titled `canary` with the marker;
|
|
277
|
-
- `/nest?n=&kind=` (and any path under `/nest`): the `C4.iframe-preattach` frame, with its `about:blank` or `about:srcdoc` child;
|
|
278
|
-
- `/img`: a 1×1 PNG;
|
|
279
|
-
- `/auth`: Basic `u`:`p`, then `authed`;
|
|
280
|
-
- `/rewritten` (title `rewritten`);
|
|
281
|
-
- `/plant`: sets the canary;
|
|
282
|
-
- `/beacon`, and `/log?n=`, which returns `{ hits, beacons }`.
|
|
117
|
+
## The corpus
|
|
283
118
|
|
|
284
|
-
|
|
119
|
+
Each case in `corpus/<case>/` is a fake machine: a home directory, `DevToolsActivePort` files, fake `/json/version` and `/json/list` answers, a fake process list, `--cdp` URLs and installed browsers. `expected.json` holds the ranked list it must produce. `case.json`'s `needs` say what a program must support to run the case (`process`, `probe`, `six`, `tabs`, …), and a program skips cases it can't run.
|
|
285
120
|
|
|
286
|
-
|
|
121
|
+
For each case, a test harness:
|
|
122
|
+
1. writes the case's files into a temporary home;
|
|
123
|
+
2. points discovery at that home and the case's environment only;
|
|
124
|
+
3. feeds it the fake process list, and routes every HTTP request and TCP dial to the fakes;
|
|
125
|
+
4. compares the result with `expected.json`.
|
|
287
126
|
|
|
288
|
-
|
|
127
|
+
Both libraries come with helpers for this:
|
|
289
128
|
|
|
290
|
-
|
|
129
|
+
```go
|
|
130
|
+
cases, _ := sliccbrowsers.Corpus()
|
|
131
|
+
for _, c := range cases {
|
|
132
|
+
if !c.Runs(runtime.GOOS, sliccbrowsers.NeedFlag, sliccbrowsers.NeedProcess) {
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
l := sliccbrowsers.Layout{OS: runtime.GOOS, Home: t.TempDir(), Getenv: func(k string) string { return c.Input.Env[k] }}
|
|
136
|
+
// write c.Files(l); fake c.PSOutput(l, os.Getpid()); use c.Transport() and c.DialContext;
|
|
137
|
+
// compare with c.ExpectedJSON(l)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
291
140
|
|
|
292
|
-
|
|
141
|
+
```rust
|
|
142
|
+
use slicc_browsers::corpus::{corpus, Layout, NEED_PROCESS, NEED_PROFILES};
|
|
293
143
|
|
|
294
|
-
|
|
144
|
+
let cat = slicc_browsers::catalog();
|
|
145
|
+
for case in corpus().cases {
|
|
146
|
+
if !case.runs(os, &[NEED_PROFILES, NEED_PROCESS]) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let env = |k: &str| case.input.env.get(k).cloned();
|
|
150
|
+
let layout = Layout { os, home, env: &env, catalog: &cat };
|
|
151
|
+
// write case.files(&layout); fake case.ps_output(…); answer with case.http_get(…);
|
|
152
|
+
// compare with case.expected(&layout)
|
|
153
|
+
}
|
|
154
|
+
```
|
|
295
155
|
|
|
296
|
-
`
|
|
156
|
+
slicc-cli's `test/integration/corpus_test.go` is a complete harness. The corpus never uses ports 9222 to 9229, where a developer's own Chrome may be listening.
|
|
297
157
|
|
|
298
|
-
|
|
158
|
+
## Protected origins
|
|
299
159
|
|
|
300
|
-
|
|
160
|
+
Browser control must never navigate to, open, attach to, or read the cookies or storage of:
|
|
161
|
+
- **seven and the vault**: every `sliccy.ai` host, on any port and scheme, and `vault.sliccy.ai` by name;
|
|
162
|
+
- **the browser's own privileged pages**: `chrome://` and the other Chromium browsers' equivalents, `devtools://`, `chrome-extension://` (where password managers and wallets live), `file://`, and Chrome's own UI targets. The new tab page is the one exception;
|
|
163
|
+
- **a program's extra seven origins**, such as slicc-cli's `--origin`.
|
|
301
164
|
|
|
302
|
-
|
|
165
|
+
URLs are judged the way the browser parses them, so `blob:`, `view-source:` and `about:` aliases, backslashes, encoded dots and full-width letters can't sneak past. A URL the guard can't parse counts as protected wherever the browser might load it. A tab that seven opened keeps seven's origin when it goes to `about:blank`. A refused call fails with `slicc: refused: target is on seven's origin`.
|
|
303
166
|
|
|
304
|
-
|
|
167
|
+
`catalog.json`'s `protectedOrigins` holds the rules, and `protected.json` the vectors: URLs, targets, cookie domains and storage keys, each with the expected answer. `protected.mjs` is the reference implementation:
|
|
305
168
|
|
|
306
|
-
|
|
169
|
+
```js
|
|
170
|
+
import catalog from '@ai-ecoverse/slicc-browsers' with { type: 'json' };
|
|
171
|
+
import { protectedOrigins } from '@ai-ecoverse/slicc-browsers/protected.mjs';
|
|
307
172
|
|
|
308
|
-
|
|
173
|
+
const guard = protectedOrigins(catalog.protectedOrigins, ['http://127.0.0.1:5710']);
|
|
174
|
+
guard.url('https://seven.sliccy.ai/');
|
|
175
|
+
guard.cookieDomain('.sliccy.ai');
|
|
176
|
+
guard.target({ url: 'about:blank', opener: 'protected' });
|
|
177
|
+
```
|
|
309
178
|
|
|
310
|
-
|
|
179
|
+
It also offers `storageKey(key)` and `unparseable(text)`. A Go or Rust implementation needs a WHATWG URL parser, not `net/url`, and must pass every vector in `protected.json`.
|
|
311
180
|
|
|
312
|
-
|
|
181
|
+
## Guard vectors
|
|
313
182
|
|
|
314
|
-
|
|
183
|
+
A guard sits between a CDP client, such as an agent's browser tool, and the browser, and keeps the client away from protected origins. `guard-vectors.json` holds more than a hundred attacks on that promise, and every guard must pass them all. They include navigation, redirect chains, popups and frames, request interception, cookie and storage access, privileged pages, error pages, foreign extension worlds, debugger and instrumentation tricks, and session overrides that would leak into a protected page.
|
|
315
184
|
|
|
316
|
-
|
|
317
|
-
{ "id": "C1.script-nav", "target": "vault", "variant": "lagging", "transport": "cli", "via": "guard", "control": "pass", "guarded": "pass", "live": "pass" }
|
|
318
|
-
```
|
|
185
|
+
Each vector runs twice. The **control** runs it against an ordinary origin, where it must work. The **guarded** leg runs it through the guard against a protected target, where it must be refused or have no effect. A **liveness** check, made outside the guard, proves the protected target was really there. A row passes only when all three do, so a guard can't pass by breaking everything, and a test can't pass by never reaching the target.
|
|
319
186
|
|
|
320
|
-
|
|
321
|
-
- `send(method, params, sessionId)`: returns a promise of the result, and rejects with an error carrying `cdp`, the CDP error;
|
|
322
|
-
- `waitEvent(predicate, ms)`: resolves with the first matching event `{ method, params, sessionId }`;
|
|
323
|
-
- `events`: every event received so far.
|
|
324
|
-
|
|
325
|
-
It exports:
|
|
326
|
-
- `beacon(log, tag)` and `probe(log, tag)`: the sources for `{beacon}` and `{probe}`;
|
|
327
|
-
- `canary.plant(value)`, `canary.read()` and `canary.untouched(value, read)`: the canary in `localStorage`, IndexedDB and the stand-in's marker;
|
|
328
|
-
- `hop`, `expand(value, vars)`, and `steps(vector, side, vars)`, a leg's expanded steps, with `refusal.name` on each refusable step and `background` set on each `tab` step;
|
|
329
|
-
- `refusals(vector)`: the vector's refusal names;
|
|
330
|
-
- `outcome`, `call(client, step, sessionId)` and `isRefusal(outcome, step)`;
|
|
331
|
-
- `instrument(client, sessionId, log, tag)`, `load(client, sessionId, url)` and `popup(client, openerId)`;
|
|
332
|
-
- `waitFor(check, ms, step?)`: polls `check` every `step` ms (default 25) until it holds, checking once more after the bound. A timer heartbeat every `step` runs for the whole wait, including while `check` is awaiting a reply, and any gap between beats longer than a second (or four steps, if longer) extends the bound by that gap, so a synchronous event-loop stall elsewhere in the process does not consume it. The extensions add up to at most `max(4 × ms, 30000)` ms, so a throttled page cannot stretch a wait without limit. `load`, the runner's waits and the error-page and redirect-body probes use it;
|
|
333
|
-
- `judge(vector, side, observed)`: `{ pass, refused, skipped, mismatches }`, where `refused` lists the leg's refusals and `skipped` the observations that depend on them or explicitly record unavailable observation capabilities;
|
|
334
|
-
- `result(vector, run)`: a report row from `{ target, variant, transport, via?, capabilities?, control, guarded, live }`. `capabilities` is the transport's; without it, the transport has them all;
|
|
335
|
-
- `skipReason(vector, target, scope)`: the reason the runner skips the row with when it has every capability: the vector's `skip.reason` when its `skip.target` is `target`, `not-applicable` when its `observable[target]` is `false` or its `scope` is neither `any` nor `scope`, otherwise `null`; `inapplicable(vector, target, scope)` is whether it isn't `null`;
|
|
336
|
-
- `knownOrigins` and `originDefaults`: the origin names a runner can miss, and the origins `plain`, `cross`, `log` and `own` fall back to when unset;
|
|
337
|
-
- `parseURL(value, base?)`: a `URL`, or `null` for anything `new URL` rejects or a non-string; every parse of a fixture log entry or a Chrome-reported URL in the runner and the fixture goes through it (`protected.mjs` keeps its own copy, since it imports nothing), so an unparseable entry never throws;
|
|
338
|
-
- `owned(entries, tag)`: the fixture log entries whose raw URL contains `tag`, the row's attribution through its `n=`, `tag=` or `to=` query;
|
|
339
|
-
- `summary({ guard, sha, set, results })`: `set` is the parsed `guard-vectors.json`. It returns `{ set, schemaVersion, guard, sha, variants, targets, transports, results, skipped, notRun, missing, forged, pass, line }`. `missing` lists each `{ id, target, variant, transport }` without a row, for every vector whose `scope` is `any` or the guard, every target it lists, every variant in `variants` or `results`, and every transport in `results`; `pass` needs it empty, where `line` is `GUARD-PASS <guard>@<sha> (vectors @ai-ecoverse/slicc-browsers@X.Y.Z)` or `GUARD-FAIL …`. `forged` lists each `{ id, target, variant, transport, why }` that is `not-run` on a vector that isn't `kind: "text"`, or `skip` without a reason the runner gives (`skipReason(vector, target, guard)`, the reason the runner gives: the vector's own `skip.reason` on its `skip.target`, or `not-applicable` for a target it marks unobservable or a `scope` other than `any` and the guard; `not-applicable:no-<origin>-origin` for an origin in `knownOrigins` that the row's target, `plain`, the control's origin, the steps or a check names, directly or through `originDefaults`, where only the first branch of an `any` counts, or `not-supported:<capability>` for a capability the set requires, `browserSession` or `unguarded-observer`); `pass` needs it empty too, and `line` then ends in `forged <n>`.
|
|
340
|
-
|
|
341
|
-
**`npm run validate`** checks that:
|
|
342
|
-
- the file matches the schema, and ids are unique;
|
|
343
|
-
- every vector has a control and an `expected`, text rows included;
|
|
344
|
-
- each id is in its family;
|
|
345
|
-
- a `cdp` vector has a `cdp` or `page` step, and a `text` vector has neither;
|
|
346
|
-
- every CDP method on the guarded leg also runs on the control;
|
|
347
|
-
- a skip names one of the vector's targets;
|
|
348
|
-
- `observe` defines every expected name;
|
|
349
|
-
- in a vector with a refusable step, every observation, except an `as` of a step before the first refusable one, has `dependsOn`, and each name in it is a refusable step's refusal name;
|
|
350
|
-
- every `Target.setAutoAttach`, `children`, child-session, browser-session and browser-context step has the matching `requires`, and a vector's `requires` includes its steps' requirements; observation-only requirements can remain local;
|
|
351
|
-
- a client exit step is top-level and its vector requires `clientExit`, and only a vector with a client exit step scopes a check with `exit`;
|
|
352
|
-
- every `{name}` is in `placeholders`;
|
|
353
|
-
- no URL uses a port from 9222 to 9229.
|
|
354
|
-
|
|
355
|
-
## Reference runner
|
|
356
|
-
|
|
357
|
-
`guard-runner.mjs` is dependency-free browser ESM. `run(set, options)` returns
|
|
358
|
-
`{ results, summary(metadata) }`; the module also exports `summary`. Results have
|
|
359
|
-
`id`, `target`, `variant`, `control`, `guarded`, `live`, boolean observations and
|
|
360
|
-
errors. Statuses are `pass`, `fail`, `incomplete`, `skip` and `not-run`. Text vectors are reported
|
|
361
|
-
as `not-run`; they are not silently certified. A missing observation check is an
|
|
362
|
-
error before execution.
|
|
363
|
-
|
|
364
|
-
Supply a CDP client with `send(method, params, sessionId): Promise<object>`,
|
|
365
|
-
`waitEvent(predicate, milliseconds): Promise<event>` and an append-only `events`
|
|
366
|
-
array of `{ method, params, sessionId }`. Rejections should expose the protocol
|
|
367
|
-
error as `error.cdp`, or an error message. `client` carries the guarded leg;
|
|
368
|
-
`unguarded`, when supplied, carries unguarded controls and observation reads.
|
|
369
|
-
There is no fallback from observer reads to the guarded client. A bare client
|
|
370
|
-
means guarded transport only; rows requiring an observer report
|
|
371
|
-
`skip` with `not-supported:unguarded-observer`, counted in `summary().observerSkipped`. The run's `summary()` also reports
|
|
372
|
-
`skips`, the number of skipped rows per reason, and appends
|
|
373
|
-
`skips <n> (<reason>=<count>, ...)` to `line` whenever any row was skipped, so a
|
|
374
|
-
certification line never reads skipped rows as coverage.
|
|
375
|
-
`clientFactory({ variant, detachDelay, vector, target, slot, leg? })` is called per row
|
|
376
|
-
and may return either a client or `{ client, unguarded, close, transportVariant }`.
|
|
377
|
-
The factory can configure the guard's own transport delay from this metadata;
|
|
378
|
-
the runner never reaches into the guard. `transportVariant: true` acknowledges
|
|
379
|
-
that configuration, and every result records that boolean (false by default).
|
|
380
|
-
`slot` allows pooling browsers while keeping each row's clients independent.
|
|
381
|
-
A factory that rejects or throws makes only that row `incomplete` for control, guarded and live, with `clientFactory: <reason>` in its `incomplete`; the run goes on with the next row instead of rejecting. Such a row is labelled with the transport of the last client the factory made (`cdp` if none), and is passed to `log` once the run completes, so `summary()` never counts a placeholder transport. The optional `close` releases the row's clients; release pooled browsers after
|
|
382
|
-
`run` completes. Each parallel slot needs an independent browser for
|
|
383
|
-
browser-wide settings. Without a factory execution is serial. Cookie and storage
|
|
384
|
-
rows use fresh browser contexts when `capabilities.browserContexts` is true;
|
|
385
|
-
otherwise they run serially. `capabilities` is an object of booleans or an array
|
|
386
|
-
of supported names, and a capability that isn't declared is unsupported: a CDP
|
|
387
|
-
transport over a DevTools port declares `chromeUrlsAttach: true`, and an
|
|
388
|
-
extension on `chrome.debugger` leaves it out, so `C8.newtab` is skipped with
|
|
389
|
-
`not-supported:chromeUrlsAttach`. `capabilities.scope` selects owner-specific rows; pass the same name as `guard` to `summary()`.
|
|
390
|
-
A row with a client exit step needs `clientExit` declared, a `clientFactory` and an
|
|
391
|
-
unguarded observer that isn't the guarded client: without the capability or the factory
|
|
392
|
-
it is `skip` with `not-supported:clientExit`, and without such an observer, with
|
|
393
|
-
`not-supported:unguarded-observer`, since nothing could watch the page after the exit.
|
|
394
|
-
When the control leg exits, the runner calls `clientFactory` again with `leg: "guarded"`
|
|
395
|
-
for the guarded leg's clients and releases them with their `close` after the row. If
|
|
396
|
-
that call fails, or its observer is the client, the guarded leg is `incomplete`, with
|
|
397
|
-
`clientFactory: <reason>` or `client exit: <reason>` in `incomplete`, never a pass. A
|
|
398
|
-
leg whose client can't exit (it has no `close()`) fails with `client exit: <reason>`.
|
|
399
|
-
|
|
400
|
-
Map `ordinary`, `ordinary-same-site`, `seven`, `vault`, `vault-x`, `apex`, `own`
|
|
401
|
-
and `client` to origins. `chrome-newtab`, `chrome-new-tab-page`, `chrome-settings`, `chrome-passwords`, `chrome-search`, `network-error`, `chrome-extension-page` and `file-root` default to their `url`.
|
|
402
|
-
`{browserUi}` is the id of one of Chrome's own `browser_ui` targets, which the runner looks up through
|
|
403
|
-
its unguarded observer (or the client) before the row; a row that uses it fails when there is none.
|
|
404
|
-
A vector with `"defaultContext": true` runs in the browser's default context even with the
|
|
405
|
-
`browserContexts` capability: navigating a page in a created browser context to `chrome://settings`
|
|
406
|
-
closes Chrome's DevTools connection (Chrome for Testing 156), so `C8.navigate` stays in the default
|
|
407
|
-
context. Optionally map `plain`, `cross` and `log`; their defaults
|
|
408
|
-
are `ordinary`, `client` and `ordinary`. Origins must preserve the site
|
|
409
|
-
relationships of the data. Map an unavailable target to `null`, or leave it out; a
|
|
410
|
-
missing or `undefined` origin is treated exactly like `null`: every
|
|
411
|
-
row needing that origin is reported as `skip` for control, guarded and live, with
|
|
412
|
-
reason `not-applicable:no-<target>-origin`; no substitute origin is used and the
|
|
413
|
-
row never passes. A guard whose whole site is protected, such as the extension
|
|
414
|
-
where every `sliccy.ai` host is protected, passes `ordinary-same-site: null`.
|
|
415
|
-
This also applies to origins referenced in setup and triggers, not only the row
|
|
416
|
-
target. In observations and liveness checks, an `any` alternative that references
|
|
417
|
-
a `null` origin is dropped and the rest are evaluated; the row skips only when
|
|
418
|
-
every alternative of an `any`, or a required (`all`, `not` or plain) check, needs it. Tags and cookie names are unique per leg. `variant`
|
|
419
|
-
selects one variant or an array; omission selects all. `parallel` defaults to 1.
|
|
420
|
-
With the `browserContexts` capability each leg runs in its own browser context. A `Target.createBrowserContext` that fails with a timeout is retried once, and the row's `notes` records it; any other failure fails the row.
|
|
421
|
-
|
|
422
|
-
Every target created by the runner’s own unguarded observer uses `Target.createTarget` with `newWindow: true`, then verifies through `Target.getTargetInfo` that `openerId` is absent. A supplied opener or missing target evidence fails and cleans up that target (in the protected-error-page probe, the row is `incomplete` instead), rather than inheriting the active client tab (which could be seven). This includes ordinary control setup, storage canaries, protected-error history seeds, the frame-read page and the malformed-URL observer. The row notes record `newWindow=true, openerId absent`. Intentional guarded-client creation commands and popup/opener tests keep their original parameters.
|
|
423
|
-
|
|
424
|
-
`settleScale` (default 1) multiplies the bounds of waits for events the leg expects to happen: settles (beacon, served, title and load) on the control leg and `required` settles on either leg, `paused` on the control leg, page and child loads, the wait for a background step's `as` result (`<as> did not complete within <ms>ms`), the wait for the pre-action beacons, the C9 control's `Debugger.paused`, and the wait for an observation the leg expects to be `true`. Adopters on slow hosts can set 3 or 4. Every wait polls and returns as soon as its event arrives, so a larger bound only costs time when something is really missing, and each timeout error names the bound it used. An observation read from the fixture (`observer: "fixture"`, such as a beacon or a served request) that the leg expects to be `true` is polled for up to 5000 ms (scaled) instead of read once. One that the leg expects to be `false` is still read once.
|
|
425
|
-
|
|
426
|
-
`refusalScale` (default 1) multiplies the bounds of waits for what the guarded leg expects to be absent, the beacon, request, title or navigation that the guard should block: settles that are not `required`, and `paused`, on the guarded leg. Such a wait runs to its full bound whenever the guard works, so `settleScale` leaves it alone. The trade-off: a larger `refusalScale` gives more sensitivity to a leak that arrives late on a slow host, because the observations after the wait see it, at the cost of that much more time per guarded row. Adopters should keep `refusalScale` at 1 or higher, never below 1; the runner refuses a `refusalScale` below 1 with `refusalScale must be a number of at least 1`.
|
|
427
|
-
|
|
428
|
-
**Incomplete.** A probe that returns `incomplete` (including `errorPage`, `redirectSecret`, `frameRead` and `bfcacheRestore` steps) records the reason in the row's `incomplete` as `<leg> <probe>: <reason>`. Its unmeasured observations read `"incomplete"`, and so does guarded liveness when the probe never measured it. A leg whose measured observations all match is then `incomplete`, never `pass`, and `live` is `incomplete` when only its evidence is missing; a measured mismatch still makes the leg `fail`, and any row error still fails both legs. `summary()` counts `incomplete` per leg, returns the number of rows with any `incomplete` status as `incomplete`, appends `incomplete <n>` to `line`, and never passes such a run. A leg that throws keeps the errors it recorded before the throw.
|
|
429
|
-
|
|
430
|
-
`onStart(row)` receives each row's `id`, `target`, `variant`, `transport` and `via` as the row starts. `run()` returns a promise that also has `inFlight()`, and its result has `inFlight` too. `inFlight()` lists the rows that are running now, each as `{ id, target, variant, leg, phase, step, clients }`, where `step` is the index of the current step in its `phase` (`setup`, `trigger` or `child`), and `clients` lists each client's pending CDP calls as `{ method, session, ageMs }` (`client`, plus `unguarded` when it is a separate client). With `rowTimeoutMs`, a row still running after that many milliseconds fails on its own. Its `errors` hold `row timed out after <ms>ms: <the in-flight view>`, `inFlight` holds the view, and the clients that `clientFactory` made for it are closed. The run then goes on to the next row. It is off by default, and a client passed as `client` is never closed.
|
|
431
|
-
|
|
432
|
-
`log(row)` receives completed rows. Besides `control`, `guarded`, `live`, `why` and `errors`, a row may carry `notSupported` (unavailable observation fields), `ran` (steps that ran on one leg only) and `notes` (settles skipped because their registration was refused). Both are informational: they explain a row's verdict, and adopters (bios, slicc-cli, slicc-cdp) must not count them as failures, or as passes. The verdict is `control`, `guarded` and `live`. A variant supplies the wait before an
|
|
433
|
-
explicit `Target.detachFromTarget` step and for a `settle.variant` step.
|
|
434
|
-
|
|
435
|
-
Every runnable vector has a declarative `liveCheck`; every observation has a
|
|
436
|
-
declarative `check`. Each check declares its `observer`: `fixture` reads request
|
|
437
|
-
logs, `client` reads already-delivered transport data, and `unguarded` requires
|
|
438
|
-
an independent CDP observer. `all` needs the observers for every operand; `any`
|
|
439
|
-
can use an available fixture or client alternative without an unguarded observer.
|
|
440
|
-
Unavailable optional observer alternatives are false, never substituted with guarded reads.
|
|
441
|
-
Checks include: `beacon`, `served`, `header` with
|
|
442
|
-
`on`, `event` with `where`, `result` with `path` and `equals`, `cookie` with `from`,
|
|
443
|
-
`compare`, `refused`, `reached`, `attached`, `delivered`, `title`, `expression`,
|
|
444
|
-
`hops` (each exact nested redirect URL), `detached`, `loaded`, `pre`, `blocked`,
|
|
445
|
-
`history`, `frameBlocked`, `registration`, `state` and `filtered`. The diagnostic `executed` and `live`
|
|
446
|
-
checks are not valid liveness proofs. A `beacon`, `served` or `header` check with
|
|
447
|
-
`exit: "before"` reads only the requests logged before the leg's client exit, and with
|
|
448
|
-
`exit: "after"` only those logged after it; on a leg that never exited it is false.
|
|
449
|
-
Compose checks with `all`, `any` or `not`. Predicates support nested partial
|
|
450
|
-
objects, `$contains`, `$regex`, `$some`, nonempty `$every`, and `$encoded` with
|
|
451
|
-
`{ value, prefix }` for a base64-encoded value with an optional prefix. Reads of titles and
|
|
452
|
-
expressions use the unguarded observer. A `refused` check can declare `refusal.match` to match protocol errors. Transport timeouts, timeout sentinel values and evaluation exceptions fail result,
|
|
453
|
-
comparison and observer-read
|
|
454
|
-
observations; they are never comparable values. Registration refusals must match the
|
|
455
|
-
step's declared expression; dependent observations are omitted and reported by
|
|
456
|
-
the existing result evaluator.
|
|
457
|
-
|
|
458
|
-
`liveCheck` applies only to the guarded leg. Controls are judged by their positive
|
|
459
|
-
observations: a missing or failing effect fails the control. Guarded liveness
|
|
460
|
-
requires the exact target URL in the fixture log or a target-origin `/live`
|
|
461
|
-
heartbeat; alternatively a pre-action `/live?tag=...&step=pre&to=...` request and
|
|
462
|
-
an exact-URL block/error from the client or unguarded frame tree. An error for a
|
|
463
|
-
different URL is not evidence. Before judging, the runner waits, bounded at 5 s (scaled by `settleScale`), for the
|
|
464
|
-
pre-action beacon of every trigger step it ran, since a keepalive beacon has no ordering
|
|
465
|
-
guarantee. When one never arrives, an `eitherWhen` that needs it is `"incomplete"` and
|
|
466
|
-
excluded from that field's expectations, and the guarded leg is not live with
|
|
467
|
-
`not live: pre beacon not observed for <url>` in the row's `why`, never a false
|
|
468
|
-
mismatch. Once the beacon is in, a failing `eitherWhen` whose field mismatches, and a
|
|
469
|
-
failing `liveCheck`, are rechecked for up to 1.5 s (scaled) before they count. `blocked` accepts either `{ url }` for one exact URL
|
|
470
|
-
or `{ anyOf: [url, ...] }` for a nonempty list of exact URLs; each URL can be a
|
|
471
|
-
string or `{ hops, to, index? }`. Both forms allow `session` and `frame`. With `frame`,
|
|
472
|
-
the unguarded observer also reads the page's out-of-process iframe targets (matched by
|
|
473
|
-
their parent frame): a site-isolated iframe isn't in the parent's frame tree, so its
|
|
474
|
-
chrome-error main frame with the exact `unreachableUrl` is the block. `history` (unguarded,
|
|
475
|
-
same URL forms) is for guards that replace the error page, such as with `about:blank#blocked`: it
|
|
476
|
-
needs the pre-action beacon, an entry with the exact URL in the observer's
|
|
477
|
-
`Page.getNavigationHistory` of the page or child, and no fixture request for that URL.
|
|
478
|
-
`frameBlocked` (unguarded) is that out-of-process chrome-error iframe target, and the
|
|
479
|
-
client must be unable to attach to it; the cross-site `C4` rows accept it as `errorFrame`.
|
|
480
|
-
A step that the guard refuses in every context, such as `Network.clearBrowserCookies`
|
|
481
|
-
in `C6.clear-cookies`, is refusable, so refusing it on both legs passes and allowing it
|
|
482
|
-
is judged on its effect. A child step that fails after its child session detached is
|
|
483
|
-
recorded as detached, not as a refusal, and the runner awaits a child's started
|
|
484
|
-
`children` steps before any later `session: "child"` step and before the trigger. A page
|
|
485
|
-
navigation waits for the load of its own `loaderId` (its `Page.frameNavigated` and then a
|
|
486
|
-
load event), and a settle waits for a main-frame commit followed by its load, so a stale
|
|
487
|
-
load event never ends a wait. After a child's `children` steps that include `Page.enable`,
|
|
488
|
-
the runner waits, bounded at 5 s, for the child's first document to commit and load before
|
|
489
|
-
any later child step. `{ "settle": { "beacon", "required": true, "dependsOn"? } }` waits
|
|
490
|
-
for that beacon at the fixture and fails the row with `settle: no beacon from <origin>
|
|
491
|
-
within <ms>ms` when it never arrives, unless a refusal it `dependsOn` was recorded. It checks `dependsOn` again while it waits, since a child's `children` steps may still be running. When the registration it depends on was refused, the settle is skipped, the row's `notes` gets `<leg> settle:skipped-refused beacon <origin>`, and the row is judged on its other observations. The rows
|
|
492
|
-
whose `allowedBeacon` comes from an earlier document settle on it before their trigger.
|
|
493
|
-
Beacons use `navigator.sendBeacon`, with a `fetch(…, { keepalive: true })` fallback. An observation that reads a background step's `as` result
|
|
494
|
-
waits for that step, up to 10 s, and fails with `<as> did not complete within …` past it. Redirect
|
|
495
|
-
vectors list every nested hop URL and the final destination because Chromium
|
|
496
|
-
reports the URL intercepted by the guard. A matching block still requires the
|
|
497
|
-
pre-action beacon alongside it; an unrelated error cannot certify the chain.
|
|
498
|
-
A declared setup refusal can supply liveness only
|
|
499
|
-
when its `refusal.match` matched. Transport connectivity and command completion
|
|
500
|
-
alone never prove liveness. Cookie and storage rows seed a canary outside the
|
|
501
|
-
guard and read it back outside the guard; refused writes require unchanged
|
|
502
|
-
canary state and a refusal or filtered call response. Their control can clear or
|
|
503
|
-
modify state as required by its positive observations. `C5.auth` and
|
|
504
|
-
`C5.guessed-id.continue-with-auth` use fixture Authorization logs and refusal of
|
|
505
|
-
candidate auth ids; `C5.auth` also checks delivered auth events. Their liveness
|
|
506
|
-
requires an auth request at the fixture, not a completed fetch. Neither uses a
|
|
507
|
-
plain-tab baseline: headless Chromium can leave a credentials-including fetch
|
|
508
|
-
to a Basic 401 pending even without an attached client.
|
|
509
|
-
|
|
510
|
-
Beacon settlement uses only requests recorded after the page action starts. For
|
|
511
|
-
redirect chains it also requires the exact final destination request in the
|
|
512
|
-
fixture log. A setup beacon on the same origin and tag cannot settle an unfinished
|
|
513
|
-
redirect chain. This does not change the observations used to judge either leg.
|
|
514
|
-
|
|
515
|
-
The fixture log is shared by every row in a run, and `C8.unparseable` leaves entries such
|
|
516
|
-
as `http://exa%20mple.sliccy.ai:<port>/doc?n=<tag>-create-0` that `new URL` rejects. A
|
|
517
|
-
row reads only the entries that carry its own tag; a tagged `liveCheck`, `served`,
|
|
518
|
-
`on` or `beacon` check never sees another row's entries, and an entry that does not
|
|
519
|
-
parse is matched only by its raw or canonical string, never parsed into a throw.
|
|
520
|
-
`hops` checks compare exact URLs. `"tagged": false` reads every entry and skips the ones
|
|
521
|
-
that don't parse.
|
|
522
|
-
|
|
523
|
-
The separately exported Node module `@ai-ecoverse/slicc-browsers/fixture` exposes
|
|
524
|
-
`fixtureServer({ host, port, tls, latency })`, defaulting to a random loopback port. `latency(url)` returns milliseconds to hold that response, for timing tests.
|
|
525
|
-
Pass `tls: { key, cert }` to serve HTTPS using Node TLS key and certificate
|
|
526
|
-
values. The returned origin, request logs, redirect URLs and document heartbeats
|
|
527
|
-
use `https://<Host>` with TLS, so `served`, `hops` and `liveCheck` compare against
|
|
528
|
-
the mapped HTTPS origins, including an extension client at `https://*.sliccy.ai`.
|
|
529
|
-
Without `tls`, the server and logged URLs remain HTTP. It returns
|
|
530
|
-
`origin`, `port`, `log()`, `reset()` and `close()`. Log entries contain `url`,
|
|
531
|
-
`method`, `headers` and `time`. Mirror that API when using another fixture.
|
|
532
|
-
The server serves document, frame, redirect, CSP, authentication, data, image,
|
|
533
|
-
canary and beacon paths. Configure host resolution in your test browser, not in
|
|
534
|
-
the runner. `npm run test:guard` exercises Chromium twice with no guard, one target per
|
|
535
|
-
invocation: controls must pass and guarded protection expectations must fail; the `C8.newtab` reachability control and `X1.after-exit`, whose plain client leaves nothing behind when it exits, must instead pass both legs. Rows that require `siteIsolationOff` run in one more Chromium launched without site isolation. For a single
|
|
536
|
-
target use `GUARD_TARGET=seven npm run test:guard-target`. The fixture also serves
|
|
537
|
-
`/live` image requests from stand-in documents, independently of injected code.
|
|
538
|
-
|
|
539
|
-
The integration launcher owns each Chrome process group and temporary profile. `close()` terminates the group, escalates to SIGKILL if necessary and removes the profile; node:test workers also register an after hook for every launch. Process exit, uncaught errors, SIGINT, SIGTERM and SIGHUP reap tracked groups synchronously. An independent POSIX watchdog watches the runner’s pipe and reaps Chrome and its profile even if the runner receives SIGKILL. `launch(fixture, { signal })` also accepts a test’s AbortSignal for timeout cleanup. `guard-lifecycle.test.mjs` interrupts real runner subprocesses and checks every Chrome PID returned by CDP’s `SystemInfo.getProcessInfo` plus profile removal, without consulting a host-wide process list.
|
|
187
|
+
### Running them
|
|
540
188
|
|
|
189
|
+
The reference runner works with any guard that speaks CDP:
|
|
541
190
|
|
|
542
191
|
```js
|
|
543
192
|
import set from '@ai-ecoverse/slicc-browsers/guard-vectors.json' with { type: 'json' };
|
|
544
193
|
import { run } from '@ai-ecoverse/slicc-browsers/guard-runner';
|
|
545
194
|
import { fixtureServer } from '@ai-ecoverse/slicc-browsers/fixture';
|
|
195
|
+
|
|
546
196
|
const fixture = await fixtureServer();
|
|
547
|
-
const targets = await configureTestOrigins(fixture.port);
|
|
548
|
-
const capabilities = {
|
|
549
|
-
autoAttach: true,
|
|
550
|
-
browserSession: true,
|
|
551
|
-
browserContexts: true,
|
|
552
|
-
chromeUrlsAttach: true,
|
|
553
|
-
discovery: true,
|
|
554
|
-
};
|
|
555
197
|
const execution = await run(set, {
|
|
556
|
-
clientFactory: (
|
|
557
|
-
targets,
|
|
558
|
-
capabilities,
|
|
559
|
-
|
|
560
|
-
parallel: 6,
|
|
198
|
+
clientFactory: (row) => connectGuardAndObserver(row),
|
|
199
|
+
targets: originsFor(fixture),
|
|
200
|
+
capabilities: { autoAttach: true, browserSession: true, browserContexts: true, discovery: true },
|
|
201
|
+
parallel: 4,
|
|
561
202
|
fixture,
|
|
562
203
|
});
|
|
563
|
-
console.log(execution.summary({ guard: 'my-guard', sha:
|
|
204
|
+
console.log(execution.summary({ guard: 'my-guard', sha: process.env.GITHUB_SHA }).line);
|
|
564
205
|
await fixture.close();
|
|
565
206
|
```
|
|
566
207
|
|
|
567
|
-
|
|
208
|
+
You supply:
|
|
209
|
+
- **a guarded client** per row: your guard's CDP connection, with `send(method, params, sessionId)`, `waitEvent(predicate, ms)` and an `events` array;
|
|
210
|
+
- **an unguarded observer**: a direct CDP connection to the same browser, which runs the controls and checks liveness;
|
|
211
|
+
- **origins**: the fixture server reachable as seven, the vault and an ordinary site. Point the protected hostnames at it with Chrome's `--host-resolver-rules`, so no test ever touches the real seven;
|
|
212
|
+
- **capabilities**: what your transport can do. A row that needs something you don't declare is skipped as `not-supported:<capability>`.
|
|
568
213
|
|
|
569
|
-
|
|
570
|
-
- **Process list.** On Unix it's `discovery.process.unix` (`ps -ax -o pid= -o command=`): one process a line, the pid, whitespace, and the command line. On Windows it's `discovery.process.windows`, which prints a JSON array (or one object) of `ProcessId` and `CommandLine`; entries with no command line are skipped.
|
|
571
|
-
- **What's skipped:** pid 0, the scanning process itself, and commands whose first token is a wrapper. The first token is the quoted string when the command starts with a quote. A wrapper is a token whose base name, without `.exe`, `.cmd` or `.bat`, is one of `wrappers`, case-insensitively. A command with no executable before its first `-` token is skipped too.
|
|
572
|
-
- **The port** comes from the first `--remote-debugging-port` followed by `=` or whitespace and then digits. A port above 65535 drops the process.
|
|
573
|
-
- **The dir** is the value of `--user-data-dir` at the start of a token, followed by `=` or by whitespace (which is skipped). A value that starts with one of `valueQuotes` (`"` or `'`) runs to the matching quote. Any other value runs up to the first whitespace that is followed by `valueStop` (`--`), or to the end of the command, without trailing whitespace. So `--user-data-dir=/Users/me/Library/Application Support/x --no-first-run` gives `/Users/me/Library/Application Support/x`. A value that would start with `--` is empty.
|
|
574
|
-
- **Port 0** means the port is in the dir's `DevToolsActivePort`, checked like any other. With no dir, or a dir under the legacy profile root (a `slicc` segment followed by `profiles`, case-insensitively) that isn't one of [six's profile dirs](#slicc-sixs-profiles), the process is skipped.
|
|
575
|
-
- **Any other port** is probed on `127.0.0.1`. An endpoint from the process scan has `source: "process"` and `userDataDir` set to the dir when there is one.
|
|
576
|
-
- **Tests** must fake the process list: never scan the real one in a test.
|
|
577
|
-
|
|
578
|
-
## SLICC six's profiles
|
|
579
|
-
|
|
580
|
-
SLICC six launches Chrome in a profile of its own, and slicc-cli can use that profile in place (`slicc --profile six`). Discovery never chooses it: its endpoints are listed, marked and ranked last.
|
|
581
|
-
|
|
582
|
-
| | macOS | Linux | Windows |
|
|
583
|
-
| --- | --- | --- | --- |
|
|
584
|
-
| six's root | `~/Library/Application Support/Slicc/profiles` | `$XDG_STATE_HOME/slicc/profiles`, else `~/.local/state/slicc/profiles` | `%LOCALAPPDATA%\Slicc\profiles`, else `~\AppData\Local\Slicc\profiles` |
|
|
585
|
-
|
|
586
|
-
Taken from [ai-ecoverse/slicc](https://github.com/ai-ecoverse/slicc) at `65c5a79b4` (v6.247.17-8):
|
|
587
|
-
|
|
588
|
-
| what | where |
|
|
214
|
+
| capability | means the transport can |
|
|
589
215
|
| --- | --- |
|
|
590
|
-
|
|
|
591
|
-
| `
|
|
592
|
-
|
|
|
593
|
-
| `
|
|
594
|
-
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
**Discovery** marks an endpoint `six: true` when one of its sources found it in six's profile:
|
|
599
|
-
- `DevToolsActivePort` in each of six's profile dirs, checked like any other. No other dir in six's root is read.
|
|
600
|
-
- The process list: a process whose `--user-data-dir` is six's. With port 0 its `DevToolsActivePort` is read, which the legacy-root rule would otherwise skip.
|
|
601
|
-
- Merging: an endpoint is six's when any source that found it, and whose checks passed, is six's. So a port probe hit or a `--cdp` URL that the process list maps to six's profile is six's, while a stale `DevToolsActivePort` in six's profile marks nothing. Its `userDataDir` is six's dir when the source that wins has none.
|
|
602
|
-
|
|
603
|
-
A six endpoint has tier `six`, whatever its seven tabs. **A program never chooses it by itself**: a default choice, an "attach to the best" rule, or a non-interactive mode skips it. Only an explicit request for six's profile unlocks it, such as `slicc --profile six` or picking it in a picker.
|
|
604
|
-
|
|
605
|
-
## Output schema 1
|
|
606
|
-
|
|
607
|
-
`schema/browsers.schema.json` describes `slicc browsers --json`, which `playwright-cli list --json` also prints. [slicc-cli's README](https://github.com/ai-ecoverse/slicc-cli#slicc-browsers---json---cdp-url---no-bundle-scan) is the authoritative spec for it; this schema is a copy and follows it. Fields can be added; `source` is one of `devtools-active-port`, `flag`, `probe` and `process`. `tier` is 1 to 6, and `six: true` marks an endpoint in SLICC six's profile (tier 6). An endpoint may carry `tabs`: every page target except those on protected origins. Seven's own tabs are protected, so they are in `sevenTabs` only, never in `tabs`. In `sevenTabs`, `url` and `title` are the origin only, never a path, query, fragment or document title, including configured extra seven origins and default-port normalization. `playwright-cli list --json` prints it, and a corpus case that checks it needs `tabs`.
|
|
608
|
-
|
|
609
|
-
## The corpus
|
|
610
|
-
|
|
611
|
-
Each case is a fake machine: a home tree, `DevToolsActivePort` files, fake `/json/version` and `/json/list` answers, a fake process list, `--cdp` URLs, and installed browsers, with the ranked list it must produce in `expected.json`. `corpus.json` holds every case as `{ "name", "case", "expected" }`.
|
|
612
|
-
|
|
613
|
-
**`case.json`:**
|
|
614
|
-
- `description`, and `os?`: the OSes the case runs on (`darwin`, `linux`, `windows`; default all).
|
|
615
|
-
- `needs`: what a program must support to run the case: `user-data-dirs` (`DevToolsActivePort` in browsers' default user-data dirs), `seven-profiles` (in the profile roots), `flag` (`--cdp` URLs), `process`, `probe` (port probes), `origins` (extra seven origins), `installed` (ranking installed browsers), `six` (finding and marking SLICC six's profiles), `tabs` (each endpoint's `tabs` list). A program skips a case that needs something it doesn't do.
|
|
616
|
-
- `env?`: environment variables for discovery, which sees nothing else from the real environment.
|
|
617
|
-
- `files?`: file contents by path. Every path starts with a token.
|
|
618
|
-
- `cdp?`, `origins?`, `probePorts?`: `--cdp` URLs, extra seven origins, and the ports to probe.
|
|
619
|
-
- `processes?`: `{ "pid", "command" }`, or `{ "self": true, "command" }` for the scanning process, whose pid the harness fills in.
|
|
620
|
-
- `http?`: fake servers by `host:port`. `localhost` and `[::1]` are `127.0.0.1`. A listed port accepts TCP connections. `/json/version` answers 200 with `version`, `/json/list` (and `/json`) with `list`. A missing body, or any other path, answers 404. An unlisted port refuses connections.
|
|
621
|
-
- `installed?`: installed browsers in the shape of output schema 1's `installed`, ranked along with the endpoints.
|
|
622
|
-
- `auto`: exactly in the cases that need `six`, the id of the first entry in `expected.json` that isn't six's, or `null` when every entry is. A program that chooses without being asked must choose no entry before it, and never a six entry.
|
|
216
|
+
| `autoAttach` | auto-attach to child targets such as popups and iframes |
|
|
217
|
+
| `browserSession` | send commands to the browser session, not only to a page |
|
|
218
|
+
| `browserContexts` | create isolated browser contexts |
|
|
219
|
+
| `discovery` | report target creation and changes without a session |
|
|
220
|
+
| `chromeUrlsAttach` | attach to `chrome://` pages (CDP over a DevTools port can; `chrome.debugger` can't) |
|
|
221
|
+
| `extensionWorld` | host a foreign extension's content script on the test page |
|
|
222
|
+
| `clientExit` | close the guarded client in the middle of a row |
|
|
223
|
+
| `siteIsolationOff`, `inboundDelay`, `forwardAudit` | run without site isolation, hold the browser's messages to the guard, and audit what the guard forwarded |
|
|
623
224
|
|
|
624
|
-
|
|
225
|
+
`forwardAudit` permits observer restore after a client history timeout only with terminal `historyTrigger: { id: "history-trigger", complete: true, forwarded: false }` proof from `audit({ historyTrigger: command, probes: [], final: false })`, keeping the probe ledger open; missing proof, forwarding or a late successful reply never qualifies as refusal.
|
|
625
226
|
|
|
626
|
-
|
|
227
|
+
### Reading the result
|
|
627
228
|
|
|
628
|
-
|
|
229
|
+
Each row reports `control`, `guarded` and `live` as `pass`, `fail`, `skip`, `incomplete` (the test couldn't gather its own evidence) or `not-run` (vectors described only in words). `summary()` turns the rows into one line:
|
|
629
230
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
2. points discovery at that home and the case's `env` only, so no real user-data dir, profile or process is ever read;
|
|
633
|
-
3. feeds the fake process list (Unix: `PSOutput`, Windows: `CIMOutput`) and routes every HTTP request and TCP dial to the fakes, so discovery never touches the network;
|
|
634
|
-
4. discovers endpoints, ranks them with `installed`, and compares the JSON with `expected.json` after token expansion, as parsed JSON.
|
|
635
|
-
|
|
636
|
-
Go (`slicc-cli` does this in `test/integration/corpus_test.go`):
|
|
637
|
-
|
|
638
|
-
```go
|
|
639
|
-
cases, _ := sliccbrowsers.Corpus()
|
|
640
|
-
for _, c := range cases {
|
|
641
|
-
if !c.Runs(runtime.GOOS, sliccbrowsers.NeedFlag, sliccbrowsers.NeedProcess /* … */) {
|
|
642
|
-
continue
|
|
643
|
-
}
|
|
644
|
-
l := sliccbrowsers.Layout{OS: runtime.GOOS, Home: t.TempDir(), Getenv: func(k string) string { return c.Input.Env[k] }}
|
|
645
|
-
// write c.Files(l); use c.CDP(l), c.PSOutput(l, os.Getpid()) or c.CIMOutput(l, os.Getpid()),
|
|
646
|
-
// c.Transport() as the http.RoundTripper and c.DialContext for TCP; compare with c.ExpectedJSON(l)
|
|
647
|
-
}
|
|
231
|
+
```text
|
|
232
|
+
GUARD-PASS my-guard@1a2b3c4 (vectors @ai-ecoverse/slicc-browsers@1.18.5)
|
|
648
233
|
```
|
|
649
234
|
|
|
650
|
-
|
|
235
|
+
The run passes only when every row passes and none is missing. A skip with a reason the runner would never give, such as a made-up capability, is reported as forged, and skipped and incomplete rows are counted on the line, so the line can't overstate what was tested.
|
|
651
236
|
|
|
652
|
-
|
|
653
|
-
use slicc_browsers::corpus::{corpus, Layout, NEED_PROCESS, NEED_PROFILES};
|
|
654
|
-
|
|
655
|
-
let cat = slicc_browsers::catalog();
|
|
656
|
-
let os = match std::env::consts::OS { "macos" => "darwin", other => other };
|
|
657
|
-
for case in corpus().cases {
|
|
658
|
-
if !case.runs(os, &[NEED_PROFILES, NEED_PROCESS]) {
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
let home = tempdir.path().to_str().unwrap();
|
|
662
|
-
let env = |k: &str| case.input.env.get(k).cloned();
|
|
663
|
-
let layout = Layout { os, home, env: &env, catalog: &cat };
|
|
664
|
-
// write case.files(&layout); case.ps_output(&layout, std::process::id()) or case.cim_output(…);
|
|
665
|
-
// answer HTTP with case.http_get(host_port, path) and TCP with case.server(host_port).is_some();
|
|
666
|
-
// compare with case.expected(&layout)
|
|
667
|
-
}
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
`Layout` also gives `Config`, `SevenProfiles`, `SixProfiles`, `UserDataDirs(id)`, `Token(name)` and `Expand(text)` (Rust: `config`, `seven_profiles`, `six_profiles`, `user_data_dirs`, `token`, `expand`). Their results for a few homes are pinned in `test/integration/layout.json`, which the Go and Rust tests both check.
|
|
237
|
+
To find out what a vector checks, read it in `guard-vectors.json`: each one lists its steps, each observation's `means`, its `live` condition and its expected outcome. [`schema/guard-vectors.schema.json`](schema/guard-vectors.schema.json) defines the format.
|
|
671
238
|
|
|
672
239
|
## Development
|
|
673
240
|
|
|
674
|
-
Go 1.26, Rust stable with `wasm32-wasip1
|
|
675
|
-
- `npm run corpus` regenerates `corpus.json` from `corpus/`; the pre-commit hook and CI fail when it's stale.
|
|
676
|
-
- `npm run validate` checks `catalog.json`, `corpus.json`, `protected.json` and `guard-vectors.json` against the schemas, runs the protected-origin vectors through `protected.mjs`, loads `protected.mjs` and `seven.mjs` with only `URL`, and checks what a schema can't say: unique ids, bundle ids and names, known Windows roots, rising tiers, compiling patterns, known tokens, tier order, `sevenOpen`, `tabs` (which holds no entry of `sevenTabs`), `six` and `auto` in each `expected.json`, and no port from 9222 to 9229.
|
|
677
|
-
- Every gate prints one verdict line when it passes: `validate: PASS …`, `corpus: PASS <n> cases` (`node scripts/corpus.mjs --check`), `major-guard: PASS v<version> fits <module>`, and `guard-sweep: PASS 14 targets` (`npm run test:guard-sweep`). CI runs each through `scripts/gate.mjs --expect <regex> -- <command>`, which fails when the command exits non-zero or prints no matching line, so a silent or early exit never reads as a pass. `gate.mjs --expect <regex> --in <file>` checks a log instead; the release job uses it on semantic-release's log for the major-guard line. `npm run test:gates` tests the gates, including runs through a symlinked checkout and a symlinked script.
|
|
678
|
-
- `npm run lint`: the slicc lint tools (Biome, no comments, no unit tests in git, `AGENTS.md`), `gofmt`, `go mod tidy -diff`, `go vet`, golangci-lint, `cargo fmt --check` and clippy.
|
|
679
|
-
- `npm test`: `validate`, `test/integration/self-contained.test.mjs`, `test/integration/git-retry.test.mjs`, the Go tests in `test/integration/`, and `cargo test --all-features`.
|
|
680
|
-
- `npm run build`: the Go module, the crate (also for `wasm32-wasip1`), and `npm pack --dry-run`.
|
|
681
|
-
|
|
682
|
-
Changing a table in `slicc-cli/internal/browser` means changing `catalog.json` here, releasing, and bumping the dependency; slicc-cli has no tables of its own. A new behaviour gets a corpus case.
|
|
241
|
+
You need Go 1.26, Rust stable with the `wasm32-wasip1` target, and Node 24 or later.
|
|
683
242
|
|
|
684
|
-
|
|
243
|
+
| command | what it does |
|
|
244
|
+
| --- | --- |
|
|
245
|
+
| `npm run corpus` | rebuilds `corpus.json` from `corpus/` |
|
|
246
|
+
| `npm run validate` | checks every file against its schema and the rules a schema can't express, and runs `protected.json` through `protected.mjs` |
|
|
247
|
+
| `npm run lint` | Biome, the slicc lint rules, `gofmt`, `go vet`, golangci-lint, `cargo fmt` and clippy |
|
|
248
|
+
| `npm test` | validation, the Node, Go and Rust tests |
|
|
249
|
+
| `npm run test:guard` | the reference runner against real Chromium with no guard, where every control must pass and every protection must fail |
|
|
250
|
+
| `npm run build` | the Go module, the crate (also for `wasm32-wasip1`), and the npm package |
|
|
685
251
|
|
|
686
|
-
|
|
687
|
-
1. The tag `vX.Y.Z` and a GitHub release are the Go module's release. `scripts/major-guard.mjs` fails the release when the major doesn't fit the module path in `go.mod` (no suffix allows v0 and v1, `/vN` only vN). The workflow then asks `proxy.golang.org` for the new version and `pkg.go.dev` to fetch it; both only warn.
|
|
688
|
-
2. **npm:** with the repository variable `SLICC_BROWSERS_NPM_PUBLISH` set to `true`, the package is stamped with the version and published through npm trusted publishing (OIDC, with provenance). A version that is already on npm is skipped. It goes out on the `latest` dist-tag without `--tag`, so npm refuses a version below the current `latest`.
|
|
689
|
-
3. **crates.io:** with the repository variable `SLICC_BROWSERS_CRATES_PUBLISH` set to `true`, `Cargo.toml` is stamped with the version and `cargo publish` runs with a short-lived token from crates.io trusted publishing (`rust-lang/crates-io-auth-action`). A version that is already on crates.io is skipped.
|
|
690
|
-
4. **Cert line:** after the npm publish, the workflow runs `npm pack @ai-ecoverse/slicc-browsers@X.Y.Z` against the registry and appends `cert: @ai-ecoverse/slicc-browsers@X.Y.Z sha256=<hex> gitHead=<sha>` to the `vX.Y.Z` GitHub release and the job summary, once; the cert lane copies it from there.
|
|
252
|
+
A new discovery behaviour gets a corpus case, and a new guard behaviour gets a guard vector.
|
|
691
253
|
|
|
692
|
-
|
|
254
|
+
## Releases
|
|
693
255
|
|
|
694
|
-
|
|
256
|
+
Every merge to `main` that warrants it is released by semantic-release, as one version for all three packages. The `vX.Y.Z` tag is the Go release; npm and crates.io are published with trusted publishing, and the GitHub release records the npm tarball's hash for certification. Pushed tags are never moved or deleted: a bad release is fixed by the next one.
|
|
695
257
|
|
|
696
|
-
|
|
258
|
+
## License
|
|
697
259
|
|
|
698
|
-
|
|
699
|
-
1. npm: `npx fledgling add --new @ai-ecoverse/slicc-browsers` claims the name with a `0.0.0` placeholder and trusts `ai-ecoverse/slicc-browsers`' `release.yml`. Then set `SLICC_BROWSERS_NPM_PUBLISH` to `true`.
|
|
700
|
-
2. crates.io: trusted publishing needs the crate to exist. Publish it once by hand from the latest tag: `git checkout vX.Y.Z && node scripts/set-crate-version.mjs X.Y.Z && cargo publish --allow-dirty`, logged in with `cargo login`. Then, on crates.io, add a trusted publisher to `slicc-browsers`: GitHub, owner `ai-ecoverse`, repository `slicc-browsers`, workflow `release.yml`, no environment. Then set `SLICC_BROWSERS_CRATES_PUBLISH` to `true`.
|
|
260
|
+
[Apache-2.0](LICENSE)
|