@cassiomc1/forgeloop 1.12.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 (249) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/AGENT_COMPATIBILITY.md +8 -0
  4. package/CLAUDE.md +1 -1
  5. package/CONTRIBUTING.md +90 -0
  6. package/DOCS_INDEX.md +46 -12
  7. package/ENG/c-development-eng.md +112 -0
  8. package/ENG/cpp-development-eng.md +109 -0
  9. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  10. package/ENG/go-development-eng.md +103 -0
  11. package/ENG/java-development-eng.md +125 -0
  12. package/ENG/nodejs-backend-development-eng.md +605 -0
  13. package/ENG/php-development-eng.md +104 -0
  14. package/ENG/rust-development-eng.md +422 -0
  15. package/ENG/sec-code-eng.md +7 -7
  16. package/ENG/sql-development-eng.md +108 -0
  17. package/ENG/swift-development-eng.md +111 -0
  18. package/ENG/typescript-development-eng.md +108 -0
  19. package/EXECUTION_STATE.md +12 -0
  20. package/GUIDE_ROUTER.md +418 -9
  21. package/LOOP_ENGINEERING.md +28 -2
  22. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  23. package/PROTOCOL_INTEGRATION.md +55 -2
  24. package/QUALITY_SCORECARD.md +1 -0
  25. package/README.md +78 -52
  26. package/TERMINOLOGY.md +2 -0
  27. package/THIRD_PARTY_NOTICES.md +19 -7
  28. package/THREAT_MODEL.md +140 -1
  29. package/completions/_forgeloop +22 -4
  30. package/completions/forgeloop.bash +40 -4
  31. package/completions/forgeloop.fish +130 -1
  32. package/docs/ADVISORY_CONTEXT.md +25 -0
  33. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  34. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  35. package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
  36. package/docs/AGENT_SKILL.md +66 -0
  37. package/docs/ARTIFACT_REFERENCE.md +123 -0
  38. package/docs/AUDIT_UX.md +46 -0
  39. package/docs/BROWSER_VERIFICATION.md +136 -0
  40. package/docs/CLI_REFERENCE.md +392 -10
  41. package/docs/CODE_ATTESTATION.md +2 -2
  42. package/docs/DOCUMENTATION_GUIDE.md +34 -12
  43. package/docs/GETTING_STARTED.md +59 -0
  44. package/docs/JEV_BENCHMARKS.md +31 -0
  45. package/docs/MODEL_ROUTING.md +37 -0
  46. package/docs/OPENSRC_ADAPTER.md +241 -0
  47. package/docs/PACKAGE_CONTENTS.md +60 -19
  48. package/docs/PROVIDERS.md +126 -0
  49. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  50. package/docs/RECIPES.md +32 -0
  51. package/docs/RELEASE_CHECKLIST.md +66 -5
  52. package/docs/SECURITY_REVIEW.md +71 -0
  53. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  54. package/docs/TEST_INTELLIGENCE.md +29 -0
  55. package/docs/TEST_PRUNING.md +14 -0
  56. package/docs/TROUBLESHOOTING.md +298 -3
  57. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  58. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  59. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  60. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  61. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  62. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  63. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  64. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  65. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  66. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  67. package/docs/diagrams/README.md +13 -9
  68. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  69. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  70. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  71. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  72. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  73. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  74. package/docs/documentation-manifest.json +1397 -0
  75. package/docs/protocol-requirements.json +101 -0
  76. package/package.json +46 -4
  77. package/schemas/config.schema.json +14 -0
  78. package/schemas/context-plan.schema.json +18 -0
  79. package/schemas/routing-input.schema.json +1 -1
  80. package/schemas/semantic-decision.schema.json +46 -0
  81. package/schemas/test-utility.schema.json +44 -0
  82. package/scripts/CI_VALIDATORS.md +84 -11
  83. package/scripts/benchmark-jev.mjs +5 -0
  84. package/scripts/benchmark-test-intelligence.mjs +4 -0
  85. package/scripts/generate-agent-protocol-summary.mjs +40 -1
  86. package/scripts/generate-forgeloop-skill.mjs +133 -0
  87. package/scripts/jev-smoke.mjs +19 -0
  88. package/skills/forgeloop/README.md +9 -0
  89. package/skills/forgeloop/SKILL.md +77 -0
  90. package/skills/forgeloop/references/lifecycle.md +9 -0
  91. package/skills/forgeloop/references/recovery.md +7 -0
  92. package/skills/forgeloop/references/verification.md +7 -0
  93. package/src/adapters/agent-browser/assertions.js +47 -0
  94. package/src/adapters/agent-browser/commands.js +54 -0
  95. package/src/adapters/agent-browser/index.js +3 -0
  96. package/src/adapters/agent-browser/locator.js +40 -0
  97. package/src/adapters/agent-browser/process.js +215 -0
  98. package/src/adapters/agent-browser/provider.js +313 -0
  99. package/src/adapters/emulated-services/constants.js +24 -0
  100. package/src/adapters/emulated-services/index.js +7 -0
  101. package/src/adapters/emulated-services/process.js +162 -0
  102. package/src/adapters/emulated-services/provider.js +282 -0
  103. package/src/adapters/opensrc/normalize.js +90 -0
  104. package/src/adapters/opensrc/process.js +248 -0
  105. package/src/adapters/opensrc/provider.js +338 -0
  106. package/src/adapters/opensrc/search.js +264 -0
  107. package/src/adapters/typesafe/client.js +28 -0
  108. package/src/adapters/typesafe/engine.js +63 -0
  109. package/src/adapters/typesafe/normalize.js +41 -0
  110. package/src/cli.js +108 -0
  111. package/src/commands/checkpoint-revalidate.js +176 -0
  112. package/src/commands/context-plan.js +38 -0
  113. package/src/commands/contract-create.js +264 -0
  114. package/src/commands/contract-revise.js +236 -0
  115. package/src/commands/decision-show.js +14 -0
  116. package/src/commands/decision-status.js +22 -0
  117. package/src/commands/discover.js +41 -0
  118. package/src/commands/doctor.js +15 -0
  119. package/src/commands/gate-record.js +205 -0
  120. package/src/commands/gate-revalidate.js +137 -0
  121. package/src/commands/model-route.js +32 -0
  122. package/src/commands/next.js +19 -7
  123. package/src/commands/route.js +146 -18
  124. package/src/commands/semantic-plan.js +17 -0
  125. package/src/commands/task-abandon.js +224 -0
  126. package/src/commands/task-create.js +84 -25
  127. package/src/commands/task-list.js +22 -2
  128. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  129. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  130. package/src/commands/test-inventory.js +5 -0
  131. package/src/commands/test-prune-plan.js +5 -0
  132. package/src/commands/test-prune-probe.js +5 -0
  133. package/src/commands/test-utility.js +5 -0
  134. package/src/commands/validate-protocol.js +10 -1
  135. package/src/config/guides.json +44 -0
  136. package/src/core/artifact-registry.js +24 -0
  137. package/src/core/audit-ux.js +514 -0
  138. package/src/core/browser-verification/constants.js +149 -0
  139. package/src/core/browser-verification/normalize.js +254 -0
  140. package/src/core/browser-verification/provider.js +519 -0
  141. package/src/core/browser-verification/service.js +115 -0
  142. package/src/core/build-script.js +151 -0
  143. package/src/core/c-cpp-project.js +143 -0
  144. package/src/core/checkpoint-revalidation.js +319 -0
  145. package/src/core/cli-command-definitions.js +249 -1
  146. package/src/core/command-executors.js +115 -3
  147. package/src/core/command-input.js +212 -102
  148. package/src/core/completion-artifacts.js +14 -5
  149. package/src/core/completion.js +4 -6
  150. package/src/core/config.js +3 -0
  151. package/src/core/context-compiler/budget.js +9 -0
  152. package/src/core/context-compiler/candidates.js +39 -0
  153. package/src/core/context-compiler/compiler.js +63 -0
  154. package/src/core/context-compiler/fingerprint.js +11 -0
  155. package/src/core/context-compiler/policy.js +13 -0
  156. package/src/core/context-compiler/result.js +23 -0
  157. package/src/core/contract-bootstrap-recovery.js +655 -0
  158. package/src/core/contract-presets.js +82 -0
  159. package/src/core/contract-revision.js +210 -0
  160. package/src/core/decision/artifact.js +69 -0
  161. package/src/core/decision/benchmarks.js +103 -0
  162. package/src/core/decision/cache.js +27 -0
  163. package/src/core/decision/constants.js +58 -0
  164. package/src/core/decision/cutover.js +34 -0
  165. package/src/core/decision/engine.js +22 -0
  166. package/src/core/decision/errors.js +68 -0
  167. package/src/core/decision/events.js +101 -0
  168. package/src/core/decision/freshness.js +19 -0
  169. package/src/core/decision/normalizers/index.js +115 -0
  170. package/src/core/decision/policy.js +18 -0
  171. package/src/core/decision/projection.js +16 -0
  172. package/src/core/decision/question-registry.js +201 -0
  173. package/src/core/decision/request.js +26 -0
  174. package/src/core/decision/resolver.js +130 -0
  175. package/src/core/decision/result.js +58 -0
  176. package/src/core/decision/service.js +156 -0
  177. package/src/core/decision/state-builder.js +65 -0
  178. package/src/core/decision/task-bindings.js +30 -0
  179. package/src/core/decision/test-provider.js +32 -0
  180. package/src/core/decision/thresholds.js +15 -0
  181. package/src/core/error-codes.js +281 -3
  182. package/src/core/events.js +226 -57
  183. package/src/core/evidence-readiness.js +9 -0
  184. package/src/core/execution-prerequisites.js +14 -0
  185. package/src/core/execution-profile.js +63 -38
  186. package/src/core/filesystem.js +1 -10
  187. package/src/core/gate-provenance.js +124 -0
  188. package/src/core/go-project.js +206 -0
  189. package/src/core/integration-invocation-policy.js +27 -4
  190. package/src/core/integration-resources.js +86 -61
  191. package/src/core/java-project.js +403 -0
  192. package/src/core/model-router/constants.js +10 -0
  193. package/src/core/model-router/policy.js +103 -0
  194. package/src/core/model-router/router.js +37 -0
  195. package/src/core/multi-language-project.js +117 -0
  196. package/src/core/next-action-model.js +58 -0
  197. package/src/core/next-action-phases.js +130 -42
  198. package/src/core/next-action-refresh.js +43 -9
  199. package/src/core/next-action-review-phase.js +7 -2
  200. package/src/core/next-action.js +35 -7
  201. package/src/core/next-explanation.js +63 -0
  202. package/src/core/phase.js +128 -10
  203. package/src/core/php-project.js +85 -0
  204. package/src/core/preflight-consistency.js +23 -9
  205. package/src/core/preflight-loaders.js +37 -5
  206. package/src/core/project-detection.js +1760 -52
  207. package/src/core/protocol-info.js +65 -0
  208. package/src/core/protocol.js +20 -0
  209. package/src/core/reconcile-closure.js +132 -53
  210. package/src/core/recovery-history.js +1 -0
  211. package/src/core/resumability.js +154 -44
  212. package/src/core/route-artifact.js +15 -1
  213. package/src/core/router.js +223 -4
  214. package/src/core/runtime-context.js +118 -61
  215. package/src/core/rust-project.js +400 -0
  216. package/src/core/schema-validation.js +3 -0
  217. package/src/core/security-review/constants.js +64 -0
  218. package/src/core/security-review/normalize.js +245 -0
  219. package/src/core/security-review/provider.js +204 -0
  220. package/src/core/security-review/service.js +134 -0
  221. package/src/core/semantic-planning/constants.js +19 -0
  222. package/src/core/semantic-planning/projection.js +94 -0
  223. package/src/core/semantic-planning/service.js +15 -0
  224. package/src/core/sources.js +37 -0
  225. package/src/core/sql-project.js +141 -0
  226. package/src/core/swift-project.js +200 -0
  227. package/src/core/task-claim-state.js +201 -1
  228. package/src/core/task-conflict-inspection.js +31 -5
  229. package/src/core/task-paths.js +13 -0
  230. package/src/core/task-recovery.js +1 -0
  231. package/src/core/templates.js +3 -0
  232. package/src/core/test-intelligence/benchmarks.js +68 -0
  233. package/src/core/test-intelligence/inventory.js +73 -0
  234. package/src/core/test-intelligence/prune.js +90 -0
  235. package/src/core/test-intelligence/semantic-state.js +15 -0
  236. package/src/core/test-intelligence/service.js +40 -0
  237. package/src/core/test-intelligence/utility.js +50 -0
  238. package/src/core/trace.js +11 -7
  239. package/src/core/transaction.js +1 -0
  240. package/src/core/typescript-project.js +349 -0
  241. package/src/core/xml-structure.js +123 -0
  242. package/src/integration.d.ts +492 -0
  243. package/src/integration.js +54 -0
  244. package/src/providers/README.md +47 -0
  245. package/src/providers/capabilities.js +46 -0
  246. package/src/providers/errors.js +15 -0
  247. package/src/providers/index.js +29 -0
  248. package/src/providers/json-snapshot.js +105 -0
  249. package/src/providers/registry.js +152 -0
@@ -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
+ }
@@ -0,0 +1,24 @@
1
+ export const EMULATED_SERVICES_ID = "emulated-services";
2
+ export const EMULATED_SERVICES_SUPPORTED_VERSION = "0.11.2";
3
+
4
+ export const EMULATED_SERVICES_PROCESS_LIMITS = Object.freeze({
5
+ maxStdoutBytes: 64 * 1024,
6
+ maxStderrBytes: 64 * 1024,
7
+ maxServices: 32,
8
+ maxTimeoutMs: 120_000,
9
+ terminationGraceMs: 250,
10
+ readinessPollMs: 50,
11
+ });
12
+
13
+ export const EMULATED_SERVICES_ERROR_CODES = Object.freeze({
14
+ CONFIG_INVALID: "E_EMULATED_SERVICES_CONFIG_INVALID",
15
+ UNAVAILABLE: "E_EMULATED_SERVICES_UNAVAILABLE",
16
+ VERSION_UNSUPPORTED: "E_EMULATED_SERVICES_VERSION_UNSUPPORTED",
17
+ OUTPUT_INVALID: "E_EMULATED_SERVICES_OUTPUT_INVALID",
18
+ OUTPUT_LIMIT: "E_EMULATED_SERVICES_OUTPUT_LIMIT",
19
+ TIMEOUT: "E_EMULATED_SERVICES_TIMEOUT",
20
+ CANCELLED: "E_EMULATED_SERVICES_CANCELLED",
21
+ EXECUTION_FAILED: "E_EMULATED_SERVICES_EXECUTION_FAILED",
22
+ ENDPOINT_INVALID: "E_EMULATED_SERVICES_ENDPOINT_INVALID",
23
+ CLEANUP_FAILED: "E_EMULATED_SERVICES_CLEANUP_FAILED",
24
+ });
@@ -0,0 +1,7 @@
1
+ export { createEmulatedServicesProvider, normalizeEndpoint, parseVersion } from "./provider.js";
2
+ export {
3
+ EMULATED_SERVICES_ERROR_CODES,
4
+ EMULATED_SERVICES_ID,
5
+ EMULATED_SERVICES_PROCESS_LIMITS,
6
+ EMULATED_SERVICES_SUPPORTED_VERSION,
7
+ } from "./constants.js";
@@ -0,0 +1,162 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+
3
+ import {
4
+ EMULATED_SERVICES_ERROR_CODES,
5
+ EMULATED_SERVICES_PROCESS_LIMITS,
6
+ } from "./constants.js";
7
+
8
+ function processError(code, message, cause) {
9
+ const error = new Error(message, cause ? { cause } : undefined);
10
+ error.name = "EmulatedServicesProcessError";
11
+ error.code = code;
12
+ return error;
13
+ }
14
+
15
+ function byteLength(value) {
16
+ return Buffer.isBuffer(value) ? value.byteLength : Buffer.byteLength(String(value));
17
+ }
18
+
19
+ function terminateChild(child) {
20
+ if (!child || typeof child.kill !== "function") return;
21
+ try {
22
+ child.kill("SIGTERM");
23
+ } catch {
24
+ // The process may have exited between a boundary failure and termination.
25
+ }
26
+ const timer = setTimeout(() => {
27
+ try {
28
+ child.kill("SIGKILL");
29
+ } catch {
30
+ // Preserve the original boundary error.
31
+ }
32
+ }, EMULATED_SERVICES_PROCESS_LIMITS.terminationGraceMs);
33
+ timer.unref?.();
34
+ }
35
+
36
+ function waitForClose(child, alreadyClosed = () => false) {
37
+ if (alreadyClosed()) return Promise.resolve();
38
+ return new Promise((resolve) => {
39
+ let settled = false;
40
+ const finish = () => {
41
+ if (settled) return;
42
+ settled = true;
43
+ resolve();
44
+ };
45
+ child.once?.("close", finish);
46
+ const timer = setTimeout(finish, EMULATED_SERVICES_PROCESS_LIMITS.terminationGraceMs + 1000);
47
+ timer.unref?.();
48
+ });
49
+ }
50
+
51
+ function assertArgv(args) {
52
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || /\p{Cc}/u.test(arg))) {
53
+ throw processError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services arguments must be portable strings");
54
+ }
55
+ }
56
+
57
+ export function spawnManagedEmulatedServicesProcess(executablePath, args, options = {}) {
58
+ assertArgv(args);
59
+ const spawnImpl = options.spawnImpl ?? nodeSpawn;
60
+ if (typeof spawnImpl !== "function") {
61
+ throw processError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services spawn implementation must be a function");
62
+ }
63
+
64
+ let child;
65
+ try {
66
+ child = spawnImpl(executablePath, [...args], {
67
+ cwd: options.cwd,
68
+ shell: false,
69
+ stdio: ["ignore", "pipe", "pipe"],
70
+ env: options.env,
71
+ });
72
+ } catch (error) {
73
+ throw processError(EMULATED_SERVICES_ERROR_CODES.UNAVAILABLE, "Unable to start the Emulated Services executable", error);
74
+ }
75
+
76
+ if (!child || !child.stdout || !child.stderr || typeof child.on !== "function") {
77
+ terminateChild(child);
78
+ throw processError(EMULATED_SERVICES_ERROR_CODES.UNAVAILABLE, "Emulated Services process did not expose piped output");
79
+ }
80
+
81
+ const maxStdoutBytes = options.maxStdoutBytes ?? EMULATED_SERVICES_PROCESS_LIMITS.maxStdoutBytes;
82
+ const maxStderrBytes = options.maxStderrBytes ?? EMULATED_SERVICES_PROCESS_LIMITS.maxStderrBytes;
83
+ let stdoutBytes = 0;
84
+ let stderrBytes = 0;
85
+ let overflowError = null;
86
+ const stdoutChunks = [];
87
+
88
+ child.stdout.on("data", (chunk) => {
89
+ stdoutBytes += byteLength(chunk);
90
+ if (stdoutBytes > maxStdoutBytes && !overflowError) {
91
+ overflowError = processError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_LIMIT, "Emulated Services stdout exceeded its safety limit");
92
+ terminateChild(child);
93
+ return;
94
+ }
95
+ if (!overflowError) stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
96
+ });
97
+ child.stderr.on("data", (chunk) => {
98
+ stderrBytes += byteLength(chunk);
99
+ if (stderrBytes > maxStderrBytes && !overflowError) {
100
+ overflowError = processError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_LIMIT, "Emulated Services stderr exceeded its safety limit");
101
+ terminateChild(child);
102
+ }
103
+ });
104
+
105
+ let processErrorValue = null;
106
+ child.on("error", (error) => {
107
+ processErrorValue = processError(
108
+ error?.code === "ENOENT" ? EMULATED_SERVICES_ERROR_CODES.UNAVAILABLE : EMULATED_SERVICES_ERROR_CODES.EXECUTION_FAILED,
109
+ "Emulated Services process failed",
110
+ error,
111
+ );
112
+ });
113
+
114
+ let hasClosed = false;
115
+ const closed = new Promise((resolve) => {
116
+ child.on("close", (code, signal) => {
117
+ hasClosed = true;
118
+ resolve({ code, signal });
119
+ });
120
+ });
121
+
122
+ return Object.freeze({
123
+ child,
124
+ closed,
125
+ output: () => ({
126
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
127
+ stdoutBytes,
128
+ stderrBytes,
129
+ }),
130
+ failure: () => overflowError ?? processErrorValue,
131
+ stop: () => terminateChild(child),
132
+ waitForClose: () => waitForClose(child, () => hasClosed),
133
+ });
134
+ }
135
+
136
+ export async function runBoundedEmulatedServicesCommand(executablePath, args, options = {}) {
137
+ const managed = spawnManagedEmulatedServicesProcess(executablePath, args, options);
138
+ const timeoutMs = options.timeoutMs ?? 10_000;
139
+ const timeout = new Promise((_, reject) => {
140
+ const timer = setTimeout(() => {
141
+ managed.stop();
142
+ reject(processError(EMULATED_SERVICES_ERROR_CODES.TIMEOUT, "Emulated Services command exceeded its timeout"));
143
+ }, timeoutMs);
144
+ timer.unref?.();
145
+ });
146
+ try {
147
+ const result = await Promise.race([managed.closed, timeout]);
148
+ const failure = managed.failure();
149
+ if (failure) throw failure;
150
+ if (result.code !== 0) {
151
+ throw processError(EMULATED_SERVICES_ERROR_CODES.EXECUTION_FAILED, "Emulated Services command exited unsuccessfully");
152
+ }
153
+ return managed.output();
154
+ } finally {
155
+ managed.stop();
156
+ await managed.waitForClose();
157
+ }
158
+ }
159
+
160
+ export function emulatedServicesProcessError(code, message, cause) {
161
+ return processError(code, message, cause);
162
+ }