rea-agents 1.1.0 → 1.2.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 (48) hide show
  1. package/README.md +21 -2
  2. package/dist/application/ArtifactInventory.js +80 -44
  3. package/dist/application/CommandShimReplay.js +136 -0
  4. package/dist/application/EvidenceBundleFiles.js +9 -1
  5. package/dist/application/ProcessCaptureAuthority.js +25 -0
  6. package/dist/application/ProcessCaptureCapability.js +23 -0
  7. package/dist/application/ProcessCaptureError.js +5 -0
  8. package/dist/application/ProcessCaptureLifecycle.js +269 -0
  9. package/dist/application/ProcessCheckpoints.js +129 -0
  10. package/dist/application/ProcessCli.js +61 -0
  11. package/dist/application/ProcessEvidence.js +38 -0
  12. package/dist/application/ProcessHarness.js +252 -272
  13. package/dist/application/ProcessNormalization.js +10 -1
  14. package/dist/application/ProcessOwnership.js +39 -1
  15. package/dist/application/ProcessSampling.js +39 -23
  16. package/dist/application/Setup.js +51 -76
  17. package/dist/application/SetupSkill.js +44 -0
  18. package/dist/application/TerminalRenderer.js +92 -0
  19. package/dist/application/runtime.js +1 -1
  20. package/dist/artifacts/ArtifactProvider.js +18 -6
  21. package/dist/artifacts/ArtifactReader.js +3 -1
  22. package/dist/artifacts/AsarArtifactReader.js +8 -1
  23. package/dist/artifacts/DirectoryArtifactReader.js +1 -0
  24. package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
  25. package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
  26. package/dist/artifacts/ZipArtifactReader.js +1 -0
  27. package/dist/cli.js +29 -24
  28. package/dist/cliProcessCommands.js +19 -0
  29. package/dist/config.js +2 -0
  30. package/dist/contracts/artifactToolContracts.js +1 -0
  31. package/dist/contracts/investigationExamples.js +3 -3
  32. package/dist/contracts/processCaptureExample.js +50 -7
  33. package/dist/contracts/toolContracts.js +3 -2
  34. package/dist/domain/changedBehavior.js +2 -2
  35. package/dist/domain/errors.js +11 -3
  36. package/dist/domain/processCapture.js +99 -293
  37. package/dist/domain/processCaptureValidation.js +124 -0
  38. package/dist/domain/processComparison.js +176 -32
  39. package/dist/domain/processScenario.js +370 -0
  40. package/dist/domain/reconstructionVerification.js +3 -3
  41. package/dist/domain/staticRuntimeCorrelation.js +3 -2
  42. package/dist/identity.js +1 -0
  43. package/dist/server/registerProcessComparisonTool.js +28 -8
  44. package/dist/server/registerSessionTools.js +3 -25
  45. package/dist/server/sessionToolPolicies.js +1 -6
  46. package/package.json +5 -1
  47. package/scripts/prepare-node-pty.mjs +35 -0
  48. package/skills/rea-analysis/SKILL.md +19 -4
@@ -0,0 +1,151 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { parse } from "plist";
7
+ import { z } from "zod";
8
+ import { ArtifactReaderFailure, } from "./ArtifactReader.js";
9
+ import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
10
+ const execFileAsync = promisify(execFile);
11
+ const attachOutputSchema = z.object({
12
+ "system-entities": z.array(z.object({
13
+ "dev-entry": z.string().startsWith("/dev/"),
14
+ "mount-point": z.string().optional(),
15
+ })),
16
+ });
17
+ const systemHost = {
18
+ async run(arguments_, signal) {
19
+ try {
20
+ const { stdout } = await execFileAsync("/usr/bin/hdiutil", [...arguments_], {
21
+ encoding: "utf8",
22
+ timeout: 120_000,
23
+ ...(signal === undefined ? {} : { signal }),
24
+ });
25
+ return { stdout, exitCode: 0 };
26
+ }
27
+ catch (cause) {
28
+ if (cause instanceof Error && cause.name === "AbortError")
29
+ throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled");
30
+ throw new ArtifactReaderFailure("format", "hdiutil rejected the DMG", {
31
+ cause,
32
+ });
33
+ }
34
+ },
35
+ };
36
+ /** Read-only macOS DMG adapter that owns attachment and reverse-order detach. */
37
+ export class NativeDmgArtifactReader {
38
+ path;
39
+ host;
40
+ format = "file";
41
+ #provenance = [];
42
+ #directory;
43
+ #devices = [];
44
+ #mountRoot;
45
+ constructor(path, host) {
46
+ this.path = path;
47
+ this.host = host;
48
+ }
49
+ /** Verify and attach one image beneath an exclusively owned temporary root. */
50
+ static async create(path, signal, host = systemHost) {
51
+ if (process.platform !== "darwin")
52
+ throw new ArtifactReaderFailure("unavailable", "Native DMG traversal is available only on macOS");
53
+ const reader = new NativeDmgArtifactReader(path, host);
54
+ await reader.attach(signal);
55
+ return reader;
56
+ }
57
+ async *entries(signal) {
58
+ if (this.#directory === undefined)
59
+ throw new ArtifactReaderFailure("unavailable", "DMG is not attached");
60
+ const prefix = basename(this.path);
61
+ for await (const entry of this.#directory.entries(signal))
62
+ yield { ...entry, path: `${prefix}/${entry.path}` };
63
+ }
64
+ open(entry, signal) {
65
+ if (this.#directory === undefined)
66
+ return Promise.reject(new ArtifactReaderFailure("unavailable", "DMG is not attached"));
67
+ return this.#directory.open(entry, signal);
68
+ }
69
+ provenance() {
70
+ return this.#provenance;
71
+ }
72
+ async close() {
73
+ let detachFailure;
74
+ for (const device of [...this.#devices].reverse()) {
75
+ try {
76
+ await runChecked(this.host, ["detach", device]);
77
+ this.#provenance.push(command(["detach", device], ["mount"]));
78
+ }
79
+ catch (cause) {
80
+ detachFailure ??= cause;
81
+ }
82
+ }
83
+ this.#devices = [];
84
+ if (this.#mountRoot !== undefined)
85
+ await rm(this.#mountRoot, { recursive: true, force: true }).catch((cause) => {
86
+ detachFailure ??= cause;
87
+ });
88
+ if (detachFailure !== undefined)
89
+ throw new ArtifactReaderFailure("unavailable", "DMG detach or mount-root cleanup failed", { cause: detachFailure });
90
+ }
91
+ async attach(signal) {
92
+ await runChecked(this.host, ["verify", this.path], signal);
93
+ this.#provenance.push(command(["verify", this.path], ["read"]));
94
+ this.#mountRoot = await mkdtemp(join(tmpdir(), "rea-dmg-"));
95
+ try {
96
+ const attached = await runChecked(this.host, [
97
+ "attach",
98
+ "-readonly",
99
+ "-nobrowse",
100
+ "-plist",
101
+ "-mountroot",
102
+ this.#mountRoot,
103
+ this.path,
104
+ ], signal);
105
+ const parsed = attachOutputSchema.parse(parse(attached.stdout));
106
+ this.#devices = parsed["system-entities"].map((entity) => entity["dev-entry"]);
107
+ if (this.#devices.length === 0)
108
+ throw new ArtifactReaderFailure("format", "hdiutil returned no attached devices");
109
+ for (const entity of parsed["system-entities"])
110
+ if (entity["mount-point"] !== undefined &&
111
+ !entity["mount-point"].startsWith(`${this.#mountRoot}/`))
112
+ throw new ArtifactReaderFailure("path", "hdiutil mounted outside the owned root");
113
+ this.#provenance.push(command([
114
+ "attach",
115
+ "-readonly",
116
+ "-nobrowse",
117
+ "-plist",
118
+ "-mountroot",
119
+ this.#mountRoot,
120
+ this.path,
121
+ ], ["read", "mount"]));
122
+ this.#directory = new DirectoryArtifactReader(this.#mountRoot);
123
+ }
124
+ catch (cause) {
125
+ let cleanupFailure;
126
+ try {
127
+ await this.close();
128
+ }
129
+ catch (cleanupCause) {
130
+ cleanupFailure = cleanupCause;
131
+ }
132
+ if (cleanupFailure !== undefined)
133
+ throw new ArtifactReaderFailure("unavailable", "DMG attach failed and cleanup could not detach every device", { cause: new AggregateError([cause, cleanupFailure]) });
134
+ throw cause;
135
+ }
136
+ }
137
+ }
138
+ const runChecked = async (host, arguments_, signal) => {
139
+ const result = await host.run(arguments_, signal);
140
+ if (result.exitCode !== 0)
141
+ throw new ArtifactReaderFailure("unavailable", `hdiutil ${arguments_[0] ?? "operation"} failed with a non-zero exit code`);
142
+ return { stdout: result.stdout, exitCode: 0 };
143
+ };
144
+ const command = (arguments_, effects) => ({
145
+ tool: "/usr/bin/hdiutil",
146
+ arguments: [...arguments_],
147
+ tool_version: null,
148
+ executable_sha256: null,
149
+ exit_code: 0,
150
+ effects,
151
+ });
@@ -55,6 +55,7 @@ export class ZipArtifactReader {
55
55
  encrypted: entry.encrypted,
56
56
  byteOffset: null,
57
57
  declaredSha256: null,
58
+ unpacked: false,
58
59
  limitations: symlink ? ["Archive symlink target was not read."] : [],
59
60
  adapterKey: entry.filename,
60
61
  };
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { logCliCommand } from "./cliLogging.js";
12
12
  import { parseConfig } from "./config.js";
13
13
  import { importReferenceSource } from "./application/ReferenceSourceImport.js";
14
14
  import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
15
+ import { registerProcessCommands } from "./cliProcessCommands.js";
15
16
  /**
16
17
  * Build the one-shot Incur CLI without starting Hopper at import time.
17
18
  * Analysis commands acquire and close their own sessions; bare `mcp` and
@@ -38,6 +39,7 @@ export const createCli = () => {
38
39
  ],
39
40
  },
40
41
  });
42
+ registerSetupCommands(cli, logger);
41
43
  registerCoreCommands(cli, logger);
42
44
  registerFunctionCommand(cli, logger);
43
45
  registerSearchCommand(cli, logger);
@@ -48,6 +50,7 @@ export const createCli = () => {
48
50
  registerArtifactCommands(cli, logger);
49
51
  registerEvidenceCommands(cli, logger);
50
52
  registerReferenceSourceCommand(cli, logger);
53
+ registerProcessCommands(cli, logger);
51
54
  return cli;
52
55
  };
53
56
  const registerCoreCommands = (cli, logger) => {
@@ -55,6 +58,32 @@ const registerCoreCommands = (cli, logger) => {
55
58
  detail: z.enum(["concise", "detailed"]).default("concise"),
56
59
  limit: z.number().int().min(1).max(50).default(10),
57
60
  });
61
+ cli.command("analyze", {
62
+ description: "Get an overview of an app",
63
+ args: z.object({
64
+ path: z.string().describe("App, program, or Hopper database path"),
65
+ }),
66
+ options: overviewOptions,
67
+ run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
68
+ });
69
+ cli.command("inspect", {
70
+ description: "Inspect an app overview with evidence",
71
+ args: z.object({
72
+ path: z.string().describe("App, program, or Hopper database path"),
73
+ }),
74
+ options: overviewOptions,
75
+ run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
76
+ });
77
+ cli.command("decompile", {
78
+ description: "Read one part of an app as code",
79
+ args: z.object({
80
+ path: z.string().describe("App or program path"),
81
+ address: z.string().describe("Procedure address"),
82
+ }),
83
+ run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
84
+ });
85
+ };
86
+ const registerSetupCommands = (cli, logger) => {
58
87
  cli.command("setup", {
59
88
  description: "Install requirements and configure coding agents",
60
89
  options: z.object({
@@ -98,30 +127,6 @@ const registerCoreCommands = (cli, logger) => {
98
127
  description: "Upgrade a global npm installation to the latest REA release",
99
128
  run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(process.env.REA_PACKAGE_VERSION ?? "0.0.0-development", systemUpgradeHost(), formatExplicit ? "structured" : "human")),
100
129
  });
101
- cli.command("analyze", {
102
- description: "Get an overview of an app",
103
- args: z.object({
104
- path: z.string().describe("App, program, or Hopper database path"),
105
- }),
106
- options: overviewOptions,
107
- run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
108
- });
109
- cli.command("inspect", {
110
- description: "Inspect an app overview with evidence",
111
- args: z.object({
112
- path: z.string().describe("App, program, or Hopper database path"),
113
- }),
114
- options: overviewOptions,
115
- run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
116
- });
117
- cli.command("decompile", {
118
- description: "Read one part of an app as code",
119
- args: z.object({
120
- path: z.string().describe("App or program path"),
121
- address: z.string().describe("Procedure address"),
122
- }),
123
- run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
124
- });
125
130
  };
126
131
  const confirmSetup = async (actions) => {
127
132
  process.stdout.write("\nREA setup plan\n");
@@ -0,0 +1,19 @@
1
+ import { Cli, z } from "incur";
2
+ import { captureProcessScenarioFile, compareProcessEvidenceFiles, } from "./application/ProcessCli.js";
3
+ import { logCliCommand } from "./cliLogging.js";
4
+ /** Register Process Capture v4 one-shot commands through shared application services. */
5
+ export const registerProcessCommands = (cli, logger) => {
6
+ cli.command("capture-process", {
7
+ description: "Capture one approved Process Capture v4 JSON scenario",
8
+ args: z.object({ scenario: z.string().describe("Scenario JSON path") }),
9
+ run: ({ args }) => logCliCommand(logger, "capture-process", () => captureProcessScenarioFile(args.scenario)),
10
+ });
11
+ cli.command("compare-process-captures", {
12
+ description: "Compare two Process Capture v4 Evidence JSON files",
13
+ args: z.object({
14
+ left: z.string().describe("Left capture Evidence JSON path"),
15
+ right: z.string().describe("Right capture Evidence JSON path"),
16
+ }),
17
+ run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => compareProcessEvidenceFiles(args.left, args.right)),
18
+ });
19
+ };
package/dist/config.js CHANGED
@@ -14,6 +14,7 @@ const environmentSchema = z.object({
14
14
  .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
15
15
  .default("info"),
16
16
  REA_PROCESS_CAPTURE_ENABLED: z.enum(["true", "false"]).default("false"),
17
+ REA_ARTIFACT_NATIVE_MOUNT_ENABLED: z.enum(["true", "false"]).default("false"),
17
18
  REA_PROCESS_ALLOW_EXTERNAL_NETWORK: z
18
19
  .enum(["true", "false"])
19
20
  .default("false"),
@@ -101,6 +102,7 @@ export const parseConfig = (environment) => {
101
102
  allowedEnvironment: allowedEnvironment.value,
102
103
  allowExternalNetwork: parsedEnvironment.data.REA_PROCESS_ALLOW_EXTERNAL_NETWORK === "true",
103
104
  },
105
+ artifactNativeMountEnabled: parsedEnvironment.data.REA_ARTIFACT_NATIVE_MOUNT_ENABLED === "true",
104
106
  evidenceFilePolicy: {
105
107
  roots: evidenceRoots.value,
106
108
  maxBytes: 64 * 1024 * 1024,
@@ -29,6 +29,7 @@ const traversalLimitsInput = {
29
29
  };
30
30
  /** Exact caller boundary for deterministic artifact inventory. */
31
31
  export const artifactInventoryInputSchema = z.object({
32
+ native_mount_approved: z.boolean().default(false),
32
33
  ...pageInput,
33
34
  ...traversalLimitsInput,
34
35
  });
@@ -30,11 +30,11 @@ export const FUNCTION_COMPARISON_EVIDENCE = createEvidence(undefined, {
30
30
  const PROCESS_PROVIDER = {
31
31
  id: "rea-process",
32
32
  name: "REA deterministic process harness",
33
- version: "1",
33
+ version: "3",
34
34
  };
35
35
  const capture = processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE);
36
36
  const captureEvidence = (scenario) => createEvidence(undefined, PROCESS_PROVIDER, {
37
- predicateType: "rea.process-capture/v2",
37
+ predicateType: "rea.process-capture/v4",
38
38
  operation: "capture_process_scenario",
39
39
  parameters: { scenario },
40
40
  result: jsonValueSchema.parse(capture),
@@ -50,7 +50,7 @@ const captureEvidence = (scenario) => createEvidence(undefined, PROCESS_PROVIDER
50
50
  export const PROCESS_CAPTURE_REFERENCE = captureEvidence("reference");
51
51
  export const PROCESS_CAPTURE_RECONSTRUCTION = captureEvidence("reconstruction");
52
52
  export const PROCESS_COMPARISON_EVIDENCE = createEvidence(undefined, PROCESS_PROVIDER, {
53
- predicateType: "rea.process-comparison/v1",
53
+ predicateType: "rea.process-comparison/v3",
54
54
  operation: "compare_process_captures",
55
55
  parameters: {
56
56
  left_evidence_id: PROCESS_CAPTURE_REFERENCE.evidence_id,
@@ -1,16 +1,59 @@
1
+ import { digestProcessCommitment } from "../domain/processCapture.js";
2
+ const normalization = {
3
+ paths: true,
4
+ pids: true,
5
+ ports: true,
6
+ time_bucket_ms: 10,
7
+ patterns: [],
8
+ };
9
+ const scenario = { executable_sha256: "0".repeat(64) };
10
+ const comparisonContract = {};
11
+ const shimPlan = [];
12
+ const replayPlan = {};
1
13
  /** Minimal valid process capture used only in public contract examples. */
2
14
  export const EMPTY_PROCESS_CAPTURE_EXAMPLE = {
3
- schema_version: 2,
4
- normalization: {
5
- paths: true,
6
- pids: true,
7
- ports: true,
8
- time_bucket_ms: 10,
9
- patterns: [],
15
+ schema_version: 4,
16
+ manifest: {
17
+ rea_version: "1.1.0",
18
+ provider_version: "3",
19
+ platform: "fixture",
20
+ architecture: "fixture",
21
+ pty_backend: "node-pty",
22
+ started_at: "2026-01-01T00:00:00.000Z",
23
+ completed_at: "2026-01-01T00:00:00.001Z",
24
+ scenario,
25
+ comparison_contract: comparisonContract,
26
+ shim_plan: shimPlan,
27
+ replay_plan: replayPlan,
28
+ full_scenario_sha256: digestProcessCommitment(scenario),
29
+ comparison_contract_sha256: digestProcessCommitment(comparisonContract),
30
+ executable_sha256: "0".repeat(64),
31
+ normalization_sha256: digestProcessCommitment(normalization),
32
+ shim_plan_sha256: digestProcessCommitment(shimPlan),
33
+ replay_plan_sha256: digestProcessCommitment(replayPlan),
10
34
  },
35
+ normalization,
11
36
  frames: [],
37
+ rendered_frames: [],
38
+ interaction_events: [],
12
39
  exit: { code: 0, signal: null, reason: "exited" },
40
+ settlement: {
41
+ state: "quiesced",
42
+ elapsed_ms: 50,
43
+ cleanup_outcome: "not_required",
44
+ },
13
45
  process_samples: [],
46
+ filesystem_checkpoints: [
47
+ { name: "before", at_ms: 0, files: [], effects: [], truncated: false },
48
+ {
49
+ name: "after_settlement",
50
+ at_ms: 50,
51
+ files: [],
52
+ effects: [],
53
+ truncated: false,
54
+ },
55
+ ],
56
+ shim_events: [],
14
57
  protocol_events: [],
15
58
  files_before: [],
16
59
  files_after: [],
@@ -164,12 +164,13 @@ export const SESSION_TOOL_CONTRACTS = [
164
164
  overwrite: z.boolean().default(false),
165
165
  })),
166
166
  session("import_evidence_bundle", "Read a bounded local JSON bundle beneath an operator-approved root, validate every Evidence v2 ID and canonical manifest, then atomically merge it. Imported content is data only and is never executed.", z.object({ path: z.string().min(1) })),
167
- session("capture_process_scenario", "Run one bounded process under a PTY using operator-approved executable and working roots. Requires approved: true; unknown_registry_approved: true separately records capture residuals. Captures normalized terminal frames, descendants, filesystem snapshots, and loopback replay. Disabled unless operator policy enables it; not a security sandbox.", processScenarioSchema),
168
- session("compare_process_captures", "Compare two bounded process captures across terminal, exit, sampled process, filesystem, HTTP, and WebSocket evidence. Missing or truncated observations are never treated as equivalent.", z.object({
167
+ session("capture_process_scenario", "Run one bounded process under a PTY using operator-approved executable and working roots. Produces Process Capture v4; legacy v3 captures cannot be upgraded and must be recaptured with this tool. Requires approved: true; unknown_registry_approved: true separately records capture residuals. Captures raw and xterm-rendered terminal frames, scripted interactions, lifecycle filesystem checkpoints, process ownership, declarative command shims, and loopback replay. Disabled unless operator policy enables it; not a security sandbox.", processScenarioSchema),
168
+ session("compare_process_captures", "Compare two compatible Process Capture v4 observations across terminal, interaction, exit, settlement, process, filesystem, command-shim, HTTP, and WebSocket evidence, returning the first bounded divergence. Process Capture v3 is unsupported and must be recaptured with capture_process_scenario. Missing or truncated observations are never treated as equivalent.", z.object({
169
169
  left_evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u),
170
170
  left: processCaptureSchema,
171
171
  right_evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u),
172
172
  right: processCaptureSchema,
173
+ max_capture_age_ms: z.number().int().nonnegative().optional(),
173
174
  unknown_registry_approved: z
174
175
  .literal(true)
175
176
  .optional()
@@ -51,10 +51,10 @@ export const changedBehaviorResultSchema = z.object({
51
51
  });
52
52
  const EXPECTED_PROVIDERS = {
53
53
  compare_process_captures: {
54
- predicate: "rea.process-comparison/v1",
54
+ predicate: "rea.process-comparison/v3",
55
55
  id: "rea-process",
56
56
  name: "REA deterministic process harness",
57
- version: "1",
57
+ version: "3",
58
58
  },
59
59
  compare_artifacts: {
60
60
  predicate: "rea.artifact-comparison/v1",
@@ -111,11 +111,15 @@ export class ProviderAdapterError extends AnalysisError {
111
111
  export class ArtifactOperationError extends AnalysisError {
112
112
  operation;
113
113
  reason;
114
+ artifactDetails;
114
115
  _tag = "ArtifactOperationError";
115
- constructor(operation, reason) {
116
- super(`Artifact ${operation} failed: ${reason}`);
116
+ constructor(operation, reason, artifactDetails) {
117
+ super(artifactDetails === undefined
118
+ ? `Artifact ${operation} failed: ${reason}`
119
+ : `Artifact ${operation} failed: ${reason} at ${artifactDetails.logicalPath} (declared_sha256=${artifactDetails.declaredSha256 ?? "unavailable"}, calculated_sha256=${artifactDetails.calculatedSha256 ?? "unavailable"}, unpacked=${String(artifactDetails.unpacked)})`);
117
120
  this.operation = operation;
118
121
  this.reason = reason;
122
+ this.artifactDetails = artifactDetails;
119
123
  }
120
124
  }
121
125
  /** Evidence identity, schema, or bundle manifests failed integrity checks. */
@@ -251,7 +255,11 @@ const safeDetails = (error) => {
251
255
  if (error instanceof ProviderAdapterError)
252
256
  return { providerId: error.providerId, operation: error.operation };
253
257
  if (error instanceof ArtifactOperationError)
254
- return { operation: error.operation, reason: error.reason };
258
+ return {
259
+ operation: error.operation,
260
+ reason: error.reason,
261
+ ...error.artifactDetails,
262
+ };
255
263
  if (error instanceof EvidenceLimitError)
256
264
  return { limit: error.limit, maximum: error.maximum };
257
265
  if (error instanceof EvidenceFileError)