rea-agents 0.3.0 → 0.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 +124 -20
- package/bridge/hopper_bridge.py +141 -13
- package/dist/application/AnalysisProvider.js +12 -1
- package/dist/application/ArtifactExtraction.js +166 -0
- package/dist/application/ArtifactGraphConstruction.js +257 -0
- package/dist/application/ArtifactInventory.js +253 -0
- package/dist/application/BinarySession.js +164 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +27 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +8 -7
- package/dist/application/EvidenceBundleCommands.js +29 -0
- package/dist/application/EvidenceBundleFiles.js +127 -0
- package/dist/application/EvidenceLedger.js +225 -18
- package/dist/application/FilesystemSnapshot.js +124 -0
- package/dist/application/LinuxHopper.js +186 -0
- package/dist/application/LoopbackReplay.js +195 -37
- package/dist/application/ProcessHarness.js +200 -191
- package/dist/application/ProcessNormalization.js +44 -0
- package/dist/application/ProcessOwnership.js +84 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +105 -0
- package/dist/application/ReferenceSourceImport.js +182 -0
- package/dist/application/ReferenceSourceImportEntries.js +122 -0
- package/dist/application/ReferenceSourceImportPolicy.js +73 -0
- package/dist/application/ReferenceSourceImportTypes.js +18 -0
- package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
- package/dist/application/Setup.js +192 -41
- package/dist/application/Uninstall.js +130 -0
- package/dist/application/runtime.js +8 -1
- package/dist/artifacts/ArtifactPaths.js +51 -0
- package/dist/artifacts/ArtifactProvider.js +130 -0
- package/dist/artifacts/ArtifactReader.js +9 -0
- package/dist/artifacts/AsarArtifactReader.js +62 -0
- package/dist/artifacts/DirectoryArtifactReader.js +107 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
- package/dist/artifacts/SafeOutputTree.js +199 -0
- package/dist/artifacts/StreamBytes.js +10 -0
- package/dist/artifacts/ZipArtifactReader.js +109 -0
- package/dist/cli.js +149 -2
- package/dist/config.js +35 -1
- package/dist/contracts/artifactComparisonExample.js +95 -0
- package/dist/contracts/artifactToolContracts.js +84 -0
- package/dist/contracts/enhancedInputs.js +4 -0
- package/dist/contracts/functionComparisonExample.js +57 -0
- package/dist/contracts/investigationExamples.js +118 -0
- package/dist/contracts/nativeToolContracts.js +52 -0
- package/dist/contracts/processCaptureExample.js +25 -0
- package/dist/contracts/toolContractExamples.js +69 -0
- package/dist/contracts/toolContracts.js +99 -36
- package/dist/contracts/toolOutputSchemas.js +154 -82
- package/dist/contracts/unknownContractExamples.js +33 -0
- package/dist/domain/artifactComparison.js +273 -0
- package/dist/domain/artifactGraph.js +194 -0
- package/dist/domain/artifactInventoryEvidence.js +150 -0
- package/dist/domain/binaryTarget.js +55 -0
- package/dist/domain/bundleComparison.js +266 -0
- package/dist/domain/callPath.js +346 -0
- package/dist/domain/changedBehavior.js +294 -0
- package/dist/domain/errors.js +192 -4
- package/dist/domain/evidence.js +41 -10
- package/dist/domain/evidenceBundle.js +187 -6
- package/dist/domain/functionComparison.js +201 -0
- package/dist/domain/functionComparisonNormalization.js +112 -0
- package/dist/domain/functionComparisonResults.js +54 -0
- package/dist/domain/functionComparisonSchemas.js +82 -0
- package/dist/domain/functionDossierEvidence.js +171 -0
- package/dist/domain/hopperValues.js +14 -7
- package/dist/domain/nativeInspection.js +142 -0
- package/dist/domain/processCapture.js +152 -54
- package/dist/domain/processComparison.js +106 -0
- package/dist/domain/reconstructionUnknowns.js +90 -0
- package/dist/domain/reconstructionVerification.js +285 -0
- package/dist/domain/reconstructionVerificationSchemas.js +126 -0
- package/dist/domain/referenceSourceClassification.js +496 -0
- package/dist/domain/referenceSourceGraph.js +376 -0
- package/dist/domain/referenceSourceImportParsing.js +235 -0
- package/dist/domain/referenceSourcePolicy.js +1 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +40 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/identity.js +1 -0
- package/dist/main.js +5 -1
- package/dist/native/CommandRunner.js +156 -0
- package/dist/native/NativeMacOSProvider.js +306 -0
- package/dist/native/NativeMachoInspection.js +135 -0
- package/dist/native/parsers/codesign.js +55 -0
- package/dist/native/parsers/demangle.js +26 -0
- package/dist/native/parsers/dyldInfo.js +25 -0
- package/dist/native/parsers/lipo.js +67 -0
- package/dist/native/parsers/otool.js +193 -0
- package/dist/native/parsers/plist.js +23 -0
- package/dist/reference/ReferenceSourceReader.js +73 -0
- package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
- package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
- package/dist/reference/ReferenceSourceReaderFile.js +119 -0
- package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
- package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
- package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
- package/dist/server/createServer.js +31 -5
- package/dist/server/recordDerivedEvidence.js +10 -0
- package/dist/server/registerArtifactComparisonTool.js +62 -0
- package/dist/server/registerArtifactTools.js +6 -0
- package/dist/server/registerBundleComparisonTool.js +47 -0
- package/dist/server/registerEnhancedTools.js +64 -12
- package/dist/server/registerEvidenceTools.js +36 -0
- package/dist/server/registerFunctionComparisonTool.js +68 -0
- package/dist/server/registerInvestigationTools.js +224 -0
- package/dist/server/registerNativeTools.js +6 -0
- package/dist/server/registerOfficialTools.js +47 -14
- package/dist/server/registerProcessComparisonTool.js +106 -0
- package/dist/server/registerSessionTools.js +179 -70
- package/dist/server/sessionEvidence.js +28 -0
- package/dist/server/sessionToolPolicies.js +64 -0
- package/dist/server/toolRegistrationOptions.js +7 -0
- package/dist/server/toolResult.js +8 -5
- package/install.sh +198 -0
- package/package.json +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
package/dist/domain/errors.js
CHANGED
|
@@ -8,9 +8,124 @@ export class HopperError extends AnalysisError {
|
|
|
8
8
|
export class AnalysisProtocolError extends AnalysisError {
|
|
9
9
|
_tag = "AnalysisProtocolError";
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
export class
|
|
13
|
-
|
|
11
|
+
/** Caller input failed provider-neutral application parsing. */
|
|
12
|
+
export class AnalysisInputError extends AnalysisError {
|
|
13
|
+
operation;
|
|
14
|
+
_tag = "AnalysisInputError";
|
|
15
|
+
constructor(operation, options) {
|
|
16
|
+
super(`Invalid analysis input for ${operation}`, options);
|
|
17
|
+
this.operation = operation;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Provider output failed provider-neutral application parsing. */
|
|
21
|
+
export class AnalysisOutputError extends AnalysisError {
|
|
22
|
+
operation;
|
|
23
|
+
reason;
|
|
24
|
+
_tag = "AnalysisOutputError";
|
|
25
|
+
constructor(operation, reason, options) {
|
|
26
|
+
super(`Invalid analysis output for ${operation}: ${reason}`, options);
|
|
27
|
+
this.operation = operation;
|
|
28
|
+
this.reason = reason;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Selected provider cannot execute a declared analysis operation. */
|
|
32
|
+
export class AnalysisCapabilityUnavailableError extends AnalysisError {
|
|
33
|
+
providerId;
|
|
34
|
+
operation;
|
|
35
|
+
reason;
|
|
36
|
+
_tag = "AnalysisCapabilityUnavailableError";
|
|
37
|
+
constructor(providerId, operation, reason) {
|
|
38
|
+
super(`Provider ${providerId} cannot execute ${operation}: ${reason}`);
|
|
39
|
+
this.providerId = providerId;
|
|
40
|
+
this.operation = operation;
|
|
41
|
+
this.reason = reason;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Caller cancellation won before provider-neutral work completed. */
|
|
45
|
+
export class AnalysisCancelledError extends AnalysisError {
|
|
46
|
+
operation;
|
|
47
|
+
_tag = "AnalysisCancelledError";
|
|
48
|
+
constructor(operation) {
|
|
49
|
+
super(`Analysis operation was cancelled: ${operation}`);
|
|
50
|
+
this.operation = operation;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Provider-neutral operation exceeded its declared execution deadline. */
|
|
54
|
+
export class AnalysisTimeoutError extends AnalysisError {
|
|
55
|
+
operation;
|
|
56
|
+
timeoutMs;
|
|
57
|
+
_tag = "AnalysisTimeoutError";
|
|
58
|
+
constructor(operation, timeoutMs) {
|
|
59
|
+
super(`Analysis operation timed out after ${String(timeoutMs)}ms: ${operation}`);
|
|
60
|
+
this.operation = operation;
|
|
61
|
+
this.timeoutMs = timeoutMs;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** No configured provider can satisfy a requested operation. */
|
|
65
|
+
export class ProviderSelectionError extends AnalysisError {
|
|
66
|
+
operation;
|
|
67
|
+
_tag = "ProviderSelectionError";
|
|
68
|
+
constructor(operation) {
|
|
69
|
+
super(`No configured provider can execute ${operation}`);
|
|
70
|
+
this.operation = operation;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** A provider adapter failed outside its more precise typed variants. */
|
|
74
|
+
export class ProviderAdapterError extends AnalysisError {
|
|
75
|
+
providerId;
|
|
76
|
+
operation;
|
|
77
|
+
_tag = "ProviderAdapterError";
|
|
78
|
+
constructor(providerId, operation, options) {
|
|
79
|
+
super(`Provider ${providerId} adapter failed during ${operation}`, options);
|
|
80
|
+
this.providerId = providerId;
|
|
81
|
+
this.operation = operation;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Artifact inventory or extraction failed a typed safety boundary. */
|
|
85
|
+
export class ArtifactOperationError extends AnalysisError {
|
|
86
|
+
operation;
|
|
87
|
+
reason;
|
|
88
|
+
_tag = "ArtifactOperationError";
|
|
89
|
+
constructor(operation, reason) {
|
|
90
|
+
super(`Artifact ${operation} failed: ${reason}`);
|
|
91
|
+
this.operation = operation;
|
|
92
|
+
this.reason = reason;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Evidence identity, schema, or bundle manifests failed integrity checks. */
|
|
96
|
+
export class EvidenceIntegrityError extends AnalysisError {
|
|
97
|
+
_tag = "EvidenceIntegrityError";
|
|
98
|
+
}
|
|
99
|
+
/** A bounded evidence ledger cannot accept more records or serialized bytes. */
|
|
100
|
+
export class EvidenceLimitError extends AnalysisError {
|
|
101
|
+
limit;
|
|
102
|
+
maximum;
|
|
103
|
+
_tag = "EvidenceLimitError";
|
|
104
|
+
constructor(limit, maximum) {
|
|
105
|
+
super(`Evidence ledger ${limit} limit exceeded (${String(maximum)})`);
|
|
106
|
+
this.limit = limit;
|
|
107
|
+
this.maximum = maximum;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Evidence bundle filesystem access failed within the configured policy. */
|
|
111
|
+
export class EvidenceFileError extends AnalysisError {
|
|
112
|
+
operation;
|
|
113
|
+
reason;
|
|
114
|
+
_tag = "EvidenceFileError";
|
|
115
|
+
constructor(operation, reason, options) {
|
|
116
|
+
super(`Evidence bundle ${operation} failed: ${reason}`, options);
|
|
117
|
+
this.operation = operation;
|
|
118
|
+
this.reason = reason;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Residual-unknown mutation failed a lifecycle, reference, or CAS invariant. */
|
|
122
|
+
export class UnknownRegistryError extends AnalysisError {
|
|
123
|
+
reason;
|
|
124
|
+
_tag = "UnknownRegistryError";
|
|
125
|
+
constructor(reason, options) {
|
|
126
|
+
super(`Residual unknown registry mutation failed: ${reason}`, options);
|
|
127
|
+
this.reason = reason;
|
|
128
|
+
}
|
|
14
129
|
}
|
|
15
130
|
/** Hopper did not respond within the configured operation deadline. */
|
|
16
131
|
export class HopperTimeoutError extends HopperError {
|
|
@@ -75,7 +190,80 @@ export class BinaryTargetError extends AnalysisError {
|
|
|
75
190
|
path;
|
|
76
191
|
_tag = "BinaryTargetError";
|
|
77
192
|
constructor(path, reason, options) {
|
|
78
|
-
super(`Cannot open
|
|
193
|
+
super(`Cannot open artifact: ${reason}`, options);
|
|
79
194
|
this.path = path;
|
|
80
195
|
}
|
|
81
196
|
}
|
|
197
|
+
/** Project expected failures into exhaustive, secret-safe caller fields. */
|
|
198
|
+
export const projectAnalysisError = (error) => {
|
|
199
|
+
assertKnownTag(error._tag);
|
|
200
|
+
return {
|
|
201
|
+
tag: error._tag,
|
|
202
|
+
message: error.message,
|
|
203
|
+
details: safeDetails(error),
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
const safeDetails = (error) => {
|
|
207
|
+
if (error instanceof AnalysisInputError)
|
|
208
|
+
return { operation: error.operation };
|
|
209
|
+
if (error instanceof AnalysisOutputError)
|
|
210
|
+
return { operation: error.operation, reason: error.reason };
|
|
211
|
+
if (error instanceof AnalysisCapabilityUnavailableError)
|
|
212
|
+
return {
|
|
213
|
+
providerId: error.providerId,
|
|
214
|
+
operation: error.operation,
|
|
215
|
+
reason: error.reason,
|
|
216
|
+
};
|
|
217
|
+
if (error instanceof AnalysisCancelledError)
|
|
218
|
+
return { operation: error.operation };
|
|
219
|
+
if (error instanceof AnalysisTimeoutError)
|
|
220
|
+
return { operation: error.operation, timeoutMs: error.timeoutMs };
|
|
221
|
+
if (error instanceof ProviderSelectionError)
|
|
222
|
+
return { operation: error.operation };
|
|
223
|
+
if (error instanceof ProviderAdapterError)
|
|
224
|
+
return { providerId: error.providerId, operation: error.operation };
|
|
225
|
+
if (error instanceof ArtifactOperationError)
|
|
226
|
+
return { operation: error.operation, reason: error.reason };
|
|
227
|
+
if (error instanceof EvidenceLimitError)
|
|
228
|
+
return { limit: error.limit, maximum: error.maximum };
|
|
229
|
+
if (error instanceof EvidenceFileError)
|
|
230
|
+
return { operation: error.operation, reason: error.reason };
|
|
231
|
+
if (error instanceof UnknownRegistryError)
|
|
232
|
+
return { reason: error.reason };
|
|
233
|
+
if (error instanceof HopperTimeoutError)
|
|
234
|
+
return { timeoutMs: error.timeoutMs };
|
|
235
|
+
if (error instanceof HopperRemoteError)
|
|
236
|
+
return { code: error.code, safeMessage: error.safeMessage };
|
|
237
|
+
if (error instanceof HopperProcessError)
|
|
238
|
+
return { exitCode: error.exitCode };
|
|
239
|
+
return {};
|
|
240
|
+
};
|
|
241
|
+
const KNOWN_ERROR_TAGS = {
|
|
242
|
+
AnalysisProtocolError: true,
|
|
243
|
+
AnalysisInputError: true,
|
|
244
|
+
AnalysisOutputError: true,
|
|
245
|
+
AnalysisCapabilityUnavailableError: true,
|
|
246
|
+
AnalysisCancelledError: true,
|
|
247
|
+
AnalysisTimeoutError: true,
|
|
248
|
+
ProviderSelectionError: true,
|
|
249
|
+
ProviderAdapterError: true,
|
|
250
|
+
ArtifactOperationError: true,
|
|
251
|
+
ProcessCaptureError: true,
|
|
252
|
+
EvidenceIntegrityError: true,
|
|
253
|
+
EvidenceLimitError: true,
|
|
254
|
+
EvidenceFileError: true,
|
|
255
|
+
UnknownRegistryError: true,
|
|
256
|
+
HopperTimeoutError: true,
|
|
257
|
+
HopperCancelledError: true,
|
|
258
|
+
HopperProtocolError: true,
|
|
259
|
+
HopperRemoteError: true,
|
|
260
|
+
HopperProcessError: true,
|
|
261
|
+
HopperStartError: true,
|
|
262
|
+
ConfigurationError: true,
|
|
263
|
+
NoBinaryOpenError: true,
|
|
264
|
+
BinaryTargetError: true,
|
|
265
|
+
};
|
|
266
|
+
const assertKnownTag = (tag) => {
|
|
267
|
+
if (KNOWN_ERROR_TAGS[tag] !== true)
|
|
268
|
+
throw new TypeError("Unknown analysis error tag");
|
|
269
|
+
};
|
package/dist/domain/evidence.js
CHANGED
|
@@ -11,11 +11,32 @@ const providerSchema = z.object({
|
|
|
11
11
|
const subjectSchema = z.object({
|
|
12
12
|
name: z.string().min(1),
|
|
13
13
|
digest: z.object({ sha256: digestSchema }),
|
|
14
|
-
format: z.enum([
|
|
14
|
+
format: z.enum([
|
|
15
|
+
"hopper",
|
|
16
|
+
"mach-o",
|
|
17
|
+
"elf",
|
|
18
|
+
"pe",
|
|
19
|
+
"zip",
|
|
20
|
+
"ipa",
|
|
21
|
+
"apk",
|
|
22
|
+
"asar",
|
|
23
|
+
"dmg",
|
|
24
|
+
"pkg",
|
|
25
|
+
"plist",
|
|
26
|
+
"javascript",
|
|
27
|
+
"source-map",
|
|
28
|
+
"directory",
|
|
29
|
+
"file",
|
|
30
|
+
"unknown",
|
|
31
|
+
"mach-o-universal",
|
|
32
|
+
"javascript-bundle",
|
|
33
|
+
"entitlements",
|
|
34
|
+
]),
|
|
15
35
|
architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
|
|
16
36
|
local_path: z.string(),
|
|
17
37
|
});
|
|
18
|
-
|
|
38
|
+
/** Source location attached to an evidence observation. */
|
|
39
|
+
const evidenceLocationSchema = z.discriminatedUnion("kind", [
|
|
19
40
|
z.object({ kind: z.literal("address"), address: z.string().min(1) }),
|
|
20
41
|
z.object({
|
|
21
42
|
kind: z.literal("address-range"),
|
|
@@ -23,6 +44,12 @@ const locationSchema = z.discriminatedUnion("kind", [
|
|
|
23
44
|
end: z.string().min(1),
|
|
24
45
|
}),
|
|
25
46
|
z.object({ kind: z.literal("artifact-path"), path: z.string().min(1) }),
|
|
47
|
+
z.object({ kind: z.literal("file-offset"), offset: z.number().int().min(0) }),
|
|
48
|
+
z.object({
|
|
49
|
+
kind: z.literal("file-offset-range"),
|
|
50
|
+
start: z.number().int().min(0),
|
|
51
|
+
end: z.number().int().min(0),
|
|
52
|
+
}),
|
|
26
53
|
]);
|
|
27
54
|
const evidenceAuthoritySchema = z.enum([
|
|
28
55
|
"shipped-artifact",
|
|
@@ -46,13 +73,13 @@ export const evidenceSchema = z.object({
|
|
|
46
73
|
predicate_type: z.string().min(1),
|
|
47
74
|
operation: z.string().min(1),
|
|
48
75
|
parameters: z.record(z.string(), jsonValueSchema),
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
raw_result: jsonValueSchema.nullable(),
|
|
77
|
+
normalized_result: jsonValueSchema,
|
|
51
78
|
confidence: z.enum(["observed", "derived", "inferred"]),
|
|
52
79
|
authority: evidenceAuthoritySchema,
|
|
53
80
|
environment: executionEnvironmentSchema.nullable(),
|
|
54
81
|
limitations: z.array(z.string()),
|
|
55
|
-
locations: z.array(
|
|
82
|
+
locations: z.array(evidenceLocationSchema),
|
|
56
83
|
evidence_links: z.array(z.string().regex(/^ev_[a-f0-9]{64}$/u)),
|
|
57
84
|
});
|
|
58
85
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -76,7 +103,8 @@ const semanticProjection = (evidence) => ({
|
|
|
76
103
|
predicate_type: evidence.predicate_type,
|
|
77
104
|
operation: evidence.operation,
|
|
78
105
|
parameters: evidence.parameters,
|
|
79
|
-
|
|
106
|
+
raw_result: evidence.raw_result,
|
|
107
|
+
normalized_result: evidence.normalized_result,
|
|
80
108
|
confidence: evidence.confidence,
|
|
81
109
|
authority: evidence.authority,
|
|
82
110
|
environment: evidence.environment,
|
|
@@ -88,6 +116,11 @@ const semanticProjection = (evidence) => ({
|
|
|
88
116
|
const computeEvidenceId = (evidence) => `ev_${sha256(canonicalJson(semanticProjection(evidence)))}`;
|
|
89
117
|
/** Parse evidence and reject a syntactically valid but tampered semantic ID. */
|
|
90
118
|
export const parseEvidence = (input) => {
|
|
119
|
+
if (typeof input === "object" &&
|
|
120
|
+
input !== null &&
|
|
121
|
+
"schema_version" in input &&
|
|
122
|
+
input.schema_version !== 2)
|
|
123
|
+
throw new TypeError(`Unsupported evidence schema_version ${String(input.schema_version)}; Evidence v1 is not accepted. Produce Evidence v2.`);
|
|
91
124
|
const evidence = evidenceSchema.parse(input);
|
|
92
125
|
const { evidence_id: evidenceId, ...withoutId } = evidence;
|
|
93
126
|
if (computeEvidenceId(withoutId) !== evidenceId)
|
|
@@ -122,7 +155,8 @@ export const createEvidence = (target, provider, observation) => {
|
|
|
122
155
|
predicate_type: observation.predicateType ?? "rea.analysis/v2",
|
|
123
156
|
operation: observation.operation,
|
|
124
157
|
parameters: observation.parameters,
|
|
125
|
-
|
|
158
|
+
raw_result: observation.rawResult ?? null,
|
|
159
|
+
normalized_result: observation.result,
|
|
126
160
|
confidence: observation.confidence ?? "observed",
|
|
127
161
|
authority: observation.authority ?? "shipped-artifact",
|
|
128
162
|
environment: observation.environment ?? null,
|
|
@@ -139,8 +173,5 @@ export const createEvidence = (target, provider, observation) => {
|
|
|
139
173
|
...semantic,
|
|
140
174
|
evidence_id: `ev_${sha256(canonicalJson(semantic))}`,
|
|
141
175
|
subject,
|
|
142
|
-
raw_payload_sha256: observation.redactedRawPayload === undefined
|
|
143
|
-
? null
|
|
144
|
-
: sha256(canonicalJson(observation.redactedRawPayload)),
|
|
145
176
|
});
|
|
146
177
|
};
|
|
@@ -1,11 +1,192 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { evidenceSchema, parseEvidence } from "./evidence.js";
|
|
4
|
+
import { residualUnknownSchema, } from "./residualUnknown.js";
|
|
5
|
+
const artifactManifestSchema = evidenceSchema.shape.subject.unwrap().pick({
|
|
6
|
+
digest: true,
|
|
7
|
+
format: true,
|
|
8
|
+
architecture: true,
|
|
9
|
+
});
|
|
10
|
+
const providerManifestSchema = evidenceSchema.shape.provider;
|
|
11
|
+
const environmentManifestSchema = evidenceSchema.shape.environment.unwrap();
|
|
12
|
+
const scenarioManifestSchema = z.object({
|
|
13
|
+
evidence_id: evidenceSchema.shape.evidence_id,
|
|
14
|
+
operation: z.string().min(1),
|
|
15
|
+
authority: z.literal("controlled-replay"),
|
|
16
|
+
});
|
|
17
|
+
const captureManifestSchema = z.object({
|
|
18
|
+
evidence_id: evidenceSchema.shape.evidence_id,
|
|
19
|
+
predicate_type: z.string().min(1),
|
|
20
|
+
});
|
|
3
21
|
export const evidenceBundleSchema = z.object({
|
|
4
|
-
bundle_version: z.literal(
|
|
22
|
+
bundle_version: z.literal(2),
|
|
23
|
+
artifacts: z.array(artifactManifestSchema),
|
|
24
|
+
providers: z.array(providerManifestSchema),
|
|
25
|
+
environments: z.array(environmentManifestSchema),
|
|
26
|
+
scenarios: z.array(scenarioManifestSchema),
|
|
27
|
+
captures: z.array(captureManifestSchema),
|
|
28
|
+
unknowns: z.array(residualUnknownSchema),
|
|
5
29
|
records: z.array(evidenceSchema),
|
|
6
30
|
});
|
|
7
31
|
/** Project records into a deterministic bundle whose order has no semantics. */
|
|
8
|
-
export const createEvidenceBundle = (records) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
32
|
+
export const createEvidenceBundle = (records, unknowns = []) => {
|
|
33
|
+
const sortedRecords = [...records].sort((left, right) => left.evidence_id.localeCompare(right.evidence_id));
|
|
34
|
+
return {
|
|
35
|
+
bundle_version: 2,
|
|
36
|
+
artifacts: uniqueSorted(sortedRecords.flatMap(({ subject }) => subject === null
|
|
37
|
+
? []
|
|
38
|
+
: [
|
|
39
|
+
{
|
|
40
|
+
digest: subject.digest,
|
|
41
|
+
format: subject.format,
|
|
42
|
+
architecture: subject.architecture,
|
|
43
|
+
},
|
|
44
|
+
])),
|
|
45
|
+
providers: uniqueSorted(sortedRecords.map(({ provider }) => provider)),
|
|
46
|
+
environments: uniqueSorted(sortedRecords.flatMap(({ environment }) => environment === null ? [] : [environment])),
|
|
47
|
+
scenarios: sortedRecords.flatMap((evidence) => evidence.authority === "controlled-replay"
|
|
48
|
+
? [
|
|
49
|
+
{
|
|
50
|
+
evidence_id: evidence.evidence_id,
|
|
51
|
+
operation: evidence.operation,
|
|
52
|
+
authority: evidence.authority,
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
: []),
|
|
56
|
+
captures: sortedRecords.flatMap((evidence) => evidence.predicate_type.startsWith("rea.process-capture/")
|
|
57
|
+
? [
|
|
58
|
+
{
|
|
59
|
+
evidence_id: evidence.evidence_id,
|
|
60
|
+
predicate_type: evidence.predicate_type,
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
unknowns: [...unknowns].sort((left, right) => left.unknown_id.localeCompare(right.unknown_id) ||
|
|
65
|
+
left.revision - right.revision),
|
|
66
|
+
records: sortedRecords,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/** Parse records, verify semantic IDs, and reject inconsistent manifests. */
|
|
70
|
+
export const parseEvidenceBundle = (input) => {
|
|
71
|
+
const parsed = evidenceBundleSchema.parse(input);
|
|
72
|
+
const recordIds = parsed.records.map(({ evidence_id: id }) => id);
|
|
73
|
+
if (new Set(recordIds).size !== recordIds.length)
|
|
74
|
+
throw new TypeError("Evidence bundle contains duplicate record IDs");
|
|
75
|
+
validateUnknownGraph(parsed.unknowns, parsed.records);
|
|
76
|
+
const canonical = createEvidenceBundle(parsed.records.map(parseEvidence), parsed.unknowns);
|
|
77
|
+
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
|
|
78
|
+
throw new TypeError("Evidence bundle manifests are not canonical");
|
|
79
|
+
return canonical;
|
|
80
|
+
};
|
|
81
|
+
const validateUnknownGraph = (unknowns, records) => {
|
|
82
|
+
const evidenceById = new Map(records.map((record) => [record.evidence_id, record]));
|
|
83
|
+
const histories = new Map();
|
|
84
|
+
for (const unknown of unknowns) {
|
|
85
|
+
const history = histories.get(unknown.unknown_id) ?? [];
|
|
86
|
+
history.push(unknown);
|
|
87
|
+
histories.set(unknown.unknown_id, history);
|
|
88
|
+
}
|
|
89
|
+
const unknownById = new Map();
|
|
90
|
+
for (const [id, unordered] of histories) {
|
|
91
|
+
const history = [...unordered].sort((left, right) => left.revision - right.revision);
|
|
92
|
+
for (const [index, revision] of history.entries()) {
|
|
93
|
+
if (revision.revision !== index + 1)
|
|
94
|
+
throw new TypeError(`Residual unknown ${id} revision history has a gap`);
|
|
95
|
+
if (index > 0 &&
|
|
96
|
+
revision.previous_revision_digest !==
|
|
97
|
+
history[index - 1]?.revision_digest)
|
|
98
|
+
throw new TypeError(`Residual unknown ${id} revision chain is broken`);
|
|
99
|
+
}
|
|
100
|
+
const head = history.at(-1);
|
|
101
|
+
if (head === undefined)
|
|
102
|
+
throw new TypeError("Residual unknown revision history is empty");
|
|
103
|
+
unknownById.set(id, head);
|
|
104
|
+
}
|
|
105
|
+
for (const unknown of unknowns) {
|
|
106
|
+
const referencedEvidence = [
|
|
107
|
+
...unknown.supporting_evidence_ids,
|
|
108
|
+
...unknown.contradicting_evidence_ids,
|
|
109
|
+
...unknown.mutation_evidence_ids,
|
|
110
|
+
...(unknown.resolution?.evidence_ids ?? []),
|
|
111
|
+
];
|
|
112
|
+
for (const evidenceId of referencedEvidence)
|
|
113
|
+
if (!evidenceById.has(evidenceId))
|
|
114
|
+
throw new TypeError(`Residual unknown references missing evidence ${evidenceId}`);
|
|
115
|
+
for (const relationship of unknown.relationships)
|
|
116
|
+
if (!unknownById.has(relationship.unknown_id))
|
|
117
|
+
throw new TypeError(`Residual unknown references missing unknown ${relationship.unknown_id}`);
|
|
118
|
+
if (unknown.resolution?.disposition === "verified")
|
|
119
|
+
validateVerifiedResolution(unknown, evidenceById);
|
|
120
|
+
}
|
|
121
|
+
rejectDependencyCycles(unknownById);
|
|
122
|
+
};
|
|
123
|
+
const validateVerifiedResolution = (unknown, evidenceById) => {
|
|
124
|
+
if (unknown.contradicting_evidence_ids.length > 0)
|
|
125
|
+
throw new TypeError(`Residual unknown ${unknown.unknown_id} retains contradicting evidence`);
|
|
126
|
+
const qualifies = unknown.resolution?.evidence_ids.some((evidenceId) => {
|
|
127
|
+
const evidence = evidenceById.get(evidenceId);
|
|
128
|
+
return (unknown.supporting_evidence_ids.includes(evidenceId) &&
|
|
129
|
+
evidence !== undefined &&
|
|
130
|
+
evidence.predicate_type !== "rea.residual-unknown-mutation/v1" &&
|
|
131
|
+
evidenceQualifies(unknown, evidence));
|
|
132
|
+
});
|
|
133
|
+
if (qualifies !== true)
|
|
134
|
+
throw new TypeError(`Residual unknown ${unknown.unknown_id} has no qualifying resolution evidence`);
|
|
135
|
+
};
|
|
136
|
+
const evidenceQualifies = (unknown, evidence) => {
|
|
137
|
+
if (unknown.required_authority !== null &&
|
|
138
|
+
evidence.authority !== unknown.required_authority)
|
|
139
|
+
return false;
|
|
140
|
+
const confidenceRank = { inferred: 1, derived: 2, observed: 3 };
|
|
141
|
+
if (confidenceRank[evidence.confidence] <
|
|
142
|
+
confidenceRank[unknown.required_confidence])
|
|
143
|
+
return false;
|
|
144
|
+
const requirement = unknown.required_environment;
|
|
145
|
+
if (requirement === null)
|
|
146
|
+
return true;
|
|
147
|
+
if (evidence.environment === null)
|
|
148
|
+
return false;
|
|
149
|
+
return ((requirement.id === null || requirement.id === evidence.environment.id) &&
|
|
150
|
+
(requirement.platform === null ||
|
|
151
|
+
requirement.platform === evidence.environment.platform) &&
|
|
152
|
+
(requirement.architecture === null ||
|
|
153
|
+
requirement.architecture === evidence.environment.architecture) &&
|
|
154
|
+
(requirement.isolation === null ||
|
|
155
|
+
requirement.isolation === evidence.environment.isolation));
|
|
156
|
+
};
|
|
157
|
+
const rejectDependencyCycles = (unknownById) => {
|
|
158
|
+
const visited = new Set();
|
|
159
|
+
const active = new Set();
|
|
160
|
+
const visit = (id) => {
|
|
161
|
+
if (active.has(id))
|
|
162
|
+
throw new TypeError("Residual unknown dependency graph contains a cycle");
|
|
163
|
+
if (visited.has(id))
|
|
164
|
+
return;
|
|
165
|
+
active.add(id);
|
|
166
|
+
const unknown = unknownById.get(id);
|
|
167
|
+
if (unknown === undefined)
|
|
168
|
+
throw new TypeError("Residual unknown dependency graph is inconsistent");
|
|
169
|
+
for (const relationship of unknown.relationships)
|
|
170
|
+
if (relationship.type === "depends-on")
|
|
171
|
+
visit(relationship.unknown_id);
|
|
172
|
+
active.delete(id);
|
|
173
|
+
visited.add(id);
|
|
174
|
+
};
|
|
175
|
+
for (const id of unknownById.keys())
|
|
176
|
+
visit(id);
|
|
177
|
+
};
|
|
178
|
+
/** Encode a validated bundle as byte-stable RFC 8785 canonical JSON. */
|
|
179
|
+
export const serializeEvidenceBundle = (bundle) => {
|
|
180
|
+
const serialized = canonicalize(parseEvidenceBundle(bundle));
|
|
181
|
+
if (serialized === undefined)
|
|
182
|
+
throw new TypeError("Evidence bundle canonicalization failed");
|
|
183
|
+
return serialized;
|
|
184
|
+
};
|
|
185
|
+
const uniqueSorted = (values) => {
|
|
186
|
+
const unique = new Map();
|
|
187
|
+
for (const value of values)
|
|
188
|
+
unique.set(JSON.stringify(value), value);
|
|
189
|
+
return [...unique.entries()]
|
|
190
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
191
|
+
.map(([, value]) => value);
|
|
192
|
+
};
|