rea-agents 1.2.0 → 1.4.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 +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import writeFileAtomic from "write-file-atomic";
|
|
4
|
+
import { EvidenceFileError } from "../domain/errors.js";
|
|
5
|
+
import { err, ok } from "../domain/result.js";
|
|
6
|
+
/** Read bounded JSON data from a regular file beneath an approved root. */
|
|
7
|
+
export const readBoundedJson = async (path, policy) => {
|
|
8
|
+
if (policy.roots.length === 0)
|
|
9
|
+
return err(new EvidenceFileError("read", "disabled"));
|
|
10
|
+
try {
|
|
11
|
+
const canonicalPath = await realpath(resolve(path));
|
|
12
|
+
if (!(await isApproved(canonicalPath, policy.roots)))
|
|
13
|
+
return err(new EvidenceFileError("read", "outside-root"));
|
|
14
|
+
const stats = await lstat(canonicalPath);
|
|
15
|
+
if (!stats.isFile())
|
|
16
|
+
return err(new EvidenceFileError("read", "not-file"));
|
|
17
|
+
if (stats.size > policy.maxBytes)
|
|
18
|
+
return err(new EvidenceFileError("read", "too-large"));
|
|
19
|
+
const encoded = await readFile(canonicalPath);
|
|
20
|
+
if (encoded.byteLength > policy.maxBytes)
|
|
21
|
+
return err(new EvidenceFileError("read", "too-large"));
|
|
22
|
+
let decoded;
|
|
23
|
+
try {
|
|
24
|
+
decoded = JSON.parse(encoded.toString("utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
return err(new EvidenceFileError("read", "invalid-json", { cause }));
|
|
28
|
+
}
|
|
29
|
+
const limits = validateJsonLimits(decoded, policy);
|
|
30
|
+
return limits.ok ? ok(decoded) : limits;
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
return err(new EvidenceFileError("read", "io", { cause }));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/** Atomically write bounded text to a regular file beneath an approved root. */
|
|
37
|
+
export const writeBoundedText = async (encoded, path, overwrite, policy) => {
|
|
38
|
+
if (policy.roots.length === 0)
|
|
39
|
+
return err(new EvidenceFileError("write", "disabled"));
|
|
40
|
+
const bytes = Buffer.byteLength(encoded, "utf8");
|
|
41
|
+
if (bytes > policy.maxBytes)
|
|
42
|
+
return err(new EvidenceFileError("write", "too-large"));
|
|
43
|
+
try {
|
|
44
|
+
const requestedPath = resolve(path);
|
|
45
|
+
const canonicalParent = await realpath(dirname(requestedPath));
|
|
46
|
+
const destination = resolve(canonicalParent, basename(requestedPath));
|
|
47
|
+
if (!(await isApproved(destination, policy.roots)))
|
|
48
|
+
return err(new EvidenceFileError("write", "outside-root"));
|
|
49
|
+
const existing = await lstat(destination).catch((cause) => {
|
|
50
|
+
if (isFileNotFound(cause))
|
|
51
|
+
return undefined;
|
|
52
|
+
throw cause;
|
|
53
|
+
});
|
|
54
|
+
if (existing !== undefined) {
|
|
55
|
+
if (!overwrite)
|
|
56
|
+
return err(new EvidenceFileError("write", "exists"));
|
|
57
|
+
if (!existing.isFile() || existing.isSymbolicLink())
|
|
58
|
+
return err(new EvidenceFileError("write", "not-file"));
|
|
59
|
+
}
|
|
60
|
+
await writeFileAtomic(destination, encoded, {
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
mode: 0o600,
|
|
63
|
+
fsync: true,
|
|
64
|
+
});
|
|
65
|
+
return ok({ path: requestedPath, bytes });
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
return err(new EvidenceFileError("write", "io", { cause }));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const isApproved = async (candidate, roots) => {
|
|
72
|
+
for (const configuredRoot of roots) {
|
|
73
|
+
const root = await realpath(resolve(configuredRoot));
|
|
74
|
+
const relation = relative(root, candidate);
|
|
75
|
+
if (relation === "" ||
|
|
76
|
+
(!relation.startsWith("..") && !isAbsolute(relation)))
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
const validateJsonLimits = (root, policy) => {
|
|
82
|
+
const pending = [
|
|
83
|
+
{ value: root, depth: 0 },
|
|
84
|
+
];
|
|
85
|
+
let nodes = 0;
|
|
86
|
+
while (pending.length > 0) {
|
|
87
|
+
const current = pending.pop();
|
|
88
|
+
if (current === undefined)
|
|
89
|
+
break;
|
|
90
|
+
nodes += 1;
|
|
91
|
+
if (nodes > policy.maxNodes || current.depth > policy.maxDepth)
|
|
92
|
+
return err(new EvidenceFileError("read", "too-large"));
|
|
93
|
+
if (typeof current.value === "string" &&
|
|
94
|
+
current.value.length > policy.maxStringLength)
|
|
95
|
+
return err(new EvidenceFileError("read", "too-large"));
|
|
96
|
+
if (typeof current.value !== "object" || current.value === null)
|
|
97
|
+
continue;
|
|
98
|
+
for (const [key, value] of Object.entries(current.value)) {
|
|
99
|
+
if (key.length > policy.maxStringLength)
|
|
100
|
+
return err(new EvidenceFileError("read", "too-large"));
|
|
101
|
+
pending.push({ value, depth: current.depth + 1 });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return ok(null);
|
|
105
|
+
};
|
|
106
|
+
const isFileNotFound = (cause) => typeof cause === "object" &&
|
|
107
|
+
cause !== null &&
|
|
108
|
+
"code" in cause &&
|
|
109
|
+
cause.code === "ENOENT";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
3
|
+
const statusSchema = z.object({
|
|
4
|
+
open: z.boolean(),
|
|
5
|
+
kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
|
|
6
|
+
format: z.string().optional(),
|
|
7
|
+
capabilities: z.array(z.object({
|
|
8
|
+
operation: z.string(),
|
|
9
|
+
available: z.boolean(),
|
|
10
|
+
reason: z.string().nullable(),
|
|
11
|
+
})),
|
|
12
|
+
});
|
|
13
|
+
const ENHANCED_REQUIREMENTS = {
|
|
14
|
+
swift_classes: ["list_procedures"],
|
|
15
|
+
get_objc_classes: ["list_names"],
|
|
16
|
+
get_objc_protocols: ["list_names"],
|
|
17
|
+
batch_decompile: ["procedure_pseudo_code"],
|
|
18
|
+
get_call_graph: ["procedure_callees", "procedure_callers"],
|
|
19
|
+
analyze_swift_types: ["list_procedures"],
|
|
20
|
+
find_xrefs_to_name: ["list_names", "xrefs"],
|
|
21
|
+
binary_overview: [
|
|
22
|
+
"list_segments",
|
|
23
|
+
"list_documents",
|
|
24
|
+
"list_procedures",
|
|
25
|
+
"list_strings",
|
|
26
|
+
],
|
|
27
|
+
analyze_function: ["analyze_function"],
|
|
28
|
+
trace_feature: [
|
|
29
|
+
"list_strings",
|
|
30
|
+
"list_procedures",
|
|
31
|
+
"xrefs",
|
|
32
|
+
"resolve_containing_procedure",
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
/** Build stable per-operation availability without hiding familiar tools. */
|
|
36
|
+
export const buildCapabilityInventory = (sessionStatus, policy) => {
|
|
37
|
+
const status = statusSchema.parse(sessionStatus);
|
|
38
|
+
const descriptors = new Map(status.capabilities.map((descriptor) => [descriptor.operation, descriptor]));
|
|
39
|
+
return TOOL_CONTRACTS.map((contract) => {
|
|
40
|
+
const availability = availabilityFor(contract.name, contract.kind, status.open, status.kind, descriptors, policy);
|
|
41
|
+
return {
|
|
42
|
+
name: contract.name,
|
|
43
|
+
surface: contract.kind,
|
|
44
|
+
available: availability.reason === "available",
|
|
45
|
+
reason: availability.reason,
|
|
46
|
+
remediation: availability.remediation,
|
|
47
|
+
annotations: {
|
|
48
|
+
read_only: contract.annotations.readOnlyHint ?? false,
|
|
49
|
+
destructive: contract.annotations.destructiveHint ?? false,
|
|
50
|
+
idempotent: contract.annotations.idempotentHint ?? false,
|
|
51
|
+
open_world: contract.annotations.openWorldHint ?? true,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
55
|
+
};
|
|
56
|
+
const availabilityFor = (name, kind, targetOpen, targetKind, descriptors, policy) => {
|
|
57
|
+
if (name === "capture_process_scenario" && !policy.processCaptureEnabled)
|
|
58
|
+
return {
|
|
59
|
+
reason: "policy_disabled",
|
|
60
|
+
remediation: "Enable or grant process_capture within the administrator ceiling.",
|
|
61
|
+
};
|
|
62
|
+
if (name === "import_evidence_bundle" && policy.evidenceFileRoots === 0)
|
|
63
|
+
return {
|
|
64
|
+
reason: "policy_disabled",
|
|
65
|
+
remediation: "Configure or grant an evidence_read root.",
|
|
66
|
+
};
|
|
67
|
+
if (kind === "session")
|
|
68
|
+
return { reason: "available", remediation: null };
|
|
69
|
+
if (!targetOpen)
|
|
70
|
+
return {
|
|
71
|
+
reason: "target_required",
|
|
72
|
+
remediation: "Call open_binary with a supported local target.",
|
|
73
|
+
};
|
|
74
|
+
if (targetKind !== undefined &&
|
|
75
|
+
targetKind !== "executable" &&
|
|
76
|
+
targetKind !== "database" &&
|
|
77
|
+
(kind === "official-proxy" || kind === "enhanced"))
|
|
78
|
+
return {
|
|
79
|
+
reason: "target_unsupported",
|
|
80
|
+
remediation: "Inventory or extract a native executable, then call open_binary on that executable.",
|
|
81
|
+
};
|
|
82
|
+
if (kind === "enhanced")
|
|
83
|
+
return composedAvailability(name, descriptors);
|
|
84
|
+
const descriptor = descriptors.get(name);
|
|
85
|
+
if (descriptor === undefined)
|
|
86
|
+
return {
|
|
87
|
+
reason: "provider_missing",
|
|
88
|
+
remediation: "Install or configure a provider that declares this operation.",
|
|
89
|
+
};
|
|
90
|
+
if (!descriptor.available &&
|
|
91
|
+
descriptor.reason?.toLowerCase().includes("require macos") === true)
|
|
92
|
+
return {
|
|
93
|
+
reason: "unsupported_host",
|
|
94
|
+
remediation: descriptor.reason,
|
|
95
|
+
};
|
|
96
|
+
return descriptor.available
|
|
97
|
+
? { reason: "available", remediation: null }
|
|
98
|
+
: {
|
|
99
|
+
reason: "provider_unavailable",
|
|
100
|
+
remediation: descriptor.reason ?? "Choose another target or configured provider.",
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const composedAvailability = (name, descriptors) => {
|
|
104
|
+
const requirements = ENHANCED_REQUIREMENTS[name];
|
|
105
|
+
if (requirements === undefined)
|
|
106
|
+
return {
|
|
107
|
+
reason: "provider_missing",
|
|
108
|
+
remediation: "No provider composition is declared for this operation.",
|
|
109
|
+
};
|
|
110
|
+
const missing = requirements.find((operation) => !descriptors.has(operation));
|
|
111
|
+
if (missing !== undefined)
|
|
112
|
+
return {
|
|
113
|
+
reason: "provider_missing",
|
|
114
|
+
remediation: `Configure a provider for required operation ${missing}.`,
|
|
115
|
+
};
|
|
116
|
+
const unavailable = requirements
|
|
117
|
+
.map((operation) => descriptors.get(operation))
|
|
118
|
+
.find((descriptor) => descriptor?.available === false);
|
|
119
|
+
if (unavailable !== undefined)
|
|
120
|
+
return {
|
|
121
|
+
reason: unavailable.reason?.toLowerCase().includes("require macos") === true
|
|
122
|
+
? "unsupported_host"
|
|
123
|
+
: "provider_unavailable",
|
|
124
|
+
remediation: unavailable.reason ?? "Choose another target or configured provider.",
|
|
125
|
+
};
|
|
126
|
+
return { reason: "available", remediation: null };
|
|
127
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
6
|
+
import { supportedClients } from "./SupportedClients.js";
|
|
7
|
+
const objectSchema = z.record(z.string(), z.unknown());
|
|
8
|
+
const registrationSchema = z
|
|
9
|
+
.object({ command: z.string().min(1), args: z.array(z.string()).default([]) })
|
|
10
|
+
.passthrough();
|
|
11
|
+
/** Inspect supported client registrations without reading their environment. */
|
|
12
|
+
export const readClientRegistrationStatuses = async (home, currentCommandPath = resolve(process.argv[1] ?? "unknown")) => {
|
|
13
|
+
const statuses = [];
|
|
14
|
+
for (const client of supportedClients(home)) {
|
|
15
|
+
if (client.format === "unsupported" || !(await exists(client.markerPath)))
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(client.configPath, "utf8");
|
|
19
|
+
const document = objectSchema.parse(client.format === "toml" ? parseToml(content) : JSON.parse(content));
|
|
20
|
+
const servers = objectSchema.parse(document[client.format === "toml" ? "mcp_servers" : "mcpServers"] ?? {});
|
|
21
|
+
const raw = servers[PRODUCT_IDENTITY.mcpServerKey];
|
|
22
|
+
if (raw === undefined) {
|
|
23
|
+
statuses.push(status(client.name, client.configPath, [], "missing"));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const registration = registrationSchema.parse(raw);
|
|
27
|
+
const command = [registration.command, ...registration.args];
|
|
28
|
+
statuses.push(status(client.name, client.configPath, command, registrationAligned(command, currentCommandPath)
|
|
29
|
+
? "aligned"
|
|
30
|
+
: "stale"));
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
statuses.push(status(client.name, client.configPath, [], isMissing(cause) ? "missing" : "invalid"));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return statuses.sort((left, right) => left.client.localeCompare(right.client));
|
|
37
|
+
};
|
|
38
|
+
const registrationAligned = (command, currentCommandPath) => {
|
|
39
|
+
if (command.length === 4 &&
|
|
40
|
+
command[0] === "npx" &&
|
|
41
|
+
command[1] === "-y" &&
|
|
42
|
+
command[2] === PRODUCT_IDENTITY.packageName &&
|
|
43
|
+
command[3] === "mcp")
|
|
44
|
+
return true;
|
|
45
|
+
return (command.length === 2 &&
|
|
46
|
+
command[1] === "mcp" &&
|
|
47
|
+
resolve(command[0] ?? "") === currentCommandPath);
|
|
48
|
+
};
|
|
49
|
+
const status = (client, configPath, command, state) => ({
|
|
50
|
+
client,
|
|
51
|
+
config_path: configPath,
|
|
52
|
+
command,
|
|
53
|
+
state,
|
|
54
|
+
remediation: state === "aligned"
|
|
55
|
+
? null
|
|
56
|
+
: "Run rea setup to refresh this registration, then restart the client.",
|
|
57
|
+
});
|
|
58
|
+
const exists = async (path) => {
|
|
59
|
+
if (path === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
await access(path);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const isMissing = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
2
|
+
import { createEvidence } from "../domain/evidence.js";
|
|
3
|
+
import { AnalysisCancelledError, ArtifactOperationError, } from "../domain/errors.js";
|
|
4
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
5
|
+
import { err, ok } from "../domain/result.js";
|
|
6
|
+
import { paginateArtifactInventory, } from "./ArtifactInventory.js";
|
|
7
|
+
import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.js";
|
|
8
|
+
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
|
|
9
|
+
export const AUTOMATIC_RUN_LIMITATION = "Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
|
|
10
|
+
/** Authorize and scan both inputs once under the same bounded traversal policy. */
|
|
11
|
+
export const scanVersions = async (input, inputRoots, signal, integrityContinueEnabled = false) => {
|
|
12
|
+
try {
|
|
13
|
+
if (signal?.aborted === true)
|
|
14
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
15
|
+
const limits = artifactLimits(input.options);
|
|
16
|
+
const integrity = {
|
|
17
|
+
mode: input.integrity_policy,
|
|
18
|
+
approved: input.integrity_continue_approved,
|
|
19
|
+
enabled: integrityContinueEnabled,
|
|
20
|
+
maxMismatches: input.max_integrity_mismatches,
|
|
21
|
+
};
|
|
22
|
+
const left = await scanAuthorizedArtifactInventory(input.left_path, inputRoots, limits, signal, integrity);
|
|
23
|
+
const right = await scanAuthorizedArtifactInventory(input.right_path, inputRoots, limits, signal, integrity);
|
|
24
|
+
return ok({ left, right });
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
if (cause instanceof ArtifactReaderFailure)
|
|
28
|
+
return err(new ArtifactOperationError("inventory_artifact", cause.reason, cause.details));
|
|
29
|
+
return err(new ArtifactOperationError("inventory_artifact", "io"));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** Project complete snapshots into bounded, comparable Evidence page sets. */
|
|
33
|
+
export const createInventoryEvidencePages = (input, snapshots) => {
|
|
34
|
+
const left = inventoryPages(input.left_path, snapshots.left, input);
|
|
35
|
+
const right = inventoryPages(input.right_path, snapshots.right, input);
|
|
36
|
+
if (left.length > 100 || right.length > 100)
|
|
37
|
+
return err(new ArtifactOperationError("inventory_artifact", "limit"));
|
|
38
|
+
return ok({ left, right });
|
|
39
|
+
};
|
|
40
|
+
/** Reduce a snapshot to the content commitments used by run identity. */
|
|
41
|
+
export const targetFor = (snapshot) => ({
|
|
42
|
+
root_sha256: snapshot.manifest.root_sha256,
|
|
43
|
+
graph_sha256: snapshot.manifest.graph_sha256,
|
|
44
|
+
manifest_id: snapshot.manifest.manifest_id,
|
|
45
|
+
format: snapshot.manifest.root_format,
|
|
46
|
+
});
|
|
47
|
+
/** Preserve scan and execution limitations in stable order. */
|
|
48
|
+
export const runLimitations = (snapshots) => [
|
|
49
|
+
...new Set([
|
|
50
|
+
...snapshots.left.limitations.map((item) => `Left: ${item}`),
|
|
51
|
+
...snapshots.right.limitations.map((item) => `Right: ${item}`),
|
|
52
|
+
AUTOMATIC_RUN_LIMITATION,
|
|
53
|
+
]),
|
|
54
|
+
].sort((left, right) => left.localeCompare(right));
|
|
55
|
+
const inventoryPages = (path, snapshot, input) => {
|
|
56
|
+
const options = input.options;
|
|
57
|
+
const count = Math.max(1, Math.ceil(snapshot.nodes.length / options.page_size), Math.ceil(snapshot.occurrences.length / options.page_size), Math.ceil(snapshot.edges.length / options.page_size));
|
|
58
|
+
return Array.from({ length: count }, (_, index) => inventoryPage(path, snapshot, input, index * options.page_size));
|
|
59
|
+
};
|
|
60
|
+
const inventoryPage = (path, snapshot, input, offset) => {
|
|
61
|
+
const options = input.options;
|
|
62
|
+
const result = paginateArtifactInventory(snapshot, {
|
|
63
|
+
nodeOffset: offset,
|
|
64
|
+
nodeLimit: options.page_size,
|
|
65
|
+
occurrenceOffset: offset,
|
|
66
|
+
occurrenceLimit: options.page_size,
|
|
67
|
+
edgeOffset: offset,
|
|
68
|
+
edgeLimit: options.page_size,
|
|
69
|
+
});
|
|
70
|
+
return createEvidence({
|
|
71
|
+
path,
|
|
72
|
+
sha256: snapshot.manifest.root_sha256,
|
|
73
|
+
format: snapshot.manifest.root_format,
|
|
74
|
+
}, ARTIFACT_GRAPH_PROVIDER, {
|
|
75
|
+
operation: "inventory_artifact",
|
|
76
|
+
parameters: {
|
|
77
|
+
node_offset: offset,
|
|
78
|
+
node_limit: options.page_size,
|
|
79
|
+
occurrence_offset: offset,
|
|
80
|
+
occurrence_limit: options.page_size,
|
|
81
|
+
edge_offset: offset,
|
|
82
|
+
edge_limit: options.page_size,
|
|
83
|
+
max_entries: options.max_entries,
|
|
84
|
+
max_total_bytes: options.max_total_bytes,
|
|
85
|
+
max_entry_bytes: options.max_entry_bytes,
|
|
86
|
+
max_compression_ratio: options.max_compression_ratio,
|
|
87
|
+
max_depth: options.max_depth,
|
|
88
|
+
max_path_bytes: options.max_path_bytes,
|
|
89
|
+
integrity_policy: input.integrity_policy,
|
|
90
|
+
integrity_continue_approved: input.integrity_continue_approved,
|
|
91
|
+
max_integrity_mismatches: input.max_integrity_mismatches,
|
|
92
|
+
},
|
|
93
|
+
result: jsonValueSchema.parse(result),
|
|
94
|
+
rawResult: null,
|
|
95
|
+
limitations: result.limitations,
|
|
96
|
+
locations: result.occurrences.items.map(({ logical_path: logicalPath }) => ({
|
|
97
|
+
kind: "artifact-path",
|
|
98
|
+
path: logicalPath,
|
|
99
|
+
})),
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const artifactLimits = (options) => ({
|
|
103
|
+
maxEntries: options.max_entries,
|
|
104
|
+
maxTotalBytes: options.max_total_bytes,
|
|
105
|
+
maxEntryBytes: options.max_entry_bytes,
|
|
106
|
+
maxCompressionRatio: options.max_compression_ratio,
|
|
107
|
+
maxDepth: options.max_depth,
|
|
108
|
+
maxPathBytes: options.max_path_bytes,
|
|
109
|
+
});
|