@ai-ecoverse/slicc-browsers 1.8.3 → 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
@@ -147,6 +147,8 @@ Every CDP guard runs the vectors in `guard-vectors.json` (schema `schema/guard-v
147
147
 
148
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
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.
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.
@@ -169,7 +171,7 @@ The `C8.newtab` reachability control attaches through the guard to a settled ord
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
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.
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.
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
 
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.3",
3
+ "setVersion": "1.8.4",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
@@ -7806,6 +7806,11 @@
7806
7806
  "requestStage": "Response"
7807
7807
  }
7808
7808
  ]
7809
+ },
7810
+ "refusable": true,
7811
+ "refusal": {
7812
+ "match": "^slicc: refused",
7813
+ "onlyProtected": true
7809
7814
  }
7810
7815
  },
7811
7816
  {
@@ -7875,6 +7880,7 @@
7875
7880
  "targets": ["seven", "vault", "vault-x", "apex"],
7876
7881
  "observe": {
7877
7882
  "candidates": {
7883
+ "dependsOn": ["refused.2"],
7878
7884
  "means": "attempts is not empty",
7879
7885
  "check": {
7880
7886
  "result": "attempts",
@@ -7886,6 +7892,7 @@
7886
7892
  }
7887
7893
  },
7888
7894
  "refused": {
7895
+ "dependsOn": ["refused.2"],
7889
7896
  "means": "every attempt was refused",
7890
7897
  "check": {
7891
7898
  "refused": ["attempts"],
@@ -7893,6 +7900,7 @@
7893
7900
  }
7894
7901
  },
7895
7902
  "answered": {
7903
+ "dependsOn": ["refused.2"],
7896
7904
  "means": "body is 'real-{tag}'",
7897
7905
  "check": {
7898
7906
  "result": "body",
@@ -7902,6 +7910,7 @@
7902
7910
  }
7903
7911
  },
7904
7912
  "overridden": {
7913
+ "dependsOn": ["refused.2"],
7905
7914
  "means": "the fetch reported the overridden response status",
7906
7915
  "check": {
7907
7916
  "result": "body",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.8.3",
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",
@@ -456,6 +456,9 @@
456
456
  "additionalProperties": false,
457
457
  "required": ["match"],
458
458
  "properties": {
459
+ "onlyProtected": {
460
+ "type": "boolean"
461
+ },
459
462
  "match": {
460
463
  "type": "string",
461
464
  "minLength": 1