@ai-ecoverse/slicc-browsers 1.9.0 → 1.9.2
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 +5 -2
- package/extension-world.mjs +75 -15
- package/guard-vectors.json +28 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ Every CDP guard runs the vectors in `guard-vectors.json` (schema `schema/guard-v
|
|
|
172
172
|
- `C5.continue-request` and `C5.continue-response`, added next to `C5.document`;
|
|
173
173
|
- 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.
|
|
174
174
|
|
|
175
|
-
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
|
|
175
|
+
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. `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.
|
|
176
176
|
|
|
177
177
|
The integration launcher loads shared-web 1.25.1’s harness extension exactly once through `Extensions.loadUnpacked` (`ankmbodmgimjemnojmeddfcdlbocolob`), whose content script runs on HTTP(S) pages. shared-web 1.25.1 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.
|
|
178
178
|
|
|
@@ -237,7 +237,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
|
|
|
237
237
|
- `/plant`: sets the canary;
|
|
238
238
|
- `/beacon`, and `/log?n=`, which returns `{ hits, beacons }`.
|
|
239
239
|
|
|
240
|
-
`{ "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. 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
|
|
240
|
+
`{ "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. 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.
|
|
241
241
|
|
|
242
242
|
**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`.
|
|
243
243
|
|
|
@@ -443,6 +443,9 @@ invocation: controls must pass and guarded protection expectations must fail; th
|
|
|
443
443
|
target use `GUARD_TARGET=seven npm run test:guard-target`. The fixture also serves
|
|
444
444
|
`/live` image requests from stand-in documents, independently of injected code.
|
|
445
445
|
|
|
446
|
+
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.
|
|
447
|
+
|
|
448
|
+
|
|
446
449
|
```js
|
|
447
450
|
import set from '@ai-ecoverse/slicc-browsers/guard-vectors.json' with { type: 'json' };
|
|
448
451
|
import { run } from '@ai-ecoverse/slicc-browsers/guard-runner';
|
package/extension-world.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { outcome } from './guard-driver.mjs';
|
|
|
3
3
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
4
|
const extension = (context) =>
|
|
5
5
|
context?.origin?.startsWith('chrome-extension://') && !context.auxData?.isDefault;
|
|
6
|
-
const marker =
|
|
6
|
+
const marker =
|
|
7
|
+
'typeof root !== "undefined" && root === document.documentElement && root.dataset.sliccHarnessExtension === chrome.runtime.id';
|
|
7
8
|
const refused = (out) => !out.ok && /^slicc: refused:/.test(out.error?.message ?? '');
|
|
8
9
|
|
|
9
10
|
async function discover(reader, session) {
|
|
@@ -174,7 +175,74 @@ function commands(world, objectId, promiseObjectId, nodeId) {
|
|
|
174
175
|
];
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
async function
|
|
178
|
+
async function guessWorld({ client, session, world, handles, events }) {
|
|
179
|
+
const exposed = new Set(
|
|
180
|
+
events()
|
|
181
|
+
.filter((event) => event.method === 'Runtime.executionContextCreated')
|
|
182
|
+
.map((event) => event.params.context.id)
|
|
183
|
+
);
|
|
184
|
+
const ids = new Set([
|
|
185
|
+
...Array.from({ length: 32 }, (_, index) => index + 1),
|
|
186
|
+
handles.world.id,
|
|
187
|
+
world.id,
|
|
188
|
+
...exposed,
|
|
189
|
+
]);
|
|
190
|
+
const numeric = [];
|
|
191
|
+
for (const id of ids) {
|
|
192
|
+
const probes = [
|
|
193
|
+
['Runtime.evaluate', { contextId: id, expression: marker, returnByValue: true }],
|
|
194
|
+
[
|
|
195
|
+
'Runtime.callFunctionOn',
|
|
196
|
+
{
|
|
197
|
+
executionContextId: id,
|
|
198
|
+
functionDeclaration: `function(){return ${marker}}`,
|
|
199
|
+
returnByValue: true,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
];
|
|
203
|
+
for (const [method, params] of probes) {
|
|
204
|
+
const out = await outcome(client.send(method, params, session));
|
|
205
|
+
numeric.push({
|
|
206
|
+
id,
|
|
207
|
+
method,
|
|
208
|
+
exposed: exposed.has(id),
|
|
209
|
+
refused: refused(out),
|
|
210
|
+
ok: out.ok && !out.result.exceptionDetails && typeof out.result.result?.value === 'boolean',
|
|
211
|
+
reached: out.ok && out.result.result?.value === true,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const unique = [];
|
|
216
|
+
for (const uniqueContextId of new Set([handles.world.uniqueId, world.uniqueId])) {
|
|
217
|
+
const out = await outcome(
|
|
218
|
+
client.send(
|
|
219
|
+
'Runtime.evaluate',
|
|
220
|
+
{ uniqueContextId, expression: marker, returnByValue: true },
|
|
221
|
+
session
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
unique.push({
|
|
225
|
+
uniqueContextId,
|
|
226
|
+
refused: refused(out),
|
|
227
|
+
reached: out.ok && out.result.result?.value === true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
guessesRefused:
|
|
232
|
+
numeric.filter((guess) => !guess.exposed).every((guess) => guess.refused) &&
|
|
233
|
+
unique.every((guess) => guess.refused),
|
|
234
|
+
guessesReached: [...numeric, ...unique].some((guess) => guess.reached),
|
|
235
|
+
exposedContextsWork: numeric
|
|
236
|
+
.filter((guess) => guess.exposed)
|
|
237
|
+
.every((guess) => guess.ok && !guess.refused),
|
|
238
|
+
numericGuesses: numeric,
|
|
239
|
+
uniqueGuesses: unique,
|
|
240
|
+
controlNumericId: handles.world.id,
|
|
241
|
+
hiddenNumericId: world.id,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function selectWorld({ client, session, world, side, controls, live, events }) {
|
|
178
246
|
let handles = controls.select;
|
|
179
247
|
if (side === 'control') {
|
|
180
248
|
const object = await client.send(
|
|
@@ -220,19 +288,11 @@ async function selectWorld({ client, session, world, side, controls, live }) {
|
|
|
220
288
|
: true);
|
|
221
289
|
result[`${name}Error`] = out.error?.message ?? out.result?.exceptionDetails?.text;
|
|
222
290
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
).slice(0, 3);
|
|
229
|
-
result.guessesRefused = true;
|
|
230
|
-
result.guessesReached = true;
|
|
231
|
-
for (const [, method, params] of guesses) {
|
|
232
|
-
const out = await outcome(client.send(method, params, session));
|
|
233
|
-
result.guessesRefused &&= refused(out);
|
|
234
|
-
result.guessesReached &&= out.ok && out.result.result?.value === true;
|
|
235
|
-
}
|
|
291
|
+
Object.assign(result, await guessWorld({ client, session, world, handles, events }));
|
|
292
|
+
const pageMarker = await outcome(
|
|
293
|
+
client.send('Runtime.evaluate', { expression: marker, returnByValue: true }, session)
|
|
294
|
+
);
|
|
295
|
+
result.pageMarkerAbsent = pageMarker.ok && pageMarker.result.result?.value === false;
|
|
236
296
|
result.alive = await live();
|
|
237
297
|
return result;
|
|
238
298
|
}
|
package/guard-vectors.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"setVersion": "1.9.
|
|
3
|
+
"setVersion": "1.9.2",
|
|
4
4
|
"sources": [
|
|
5
5
|
{
|
|
6
6
|
"repo": "ai-ecoverse/slicc-cli",
|
|
@@ -415,7 +415,7 @@
|
|
|
415
415
|
}
|
|
416
416
|
},
|
|
417
417
|
"guessesRefused": {
|
|
418
|
-
"means": "numeric and unique
|
|
418
|
+
"means": "numeric ids 1..32 not exposed to this client, the same-tab hidden numeric id and extension unique-id guesses receive matched refusals; exposed numeric ids need not be refused",
|
|
419
419
|
"check": {
|
|
420
420
|
"result": "world",
|
|
421
421
|
"path": "result.guessesRefused",
|
|
@@ -424,13 +424,31 @@
|
|
|
424
424
|
}
|
|
425
425
|
},
|
|
426
426
|
"guessesReached": {
|
|
427
|
-
"means": "numeric and unique
|
|
427
|
+
"means": "numeric sweeps and extension unique-id guesses reach the content-script-only marker on the control, never on the guarded leg",
|
|
428
428
|
"check": {
|
|
429
429
|
"result": "world",
|
|
430
430
|
"path": "result.guessesReached",
|
|
431
431
|
"equals": true,
|
|
432
432
|
"observer": "client"
|
|
433
433
|
}
|
|
434
|
+
},
|
|
435
|
+
"exposedContextsWork": {
|
|
436
|
+
"means": "every numeric context id exposed to this client remains usable",
|
|
437
|
+
"check": {
|
|
438
|
+
"result": "world",
|
|
439
|
+
"path": "result.exposedContextsWork",
|
|
440
|
+
"equals": true,
|
|
441
|
+
"observer": "client"
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
"pageMarkerAbsent": {
|
|
445
|
+
"means": "the fixture content-script marker is absent from the page default world",
|
|
446
|
+
"check": {
|
|
447
|
+
"result": "world",
|
|
448
|
+
"path": "result.pageMarkerAbsent",
|
|
449
|
+
"equals": true,
|
|
450
|
+
"observer": "client"
|
|
451
|
+
}
|
|
434
452
|
}
|
|
435
453
|
},
|
|
436
454
|
"live": "the observer confirms a foreign extension isolated world exists on an ordinary page",
|
|
@@ -465,7 +483,9 @@
|
|
|
465
483
|
"resolveNodeRefused": false,
|
|
466
484
|
"resolveNodeReached": true,
|
|
467
485
|
"guessesRefused": false,
|
|
468
|
-
"guessesReached": true
|
|
486
|
+
"guessesReached": true,
|
|
487
|
+
"exposedContextsWork": true,
|
|
488
|
+
"pageMarkerAbsent": true
|
|
469
489
|
}
|
|
470
490
|
},
|
|
471
491
|
"expected": {
|
|
@@ -480,7 +500,10 @@
|
|
|
480
500
|
"propertiesRefused": true,
|
|
481
501
|
"bindingRefused": true,
|
|
482
502
|
"resolveNodeRefused": true,
|
|
483
|
-
"guessesRefused": true
|
|
503
|
+
"guessesRefused": true,
|
|
504
|
+
"exposedContextsWork": true,
|
|
505
|
+
"pageMarkerAbsent": true,
|
|
506
|
+
"guessesReached": false
|
|
484
507
|
}
|
|
485
508
|
},
|
|
486
509
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-browsers",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
|
|
67
67
|
"release": "semantic-release",
|
|
68
68
|
"test:guard": "npm run test:guard-integration && npm run test:guard-sweep",
|
|
69
|
-
"test:guard-integration": "node --test --test-concurrency=2 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-worlds.test.mjs test/integration/guard-scales.test.mjs",
|
|
69
|
+
"test:guard-integration": "node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs 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-worlds.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-scales.test.mjs",
|
|
70
70
|
"test:guard-sweep": "node scripts/test-guards.mjs",
|
|
71
71
|
"test:gates": "node --test test/integration/gates.test.mjs",
|
|
72
72
|
"test:guard-target": "node --test test/integration/guard-runner.test.mjs",
|