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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const categorySchema = z.enum([
|
|
3
|
+
"invalid_input",
|
|
4
|
+
"permission_required",
|
|
5
|
+
"unsupported_provider",
|
|
6
|
+
"integrity_mismatch",
|
|
7
|
+
"truncated",
|
|
8
|
+
"cancelled",
|
|
9
|
+
"timeout",
|
|
10
|
+
"unavailable",
|
|
11
|
+
"execution_failure",
|
|
12
|
+
]);
|
|
13
|
+
const remediationSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
action: z.string().min(1),
|
|
16
|
+
restart_required: z.boolean(),
|
|
17
|
+
elicitation_supported: z.boolean().optional(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
const common = {
|
|
21
|
+
category: categorySchema,
|
|
22
|
+
message: z.string().min(1),
|
|
23
|
+
retryable: z.boolean(),
|
|
24
|
+
remediation: remediationSchema,
|
|
25
|
+
};
|
|
26
|
+
const genericDetails = z.record(z.string(), z.json()).optional();
|
|
27
|
+
const generic = (code) => z
|
|
28
|
+
.object({ code: z.literal(code), ...common, details: genericDetails })
|
|
29
|
+
.strict();
|
|
30
|
+
const scopeSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
capability: z.string(),
|
|
33
|
+
roots: z.array(z.string()),
|
|
34
|
+
executables: z.array(z.string()),
|
|
35
|
+
environment_names: z.array(z.string()),
|
|
36
|
+
network: z.enum(["none", "loopback", "external"]),
|
|
37
|
+
mount: z.boolean(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
const missingScopeSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
roots: z.array(z.string()).optional(),
|
|
43
|
+
executables: z.array(z.string()).optional(),
|
|
44
|
+
environment_names: z.array(z.string()).optional(),
|
|
45
|
+
network: z.enum(["none", "loopback", "external"]).optional(),
|
|
46
|
+
mount: z.literal(true).optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
/** Stable discriminated schema shared by every CLI and MCP error surface. */
|
|
50
|
+
export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
|
|
51
|
+
generic("invalid_request"),
|
|
52
|
+
generic("unreadable_output"),
|
|
53
|
+
generic("capability_unavailable"),
|
|
54
|
+
generic("provider_unavailable"),
|
|
55
|
+
generic("provider_timeout"),
|
|
56
|
+
generic("cancelled"),
|
|
57
|
+
z
|
|
58
|
+
.object({
|
|
59
|
+
code: z.literal("artifact_integrity_mismatch"),
|
|
60
|
+
...common,
|
|
61
|
+
details: z
|
|
62
|
+
.object({
|
|
63
|
+
logical_path: z.string(),
|
|
64
|
+
declared_sha256: z.string().nullable(),
|
|
65
|
+
calculated_sha256: z.string().nullable(),
|
|
66
|
+
unpacked: z.boolean(),
|
|
67
|
+
})
|
|
68
|
+
.strict(),
|
|
69
|
+
})
|
|
70
|
+
.strict(),
|
|
71
|
+
generic("artifact_operation_failed"),
|
|
72
|
+
generic("evidence_integrity_mismatch"),
|
|
73
|
+
generic("truncated"),
|
|
74
|
+
z
|
|
75
|
+
.object({
|
|
76
|
+
code: z.literal("permission_required"),
|
|
77
|
+
...common,
|
|
78
|
+
details: z
|
|
79
|
+
.object({
|
|
80
|
+
capability: z.string(),
|
|
81
|
+
requested: scopeSchema,
|
|
82
|
+
missing: missingScopeSchema,
|
|
83
|
+
ceiling: scopeSchema.nullable(),
|
|
84
|
+
})
|
|
85
|
+
.strict()
|
|
86
|
+
.optional(),
|
|
87
|
+
})
|
|
88
|
+
.strict(),
|
|
89
|
+
generic("process_capture_failed"),
|
|
90
|
+
generic("cleanup_incomplete"),
|
|
91
|
+
generic("revision_conflict"),
|
|
92
|
+
generic("outside_approved_root"),
|
|
93
|
+
generic("configuration_invalid"),
|
|
94
|
+
generic("target_unavailable"),
|
|
95
|
+
generic("execution_failure"),
|
|
96
|
+
]);
|
|
97
|
+
/** JSON Schema document used by generated API documentation and clients. */
|
|
98
|
+
export const analysisErrorJsonSchema = z.toJSONSchema(analysisErrorProjectionSchema);
|
|
@@ -158,11 +158,20 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
|
|
158
158
|
enhanced("analyze_function", "Preferred bounded analysis for one procedure symbol or address. Returns identity, pseudocode, optional assembly, comments, calls, incoming references, and blocks; unsupported outgoing references and CFG edges carry explicit unavailable metadata.", enhancedInputSchemas.analyze_function),
|
|
159
159
|
enhanced("trace_feature", "Trace a bounded literal feature query through matching strings and procedures, xrefs, and truthful containing-procedure resolution. Returns the operation budget, truncation, and residual unknowns; unknown_registry_approved: true records them durably without inferring reference kinds.", enhancedInputSchemas.trace_feature),
|
|
160
160
|
];
|
|
161
|
+
/** Caller observations used to compare a live server with expected identity. */
|
|
162
|
+
export const binarySessionInputSchema = z.object({
|
|
163
|
+
expected_package_version: z.string().min(1).optional(),
|
|
164
|
+
expected_catalog_digest: z
|
|
165
|
+
.string()
|
|
166
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
167
|
+
.optional(),
|
|
168
|
+
expected_server_path: z.string().min(1).optional(),
|
|
169
|
+
});
|
|
161
170
|
/** Target lifecycle tools available only on the long-lived MCP adapter. */
|
|
162
171
|
export const SESSION_TOOL_CONTRACTS = [
|
|
163
172
|
session("open_binary", "Open a local executable, application bundle, archive, JavaScript, source map, plist, or Hopper database after validation. An optional provider-neutral snapshot is imported atomically and must match the binary digest, format, and architecture exactly; provider resources may still start before an MCP cache replay.", openBinaryInputSchema),
|
|
164
173
|
session("close_binary", "Optionally write a provider-neutral analysis snapshot atomically, then close the active target and every provider resource started for it. Snapshot files require an operator-approved root and explicit overwrite; a failed save leaves the session open so cached analysis is not lost.", closeBinaryInputSchema),
|
|
165
|
-
session("binary_session", "Report provider identity, deterministic capability descriptors, and whether a target is open; open targets include canonical path, format, and kind. Use availability, effects, limits, and limitations before selecting analysis operations.",
|
|
174
|
+
session("binary_session", "Report provider identity, deterministic capability descriptors, and whether a target is open; open targets include canonical path, format, and kind. Use availability, effects, limits, and limitations before selecting analysis operations.", binarySessionInputSchema),
|
|
166
175
|
session("export_evidence_bundle", "Return the session's deterministic Evidence v2 bundle, or atomically write it beneath an operator-approved root. Existing files require overwrite: true; records and manifests use canonical byte-stable ordering.", z.object({
|
|
167
176
|
path: z.string().min(1).optional(),
|
|
168
177
|
overwrite: z.boolean().default(false),
|
|
@@ -13,6 +13,7 @@ import { changedBehaviorResultSchema } from "../domain/changedBehavior.js";
|
|
|
13
13
|
import { callPathResultSchema } from "../domain/callPath.js";
|
|
14
14
|
import { staticRuntimeCorrelationResultSchema } from "../domain/staticRuntimeCorrelation.js";
|
|
15
15
|
import { reconstructionVerificationResultSchema } from "../domain/reconstructionVerification.js";
|
|
16
|
+
import { analysisErrorProjectionSchema } from "./errorSchemas.js";
|
|
16
17
|
const resultOf = (schema) => evidenceSchema
|
|
17
18
|
.omit({ normalized_result: true })
|
|
18
19
|
.extend({ normalized_result: schema });
|
|
@@ -78,6 +79,67 @@ const sessionProvider = z.object({
|
|
|
78
79
|
provider: providerIdentity,
|
|
79
80
|
providers: z.array(providerIdentity),
|
|
80
81
|
capabilities: z.array(providerCapability),
|
|
82
|
+
tool_availability: z.array(z.object({
|
|
83
|
+
name: z.string(),
|
|
84
|
+
surface: z.string(),
|
|
85
|
+
available: z.boolean(),
|
|
86
|
+
reason: z.enum([
|
|
87
|
+
"available",
|
|
88
|
+
"target_required",
|
|
89
|
+
"provider_missing",
|
|
90
|
+
"provider_unavailable",
|
|
91
|
+
"target_unsupported",
|
|
92
|
+
"unsupported_host",
|
|
93
|
+
"policy_disabled",
|
|
94
|
+
]),
|
|
95
|
+
remediation: z.string().nullable(),
|
|
96
|
+
annotations: z.object({
|
|
97
|
+
read_only: z.boolean(),
|
|
98
|
+
destructive: z.boolean(),
|
|
99
|
+
idempotent: z.boolean(),
|
|
100
|
+
open_world: z.boolean(),
|
|
101
|
+
}),
|
|
102
|
+
})),
|
|
103
|
+
client_features: z.object({
|
|
104
|
+
elicitation_form: z.boolean(),
|
|
105
|
+
elicitation_url: z.boolean(),
|
|
106
|
+
roots: z.boolean(),
|
|
107
|
+
sampling: z.boolean(),
|
|
108
|
+
}),
|
|
109
|
+
server_identity: z.object({
|
|
110
|
+
package: z.object({
|
|
111
|
+
name: z.string(),
|
|
112
|
+
version: z.string(),
|
|
113
|
+
root_path: z.string(),
|
|
114
|
+
build_commit: z.string().nullable(),
|
|
115
|
+
}),
|
|
116
|
+
server: z.object({
|
|
117
|
+
name: z.string(),
|
|
118
|
+
version: z.string(),
|
|
119
|
+
started_at: z.string(),
|
|
120
|
+
command_path: z.string(),
|
|
121
|
+
}),
|
|
122
|
+
sdk: z.object({
|
|
123
|
+
server: z.string(),
|
|
124
|
+
client_test: z.string(),
|
|
125
|
+
core: z.string(),
|
|
126
|
+
}),
|
|
127
|
+
negotiated_protocol_version: z.string().nullable(),
|
|
128
|
+
client: z.object({ name: z.string(), version: z.string() }).nullable(),
|
|
129
|
+
skill: z.object({ name: z.string(), expected_version: z.string() }),
|
|
130
|
+
catalog: z.record(z.string(), z.json()),
|
|
131
|
+
protocol_features: z.object({
|
|
132
|
+
progress: z.boolean(),
|
|
133
|
+
cancellation: z.boolean(),
|
|
134
|
+
evidence_resources: z.boolean(),
|
|
135
|
+
elicitation: z.boolean(),
|
|
136
|
+
}),
|
|
137
|
+
alignment: z.object({
|
|
138
|
+
state: z.enum(["aligned", "mcp_server_restart_required", "unknown"]),
|
|
139
|
+
reasons: z.array(z.string()),
|
|
140
|
+
remediation: z.string().nullable(),
|
|
141
|
+
}),
|
|
142
|
+
}),
|
|
81
143
|
});
|
|
82
144
|
const nullableText = z.string().nullable();
|
|
83
145
|
const addressList = z.array(z.string());
|
|
@@ -149,36 +211,6 @@ const symbolDiscoveryOutput = (property) => resultOf(z.object({
|
|
|
149
211
|
count: z.number().int().min(0),
|
|
150
212
|
[property]: z.array(addressedEntry),
|
|
151
213
|
}));
|
|
152
|
-
const analysisErrorProjectionSchema = z
|
|
153
|
-
.object({
|
|
154
|
-
category: z.enum([
|
|
155
|
-
"invalid_input",
|
|
156
|
-
"permission_required",
|
|
157
|
-
"unsupported_provider",
|
|
158
|
-
"integrity_mismatch",
|
|
159
|
-
"truncated",
|
|
160
|
-
"cancelled",
|
|
161
|
-
"timeout",
|
|
162
|
-
"unavailable",
|
|
163
|
-
"execution_failure",
|
|
164
|
-
]),
|
|
165
|
-
message: z.string(),
|
|
166
|
-
code: z
|
|
167
|
-
.enum([
|
|
168
|
-
"private_display_unavailable",
|
|
169
|
-
"x11_authorization_failed",
|
|
170
|
-
"unsupported_hopper_build",
|
|
171
|
-
"invalid_launch_command",
|
|
172
|
-
"process_ownership_mismatch",
|
|
173
|
-
"hopper_exited_during_startup",
|
|
174
|
-
"unsupported_demo_dialog",
|
|
175
|
-
"unexpected_display_geometry",
|
|
176
|
-
"x11_input_failed",
|
|
177
|
-
"runtime_dependency_unavailable",
|
|
178
|
-
])
|
|
179
|
-
.optional(),
|
|
180
|
-
})
|
|
181
|
-
.strict();
|
|
182
214
|
const graphNode = z.discriminatedUnion("status", [
|
|
183
215
|
z.object({
|
|
184
216
|
address: z.string(),
|
|
@@ -11,8 +11,15 @@ const comparisonStatusSchema = z.enum([
|
|
|
11
11
|
"changed",
|
|
12
12
|
"truncated",
|
|
13
13
|
"unknown",
|
|
14
|
+
"contradiction",
|
|
15
|
+
]);
|
|
16
|
+
const changeKindSchema = z.enum([
|
|
17
|
+
"added",
|
|
18
|
+
"removed",
|
|
19
|
+
"changed",
|
|
20
|
+
"unknown",
|
|
21
|
+
"contradiction",
|
|
14
22
|
]);
|
|
15
|
-
const changeKindSchema = z.enum(["added", "removed", "changed", "unknown"]);
|
|
16
23
|
const comparisonDimensionSchema = z.enum([
|
|
17
24
|
"content",
|
|
18
25
|
"kind",
|
|
@@ -22,6 +29,7 @@ const comparisonDimensionSchema = z.enum([
|
|
|
22
29
|
"relations",
|
|
23
30
|
"metadata",
|
|
24
31
|
"availability",
|
|
32
|
+
"integrity",
|
|
25
33
|
]);
|
|
26
34
|
/** Strict Evidence-backed input for deterministic artifact comparison. */
|
|
27
35
|
export const artifactComparisonInputSchema = z.object({
|
|
@@ -68,6 +76,7 @@ export const artifactComparisonResultSchema = z.object({
|
|
|
68
76
|
removed: z.number().int().min(0),
|
|
69
77
|
changed: z.number().int().min(0),
|
|
70
78
|
unknown: z.number().int().min(0),
|
|
79
|
+
contradiction: z.number().int().min(0).default(0),
|
|
71
80
|
}),
|
|
72
81
|
changes: z.object({
|
|
73
82
|
items: z.array(artifactChangeSchema).max(500),
|
|
@@ -84,8 +93,8 @@ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) =>
|
|
|
84
93
|
const right = parseArtifactInventoryEvidence(rightEvidence);
|
|
85
94
|
const leftComplete = left.inventory.complete;
|
|
86
95
|
const rightComplete = right.inventory.complete;
|
|
87
|
-
const leftCovered = leftComplete && left.inventory.limitations
|
|
88
|
-
const rightCovered = rightComplete && right.inventory.limitations
|
|
96
|
+
const leftCovered = leftComplete && !hasCoverageLimitation(left.inventory.limitations);
|
|
97
|
+
const rightCovered = rightComplete && !hasCoverageLimitation(right.inventory.limitations);
|
|
89
98
|
const limitations = [
|
|
90
99
|
...left.inventory.limitations.map((item) => `Left: ${item}`),
|
|
91
100
|
...right.inventory.limitations.map((item) => `Right: ${item}`),
|
|
@@ -101,13 +110,15 @@ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) =>
|
|
|
101
110
|
return artifactComparisonResultSchema.parse({
|
|
102
111
|
status: !leftComplete || !rightComplete
|
|
103
112
|
? "truncated"
|
|
104
|
-
:
|
|
105
|
-
? "
|
|
106
|
-
:
|
|
113
|
+
: changes.some(({ classification }) => classification === "contradiction")
|
|
114
|
+
? "contradiction"
|
|
115
|
+
: !leftCovered || !rightCovered
|
|
107
116
|
? "unknown"
|
|
108
|
-
: changes.
|
|
109
|
-
? "
|
|
110
|
-
:
|
|
117
|
+
: changes.some(({ classification }) => classification === "unknown")
|
|
118
|
+
? "unknown"
|
|
119
|
+
: changes.length === 0
|
|
120
|
+
? "unchanged"
|
|
121
|
+
: "changed",
|
|
111
122
|
left_manifest_id: left.inventory.manifest.manifest_id,
|
|
112
123
|
right_manifest_id: right.inventory.manifest.manifest_id,
|
|
113
124
|
summary,
|
|
@@ -172,6 +183,13 @@ const classifyPath = (input) => {
|
|
|
172
183
|
classification: input.complete ? "removed" : "unknown",
|
|
173
184
|
dimensions: [input.complete ? "content" : "availability"],
|
|
174
185
|
};
|
|
186
|
+
if (input.leftOccurrence.hash_status === "mismatched" ||
|
|
187
|
+
input.rightOccurrence.hash_status === "mismatched")
|
|
188
|
+
return {
|
|
189
|
+
...base,
|
|
190
|
+
classification: "contradiction",
|
|
191
|
+
dimensions: ["integrity"],
|
|
192
|
+
};
|
|
175
193
|
if (input.leftOccurrence.hash_status !== "verified" ||
|
|
176
194
|
input.rightOccurrence.hash_status !== "verified" ||
|
|
177
195
|
input.leftNode === undefined ||
|
|
@@ -253,13 +271,20 @@ const relationsByPath = (inventory) => {
|
|
|
253
271
|
return output;
|
|
254
272
|
};
|
|
255
273
|
const summarize = (left, right, changes, covered) => {
|
|
256
|
-
const counts = {
|
|
274
|
+
const counts = {
|
|
275
|
+
added: 0,
|
|
276
|
+
removed: 0,
|
|
277
|
+
changed: 0,
|
|
278
|
+
unknown: 0,
|
|
279
|
+
contradiction: 0,
|
|
280
|
+
};
|
|
257
281
|
for (const change of changes)
|
|
258
282
|
counts[change.classification] += 1;
|
|
259
283
|
return {
|
|
260
284
|
unchanged: covered
|
|
261
285
|
? Math.max(0, Math.min(left.occurrences.length, right.occurrences.length) -
|
|
262
286
|
counts.changed -
|
|
287
|
+
counts.contradiction -
|
|
263
288
|
counts.unknown)
|
|
264
289
|
: 0,
|
|
265
290
|
...counts,
|
|
@@ -268,6 +293,8 @@ const summarize = (left, right, changes, covered) => {
|
|
|
268
293
|
: Math.max(counts.unknown, Math.max(left.manifest.occurrence_count, right.manifest.occurrence_count) -
|
|
269
294
|
counts.added -
|
|
270
295
|
counts.removed -
|
|
271
|
-
counts.changed
|
|
296
|
+
counts.changed -
|
|
297
|
+
counts.contradiction),
|
|
272
298
|
};
|
|
273
299
|
};
|
|
300
|
+
const hasCoverageLimitation = (limitations) => limitations.some((limitation) => !/integrity contradiction\(s\) were recorded/u.test(limitation));
|
|
@@ -5,6 +5,7 @@ const occurrenceIdSchema = z.string().regex(/^occ_[a-f0-9]{64}$/u);
|
|
|
5
5
|
const edgeIdSchema = z.string().regex(/^edge_[a-f0-9]{64}$/u);
|
|
6
6
|
const manifestIdSchema = z.string().regex(/^agm_[a-f0-9]{64}$/u);
|
|
7
7
|
const extractionIdSchema = z.string().regex(/^aex_[a-f0-9]{64}$/u);
|
|
8
|
+
const contradictionIdSchema = z.string().regex(/^ic_[a-f0-9]{64}$/u);
|
|
8
9
|
const boundedRelativePathSchema = z
|
|
9
10
|
.string()
|
|
10
11
|
.min(1)
|
|
@@ -106,10 +107,29 @@ const artifactOccurrenceSchema = z.object({
|
|
|
106
107
|
.nullable(),
|
|
107
108
|
executable: z.boolean(),
|
|
108
109
|
encrypted: z.boolean(),
|
|
109
|
-
hash_status: z.enum([
|
|
110
|
+
hash_status: z.enum([
|
|
111
|
+
"verified",
|
|
112
|
+
"mismatched",
|
|
113
|
+
"not-hashed-limit",
|
|
114
|
+
"unavailable",
|
|
115
|
+
]),
|
|
110
116
|
source_location: artifactByteRangeSchema.nullable(),
|
|
111
117
|
limitations: z.array(z.string()),
|
|
112
118
|
});
|
|
119
|
+
/** Observed bytes that contradict a container's declared integrity identity. */
|
|
120
|
+
const integrityContradictionSchema = z.object({
|
|
121
|
+
contradiction_id: contradictionIdSchema,
|
|
122
|
+
occurrence_id: occurrenceIdSchema,
|
|
123
|
+
parent_artifact_id: artifactIdSchema,
|
|
124
|
+
logical_path: boundedRelativePathSchema,
|
|
125
|
+
declared_sha256: sha256Schema,
|
|
126
|
+
observed_sha256: sha256Schema,
|
|
127
|
+
entry_kind: z.enum(["file", "slice"]),
|
|
128
|
+
unpacked: z.boolean(),
|
|
129
|
+
trust: z.literal("observed-untrusted"),
|
|
130
|
+
provenance: z.literal("container-integrity-metadata-versus-observed-bytes"),
|
|
131
|
+
limitations: z.array(z.string()).min(1),
|
|
132
|
+
});
|
|
113
133
|
/** Parent-child derivation relation between two content-addressed artifacts. */
|
|
114
134
|
const artifactEdgeSchema = z.object({
|
|
115
135
|
edge_id: edgeIdSchema,
|
|
@@ -157,6 +177,10 @@ export const artifactInventoryResultSchema = z.object({
|
|
|
157
177
|
edges: artifactPageSchema(artifactEdgeSchema),
|
|
158
178
|
limits: artifactTraversalLimitsSchema,
|
|
159
179
|
provenance: z.array(artifactCommandSchema).max(256),
|
|
180
|
+
integrity_contradictions: z
|
|
181
|
+
.array(integrityContradictionSchema)
|
|
182
|
+
.max(100)
|
|
183
|
+
.default([]),
|
|
160
184
|
limitations: z.array(z.string()),
|
|
161
185
|
});
|
|
162
186
|
/** One safely materialized artifact beneath caller-approved output root. */
|
|
@@ -39,6 +39,9 @@ const assembleInventorySet = (pages) => {
|
|
|
39
39
|
for (const page of pages) {
|
|
40
40
|
if (canonicalJson(page.inventory.manifest) !== manifestJson)
|
|
41
41
|
throw new TypeError("Artifact inventory pages do not share one manifest");
|
|
42
|
+
if (canonicalJson(page.inventory.integrity_contradictions) !==
|
|
43
|
+
canonicalJson(first.inventory.integrity_contradictions))
|
|
44
|
+
throw new TypeError("Artifact inventory pages do not share integrity contradictions");
|
|
42
45
|
for (const node of page.inventory.nodes.items)
|
|
43
46
|
mergeExact(nodes, node.artifact_id, node, "node");
|
|
44
47
|
for (const occurrence of page.inventory.occurrences.items) {
|
|
@@ -66,6 +69,7 @@ const orderedInventory = (first, pages, values) => {
|
|
|
66
69
|
nodes,
|
|
67
70
|
occurrences,
|
|
68
71
|
edges,
|
|
72
|
+
integrityContradictions: first.integrity_contradictions,
|
|
69
73
|
limitations: [
|
|
70
74
|
...new Set(pages.flatMap(({ inventory }) => inventory.limitations)),
|
|
71
75
|
].sort((left, right) => left.localeCompare(right)),
|
|
@@ -84,10 +88,24 @@ const validateCompleteInventory = (inventory) => {
|
|
|
84
88
|
validateOccurrence(occurrence, inventory, nodeIds, occurrenceIds);
|
|
85
89
|
for (const edge of inventory.edges)
|
|
86
90
|
validateEdge(edge, nodeIds, occurrenceIds);
|
|
91
|
+
for (const contradiction of inventory.integrityContradictions) {
|
|
92
|
+
if (!nodeIds.has(contradiction.parent_artifact_id) ||
|
|
93
|
+
!occurrenceIds.has(contradiction.occurrence_id))
|
|
94
|
+
throw new TypeError("Integrity contradiction references a missing graph member");
|
|
95
|
+
const expectedId = `ic_${digestCanonical({
|
|
96
|
+
root_artifact_id: inventory.manifest.root_artifact_id,
|
|
97
|
+
logical_path: contradiction.logical_path,
|
|
98
|
+
declared_sha256: contradiction.declared_sha256,
|
|
99
|
+
observed_sha256: contradiction.observed_sha256,
|
|
100
|
+
})}`;
|
|
101
|
+
if (contradiction.contradiction_id !== expectedId)
|
|
102
|
+
throw new TypeError("Integrity contradiction ID does not match its identity");
|
|
103
|
+
}
|
|
87
104
|
const graphSha256 = digestCanonical({
|
|
88
105
|
nodes: inventory.nodes,
|
|
89
106
|
occurrences: inventory.occurrences,
|
|
90
107
|
edges: inventory.edges,
|
|
108
|
+
integrity_contradictions: inventory.integrityContradictions,
|
|
91
109
|
});
|
|
92
110
|
if (graphSha256 !== inventory.manifest.graph_sha256)
|
|
93
111
|
throw new TypeError("Artifact graph commitment does not match its members");
|
|
@@ -247,9 +247,11 @@ const artifactFindings = (evidence) => {
|
|
|
247
247
|
dimension: `artifact:${change.logical_path}`,
|
|
248
248
|
scope: "static_candidate",
|
|
249
249
|
status: change.classification,
|
|
250
|
-
classification: change.classification === "
|
|
251
|
-
? "
|
|
252
|
-
: "
|
|
250
|
+
classification: change.classification === "contradiction"
|
|
251
|
+
? "contradiction"
|
|
252
|
+
: change.classification === "unknown"
|
|
253
|
+
? "unresolved_branch"
|
|
254
|
+
: "derived_relationship",
|
|
253
255
|
limitations: result.limitations,
|
|
254
256
|
links: change.evidence_links,
|
|
255
257
|
}));
|