rea-agents 0.2.1

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bridge/hopper_bridge.py +557 -0
  4. package/dist/application/AnalysisProvider.js +1 -0
  5. package/dist/application/BinarySession.js +173 -0
  6. package/dist/application/DirectAnalysis.js +31 -0
  7. package/dist/application/Doctor.js +110 -0
  8. package/dist/application/EnhancedTools.js +357 -0
  9. package/dist/application/EvidenceLedger.js +65 -0
  10. package/dist/application/HopperToolPort.js +1 -0
  11. package/dist/application/LoopbackReplay.js +84 -0
  12. package/dist/application/ProcessHarness.js +347 -0
  13. package/dist/application/Setup.js +240 -0
  14. package/dist/application/homebrew.js +19 -0
  15. package/dist/application/runtime.js +11 -0
  16. package/dist/cli.js +88 -0
  17. package/dist/config.js +86 -0
  18. package/dist/contracts/enhancedInputs.js +73 -0
  19. package/dist/contracts/toolContracts.js +144 -0
  20. package/dist/contracts/toolOutputSchemas.js +267 -0
  21. package/dist/domain/binaryTarget.js +321 -0
  22. package/dist/domain/errors.js +81 -0
  23. package/dist/domain/evidence.js +146 -0
  24. package/dist/domain/evidenceBundle.js +11 -0
  25. package/dist/domain/hopperValues.js +253 -0
  26. package/dist/domain/jsonValue.js +3 -0
  27. package/dist/domain/processCapture.js +252 -0
  28. package/dist/domain/result.js +4 -0
  29. package/dist/domain/symbolAnalysis.js +68 -0
  30. package/dist/hopper/BridgeLauncher.js +130 -0
  31. package/dist/hopper/HopperClient.js +314 -0
  32. package/dist/hopper/HopperProvider.js +66 -0
  33. package/dist/hopper/protocol.js +32 -0
  34. package/dist/identity.js +14 -0
  35. package/dist/logger.js +27 -0
  36. package/dist/main.js +81 -0
  37. package/dist/server/createServer.js +30 -0
  38. package/dist/server/registerEnhancedTools.js +53 -0
  39. package/dist/server/registerOfficialTools.js +59 -0
  40. package/dist/server/registerSessionTools.js +159 -0
  41. package/dist/server/toolLogging.js +11 -0
  42. package/dist/server/toolResult.js +44 -0
  43. package/package.json +109 -0
  44. package/scripts/rea.mjs +15 -0
  45. package/scripts/rebuild-native.mjs +22 -0
  46. package/skills/rea-analysis/SKILL.md +44 -0
@@ -0,0 +1,321 @@
1
+ import { constants } from "node:fs";
2
+ import { createReadStream } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { access, open, readFile, realpath, stat, } from "node:fs/promises";
5
+ import { execFile } from "node:child_process";
6
+ import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { BinaryTargetError } from "./errors.js";
9
+ import { err, ok } from "./result.js";
10
+ const execFileAsync = promisify(execFile);
11
+ /**
12
+ * Resolve and classify a readable local target before Hopper is launched.
13
+ * FAT Mach-O inputs select only a host-compatible architecture so setup remains
14
+ * non-interactive; unsupported or ambiguous inputs are returned as typed errors.
15
+ */
16
+ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitecture = process.arch, targetKind) => {
17
+ const candidate = isAbsolute(input) ? input : resolve(cwd, input);
18
+ try {
19
+ await access(candidate, constants.R_OK);
20
+ const canonical = await realpath(candidate);
21
+ const resolved = await resolveAppBundle(canonical);
22
+ if (!resolved.ok)
23
+ return err(resolved.error);
24
+ const path = resolved.value;
25
+ if (!(await stat(path)).isFile())
26
+ return err(new BinaryTargetError(path, "target is not a regular file"));
27
+ if (targetKind === "database" ||
28
+ (targetKind === undefined && path.toLowerCase().endsWith(".hop")))
29
+ return ok({
30
+ path,
31
+ sha256: await sha256File(path),
32
+ kind: "database",
33
+ format: "hopper",
34
+ loaderArgs: [],
35
+ });
36
+ const handle = await open(path, "r");
37
+ let detected;
38
+ try {
39
+ detected = await readExecutableMetadata(handle, hostArchitecture);
40
+ }
41
+ finally {
42
+ await handle.close();
43
+ }
44
+ if (!detected.ok)
45
+ return err(new BinaryTargetError(path, detected.error));
46
+ return ok({
47
+ path,
48
+ sha256: await sha256File(path),
49
+ kind: "executable",
50
+ ...detected.value,
51
+ });
52
+ }
53
+ catch (cause) {
54
+ return err(new BinaryTargetError(candidate, "path is not readable", { cause }));
55
+ }
56
+ };
57
+ const sha256File = async (path) => {
58
+ const hash = createHash("sha256");
59
+ for await (const chunk of createReadStream(path))
60
+ hash.update(chunk);
61
+ return hash.digest("hex");
62
+ };
63
+ const resolveAppBundle = async (path) => {
64
+ const metadata = await stat(path);
65
+ if (!metadata.isDirectory())
66
+ return ok(path);
67
+ if (extname(path).toLowerCase() !== ".app")
68
+ return err(new BinaryTargetError(path, "target is not an app or file"));
69
+ const plistPath = join(path, "Contents", "Info.plist");
70
+ let name;
71
+ try {
72
+ const plist = await readFile(plistPath);
73
+ name =
74
+ plist.subarray(0, 6).toString("ascii") === "bplist"
75
+ ? await readBinaryPlistExecutable(plistPath)
76
+ : parseXmlPlistExecutable(plist.toString("utf8"));
77
+ }
78
+ catch (cause) {
79
+ return err(new BinaryTargetError(path, "app has no readable CFBundleExecutable", {
80
+ cause,
81
+ }));
82
+ }
83
+ if (!isSafeExecutableName(name))
84
+ return err(new BinaryTargetError(path, "app has an unsafe CFBundleExecutable"));
85
+ const programs = join(path, "Contents", "MacOS");
86
+ const executable = join(programs, name);
87
+ try {
88
+ const [canonicalPrograms, canonicalExecutable] = await Promise.all([
89
+ realpath(programs),
90
+ realpath(executable),
91
+ ]);
92
+ const location = relative(canonicalPrograms, canonicalExecutable);
93
+ if (location === ".." ||
94
+ location.startsWith(`..${sep}`) ||
95
+ isAbsolute(location))
96
+ return err(new BinaryTargetError(path, "app program file leaves Contents/MacOS"));
97
+ return ok(canonicalExecutable);
98
+ }
99
+ catch (cause) {
100
+ return err(new BinaryTargetError(path, "app program file is missing", { cause }));
101
+ }
102
+ };
103
+ const parseXmlPlistExecutable = (plist) => {
104
+ const match = /<key>\s*CFBundleExecutable\s*<\/key>\s*<string>([^<]+)<\/string>/u.exec(plist);
105
+ if (match?.[1] === undefined)
106
+ throw new Error("CFBundleExecutable is missing");
107
+ return decodeXml(match[1].trim());
108
+ };
109
+ const readBinaryPlistExecutable = async (plistPath) => (await execFileAsync("/usr/bin/plutil", [
110
+ "-extract",
111
+ "CFBundleExecutable",
112
+ "raw",
113
+ "-o",
114
+ "-",
115
+ plistPath,
116
+ ])).stdout.trim();
117
+ const isSafeExecutableName = (name) => name.length > 0 &&
118
+ name !== "." &&
119
+ name !== ".." &&
120
+ !name.includes("\0") &&
121
+ !/[/\\]/u.test(name);
122
+ const decodeXml = (value) => value
123
+ .replaceAll("&amp;", "&")
124
+ .replaceAll("&lt;", "<")
125
+ .replaceAll("&gt;", ">")
126
+ .replaceAll("&quot;", '"')
127
+ .replaceAll("&apos;", "'");
128
+ const readExecutableMetadata = async (handle, hostArchitecture) => {
129
+ const prefix = Buffer.alloc(4096);
130
+ const prefixRead = await handle.read(prefix, 0, prefix.length, 0);
131
+ const bytes = prefix.subarray(0, prefixRead.bytesRead);
132
+ if (bytes.length >= 64 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
133
+ const offset = bytes.readUInt32LE(0x3c);
134
+ if (offset > bytes.length - 6) {
135
+ const record = Buffer.alloc(6);
136
+ const recordRead = await handle.read(record, 0, record.length, offset);
137
+ return recordRead.bytesRead === record.length
138
+ ? parsePeRecord(record)
139
+ : err("invalid or truncated PE header");
140
+ }
141
+ }
142
+ if (bytes.length >= 8) {
143
+ const magic = bytes.readUInt32BE(0);
144
+ if ([0xcafebabf, 0xbfbafeca].includes(magic)) {
145
+ const little = magic === 0xbfbafeca;
146
+ const count = little ? bytes.readUInt32LE(4) : bytes.readUInt32BE(4);
147
+ const required = 8 + count * 32;
148
+ if (count <= 128 && required > bytes.length) {
149
+ const header = Buffer.alloc(required);
150
+ const headerRead = await handle.read(header, 0, header.length, 0);
151
+ return parseExecutableHeader(header.subarray(0, headerRead.bytesRead), hostArchitecture);
152
+ }
153
+ }
154
+ }
155
+ return parseExecutableHeader(bytes, hostArchitecture);
156
+ };
157
+ /**
158
+ * Parse supported executable headers without I/O and derive explicit Hopper
159
+ * loader arguments. The caller must supply the host architecture used for FAT
160
+ * Mach-O slice selection.
161
+ */
162
+ export const parseExecutableHeader = (bytes, hostArchitecture) => {
163
+ if (bytes.length >= 4 &&
164
+ bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
165
+ return parseElf(bytes);
166
+ if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a)
167
+ return parsePe(bytes);
168
+ if (bytes.length < 8)
169
+ return err("truncated or unsupported binary header");
170
+ const magic = bytes.readUInt32BE(0);
171
+ if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
172
+ return parseThinMachO(bytes, magic);
173
+ if ([0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic))
174
+ return parseFatMachO(bytes, magic, hostArchitecture);
175
+ return err("unsupported binary format");
176
+ };
177
+ const parseThinMachO = (bytes, magic) => {
178
+ if (bytes.length < 8)
179
+ return err("truncated Mach-O header");
180
+ const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
181
+ const architecture = machArchitecture(little ? bytes.readUInt32LE(4) : bytes.readUInt32BE(4));
182
+ if (architecture === undefined)
183
+ return err("unsupported Mach-O architecture");
184
+ return ok({
185
+ format: "mach-o",
186
+ architecture,
187
+ availableArchitectures: [architecture],
188
+ loaderArgs: ["-l", "Mach-O", hopperArchitectureFlag(architecture)],
189
+ });
190
+ };
191
+ const parseFatMachO = (bytes, magic, host) => {
192
+ const little = magic === 0xbebafeca || magic === 0xbfbafeca;
193
+ const is64 = magic === 0xcafebabf || magic === 0xbfbafeca;
194
+ if (bytes.length < 8)
195
+ return err("truncated FAT header");
196
+ const read = (offset) => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
197
+ const count = read(4);
198
+ const entrySize = is64 ? 32 : 20;
199
+ if (count === 0 || count > 128 || bytes.length < 8 + count * entrySize)
200
+ return err("truncated or invalid FAT architecture table");
201
+ const architectures = [];
202
+ for (let index = 0; index < count; index += 1) {
203
+ const architecture = machArchitecture(read(8 + index * entrySize));
204
+ if (architecture !== undefined && !architectures.includes(architecture))
205
+ architectures.push(architecture);
206
+ }
207
+ const preferred = host === "arm64"
208
+ ? "arm64"
209
+ : host === "x64"
210
+ ? "x86_64"
211
+ : host === "ia32"
212
+ ? "x86"
213
+ : host === "arm"
214
+ ? "arm"
215
+ : undefined;
216
+ if (preferred === undefined || !architectures.includes(preferred))
217
+ return err(`FAT binary has no host-compatible ${host} architecture`);
218
+ return ok({
219
+ format: "mach-o",
220
+ architecture: preferred,
221
+ availableArchitectures: architectures,
222
+ loaderArgs: [
223
+ "-l",
224
+ "FAT",
225
+ hopperArchitectureFlag(preferred),
226
+ "-l",
227
+ "Mach-O",
228
+ ],
229
+ });
230
+ };
231
+ const parseElf = (bytes) => {
232
+ if (bytes.length < 20)
233
+ return err("truncated ELF header");
234
+ if (bytes[4] !== 1 && bytes[4] !== 2)
235
+ return err("unsupported ELF class");
236
+ const little = bytes[5] === 1;
237
+ if (!little && bytes[5] !== 2)
238
+ return err("unsupported ELF endianness");
239
+ const machine = little ? bytes.readUInt16LE(18) : bytes.readUInt16BE(18);
240
+ const architecture = elfArchitecture(machine);
241
+ if (architecture === undefined)
242
+ return err("unsupported ELF architecture");
243
+ return ok({
244
+ format: "elf",
245
+ architecture,
246
+ availableArchitectures: [architecture],
247
+ loaderArgs: ["-l", "ELF", hopperArchitectureFlag(architecture)],
248
+ });
249
+ };
250
+ const parsePe = (bytes) => {
251
+ if (bytes.length < 64)
252
+ return err("truncated PE DOS header");
253
+ const offset = bytes.readUInt32LE(0x3c);
254
+ if (offset > bytes.length - 6)
255
+ return err("invalid or truncated PE header");
256
+ return parsePeRecord(bytes.subarray(offset, offset + 6));
257
+ };
258
+ const parsePeRecord = (record) => {
259
+ if (record.length < 6 || record.toString("binary", 0, 4) !== "PE\u0000\u0000")
260
+ return err("invalid or truncated PE header");
261
+ const architecture = peArchitecture(record.readUInt16LE(4));
262
+ if (architecture === undefined)
263
+ return err("unsupported PE architecture");
264
+ return ok({
265
+ format: "pe",
266
+ architecture,
267
+ availableArchitectures: [architecture],
268
+ loaderArgs: ["-l", "WinPE", hopperArchitectureFlag(architecture)],
269
+ });
270
+ };
271
+ const machArchitecture = (cpu) => {
272
+ switch (cpu) {
273
+ case 7:
274
+ return "x86";
275
+ case 0x01000007:
276
+ return "x86_64";
277
+ case 12:
278
+ return "arm";
279
+ case 0x0100000c:
280
+ return "arm64";
281
+ }
282
+ return undefined;
283
+ };
284
+ const elfArchitecture = (machine) => {
285
+ switch (machine) {
286
+ case 3:
287
+ return "x86";
288
+ case 62:
289
+ return "x86_64";
290
+ case 40:
291
+ return "arm";
292
+ case 183:
293
+ return "arm64";
294
+ }
295
+ return undefined;
296
+ };
297
+ const peArchitecture = (machine) => {
298
+ switch (machine) {
299
+ case 0x14c:
300
+ return "x86";
301
+ case 0x8664:
302
+ return "x86_64";
303
+ case 0x1c0:
304
+ return "arm";
305
+ case 0xaa64:
306
+ return "arm64";
307
+ }
308
+ return undefined;
309
+ };
310
+ const hopperArchitectureFlag = (architecture) => {
311
+ switch (architecture) {
312
+ case "x86":
313
+ return "--intel-32";
314
+ case "x86_64":
315
+ return "--intel-64";
316
+ case "arm":
317
+ return "--armv7";
318
+ case "arm64":
319
+ return "--aarch64";
320
+ }
321
+ };
@@ -0,0 +1,81 @@
1
+ /** Base class for expected analysis, provider, and session failures. */
2
+ export class AnalysisError extends Error {
3
+ }
4
+ /** Base class for failures produced specifically by the Hopper provider. */
5
+ export class HopperError extends AnalysisError {
6
+ }
7
+ /** Provider-neutral invalid analysis input or output at an application boundary. */
8
+ export class AnalysisProtocolError extends AnalysisError {
9
+ _tag = "AnalysisProtocolError";
10
+ }
11
+ /** An evidence bundle or bounded session ledger rejected caller-controlled data. */
12
+ export class EvidenceLedgerError extends AnalysisError {
13
+ _tag = "EvidenceLedgerError";
14
+ }
15
+ /** Hopper did not respond within the configured operation deadline. */
16
+ export class HopperTimeoutError extends HopperError {
17
+ timeoutMs;
18
+ _tag = "HopperTimeoutError";
19
+ constructor(timeoutMs) {
20
+ super(`Hopper did not respond within ${String(timeoutMs)}ms`);
21
+ this.timeoutMs = timeoutMs;
22
+ }
23
+ }
24
+ /** The caller cancelled a Hopper operation before it completed. */
25
+ export class HopperCancelledError extends HopperError {
26
+ _tag = "HopperCancelledError";
27
+ constructor() {
28
+ super("Hopper operation was cancelled");
29
+ }
30
+ }
31
+ /** Hopper returned bytes or JSON that do not satisfy the NDJSON RPC contract. */
32
+ export class HopperProtocolError extends HopperError {
33
+ _tag = "HopperProtocolError";
34
+ }
35
+ /** Hopper's JSON-RPC endpoint returned an expected remote error response. */
36
+ export class HopperRemoteError extends HopperError {
37
+ code;
38
+ safeMessage;
39
+ _tag = "HopperRemoteError";
40
+ constructor(code, safeMessage) {
41
+ super(`Hopper request failed (${String(code)}): ${safeMessage}`);
42
+ this.code = code;
43
+ this.safeMessage = safeMessage;
44
+ }
45
+ }
46
+ /** The owned Hopper bridge stopped before its client was closed. */
47
+ export class HopperProcessError extends HopperError {
48
+ exitCode;
49
+ _tag = "HopperProcessError";
50
+ constructor(exitCode) {
51
+ super(`Hopper bridge stopped unexpectedly with code ${String(exitCode)}`);
52
+ this.exitCode = exitCode;
53
+ }
54
+ }
55
+ /** Hopper or the repository bridge could not be started. */
56
+ export class HopperStartError extends HopperError {
57
+ _tag = "HopperStartError";
58
+ constructor(options) {
59
+ super("Hopper application bridge could not be started", options);
60
+ }
61
+ }
62
+ /** Runtime configuration could not be parsed safely. */
63
+ export class ConfigurationError extends AnalysisError {
64
+ _tag = "ConfigurationError";
65
+ }
66
+ /** No app or binary session exists for an analysis request. */
67
+ export class NoBinaryOpenError extends AnalysisError {
68
+ _tag = "NoBinaryOpenError";
69
+ constructor() {
70
+ super("No app is open. Ask the user which app to investigate, then call open_binary with its local path.");
71
+ }
72
+ }
73
+ /** A supplied target path could not be safely opened as a supported app or binary. */
74
+ export class BinaryTargetError extends AnalysisError {
75
+ path;
76
+ _tag = "BinaryTargetError";
77
+ constructor(path, reason, options) {
78
+ super(`Cannot open app: ${reason}: ${path}`, options);
79
+ this.path = path;
80
+ }
81
+ }
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ import { jsonValueSchema } from "./jsonValue.js";
5
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
6
+ const providerSchema = z.object({
7
+ id: z.string().min(1),
8
+ name: z.string().min(1),
9
+ version: z.string().nullable(),
10
+ });
11
+ const subjectSchema = z.object({
12
+ name: z.string().min(1),
13
+ digest: z.object({ sha256: digestSchema }),
14
+ format: z.enum(["hopper", "mach-o", "elf", "pe"]),
15
+ architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
16
+ local_path: z.string(),
17
+ });
18
+ const locationSchema = z.discriminatedUnion("kind", [
19
+ z.object({ kind: z.literal("address"), address: z.string().min(1) }),
20
+ z.object({
21
+ kind: z.literal("address-range"),
22
+ start: z.string().min(1),
23
+ end: z.string().min(1),
24
+ }),
25
+ z.object({ kind: z.literal("artifact-path"), path: z.string().min(1) }),
26
+ ]);
27
+ const evidenceAuthoritySchema = z.enum([
28
+ "shipped-artifact",
29
+ "controlled-replay",
30
+ "historical-reference",
31
+ "external-service",
32
+ "analyst-inference",
33
+ ]);
34
+ const executionEnvironmentSchema = z.object({
35
+ id: z.string().min(1),
36
+ platform: z.string().min(1),
37
+ architecture: z.string().min(1),
38
+ isolation: z.enum(["none", "process", "container", "virtual-machine"]),
39
+ });
40
+ /** Strict, provider-neutral record for one successful public observation. */
41
+ export const evidenceSchema = z.object({
42
+ schema_version: z.literal(2),
43
+ evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u),
44
+ subject: subjectSchema.nullable(),
45
+ provider: providerSchema,
46
+ predicate_type: z.string().min(1),
47
+ operation: z.string().min(1),
48
+ parameters: z.record(z.string(), jsonValueSchema),
49
+ result: jsonValueSchema,
50
+ raw_payload_sha256: digestSchema.nullable(),
51
+ confidence: z.enum(["observed", "derived", "inferred"]),
52
+ authority: evidenceAuthoritySchema,
53
+ environment: executionEnvironmentSchema.nullable(),
54
+ limitations: z.array(z.string()),
55
+ locations: z.array(locationSchema),
56
+ evidence_links: z.array(z.string().regex(/^ev_[a-f0-9]{64}$/u)),
57
+ });
58
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
59
+ /** Serialize JSON according to RFC 8785 JSON Canonicalization Scheme. */
60
+ const canonicalJson = (value) => {
61
+ const serialized = canonicalize(value);
62
+ if (serialized === undefined)
63
+ throw new TypeError("RFC 8785 canonicalization rejected a JSON value");
64
+ return serialized;
65
+ };
66
+ const semanticProjection = (evidence) => ({
67
+ schema_version: evidence.schema_version,
68
+ subject: evidence.subject === null
69
+ ? null
70
+ : {
71
+ digest: evidence.subject.digest,
72
+ format: evidence.subject.format,
73
+ architecture: evidence.subject.architecture,
74
+ },
75
+ provider: evidence.provider,
76
+ predicate_type: evidence.predicate_type,
77
+ operation: evidence.operation,
78
+ parameters: evidence.parameters,
79
+ result: evidence.result,
80
+ confidence: evidence.confidence,
81
+ authority: evidence.authority,
82
+ environment: evidence.environment,
83
+ limitations: evidence.limitations,
84
+ locations: evidence.locations,
85
+ evidence_links: evidence.evidence_links,
86
+ });
87
+ /** Recompute the semantic identifier, excluding paths and raw payload bytes. */
88
+ const computeEvidenceId = (evidence) => `ev_${sha256(canonicalJson(semanticProjection(evidence)))}`;
89
+ /** Parse evidence and reject a syntactically valid but tampered semantic ID. */
90
+ export const parseEvidence = (input) => {
91
+ const evidence = evidenceSchema.parse(input);
92
+ const { evidence_id: evidenceId, ...withoutId } = evidence;
93
+ if (computeEvidenceId(withoutId) !== evidenceId)
94
+ throw new TypeError("Evidence semantic identifier does not match its record");
95
+ return evidence;
96
+ };
97
+ /** Build deterministic Evidence v2 from an immutable artifact subject. */
98
+ export const createEvidence = (target, provider, observation) => {
99
+ const subject = target === undefined
100
+ ? null
101
+ : {
102
+ name: target.path.split("/").at(-1) ?? target.path,
103
+ digest: { sha256: target.sha256 },
104
+ format: target.format,
105
+ architecture: target.architecture ?? null,
106
+ local_path: target.path,
107
+ };
108
+ const semantic = {
109
+ schema_version: 2,
110
+ subject: subject === null
111
+ ? null
112
+ : {
113
+ digest: subject.digest,
114
+ format: subject.format,
115
+ architecture: subject.architecture,
116
+ },
117
+ provider: {
118
+ id: provider.id,
119
+ name: provider.name,
120
+ version: provider.version,
121
+ },
122
+ predicate_type: observation.predicateType ?? "rea.analysis/v2",
123
+ operation: observation.operation,
124
+ parameters: observation.parameters,
125
+ result: observation.result,
126
+ confidence: observation.confidence ?? "observed",
127
+ authority: observation.authority ?? "shipped-artifact",
128
+ environment: observation.environment ?? null,
129
+ limitations: [
130
+ ...(target === undefined
131
+ ? ["Artifact identity is unavailable for this observation."]
132
+ : []),
133
+ ...(observation.limitations ?? []),
134
+ ],
135
+ locations: [...(observation.locations ?? [])],
136
+ evidence_links: [...(observation.evidenceLinks ?? [])],
137
+ };
138
+ return evidenceSchema.parse({
139
+ ...semantic,
140
+ evidence_id: `ev_${sha256(canonicalJson(semantic))}`,
141
+ subject,
142
+ raw_payload_sha256: observation.redactedRawPayload === undefined
143
+ ? null
144
+ : sha256(canonicalJson(observation.redactedRawPayload)),
145
+ });
146
+ };
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { evidenceSchema } from "./evidence.js";
3
+ export const evidenceBundleSchema = z.object({
4
+ bundle_version: z.literal(1),
5
+ records: z.array(evidenceSchema),
6
+ });
7
+ /** Project records into a deterministic bundle whose order has no semantics. */
8
+ export const createEvidenceBundle = (records) => ({
9
+ bundle_version: 1,
10
+ records: [...records].sort((left, right) => left.evidence_id.localeCompare(right.evidence_id)),
11
+ });