@cassiomc1/forgeloop 1.13.0 → 1.14.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 (211) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/DOCS_INDEX.md +35 -3
  3. package/ENG/nodejs-backend-development-eng.md +2 -2
  4. package/ENG/sec-code-eng.md +7 -7
  5. package/EXECUTION_STATE.md +12 -0
  6. package/LOOP_ENGINEERING.md +28 -2
  7. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  8. package/PROTOCOL_INTEGRATION.md +55 -2
  9. package/README.md +40 -25
  10. package/TERMINOLOGY.md +2 -0
  11. package/THREAT_MODEL.md +140 -1
  12. package/completions/_forgeloop +19 -1
  13. package/completions/forgeloop.bash +37 -1
  14. package/completions/forgeloop.fish +123 -1
  15. package/docs/ADVISORY_CONTEXT.md +25 -0
  16. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  17. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  18. package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
  19. package/docs/AGENT_SKILL.md +66 -0
  20. package/docs/ARTIFACT_REFERENCE.md +123 -0
  21. package/docs/AUDIT_UX.md +46 -0
  22. package/docs/BROWSER_VERIFICATION.md +136 -0
  23. package/docs/CLI_REFERENCE.md +366 -6
  24. package/docs/CODE_ATTESTATION.md +2 -2
  25. package/docs/DOCUMENTATION_GUIDE.md +32 -11
  26. package/docs/JEV_BENCHMARKS.md +31 -0
  27. package/docs/MODEL_ROUTING.md +37 -0
  28. package/docs/OPENSRC_ADAPTER.md +241 -0
  29. package/docs/PACKAGE_CONTENTS.md +35 -8
  30. package/docs/PROVIDERS.md +126 -0
  31. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  32. package/docs/RECIPES.md +9 -0
  33. package/docs/RELEASE_CHECKLIST.md +38 -5
  34. package/docs/SECURITY_REVIEW.md +71 -0
  35. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  36. package/docs/TEST_INTELLIGENCE.md +29 -0
  37. package/docs/TEST_PRUNING.md +14 -0
  38. package/docs/TROUBLESHOOTING.md +198 -1
  39. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  40. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  41. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  42. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  43. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  44. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  45. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  46. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  47. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  48. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  49. package/docs/diagrams/README.md +13 -9
  50. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  51. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  52. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  53. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  54. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  55. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  56. package/docs/documentation-manifest.json +750 -5
  57. package/docs/protocol-requirements.json +24 -0
  58. package/package.json +30 -3
  59. package/schemas/config.schema.json +14 -0
  60. package/schemas/context-plan.schema.json +18 -0
  61. package/schemas/semantic-decision.schema.json +46 -0
  62. package/schemas/test-utility.schema.json +44 -0
  63. package/scripts/CI_VALIDATORS.md +6 -6
  64. package/scripts/benchmark-jev.mjs +5 -0
  65. package/scripts/benchmark-test-intelligence.mjs +4 -0
  66. package/scripts/generate-agent-protocol-summary.mjs +4 -1
  67. package/scripts/generate-forgeloop-skill.mjs +133 -0
  68. package/scripts/jev-smoke.mjs +19 -0
  69. package/skills/forgeloop/README.md +9 -0
  70. package/skills/forgeloop/SKILL.md +77 -0
  71. package/skills/forgeloop/references/lifecycle.md +9 -0
  72. package/skills/forgeloop/references/recovery.md +7 -0
  73. package/skills/forgeloop/references/verification.md +7 -0
  74. package/src/adapters/agent-browser/assertions.js +47 -0
  75. package/src/adapters/agent-browser/commands.js +54 -0
  76. package/src/adapters/agent-browser/index.js +3 -0
  77. package/src/adapters/agent-browser/locator.js +40 -0
  78. package/src/adapters/agent-browser/process.js +215 -0
  79. package/src/adapters/agent-browser/provider.js +313 -0
  80. package/src/adapters/emulated-services/constants.js +24 -0
  81. package/src/adapters/emulated-services/index.js +7 -0
  82. package/src/adapters/emulated-services/process.js +162 -0
  83. package/src/adapters/emulated-services/provider.js +282 -0
  84. package/src/adapters/opensrc/normalize.js +90 -0
  85. package/src/adapters/opensrc/process.js +248 -0
  86. package/src/adapters/opensrc/provider.js +338 -0
  87. package/src/adapters/opensrc/search.js +264 -0
  88. package/src/adapters/typesafe/client.js +28 -0
  89. package/src/adapters/typesafe/engine.js +63 -0
  90. package/src/adapters/typesafe/normalize.js +41 -0
  91. package/src/cli.js +108 -0
  92. package/src/commands/checkpoint-revalidate.js +176 -0
  93. package/src/commands/context-plan.js +38 -0
  94. package/src/commands/contract-create.js +264 -0
  95. package/src/commands/contract-revise.js +236 -0
  96. package/src/commands/decision-show.js +14 -0
  97. package/src/commands/decision-status.js +22 -0
  98. package/src/commands/discover.js +41 -0
  99. package/src/commands/doctor.js +15 -0
  100. package/src/commands/gate-record.js +205 -0
  101. package/src/commands/gate-revalidate.js +137 -0
  102. package/src/commands/model-route.js +32 -0
  103. package/src/commands/route.js +146 -18
  104. package/src/commands/semantic-plan.js +17 -0
  105. package/src/commands/task-abandon.js +224 -0
  106. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  107. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  108. package/src/commands/test-inventory.js +5 -0
  109. package/src/commands/test-prune-plan.js +5 -0
  110. package/src/commands/test-prune-probe.js +5 -0
  111. package/src/commands/test-utility.js +5 -0
  112. package/src/commands/validate-protocol.js +10 -1
  113. package/src/core/artifact-registry.js +24 -0
  114. package/src/core/audit-ux.js +514 -0
  115. package/src/core/browser-verification/constants.js +149 -0
  116. package/src/core/browser-verification/normalize.js +254 -0
  117. package/src/core/browser-verification/provider.js +519 -0
  118. package/src/core/browser-verification/service.js +115 -0
  119. package/src/core/checkpoint-revalidation.js +319 -0
  120. package/src/core/cli-command-definitions.js +241 -0
  121. package/src/core/command-executors.js +110 -0
  122. package/src/core/command-input.js +115 -43
  123. package/src/core/completion-artifacts.js +14 -5
  124. package/src/core/completion.js +4 -6
  125. package/src/core/config.js +3 -0
  126. package/src/core/context-compiler/budget.js +9 -0
  127. package/src/core/context-compiler/candidates.js +39 -0
  128. package/src/core/context-compiler/compiler.js +63 -0
  129. package/src/core/context-compiler/fingerprint.js +11 -0
  130. package/src/core/context-compiler/policy.js +13 -0
  131. package/src/core/context-compiler/result.js +23 -0
  132. package/src/core/contract-bootstrap-recovery.js +655 -0
  133. package/src/core/contract-revision.js +210 -0
  134. package/src/core/decision/artifact.js +69 -0
  135. package/src/core/decision/benchmarks.js +103 -0
  136. package/src/core/decision/cache.js +27 -0
  137. package/src/core/decision/constants.js +58 -0
  138. package/src/core/decision/cutover.js +34 -0
  139. package/src/core/decision/engine.js +22 -0
  140. package/src/core/decision/errors.js +68 -0
  141. package/src/core/decision/events.js +101 -0
  142. package/src/core/decision/freshness.js +19 -0
  143. package/src/core/decision/normalizers/index.js +115 -0
  144. package/src/core/decision/policy.js +18 -0
  145. package/src/core/decision/projection.js +16 -0
  146. package/src/core/decision/question-registry.js +201 -0
  147. package/src/core/decision/request.js +26 -0
  148. package/src/core/decision/resolver.js +130 -0
  149. package/src/core/decision/result.js +58 -0
  150. package/src/core/decision/service.js +156 -0
  151. package/src/core/decision/state-builder.js +65 -0
  152. package/src/core/decision/task-bindings.js +30 -0
  153. package/src/core/decision/test-provider.js +32 -0
  154. package/src/core/decision/thresholds.js +15 -0
  155. package/src/core/error-codes.js +278 -0
  156. package/src/core/events.js +226 -57
  157. package/src/core/evidence-readiness.js +9 -0
  158. package/src/core/execution-prerequisites.js +14 -0
  159. package/src/core/execution-profile.js +63 -38
  160. package/src/core/gate-provenance.js +124 -0
  161. package/src/core/integration-invocation-policy.js +27 -4
  162. package/src/core/integration-resources.js +86 -61
  163. package/src/core/model-router/constants.js +10 -0
  164. package/src/core/model-router/policy.js +103 -0
  165. package/src/core/model-router/router.js +37 -0
  166. package/src/core/next-action-model.js +58 -0
  167. package/src/core/next-action-phases.js +130 -42
  168. package/src/core/next-action-refresh.js +43 -9
  169. package/src/core/next-action-review-phase.js +7 -2
  170. package/src/core/next-action.js +35 -7
  171. package/src/core/phase.js +128 -10
  172. package/src/core/preflight-consistency.js +23 -9
  173. package/src/core/preflight-loaders.js +37 -5
  174. package/src/core/protocol-info.js +65 -0
  175. package/src/core/protocol.js +20 -0
  176. package/src/core/reconcile-closure.js +128 -52
  177. package/src/core/recovery-history.js +1 -0
  178. package/src/core/resumability.js +154 -44
  179. package/src/core/route-artifact.js +15 -1
  180. package/src/core/router.js +67 -1
  181. package/src/core/runtime-context.js +118 -61
  182. package/src/core/schema-validation.js +3 -0
  183. package/src/core/security-review/constants.js +64 -0
  184. package/src/core/security-review/normalize.js +245 -0
  185. package/src/core/security-review/provider.js +204 -0
  186. package/src/core/security-review/service.js +134 -0
  187. package/src/core/semantic-planning/constants.js +19 -0
  188. package/src/core/semantic-planning/projection.js +94 -0
  189. package/src/core/semantic-planning/service.js +15 -0
  190. package/src/core/sources.js +37 -0
  191. package/src/core/task-claim-state.js +201 -1
  192. package/src/core/task-conflict-inspection.js +31 -5
  193. package/src/core/task-paths.js +13 -0
  194. package/src/core/task-recovery.js +1 -0
  195. package/src/core/templates.js +3 -0
  196. package/src/core/test-intelligence/benchmarks.js +68 -0
  197. package/src/core/test-intelligence/inventory.js +73 -0
  198. package/src/core/test-intelligence/prune.js +90 -0
  199. package/src/core/test-intelligence/semantic-state.js +15 -0
  200. package/src/core/test-intelligence/service.js +40 -0
  201. package/src/core/test-intelligence/utility.js +50 -0
  202. package/src/core/trace.js +11 -7
  203. package/src/core/transaction.js +1 -0
  204. package/src/integration.d.ts +492 -0
  205. package/src/integration.js +54 -0
  206. package/src/providers/README.md +47 -0
  207. package/src/providers/capabilities.js +46 -0
  208. package/src/providers/errors.js +15 -0
  209. package/src/providers/index.js +29 -0
  210. package/src/providers/json-snapshot.js +105 -0
  211. package/src/providers/registry.js +152 -0
@@ -0,0 +1,47 @@
1
+ import { E_BROWSER_VERIFICATION_RESULT_INVALID } from "../../core/error-codes.js";
2
+
3
+ export function actualValue(data) {
4
+ if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") return String(data);
5
+ if (!data || typeof data !== "object" || Array.isArray(data)) return "";
6
+ for (const key of ["text", "value", "attribute", "url", "title", "visible"]) {
7
+ if (data[key] !== undefined && data[key] !== null) return String(data[key]);
8
+ }
9
+ return "";
10
+ }
11
+
12
+ export function observedScalar(data, label = "Agent Browser observation") {
13
+ if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") {
14
+ return String(data);
15
+ }
16
+ if (data && typeof data === "object" && !Array.isArray(data)) {
17
+ for (const key of ["text", "value", "attribute", "url", "title", "visible"]) {
18
+ if (Object.prototype.hasOwnProperty.call(data, key) && data[key] !== undefined && data[key] !== null) {
19
+ const value = data[key];
20
+ if (["string", "number", "boolean"].includes(typeof value)) return String(value);
21
+ }
22
+ }
23
+ }
24
+ const error = new Error(`${label} was malformed`);
25
+ error.code = E_BROWSER_VERIFICATION_RESULT_INVALID;
26
+ throw error;
27
+ }
28
+
29
+ export function matchesAssertion(kind, actual, expected) {
30
+ if (kind === "VISIBLE") return actual === "true";
31
+ if (kind === "HIDDEN") return actual !== "true";
32
+ if (kind === "TEXT_CONTAINS") return actual.includes(expected);
33
+ if (kind === "TEXT_EQUALS" || kind === "VALUE_EQUALS" || kind === "ATTRIBUTE_EQUALS" || kind === "TITLE_EQUALS") return actual === expected;
34
+ if (kind === "URL_IS") return actual === expected;
35
+ if (kind === "URL_PREFIX") return actual.startsWith(expected);
36
+ return false;
37
+ }
38
+
39
+ export function assertionResult(assertion, status, actual, message) {
40
+ return {
41
+ id: assertion.id,
42
+ kind: assertion.kind,
43
+ status,
44
+ ...(actual !== undefined ? { actual: String(actual) } : {}),
45
+ ...(message ? { message } : {}),
46
+ };
47
+ }
@@ -0,0 +1,54 @@
1
+ const SAFE_GLOBAL_FLAGS = Object.freeze(["--content-boundaries", "--no-webmcp"]);
2
+
3
+ function globalArgs({ sessionId, allowedOrigins, maxOutputChars = 50_000 } = {}) {
4
+ const domains = [...new Set((allowedOrigins ?? []).map((origin) => new URL(origin).hostname.toLowerCase()))].join(",");
5
+ return [
6
+ "--session", sessionId,
7
+ "--allowed-domains", domains,
8
+ "--max-output", String(maxOutputChars),
9
+ ...SAFE_GLOBAL_FLAGS,
10
+ ];
11
+ }
12
+
13
+ function command({ sessionId, allowedOrigins, maxOutputChars, name, args = [], json = true }) {
14
+ return [
15
+ ...globalArgs({ sessionId, allowedOrigins, maxOutputChars }),
16
+ name,
17
+ ...args,
18
+ ...(json ? ["--json"] : []),
19
+ ];
20
+ }
21
+
22
+ export function versionCommand() { return ["--version"]; }
23
+ export function openCommand(input) { return command({ ...input, name: "open", args: [input.url] }); }
24
+ export function clickCommand(input) { return command({ ...input, name: "click", args: [input.selector] }); }
25
+ export function focusCommand(input) { return command({ ...input, name: "focus", args: [input.selector] }); }
26
+ export function fillCommand(input) { return command({ ...input, name: "fill", args: [input.selector, input.text] }); }
27
+ export function pressCommand(input) { return command({ ...input, name: "press", args: [input.key] }); }
28
+ export function snapshotCommand(input) { return command({ ...input, name: "snapshot", args: ["-i"] }); }
29
+ export function visibleCommand(input) { return command({ ...input, name: "is", args: ["visible", input.selector] }); }
30
+ export function textCommand(input) { return command({ ...input, name: "get", args: ["text", input.selector] }); }
31
+ export function valueCommand(input) { return command({ ...input, name: "get", args: ["value", input.selector] }); }
32
+ export function attributeCommand(input) { return command({ ...input, name: "get", args: ["attr", input.selector, input.attribute] }); }
33
+ export function urlCommand(input) { return command({ ...input, name: "get", args: ["url"] }); }
34
+ export function titleCommand(input) { return command({ ...input, name: "get", args: ["title"] }); }
35
+ export function screenshotCommand(input) { return command({ ...input, name: "screenshot", args: [input.outputPath], json: true }); }
36
+ export function closeCommand(input) { return command({ ...input, name: "close", args: [] }); }
37
+
38
+ export const AGENT_BROWSER_COMMANDS = Object.freeze({
39
+ version: versionCommand,
40
+ open: openCommand,
41
+ click: clickCommand,
42
+ focus: focusCommand,
43
+ fill: fillCommand,
44
+ press: pressCommand,
45
+ snapshot: snapshotCommand,
46
+ visible: visibleCommand,
47
+ text: textCommand,
48
+ value: valueCommand,
49
+ attribute: attributeCommand,
50
+ url: urlCommand,
51
+ title: titleCommand,
52
+ screenshot: screenshotCommand,
53
+ close: closeCommand,
54
+ });
@@ -0,0 +1,3 @@
1
+ export { createAgentBrowserVerificationProvider } from "./provider.js";
2
+ export { AGENT_BROWSER_PROCESS_LIMITS, parseAgentBrowserJson, runAgentBrowserCommand } from "./process.js";
3
+ export * from "./commands.js";
@@ -0,0 +1,40 @@
1
+ import { E_BROWSER_VERIFICATION_RESULT_INVALID } from "../../core/error-codes.js";
2
+
3
+ function locatorError(message) {
4
+ const error = new Error(message);
5
+ error.code = E_BROWSER_VERIFICATION_RESULT_INVALID;
6
+ error.kind = "BLOCKED";
7
+ return error;
8
+ }
9
+
10
+ function refsFromSnapshot(snapshot) {
11
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return [];
12
+ const refs = snapshot.refs;
13
+ if (!refs || typeof refs !== "object" || Array.isArray(refs)) return [];
14
+ return Object.entries(refs).map(([ref, value]) => ({ ref, ...(value && typeof value === "object" ? value : { name: value }) }));
15
+ }
16
+
17
+ function candidateText(value) {
18
+ return [value?.name, value?.text, value?.label, value?.value].filter((item) => typeof item === "string");
19
+ }
20
+
21
+ export function resolveSnapshotLocator(locator, snapshot) {
22
+ if (!locator || typeof locator !== "object") throw locatorError("Browser locator is invalid");
23
+ if (locator.kind === "CSS") return locator.value;
24
+ const refs = refsFromSnapshot(snapshot);
25
+ const needle = locator.value;
26
+ const matches = refs.filter((entry) => {
27
+ const values = candidateText(entry);
28
+ if (locator.kind === "TEXT" || locator.kind === "LABEL") return values.some((value) => value === needle);
29
+ if (locator.kind === "ROLE") {
30
+ return entry.role === needle || values.some((value) => `${entry.role ?? ""} ${value}`.trim() === needle);
31
+ }
32
+ return false;
33
+ });
34
+ if (matches.length !== 1) {
35
+ throw locatorError(matches.length === 0 ? "Browser locator was not found" : "Browser locator was ambiguous");
36
+ }
37
+ return matches[0].ref;
38
+ }
39
+
40
+ export function snapshotRefs(snapshot) { return refsFromSnapshot(snapshot); }
@@ -0,0 +1,215 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ E_BROWSER_VERIFICATION_OUTPUT_LIMIT,
7
+ E_BROWSER_VERIFICATION_CANCELLED,
8
+ E_BROWSER_VERIFICATION_EXECUTION_FAILED,
9
+ E_BROWSER_VERIFICATION_PROVIDER_INVALID,
10
+ E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
11
+ E_BROWSER_VERIFICATION_RESULT_INVALID,
12
+ E_BROWSER_VERIFICATION_TIMEOUT,
13
+ } from "../../core/error-codes.js";
14
+
15
+ export const AGENT_BROWSER_PROCESS_LIMITS = Object.freeze({
16
+ maxStdoutBytes: 1024 * 1024,
17
+ maxStderrBytes: 64 * 1024,
18
+ terminationGraceMs: 150,
19
+ maxVersionBytes: 64 * 1024,
20
+ maxOutputChars: 50_000,
21
+ });
22
+
23
+ function processError(code, message) {
24
+ const error = new Error(message);
25
+ error.name = "AgentBrowserProcessError";
26
+ error.code = code;
27
+ return error;
28
+ }
29
+
30
+ function portableString(value, label) {
31
+ if (typeof value !== "string" || value.length === 0 || /\p{Cc}/u.test(value)) {
32
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, `${label} must be a non-empty portable string`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ export function assertAbsoluteRegularFile(value, label, fsImpl = fs) {
38
+ portableString(value, label);
39
+ if (!path.isAbsolute(value)) {
40
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, `${label} must be an absolute path`);
41
+ }
42
+ let link;
43
+ let stat;
44
+ try {
45
+ link = fsImpl.lstatSync(value);
46
+ stat = fsImpl.statSync(value);
47
+ } catch {
48
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, `${label} is unavailable`);
49
+ }
50
+ if (link.isSymbolicLink?.() || !stat.isFile?.()) {
51
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, `${label} must be a regular file without symlink indirection`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ function bytes(chunk) {
57
+ return Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(String(chunk));
58
+ }
59
+
60
+ function terminate(child) {
61
+ if (!child || typeof child.kill !== "function") return;
62
+ try { child.kill("SIGTERM"); } catch { /* exited between checks */ }
63
+ const timer = setTimeout(() => {
64
+ try { child.kill("SIGKILL"); } catch { /* preserve original failure */ }
65
+ }, AGENT_BROWSER_PROCESS_LIMITS.terminationGraceMs);
66
+ timer.unref?.();
67
+ }
68
+
69
+ function waitForClose(child) {
70
+ return new Promise((resolve) => {
71
+ let settled = false;
72
+ const finish = () => {
73
+ if (settled) return;
74
+ settled = true;
75
+ clearTimeout(timer);
76
+ resolve();
77
+ };
78
+ child?.once?.("close", finish);
79
+ const timer = setTimeout(finish, AGENT_BROWSER_PROCESS_LIMITS.terminationGraceMs + 500);
80
+ timer.unref?.();
81
+ });
82
+ }
83
+
84
+ function filteredEnvironment(overrides = {}, trustedEnvKeys = new Set()) {
85
+ const forbidden = new Set([
86
+ "AGENT_BROWSER_PROVIDER", "AGENT_BROWSER_PROFILE", "AGENT_BROWSER_RESTORE", "AGENT_BROWSER_STATE",
87
+ "AGENT_BROWSER_CDP", "AGENT_BROWSER_AUTO_CONNECT", "AGENT_BROWSER_PLUGINS", "AGENT_BROWSER_INIT_SCRIPTS",
88
+ "AGENT_BROWSER_EXTENSIONS", "AGENT_BROWSER_ALLOWED_DOMAINS", "AGENT_BROWSER_MAX_OUTPUT",
89
+ "AGENT_BROWSER_CONTENT_BOUNDARIES", "AGENT_BROWSER_NO_WEBMCP", "AGENT_BROWSER_EXECUTABLE_PATH",
90
+ "AGENT_BROWSER_SESSION", "AGENT_BROWSER_SCREENSHOT_DIR", "AGENT_BROWSER_DOWNLOAD_PATH",
91
+ "AGENT_BROWSER_ACTION_POLICY", "AGENT_BROWSER_CONFIRM_ACTIONS", "AGENT_BROWSER_ENGINE",
92
+ "AI_GATEWAY_API_KEY", "AI_GATEWAY_MODEL", "AI_GATEWAY_URL", "BROWSERBASE_API_KEY",
93
+ "BROWSERLESS_API_KEY", "BROWSER_USE_API_KEY", "KERNEL_API_KEY", "AGENT_BROWSER_CONFIG",
94
+ ]);
95
+ const env = {};
96
+ for (const [key, value] of Object.entries({ ...process.env, ...overrides })) {
97
+ if ((!forbidden.has(key) || trustedEnvKeys.has(key)) && typeof value === "string") env[key] = value;
98
+ }
99
+ return env;
100
+ }
101
+
102
+ export async function runAgentBrowserCommand(executablePath, args, {
103
+ cwd,
104
+ timeoutMs,
105
+ signal,
106
+ spawnImpl = nodeSpawn,
107
+ env,
108
+ maxStdoutBytes = AGENT_BROWSER_PROCESS_LIMITS.maxStdoutBytes,
109
+ maxStderrBytes = AGENT_BROWSER_PROCESS_LIMITS.maxStderrBytes,
110
+ fsImpl = fs,
111
+ trustedEnvKeys = [],
112
+ } = {}) {
113
+ assertAbsoluteRegularFile(executablePath, "Agent Browser executable", fsImpl);
114
+ portableString(cwd, "Agent Browser cwd");
115
+ if (!path.isAbsolute(cwd)) throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser cwd must be absolute");
116
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || /\p{Cc}/u.test(arg))) {
117
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser argv must be a portable string array");
118
+ }
119
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
120
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser timeout must be a positive integer");
121
+ }
122
+ if (signal?.aborted) throw processError(E_BROWSER_VERIFICATION_CANCELLED, "Agent Browser verification was cancelled");
123
+
124
+ let child;
125
+ try {
126
+ child = spawnImpl(executablePath, [...args], {
127
+ cwd,
128
+ shell: false,
129
+ stdio: ["ignore", "pipe", "pipe"],
130
+ env: filteredEnvironment(env, new Set(trustedEnvKeys)),
131
+ });
132
+ } catch (error) {
133
+ throw processError(
134
+ error?.code === "ENOENT" ? E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE : E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
135
+ "Agent Browser executable could not be started",
136
+ );
137
+ }
138
+ if (!child?.stdout || !child?.stderr || typeof child.on !== "function") {
139
+ terminate(child);
140
+ throw processError(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, "Agent Browser process did not expose safe output streams");
141
+ }
142
+
143
+ return new Promise((resolve, reject) => {
144
+ let stdoutBytes = 0;
145
+ let stderrBytes = 0;
146
+ const stdout = [];
147
+ let settled = false;
148
+ const timerRef = { value: null };
149
+ const cleanup = () => {
150
+ if (timerRef.value) clearTimeout(timerRef.value);
151
+ signal?.removeEventListener?.("abort", abortListener);
152
+ child.stdout.removeAllListeners?.("data");
153
+ child.stderr.removeAllListeners?.("data");
154
+ };
155
+ const fail = (error) => {
156
+ if (settled) return;
157
+ settled = true;
158
+ cleanup();
159
+ terminate(child);
160
+ child.stdout.resume?.();
161
+ child.stderr.resume?.();
162
+ waitForClose(child).then(() => reject(error));
163
+ };
164
+ const finish = (code, signalName) => {
165
+ if (settled) return;
166
+ settled = true;
167
+ cleanup();
168
+ if (code !== 0) {
169
+ reject(processError(E_BROWSER_VERIFICATION_EXECUTION_FAILED, `Agent Browser command failed (${code ?? "null"}/${signalName ?? "none"})`));
170
+ return;
171
+ }
172
+ resolve({
173
+ stdout: Buffer.concat(stdout).toString("utf8"),
174
+ stdoutBytes,
175
+ stderrBytes,
176
+ });
177
+ };
178
+ const abortListener = () => fail(processError(E_BROWSER_VERIFICATION_CANCELLED, "Agent Browser verification was cancelled"));
179
+ child.stdout.on("data", (chunk) => {
180
+ stdoutBytes += bytes(chunk);
181
+ if (stdoutBytes > maxStdoutBytes) {
182
+ fail(processError(E_BROWSER_VERIFICATION_OUTPUT_LIMIT, "Agent Browser stdout exceeded its bound"));
183
+ return;
184
+ }
185
+ stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
186
+ });
187
+ child.stderr.on("data", (chunk) => {
188
+ stderrBytes += bytes(chunk);
189
+ if (stderrBytes > maxStderrBytes) fail(processError(E_BROWSER_VERIFICATION_OUTPUT_LIMIT, "Agent Browser stderr exceeded its bound"));
190
+ });
191
+ child.once?.("error", () => fail(processError(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, "Agent Browser process failed")));
192
+ child.once?.("close", finish);
193
+ signal?.addEventListener?.("abort", abortListener, { once: true });
194
+ timerRef.value = setTimeout(() => fail(processError(E_BROWSER_VERIFICATION_TIMEOUT, "Agent Browser command timed out")), timeoutMs);
195
+ timerRef.value.unref?.();
196
+ });
197
+ }
198
+
199
+ export function parseAgentBrowserJson(stdout, label = "Agent Browser response") {
200
+ if (typeof stdout !== "string" || stdout.length > AGENT_BROWSER_PROCESS_LIMITS.maxStdoutBytes) {
201
+ throw processError(E_BROWSER_VERIFICATION_OUTPUT_LIMIT, `${label} exceeded its bound`);
202
+ }
203
+ let value;
204
+ try { value = JSON.parse(stdout); } catch { throw processError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} was not valid JSON`); }
205
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
206
+ throw processError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} must be a JSON object`);
207
+ }
208
+ if (value.success !== true || !Object.prototype.hasOwnProperty.call(value, "data")
209
+ || Object.keys(value).some((key) => !["success", "data"].includes(key))) {
210
+ throw processError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} had an unsupported response envelope`);
211
+ }
212
+ return value.data === undefined ? value : value.data;
213
+ }
214
+
215
+ export { filteredEnvironment, processError as agentBrowserProcessError };
@@ -0,0 +1,313 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ E_BROWSER_VERIFICATION_ORIGIN_DENIED,
9
+ E_BROWSER_VERIFICATION_CANCELLED,
10
+ E_BROWSER_VERIFICATION_EXECUTION_FAILED,
11
+ E_BROWSER_VERIFICATION_PROVIDER_INVALID,
12
+ E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
13
+ E_BROWSER_VERIFICATION_RESULT_INVALID,
14
+ E_BROWSER_VERIFICATION_TIMEOUT,
15
+ E_BROWSER_VERIFICATION_VERSION_UNSUPPORTED,
16
+ } from "../../core/error-codes.js";
17
+ import { assertAbsoluteRegularFile, parseAgentBrowserJson, runAgentBrowserCommand, AGENT_BROWSER_PROCESS_LIMITS } from "./process.js";
18
+ import {
19
+ attributeCommand, clickCommand, closeCommand, fillCommand, focusCommand, openCommand,
20
+ pressCommand, screenshotCommand, snapshotCommand, textCommand, titleCommand, urlCommand, valueCommand,
21
+ versionCommand, visibleCommand,
22
+ } from "./commands.js";
23
+ import { resolveSnapshotLocator } from "./locator.js";
24
+ import { observedScalar, assertionResult, matchesAssertion } from "./assertions.js";
25
+
26
+ function providerError(code, message) {
27
+ const error = new Error(message);
28
+ error.name = "AgentBrowserProviderError";
29
+ error.code = code;
30
+ return error;
31
+ }
32
+
33
+ function expectedVersion(value) {
34
+ if (value === undefined || value === null) return undefined;
35
+ if (typeof value !== "string" || value.length === 0 || value.length > 64 || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(value)) {
36
+ throw providerError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser expectedVersion is invalid");
37
+ }
38
+ return value;
39
+ }
40
+
41
+ function parseVersion(stdout) {
42
+ const match = /^\s*agent-browser\s+([^\s]+)(?:\s|$)/iu.exec(stdout);
43
+ if (!match) throw providerError(E_BROWSER_VERIFICATION_RESULT_INVALID, "Agent Browser version output was not qualified");
44
+ return match[1];
45
+ }
46
+
47
+ function remaining(deadline) {
48
+ const value = deadline - Date.now();
49
+ if (value < 1) throw providerError(E_BROWSER_VERIFICATION_TIMEOUT, "Agent Browser verification deadline expired");
50
+ return value;
51
+ }
52
+
53
+ function originOf(value) {
54
+ const parsed = new URL(value);
55
+ return `${parsed.protocol}//${parsed.hostname.toLowerCase()}${parsed.port ? `:${parsed.port}` : ""}`;
56
+ }
57
+
58
+ function verifyOrigin(value, allowedOrigins) {
59
+ let origin;
60
+ try { origin = originOf(value); } catch { throw providerError(E_BROWSER_VERIFICATION_ORIGIN_DENIED, "Agent Browser reported an invalid URL"); }
61
+ if (!allowedOrigins.includes(origin)) throw providerError(E_BROWSER_VERIFICATION_ORIGIN_DENIED, "Agent Browser left the allowed origins");
62
+ return value;
63
+ }
64
+
65
+ function dataValue(data, label) { return observedScalar(data, label); }
66
+
67
+ function assertBooleanObservation(data, label) {
68
+ const value = dataValue(data, label);
69
+ if (value !== "true" && value !== "false") {
70
+ throw providerError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} was not boolean`);
71
+ }
72
+ return value;
73
+ }
74
+
75
+ function isWithin(parent, child) {
76
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
77
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
78
+ }
79
+
80
+ function assertTempRootOutsideTarget(root, target) {
81
+ if (typeof target !== "string" || !path.isAbsolute(target)) return;
82
+ if (isWithin(target, root)) {
83
+ throw providerError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser tempRoot must be outside the verification target");
84
+ }
85
+ }
86
+
87
+ function resultWithAssertions(assertions, finalUrl, navigations, diagnostics = [], snapshots = [], artifacts = []) {
88
+ return { assertions, finalUrl, navigations, diagnostics, snapshots, artifacts };
89
+ }
90
+
91
+ function blockedAssertions(request, message) {
92
+ return request.assertions.map((assertion) => assertionResult(assertion, "BLOCKED", undefined, message));
93
+ }
94
+
95
+ async function createCwd(tempRoot, fsImpl, target) {
96
+ const root = tempRoot ?? os.tmpdir();
97
+ if (typeof root !== "string" || !path.isAbsolute(root)) throw providerError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser tempRoot must be absolute");
98
+ assertTempRootOutsideTarget(root, target);
99
+ const make = fsImpl?.mkdtemp ?? mkdtemp;
100
+ const cwd = await make(path.join(root, "forgeloop-agent-browser-"));
101
+ try {
102
+ if (typeof cwd !== "string" || !path.isAbsolute(cwd)) {
103
+ throw providerError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser temporary cwd is invalid");
104
+ }
105
+ assertTempRootOutsideTarget(cwd, target);
106
+ } catch (error) {
107
+ try { await fsImpl?.rm?.(cwd, { recursive: true, force: true }); } catch { /* preserve the boundary failure */ }
108
+ throw error;
109
+ }
110
+ return cwd;
111
+ }
112
+
113
+ function screenshotRef(verificationId, digest) { return `agent-browser/${verificationId}/${digest}.png`; }
114
+
115
+ async function executeWait(step, { invoke, resolve, readUrl, deadline, clock }) {
116
+ const waitDeadline = Math.min(deadline, clock.now() + Math.max(100, Math.min(remaining(deadline), 5000)));
117
+ let satisfied = false;
118
+ while (!satisfied) {
119
+ if (clock.now() >= waitDeadline) throw providerError(E_BROWSER_VERIFICATION_TIMEOUT, "Agent Browser wait condition timed out");
120
+ if (step.condition === "VISIBLE" || step.condition === "HIDDEN") {
121
+ const visible = assertBooleanObservation(
122
+ await invoke(visibleCommand, { selector: await resolve(step.locator) }),
123
+ "Agent Browser visibility",
124
+ ) === "true";
125
+ satisfied = step.condition === "VISIBLE" ? visible : !visible;
126
+ } else if (step.condition === "TEXT_CONTAINS") {
127
+ const text = dataValue(await invoke(textCommand, { selector: await resolve(step.locator) }), "Agent Browser wait text");
128
+ satisfied = text.includes(step.expected ?? "");
129
+ } else {
130
+ const url = await readUrl();
131
+ satisfied = step.condition === "URL_IS" ? url === step.expected : url.startsWith(step.expected);
132
+ }
133
+ if (!satisfied) await new Promise((resolvePromise) => setTimeout(resolvePromise, Math.min(50, Math.max(1, waitDeadline - clock.now()))));
134
+ }
135
+ }
136
+
137
+ async function executeStep(step, helpers, navigations) {
138
+ const { invoke, resolve, readUrl } = helpers;
139
+ if (step.kind === "NAVIGATE") {
140
+ await invoke(openCommand, { url: step.url });
141
+ const observed = await readUrl();
142
+ navigations.push({ url: step.url, kind: "NAVIGATE" });
143
+ if (observed !== step.url) navigations.push({ url: observed, kind: "REDIRECT" });
144
+ } else if (step.kind === "CLICK") {
145
+ await invoke(clickCommand, { selector: await resolve(step.locator) });
146
+ await readUrl();
147
+ } else if (step.kind === "FILL") {
148
+ await invoke(fillCommand, { selector: await resolve(step.locator), text: step.text });
149
+ await readUrl();
150
+ } else if (step.kind === "PRESS") {
151
+ await invoke(focusCommand, { selector: await resolve(step.locator) });
152
+ await invoke(pressCommand, { key: step.key });
153
+ await readUrl();
154
+ } else if (step.kind === "WAIT_FOR") {
155
+ await executeWait(step, helpers);
156
+ }
157
+ }
158
+
159
+ async function executeSteps(request, helpers, navigations) {
160
+ for (const step of request.steps) await executeStep(step, helpers, navigations);
161
+ }
162
+
163
+ async function readAssertionValue(assertion, { invoke, resolve, readUrl }) {
164
+ if (assertion.kind === "VISIBLE" || assertion.kind === "HIDDEN") {
165
+ return assertBooleanObservation(await invoke(visibleCommand, { selector: await resolve(assertion.locator) }), "Agent Browser visibility");
166
+ }
167
+ if (assertion.kind === "TEXT_CONTAINS" || assertion.kind === "TEXT_EQUALS") {
168
+ return dataValue(await invoke(textCommand, { selector: await resolve(assertion.locator) }), "Agent Browser text");
169
+ }
170
+ if (assertion.kind === "VALUE_EQUALS") return dataValue(await invoke(valueCommand, { selector: await resolve(assertion.locator) }), "Agent Browser value");
171
+ if (assertion.kind === "ATTRIBUTE_EQUALS") {
172
+ return dataValue(await invoke(attributeCommand, { selector: await resolve(assertion.locator), attribute: assertion.attribute }), "Agent Browser attribute");
173
+ }
174
+ if (assertion.kind === "URL_IS" || assertion.kind === "URL_PREFIX") return readUrl();
175
+ return dataValue(await invoke(titleCommand), "Agent Browser title");
176
+ }
177
+
178
+ async function executeAssertions(request, helpers) {
179
+ const results = [];
180
+ for (const assertion of request.assertions) {
181
+ try {
182
+ const actual = await readAssertionValue(assertion, helpers);
183
+ results.push(assertionResult(assertion, matchesAssertion(assertion.kind, actual, assertion.expected) ? "PASS" : "FAIL", actual));
184
+ } catch (error) {
185
+ if (error.code === E_BROWSER_VERIFICATION_TIMEOUT) throw error;
186
+ results.push(assertionResult(assertion, "BLOCKED", undefined, "Browser observation was unavailable"));
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+
192
+ async function captureScreenshot(request, { cwd, run, globals, fileSystem }, results, artifacts) {
193
+ const capture = request.capture?.screenshot ?? "NEVER";
194
+ const shouldCapture = capture === "ALWAYS" || (capture === "ON_FAILURE" && results.some((item) => item.status !== "PASS"));
195
+ if (!shouldCapture) return;
196
+ const outputPath = path.join(cwd, "verification.png");
197
+ await run(screenshotCommand({ ...globals, outputPath }));
198
+ const content = await fileSystem.readFile(outputPath);
199
+ const digest = crypto.createHash("sha256").update(content).digest("hex");
200
+ artifacts.push({ kind: "SCREENSHOT", mimeType: "image/png", byteLength: content.byteLength, sha256: digest, ref: screenshotRef(request.verificationId, digest) });
201
+ }
202
+
203
+ export function createAgentBrowserVerificationProvider({
204
+ executablePath,
205
+ expectedVersion: expected,
206
+ browserExecutablePath,
207
+ spawnImpl,
208
+ env,
209
+ fsImpl = null,
210
+ tempRoot,
211
+ clock = Date,
212
+ } = {}) {
213
+ const fileSystem = fsImpl ?? {
214
+ lstatSync: fs.lstatSync.bind(fs),
215
+ statSync: fs.statSync.bind(fs),
216
+ mkdtemp,
217
+ readFile,
218
+ rm,
219
+ };
220
+ const executable = assertAbsoluteRegularFile(executablePath, "Agent Browser executable", fileSystem);
221
+ const expectedVersionValue = expectedVersion(expected);
222
+ if (browserExecutablePath !== undefined) assertAbsoluteRegularFile(browserExecutablePath, "Browser executable", fileSystem);
223
+
224
+ return Object.freeze({
225
+ id: "agent-browser",
226
+ async verify(request) {
227
+ const deadline = clock.now() + request.timeoutMs;
228
+ const sessionId = `forgeloop-${crypto.randomUUID()}`;
229
+ let cwd;
230
+ let closed = false;
231
+ const run = async (args, limit = AGENT_BROWSER_PROCESS_LIMITS.maxStdoutBytes) => {
232
+ const output = await runAgentBrowserCommand(executable, args, {
233
+ cwd,
234
+ timeoutMs: remaining(deadline),
235
+ signal: request.signal,
236
+ spawnImpl,
237
+ env: {
238
+ ...env,
239
+ ...(browserExecutablePath ? { AGENT_BROWSER_EXECUTABLE_PATH: browserExecutablePath } : {}),
240
+ AGENT_BROWSER_CONTENT_BOUNDARIES: "1",
241
+ AGENT_BROWSER_MAX_OUTPUT: String(AGENT_BROWSER_PROCESS_LIMITS.maxOutputChars),
242
+ },
243
+ maxStdoutBytes: limit,
244
+ fsImpl: fileSystem,
245
+ trustedEnvKeys: browserExecutablePath ? ["AGENT_BROWSER_EXECUTABLE_PATH"] : [],
246
+ });
247
+ return parseAgentBrowserJson(output.stdout);
248
+ };
249
+ const globals = { sessionId, allowedOrigins: request.allowedOrigins, maxOutputChars: AGENT_BROWSER_PROCESS_LIMITS.maxOutputChars };
250
+ const invoke = (builder, input = {}) => run(builder({ ...globals, ...input }));
251
+ const snapshots = [];
252
+ const navigations = [];
253
+ const diagnostics = [];
254
+ const artifacts = [];
255
+ let currentUrl = request.startUrl;
256
+ try {
257
+ cwd = await createCwd(tempRoot, fileSystem, request.target);
258
+ const versionOutput = await runAgentBrowserCommand(executable, versionCommand(), {
259
+ cwd, timeoutMs: remaining(deadline), signal: request.signal, spawnImpl, env,
260
+ maxStdoutBytes: AGENT_BROWSER_PROCESS_LIMITS.maxVersionBytes,
261
+ fsImpl: fileSystem,
262
+ });
263
+ const version = parseVersion(versionOutput.stdout);
264
+ if (expectedVersionValue !== undefined && version !== expectedVersionValue) {
265
+ throw providerError(E_BROWSER_VERIFICATION_VERSION_UNSUPPORTED, "Agent Browser version does not match expectedVersion");
266
+ }
267
+
268
+ const readUrl = async () => {
269
+ const value = dataValue(await invoke(urlCommand), "Agent Browser URL");
270
+ currentUrl = verifyOrigin(value, request.allowedOrigins);
271
+ return currentUrl;
272
+ };
273
+ const readSnapshot = async () => {
274
+ const value = await invoke(snapshotCommand);
275
+ snapshots.push({ kind: "ACCESSIBILITY", text: typeof value === "string" ? value : JSON.stringify(value) });
276
+ return value;
277
+ };
278
+ const resolve = async (locator) => resolveSnapshotLocator(locator, await readSnapshot());
279
+ const helpers = { invoke, resolve, readUrl, deadline, clock };
280
+ await invoke(openCommand, { url: request.startUrl });
281
+ const initialUrl = await readUrl();
282
+ navigations.push({ url: request.startUrl, kind: "NAVIGATE" });
283
+ if (initialUrl !== request.startUrl) navigations.push({ url: initialUrl, kind: "REDIRECT" });
284
+ await executeSteps(request, helpers, navigations);
285
+ const results = await executeAssertions(request, helpers);
286
+ await captureScreenshot(request, { cwd, run, globals, fileSystem }, results, artifacts);
287
+ currentUrl = await readUrl();
288
+ return resultWithAssertions(results, currentUrl, navigations, diagnostics, snapshots, artifacts);
289
+ } catch (error) {
290
+ if ([E_BROWSER_VERIFICATION_TIMEOUT, E_BROWSER_VERIFICATION_CANCELLED, E_BROWSER_VERIFICATION_ORIGIN_DENIED,
291
+ E_BROWSER_VERIFICATION_PROVIDER_INVALID, E_BROWSER_VERIFICATION_VERSION_UNSUPPORTED].includes(error.code)) throw error;
292
+ if (error.code === E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE) throw error;
293
+ if (error.code === E_BROWSER_VERIFICATION_EXECUTION_FAILED) throw error;
294
+ return { assertions: blockedAssertions(request, "Browser verification could not complete"), finalUrl: currentUrl, navigations, diagnostics, snapshots, artifacts };
295
+ } finally {
296
+ if (cwd && !closed) {
297
+ closed = true;
298
+ try {
299
+ await runAgentBrowserCommand(executable, closeCommand({ ...globals }), {
300
+ cwd,
301
+ timeoutMs: Math.max(1, Math.min(Math.max(1, deadline - Date.now()), 1000)),
302
+ signal: undefined,
303
+ spawnImpl,
304
+ env,
305
+ fsImpl: fileSystem,
306
+ });
307
+ } catch { /* cleanup is best effort and never leaks process data */ }
308
+ try { await fileSystem.rm(cwd, { recursive: true, force: true }); } catch { /* temp cleanup is bounded and non-authoritative */ }
309
+ }
310
+ }
311
+ },
312
+ });
313
+ }