rea-agents 2.3.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.
- package/README.md +80 -18
- package/bridge/ghidra/ReaGhidraBridge.java +48 -1
- package/bridge/hopper_bridge.py +259 -24
- package/dist/application/AnalysisClientCleanup.js +14 -0
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +31 -13
- package/dist/application/ArtifactExtraction.js +5 -1
- package/dist/application/ArtifactGraphConstruction.js +3 -15
- package/dist/application/ArtifactInventory/classify.js +5 -1
- package/dist/application/ArtifactInventory/reader.js +2 -0
- package/dist/application/BinarySession.js +15 -4
- package/dist/application/BinarySessionStatus.js +32 -1
- package/dist/application/BrowserEvidence.js +14 -0
- package/dist/application/BrowserScenarioCapturePort.js +1 -0
- package/dist/application/BrowserScenarioCaptureService.js +53 -0
- package/dist/application/BrowserScenarioEvidence.js +40 -0
- package/dist/application/CallPathTracing.js +160 -0
- package/dist/application/CapabilityInventory.js +79 -15
- package/dist/application/CompositeProvider.js +10 -1
- package/dist/application/DirectAnalysis.js +7 -2
- package/dist/application/EnhancedLiteralTracing.js +118 -0
- package/dist/application/EnhancedTools.js +35 -121
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
- package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
- package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
- package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
- package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
- package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
- package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
- package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
- package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
- package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
- package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
- package/dist/application/ProcessCaptureEnvironment.js +26 -0
- package/dist/application/ProcessCaptureLifecycle.js +1 -0
- package/dist/application/ProcessCheckpoints.js +5 -0
- package/dist/application/ProcessHarness.js +45 -34
- package/dist/application/ProcessNormalization.js +6 -0
- package/dist/application/ProcessReactiveCoordinator.js +10 -3
- package/dist/application/ProcessReactiveEffects.js +15 -1
- package/dist/application/ProcessReactiveHarness.js +100 -0
- package/dist/application/ProcessReactiveObservations.js +101 -16
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/ReconstructionObligationAuthority.js +72 -0
- package/dist/application/ReconstructionObligationCandidates.js +265 -0
- package/dist/application/ReconstructionObligationLedgerDiagnostics.js +47 -0
- package/dist/application/ReconstructionObligationLedgerEvaluation.js +263 -0
- package/dist/application/ReconstructionObligationLedgerProjection.js +86 -0
- package/dist/application/ReconstructionObligationLedgerService.js +72 -0
- package/dist/application/ReconstructionObligationOriginalCases.js +33 -0
- package/dist/application/ReconstructionObligationPolicies.js +132 -0
- package/dist/application/ReconstructionReadinessService.js +48 -0
- package/dist/artifacts/ArtifactProvider.js +87 -22
- package/dist/browser/BrowserScenarioSecrets.js +53 -0
- package/dist/browser/BrowserScenarioSessionPort.js +1 -0
- package/dist/browser/CdpBrowserProvider.js +2 -2
- package/dist/browser/CdpCaptureDocuments.js +57 -0
- package/dist/browser/CdpCaptureStorage.js +71 -29
- package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
- package/dist/browser/CdpEndpoint.js +8 -6
- package/dist/browser/CdpPageCapture.js +2 -0
- package/dist/browser/JavaScriptRuntimeScope.js +38 -0
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +253 -0
- package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
- package/dist/browser/PlaywrightScenarioActions.js +62 -0
- package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
- package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
- package/dist/browser/PlaywrightScenarioEvents.js +301 -0
- package/dist/browser/PlaywrightScenarioSession.js +300 -0
- package/dist/browser/V8InspectorCaptureProjection.js +110 -0
- package/dist/browser/V8InspectorEndpoint.js +72 -0
- package/dist/browser/V8InspectorProvider.js +267 -0
- package/dist/catalogIdentity.js +8 -0
- package/dist/cli/artifactCommands.js +39 -0
- package/dist/cli/coreAnalysisCommands.js +37 -0
- package/dist/cli.js +4 -0
- package/dist/cliApplicationCommands.js +30 -2
- package/dist/cliBrowserAdvancedCommands.js +17 -5
- package/dist/cliBrowserCommands.js +1 -0
- package/dist/cliBrowserScenarioCommands.js +43 -0
- package/dist/cliCommandNames.js +8 -0
- package/dist/cliJavaScriptRuntimeCommands.js +164 -0
- package/dist/cliObservationOptions.js +4 -0
- package/dist/cliPolicyCommands.js +1 -0
- package/dist/config/environment.js +12 -0
- package/dist/config/parseConfig.js +88 -17
- package/dist/contracts/analysisSearchContract.js +14 -0
- package/dist/contracts/applicationToolContracts.js +86 -2
- package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
- package/dist/contracts/artifactToolContracts.js +22 -0
- package/dist/contracts/browserProviderToolContracts.js +7 -0
- package/dist/contracts/browserScenarioToolContracts.js +81 -0
- package/dist/contracts/browserToolContracts.js +88 -98
- package/dist/contracts/enhancedInputs.js +25 -6
- package/dist/contracts/functionInstructionContract.js +8 -0
- package/dist/contracts/hopperMemoryContracts.js +22 -0
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
- package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
- package/dist/contracts/reconstructionReadinessExample.js +436 -0
- package/dist/contracts/toolContractExamples.js +5 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +13 -21
- package/dist/contracts/toolEffects.js +21 -0
- package/dist/contracts/toolOutputSchemaGroups.js +70 -20
- package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
- package/dist/domain/analysisErrorProjection.js +8 -0
- package/dist/domain/analysisSnapshot.js +2 -0
- package/dist/domain/artifactGraph.js +2 -0
- package/dist/domain/artifactInspection.js +347 -0
- package/dist/domain/binaryTarget.js +3 -6
- package/dist/domain/browserCaptureComparison.js +82 -0
- package/dist/domain/browserObservation.js +8 -1
- package/dist/domain/browserObservationErrors.js +1 -0
- package/dist/domain/browserObservationSchemas.js +26 -0
- package/dist/domain/browserScenario.js +165 -0
- package/dist/domain/browserScenarioCapture.js +57 -0
- package/dist/domain/browserScenarioCaptureValues.js +204 -0
- package/dist/domain/browserScenarioDiff.js +324 -0
- package/dist/domain/browserScenarioDiffValues.js +157 -0
- package/dist/domain/browserScenarioNormalization.js +45 -0
- package/dist/domain/browserScenarioValues.js +372 -0
- package/dist/domain/evidence.js +2 -0
- package/dist/domain/hopperValues.js +10 -0
- package/dist/domain/javascriptRuntimeObservation.js +170 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
- package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
- package/dist/domain/javascriptSemanticAnalysis.js +5 -4
- package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
- package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
- package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
- package/dist/domain/javascriptSemanticDataEffects.js +284 -0
- package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
- package/dist/domain/javascriptSemanticFingerprints.js +174 -0
- package/dist/domain/javascriptSemanticIr.js +21 -1
- package/dist/domain/javascriptSemanticObjects.js +115 -0
- package/dist/domain/javascriptSemanticPromises.js +290 -0
- package/dist/domain/javascriptSemanticResources.js +136 -0
- package/dist/domain/javascriptStaticAnalysis.js +4 -3
- package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
- package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
- package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
- package/dist/domain/processCapture.js +71 -1
- package/dist/domain/processCaptureReactiveValidation.js +175 -0
- package/dist/domain/processCaptureValidation.js +2 -0
- package/dist/domain/processObservation.js +15 -6
- package/dist/domain/processReactiveRuntime.js +21 -10
- package/dist/domain/processReactiveScenario.js +1 -0
- package/dist/domain/processReactiveTransition.js +17 -10
- package/dist/domain/processScenario.js +21 -0
- package/dist/domain/providerCleanupError.js +11 -0
- package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
- package/dist/domain/reconstructionReadiness.js +121 -0
- package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
- package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
- package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
- package/dist/domain/reconstructionReadinessSchemas.js +240 -0
- package/dist/domain/sourceToBundleComparison.js +335 -0
- package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
- package/dist/domain/sourceToBundleSignals.js +202 -0
- package/dist/domain/webCaptureDiff.js +141 -27
- package/dist/domain/webScreenshot.js +1 -1
- package/dist/domain/zipPackageFormat.js +20 -0
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +13 -1
- package/dist/ghidra/GhidraProviderCapabilities.js +7 -0
- package/dist/hopper/HopperCleanup.js +158 -0
- package/dist/hopper/HopperClient.js +69 -99
- package/dist/hopper/HopperProvider.js +20 -1
- package/dist/hopper/HopperRequestQueue.js +187 -0
- package/dist/hopper/HopperResponseStream.js +46 -0
- package/dist/hopper/protocol.js +1 -0
- package/dist/main/transport.js +24 -24
- package/dist/main.js +6 -0
- package/dist/mcpDoctor.js +25 -2
- package/dist/native/NativeMacOSProvider.js +1 -0
- package/dist/process/ProcessCommandIdentity.js +19 -0
- package/dist/process/ProcessOwnership.js +11 -8
- package/dist/server/DynamicToolAvailability.js +70 -0
- package/dist/server/createServer.js +46 -1
- package/dist/server/lifecycleProgress.js +10 -0
- package/dist/server/permissionFailure.js +12 -0
- package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
- package/dist/server/registerApplicationTools/helpers.js +11 -7
- package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
- package/dist/server/registerApplicationTools/readiness.js +55 -0
- package/dist/server/registerApplicationTools.js +6 -0
- package/dist/server/registerArtifactTools.js +9 -1
- package/dist/server/registerBrowserScenarioTool.js +25 -0
- package/dist/server/registerBrowserTools.js +2 -2
- package/dist/server/registerCloseLifecycleTool.js +57 -0
- package/dist/server/registerEnhancedTools.js +16 -5
- package/dist/server/registerEvidenceResources.js +4 -0
- package/dist/server/registerEvidenceTools.js +9 -2
- package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
- package/dist/server/registerReconstructionObligationLedgerResource.js +39 -0
- package/dist/server/registerReconstructionReadinessResource.js +39 -0
- package/dist/server/registerSessionStatusTool.js +11 -8
- package/dist/server/registerSessionTools.js +11 -57
- package/dist/server/sessionAvailabilityPolicy.js +10 -8
- package/package.json +12 -6
- package/skills/reverse-engineer-anything/SKILL.md +4 -3
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { CdpConnection } from "./CdpConnection.js";
|
|
4
|
+
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
|
|
5
|
+
import { recordValue, recordsValue, stringValue } from "./CdpCaptureValues.js";
|
|
6
|
+
const MAX_CACHE_BODY_BYTES = 64 * 1_024;
|
|
7
|
+
/** Capture stable hashes of approved storage content without returning values. */
|
|
8
|
+
export const captureStorageFingerprints = async ({ context, origin, local, session, indexedDbNames, caches, limitations, }) => {
|
|
9
|
+
const state = {
|
|
10
|
+
items: [],
|
|
11
|
+
maximum: context.input.limits.max_storage_keys,
|
|
12
|
+
complete: local.complete && session.complete,
|
|
13
|
+
truncated: false,
|
|
14
|
+
};
|
|
15
|
+
const run = { context, limitations, state };
|
|
16
|
+
addStorageItems(state, "local_storage", local.items);
|
|
17
|
+
addStorageItems(state, "session_storage", session.items);
|
|
18
|
+
await addCookies(run, origin);
|
|
19
|
+
await addIndexedDb(run, origin, indexedDbNames);
|
|
20
|
+
await addCaches(run, caches);
|
|
21
|
+
state.items.sort((left, right) => compareCodePoints(`${left.scope}:${left.identity_sha256}`, `${right.scope}:${right.identity_sha256}`));
|
|
22
|
+
return {
|
|
23
|
+
items: state.items,
|
|
24
|
+
complete: state.complete &&
|
|
25
|
+
!state.truncated &&
|
|
26
|
+
state.items.every(({ complete }) => complete),
|
|
27
|
+
truncated: state.truncated,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const addStorageItems = (state, scope, items) => {
|
|
31
|
+
for (const item of items)
|
|
32
|
+
addFingerprint(state, {
|
|
33
|
+
scope,
|
|
34
|
+
identity: item.key,
|
|
35
|
+
value: item.value,
|
|
36
|
+
complete: true,
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const addCookies = async (run, origin) => {
|
|
40
|
+
const raw = await optionalCdpCommand(run.context, "Network.getCookies", { urls: [origin] }, run.limitations);
|
|
41
|
+
if (raw === undefined) {
|
|
42
|
+
run.state.complete = false;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const cookieValues = recordValue(raw)?.cookies;
|
|
46
|
+
if (!Array.isArray(cookieValues)) {
|
|
47
|
+
run.state.complete = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const cookies = recordsValue(cookieValues);
|
|
51
|
+
if (cookies.length !== cookieValues.length)
|
|
52
|
+
run.state.complete = false;
|
|
53
|
+
for (const cookie of cookies) {
|
|
54
|
+
const name = stringValue(cookie.name);
|
|
55
|
+
const value = stringValue(cookie.value);
|
|
56
|
+
const domain = stringValue(cookie.domain);
|
|
57
|
+
const path = stringValue(cookie.path);
|
|
58
|
+
if (name === undefined ||
|
|
59
|
+
value === undefined ||
|
|
60
|
+
domain === undefined ||
|
|
61
|
+
path === undefined) {
|
|
62
|
+
run.state.complete = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
addFingerprint(run.state, {
|
|
66
|
+
scope: "cookie",
|
|
67
|
+
identity: {
|
|
68
|
+
domain,
|
|
69
|
+
path,
|
|
70
|
+
name,
|
|
71
|
+
partition_key: cookie.partitionKey ?? null,
|
|
72
|
+
},
|
|
73
|
+
value,
|
|
74
|
+
complete: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const addIndexedDb = async (run, origin, names) => {
|
|
79
|
+
for (const databaseName of names) {
|
|
80
|
+
if (run.state.items.length >= run.state.maximum) {
|
|
81
|
+
run.state.truncated = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const raw = await optionalCdpCommand(run.context, "IndexedDB.requestDatabase", { securityOrigin: origin, databaseName }, run.limitations);
|
|
85
|
+
const database = recordValue(recordValue(raw)?.databaseWithObjectStores);
|
|
86
|
+
if (raw === undefined || database === undefined) {
|
|
87
|
+
run.state.complete = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (stringValue(database.name) !== databaseName) {
|
|
91
|
+
run.state.complete = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(database.objectStores)) {
|
|
95
|
+
run.state.complete = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const stores = recordsValue(database.objectStores);
|
|
99
|
+
if (stores.length !== database.objectStores.length)
|
|
100
|
+
run.state.complete = false;
|
|
101
|
+
addFingerprint(run.state, {
|
|
102
|
+
scope: "indexed_db_schema",
|
|
103
|
+
identity: databaseName,
|
|
104
|
+
value: stores.map(indexedDbStoreSchema),
|
|
105
|
+
complete: indexedDbSchemaComplete(stores),
|
|
106
|
+
});
|
|
107
|
+
for (const store of stores) {
|
|
108
|
+
const objectStoreName = stringValue(store.name);
|
|
109
|
+
if (objectStoreName === undefined) {
|
|
110
|
+
run.state.complete = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
await addIndexedDbRecords(run, {
|
|
114
|
+
origin,
|
|
115
|
+
databaseName,
|
|
116
|
+
objectStoreName,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const indexedDbStoreSchema = (store) => ({
|
|
122
|
+
name: stringValue(store.name) ?? null,
|
|
123
|
+
key_path: stableValue(store.keyPath),
|
|
124
|
+
auto_increment: store.autoIncrement === true,
|
|
125
|
+
indexes: recordsValue(store.indexes).map((index) => ({
|
|
126
|
+
name: stringValue(index.name) ?? null,
|
|
127
|
+
key_path: stableValue(index.keyPath),
|
|
128
|
+
unique: index.unique === true,
|
|
129
|
+
multi_entry: index.multiEntry === true,
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
const indexedDbSchemaComplete = (stores) => stores.every((store) => stringValue(store.name) !== undefined &&
|
|
133
|
+
stableValueComplete(store.keyPath) &&
|
|
134
|
+
typeof store.autoIncrement === "boolean" &&
|
|
135
|
+
Array.isArray(store.indexes) &&
|
|
136
|
+
recordsValue(store.indexes).length === store.indexes.length &&
|
|
137
|
+
recordsValue(store.indexes).every((index) => stringValue(index.name) !== undefined &&
|
|
138
|
+
stableValueComplete(index.keyPath) &&
|
|
139
|
+
typeof index.unique === "boolean" &&
|
|
140
|
+
typeof index.multiEntry === "boolean"));
|
|
141
|
+
const addIndexedDbRecords = async (run, input) => {
|
|
142
|
+
const remaining = run.state.maximum - run.state.items.length;
|
|
143
|
+
if (remaining <= 0) {
|
|
144
|
+
run.state.truncated = true;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const raw = await optionalCdpCommand(run.context, "IndexedDB.requestData", {
|
|
148
|
+
securityOrigin: input.origin,
|
|
149
|
+
databaseName: input.databaseName,
|
|
150
|
+
objectStoreName: input.objectStoreName,
|
|
151
|
+
indexName: "",
|
|
152
|
+
skipCount: 0,
|
|
153
|
+
pageSize: remaining,
|
|
154
|
+
}, run.limitations);
|
|
155
|
+
const result = recordValue(raw);
|
|
156
|
+
if (raw === undefined || result === undefined) {
|
|
157
|
+
run.state.complete = false;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(result.objectStoreDataEntries) ||
|
|
161
|
+
typeof result.hasMore !== "boolean") {
|
|
162
|
+
run.state.complete = false;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const entries = recordsValue(result.objectStoreDataEntries);
|
|
166
|
+
if (entries.length !== result.objectStoreDataEntries.length)
|
|
167
|
+
run.state.complete = false;
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const key = stableValue(entry.key);
|
|
170
|
+
const primaryKey = stableValue(entry.primaryKey);
|
|
171
|
+
const value = stableValue(entry.value);
|
|
172
|
+
addFingerprint(run.state, {
|
|
173
|
+
scope: "indexed_db_record",
|
|
174
|
+
identity: {
|
|
175
|
+
database: input.databaseName,
|
|
176
|
+
store: input.objectStoreName,
|
|
177
|
+
key,
|
|
178
|
+
primaryKey,
|
|
179
|
+
},
|
|
180
|
+
value,
|
|
181
|
+
complete: remoteObjectComplete(entry.value),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (result.hasMore === true || entries.length > remaining)
|
|
185
|
+
run.state.truncated = true;
|
|
186
|
+
};
|
|
187
|
+
const addCaches = async (run, caches) => {
|
|
188
|
+
for (const cache of caches) {
|
|
189
|
+
const remaining = run.state.maximum - run.state.items.length;
|
|
190
|
+
if (remaining <= 0) {
|
|
191
|
+
run.state.truncated = true;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const raw = await optionalCdpCommand(run.context, "CacheStorage.requestEntries", { cacheId: cache.id, skipCount: 0, pageSize: remaining }, run.limitations);
|
|
195
|
+
const result = recordValue(raw);
|
|
196
|
+
if (raw === undefined || result === undefined) {
|
|
197
|
+
run.state.complete = false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(result.cacheDataEntries) ||
|
|
201
|
+
typeof result.returnCount !== "number") {
|
|
202
|
+
run.state.complete = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const entries = recordsValue(result.cacheDataEntries);
|
|
206
|
+
if (entries.length !== result.cacheDataEntries.length)
|
|
207
|
+
run.state.complete = false;
|
|
208
|
+
for (const entry of entries)
|
|
209
|
+
await addCacheEntry(run, cache, entry);
|
|
210
|
+
if (result.returnCount > entries.length)
|
|
211
|
+
run.state.truncated = true;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const addCacheEntry = async (run, cache, entry) => {
|
|
215
|
+
const requestURL = stringValue(entry.requestURL);
|
|
216
|
+
const requestMethod = stringValue(entry.requestMethod);
|
|
217
|
+
if (requestURL === undefined || requestMethod === undefined) {
|
|
218
|
+
run.state.complete = false;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!Array.isArray(entry.requestHeaders)) {
|
|
222
|
+
run.state.complete = false;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const headerRecords = recordsValue(entry.requestHeaders);
|
|
226
|
+
const requestHeaders = headerRecords.flatMap((header) => {
|
|
227
|
+
const name = stringValue(header.name);
|
|
228
|
+
const value = stringValue(header.value);
|
|
229
|
+
return name === undefined || value === undefined ? [] : [{ name, value }];
|
|
230
|
+
});
|
|
231
|
+
if (headerRecords.length !== entry.requestHeaders.length ||
|
|
232
|
+
requestHeaders.length !== headerRecords.length) {
|
|
233
|
+
run.state.complete = false;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const raw = await optionalCdpCommand(run.context, "CacheStorage.requestCachedResponse", { cacheId: cache.id, requestURL, requestHeaders }, run.limitations);
|
|
237
|
+
const body = stringValue(recordValue(recordValue(raw)?.response)?.body);
|
|
238
|
+
const decoded = body === undefined ? null : decodeBase64(body);
|
|
239
|
+
const complete = decoded !== null && decoded.byteLength <= MAX_CACHE_BODY_BYTES;
|
|
240
|
+
if (!complete)
|
|
241
|
+
run.state.complete = false;
|
|
242
|
+
addFingerprint(run.state, {
|
|
243
|
+
scope: "cache_entry",
|
|
244
|
+
identity: { cache: cache.name, requestURL, requestMethod, requestHeaders },
|
|
245
|
+
value: decoded ?? undefined,
|
|
246
|
+
complete,
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
const addFingerprint = (state, input) => {
|
|
250
|
+
if (state.items.length >= state.maximum) {
|
|
251
|
+
state.truncated = true;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
state.items.push({
|
|
255
|
+
scope: input.scope,
|
|
256
|
+
identity_sha256: digest(input.identity),
|
|
257
|
+
value_sha256: input.value === undefined ? null : digest(input.value),
|
|
258
|
+
complete: input.complete &&
|
|
259
|
+
stableValueComplete(input.identity) &&
|
|
260
|
+
stableValueComplete(input.value),
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
const digest = (value) => createHash("sha256").update(encodedValue(value)).digest("hex");
|
|
264
|
+
const encodedValue = (value) => {
|
|
265
|
+
if (value instanceof Uint8Array)
|
|
266
|
+
return value;
|
|
267
|
+
const encoded = canonicalize(stableValue(value));
|
|
268
|
+
if (encoded === undefined)
|
|
269
|
+
throw new TypeError("Expected canonical JSON");
|
|
270
|
+
return encoded;
|
|
271
|
+
};
|
|
272
|
+
const stableValue = (value, depth = 0) => {
|
|
273
|
+
if (value === null ||
|
|
274
|
+
typeof value === "string" ||
|
|
275
|
+
typeof value === "number" ||
|
|
276
|
+
typeof value === "boolean")
|
|
277
|
+
return value;
|
|
278
|
+
if (depth >= 8)
|
|
279
|
+
return "<depth-limit>";
|
|
280
|
+
if (Array.isArray(value))
|
|
281
|
+
return value.slice(0, 64).map((item) => stableValue(item, depth + 1));
|
|
282
|
+
const record = recordValue(value);
|
|
283
|
+
if (record === undefined)
|
|
284
|
+
return String(value);
|
|
285
|
+
return Object.fromEntries(Object.entries(record)
|
|
286
|
+
.filter(([key]) => key !== "objectId" && key !== "customPreview")
|
|
287
|
+
.sort(([left], [right]) => compareCodePoints(left, right))
|
|
288
|
+
.slice(0, 64)
|
|
289
|
+
.map(([key, item]) => [key, stableValue(item, depth + 1)]));
|
|
290
|
+
};
|
|
291
|
+
const stableValueComplete = (value, depth = 0) => {
|
|
292
|
+
if (value instanceof Uint8Array)
|
|
293
|
+
return true;
|
|
294
|
+
if (value === null ||
|
|
295
|
+
value === undefined ||
|
|
296
|
+
typeof value === "string" ||
|
|
297
|
+
typeof value === "number" ||
|
|
298
|
+
typeof value === "boolean")
|
|
299
|
+
return true;
|
|
300
|
+
if (depth >= 8)
|
|
301
|
+
return false;
|
|
302
|
+
if (Array.isArray(value))
|
|
303
|
+
return (value.length <= 64 &&
|
|
304
|
+
value.every((item) => stableValueComplete(item, depth + 1)));
|
|
305
|
+
const record = recordValue(value);
|
|
306
|
+
if (record === undefined)
|
|
307
|
+
return false;
|
|
308
|
+
const entries = Object.entries(record).filter(([key]) => key !== "objectId" && key !== "customPreview");
|
|
309
|
+
return (entries.length <= 64 &&
|
|
310
|
+
entries.every(([, item]) => stableValueComplete(item, depth + 1)));
|
|
311
|
+
};
|
|
312
|
+
const remoteObjectComplete = (value) => {
|
|
313
|
+
const record = recordValue(value);
|
|
314
|
+
if (record === undefined)
|
|
315
|
+
return true;
|
|
316
|
+
return (record.objectId === undefined &&
|
|
317
|
+
record.preview === undefined &&
|
|
318
|
+
record.customPreview === undefined &&
|
|
319
|
+
(record.type !== "object" || record.value !== undefined) &&
|
|
320
|
+
stableValueComplete(record));
|
|
321
|
+
};
|
|
322
|
+
const decodeBase64 = (value) => {
|
|
323
|
+
if (value.length % 4 !== 0 ||
|
|
324
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value))
|
|
325
|
+
return null;
|
|
326
|
+
return Buffer.from(value, "base64");
|
|
327
|
+
};
|
|
328
|
+
const compareCodePoints = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -24,10 +24,10 @@ const endpointTargetSchema = z.object({
|
|
|
24
24
|
const endpointTargetsSchema = z.array(endpointTargetSchema).max(1_000);
|
|
25
25
|
/** Read bounded CDP discovery endpoints without following redirects. */
|
|
26
26
|
export const discoverCdpEndpoint = async (endpoint, operation, signal) => {
|
|
27
|
-
const versionInput = await
|
|
28
|
-
const targetsInput = await
|
|
29
|
-
const version =
|
|
30
|
-
const targets =
|
|
27
|
+
const versionInput = await readBoundedCdpJson(new URL("/json/version", endpoint), MAX_VERSION_BYTES, operation, signal);
|
|
28
|
+
const targetsInput = await readBoundedCdpJson(new URL("/json/list", endpoint), MAX_TARGET_LIST_BYTES, operation, signal);
|
|
29
|
+
const version = parseCdpEndpointValue(endpointVersionSchema, versionInput, operation);
|
|
30
|
+
const targets = parseCdpEndpointValue(endpointTargetsSchema, targetsInput, operation);
|
|
31
31
|
return {
|
|
32
32
|
webSocket: safeCdpWebSocketEndpoint(endpoint, version.webSocketDebuggerUrl, operation),
|
|
33
33
|
version: {
|
|
@@ -110,7 +110,8 @@ const availableCdpTargetWebSocket = (discovery, target) => {
|
|
|
110
110
|
? discovery.webSocket
|
|
111
111
|
: undefined;
|
|
112
112
|
};
|
|
113
|
-
|
|
113
|
+
/** Parse one CDP discovery value through its exact boundary schema. */
|
|
114
|
+
export const parseCdpEndpointValue = (schema, input, operation) => {
|
|
114
115
|
const parsed = schema.safeParse(input);
|
|
115
116
|
if (!parsed.success)
|
|
116
117
|
throw new BrowserObservationError(operation, "invalid_endpoint_response", {
|
|
@@ -173,7 +174,8 @@ const targetWebSocket = (endpoint, target, operation) => {
|
|
|
173
174
|
return {};
|
|
174
175
|
}
|
|
175
176
|
};
|
|
176
|
-
|
|
177
|
+
/** Read one bounded loopback CDP discovery document without redirects. */
|
|
178
|
+
export const readBoundedCdpJson = async (url, maximumBytes, operation, signal) => await new Promise((resolve, reject) => {
|
|
177
179
|
if (signal?.aborted === true) {
|
|
178
180
|
reject(new AnalysisCancelledError(operation));
|
|
179
181
|
return;
|
|
@@ -226,6 +226,8 @@ const accessibilityForFrames = async (state, frames) => {
|
|
|
226
226
|
events.completeness.exclude("accessibility", "not_approved", capture.textCapture.excluded_fields);
|
|
227
227
|
if (capture.textCapture.status === "truncated")
|
|
228
228
|
events.completeness.truncate("accessibility");
|
|
229
|
+
if (capture.treeIncomplete)
|
|
230
|
+
events.completeness.truncate("accessibility");
|
|
229
231
|
return capture;
|
|
230
232
|
};
|
|
231
233
|
const publicResource = ({ rawUrl: _rawUrl, ...resource }) => resource;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { browserOriginSchema, sanitizeBrowserUrl, } from "../domain/browserObservation.js";
|
|
2
|
+
import { authorizedElectronFile, canonicalElectronRoots, } from "./ElectronFileScope.js";
|
|
3
|
+
/** Canonicalize exact filesystem scopes before Inspector attachment. */
|
|
4
|
+
export const canonicalRuntimeRoots = canonicalElectronRoots;
|
|
5
|
+
/** Authorize one protocol URL without retaining an out-of-scope value. */
|
|
6
|
+
export const authorizeRuntimeLocation = async (value, roots, origins) => {
|
|
7
|
+
if (value.startsWith("node:") && value.length <= 4_096)
|
|
8
|
+
return {
|
|
9
|
+
allowed: true,
|
|
10
|
+
location: { kind: "builtin", specifier: value },
|
|
11
|
+
};
|
|
12
|
+
if (value.startsWith("file:")) {
|
|
13
|
+
const filePath = await authorizedElectronFile(value, roots);
|
|
14
|
+
return filePath === undefined
|
|
15
|
+
? { allowed: false, reason: "outside_file_roots" }
|
|
16
|
+
: { allowed: true, location: { kind: "file", file_path: filePath } };
|
|
17
|
+
}
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { allowed: false, reason: "unsupported_location" };
|
|
24
|
+
}
|
|
25
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
26
|
+
return { allowed: false, reason: "unsupported_location" };
|
|
27
|
+
const parsedOrigin = browserOriginSchema.safeParse(url.origin);
|
|
28
|
+
if (!parsedOrigin.success || !origins.includes(parsedOrigin.data))
|
|
29
|
+
return { allowed: false, reason: "outside_origins" };
|
|
30
|
+
return {
|
|
31
|
+
allowed: true,
|
|
32
|
+
location: {
|
|
33
|
+
kind: "url",
|
|
34
|
+
origin: parsedOrigin.data,
|
|
35
|
+
sanitized_url: sanitizeBrowserUrl(value).url,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
|
2
|
+
import { browserScenarioCaptureSchema, browserScenarioCompletenessSchema, browserScenarioStepSchema, browserStepArtifactsSchema, } from "../domain/browserScenarioCapture.js";
|
|
3
|
+
import { AnalysisError, BrowserObservationError, ProviderAdapterError, } from "../domain/errors.js";
|
|
4
|
+
import { err, ok } from "../domain/result.js";
|
|
5
|
+
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
|
|
6
|
+
import { PlaywrightScenarioSessionFactory } from "./PlaywrightScenarioSession.js";
|
|
7
|
+
const OPERATION = "capture_browser_scenario";
|
|
8
|
+
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY = Object.freeze({
|
|
9
|
+
id: "rea-playwright-browser-scenario",
|
|
10
|
+
name: "REA Playwright browser scenario capture provider",
|
|
11
|
+
version: "1",
|
|
12
|
+
});
|
|
13
|
+
const requestedForStep = (scenario, stepIndex) => new Set([
|
|
14
|
+
...scenario.capture.after_each_step,
|
|
15
|
+
...(stepIndex === scenario.actions.length ? scenario.capture.at_end : []),
|
|
16
|
+
]);
|
|
17
|
+
const unavailableArtifacts = (requested, reason) => {
|
|
18
|
+
const state = (kind) => requested.has(kind)
|
|
19
|
+
? { state: "missing", reason }
|
|
20
|
+
: { state: "not_requested" };
|
|
21
|
+
return browserStepArtifactsSchema.parse({
|
|
22
|
+
screenshot: state("screenshot"),
|
|
23
|
+
dom: state("dom"),
|
|
24
|
+
accessibility: state("accessibility"),
|
|
25
|
+
url: state("url"),
|
|
26
|
+
history: state("history"),
|
|
27
|
+
storage: state("storage"),
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const stepCompleteness = (artifacts, status) => {
|
|
31
|
+
const missing = Object.entries(artifacts)
|
|
32
|
+
.filter(([, state]) => state.state === "missing")
|
|
33
|
+
.map(([section]) => section);
|
|
34
|
+
const truncated = Object.entries(artifacts)
|
|
35
|
+
.filter(([, state]) => state.state === "truncated")
|
|
36
|
+
.map(([section]) => section);
|
|
37
|
+
if (status !== "completed")
|
|
38
|
+
missing.push("action");
|
|
39
|
+
const condition = truncated.length > 0
|
|
40
|
+
? "truncated"
|
|
41
|
+
: missing.length > 0
|
|
42
|
+
? "incomplete"
|
|
43
|
+
: "complete";
|
|
44
|
+
return browserScenarioCompletenessSchema.parse({
|
|
45
|
+
status: condition,
|
|
46
|
+
equality_eligible: condition === "complete",
|
|
47
|
+
missing_sections: [...new Set(missing)].sort(),
|
|
48
|
+
truncated_sections: [...new Set(truncated)].sort(),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const createStep = (input) => browserScenarioStepSchema.parse({
|
|
52
|
+
step_index: input.stepIndex,
|
|
53
|
+
step_id: input.stepId,
|
|
54
|
+
action: input.action,
|
|
55
|
+
status: input.status,
|
|
56
|
+
elapsed_ms: input.elapsedMs,
|
|
57
|
+
before_url: sanitizeBrowserUrl(input.beforeUrl),
|
|
58
|
+
after_url: sanitizeBrowserUrl(input.afterUrl),
|
|
59
|
+
error: input.error,
|
|
60
|
+
event_sequence_start: input.eventStart,
|
|
61
|
+
event_sequence_end: input.eventEnd,
|
|
62
|
+
artifacts: input.artifacts,
|
|
63
|
+
completeness: stepCompleteness(input.artifacts, input.status),
|
|
64
|
+
});
|
|
65
|
+
const initialStep = async (input) => {
|
|
66
|
+
const { session, scenario, budget, elapsedMs, maximumTimeoutMs, signal } = input;
|
|
67
|
+
const artifacts = await session.capture(requestedForStep(scenario, 0), budget, maximumTimeoutMs, signal);
|
|
68
|
+
return createStep({
|
|
69
|
+
stepIndex: 0,
|
|
70
|
+
stepId: "scenario_start",
|
|
71
|
+
action: "goto_start",
|
|
72
|
+
status: "completed",
|
|
73
|
+
elapsedMs,
|
|
74
|
+
beforeUrl: session.initialUrl,
|
|
75
|
+
afterUrl: session.currentUrl(),
|
|
76
|
+
error: null,
|
|
77
|
+
eventStart: 1,
|
|
78
|
+
eventEnd: session.lastEventSequence(),
|
|
79
|
+
artifacts,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const executeStep = async (input) => {
|
|
83
|
+
const { session, scenario, budget, action, stepIndex, startedAt, priorFailure, signal, } = input;
|
|
84
|
+
const beforeUrl = session.currentUrl();
|
|
85
|
+
const eventStart = session.nextEventSequence();
|
|
86
|
+
const requested = requestedForStep(scenario, stepIndex);
|
|
87
|
+
if (priorFailure)
|
|
88
|
+
return createStep({
|
|
89
|
+
stepIndex,
|
|
90
|
+
stepId: action.step_id,
|
|
91
|
+
action: action.action,
|
|
92
|
+
status: "cancelled",
|
|
93
|
+
elapsedMs: 0,
|
|
94
|
+
beforeUrl,
|
|
95
|
+
afterUrl: beforeUrl,
|
|
96
|
+
error: "not executed after an earlier action failure",
|
|
97
|
+
eventStart,
|
|
98
|
+
eventEnd: session.lastEventSequence(),
|
|
99
|
+
artifacts: unavailableArtifacts(requested, "action was not executed after an earlier failure"),
|
|
100
|
+
});
|
|
101
|
+
const actionStartedAt = Date.now();
|
|
102
|
+
session.setStep(stepIndex);
|
|
103
|
+
const requestCancelled = () => signal?.aborted === true;
|
|
104
|
+
let status = "completed";
|
|
105
|
+
let error = null;
|
|
106
|
+
if (requestCancelled()) {
|
|
107
|
+
status = "cancelled";
|
|
108
|
+
error = "scenario request cancelled";
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
try {
|
|
112
|
+
const elapsed = Date.now() - startedAt;
|
|
113
|
+
const remaining = scenario.limits.max_duration_ms - elapsed;
|
|
114
|
+
if (remaining <= 0)
|
|
115
|
+
throw new Error("Scenario duration limit reached");
|
|
116
|
+
await session.perform(action, remaining, signal);
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
status = requestCancelled() ? "cancelled" : "failed";
|
|
120
|
+
error = session.redactError(cause);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const remaining = scenario.limits.max_duration_ms - (Date.now() - startedAt);
|
|
124
|
+
const artifacts = status === "cancelled" || remaining <= 0
|
|
125
|
+
? unavailableArtifacts(requested, status === "cancelled"
|
|
126
|
+
? "scenario request cancelled"
|
|
127
|
+
: "scenario duration limit reached")
|
|
128
|
+
: await session.capture(requested, budget, remaining, signal);
|
|
129
|
+
return createStep({
|
|
130
|
+
stepIndex,
|
|
131
|
+
stepId: action.step_id,
|
|
132
|
+
action: action.action,
|
|
133
|
+
status,
|
|
134
|
+
elapsedMs: Date.now() - actionStartedAt,
|
|
135
|
+
beforeUrl,
|
|
136
|
+
afterUrl: session.currentUrl(),
|
|
137
|
+
error,
|
|
138
|
+
eventStart,
|
|
139
|
+
eventEnd: session.lastEventSequence(),
|
|
140
|
+
artifacts,
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const globalCompleteness = (scenario, session, steps) => {
|
|
144
|
+
const missing = steps.flatMap(({ completeness }) => completeness.missing_sections);
|
|
145
|
+
const truncated = steps.flatMap(({ completeness }) => completeness.truncated_sections);
|
|
146
|
+
truncated.push(...session.eventTruncationSections());
|
|
147
|
+
if (session.mode === "connect")
|
|
148
|
+
missing.push("events");
|
|
149
|
+
const status = truncated.length > 0
|
|
150
|
+
? "truncated"
|
|
151
|
+
: missing.length > 0
|
|
152
|
+
? "incomplete"
|
|
153
|
+
: "complete";
|
|
154
|
+
return browserScenarioCompletenessSchema.parse({
|
|
155
|
+
status,
|
|
156
|
+
equality_eligible: status === "complete",
|
|
157
|
+
missing_sections: [...new Set(missing)].sort(),
|
|
158
|
+
truncated_sections: [...new Set(truncated)].sort(),
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const runScenario = async (factory, scenario, options) => {
|
|
162
|
+
if (options.signal?.aborted === true)
|
|
163
|
+
throw new BrowserObservationError(OPERATION, "cancelled");
|
|
164
|
+
const startedAt = Date.now();
|
|
165
|
+
const budget = new BrowserScenarioCaptureBudget(scenario.limits.max_screenshots, scenario.limits.max_total_metadata_bytes);
|
|
166
|
+
const session = await factory.open(scenario, {
|
|
167
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
168
|
+
budget,
|
|
169
|
+
deadlineAt: startedAt + scenario.limits.max_duration_ms,
|
|
170
|
+
});
|
|
171
|
+
const steps = [];
|
|
172
|
+
let cleanup;
|
|
173
|
+
try {
|
|
174
|
+
steps.push(await initialStep({
|
|
175
|
+
session,
|
|
176
|
+
scenario,
|
|
177
|
+
budget,
|
|
178
|
+
elapsedMs: Date.now() - startedAt,
|
|
179
|
+
maximumTimeoutMs: scenario.limits.max_duration_ms - (Date.now() - startedAt),
|
|
180
|
+
signal: options.signal,
|
|
181
|
+
}));
|
|
182
|
+
let failed = false;
|
|
183
|
+
for (const [offset, action] of scenario.actions.entries()) {
|
|
184
|
+
const step = await executeStep({
|
|
185
|
+
session,
|
|
186
|
+
scenario,
|
|
187
|
+
budget,
|
|
188
|
+
action,
|
|
189
|
+
stepIndex: offset + 1,
|
|
190
|
+
startedAt,
|
|
191
|
+
priorFailure: failed,
|
|
192
|
+
signal: options.signal,
|
|
193
|
+
});
|
|
194
|
+
steps.push(step);
|
|
195
|
+
failed ||= step.status !== "completed";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
cleanup = await session.close();
|
|
200
|
+
}
|
|
201
|
+
const events = session.events();
|
|
202
|
+
const completeness = globalCompleteness(scenario, session, steps);
|
|
203
|
+
return browserScenarioCaptureSchema.parse({
|
|
204
|
+
schema_version: 1,
|
|
205
|
+
browser: {
|
|
206
|
+
mode: session.mode,
|
|
207
|
+
process_ownership: session.processOwnership,
|
|
208
|
+
cleanup,
|
|
209
|
+
product: session.product,
|
|
210
|
+
version: session.version,
|
|
211
|
+
},
|
|
212
|
+
scenario: {
|
|
213
|
+
start_origin: new URL(scenario.start_url.url).origin,
|
|
214
|
+
allowed_origins: scenario.allowed_origins,
|
|
215
|
+
action_count: scenario.actions.length,
|
|
216
|
+
secret_references: scenario.secrets.map(({ secret_id: id }) => id).sort(),
|
|
217
|
+
},
|
|
218
|
+
duration_ms: Date.now() - startedAt,
|
|
219
|
+
steps,
|
|
220
|
+
events,
|
|
221
|
+
completeness,
|
|
222
|
+
limitations: [
|
|
223
|
+
"Event sequence records provider receipt order; simultaneous browser causality is not inferred.",
|
|
224
|
+
"Response bodies are not retained.",
|
|
225
|
+
"Storage values are hashed only after declared-secret redaction.",
|
|
226
|
+
...(session.mode === "connect"
|
|
227
|
+
? [
|
|
228
|
+
"CDP attachment cannot recover pre-attach events or guarantee launch-time context options.",
|
|
229
|
+
]
|
|
230
|
+
: []),
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
/** Controlled Playwright/CDP scenario driver with exact process ownership. */
|
|
235
|
+
export class PlaywrightBrowserScenarioProvider {
|
|
236
|
+
factory;
|
|
237
|
+
constructor(factory = new PlaywrightScenarioSessionFactory()) {
|
|
238
|
+
this.factory = factory;
|
|
239
|
+
}
|
|
240
|
+
identity() {
|
|
241
|
+
return PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY;
|
|
242
|
+
}
|
|
243
|
+
async captureScenario(scenario, options = {}) {
|
|
244
|
+
try {
|
|
245
|
+
return ok(await runScenario(this.factory, scenario, options));
|
|
246
|
+
}
|
|
247
|
+
catch (cause) {
|
|
248
|
+
if (cause instanceof AnalysisError)
|
|
249
|
+
return err(cause);
|
|
250
|
+
return err(new ProviderAdapterError(PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id, OPERATION, { cause }));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|