comber 0.1.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.
Files changed (158) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +732 -0
  3. package/action.yml +279 -0
  4. package/comber.config.example.json +64 -0
  5. package/dist/api/classify.js +99 -0
  6. package/dist/api/classify.js.map +1 -0
  7. package/dist/api/driver.js +235 -0
  8. package/dist/api/driver.js.map +1 -0
  9. package/dist/api/graphql-driver.js +150 -0
  10. package/dist/api/graphql-driver.js.map +1 -0
  11. package/dist/api/graphql.js +115 -0
  12. package/dist/api/graphql.js.map +1 -0
  13. package/dist/api/har-driver.js +167 -0
  14. package/dist/api/har-driver.js.map +1 -0
  15. package/dist/api/har.js +100 -0
  16. package/dist/api/har.js.map +1 -0
  17. package/dist/api/load.js +83 -0
  18. package/dist/api/load.js.map +1 -0
  19. package/dist/api/openapi.js +159 -0
  20. package/dist/api/openapi.js.map +1 -0
  21. package/dist/api/request.js +141 -0
  22. package/dist/api/request.js.map +1 -0
  23. package/dist/api/schema.js +126 -0
  24. package/dist/api/schema.js.map +1 -0
  25. package/dist/cli.js +352 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/comment.js +164 -0
  28. package/dist/comment.js.map +1 -0
  29. package/dist/config.js +664 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/connectors/builtin/discord.js +55 -0
  32. package/dist/connectors/builtin/github-checks.js +53 -0
  33. package/dist/connectors/builtin/junit.js +70 -0
  34. package/dist/connectors/builtin/linear.js +58 -0
  35. package/dist/connectors/builtin/slack.js +60 -0
  36. package/dist/connectors/builtin/webhook.js +39 -0
  37. package/dist/connectors/index.js +3 -0
  38. package/dist/connectors/loader.js +72 -0
  39. package/dist/connectors/registry.js +28 -0
  40. package/dist/connectors/types.js +23 -0
  41. package/dist/core/action-gate.js +48 -0
  42. package/dist/core/baseline.js +85 -0
  43. package/dist/core/baseline.js.map +1 -0
  44. package/dist/core/blind-spots.js +22 -0
  45. package/dist/core/blind-spots.js.map +1 -0
  46. package/dist/core/contract-extras.js +1 -0
  47. package/dist/core/contract-extras.js.map +1 -0
  48. package/dist/core/contract.js +1 -0
  49. package/dist/core/contract.js.map +1 -0
  50. package/dist/core/coverage.js +70 -0
  51. package/dist/core/coverage.js.map +1 -0
  52. package/dist/core/crawl.js +229 -0
  53. package/dist/core/crawl.js.map +1 -0
  54. package/dist/core/egress.js +128 -0
  55. package/dist/core/egress.js.map +1 -0
  56. package/dist/core/ledger.js +41 -0
  57. package/dist/core/ledger.js.map +1 -0
  58. package/dist/core/net.js +39 -0
  59. package/dist/core/pipeline.js +191 -0
  60. package/dist/core/pipeline.js.map +1 -0
  61. package/dist/core/plan.js +121 -0
  62. package/dist/core/plan.js.map +1 -0
  63. package/dist/core/redact.js +240 -0
  64. package/dist/core/redact.js.map +1 -0
  65. package/dist/core/replay.js +263 -0
  66. package/dist/core/replay.js.map +1 -0
  67. package/dist/core/report.js +253 -0
  68. package/dist/core/report.js.map +1 -0
  69. package/dist/core/safety-judge.js +116 -0
  70. package/dist/core/safety.js +92 -0
  71. package/dist/core/safety.js.map +1 -0
  72. package/dist/core/signal-normalize.js +190 -0
  73. package/dist/core/signal-normalize.js.map +1 -0
  74. package/dist/core/signals.js +5 -0
  75. package/dist/core/signals.js.map +1 -0
  76. package/dist/core/triage.js +104 -0
  77. package/dist/core/triage.js.map +1 -0
  78. package/dist/core/types.js +1 -0
  79. package/dist/core/types.js.map +1 -0
  80. package/dist/desktop/desktop-profile.js +6 -0
  81. package/dist/desktop/desktop-profile.js.map +1 -0
  82. package/dist/desktop/driver.js +433 -0
  83. package/dist/desktop/driver.js.map +1 -0
  84. package/dist/desktop/evidence.js +206 -0
  85. package/dist/desktop/evidence.js.map +1 -0
  86. package/dist/desktop/macos/accessibility-runner.js +118 -0
  87. package/dist/desktop/macos/accessibility-runner.js.map +1 -0
  88. package/dist/desktop/macos/types.js +1 -0
  89. package/dist/desktop/macos/types.js.map +1 -0
  90. package/dist/desktop/preflight.js +107 -0
  91. package/dist/desktop/preflight.js.map +1 -0
  92. package/dist/desktop/proxy-proof.js +185 -0
  93. package/dist/desktop/proxy-proof.js.map +1 -0
  94. package/dist/desktop/view.js +348 -0
  95. package/dist/desktop/view.js.map +1 -0
  96. package/dist/desktop/windows/types.js +1 -0
  97. package/dist/desktop/windows/types.js.map +1 -0
  98. package/dist/desktop/windows/uia-runner.js +127 -0
  99. package/dist/desktop/windows/uia-runner.js.map +1 -0
  100. package/dist/native/apple.js +14 -0
  101. package/dist/native/apple.js.map +1 -0
  102. package/dist/native/device-profile.js +6 -0
  103. package/dist/native/device-profile.js.map +1 -0
  104. package/dist/native/driver.js +416 -0
  105. package/dist/native/driver.js.map +1 -0
  106. package/dist/native/evidence.js +164 -0
  107. package/dist/native/evidence.js.map +1 -0
  108. package/dist/native/load.js +9 -0
  109. package/dist/native/load.js.map +1 -0
  110. package/dist/native/maestro.js +153 -0
  111. package/dist/native/maestro.js.map +1 -0
  112. package/dist/native/preflight.js +72 -0
  113. package/dist/native/preflight.js.map +1 -0
  114. package/dist/native/proxy-proof.js +182 -0
  115. package/dist/native/proxy-proof.js.map +1 -0
  116. package/dist/native/samsung.js +34 -0
  117. package/dist/native/samsung.js.map +1 -0
  118. package/dist/native/signals.js +50 -0
  119. package/dist/native/signals.js.map +1 -0
  120. package/dist/native/view.js +139 -0
  121. package/dist/native/view.js.map +1 -0
  122. package/dist/profile.js +237 -0
  123. package/dist/profile.js.map +1 -0
  124. package/dist/sink.js +134 -0
  125. package/dist/sink.js.map +1 -0
  126. package/dist/sinks/file.js +49 -0
  127. package/dist/sinks/file.js.map +1 -0
  128. package/dist/sinks/http.js +38 -0
  129. package/dist/sinks/http.js.map +1 -0
  130. package/dist/sinks/index.js +5 -0
  131. package/dist/sinks/index.js.map +1 -0
  132. package/dist/sinks/multi.js +40 -0
  133. package/dist/sinks/multi.js.map +1 -0
  134. package/dist/sinks/paths.js +32 -0
  135. package/dist/sinks/paths.js.map +1 -0
  136. package/dist/sinks/record.js +58 -0
  137. package/dist/sinks/record.js.map +1 -0
  138. package/dist/sinks/types.js +1 -0
  139. package/dist/sinks/types.js.map +1 -0
  140. package/dist/web/breakage.js +210 -0
  141. package/dist/web/breakage.js.map +1 -0
  142. package/dist/web/controls.js +102 -0
  143. package/dist/web/controls.js.map +1 -0
  144. package/dist/web/driver.js +803 -0
  145. package/dist/web/driver.js.map +1 -0
  146. package/dist/web/elements.js +151 -0
  147. package/dist/web/elements.js.map +1 -0
  148. package/dist/web/fingerprint.js +13 -0
  149. package/dist/web/fingerprint.js.map +1 -0
  150. package/dist/web/forms.js +74 -0
  151. package/dist/web/forms.js.map +1 -0
  152. package/dist/web/links.js +194 -0
  153. package/dist/web/links.js.map +1 -0
  154. package/dist/web/types.js +1 -0
  155. package/dist/web/types.js.map +1 -0
  156. package/package.json +80 -0
  157. package/runtime/macos-accessibility-runner.js +275 -0
  158. package/runtime/windows-uia-runner.ps1 +539 -0
@@ -0,0 +1,85 @@
1
+ import { readFile } from "node:fs/promises";
2
+ function bySignature(findings) {
3
+ const out = new Map();
4
+ for (const finding of findings) {
5
+ out.set(finding.signature, finding);
6
+ }
7
+ return out;
8
+ }
9
+ export function compareBaseline(current, baseline, baselinePath) {
10
+ if (!baseline) {
11
+ const newFindings = [...current.findings];
12
+ return {
13
+ baselinePath,
14
+ baselineFound: false,
15
+ newFindings,
16
+ resolvedFindings: [],
17
+ unchangedFindings: [],
18
+ summary: {
19
+ newFail: newFindings.filter((finding) => finding.severity === "fail").length,
20
+ newWarn: newFindings.filter((finding) => finding.severity === "warn").length,
21
+ resolved: 0,
22
+ unchanged: 0,
23
+ },
24
+ };
25
+ }
26
+ const currentBySignature = bySignature(current.findings);
27
+ const baselineBySignature = bySignature(baseline.findings);
28
+ const newFindings = [];
29
+ const resolvedFindings = [];
30
+ const unchangedFindings = [];
31
+ for (const finding of current.findings) {
32
+ if (baselineBySignature.has(finding.signature))
33
+ unchangedFindings.push(finding);
34
+ else
35
+ newFindings.push(finding);
36
+ }
37
+ for (const finding of baseline.findings) {
38
+ if (!currentBySignature.has(finding.signature))
39
+ resolvedFindings.push(finding);
40
+ }
41
+ return {
42
+ baselinePath,
43
+ baselineFound: true,
44
+ newFindings,
45
+ resolvedFindings,
46
+ unchangedFindings,
47
+ summary: {
48
+ newFail: newFindings.filter((finding) => finding.severity === "fail").length,
49
+ newWarn: newFindings.filter((finding) => finding.severity === "warn").length,
50
+ resolved: resolvedFindings.length,
51
+ unchanged: unchangedFindings.length,
52
+ },
53
+ };
54
+ }
55
+ export async function readBaselineResult(path) {
56
+ // A missing OR malformed baseline must degrade to "no baseline" (the run still
57
+ // writes its report + result.json) — never abort the whole crawl after the fact.
58
+ // We warn loudly so a typo'd / corrupt baseline path is visible, not silent.
59
+ let text;
60
+ try {
61
+ text = await readFile(path, "utf8");
62
+ }
63
+ catch (err) {
64
+ if (err.code === "ENOENT")
65
+ return undefined;
66
+ console.warn(`comber: baseline unreadable (${path}): ${String(err).slice(0, 120)} — treating as no baseline`);
67
+ return undefined;
68
+ }
69
+ try {
70
+ const parsed = JSON.parse(text);
71
+ if (!Array.isArray(parsed.findings)) {
72
+ console.warn(`comber: baseline has no findings array (${path}) — treating as no baseline`);
73
+ return undefined;
74
+ }
75
+ return parsed;
76
+ }
77
+ catch (err) {
78
+ console.warn(`comber: baseline not valid JSON (${path}): ${String(err).slice(0, 120)} — treating as no baseline`);
79
+ return undefined;
80
+ }
81
+ }
82
+ export async function compareBaselineFromPath(current, path) {
83
+ const baseline = await readBaselineResult(path);
84
+ return compareBaseline(current, baseline, path);
85
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/core/baseline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ export function fallbackBlindSpots(count, scope, message) {
2
+ const out = [];
3
+ for (let i = 0; i < count; i++) {
4
+ out.push({
5
+ kind: "unknown-surface",
6
+ severity: "blocker-for-exhaustive",
7
+ scope,
8
+ message,
9
+ });
10
+ }
11
+ return out;
12
+ }
13
+ export function summarizeBlindSpots(blindSpots) {
14
+ const out = {};
15
+ for (const blindSpot of blindSpots) {
16
+ out[blindSpot.kind] = (out[blindSpot.kind] ?? 0) + 1;
17
+ }
18
+ return out;
19
+ }
20
+ export function blindSpotsBlockExhaustive(blindSpots) {
21
+ return blindSpots.some((blindSpot) => blindSpot.severity === "blocker-for-exhaustive");
22
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blind-spots.js","sourceRoot":"","sources":["../../src/core/blind-spots.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,KAAa,EAAE,OAAe;IAC9E,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,iBAAiB;YACvB,QAAQ,EAAE,wBAAwB;YAClC,KAAK;YACL,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAgC;IAClE,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,UAAgC;IACxE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,wBAAwB,CAAC,CAAC;AACzF,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-extras.js","sourceRoot":"","sources":["../../src/core/contract-extras.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/core/contract.ts"],"names":[],"mappings":""}
@@ -0,0 +1,70 @@
1
+ import { blindSpotsBlockExhaustive, fallbackBlindSpots, summarizeBlindSpots } from "./blind-spots.js";
2
+ export class CoverageAccumulator {
3
+ statesVisited = 0;
4
+ elementsDiscovered = 0;
5
+ elementsExercised = 0;
6
+ frameBlindSpots = 0;
7
+ cappedNotExhaustive = false;
8
+ blindSpots = [];
9
+ skipLedger = {};
10
+ /** Driver sub-metrics + orchestrator-tracked link discovery. */
11
+ extra = {};
12
+ /** Element-partition skips only (denylist/disabled/file-input/idempotent/cap) — drives the
13
+ * element-coverage denominator. Form/route skips go in skipLedger but NOT here. */
14
+ elementSkips = 0;
15
+ noteState() {
16
+ this.statesVisited++;
17
+ }
18
+ discover(n) {
19
+ this.elementsDiscovered += n;
20
+ }
21
+ exercise() {
22
+ this.elementsExercised++;
23
+ }
24
+ /** Non-element skip (forms, route-visit-cap, near-dup-state) — ledger only. */
25
+ skip(reason) {
26
+ this.skipLedger[reason] = (this.skipLedger[reason] ?? 0) + 1;
27
+ }
28
+ /** Element-partition skip — ledger AND the coverage denominator. */
29
+ skipElement(reason) {
30
+ this.elementSkips++;
31
+ this.skip(reason);
32
+ }
33
+ blind(n, details, scope = "unknown") {
34
+ this.frameBlindSpots += n;
35
+ if (details && details.length > 0) {
36
+ this.blindSpots.push(...details);
37
+ return;
38
+ }
39
+ this.blindSpots.push(...fallbackBlindSpots(n, scope, "surface could not be inspected"));
40
+ }
41
+ /** Bump a single driver/orchestrator sub-metric. */
42
+ bumpExtra(key, n = 1) {
43
+ this.extra[key] = (this.extra[key] ?? 0) + n;
44
+ }
45
+ /** Merge a driver's end-of-run sub-metrics into `extra`. */
46
+ mergeExtra(record) {
47
+ for (const [k, v] of Object.entries(record))
48
+ this.extra[k] = (this.extra[k] ?? 0) + v;
49
+ }
50
+ summary() {
51
+ // Honest denominator: non-link exercisable = discovered − element-skips − links.
52
+ // Only ELEMENT-partition skips count (not form/route skips, which aren't elements).
53
+ const followTargets = this.extra.followTargetsDiscovered ?? this.extra.linksDiscovered ?? 0;
54
+ const denom = Math.max(this.elementsExercised, this.elementsDiscovered - this.elementSkips - followTargets);
55
+ return {
56
+ statesVisited: this.statesVisited,
57
+ elementsDiscovered: this.elementsDiscovered,
58
+ elementsExercised: this.elementsExercised,
59
+ elementsSkipped: this.elementSkips,
60
+ skipLedger: { ...this.skipLedger },
61
+ frameBlindSpots: this.frameBlindSpots,
62
+ blindSpots: this.blindSpots.map((blindSpot) => ({ ...blindSpot })),
63
+ blindSpotSummary: summarizeBlindSpots(this.blindSpots),
64
+ elementCoveragePct: denom > 0 ? Math.round((100 * this.elementsExercised) / denom) : 100,
65
+ exhaustiveClaimAllowed: !this.cappedNotExhaustive && !blindSpotsBlockExhaustive(this.blindSpots),
66
+ cappedNotExhaustive: this.cappedNotExhaustive,
67
+ extra: { ...this.extra },
68
+ };
69
+ }
70
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/core/coverage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,229 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { exerciseState } from "./pipeline.js";
4
+ import { CoverageAccumulator } from "./coverage.js";
5
+ import { ExhaustivenessLedger } from "./ledger.js";
6
+ import { PlanRecorder } from "./plan.js";
7
+ import { makeSafetyJudge } from "./safety-judge.js";
8
+ import { invariants, jaccardNearDup, nameSet, stateFingerprint } from "./safety.js";
9
+ import { compareFindings, normalizeBreakageSignals } from "./signal-normalize.js";
10
+ /**
11
+ * Frontier BFS over in-scope Addresses. The frontier grows TWO ways: it FOLLOWS the
12
+ * driver's outbound addresses (links, never clicked) AND enqueues addresses the driver
13
+ * reports it NAVIGATED to while acting (`ExerciseResult.discovered` — native tap-only
14
+ * screens). `exerciseState` exhaustively interacts with every other node per state.
15
+ *
16
+ * TWO DISTINCT DEDUP LAYERS (contract v2), never conflated:
17
+ * • FRONTIER de-dup — keyed by the GOTO HANDLE (`addr.dedupeKey ?? addr.id`): the `queued`
18
+ * set, so the same handle is never `goto`'d twice.
19
+ * • VISITED-by-STATE — keyed by `stateFingerprint(snapshot())`: the `visited` set, so two
20
+ * handles that land on the same structural screen are processed once.
21
+ *
22
+ * The driver owns "what is a location" (Address) and "what is in scope" (inScope). Probes
23
+ * are driver-DECLARED with a lifecycle slot (`capabilities.probes`), and the orchestrator
24
+ * fires each driver's probes at the matching slot — reproducing web's original fixed
25
+ * ordering (after-goto: session · after-snapshot: axe/dom/links · after-exercise: forms ·
26
+ * finalize) while letting any driver register new phases (native: crash/logs). Hardened
27
+ * near-dup fingerprint + per-scope visit cap terminate infinite-feed/live pages; all global
28
+ * caps are authoritative backstops.
29
+ */
30
+ export async function crawl(driver, cfg, runDir) {
31
+ const startedAt = Date.now();
32
+ const spend = { input: 0, output: 0 };
33
+ const visited = new Set(); // exact-ish state fingerprints
34
+ const visitedByScope = new Map(); // for Jaccard near-dup collapse
35
+ const queued = new Set(); // Address.id already in/through the frontier
36
+ const frontier = [];
37
+ const rawSignals = [];
38
+ const visitedUrls = [];
39
+ const triedSignatures = new Set();
40
+ const performedMutations = new Set();
41
+ const visitsPerScope = new Map();
42
+ const coverage = new CoverageAccumulator();
43
+ const ledger = new ExhaustivenessLedger();
44
+ // Record an ordered, driver-agnostic execution PLAN (addresses + node ids only) as the
45
+ // crawl runs — the cheap half of "discover once, replay cheaply". Persisted below.
46
+ const recorder = new PlanRecorder({ target: cfg.target, startUrl: cfg.startUrl });
47
+ // The LLM safety-judge is the HOSTED (strict) gate. In advisory (default) the deterministic
48
+ // floor's effect-based DENIES still apply, but ambiguous mutations proceed WITHOUT a per-action
49
+ // LLM call — preserving the LLM-free crawl and today's behavior for internal / BYO-key runs.
50
+ // Built once (one Anthropic client); undefined unless strict AND an LLM is configured.
51
+ const safetyJudge = cfg.actionGateMode === "strict" ? makeSafetyJudge(cfg) : undefined;
52
+ let states = 0;
53
+ let actions = 0;
54
+ let cappedBy;
55
+ // Hand the driver the coverage sink so its sub-metrics + target-specific skips land in
56
+ // the one honest ledger (BEFORE start, so any pass the driver wires at start can record).
57
+ driver.attachCoverage?.(coverage);
58
+ driver.attachRunDir?.(runDir);
59
+ // Driver-declared probes, fired at their lifecycle slot in registration order. `record`
60
+ // feeds the replay plan (no-op for the pre-beginState after-goto slot + the un-scoped
61
+ // finalize slot). This reproduces web's old fixed session/axe/dom/links/forms ordering
62
+ // exactly while letting any driver register new phases at the same slots.
63
+ const probesAt = (slot) => driver.capabilities.probes.filter((p) => p.slot === slot);
64
+ const runSlot = async (slot, record) => {
65
+ if (!driver.probe)
66
+ return [];
67
+ const out = [];
68
+ for (const reg of probesAt(slot)) {
69
+ if (record)
70
+ recorder.recordProbe(reg.name); // no-op until a state is begun
71
+ out.push(...(await driver.probe(reg.name)));
72
+ }
73
+ return out;
74
+ };
75
+ // Traversal itself spends NO tokens (picker is heuristic); the only model call is the
76
+ // post-crawl Opus judge, which enforces its own input cap (see triage.ts). So the token
77
+ // ceiling is not a crawl-loop backstop — these gates are wall-clock / states / actions only.
78
+ const overBudget = () => {
79
+ if (states >= cfg.maxStates)
80
+ return `maxStates(${cfg.maxStates})`;
81
+ if (actions >= cfg.maxActions)
82
+ return `maxActions(${cfg.maxActions})`;
83
+ if (Date.now() - startedAt >= cfg.maxWallClockMs)
84
+ return "maxWallClockMs";
85
+ return undefined;
86
+ };
87
+ const overActionBudget = () => {
88
+ if (actions >= cfg.maxActions)
89
+ return `maxActions(${cfg.maxActions})`;
90
+ if (Date.now() - startedAt >= cfg.maxWallClockMs)
91
+ return "maxWallClockMs";
92
+ return undefined;
93
+ };
94
+ // FRONTIER de-dup is by GOTO HANDLE (`dedupeKey ?? id`) — never the structural state hash
95
+ // (that is the `visited` fingerprint layer below). Web/API leave `dedupeKey` unset, so the
96
+ // key is `id` and behavior is unchanged; native sets it to a destination-screen fingerprint
97
+ // so two tap-paths reaching the same screen enqueue once.
98
+ const enqueue = (addr) => {
99
+ const key = addr.dedupeKey ?? addr.id;
100
+ if (driver.inScope(addr) && !queued.has(key)) {
101
+ queued.add(key);
102
+ frontier.push(addr);
103
+ }
104
+ };
105
+ // start() is INSIDE the try so a mid-start failure (e.g. WebDriver launches Chromium then
106
+ // throws in newContext()/route()) still hits the finally and closes the driver — otherwise a
107
+ // failed start leaks a headless browser process, compounding across CI runs.
108
+ try {
109
+ await driver.start();
110
+ const entry = driver.entryAddress();
111
+ enqueue(entry);
112
+ while (frontier.length > 0) {
113
+ cappedBy = overBudget();
114
+ if (cappedBy) {
115
+ coverage.cappedNotExhaustive = true;
116
+ break;
117
+ }
118
+ invariants({ actions, cfg });
119
+ const addr = frontier.shift();
120
+ const scope = addr.scope;
121
+ const visits = visitsPerScope.get(scope) ?? 0;
122
+ if (visits >= cfg.maxVisitsPerRoute) {
123
+ coverage.skip("route-visit-cap");
124
+ continue;
125
+ }
126
+ visitsPerScope.set(scope, visits + 1);
127
+ await driver.goto(addr);
128
+ rawSignals.push(...driver.drainSignals());
129
+ // AFTER-GOTO slot (web: session-bounce — emit, don't halt, avoids false-halt on
130
+ // legit /login routes). Fires before beginState, so it is not recorded into a state.
131
+ rawSignals.push(...(await runSlot("after-goto", true)));
132
+ const snap = await driver.snapshot();
133
+ const fp = stateFingerprint(snap);
134
+ if (visited.has(fp))
135
+ continue;
136
+ // Near-dup collapse, scoped to the same scope (so /user/1 vs /user/2 never merge).
137
+ const ns = nameSet(snap);
138
+ const priors = visitedByScope.get(scope) ?? [];
139
+ if (priors.some((p) => jaccardNearDup(ns, p) >= cfg.nearDupThreshold)) {
140
+ coverage.skip("near-dup-state");
141
+ continue;
142
+ }
143
+ visited.add(fp);
144
+ priors.push(ns);
145
+ visitedByScope.set(scope, priors);
146
+ states++;
147
+ coverage.noteState();
148
+ visitedUrls.push(snap.address.label ?? snap.address.id);
149
+ // Open this state's plan record BEFORE its probes/outbound/exercises so they attach to it.
150
+ recorder.beginState(snap.address);
151
+ // Screenshot the state for the report. Screenshots are RAW pixels of the target (on-screen
152
+ // secrets/PII can't be text-redacted), so --no-screenshots skips capture entirely.
153
+ if (cfg.screenshots) {
154
+ const shot = await driver.screenshot();
155
+ const shotPath = join(runDir, `state-${states}.png`);
156
+ if (shot.length)
157
+ await writeFile(shotPath, shot).catch(() => { });
158
+ }
159
+ // AFTER-SNAPSHOT (pre-exercise) slot. Web order = axe (bounded a11y, driver
160
+ // self-limits) → dom (every-state sweep) → links (collect for the post-pass);
161
+ // registration order is preserved.
162
+ rawSignals.push(...(await runSlot("after-snapshot", true)));
163
+ // Frontier expansion #1: FOLLOW (never click) the driver's in-scope outbound targets.
164
+ if (driver.outboundAddresses)
165
+ for (const a of driver.outboundAddresses()) {
166
+ if (driver.inScope(a))
167
+ recorder.recordOutbound(a);
168
+ enqueue(a);
169
+ }
170
+ const deps = {
171
+ driver,
172
+ cfg,
173
+ triedSignatures,
174
+ performedMutations,
175
+ overBudget: overActionBudget,
176
+ onSignal: (s) => rawSignals.push(s),
177
+ coverage,
178
+ ledger,
179
+ recordExercise: (id) => recorder.recordExercise(id),
180
+ // Frontier expansion #2: enqueue states the driver NAVIGATED to while acting
181
+ // (ExerciseResult.discovered), subject to inScope + caps/dedup (enqueue handles both).
182
+ enqueue,
183
+ safetyJudge,
184
+ };
185
+ actions += await exerciseState(snap, scope, deps);
186
+ // AFTER-EXERCISE slot (web: form testing — submits are egress-gated, state restored
187
+ // per-form). Gated behind dry-run / action budget exactly as the old forms call was.
188
+ if (!cfg.dryRun && !overActionBudget())
189
+ rawSignals.push(...(await runSlot("after-exercise", true)));
190
+ }
191
+ // FINALIZE slot (native: a final crash/ANR log scan) + the finalize extras (web:
192
+ // validate EVERY collected link) — both run while the session is still live.
193
+ rawSignals.push(...(await runSlot("finalize", false)));
194
+ if (driver.finalize)
195
+ rawSignals.push(...(await driver.finalize()));
196
+ }
197
+ finally {
198
+ await driver.close();
199
+ }
200
+ // Persist the recorded execution plan (always, cheap) for deterministic replay. Driver-
201
+ // agnostic (addresses + node ids only, no handles); honors --replay-out, else <runDir>/plan.json.
202
+ const planPath = cfg.replayOutPath ?? join(runDir, "plan.json");
203
+ await writeFile(planPath, JSON.stringify(recorder.toPlan(), null, 2)).catch(() => { });
204
+ // Pull any per-driver structured audit data (web: linkAudit) for the report/result.json.
205
+ const extra = driver.auditExtra ? driver.auditExtra() : undefined;
206
+ // Dedupe by signature → Finding[] with counts.
207
+ const bySig = new Map();
208
+ for (const s of normalizeBreakageSignals(rawSignals)) {
209
+ const existing = bySig.get(s.signature);
210
+ if (existing)
211
+ existing.count++;
212
+ else
213
+ bySig.set(s.signature, { ...s, count: 1 });
214
+ }
215
+ return {
216
+ startUrl: cfg.startUrl,
217
+ visitedStates: states,
218
+ visitedUrls,
219
+ actionsTaken: actions,
220
+ findings: [...bySig.values()].sort(compareFindings),
221
+ durationMs: Date.now() - startedAt,
222
+ tokenSpend: spend,
223
+ cappedBy,
224
+ frameBlindSpots: coverage.frameBlindSpots,
225
+ coverage: coverage.summary(),
226
+ exhaustivenessLedger: ledger.snapshot(),
227
+ ...(extra ? { extra } : {}),
228
+ };
229
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crawl.js","sourceRoot":"","sources":["../../src/core/crawl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ function normalizeHostname(hostname) {
4
+ return hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
5
+ }
6
+ function isPrivateIpv4(address) {
7
+ const parts = address.split(".").map((part) => Number(part));
8
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
9
+ return false;
10
+ }
11
+ const [a, b] = parts;
12
+ if (a === undefined || b === undefined)
13
+ return false;
14
+ return (a === 0 ||
15
+ a === 10 ||
16
+ a === 127 ||
17
+ (a === 100 && b >= 64 && b <= 127) ||
18
+ (a === 169 && b === 254) ||
19
+ (a === 172 && b >= 16 && b <= 31) ||
20
+ (a === 192 && b === 168) ||
21
+ (a === 198 && (b === 18 || b === 19)) ||
22
+ a >= 224);
23
+ }
24
+ function isPrivateIpv6(address) {
25
+ const value = normalizeHostname(address);
26
+ if (value === "::" || value === "::1" || value === "")
27
+ return true; // unspecified / loopback
28
+ if (value.startsWith("::ffff:"))
29
+ return isPrivateIp(value.slice("::ffff:".length)); // IPv4-mapped
30
+ const firstHextet = Number.parseInt(value.split(":")[0] || "", 16);
31
+ if (!Number.isFinite(firstHextet))
32
+ return false;
33
+ if (firstHextet >= 0xfe80 && firstHextet <= 0xfebf)
34
+ return true; // link-local fe80::/10 (was only fe80:*)
35
+ if ((firstHextet & 0xfe00) === 0xfc00)
36
+ return true; // unique-local fc00::/7
37
+ return false;
38
+ }
39
+ export function isPrivateIp(address) {
40
+ const value = normalizeHostname(address);
41
+ const family = isIP(value);
42
+ if (family === 4)
43
+ return isPrivateIpv4(value);
44
+ if (family === 6)
45
+ return isPrivateIpv6(value);
46
+ return false;
47
+ }
48
+ // Distinct sentinel so a DNS *timeout* is told apart from a genuine no-resolution.
49
+ class DnsTimeoutError extends Error {
50
+ }
51
+ async function defaultResolveHost(hostname) {
52
+ // 3s ceiling (was 1s — too tight, flagged legit hosts on a slow resolver). On
53
+ // timeout we REJECT (transient) rather than resolve [] (which read as NXDOMAIN).
54
+ return await Promise.race([
55
+ lookup(hostname, { all: true }).then((rows) => rows.map((row) => row.address)),
56
+ new Promise((_, reject) => setTimeout(() => reject(new DnsTimeoutError("dns timeout")), 3_000)),
57
+ ]);
58
+ }
59
+ export async function checkNetworkEgress(rawUrl, resolveHost = defaultResolveHost) {
60
+ let url;
61
+ try {
62
+ url = new URL(rawUrl);
63
+ }
64
+ catch {
65
+ return { allowed: false, reason: "unparseable URL" };
66
+ }
67
+ // Network schemes only — http(s) and ws(s) (WebSocket connect targets are gated
68
+ // too). Everything else (mailto/tel/data/blob/javascript) is not a network hop.
69
+ if (!/^(?:https?|wss?):$/.test(url.protocol)) {
70
+ return { allowed: true };
71
+ }
72
+ const hostname = normalizeHostname(url.hostname);
73
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "metadata.google.internal") {
74
+ return { allowed: false, reason: "blocked local/metadata host" };
75
+ }
76
+ if (isIP(hostname)) {
77
+ return isPrivateIp(hostname) ? { allowed: false, reason: "blocked private IP" } : { allowed: true };
78
+ }
79
+ let addresses;
80
+ try {
81
+ addresses = await resolveHost(hostname);
82
+ }
83
+ catch (err) {
84
+ const code = err.code;
85
+ // Permanent non-resolution → stable block. Timeout / temporary failure →
86
+ // fail-closed for this attempt but mark transient so it is not cached.
87
+ if (code === "ENOTFOUND" || code === "NXDOMAIN" || code === "EAI_NONAME") {
88
+ return { allowed: false, reason: "host did not resolve (NXDOMAIN)" };
89
+ }
90
+ return { allowed: false, reason: `dns resolution transient failure (${code ?? "timeout"})`, transient: true };
91
+ }
92
+ if (addresses.length === 0)
93
+ return { allowed: false, reason: "host did not resolve publicly" };
94
+ if (addresses.some((address) => isPrivateIp(address))) {
95
+ return { allowed: false, reason: "host resolves to private IP" };
96
+ }
97
+ return { allowed: true };
98
+ }
99
+ export class EgressGuard {
100
+ resolveHost;
101
+ decisions = new Map();
102
+ constructor(resolveHost = defaultResolveHost) {
103
+ this.resolveHost = resolveHost;
104
+ }
105
+ check(rawUrl) {
106
+ let cacheKey = rawUrl;
107
+ try {
108
+ const url = new URL(rawUrl);
109
+ cacheKey = `${url.protocol}//${normalizeHostname(url.hostname)}`;
110
+ }
111
+ catch {
112
+ /* keep raw */
113
+ }
114
+ const existing = this.decisions.get(cacheKey);
115
+ if (existing)
116
+ return existing;
117
+ const key = cacheKey;
118
+ const pending = checkNetworkEgress(rawUrl, this.resolveHost).then((decision) => {
119
+ // Never cache a transient (DNS-timeout) verdict — re-resolve next time so a
120
+ // single slow lookup can't block a legitimate host for the whole run.
121
+ if (decision.transient)
122
+ this.decisions.delete(key);
123
+ return decision;
124
+ });
125
+ this.decisions.set(key, pending);
126
+ return pending;
127
+ }
128
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"egress.js","sourceRoot":"","sources":["../../src/core/egress.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ export class ExhaustivenessLedger {
2
+ entries = [];
3
+ recordNode(address, node, intent, disposition, opts = {}) {
4
+ this.entries.push({
5
+ addressId: address.id,
6
+ addressScope: address.scope,
7
+ stateLabel: address.label,
8
+ nodeId: node.id,
9
+ nodeKind: node.kind,
10
+ nodeLabel: node.label,
11
+ intent,
12
+ disposition,
13
+ ...(opts.reason !== undefined ? { reason: opts.reason } : {}),
14
+ ...(opts.childTargetsDiscovered !== undefined ? { childTargetsDiscovered: opts.childTargetsDiscovered } : {}),
15
+ });
16
+ }
17
+ recordBlindSpot(address, blindSpot) {
18
+ this.entries.push({
19
+ addressId: address.id,
20
+ addressScope: address.scope,
21
+ stateLabel: address.label,
22
+ disposition: "blind-spot",
23
+ blindSpotKind: blindSpot.kind,
24
+ severity: blindSpot.severity,
25
+ message: blindSpot.message,
26
+ });
27
+ }
28
+ recordMissingNode(address, nodeId, reason) {
29
+ this.entries.push({
30
+ addressId: address.id,
31
+ addressScope: address.scope,
32
+ stateLabel: address.label,
33
+ nodeId,
34
+ disposition: "blocked",
35
+ reason,
36
+ });
37
+ }
38
+ snapshot() {
39
+ return this.entries.map((entry) => ({ ...entry }));
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/core/ledger.ts"],"names":[],"mappings":"AAqBA,MAAM,OAAO,oBAAoB;IACd,OAAO,GAAgC,EAAE,CAAC;IAE3D,UAAU,CACR,OAAgB,EAChB,IAAiB,EACjB,MAAuB,EACvB,WAA6D,EAC7D,OAA6D,EAAE;QAE/D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,OAAO,CAAC,KAAK;YACzB,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,SAAS,EAAE,IAAI,CAAC,KAAK;YACrB,MAAM;YACN,WAAW;YACX,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,sBAAsB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9G,CAAC,CAAC;IACL,CAAC;IAED,eAAe,CAAC,OAAgB,EAAE,SAAoB;QACpD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,OAAO,CAAC,KAAK;YACzB,WAAW,EAAE,YAAY;YACzB,aAAa,EAAE,SAAS,CAAC,IAAI;YAC7B,QAAQ,EAAE,SAAS,CAAC,QAAQ;YAC5B,OAAO,EAAE,SAAS,CAAC,OAAO;SAC3B,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB,CAAC,OAAgB,EAAE,MAAc,EAAE,MAAc;QAChE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,OAAO,CAAC,KAAK;YACzB,MAAM;YACN,WAAW,EAAE,SAAS;YACtB,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;CACF"}