@ai-ecoverse/slicc-browsers 1.10.0 → 1.10.1

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
@@ -182,7 +182,7 @@ The `C9` foreign extension isolated-world family requires `extensionWorld`: a br
182
182
 
183
183
  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.
184
184
 
185
- `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 requires `browserSession`, uses the default context, and cleans up its own targets and cookie. The HTTP fixture accepts Chrome’s encoded-space Host header without reparsing it as a WHATWG origin.
185
+ `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.
186
186
 
187
187
  `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.
188
188
 
@@ -206,8 +206,8 @@ A client may register `Fetch.enable` patterns naming a protected origin in eithe
206
206
  - `scope`: `any`, or the one guard the vector is for;
207
207
  - `setup` and `trigger`, both lists of steps;
208
208
  - `targets`: the protected or reachability targets it runs against;
209
- - `requires?`: the capabilities it needs, the union of its steps' and observations' `requires`. 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;
210
- - `observe?`: each observed name, as `{ "means", "dependsOn"?, "requires"?, "eitherWhen"? }`. `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);
209
+ - `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;
210
+ - `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);
211
211
  - `live`: the liveness check;
212
212
  - `control` (`origin: "ordinary"`, `via`, `expected`) and `expected`: an object of names and booleans, or text;
213
213
  - `skip?` (`target`, `reason`): `reason` starts with a code, as in `not-applicable: …`.
@@ -247,7 +247,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
247
247
 
248
248
  `{ "extensionWorld": "hidden" | "select" | "debugger" | "drivable", "as": "world", "requires": ["extensionWorld"] }` runs the C9 compound probe on the current ordinary page. The runner attaches an independent observer to discover actual context ids, unique ids and names, and sends probes through the client session. Discovery matches the expected fixture extension’s exact origin, never the first extension world: `run()`’s optional `extensionWorldId` defaults to shared-web’s harness id `ankmbodmgimjemnojmeddfcdlbocolob`. Multiple unrelated content-script worlds are ignored for fixture selection, while guards must still protect foreign worlds. A custom fixture must provide the same content-script marker contract. Object handles are minted and validated on the unguarded control session, then replayed as guesses on the guarded leg; Chrome object handles and DOM node ids are session-local, so observer handles must never be mistaken for client handles. Numeric ids from the control are also replayed alongside fresh observer-discovered selectors, but numeric ids are local to a tab: a control id may name the next tab’s exposed default context, which must work and return no extension marker. The same-tab hidden world’s actual numeric id and every extension unique-id guess still require a matched refusal. Unknown numeric ids and stale object handles (`not exposed`) need explicit refusals rather than Chrome’s invalid-id errors. `guessesRefused` aggregates only required refusals; `guessesReached` must be false on the guarded leg, and `exposedContextsWork` must remain true. Its result contains boolean evidence consumed by ordinary `result` checks. The unguarded leg validates those selectors against Chrome itself before guarded evidence can pass.
249
249
 
250
- `{ "unparseable": true, "as": "unparseable", "requires": ["browserSession"] }` runs the C8 malformed-URL compound probe on the current ordinary page: independent browser ground truth, raw/canonical destination commands, discovery/attach and cookie checks. Boolean evidence is consumed by ordinary `result` checks, with native Chrome outcomes retained in its diagnostics.
250
+ `{ "unparseable": true, "as": "unparseable" }` runs the C8 malformed-URL compound probe on the current ordinary page: independent browser ground truth, raw/canonical destination commands, discovery/attach and cookie checks. Boolean evidence is consumed by ordinary `result` checks, with native Chrome outcomes retained in its diagnostics.
251
251
 
252
252
  **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`.
253
253
 
@@ -281,7 +281,7 @@ It exports:
281
281
  - `refusals(vector)`: the vector's refusal names;
282
282
  - `outcome`, `call(client, step, sessionId)` and `isRefusal(outcome, step)`;
283
283
  - `instrument(client, sessionId, log, tag)`, `load(client, sessionId, url)` and `popup(client, openerId)`;
284
- - `judge(vector, side, observed)`: `{ pass, refused, skipped, mismatches }`, where `refused` lists the leg's refusals and `skipped` the observations that depend on them;
284
+ - `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;
285
285
  - `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;
286
286
  - `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, 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 …`.
287
287
 
@@ -294,7 +294,7 @@ It exports:
294
294
  - a skip names one of the vector's targets;
295
295
  - `observe` defines every expected name;
296
296
  - 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;
297
- - every `Target.setAutoAttach`, `children`, child-session, browser-session and browser-context step has the matching `requires`, and a vector's `requires` is the union of its steps' and observations';
297
+ - 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;
298
298
  - every `{name}` is in `placeholders`;
299
299
  - no URL uses a port from 9222 to 9229.
300
300
 
@@ -362,7 +362,7 @@ With the `browserContexts` capability each leg runs in its own browser context.
362
362
 
363
363
  `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.
364
364
 
365
- `log(row)` receives completed rows. Besides `control`, `guarded`, `live`, `why` and `errors`, a row may carry `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
365
+ `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
366
366
  explicit `Target.detachFromTarget` step and for a `settle.variant` step.
367
367
 
368
368
  Every runnable vector has a declarative `liveCheck`; every observation has a
package/guard-driver.mjs CHANGED
@@ -139,8 +139,10 @@ export function judge(vector, side, observed) {
139
139
  const mismatches = observed === true ? [] : [want];
140
140
  return { pass: mismatches.length === 0, refused, skipped: [], mismatches };
141
141
  }
142
- const skipped = Object.keys(want).filter((k) =>
143
- (vector.observe?.[k]?.dependsOn ?? []).some((d) => refused.includes(d))
142
+ const skipped = Object.keys(want).filter(
143
+ (k) =>
144
+ (observed?.[k] === 'not-supported' && vector.observe?.[k]?.requires?.length > 0) ||
145
+ (vector.observe?.[k]?.dependsOn ?? []).some((d) => refused.includes(d))
144
146
  );
145
147
  const mismatches = Object.entries(want)
146
148
  .filter(
@@ -227,6 +229,13 @@ export function result(vector, run) {
227
229
  guarded,
228
230
  live,
229
231
  ...(Object.keys(ran).length ? { ran } : {}),
232
+ ...(Object.values(run.guarded ?? {}).includes('not-supported')
233
+ ? {
234
+ notSupported: Object.keys(run.guarded).filter(
235
+ (name) => run.guarded[name] === 'not-supported'
236
+ ),
237
+ }
238
+ : {}),
230
239
  ...(why.length ? { why: why.join('; ') } : {}),
231
240
  };
232
241
  }
package/guard-runner.mjs CHANGED
@@ -664,6 +664,7 @@ class Execution {
664
664
  origin: this.vars.ordinary,
665
665
  tag: this.vars.tag,
666
666
  fixture: this.options.fixture,
667
+ browserSession: supported(['browserSession'], this.options.capabilities),
667
668
  })
668
669
  );
669
670
  if (step.extensionWorld)
@@ -1086,7 +1087,10 @@ class Execution {
1086
1087
  await sleep(100);
1087
1088
  const observed = { ...this.refused };
1088
1089
  for (const [name, observation] of Object.entries(this.vector.observe ?? {})) {
1089
- if (!supported(observation.requires, this.options.capabilities)) continue;
1090
+ if (!supported(observation.requires, this.options.capabilities)) {
1091
+ observed[name] = 'not-supported';
1092
+ continue;
1093
+ }
1090
1094
  if ((observation.dependsOn ?? []).some((dep) => this.refused[dep])) continue;
1091
1095
  observed[name] = await this.observe(name, observation.check);
1092
1096
  if (observation.eitherWhen && this.side === 'guarded')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.10.0",
3
+ "setVersion": "1.10.1",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
@@ -133,12 +133,10 @@
133
133
  "trigger": [
134
134
  {
135
135
  "unparseable": true,
136
- "as": "unparseable",
137
- "requires": ["browserSession"]
136
+ "as": "unparseable"
138
137
  }
139
138
  ],
140
139
  "targets": ["ordinary"],
141
- "requires": ["browserSession"],
142
140
  "observe": {
143
141
  "live": {
144
142
  "means": "unparseable URL protection live",
@@ -169,6 +167,7 @@
169
167
  },
170
168
  "listed": {
171
169
  "means": "unparseable URL protection listed",
170
+ "requires": ["browserSession"],
172
171
  "check": {
173
172
  "result": "unparseable",
174
173
  "path": "result.listed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -430,7 +430,7 @@
430
430
  {
431
431
  "type": "object",
432
432
  "additionalProperties": false,
433
- "required": ["unparseable", "as", "requires"],
433
+ "required": ["unparseable", "as"],
434
434
  "properties": {
435
435
  "unparseable": {
436
436
  "const": true
@@ -439,11 +439,7 @@
439
439
  "$ref": "#/$defs/name"
440
440
  },
441
441
  "requires": {
442
- "$ref": "#/$defs/requires",
443
- "type": "array",
444
- "contains": {
445
- "const": "browserSession"
446
- }
442
+ "$ref": "#/$defs/requires"
447
443
  }
448
444
  }
449
445
  },
@@ -10,7 +10,25 @@ const hosts = [
10
10
  ['xn--a.sliccy.ai', 'xn--a.sliccy.ai'],
11
11
  ];
12
12
 
13
+ export const rootURL = (root, host, path) =>
14
+ `${root.protocol}//${host}${root.port ? `:${root.port}` : ''}${path}`;
15
+
16
+ export function canonicalURL(value) {
17
+ try {
18
+ return new URL(value).href;
19
+ } catch {
20
+ const match = /^(https?:)\/\/([^/?#]+)(.*)$/.exec(value);
21
+ if (!match) return value;
22
+ const [, protocol, authority, path] = match;
23
+ const host = authority.replace(/:\d*$/, '').replaceAll(' ', '%20');
24
+ const port = /:(\d*)$/.exec(authority)?.[1];
25
+ const parsed = new URL(`${protocol}//example.test${port ? `:${port}` : ''}${path}`);
26
+ return rootURL(parsed, host, `${parsed.pathname}${parsed.search}${parsed.hash}`);
27
+ }
28
+ }
29
+
13
30
  async function document(reader, targetId, expected, allowNativeRejection = false) {
31
+ expected = canonicalURL(expected);
14
32
  const { sessionId } = await reader.send('Target.attachToTarget', { targetId, flatten: true });
15
33
  try {
16
34
  const deadline = Date.now() + 5000;
@@ -19,7 +37,7 @@ async function document(reader, targetId, expected, allowNativeRejection = false
19
37
  do {
20
38
  const { targetInfo } = await reader.send('Target.getTargetInfo', { targetId });
21
39
  targetURL = targetInfo.url;
22
- if (targetInfo.url !== expected) {
40
+ if (canonicalURL(targetInfo.url) !== expected) {
23
41
  last = { url: targetInfo.url, title: targetInfo.title };
24
42
  await pause(25);
25
43
  continue;
@@ -41,7 +59,7 @@ async function document(reader, targetId, expected, allowNativeRejection = false
41
59
  );
42
60
  const value = out.result?.result?.value;
43
61
  last = value ?? out;
44
- if (out.ok && value?.title === 'doc' && value.href === expected) return value;
62
+ if (out.ok && value?.title === 'doc' && canonicalURL(value.href) === expected) return value;
45
63
  await pause(25);
46
64
  } while (Date.now() < deadline);
47
65
  if (allowNativeRejection && ['', 'about:blank', 'about:blank#blocked'].includes(targetURL))
@@ -75,10 +93,10 @@ async function destinations({ client, reader, root, tag, targets, session, targe
75
93
  out.error?.code === -32000 && /invalid URL/i.test(out.error?.message ?? '');
76
94
  for (const [index, [input, canonical]] of hosts.entries()) {
77
95
  const strict = canonical === 'exa%20mple.sliccy.ai';
78
- const base = `${root.protocol}//${canonical}:${root.port}/doc?n=${tag}`;
96
+ const base = rootURL(root, canonical, `/doc?n=${tag}`);
79
97
  const created = await outcome(
80
98
  client.send('Target.createTarget', {
81
- url: `${root.protocol}//${input}:${root.port}/doc?n=${tag}-create-${index}`,
99
+ url: rootURL(root, input, `/doc?n=${tag}-create-${index}`),
82
100
  background: true,
83
101
  })
84
102
  );
@@ -95,7 +113,7 @@ async function destinations({ client, reader, root, tag, targets, session, targe
95
113
  const navigated = await outcome(
96
114
  client.send(
97
115
  'Page.navigate',
98
- { url: `${root.protocol}//${input}:${root.port}/doc?n=${tag}-navigate-${index}` },
116
+ { url: rootURL(root, input, `/doc?n=${tag}-navigate-${index}`) },
99
117
  session
100
118
  )
101
119
  );
@@ -116,7 +134,7 @@ async function destinations({ client, reader, root, tag, targets, session, targe
116
134
  await outcome(
117
135
  client.send(
118
136
  'Page.navigate',
119
- { url: `${root.origin}/doc?n=${tag}-ordinary-${index}` },
137
+ { url: rootURL(root, root.hostname, `/doc?n=${tag}-ordinary-${index}`) },
120
138
  session
121
139
  )
122
140
  );
@@ -124,7 +142,16 @@ async function destinations({ client, reader, root, tag, targets, session, targe
124
142
  return result;
125
143
  }
126
144
 
127
- export async function unparseableURLs({ client, reader, session, targetId, origin, tag, fixture }) {
145
+ export async function unparseableURLs({
146
+ client,
147
+ reader,
148
+ session,
149
+ targetId,
150
+ origin,
151
+ tag,
152
+ fixture,
153
+ browserSession,
154
+ }) {
128
155
  const root = new URL(origin);
129
156
  const cookie = `seven-unparseable-${tag}`;
130
157
  const value = `secret-${tag}`;
@@ -134,7 +161,7 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
134
161
  await reader.send('Storage.setCookies', {
135
162
  cookies: [{ name: cookie, value, domain: '.sliccy.ai', path: '/', secure: false }],
136
163
  });
137
- const url = `${root.protocol}//exa%20mple.sliccy.ai:${root.port}/doc?n=${tag}-observer`;
164
+ const url = rootURL(root, 'exa%20mple.sliccy.ai', `/doc?n=${tag}-observer`);
138
165
  const { targetId: probeId } = await reader.send('Target.createTarget', {
139
166
  url,
140
167
  background: true,
@@ -143,7 +170,9 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
143
170
  const ground = await document(reader, probeId, url);
144
171
  observer = (await reader.send('Target.attachToTarget', { targetId: probeId, flatten: true }))
145
172
  .sessionId;
146
- const listed = await outcome(client.send('Target.getTargets', {}));
173
+ const listed = browserSession
174
+ ? await outcome(client.send('Target.getTargets', {}))
175
+ : 'not-supported';
147
176
  const attached = await outcome(
148
177
  client.send('Target.attachToTarget', { targetId: probeId, flatten: true })
149
178
  );
@@ -168,7 +197,10 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
168
197
  const result = {
169
198
  browserVersion: (await reader.send('Browser.getVersion', {})).product,
170
199
  live: ground.cookie.includes(`${cookie}=${value}`),
171
- listed: listed.ok && listed.result.targetInfos?.some((info) => info.targetId === probeId),
200
+ listed:
201
+ listed === 'not-supported'
202
+ ? listed
203
+ : listed.ok && listed.result.targetInfos?.some((info) => info.targetId === probeId),
172
204
  attached: attached.ok,
173
205
  attachRefused: refused(attached),
174
206
  readCookie,