rea-agents 2.4.0 → 2.5.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 (186) hide show
  1. package/README.md +80 -18
  2. package/bridge/ghidra/ReaGhidraBridge.java +48 -1
  3. package/bridge/hopper_bridge.py +259 -24
  4. package/dist/application/AnalysisClientCleanup.js +14 -0
  5. package/dist/application/ApplicationWorkflowEvidenceResolver.js +31 -13
  6. package/dist/application/BinarySession.js +15 -4
  7. package/dist/application/BinarySessionStatus.js +31 -1
  8. package/dist/application/BrowserEvidence.js +14 -0
  9. package/dist/application/BrowserScenarioCapturePort.js +1 -0
  10. package/dist/application/BrowserScenarioCaptureService.js +53 -0
  11. package/dist/application/BrowserScenarioEvidence.js +40 -0
  12. package/dist/application/CallPathTracing.js +160 -0
  13. package/dist/application/CapabilityInventory.js +68 -12
  14. package/dist/application/CompositeProvider.js +10 -1
  15. package/dist/application/DirectAnalysis.js +7 -2
  16. package/dist/application/EnhancedLiteralTracing.js +118 -0
  17. package/dist/application/EnhancedTools.js +35 -121
  18. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
  19. package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
  20. package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
  21. package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
  22. package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
  23. package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
  24. package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
  25. package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
  26. package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
  27. package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
  28. package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
  29. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
  30. package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
  31. package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
  32. package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
  33. package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
  34. package/dist/application/ProcessReactiveEffects.js +6 -0
  35. package/dist/application/ProcessReactiveHarness.js +16 -1
  36. package/dist/application/ProjectPermissionStore.js +1 -0
  37. package/dist/application/ReconstructionObligationAuthority.js +72 -0
  38. package/dist/application/ReconstructionObligationCandidates.js +265 -0
  39. package/dist/application/ReconstructionObligationLedgerDiagnostics.js +47 -0
  40. package/dist/application/ReconstructionObligationLedgerEvaluation.js +263 -0
  41. package/dist/application/ReconstructionObligationLedgerProjection.js +86 -0
  42. package/dist/application/ReconstructionObligationLedgerService.js +72 -0
  43. package/dist/application/ReconstructionObligationOriginalCases.js +33 -0
  44. package/dist/application/ReconstructionObligationPolicies.js +132 -0
  45. package/dist/application/ReconstructionReadinessService.js +48 -0
  46. package/dist/artifacts/ArtifactProvider.js +87 -22
  47. package/dist/browser/BrowserScenarioSecrets.js +53 -0
  48. package/dist/browser/BrowserScenarioSessionPort.js +1 -0
  49. package/dist/browser/CdpBrowserProvider.js +2 -2
  50. package/dist/browser/CdpCaptureDocuments.js +57 -0
  51. package/dist/browser/CdpCaptureStorage.js +71 -29
  52. package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
  53. package/dist/browser/CdpEndpoint.js +8 -6
  54. package/dist/browser/CdpPageCapture.js +2 -0
  55. package/dist/browser/JavaScriptRuntimeScope.js +38 -0
  56. package/dist/browser/PlaywrightBrowserScenarioProvider.js +253 -0
  57. package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
  58. package/dist/browser/PlaywrightScenarioActions.js +62 -0
  59. package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
  60. package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
  61. package/dist/browser/PlaywrightScenarioEvents.js +301 -0
  62. package/dist/browser/PlaywrightScenarioSession.js +300 -0
  63. package/dist/browser/V8InspectorCaptureProjection.js +110 -0
  64. package/dist/browser/V8InspectorEndpoint.js +72 -0
  65. package/dist/browser/V8InspectorProvider.js +267 -0
  66. package/dist/catalogIdentity.js +8 -0
  67. package/dist/cli/artifactCommands.js +39 -0
  68. package/dist/cli/coreAnalysisCommands.js +37 -0
  69. package/dist/cli.js +4 -0
  70. package/dist/cliApplicationCommands.js +30 -2
  71. package/dist/cliBrowserAdvancedCommands.js +17 -5
  72. package/dist/cliBrowserCommands.js +1 -0
  73. package/dist/cliBrowserScenarioCommands.js +43 -0
  74. package/dist/cliCommandNames.js +8 -0
  75. package/dist/cliJavaScriptRuntimeCommands.js +164 -0
  76. package/dist/cliObservationOptions.js +4 -0
  77. package/dist/cliPolicyCommands.js +1 -0
  78. package/dist/config/environment.js +12 -0
  79. package/dist/config/parseConfig.js +88 -17
  80. package/dist/contracts/analysisSearchContract.js +14 -0
  81. package/dist/contracts/applicationToolContracts.js +86 -2
  82. package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
  83. package/dist/contracts/artifactToolContracts.js +22 -0
  84. package/dist/contracts/browserProviderToolContracts.js +7 -0
  85. package/dist/contracts/browserScenarioToolContracts.js +81 -0
  86. package/dist/contracts/browserToolContracts.js +88 -98
  87. package/dist/contracts/enhancedInputs.js +25 -6
  88. package/dist/contracts/functionInstructionContract.js +8 -0
  89. package/dist/contracts/hopperMemoryContracts.js +22 -0
  90. package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
  91. package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
  92. package/dist/contracts/reconstructionReadinessExample.js +436 -0
  93. package/dist/contracts/toolContractExamples.js +5 -0
  94. package/dist/contracts/toolContractTypes.js +1 -0
  95. package/dist/contracts/toolContracts.js +13 -21
  96. package/dist/contracts/toolEffects.js +21 -0
  97. package/dist/contracts/toolOutputSchemaGroups.js +70 -20
  98. package/dist/contracts/toolOutputSchemaPrimitives.js +37 -0
  99. package/dist/domain/analysisErrorProjection.js +8 -0
  100. package/dist/domain/artifactInspection.js +347 -0
  101. package/dist/domain/browserCaptureComparison.js +82 -0
  102. package/dist/domain/browserObservation.js +8 -1
  103. package/dist/domain/browserObservationErrors.js +1 -0
  104. package/dist/domain/browserObservationSchemas.js +26 -0
  105. package/dist/domain/browserScenario.js +165 -0
  106. package/dist/domain/browserScenarioCapture.js +57 -0
  107. package/dist/domain/browserScenarioCaptureValues.js +204 -0
  108. package/dist/domain/browserScenarioDiff.js +324 -0
  109. package/dist/domain/browserScenarioDiffValues.js +157 -0
  110. package/dist/domain/browserScenarioNormalization.js +45 -0
  111. package/dist/domain/browserScenarioValues.js +372 -0
  112. package/dist/domain/hopperValues.js +10 -0
  113. package/dist/domain/javascriptRuntimeObservation.js +170 -0
  114. package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
  115. package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
  116. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
  117. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
  118. package/dist/domain/javascriptSemanticAnalysis.js +5 -4
  119. package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
  120. package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
  121. package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
  122. package/dist/domain/javascriptSemanticDataEffects.js +284 -0
  123. package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
  124. package/dist/domain/javascriptSemanticFingerprints.js +174 -0
  125. package/dist/domain/javascriptSemanticIr.js +21 -1
  126. package/dist/domain/javascriptSemanticObjects.js +115 -0
  127. package/dist/domain/javascriptSemanticPromises.js +290 -0
  128. package/dist/domain/javascriptSemanticResources.js +136 -0
  129. package/dist/domain/javascriptStaticAnalysis.js +4 -3
  130. package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
  131. package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
  132. package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
  133. package/dist/domain/processCapture.js +8 -2
  134. package/dist/domain/processReactiveScenario.js +1 -0
  135. package/dist/domain/processReactiveTransition.js +9 -4
  136. package/dist/domain/providerCleanupError.js +11 -0
  137. package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
  138. package/dist/domain/reconstructionReadiness.js +121 -0
  139. package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
  140. package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
  141. package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
  142. package/dist/domain/reconstructionReadinessSchemas.js +240 -0
  143. package/dist/domain/sourceToBundleComparison.js +335 -0
  144. package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
  145. package/dist/domain/sourceToBundleSignals.js +202 -0
  146. package/dist/domain/webCaptureDiff.js +141 -27
  147. package/dist/domain/webScreenshot.js +1 -1
  148. package/dist/generatedPackageMetadata.js +1 -1
  149. package/dist/ghidra/GhidraDefaults.js +1 -1
  150. package/dist/ghidra/GhidraFunctionValues.js +13 -1
  151. package/dist/ghidra/GhidraProviderCapabilities.js +7 -0
  152. package/dist/hopper/HopperCleanup.js +158 -0
  153. package/dist/hopper/HopperClient.js +69 -99
  154. package/dist/hopper/HopperProvider.js +20 -1
  155. package/dist/hopper/HopperRequestQueue.js +187 -0
  156. package/dist/hopper/HopperResponseStream.js +46 -0
  157. package/dist/hopper/protocol.js +1 -0
  158. package/dist/main/transport.js +24 -24
  159. package/dist/main.js +6 -0
  160. package/dist/mcpDoctor.js +25 -2
  161. package/dist/process/ProcessCommandIdentity.js +19 -0
  162. package/dist/process/ProcessOwnership.js +11 -8
  163. package/dist/server/DynamicToolAvailability.js +70 -0
  164. package/dist/server/createServer.js +46 -1
  165. package/dist/server/lifecycleProgress.js +10 -0
  166. package/dist/server/permissionFailure.js +12 -0
  167. package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
  168. package/dist/server/registerApplicationTools/helpers.js +11 -7
  169. package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
  170. package/dist/server/registerApplicationTools/readiness.js +55 -0
  171. package/dist/server/registerApplicationTools.js +6 -0
  172. package/dist/server/registerArtifactTools.js +9 -1
  173. package/dist/server/registerBrowserScenarioTool.js +25 -0
  174. package/dist/server/registerBrowserTools.js +2 -2
  175. package/dist/server/registerCloseLifecycleTool.js +57 -0
  176. package/dist/server/registerEnhancedTools.js +16 -5
  177. package/dist/server/registerEvidenceResources.js +4 -0
  178. package/dist/server/registerEvidenceTools.js +9 -2
  179. package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
  180. package/dist/server/registerReconstructionObligationLedgerResource.js +39 -0
  181. package/dist/server/registerReconstructionReadinessResource.js +39 -0
  182. package/dist/server/registerSessionStatusTool.js +11 -8
  183. package/dist/server/registerSessionTools.js +11 -57
  184. package/dist/server/sessionAvailabilityPolicy.js +10 -8
  185. package/package.json +12 -6
  186. package/skills/reverse-engineer-anything/SKILL.md +2 -2
@@ -0,0 +1,42 @@
1
+ import { AnalysisCancelledError, AnalysisTimeoutError, } from "../domain/errors.js";
2
+ const OPERATION = "capture_browser_scenario";
3
+ export const withPlaywrightExecutionBoundary = (work, maximumTimeoutMs, signal) => {
4
+ if (maximumTimeoutMs <= 0)
5
+ return Promise.reject(new AnalysisTimeoutError(OPERATION, maximumTimeoutMs));
6
+ if (signal?.aborted === true)
7
+ return Promise.reject(new AnalysisCancelledError(OPERATION));
8
+ return new Promise((resolve, reject) => {
9
+ let settled = false;
10
+ let timer;
11
+ const cleanup = () => {
12
+ if (timer !== undefined)
13
+ clearTimeout(timer);
14
+ signal?.removeEventListener("abort", onAbort);
15
+ };
16
+ const succeed = (value) => {
17
+ if (settled)
18
+ return;
19
+ settled = true;
20
+ cleanup();
21
+ resolve(value);
22
+ };
23
+ const fail = (error) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ cleanup();
28
+ reject(error);
29
+ };
30
+ const onAbort = () => {
31
+ fail(new AnalysisCancelledError(OPERATION));
32
+ };
33
+ timer = setTimeout(() => fail(new AnalysisTimeoutError(OPERATION, maximumTimeoutMs)), maximumTimeoutMs);
34
+ timer.unref();
35
+ signal?.addEventListener("abort", onAbort, { once: true });
36
+ if (signal?.aborted === true) {
37
+ onAbort();
38
+ return;
39
+ }
40
+ void work().then(succeed, fail);
41
+ });
42
+ };
@@ -0,0 +1,62 @@
1
+ const resolveLocator = (page, locator) => {
2
+ switch (locator.kind) {
3
+ case "test_id":
4
+ return page.getByTestId(locator.value);
5
+ case "role":
6
+ return page.getByRole(locator.role, {
7
+ name: locator.name,
8
+ exact: locator.exact,
9
+ });
10
+ case "css":
11
+ return page.locator(locator.selector);
12
+ }
13
+ };
14
+ const resolveValue = (value, secrets) => secrets.value(value);
15
+ /** Execute one admitted declarative action with no caller-supplied code. */
16
+ export const performPlaywrightScenarioAction = async (input) => {
17
+ const { page, action, secrets, defaultTimeoutMs, maximumTimeoutMs } = input;
18
+ const timeout = "timeout_ms" in action ? action.timeout_ms : undefined;
19
+ const options = {
20
+ timeout: Math.min(timeout ?? defaultTimeoutMs, maximumTimeoutMs),
21
+ };
22
+ switch (action.action) {
23
+ case "goto":
24
+ await page.goto(secrets.url(action.destination), {
25
+ timeout: options.timeout,
26
+ waitUntil: action.wait_until,
27
+ });
28
+ return;
29
+ case "click":
30
+ await resolveLocator(page, action.locator).click({
31
+ ...options,
32
+ button: action.button,
33
+ clickCount: action.click_count,
34
+ });
35
+ return;
36
+ case "fill":
37
+ await resolveLocator(page, action.locator).fill(resolveValue(action.value, secrets), options);
38
+ return;
39
+ case "press":
40
+ await resolveLocator(page, action.locator).press(action.key, options);
41
+ return;
42
+ case "select_option":
43
+ await resolveLocator(page, action.locator).selectOption(resolveValue(action.value, secrets), options);
44
+ return;
45
+ case "check":
46
+ await resolveLocator(page, action.locator).check(options);
47
+ return;
48
+ case "uncheck":
49
+ await resolveLocator(page, action.locator).uncheck(options);
50
+ return;
51
+ case "wait_for":
52
+ await resolveLocator(page, action.locator).waitFor({
53
+ ...options,
54
+ state: action.state,
55
+ });
56
+ return;
57
+ case "wait_for_timeout":
58
+ if (action.duration_ms > maximumTimeoutMs)
59
+ throw new Error("Scenario duration limit reached");
60
+ await page.waitForTimeout(action.duration_ms);
61
+ }
62
+ };
@@ -0,0 +1,238 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
4
+ import { browserStepArtifactsSchema, } from "../domain/browserScenarioCapture.js";
5
+ const TEXT_ARTIFACT_MAX_BYTES = 1_048_576;
6
+ const historyValueSchema = z.strictObject({
7
+ length: z.number().int().min(0),
8
+ navigation_entries: z
9
+ .array(z.strictObject({
10
+ type: z.string(),
11
+ name: z.string(),
12
+ }))
13
+ .max(10_000),
14
+ });
15
+ const storageValueSchema = z.strictObject({
16
+ local_storage: z.array(z.tuple([z.string(), z.string()])).max(10_000),
17
+ session_storage: z.array(z.tuple([z.string(), z.string()])).max(10_000),
18
+ });
19
+ /** Shared retained-count budget for one scenario's screenshot artifacts. */
20
+ export class BrowserScenarioCaptureBudget {
21
+ maximumScreenshots;
22
+ maximumMetadataBytes;
23
+ screenshotCount = 0;
24
+ metadataBytes = 0;
25
+ constructor(maximumScreenshots, maximumMetadataBytes) {
26
+ this.maximumScreenshots = maximumScreenshots;
27
+ this.maximumMetadataBytes = maximumMetadataBytes;
28
+ }
29
+ claimScreenshot() {
30
+ if (this.screenshotCount >= this.maximumScreenshots)
31
+ return false;
32
+ this.screenshotCount += 1;
33
+ return true;
34
+ }
35
+ claimMetadata(bytes) {
36
+ if (this.metadataBytes + bytes > this.maximumMetadataBytes)
37
+ return false;
38
+ this.metadataBytes += bytes;
39
+ return true;
40
+ }
41
+ }
42
+ const notRequested = () => ({ state: "not_requested" });
43
+ const missing = (reason) => ({ state: "missing", reason });
44
+ const truncated = (observed, retained, reason) => ({
45
+ state: "truncated",
46
+ observed,
47
+ retained,
48
+ reason,
49
+ });
50
+ const textArtifact = (text, maximumBytes, budget) => {
51
+ const bytes = Buffer.from(text);
52
+ if (bytes.byteLength > maximumBytes)
53
+ return truncated(bytes.byteLength, 0, `artifact exceeds ${maximumBytes} bytes`);
54
+ if (!budget.claimMetadata(bytes.byteLength))
55
+ return truncated(bytes.byteLength, 0, "scenario metadata byte limit reached");
56
+ return {
57
+ state: "captured",
58
+ value: {
59
+ sha256: createHash("sha256").update(bytes).digest("hex"),
60
+ bytes: bytes.byteLength,
61
+ text,
62
+ },
63
+ };
64
+ };
65
+ const metadataArtifact = (value, budget) => {
66
+ const bytes = Buffer.byteLength(JSON.stringify(value));
67
+ return budget.claimMetadata(bytes)
68
+ ? { state: "captured", value }
69
+ : truncated(bytes, 0, "scenario metadata byte limit reached");
70
+ };
71
+ const captureScreenshot = async (page, scenario, budget) => {
72
+ if (!budget.claimScreenshot())
73
+ return truncated(1, 0, "scenario screenshot count limit reached");
74
+ try {
75
+ const bytes = await page.screenshot({
76
+ type: "png",
77
+ fullPage: false,
78
+ animations: "disabled",
79
+ caret: "hide",
80
+ });
81
+ if (bytes.byteLength > scenario.limits.max_screenshot_bytes)
82
+ return truncated(bytes.byteLength, 0, "screenshot byte limit reached");
83
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
84
+ return {
85
+ state: "captured",
86
+ value: {
87
+ uri: `rea://web-screenshot/sha256/${sha256}`,
88
+ sha256,
89
+ bytes: bytes.byteLength,
90
+ media_type: "image/png",
91
+ data_base64: bytes.toString("base64"),
92
+ },
93
+ };
94
+ }
95
+ catch {
96
+ return missing("screenshot capture failed");
97
+ }
98
+ };
99
+ const captureDom = async (input) => {
100
+ const { page, secrets, maximumBytes, maximumNodes, budget } = input;
101
+ try {
102
+ const nodes = await page.locator("*").count();
103
+ if (nodes > maximumNodes)
104
+ return truncated(nodes, 0, "DOM node limit reached");
105
+ return textArtifact(secrets.redact(await page.content()), Math.min(maximumBytes, TEXT_ARTIFACT_MAX_BYTES), budget);
106
+ }
107
+ catch {
108
+ return missing("DOM capture failed");
109
+ }
110
+ };
111
+ const captureAccessibility = async (input) => {
112
+ const { page, secrets, maximumBytes, maximumNodes, budget } = input;
113
+ try {
114
+ const text = await page.locator("html").ariaSnapshot();
115
+ const nodes = text.split("\n").filter((line) => line.trim() !== "").length;
116
+ if (nodes > maximumNodes)
117
+ return truncated(nodes, 0, "accessibility node limit reached");
118
+ return textArtifact(secrets.redact(text), Math.min(maximumBytes, TEXT_ARTIFACT_MAX_BYTES), budget);
119
+ }
120
+ catch {
121
+ return missing("accessibility capture failed");
122
+ }
123
+ };
124
+ const navigationType = (value) => {
125
+ switch (value) {
126
+ case "navigate":
127
+ case "reload":
128
+ case "back_forward":
129
+ case "prerender":
130
+ return value;
131
+ default:
132
+ return "unknown";
133
+ }
134
+ };
135
+ const captureHistory = async (page, budget) => {
136
+ try {
137
+ const raw = historyValueSchema.parse(await page.evaluate(`(() => ({
138
+ length: window.history.length,
139
+ navigation_entries: performance.getEntriesByType("navigation").map(
140
+ entry => ({
141
+ type: typeof entry.type === "string" ? entry.type : "unknown",
142
+ name: entry.name
143
+ })
144
+ )
145
+ }))()`));
146
+ if (raw.navigation_entries.length > 256)
147
+ return truncated(raw.navigation_entries.length, 0, "navigation history entry limit reached");
148
+ const value = {
149
+ length: raw.length,
150
+ current_url: sanitizeBrowserUrl(page.url()),
151
+ navigation_entries: raw.navigation_entries.map(({ type, name }) => ({
152
+ type: navigationType(type),
153
+ name: sanitizeBrowserUrl(name),
154
+ })),
155
+ };
156
+ return metadataArtifact(value, budget);
157
+ }
158
+ catch {
159
+ return missing("history capture failed");
160
+ }
161
+ };
162
+ const captureStorage = async (input) => {
163
+ const { context, page, scenario, secrets, budget } = input;
164
+ try {
165
+ const cookies = await context.cookies(scenario.allowed_origins);
166
+ const pageStorage = storageValueSchema.parse(await page.evaluate(`(() => ({
167
+ local_storage: Object.entries(window.localStorage),
168
+ session_storage: Object.entries(window.sessionStorage)
169
+ }))()`));
170
+ const observed = cookies.length +
171
+ pageStorage.local_storage.length +
172
+ pageStorage.session_storage.length;
173
+ if (observed > scenario.limits.max_storage_entries)
174
+ return truncated(observed, 0, "storage entry limit reached");
175
+ const value = {
176
+ cookies: cookies.map((cookie) => ({
177
+ name: cookie.name,
178
+ domain: cookie.domain,
179
+ path: cookie.path,
180
+ secure: cookie.secure,
181
+ http_only: cookie.httpOnly,
182
+ same_site: cookie.sameSite,
183
+ ...secrets.fingerprint(cookie.value),
184
+ })),
185
+ local_storage: pageStorage.local_storage.map(([name, value]) => ({
186
+ name,
187
+ ...secrets.fingerprint(value),
188
+ })),
189
+ session_storage: pageStorage.session_storage.map(([name, value]) => ({
190
+ name,
191
+ ...secrets.fingerprint(value),
192
+ })),
193
+ };
194
+ return metadataArtifact(value, budget);
195
+ }
196
+ catch {
197
+ return missing("storage capture failed");
198
+ }
199
+ };
200
+ const isAllowedPage = (page, allowedOrigins) => {
201
+ try {
202
+ return allowedOrigins.has(new URL(page.url()).origin);
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ };
208
+ /** Capture selected step artifacts without retaining data after scope loss. */
209
+ export const capturePlaywrightStepArtifacts = async (input) => {
210
+ const { context, page, scenario, secrets, requested, budget } = input;
211
+ const allowed = isAllowedPage(page, new Set(scenario.allowed_origins));
212
+ const denied = () => missing("current page is outside approved origins");
213
+ const state = (kind, capture) => !requested.has(kind)
214
+ ? Promise.resolve(notRequested())
215
+ : allowed || kind === "url"
216
+ ? capture()
217
+ : Promise.resolve(denied());
218
+ return browserStepArtifactsSchema.parse({
219
+ screenshot: await state("screenshot", () => captureScreenshot(page, scenario, budget)),
220
+ dom: await state("dom", () => captureDom({
221
+ page,
222
+ secrets,
223
+ maximumBytes: scenario.limits.max_total_metadata_bytes,
224
+ maximumNodes: scenario.limits.max_dom_nodes,
225
+ budget,
226
+ })),
227
+ accessibility: await state("accessibility", () => captureAccessibility({
228
+ page,
229
+ secrets,
230
+ maximumBytes: scenario.limits.max_total_metadata_bytes,
231
+ maximumNodes: scenario.limits.max_accessibility_nodes,
232
+ budget,
233
+ })),
234
+ url: await state("url", async () => metadataArtifact(sanitizeBrowserUrl(page.url()), budget)),
235
+ history: await state("history", () => captureHistory(page, budget)),
236
+ storage: await state("storage", () => captureStorage({ context, page, scenario, secrets, budget })),
237
+ });
238
+ };
@@ -0,0 +1,144 @@
1
+ import { chmod, mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { chromium, } from "playwright-core";
5
+ import { BrowserObservationError } from "../domain/errors.js";
6
+ const OPERATION = "capture_browser_scenario";
7
+ const allowedBrowserEnvironment = (environment) => {
8
+ const allowed = [
9
+ "DBUS_SESSION_BUS_ADDRESS",
10
+ "DISPLAY",
11
+ "HOME",
12
+ "LANG",
13
+ "LD_LIBRARY_PATH",
14
+ "PATH",
15
+ "SystemRoot",
16
+ "TEMP",
17
+ "TMP",
18
+ "TMPDIR",
19
+ "WAYLAND_DISPLAY",
20
+ "XAUTHORITY",
21
+ "XDG_RUNTIME_DIR",
22
+ ];
23
+ return Object.fromEntries(allowed
24
+ .map((name) => [name, environment[name]])
25
+ .filter((entry) => entry[1] !== undefined));
26
+ };
27
+ const originAllowed = (value, allowedOrigins) => {
28
+ try {
29
+ return allowedOrigins.has(new URL(value).origin);
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ };
35
+ const findConnectedPage = async (browser, targetId, allowedOrigins) => {
36
+ for (const context of browser.contexts())
37
+ for (const page of context.pages()) {
38
+ const session = await context.newCDPSession(page);
39
+ try {
40
+ const { targetInfo } = await session.send("Target.getTargetInfo");
41
+ if (targetInfo.targetId !== targetId)
42
+ continue;
43
+ if (page.url() !== "about:blank" &&
44
+ !originAllowed(page.url(), allowedOrigins))
45
+ throw new BrowserObservationError(OPERATION, "target_not_allowed");
46
+ return { context, page };
47
+ }
48
+ finally {
49
+ await session.detach();
50
+ }
51
+ }
52
+ throw new BrowserObservationError(OPERATION, "target_not_found");
53
+ };
54
+ const configureAttachedEnvironment = async (context, page, scenario) => {
55
+ await page.setViewportSize({
56
+ width: scenario.environment.viewport.width,
57
+ height: scenario.environment.viewport.height,
58
+ });
59
+ await page.emulateMedia({
60
+ colorScheme: scenario.environment.color_scheme,
61
+ reducedMotion: scenario.environment.reduced_motion,
62
+ });
63
+ const session = await context.newCDPSession(page);
64
+ try {
65
+ await session.send("Emulation.setDeviceMetricsOverride", {
66
+ width: scenario.environment.viewport.width,
67
+ height: scenario.environment.viewport.height,
68
+ deviceScaleFactor: scenario.environment.viewport.device_scale_factor,
69
+ mobile: false,
70
+ });
71
+ await session.send("Emulation.setLocaleOverride", {
72
+ locale: scenario.environment.locale,
73
+ });
74
+ await session.send("Emulation.setTimezoneOverride", {
75
+ timezoneId: scenario.environment.timezone,
76
+ });
77
+ }
78
+ finally {
79
+ await session.detach();
80
+ }
81
+ };
82
+ export const openPlaywrightScenarioBrowser = async (scenario, environment, maximumTimeoutMs) => {
83
+ if (scenario.browser.mode === "connect") {
84
+ const browser = await chromium.connectOverCDP(scenario.browser.cdp_endpoint, {
85
+ timeout: Math.min(scenario.limits.navigation_timeout_ms, maximumTimeoutMs),
86
+ });
87
+ try {
88
+ const target = await findConnectedPage(browser, scenario.browser.target_id, new Set(scenario.allowed_origins));
89
+ await configureAttachedEnvironment(target.context, target.page, scenario);
90
+ return { ...target, browser, profilePath: undefined };
91
+ }
92
+ catch (error) {
93
+ await browser.close();
94
+ throw error;
95
+ }
96
+ }
97
+ const profilePath = await mkdtemp(join(tmpdir(), "rea-browser-scenario-"));
98
+ await chmod(profilePath, 0o700);
99
+ try {
100
+ const context = await chromium.launchPersistentContext(profilePath, {
101
+ executablePath: scenario.browser.executable_path,
102
+ headless: true,
103
+ acceptDownloads: false,
104
+ viewport: {
105
+ width: scenario.environment.viewport.width,
106
+ height: scenario.environment.viewport.height,
107
+ },
108
+ deviceScaleFactor: scenario.environment.viewport.device_scale_factor,
109
+ locale: scenario.environment.locale,
110
+ timezoneId: scenario.environment.timezone,
111
+ colorScheme: scenario.environment.color_scheme,
112
+ reducedMotion: scenario.environment.reduced_motion,
113
+ serviceWorkers: "block",
114
+ env: allowedBrowserEnvironment(environment),
115
+ handleSIGHUP: false,
116
+ handleSIGINT: false,
117
+ handleSIGTERM: false,
118
+ timeout: maximumTimeoutMs,
119
+ });
120
+ const browser = context.browser();
121
+ if (browser === null) {
122
+ await context.close();
123
+ throw new BrowserObservationError(OPERATION, "protocol_error");
124
+ }
125
+ const page = context.pages()[0] ?? (await context.newPage());
126
+ return { context, page, browser, profilePath };
127
+ }
128
+ catch (error) {
129
+ await rm(profilePath, { recursive: true, force: true, maxRetries: 3 });
130
+ throw error;
131
+ }
132
+ };
133
+ export const closePlaywrightScenarioBrowser = async ({ context, browser, profilePath, }) => {
134
+ try {
135
+ if (profilePath === undefined)
136
+ await browser.close();
137
+ else
138
+ await context.close();
139
+ }
140
+ finally {
141
+ if (profilePath !== undefined)
142
+ await rm(profilePath, { recursive: true, force: true, maxRetries: 3 });
143
+ }
144
+ };