@ai-ecoverse/slicc-browsers 1.20.2 → 1.21.0

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
@@ -183,17 +183,21 @@ It also offers `storageKey(key)` and `unparseable(text)`. A Go or Rust implement
183
183
 
184
184
  ## Guard vectors
185
185
 
186
- 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.
186
+ 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 cover navigation, redirect chains, popups, frames, request interception, cookies, storage, privileged and error pages, foreign extension worlds, debugger tricks and session overrides that would leak into a protected page.
187
187
 
188
188
  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.
189
189
 
190
- When a vector compares ordinary and protected fixture markers, those markers do not contain one another. Protected-marker checks still examine complete probe replies, including errors, and the vector's client events and messages, including encoded values.
191
-
192
190
  A guard may refuse every client navigation to an `about:` URL that inherits an origin, such as `about:blank`. Refusing an ordinary navigation away from a protected page still fails.
193
191
 
194
- Client-exit controls use the unguarded observer; persistent-capability refusals are valid evidence, granted instruments must leave no effects after exit, and missing answers remain incomplete.
192
+ ### What a guard must let through
193
+
194
+ For stock Playwright, Puppeteer and raw CDP clients, a guard that declares `stockSetup` admits on ordinary pages:
195
+ - `Page.addScriptToEvaluateOnNewDocument` whose `source` matches `^(?:[ \t\n]|//[^\n\r\u2028\u2029]*)*$` on the whole string without the multiline flag (no code), with any `worldName` or `runImmediately`, and its removal;
196
+ - exactly `Emulation.setFocusEmulationEnabled`, `setEmulatedMedia`, `setDeviceMetricsOverride` and `setTouchEmulationEnabled`;
197
+ - `Page.createIsolatedWorld` on a frame the guard already classifies as ordinary: strip `grantUniveralAccess` and `contentSecurityPolicy`, return no `executionContextId` for any other frame; `Runtime.addBinding` scoped to such a world by a non-empty `executionContextName` or its `executionContextId`, with `Runtime.bindingCalled` only from those worlds;
198
+ - `Target.setAutoAttach`: accept the call but narrow the filter to tab targets, hold pages under the tab like pages (same URL redaction in `targetInfo`); a transport that synthesizes it declares `syntheticAutoAttach`.
195
199
 
196
- Forwarding audits retain early snapshots as evidence. A complete final ledger supersedes an incomplete early snapshot, but any forwarding reported in either audit remains a failure. An incomplete final audit remains missing evidence.
200
+ The strip holds whether or not a given Chrome lets that world reach another origin. Code init scripts, main-world bindings and other overrides stay refused or harmless. Admitted display overrides and no-code init scripts must not remain in effect on a protected document — refuse that navigation, or clear them before it runs. Undeclared, they skip, never pass.
197
201
 
198
202
  ### Running them
199
203
 
@@ -225,20 +229,20 @@ You supply:
225
229
  | capability | means the transport can |
226
230
  | --- | --- |
227
231
  | `autoAttach` | auto-attach to child targets such as popups and iframes |
228
- | `browserSession` | send commands to the browser session, not only to a page |
232
+ | `browserSession` | send commands on the browser session, not only a page |
229
233
  | `browserContexts` | create isolated browser contexts |
230
- | `discovery` | report target creation and changes without a session |
231
- | `chromeUrlsAttach` | attach to `chrome://` pages (CDP over a DevTools port can; `chrome.debugger` can't) |
232
- | `extensionWorld` | host a foreign extension's content script on the test page |
233
- | `extensionFrame` | host the harness extension, whose frame an ordinary test page embeds |
234
- | `clientExit` | close the guarded client in the middle of a row |
235
- | `siteIsolationOff`, `inboundDelay`, `forwardAudit` | run without site isolation, hold the browser's messages to the guard, and audit what the guard forwarded |
236
-
237
- `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.
234
+ | `discovery` | report target create/change without a session |
235
+ | `chromeUrlsAttach` | attach to `chrome://` pages (DevTools-port CDP; not `chrome.debugger`) |
236
+ | `extensionWorld` | host a foreign extension content script on the test page |
237
+ | `extensionFrame` | host the harness extension frame an ordinary test page embeds |
238
+ | `clientExit` | close the guarded client mid-row |
239
+ | `siteIsolationOff`, `inboundDelay`, `forwardAudit` | no site isolation; hold inbound; audit forwarded commands |
240
+ | `secureOrigin` | HTTPS origins (service workers) |
241
+ | `stockSetup`, `syntheticAutoAttach` | admit stock clients' setup calls; synthesize auto-attach |
238
242
 
239
243
  ### Reading the result
240
244
 
241
- 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:
245
+ Each row reports `control`, `guarded` and `live` as `pass`, `fail`, `skip`, `incomplete` (evidence missing) or `not-run` (words only). `summary()` makes one line:
242
246
 
243
247
  ```text
244
248
  GUARD-PASS my-guard@1a2b3c4 (vectors @ai-ecoverse/slicc-browsers@1.18.5)
@@ -266,7 +270,7 @@ A new discovery behaviour gets a corpus case, and a new guard behaviour gets a g
266
270
 
267
271
  CI is [slicc-shared-web's pipeline](https://github.com/ai-ecoverse/slicc-shared-web#pipeline), called from `.github/workflows/ci.yml`. A pull request gets lint, then `npm test` split over shards by timing, then a Claude and a Codex review; the merge queue runs lint and the tests again. Tests run on Linux only.
268
272
 
269
- Every merge to `main` that warrants it is released by semantic-release in the same workflow, as one version for all three packages. The `vX.Y.Z` tag is the Go release, and npm gets the package with provenance through trusted publishing. Then `ci.yml` publishes the crate to crates.io, also with trusted publishing, and asks the Go module proxy and pkg.go.dev for the new tag. Pushed tags are never moved or deleted: a bad release is fixed by the next one.
273
+ Every merge to `main` that warrants it is released by semantic-release as one version for all three packages. Pushed tags are never moved or deleted: a bad release is fixed by the next one.
270
274
 
271
275
  ## License
272
276
 
@@ -938,7 +938,48 @@ async function driveCast(client, page, frameMs) {
938
938
  };
939
939
  }
940
940
 
941
- export async function driveReveal(client, reader, page, observer, frameMs, missing, side) {
941
+ function classifyRevealOverlap(
942
+ captureStartedAt,
943
+ revealAt,
944
+ pendingWhenRevealAcknowledged,
945
+ captureSettledAt
946
+ ) {
947
+ if (typeof captureStartedAt !== 'number' || typeof revealAt !== 'number') return 'unknown';
948
+ if (revealAt <= captureStartedAt) return 'before';
949
+ if (pendingWhenRevealAcknowledged) return 'during';
950
+ if (typeof captureSettledAt === 'number' && captureSettledAt < revealAt) return 'after';
951
+ return 'unknown';
952
+ }
953
+
954
+ function revealRaceVerdict(overlap, side, ok, shownRefused) {
955
+ if (overlap === 'during' || overlap === 'before') return side === 'control' ? ok : shownRefused;
956
+ if (overlap === 'after') return ok || shownRefused;
957
+ return false;
958
+ }
959
+
960
+ async function finishReveal(
961
+ client,
962
+ reader,
963
+ page,
964
+ observer,
965
+ frameMs,
966
+ { ok, text, overlap, revealRacePass }
967
+ ) {
968
+ const revealedPress = await click(client, reader, page, observer);
969
+ await style(reader, observer, 'hidden', frameMs);
970
+ const rehidden = await shot(client, page);
971
+ return {
972
+ revealedShot: ok,
973
+ revealedRefusal: text,
974
+ revealOverlap: overlap,
975
+ revealRacePass,
976
+ revealedClick: revealedPress.ok,
977
+ revealedClickRefusal: refusal(revealedPress.out),
978
+ rehiddenShot: rehidden.ok,
979
+ };
980
+ }
981
+
982
+ async function attemptRevealCapture(client, reader, page, observer, side) {
942
983
  const captureStartedAt = await observe(reader, observer, 'performance.now()');
943
984
  let captureSettled = false;
944
985
  let captureSettledAt;
@@ -963,31 +1004,38 @@ export async function driveReveal(client, reader, page, observer, frameMs, missi
963
1004
  const out = await pending;
964
1005
  const ok = out.ok && typeof out.result?.data === 'string' && out.result.data.length > 0;
965
1006
  const text = refusal(out);
966
- let overlap = 'unknown';
967
- if (typeof captureStartedAt !== 'number' || typeof revealAt !== 'number') overlap = 'unknown';
968
- else if (revealAt <= captureStartedAt) overlap = 'before';
969
- else if (pendingWhenRevealAcknowledged) overlap = 'during';
970
- else if (typeof captureSettledAt === 'number' && captureSettledAt < revealAt) overlap = 'after';
971
- if (overlap === 'unknown') missing.push('reveal/capture overlap unproven');
1007
+ const overlap = classifyRevealOverlap(
1008
+ captureStartedAt,
1009
+ revealAt,
1010
+ pendingWhenRevealAcknowledged,
1011
+ captureSettledAt
1012
+ );
972
1013
  const shownRefused = Boolean(text && (text === SHOWN || /^slicc: refused/.test(text)));
973
- let revealRacePass = false;
974
- if (overlap === 'during' || overlap === 'before')
975
- revealRacePass = side === 'control' ? ok : shownRefused;
976
- else if (overlap === 'after') revealRacePass = ok || shownRefused;
977
- const revealedPress = await click(client, reader, page, observer);
978
- await style(reader, observer, 'hidden', frameMs);
979
- const rehidden = await shot(client, page);
980
1014
  return {
981
- revealedShot: ok,
982
- revealedRefusal: text,
983
- revealOverlap: overlap,
984
- revealRacePass,
985
- revealedClick: revealedPress.ok,
986
- revealedClickRefusal: refusal(revealedPress.out),
987
- rehiddenShot: rehidden.ok,
1015
+ ok,
1016
+ text,
1017
+ overlap,
1018
+ revealRacePass: revealRaceVerdict(overlap, side, ok, shownRefused),
988
1019
  };
989
1020
  }
990
1021
 
1022
+ export async function driveReveal(client, reader, page, observer, frameMs, missing, side) {
1023
+ const maxAttempts = 3;
1024
+ let last = null;
1025
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1026
+ last = await attemptRevealCapture(client, reader, page, observer, side);
1027
+ if (last.overlap !== 'unknown')
1028
+ return finishReveal(client, reader, page, observer, frameMs, last);
1029
+ if (attempt < maxAttempts) await style(reader, observer, 'hidden', frameMs);
1030
+ }
1031
+ missing.push(`reveal/capture overlap unproven (${maxAttempts} attempts)`);
1032
+ return finishReveal(client, reader, page, observer, frameMs, {
1033
+ ...last,
1034
+ overlap: 'unknown',
1035
+ revealRacePass: false,
1036
+ });
1037
+ }
1038
+
991
1039
  function frameObservations({ pageAttached, origin, read, leaks, picture, input, captures }) {
992
1040
  return {
993
1041
  exists: true,
@@ -103,6 +103,8 @@ function route(req, url, send, html, redirects) {
103
103
  },
104
104
  '/doc': () => document(url, html),
105
105
  '/csp': () => document(url, html),
106
+ '/sw.js': () =>
107
+ send(200, "self.addEventListener('install',()=>self.skipWaiting())", 'text/javascript'),
106
108
  };
107
109
  if (req.method === 'OPTIONS') return send(204, '');
108
110
  if (url.pathname.startsWith('/auth')) {