rea-agents 1.3.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.
Files changed (72) hide show
  1. package/README.md +21 -0
  2. package/bridge/hopper_bridge.py +12 -3
  3. package/dist/application/ArtifactGraphConstruction.js +1 -1
  4. package/dist/application/ArtifactInventory.js +98 -26
  5. package/dist/application/AuthorizedArtifactInventory.js +2 -2
  6. package/dist/application/BinarySession.js +83 -0
  7. package/dist/application/CapabilityInventory.js +127 -0
  8. package/dist/application/ClientRegistrationStatus.js +69 -0
  9. package/dist/application/CrossVersionInventory.js +19 -8
  10. package/dist/application/CrossVersionInvestigation.js +68 -5
  11. package/dist/application/DirectAnalysis.js +97 -9
  12. package/dist/application/Doctor.js +74 -2
  13. package/dist/application/PermissionAuthority.js +183 -0
  14. package/dist/application/PermissionConfiguration.js +26 -0
  15. package/dist/application/ProcessCaptureError.js +13 -0
  16. package/dist/application/ProcessCaptureLifecycle.js +3 -1
  17. package/dist/application/ProcessCli.js +23 -10
  18. package/dist/application/ProcessHarness.js +4 -1
  19. package/dist/application/ProgressReporter.js +30 -0
  20. package/dist/application/ProjectPermissionStore.js +110 -0
  21. package/dist/application/Setup.js +2 -41
  22. package/dist/application/SupportedClients.js +41 -0
  23. package/dist/application/Uninstall.js +1 -1
  24. package/dist/application/Upgrade.js +2 -0
  25. package/dist/application/runtime.js +1 -1
  26. package/dist/artifacts/ArtifactProvider.js +13 -3
  27. package/dist/catalogIdentity.js +121 -0
  28. package/dist/cli.js +46 -3
  29. package/dist/cliEvidenceCommands.js +42 -1
  30. package/dist/cliInvestigationCommands.js +57 -6
  31. package/dist/cliLogging.js +20 -3
  32. package/dist/cliPolicyCommands.js +115 -0
  33. package/dist/cliProcessCommands.js +4 -14
  34. package/dist/config.js +63 -3
  35. package/dist/contracts/artifactToolContracts.js +14 -1
  36. package/dist/contracts/errorSchemas.js +98 -0
  37. package/dist/contracts/toolContracts.js +10 -1
  38. package/dist/contracts/toolOutputSchemas.js +62 -30
  39. package/dist/domain/artifactComparison.js +38 -11
  40. package/dist/domain/artifactGraph.js +25 -1
  41. package/dist/domain/artifactInventoryEvidence.js +18 -0
  42. package/dist/domain/changedBehavior.js +5 -3
  43. package/dist/domain/errors.js +216 -3
  44. package/dist/domain/investigationWorkspace.js +131 -4
  45. package/dist/domain/permissionPolicy.js +157 -0
  46. package/dist/domain/processComparison.js +1 -0
  47. package/dist/domain/reconstructionVerification.js +1 -1
  48. package/dist/domain/reconstructionVerificationSchemas.js +1 -0
  49. package/dist/domain/staticRuntimeCorrelation.js +5 -1
  50. package/dist/generatedPackageMetadata.js +9 -0
  51. package/dist/hopper/BridgeLauncher.js +7 -3
  52. package/dist/hopper/HopperClient.js +32 -5
  53. package/dist/identity.js +10 -3
  54. package/dist/logger.js +2 -2
  55. package/dist/main.js +95 -7
  56. package/dist/server/createServer.js +52 -5
  57. package/dist/server/mcpProgress.js +23 -0
  58. package/dist/server/registerArtifactComparisonTool.js +6 -2
  59. package/dist/server/registerBundleComparisonTool.js +6 -2
  60. package/dist/server/registerEnhancedTools.js +18 -1
  61. package/dist/server/registerEvidenceResources.js +302 -0
  62. package/dist/server/registerEvidenceTools.js +58 -1
  63. package/dist/server/registerFunctionComparisonTool.js +6 -2
  64. package/dist/server/registerInvestigationTools.js +83 -10
  65. package/dist/server/registerOfficialTools.js +19 -1
  66. package/dist/server/registerProcessComparisonTool.js +7 -3
  67. package/dist/server/registerSessionStatusTool.js +44 -3
  68. package/dist/server/registerSessionTools.js +129 -7
  69. package/dist/server/runDerivedOperation.js +35 -0
  70. package/dist/server/toolResult.js +27 -3
  71. package/dist/serverIdentity.js +70 -0
  72. package/package.json +6 -4
@@ -1,5 +1,5 @@
1
1
  import { hopperStartupFailure, } from "./hopperStartupFailure.js";
2
- export { hopperStartupFailure } from "./hopperStartupFailure.js";
2
+ export { hopperStartupFailure };
3
3
  /** Stable tags exposed by safe analysis-error projections. */
4
4
  const ANALYSIS_ERROR_TAGS = [
5
5
  "AnalysisProtocolError",
@@ -26,11 +26,14 @@ const ANALYSIS_ERROR_TAGS = [
26
26
  "ConfigurationError",
27
27
  "NoBinaryOpenError",
28
28
  "BinaryTargetError",
29
+ "PermissionRequiredError",
29
30
  ];
30
31
  /** Base class for expected analysis, provider, and session failures. */
31
32
  export class AnalysisError extends Error {
32
33
  userMessage = undefined;
33
34
  userCategory = undefined;
35
+ cleanupIncomplete = false;
36
+ cleanupResources = [];
34
37
  }
35
38
  /** Base class for failures produced specifically by the Hopper provider. */
36
39
  export class HopperError extends AnalysisError {
@@ -247,17 +250,224 @@ export class BinaryTargetError extends AnalysisError {
247
250
  this.path = path;
248
251
  }
249
252
  }
253
+ /** Exact denied authority used by both CLI and MCP remediation. */
254
+ export class PermissionRequiredError extends AnalysisError {
255
+ requested;
256
+ missing;
257
+ ceiling;
258
+ elicitationSupported;
259
+ restartRequired;
260
+ _tag = "PermissionRequiredError";
261
+ constructor(requested, missing, ceiling, elicitationSupported, restartRequired) {
262
+ super(`Permission required for ${requested.capability}`);
263
+ this.requested = requested;
264
+ this.missing = missing;
265
+ this.ceiling = ceiling;
266
+ this.elicitationSupported = elicitationSupported;
267
+ this.restartRequired = restartRequired;
268
+ }
269
+ }
250
270
  /** Project expected failures into exhaustive, secret-safe caller fields. */
251
271
  export const projectAnalysisError = (error) => {
252
272
  assertKnownTag(error._tag);
253
- const code = error instanceof HopperProcessError ? error.failureCode : undefined;
273
+ const code = errorCode(error);
274
+ const details = errorDetails(error);
254
275
  return {
276
+ code,
255
277
  category: errorCategory(error),
256
278
  message: userMessage(error),
257
- ...(code === undefined ? {} : { code }),
279
+ retryable: RETRYABLE_CODES.has(code),
280
+ remediation: {
281
+ action: remediationAction(error),
282
+ restart_required: error instanceof PermissionRequiredError && error.restartRequired,
283
+ ...(error instanceof PermissionRequiredError
284
+ ? { elicitation_supported: error.elicitationSupported }
285
+ : {}),
286
+ },
287
+ ...(details === undefined ? {} : { details }),
258
288
  };
259
289
  };
290
+ const errorCode = (error) => {
291
+ if (error instanceof PermissionRequiredError)
292
+ return "permission_required";
293
+ if (error instanceof ArtifactOperationError)
294
+ return error.reason === "integrity" && error.artifactDetails !== undefined
295
+ ? "artifact_integrity_mismatch"
296
+ : error.reason === "limit"
297
+ ? "truncated"
298
+ : error.reason === "cancelled"
299
+ ? "cancelled"
300
+ : "artifact_operation_failed";
301
+ if (error instanceof EvidenceFileError)
302
+ return error.reason === "outside-root"
303
+ ? "outside_approved_root"
304
+ : error.reason === "too-large"
305
+ ? "truncated"
306
+ : error.reason === "disabled"
307
+ ? "capability_unavailable"
308
+ : "execution_failure";
309
+ if (error instanceof InvestigationWorkspaceError) {
310
+ if (error.reason === "revision-conflict" ||
311
+ error.reason === "name-conflict")
312
+ return "revision_conflict";
313
+ if (error.reason === "too-large")
314
+ return "truncated";
315
+ if (error.reason === "disabled")
316
+ return "capability_unavailable";
317
+ if (error.reason === "outside-root")
318
+ return "outside_approved_root";
319
+ if (error.reason === "integrity" || error.reason === "invalid-json")
320
+ return "evidence_integrity_mismatch";
321
+ return "execution_failure";
322
+ }
323
+ if (error instanceof UnknownRegistryError)
324
+ return error.reason === "revision-conflict" ||
325
+ error.reason === "already-exists"
326
+ ? "revision_conflict"
327
+ : error.reason === "limit"
328
+ ? "truncated"
329
+ : error.reason === "integrity"
330
+ ? "evidence_integrity_mismatch"
331
+ : "execution_failure";
332
+ if (error._tag === "ProcessCaptureError")
333
+ return error.cleanupIncomplete
334
+ ? "cleanup_incomplete"
335
+ : error.userCategory === "cancelled"
336
+ ? "cancelled"
337
+ : error.userCategory === "permission_required"
338
+ ? "permission_required"
339
+ : "process_capture_failed";
340
+ switch (error._tag) {
341
+ case "AnalysisProtocolError":
342
+ case "AnalysisOutputError":
343
+ case "HopperProtocolError":
344
+ return "unreadable_output";
345
+ case "AnalysisInputError":
346
+ return "invalid_request";
347
+ case "AnalysisCapabilityUnavailableError":
348
+ case "ProviderSelectionError":
349
+ return "capability_unavailable";
350
+ case "AnalysisCancelledError":
351
+ case "HopperCancelledError":
352
+ return "cancelled";
353
+ case "AnalysisTimeoutError":
354
+ case "HopperTimeoutError":
355
+ return "provider_timeout";
356
+ case "HopperProcessError":
357
+ case "HopperStartError":
358
+ return "provider_unavailable";
359
+ case "ConfigurationError":
360
+ return "configuration_invalid";
361
+ case "NoBinaryOpenError":
362
+ case "BinaryTargetError":
363
+ return "target_unavailable";
364
+ case "EvidenceIntegrityError":
365
+ return "evidence_integrity_mismatch";
366
+ case "EvidenceLimitError":
367
+ return "truncated";
368
+ case "ProviderAdapterError":
369
+ case "HopperRemoteError":
370
+ return "execution_failure";
371
+ case "ArtifactOperationError":
372
+ case "EvidenceFileError":
373
+ case "InvestigationWorkspaceError":
374
+ case "UnknownRegistryError":
375
+ case "PermissionRequiredError":
376
+ throw new TypeError("Unhandled specialized analysis error");
377
+ }
378
+ };
379
+ const errorDetails = (error) => {
380
+ if (error instanceof PermissionRequiredError)
381
+ return {
382
+ capability: error.requested.capability,
383
+ requested: scopeDetails(error.requested),
384
+ missing: missingScopeDetails(error.missing),
385
+ ceiling: error.ceiling === null ? null : scopeDetails(error.ceiling),
386
+ };
387
+ if (error instanceof ArtifactOperationError && error.artifactDetails)
388
+ return {
389
+ logical_path: error.artifactDetails.logicalPath,
390
+ declared_sha256: error.artifactDetails.declaredSha256,
391
+ calculated_sha256: error.artifactDetails.calculatedSha256,
392
+ unpacked: error.artifactDetails.unpacked,
393
+ };
394
+ if (error instanceof ArtifactOperationError)
395
+ return {
396
+ operation: error.operation,
397
+ reason: error.reason,
398
+ ...(error.reason === "limit" ? { truncated: true } : {}),
399
+ };
400
+ if (error instanceof AnalysisCancelledError)
401
+ return { operation: error.operation, cleanup: "complete" };
402
+ if (error instanceof HopperCancelledError)
403
+ return { operation: "hopper", cleanup: "complete" };
404
+ if (error instanceof AnalysisTimeoutError)
405
+ return { operation: error.operation, timeout_ms: error.timeoutMs };
406
+ if (error instanceof HopperTimeoutError)
407
+ return { operation: "hopper", timeout_ms: error.timeoutMs };
408
+ if (error instanceof EvidenceLimitError)
409
+ return { limit: error.limit, maximum: error.maximum, truncated: true };
410
+ if (error instanceof InvestigationWorkspaceError)
411
+ return { operation: error.operation, reason: error.reason };
412
+ if (error instanceof UnknownRegistryError)
413
+ return { reason: error.reason };
414
+ if (error instanceof EvidenceFileError)
415
+ return { operation: error.operation, reason: error.reason };
416
+ if (error instanceof AnalysisCapabilityUnavailableError)
417
+ return { provider_id: error.providerId, operation: error.operation };
418
+ if (error instanceof ProviderAdapterError)
419
+ return { provider_id: error.providerId, operation: error.operation };
420
+ if (error instanceof HopperRemoteError)
421
+ return { provider_code: error.code, diagnostic_type: error.diagnosticType };
422
+ if (error instanceof HopperProcessError && error.failureCode !== undefined)
423
+ return { failure_code: error.failureCode, exit_code: error.exitCode };
424
+ if (error._tag === "ProcessCaptureError" && error.cleanupIncomplete)
425
+ return {
426
+ cleanup: "incomplete",
427
+ resources: [...error.cleanupResources],
428
+ };
429
+ if (error._tag === "ProcessCaptureError" &&
430
+ error.userCategory === "cancelled")
431
+ return { operation: "process_capture", cleanup: "complete" };
432
+ if (error instanceof BinaryTargetError)
433
+ return { path: error.path };
434
+ return undefined;
435
+ };
436
+ const scopeDetails = (scope) => ({
437
+ capability: scope.capability,
438
+ roots: [...scope.roots],
439
+ executables: [...scope.executables],
440
+ environment_names: [...scope.environment_names],
441
+ network: scope.network,
442
+ mount: scope.mount,
443
+ });
444
+ const missingScopeDetails = (scope) => ({
445
+ ...(scope.roots === undefined ? {} : { roots: [...scope.roots] }),
446
+ ...(scope.executables === undefined
447
+ ? {}
448
+ : { executables: [...scope.executables] }),
449
+ ...(scope.environment_names === undefined
450
+ ? {}
451
+ : { environment_names: [...scope.environment_names] }),
452
+ ...(scope.network === undefined ? {} : { network: scope.network }),
453
+ ...(scope.mount === undefined ? {} : { mount: scope.mount }),
454
+ });
455
+ const RETRYABLE_CODES = new Set([
456
+ "provider_timeout",
457
+ "cancelled",
458
+ "revision_conflict",
459
+ "provider_unavailable",
460
+ ]);
461
+ const remediationAction = (error) => {
462
+ if (error instanceof PermissionRequiredError)
463
+ return error.elicitationSupported
464
+ ? "Approve the exact missing scope, then retry the operation."
465
+ : "Add the exact missing scope beneath the administrator ceiling, then retry.";
466
+ return userMessage(error);
467
+ };
260
468
  const errorCategory = (error) => {
469
+ if (error instanceof PermissionRequiredError)
470
+ return "permission_required";
261
471
  if (error._tag === "ProcessCaptureError")
262
472
  return error.userCategory ?? "execution_failure";
263
473
  if (error instanceof HopperRemoteError &&
@@ -307,6 +517,8 @@ const STATIC_ERROR_CATEGORIES = {
307
517
  BinaryTargetError: "unavailable",
308
518
  };
309
519
  const userMessage = (error) => {
520
+ if (error instanceof PermissionRequiredError)
521
+ return "This operation needs additional local permission. Review the requested scope and remediation.";
310
522
  if (error instanceof AnalysisInputError)
311
523
  return "Analysis input is invalid. Check the arguments and try again.";
312
524
  if (error.userMessage !== undefined)
@@ -441,6 +653,7 @@ const KNOWN_ERROR_TAGS = {
441
653
  ConfigurationError: true,
442
654
  NoBinaryOpenError: true,
443
655
  BinaryTargetError: true,
656
+ PermissionRequiredError: true,
444
657
  };
445
658
  const assertKnownTag = (tag) => {
446
659
  if (KNOWN_ERROR_TAGS[tag] !== true)
@@ -37,14 +37,27 @@ const DEFAULT_INVESTIGATION_RUN_OPTIONS = {
37
37
  change_limit: 500,
38
38
  };
39
39
  /** Explicit write approval and two local versions for an automatic run. */
40
- export const crossVersionInvestigationInputSchema = z.object({
40
+ export const crossVersionInvestigationInputSchema = z
41
+ .object({
41
42
  approved: z.literal(true),
42
43
  workspace_path: z.string().min(1).max(4_096),
43
44
  workspace_name: z.string().trim().min(1).max(200).default("default"),
44
45
  expected_workspace_revision: z.number().int().min(1).optional(),
45
46
  left_path: z.string().min(1).max(4_096),
46
47
  right_path: z.string().min(1).max(4_096),
48
+ integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"),
49
+ integrity_continue_approved: z.boolean().default(false),
50
+ max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
47
51
  options: investigationRunOptionsSchema.default(DEFAULT_INVESTIGATION_RUN_OPTIONS),
52
+ })
53
+ .superRefine((input, context) => {
54
+ if (input.integrity_policy === "record-and-continue" &&
55
+ input.integrity_continue_approved !== true)
56
+ context.addIssue({
57
+ code: "custom",
58
+ path: ["integrity_continue_approved"],
59
+ message: "record-and-continue requires explicit approval",
60
+ });
48
61
  });
49
62
  const targetSchema = z.object({
50
63
  root_sha256: digestSchema,
@@ -58,13 +71,16 @@ const stageSchema = z.enum([
58
71
  "compare_artifacts",
59
72
  "find_changed_behavior",
60
73
  ]);
61
- export const investigationRunSchema = z.object({
74
+ const investigationRunBaseSchema = z.object({
62
75
  schema_version: z.literal(1),
63
76
  run_id: z.string().regex(/^run_[a-f0-9]{64}$/u),
64
77
  request_sha256: digestSchema,
65
78
  left: targetSchema,
66
79
  right: targetSchema,
67
80
  options: investigationRunOptionsSchema,
81
+ integrity_policy: z.enum(["fail", "record-and-continue"]),
82
+ integrity_continue_approved: z.boolean(),
83
+ max_integrity_mismatches: z.number().int().min(1).max(100),
68
84
  status: z.enum(["running", "complete"]),
69
85
  completed_stages: z.array(stageSchema).max(4),
70
86
  left_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
@@ -73,6 +89,28 @@ export const investigationRunSchema = z.object({
73
89
  result_evidence_id: evidenceIdSchema.nullable(),
74
90
  limitations: z.array(z.string().max(4_096)).max(100),
75
91
  });
92
+ export const investigationRunSchema = z
93
+ .union([
94
+ investigationRunBaseSchema.extend({
95
+ legacy_request_identity: z.literal(true),
96
+ }),
97
+ investigationRunBaseSchema,
98
+ ])
99
+ .superRefine((run, context) => {
100
+ if (!("legacy_request_identity" in run))
101
+ return;
102
+ const legacyIdentity = createLegacyInvestigationRunIdentity(run);
103
+ if (run.integrity_policy !== "fail" ||
104
+ run.integrity_continue_approved ||
105
+ run.max_integrity_mismatches !== 10 ||
106
+ run.run_id !== legacyIdentity.runId ||
107
+ run.request_sha256 !== legacyIdentity.requestSha256)
108
+ context.addIssue({
109
+ code: "custom",
110
+ path: ["legacy_request_identity"],
111
+ message: "Legacy request identity is valid only for migrated strict runs",
112
+ });
113
+ });
76
114
  export const investigationRunSummarySchema = z.object({
77
115
  schema_version: z.literal(1),
78
116
  workspace_id: z.string().regex(/^ws_[a-f0-9]{64}$/u),
@@ -96,6 +134,18 @@ const investigationWorkspaceSchema = z.object({
96
134
  bundle: evidenceBundleSchema,
97
135
  runs: z.array(investigationRunSchema).max(1_000),
98
136
  });
137
+ const legacyInvestigationRunSchema = investigationRunBaseSchema
138
+ .omit({
139
+ integrity_policy: true,
140
+ integrity_continue_approved: true,
141
+ max_integrity_mismatches: true,
142
+ })
143
+ .strict();
144
+ const legacyInvestigationWorkspaceSchema = investigationWorkspaceSchema
145
+ .extend({
146
+ runs: z.array(legacyInvestigationRunSchema).max(1_000),
147
+ })
148
+ .strict();
99
149
  /** Derive a stable run identity from content commitments and bounded controls. */
100
150
  export const createInvestigationRunIdentity = (input) => {
101
151
  const requestSha256 = digestCanonical({
@@ -103,6 +153,11 @@ export const createInvestigationRunIdentity = (input) => {
103
153
  left: input.left,
104
154
  right: input.right,
105
155
  options: input.options,
156
+ integrity: {
157
+ policy: input.integrity_policy,
158
+ approved: input.integrity_continue_approved,
159
+ max_mismatches: input.max_integrity_mismatches,
160
+ },
106
161
  });
107
162
  return { runId: `run_${requestSha256}`, requestSha256 };
108
163
  };
@@ -126,6 +181,8 @@ export const reviseInvestigationWorkspace = (current, bundle, runs) => buildWork
126
181
  });
127
182
  /** Parse a workspace and verify identities, revision digest, and references. */
128
183
  export const parseInvestigationWorkspace = (input) => {
184
+ if (isLegacyWorkspace(input))
185
+ return migrateLegacyWorkspace(input);
129
186
  const parsed = investigationWorkspaceSchema.parse(input);
130
187
  const bundle = parseEvidenceBundle(parsed.bundle);
131
188
  const canonical = buildWorkspace({
@@ -170,8 +227,15 @@ const validateRunReferences = (workspace) => {
170
227
  const evidenceIds = new Set(workspace.bundle.records.map(({ evidence_id: id }) => id));
171
228
  for (const run of workspace.runs) {
172
229
  const expectedIdentity = createInvestigationRunIdentity(run);
173
- if (run.run_id !== expectedIdentity.runId ||
174
- run.request_sha256 !== expectedIdentity.requestSha256)
230
+ const legacyIdentity = createLegacyInvestigationRunIdentity(run);
231
+ if ((run.run_id !== expectedIdentity.runId ||
232
+ run.request_sha256 !== expectedIdentity.requestSha256) &&
233
+ (!("legacy_request_identity" in run) ||
234
+ run.integrity_policy !== "fail" ||
235
+ run.integrity_continue_approved ||
236
+ run.max_integrity_mismatches !== 10 ||
237
+ run.run_id !== legacyIdentity.runId ||
238
+ run.request_sha256 !== legacyIdentity.requestSha256))
175
239
  throw new TypeError("Investigation run identity is invalid");
176
240
  validateRunStages(run);
177
241
  const references = [
@@ -186,6 +250,69 @@ const validateRunReferences = (workspace) => {
186
250
  throw new TypeError("Investigation run references missing Evidence");
187
251
  }
188
252
  };
253
+ /** Derive the pre-integrity-policy run identity for strict workspace migration. */
254
+ export const createLegacyInvestigationRunIdentity = (input) => {
255
+ const requestSha256 = digestCanonical({
256
+ schema: "rea.cross-version-investigation-request/v1",
257
+ left: input.left,
258
+ right: input.right,
259
+ options: input.options,
260
+ });
261
+ return { runId: `run_${requestSha256}`, requestSha256 };
262
+ };
263
+ const isLegacyWorkspace = (input) => {
264
+ if (typeof input !== "object" || input === null || !("runs" in input))
265
+ return false;
266
+ const runs = input.runs;
267
+ return (Array.isArray(runs) &&
268
+ runs.some((run) => typeof run === "object" && run !== null && !("integrity_policy" in run)));
269
+ };
270
+ const migrateLegacyWorkspace = (input) => {
271
+ const parsed = legacyInvestigationWorkspaceSchema.parse(input);
272
+ const bundle = parseEvidenceBundle(parsed.bundle);
273
+ const runs = [...parsed.runs].sort((left, right) => left.run_id.localeCompare(right.run_id));
274
+ const semantic = {
275
+ workspace_version: 1,
276
+ workspace_id: parsed.workspace_id,
277
+ name: parsed.name,
278
+ revision: parsed.revision,
279
+ previous_revision_digest: parsed.previous_revision_digest,
280
+ bundle,
281
+ runs,
282
+ };
283
+ if (parsed.workspace_id !== workspaceIdFor(parsed.name))
284
+ throw new TypeError("Investigation workspace ID does not match its name");
285
+ if (parsed.revision_digest !== `wrev_${digestCanonical(semantic)}`)
286
+ throw new TypeError("Investigation workspace revision digest is invalid");
287
+ const canonical = legacyInvestigationWorkspaceSchema.parse({
288
+ ...semantic,
289
+ revision_digest: parsed.revision_digest,
290
+ });
291
+ if (JSON.stringify(parsed) !== JSON.stringify(canonical))
292
+ throw new TypeError("Investigation workspace is not canonical");
293
+ for (const run of runs) {
294
+ const identity = createLegacyInvestigationRunIdentity(run);
295
+ if (run.run_id !== identity.runId ||
296
+ run.request_sha256 !== identity.requestSha256)
297
+ throw new TypeError("Investigation run identity is invalid");
298
+ }
299
+ const migrated = buildWorkspace({
300
+ workspaceId: parsed.workspace_id,
301
+ name: parsed.name,
302
+ revision: parsed.revision,
303
+ previousRevisionDigest: parsed.previous_revision_digest,
304
+ bundle,
305
+ runs: runs.map((run) => ({
306
+ ...run,
307
+ legacy_request_identity: true,
308
+ integrity_policy: "fail",
309
+ integrity_continue_approved: false,
310
+ max_integrity_mismatches: 10,
311
+ })),
312
+ });
313
+ validateRunReferences(migrated);
314
+ return migrated;
315
+ };
189
316
  const validateRunStages = (run) => {
190
317
  const expected = [
191
318
  "inventory_left",
@@ -0,0 +1,157 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { err, ok } from "./result.js";
3
+ /** A proposed grant exceeds the administrator-owned maximum authority. */
4
+ export class PermissionGrantError extends Error {
5
+ reason;
6
+ missing;
7
+ _tag = "PermissionGrantError";
8
+ constructor(reason, missing) {
9
+ super(`Permission grant rejected: ${reason}`);
10
+ this.reason = reason;
11
+ this.missing = missing;
12
+ }
13
+ }
14
+ /** Create a fail-closed policy with no implicit grants. */
15
+ export const createPermissionPolicy = (ceilings, grants = []) => ({
16
+ ceilings: ceilings.map(normalizeScope),
17
+ grants: grants.map(normalizeGrant),
18
+ revoked_grant_ids: new Set(),
19
+ consumed_grant_ids: new Set(),
20
+ });
21
+ /** Add one grant only when it is a subset of its administrator ceiling. */
22
+ export const grantPermission = (policy, grant) => {
23
+ const normalized = normalizeGrant(grant);
24
+ if (normalized.lifetime === "once" && normalized.operation_identity === null)
25
+ return err(new PermissionGrantError("invalid_once_scope", {}));
26
+ const missing = missingFromScopes(normalized, policy.ceilings);
27
+ if (hasMissing(missing))
28
+ return err(new PermissionGrantError("outside_administrator_ceiling", missing));
29
+ return ok({ ...policy, grants: [...policy.grants, normalized] });
30
+ };
31
+ /** Evaluate exact requested authority against the ceiling and active grants. */
32
+ export const evaluatePermission = (policy, request, now = new Date()) => {
33
+ const normalized = normalizeRequest(request);
34
+ const outsideCeiling = missingFromScopes(normalized, policy.ceilings);
35
+ if (hasMissing(outsideCeiling))
36
+ return {
37
+ allowed: false,
38
+ reason: "outside_administrator_ceiling",
39
+ missing: outsideCeiling,
40
+ };
41
+ const candidates = policy.grants.filter((grant) => grant.capability === normalized.capability &&
42
+ !policy.revoked_grant_ids.has(grant.grant_id) &&
43
+ !policy.consumed_grant_ids.has(grant.grant_id) &&
44
+ !isExpired(grant, now) &&
45
+ (grant.lifetime !== "once" ||
46
+ grant.operation_identity === normalized.operation_identity));
47
+ const selected = candidates.find((grant) => !hasMissing(missingFromScopes(normalized, [grant])));
48
+ if (selected !== undefined)
49
+ return {
50
+ allowed: true,
51
+ grant_id: selected.grant_id,
52
+ lifetime: selected.lifetime,
53
+ };
54
+ const coveringGrant = policy.grants.find((grant) => grant.capability === normalized.capability &&
55
+ (grant.lifetime !== "once" ||
56
+ grant.operation_identity === normalized.operation_identity) &&
57
+ !hasMissing(missingFromScopes(normalized, [grant])));
58
+ return {
59
+ allowed: false,
60
+ reason: coveringGrant !== undefined && isExpired(coveringGrant, now)
61
+ ? "grant_expired"
62
+ : candidates.length === 0 &&
63
+ policy.grants.some((grant) => grant.capability === normalized.capability)
64
+ ? "grant_revoked_or_consumed"
65
+ : "grant_required",
66
+ missing: missingFromScopes(normalized, candidates),
67
+ };
68
+ };
69
+ /** Consume an allowed one-shot decision; longer-lived grants are unchanged. */
70
+ export const consumePermission = (policy, decision) => decision.lifetime === "once"
71
+ ? {
72
+ ...policy,
73
+ consumed_grant_ids: new Set([
74
+ ...policy.consumed_grant_ids,
75
+ decision.grant_id,
76
+ ]),
77
+ }
78
+ : policy;
79
+ /** Revoke one grant for every future evaluation. */
80
+ export const revokePermission = (policy, grantId) => ({
81
+ ...policy,
82
+ revoked_grant_ids: new Set([...policy.revoked_grant_ids, grantId]),
83
+ });
84
+ /** Drop connection-bound authority when an MCP or CLI session ends. */
85
+ export const clearSessionPermissions = (policy) => ({
86
+ ...policy,
87
+ grants: policy.grants.filter(({ lifetime }) => lifetime !== "once" && lifetime !== "session"),
88
+ });
89
+ /** Atomically replace administrator ceilings while retaining auditable grants. */
90
+ export const reloadPermissionCeilings = (policy, ceilings) => ({
91
+ ...policy,
92
+ ceilings: ceilings.map(normalizeScope),
93
+ });
94
+ const normalizeScope = (scope) => ({
95
+ capability: scope.capability,
96
+ roots: uniqueSorted(scope.roots.map((path) => resolve(path))),
97
+ executables: uniqueSorted(scope.executables.map((path) => resolve(path))),
98
+ environment_names: uniqueSorted(scope.environment_names),
99
+ network: scope.network,
100
+ mount: scope.mount,
101
+ });
102
+ const normalizeGrant = (grant) => ({
103
+ ...normalizeScope(grant),
104
+ grant_id: grant.grant_id,
105
+ lifetime: grant.lifetime,
106
+ operation_identity: grant.operation_identity,
107
+ expires_at: grant.expires_at,
108
+ });
109
+ const normalizeRequest = (request) => ({
110
+ ...normalizeScope(request),
111
+ operation_identity: request.operation_identity,
112
+ });
113
+ const missingFromScopes = (request, scopes) => {
114
+ const relevant = scopes.filter(({ capability }) => capability === request.capability);
115
+ const roots = request.roots.filter((path) => !relevant.some((scope) => containsAny(scope.roots, path)));
116
+ const executables = request.executables.filter((path) => !relevant.some((scope) => containsAny(scope.executables, path)));
117
+ const environmentNames = request.environment_names.filter((name) => !relevant.some((scope) => scope.environment_names.includes(name)));
118
+ const network = request.network === "none" ||
119
+ relevant.some((scope) => networkRank(scope.network) >= networkRank(request.network))
120
+ ? undefined
121
+ : request.network;
122
+ const mount = !request.mount || relevant.some((scope) => scope.mount)
123
+ ? undefined
124
+ : true;
125
+ return {
126
+ ...(roots.length === 0 ? {} : { roots }),
127
+ ...(executables.length === 0 ? {} : { executables }),
128
+ ...(environmentNames.length === 0
129
+ ? {}
130
+ : { environment_names: environmentNames }),
131
+ ...(network === undefined ? {} : { network }),
132
+ ...(mount === undefined ? {} : { mount }),
133
+ };
134
+ };
135
+ const HOST_PATH_OPERATIONS = { relative, isAbsolute, sep };
136
+ /** Test whether a candidate is inside a root using one platform path flavor. */
137
+ export const isPathContained = (root, candidate, operations = HOST_PATH_OPERATIONS) => {
138
+ const relation = operations.relative(root, candidate);
139
+ return (relation === "" ||
140
+ (relation !== ".." &&
141
+ !relation.startsWith(`..${operations.sep}`) &&
142
+ !operations.isAbsolute(relation)));
143
+ };
144
+ const containsAny = (roots, candidate) => roots.some((root) => isPathContained(root, candidate));
145
+ const networkRank = (network) => {
146
+ switch (network) {
147
+ case "none":
148
+ return 0;
149
+ case "loopback":
150
+ return 1;
151
+ case "external":
152
+ return 2;
153
+ }
154
+ };
155
+ const isExpired = (grant, now) => grant.expires_at !== null && Date.parse(grant.expires_at) <= now.getTime();
156
+ const hasMissing = (missing) => Object.keys(missing).length > 0;
157
+ const uniqueSorted = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right));
@@ -9,6 +9,7 @@ export const comparisonStatusSchema = z.enum([
9
9
  "changed",
10
10
  "truncated",
11
11
  "unknown",
12
+ "contradiction",
12
13
  ]);
13
14
  /** Pure normalized comparison between two captures. */
14
15
  export const processCaptureComparisonSchema = z
@@ -261,7 +261,7 @@ const authorityLimitations = (claim, sources, sides) => {
261
261
  const classify = (observed, limitations, unknowns) => {
262
262
  if (limitations.length > 0)
263
263
  return "unknown";
264
- if (["changed", "added", "removed"].includes(observed))
264
+ if (["changed", "added", "removed", "contradiction"].includes(observed))
265
265
  return "fail";
266
266
  if (observed === "unknown" || observed === "truncated" || unknowns.length > 0)
267
267
  return "unknown";
@@ -86,6 +86,7 @@ export const reconstructionClaimResultSchema = z.object({
86
86
  "changed",
87
87
  "truncated",
88
88
  "unknown",
89
+ "contradiction",
89
90
  ]),
90
91
  comparison_evidence_id: evidenceIdSchema,
91
92
  left_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
@@ -260,7 +260,11 @@ const selectStatic = (evidence, selector) => {
260
260
  const status = matches[0].classification;
261
261
  return {
262
262
  status,
263
- conclusion_kind: status === "unknown" ? "unresolved_branch" : "derived_relationship",
263
+ conclusion_kind: status === "contradiction"
264
+ ? "contradiction"
265
+ : status === "unknown"
266
+ ? "unresolved_branch"
267
+ : "derived_relationship",
264
268
  };
265
269
  };
266
270
  const parseStatic = (input) => {
@@ -0,0 +1,9 @@
1
+ /** Generated from package.json and package-lock.json; do not edit. */
2
+ export const PACKAGE_METADATA = {
3
+ name: "rea-agents",
4
+ version: "1.4.0",
5
+ serverSdkVersion: "2.0.0-beta.4",
6
+ clientSdkVersion: "2.0.0-beta.4",
7
+ coreSdkVersion: "2.0.0-beta.4",
8
+ skillVersion: "12",
9
+ };