@ai-ecoverse/slicc-browsers 1.8.2 → 1.8.4

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 CHANGED
@@ -145,14 +145,16 @@ Every CDP guard runs the vectors in `guard-vectors.json` (schema `schema/guard-v
145
145
  - `C5.continue-request` and `C5.continue-response`, added next to `C5.document`;
146
146
  - 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 four 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.
147
147
 
148
- The `C8.newtab` reachability control attaches through the guard to a settled ordinary new tab, including its internal frames, then navigates that same tab to an ordinary document. Both legs must succeed, including without a guard. It uses the default browser context, since an incognito new tab omits the ordinary new tab's internal frames. It covers `chrome://newtab/` and directly opened `chrome://new-tab-page/`.
148
+ The `C8.newtab` reachability control attaches through the guard to a settled ordinary new tab, including its internal frames, then navigates that same tab to an ordinary document. Both legs must succeed, including without a guard. It requires `chromeUrlsAttach`, so an extension transport reports it as `skip` with `not-supported:chromeUrlsAttach`. The other C8 rows expect a refusal on the guarded leg and run their controls unguarded, so they don't require it and still run on extension transports. It uses the default browser context, since an incognito new tab omits the ordinary new tab's internal frames. It covers `chrome://newtab/` and directly opened `chrome://new-tab-page/`.
149
+
150
+ A client may register `Fetch.enable` patterns naming a protected origin in either of two conformant ways: the guard allows registration but never pauses or forwards protected requests to the client and refuses continue, fulfil or fail calls for request ids it did not hand out; or the guard refuses registration with its declared refusal text. Setup calls explicitly naming a protected placeholder are refusable with `refusal.onlyProtected: true`, and interception-dependent observations depend on that refusal. Wildcard and ordinary-only patterns do not receive this exemption. The ordinary-origin control still runs through the guard and must succeed.
149
151
 
150
152
  **Top level.**
151
153
  - `schemaVersion` (1): guards pin it.
152
154
  - `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.
153
155
  - `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-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-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.
154
156
  - `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.
155
- - `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`.
157
+ - `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`; `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`.
156
158
  - `placeholders`: every `{name}` used in the vectors, with what it stands for. `{client}` is the client page's origin, which the runner maps.
157
159
 
158
160
  **A vector** has:
@@ -168,8 +170,8 @@ The `C8.newtab` reachability control attaches through the guard to a settled ord
168
170
  - `skip?` (`target`, `reason`): `reason` starts with a code, as in `not-applicable: …`.
169
171
 
170
172
  **Steps** are declarative and transport-neutral. Any step can take `on` (`control` or `guarded`), which runs it on that leg only.
171
- 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"`, and `browserContexts` for `Target.createBrowserContext` and `Target.disposeBrowserContext`.
172
- - `{ "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" }` (a regular expression for the error message or code) marks a call that a guard may refuse at registration.
173
+ 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.
174
+ - `{ "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.
173
175
  - `{ "page": action, … }`:
174
176
  - `tab`: a new tab at `url`, in `context`, which becomes the page. `background` defaults to `true`;
175
177
  - `navigate`: go to `url` and wait for the load;
@@ -202,7 +204,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
202
204
 
203
205
  **The control rule.** Every vector has a control on an ordinary origin: the same setup and trigger against an unprotected origin must succeed. A missing or failing control fails the id. With `via: "guard"`, the control runs through the guard. With `via: "unguarded"`, the runner runs setup and trigger through its own CDP against the ordinary origin and against the protected stand-in, and both must show the effect. The runner reports the control's transport as `via`.
204
206
 
205
- **Refusals.** Each refusable step has a refusal name: its `as`, or `refused.<i>`, where `i` is its index in the vector's steps, setup then trigger, with each `children` step's steps right after it, counted from 0 before `on` is applied. When a guard refuses the step and the error matches `refusal.match`, the runner sets that name to `true` in the leg's observations. On that leg, an observation whose `dependsOn` lists the name is skipped; every other observation is checked as usual. A guard that refuses the step on every origin passes in one of two ways: through the guard, with the step refused on both legs, or with an unguarded control and the guard's matched refusal on the guarded leg. With `via: "guard"`, a refusal on one leg only fails that leg. The comparison only counts steps that ran on both legs. A step that ran on one leg only (for example a `children` step on the control's popup, when the guarded leg never gets that popup attached) doesn't fail the row by itself, and the row reports it in `ran`, as `{ "refused.5": "control-only" }` or `"guarded-only"`. Observations that depend on such a step follow their `dependsOn` on each leg. A refusable step that fails without a matching refusal, for example with an internal error or a timeout, is not a refusal: it fails the row with `<name> failed without a refusal: <message>`. A step skipped because an earlier step failed therefore never passes through `ran`, and only a matched refusal skips the observations that depend on it. A guard that allows the step must pass its control through the guard.
207
+ **Refusals.** Each refusable step has a refusal name: its `as`, or `refused.<i>`, where `i` is its index in the vector's steps, setup then trigger, with each `children` step's steps right after it, counted from 0 before `on` is applied. When a guard refuses the step and the error matches `refusal.match`, the runner sets that name to `true` in the leg's observations. On that leg, an observation whose `dependsOn` lists the name is skipped; every other observation is checked as usual. A guard that refuses the step on every origin passes in one of two ways: through the guard, with the step refused on both legs, or with an unguarded control and the guard's matched refusal on the guarded leg. With `via: "guard"`, a refusal on one leg only fails that leg, unless the step declares `refusal.onlyProtected: true`: its matched refusal on the guarded leg alone is conformant, and its dependent observations are skipped there. Refusing such a step on the control always fails the control, even if both legs are refused. Without that flag, refusal symmetry is unchanged. The comparison only counts steps that ran on both legs. A step that ran on one leg only (for example a `children` step on the control's popup, when the guarded leg never gets that popup attached) doesn't fail the row by itself, and the row reports it in `ran`, as `{ "refused.5": "control-only" }` or `"guarded-only"`. Observations that depend on such a step follow their `dependsOn` on each leg. A refusable step that fails without a matching refusal, for example with an internal error or a timeout, is not a refusal: it fails the row with `<name> failed without a refusal: <message>`. A step skipped because an earlier step failed therefore never passes through `ran`, and only a matched refusal skips the observations that depend on it. A guard that allows the step must pass its control through the guard.
206
208
 
207
209
  **Liveness.** A runner checks `live` through its own CDP, never through the guard path: either the protected document loaded (title, marker or URL), or the guard returned its own refusal for a refusable step. A failing `live` fails the id.
208
210
 
@@ -280,7 +282,10 @@ The optional `close` releases the row's clients; release pooled browsers after
280
282
  browser-wide settings. Without a factory execution is serial. Cookie and storage
281
283
  rows use fresh browser contexts when `capabilities.browserContexts` is true;
282
284
  otherwise they run serially. `capabilities` is an object of booleans or an array
283
- of supported names. `capabilities.scope` selects owner-specific rows.
285
+ of supported names, and a capability that isn't declared is unsupported: a CDP
286
+ transport over a DevTools port declares `chromeUrlsAttach: true`, and an
287
+ extension on `chrome.debugger` leaves it out, so `C8.newtab` is skipped with
288
+ `not-supported:chromeUrlsAttach`. `capabilities.scope` selects owner-specific rows.
284
289
 
285
290
  Map `ordinary`, `ordinary-same-site`, `seven`, `vault`, `vault-x`, `apex`, `own`
286
291
  and `client` to origins. `chrome-newtab`, `chrome-new-tab-page`, `chrome-settings`, `chrome-passwords`, `chrome-extension-page` and `file-root` default to their `url`.
@@ -411,6 +416,7 @@ const capabilities = {
411
416
  autoAttach: true,
412
417
  browserSession: true,
413
418
  browserContexts: true,
419
+ chromeUrlsAttach: true,
414
420
  };
415
421
  const execution = await run(set, {
416
422
  clientFactory: (metadata) => connectGuardAndObserver(metadata),
package/guard-driver.mjs CHANGED
@@ -152,6 +152,32 @@ export function judge(vector, side, observed) {
152
152
  return { pass: mismatches.length === 0, refused, skipped, mismatches };
153
153
  }
154
154
 
155
+ const ranOn = (observed, name) => typeof observed?.[name] === 'boolean';
156
+
157
+ const refusalRuns = (vector, control, guarded) =>
158
+ Object.fromEntries(
159
+ refusals(vector)
160
+ .filter((name) => control && guarded && ranOn(control, name) !== ranOn(guarded, name))
161
+ .map((name) => [name, ranOn(control, name) ? 'control-only' : 'guarded-only'])
162
+ );
163
+
164
+ const protectedRefusals = (vector) =>
165
+ new Set(
166
+ flatten(named([...(vector.setup ?? []), ...(vector.trigger ?? [])]))
167
+ .filter((step) => step.refusable && step.refusal.onlyProtected)
168
+ .map((step) => step.refusal.name)
169
+ );
170
+
171
+ function controlRefusal(c, g, via, only, onlyProtected) {
172
+ if (!c) return 'no control';
173
+ if (c.refused.length > 0 && via !== 'guard') return 'an unguarded control was refused';
174
+ if (c.refused.some((name) => onlyProtected.has(name)))
175
+ return 'a protected-only step was refused on the control';
176
+ if (via === 'guard' && g && only(c, g).length > 0)
177
+ return `refused only on the control: ${only(c, g).join(', ')}`;
178
+ return null;
179
+ }
180
+
155
181
  export function result(vector, run) {
156
182
  const row = {
157
183
  id: vector.id,
@@ -173,33 +199,22 @@ export function result(vector, run) {
173
199
  }
174
200
  const c = run.control ? judge(vector, 'control', run.control) : null;
175
201
  const g = run.guarded ? judge(vector, 'guarded', run.guarded) : null;
176
- const ranOn = (observed, name) => typeof observed?.[name] === 'boolean';
177
202
  const both = (name) => ranOn(run.control, name) && ranOn(run.guarded, name);
178
203
  const only = (a, b) =>
179
204
  (a?.refused ?? []).filter((name) => both(name) && !(b?.refused ?? []).includes(name));
180
- const ran = Object.fromEntries(
181
- refusals(vector)
182
- .filter(
183
- (name) =>
184
- run.control && run.guarded && ranOn(run.control, name) !== ranOn(run.guarded, name)
185
- )
186
- .map((name) => [name, ranOn(run.control, name) ? 'control-only' : 'guarded-only'])
187
- );
205
+ const ran = refusalRuns(vector, run.control, run.guarded);
206
+ const onlyProtected = protectedRefusals(vector);
207
+ const protectedOnly = only(g, c).filter((name) => !onlyProtected.has(name));
188
208
  const why = [];
189
- let control = c?.pass ? 'pass' : 'fail';
190
- if (!c) why.push('no control');
191
- else if (c.refused.length > 0 && row.via !== 'guard') {
192
- control = 'fail';
193
- why.push('an unguarded control was refused');
194
- } else if (row.via === 'guard' && g && only(c, g).length > 0) {
195
- control = 'fail';
196
- why.push(`refused only on the control: ${only(c, g).join(', ')}`);
197
- } else why.push(...c.mismatches.map((m) => `control ${m}`));
209
+ const refusal = controlRefusal(c, g, row.via, only, onlyProtected);
210
+ const control = c?.pass && !refusal ? 'pass' : 'fail';
211
+ if (refusal) why.push(refusal);
212
+ else why.push(...c.mismatches.map((m) => `control ${m}`));
198
213
  let guarded = g?.pass ? 'pass' : 'fail';
199
214
  if (!g) why.push('no guarded run');
200
- else if (row.via === 'guard' && only(g, c).length > 0) {
215
+ else if (row.via === 'guard' && protectedOnly.length > 0) {
201
216
  guarded = 'fail';
202
- why.push(`refused only on the protected target: ${only(g, c).join(', ')}`);
217
+ why.push(`refused only on the protected target: ${protectedOnly.join(', ')}`);
203
218
  } else if (row.via === 'unguarded' && g.refused.length > 0 && !c?.pass) {
204
219
  guarded = 'fail';
205
220
  why.push('refused on the protected target without a passing control');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.8.2",
3
+ "setVersion": "1.8.4",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
@@ -81,7 +81,8 @@
81
81
  "capabilities": {
82
82
  "autoAttach": "Target.setAutoAttach, the children steps and child sessions",
83
83
  "browserSession": "CDP calls on the browser session",
84
- "browserContexts": "Target.createBrowserContext and Target.disposeBrowserContext"
84
+ "browserContexts": "Target.createBrowserContext and Target.disposeBrowserContext",
85
+ "chromeUrlsAttach": "attaching to and driving chrome:// pages, which CDP over a DevTools port can and chrome.debugger can't"
85
86
  },
86
87
  "placeholders": {
87
88
  "target": "the target's origin; {plain} on the control",
@@ -7805,6 +7806,11 @@
7805
7806
  "requestStage": "Response"
7806
7807
  }
7807
7808
  ]
7809
+ },
7810
+ "refusable": true,
7811
+ "refusal": {
7812
+ "match": "^slicc: refused",
7813
+ "onlyProtected": true
7808
7814
  }
7809
7815
  },
7810
7816
  {
@@ -7874,6 +7880,7 @@
7874
7880
  "targets": ["seven", "vault", "vault-x", "apex"],
7875
7881
  "observe": {
7876
7882
  "candidates": {
7883
+ "dependsOn": ["refused.2"],
7877
7884
  "means": "attempts is not empty",
7878
7885
  "check": {
7879
7886
  "result": "attempts",
@@ -7885,6 +7892,7 @@
7885
7892
  }
7886
7893
  },
7887
7894
  "refused": {
7895
+ "dependsOn": ["refused.2"],
7888
7896
  "means": "every attempt was refused",
7889
7897
  "check": {
7890
7898
  "refused": ["attempts"],
@@ -7892,6 +7900,7 @@
7892
7900
  }
7893
7901
  },
7894
7902
  "answered": {
7903
+ "dependsOn": ["refused.2"],
7895
7904
  "means": "body is 'real-{tag}'",
7896
7905
  "check": {
7897
7906
  "result": "body",
@@ -7901,6 +7910,7 @@
7901
7910
  }
7902
7911
  },
7903
7912
  "overridden": {
7913
+ "dependsOn": ["refused.2"],
7904
7914
  "means": "the fetch reported the overridden response status",
7905
7915
  "check": {
7906
7916
  "result": "body",
@@ -12386,7 +12396,7 @@
12386
12396
  },
12387
12397
  "session": "browser",
12388
12398
  "as": "attach",
12389
- "requires": ["browserSession"]
12399
+ "requires": ["browserSession", "chromeUrlsAttach"]
12390
12400
  },
12391
12401
  {
12392
12402
  "page": "navigate",
@@ -12402,7 +12412,7 @@
12402
12412
  }
12403
12413
  ],
12404
12414
  "targets": ["chrome-newtab", "chrome-new-tab-page"],
12405
- "requires": ["browserSession"],
12415
+ "requires": ["browserSession", "chromeUrlsAttach"],
12406
12416
  "observe": {
12407
12417
  "attached": {
12408
12418
  "means": "the guard attached to the settled new tab page",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,14 +65,14 @@
65
65
  "trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
66
66
  "release": "semantic-release",
67
67
  "test:guard": "npm run test:guard-integration && npm run test:guard-sweep",
68
- "test:guard-integration": "node --test test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-scales.test.mjs",
68
+ "test:guard-integration": "node --test test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-scales.test.mjs",
69
69
  "test:guard-sweep": "node scripts/test-guards.mjs",
70
70
  "test:gates": "node --test test/integration/gates.test.mjs",
71
71
  "test:guard-target": "node --test test/integration/guard-runner.test.mjs",
72
72
  "test:unit:guard": "node --experimental-test-coverage --test --test-coverage-include=guard-runner.mjs --test-coverage-include=fixture-server.mjs --test-coverage-lines=100 test/unit/guard-*.test.mjs"
73
73
  },
74
74
  "devDependencies": {
75
- "@ai-ecoverse/slicc-shared-web": "1.23.2",
75
+ "@ai-ecoverse/slicc-shared-web": "1.23.3",
76
76
  "@biomejs/biome": "2.5.15",
77
77
  "ajv": "8.20.0",
78
78
  "ajv-formats": "3.0.1",
@@ -81,7 +81,7 @@
81
81
  "capabilities": {
82
82
  "type": "object",
83
83
  "additionalProperties": false,
84
- "required": ["autoAttach", "browserSession", "browserContexts"],
84
+ "required": ["autoAttach", "browserSession", "browserContexts", "chromeUrlsAttach"],
85
85
  "properties": {
86
86
  "autoAttach": {
87
87
  "type": "string",
@@ -94,6 +94,10 @@
94
94
  "browserContexts": {
95
95
  "type": "string",
96
96
  "minLength": 1
97
+ },
98
+ "chromeUrlsAttach": {
99
+ "type": "string",
100
+ "minLength": 1
97
101
  }
98
102
  }
99
103
  },
@@ -117,7 +121,7 @@
117
121
  },
118
122
  "$defs": {
119
123
  "capability": {
120
- "enum": ["autoAttach", "browserSession", "browserContexts"]
124
+ "enum": ["autoAttach", "browserSession", "browserContexts", "chromeUrlsAttach"]
121
125
  },
122
126
  "requires": {
123
127
  "type": "array",
@@ -452,6 +456,9 @@
452
456
  "additionalProperties": false,
453
457
  "required": ["match"],
454
458
  "properties": {
459
+ "onlyProtected": {
460
+ "type": "boolean"
461
+ },
455
462
  "match": {
456
463
  "type": "string",
457
464
  "minLength": 1