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.
- package/README.md +21 -0
- package/bridge/hopper_bridge.py +12 -3
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +98 -26
- package/dist/application/AuthorizedArtifactInventory.js +2 -2
- package/dist/application/BinarySession.js +83 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +19 -8
- package/dist/application/CrossVersionInvestigation.js +68 -5
- package/dist/application/DirectAnalysis.js +97 -9
- package/dist/application/Doctor.js +74 -2
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureError.js +13 -0
- package/dist/application/ProcessCaptureLifecycle.js +3 -1
- package/dist/application/ProcessCli.js +23 -10
- package/dist/application/ProcessHarness.js +4 -1
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/Setup.js +2 -41
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +1 -1
- package/dist/application/Upgrade.js +2 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +13 -3
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +46 -3
- package/dist/cliEvidenceCommands.js +42 -1
- package/dist/cliInvestigationCommands.js +57 -6
- package/dist/cliLogging.js +20 -3
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/cliProcessCommands.js +4 -14
- package/dist/config.js +63 -3
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/toolContracts.js +10 -1
- package/dist/contracts/toolOutputSchemas.js +62 -30
- 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 +5 -3
- package/dist/domain/errors.js +216 -3
- package/dist/domain/investigationWorkspace.js +131 -4
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processComparison.js +1 -0
- 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 +7 -3
- package/dist/hopper/HopperClient.js +32 -5
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +95 -7
- package/dist/server/createServer.js +52 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +18 -1
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +58 -1
- package/dist/server/registerFunctionComparisonTool.js +6 -2
- package/dist/server/registerInvestigationTools.js +83 -10
- package/dist/server/registerOfficialTools.js +19 -1
- package/dist/server/registerProcessComparisonTool.js +7 -3
- package/dist/server/registerSessionStatusTool.js +44 -3
- package/dist/server/registerSessionTools.js +129 -7
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/toolResult.js +27 -3
- package/dist/serverIdentity.js +70 -0
- package/package.json +6 -4
package/dist/domain/errors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hopperStartupFailure, } from "./hopperStartupFailure.js";
|
|
2
|
-
export { hopperStartupFailure }
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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));
|
|
@@ -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";
|
|
@@ -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 === "
|
|
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
|
+
};
|