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
@@ -0,0 +1,302 @@
1
+ import { ResourceNotFoundError, ResourceTemplate, } from "@modelcontextprotocol/server";
2
+ import { createHash } from "node:crypto";
3
+ import canonicalize from "canonicalize";
4
+ const evidenceUri = (evidenceId) => `rea://evidence/${evidenceId}`;
5
+ /** Expose immutable session-owned Evidence v2 records as MCP resources. */
6
+ export const registerEvidenceResources = (server, session) => {
7
+ server.registerResource("investigation-workspace-revision", new ResourceTemplate("rea://workspace/{workspaceId}/revision/{revision}", {
8
+ list: () => ({
9
+ resources: session.investigationWorkspaces().map((workspace) => ({
10
+ uri: workspaceUri(workspace.workspace_id, workspace.revision),
11
+ name: `${workspace.workspace_id} revision ${String(workspace.revision)}`,
12
+ title: `${workspace.name} revision ${String(workspace.revision)}`,
13
+ description: "Immutable CAS-linked investigation workspace revision.",
14
+ mimeType: "application/json",
15
+ })),
16
+ }),
17
+ complete: {
18
+ workspaceId: (prefix) => [
19
+ ...new Set(session
20
+ .investigationWorkspaces()
21
+ .map(({ workspace_id }) => workspace_id)),
22
+ ].filter((workspaceId) => workspaceId.startsWith(prefix)),
23
+ revision: (prefix, context) => session
24
+ .investigationWorkspaces()
25
+ .filter(({ workspace_id }) => context?.arguments?.workspaceId === workspace_id)
26
+ .map(({ revision }) => String(revision))
27
+ .filter((revision) => revision.startsWith(prefix)),
28
+ },
29
+ }), {
30
+ title: "Investigation workspace revision",
31
+ description: "Immutable session-retained workspace with CAS linkage.",
32
+ mimeType: "application/json",
33
+ }, (uri, variables) => {
34
+ const workspaceId = stringVariable(variables.workspaceId, uri.href);
35
+ const revisionText = stringVariable(variables.revision, uri.href);
36
+ if (!/^\d+$/u.test(revisionText))
37
+ throw new ResourceNotFoundError(uri.href);
38
+ const workspace = session.investigationWorkspace(workspaceId, Number(revisionText));
39
+ if (workspace === undefined)
40
+ throw new ResourceNotFoundError(uri.href);
41
+ return jsonResource(uri.href, workspace);
42
+ });
43
+ server.registerResource("session-evidence", new ResourceTemplate("rea://evidence/{evidenceId}", {
44
+ list: () => ({
45
+ resources: session.exportEvidenceBundle().records.map((evidence) => ({
46
+ uri: evidenceUri(evidence.evidence_id),
47
+ name: evidence.evidence_id,
48
+ title: `${evidence.operation} evidence`,
49
+ description: `Session-owned Evidence v2 record for ${evidence.operation}.`,
50
+ mimeType: "application/json",
51
+ })),
52
+ }),
53
+ complete: {
54
+ evidenceId: (prefix) => session
55
+ .exportEvidenceBundle()
56
+ .records.map(({ evidence_id }) => evidence_id)
57
+ .filter((evidenceId) => evidenceId.startsWith(prefix)),
58
+ },
59
+ }), {
60
+ title: "Session evidence",
61
+ description: "Immutable Evidence v2 records owned by this REA session.",
62
+ mimeType: "application/json",
63
+ }, (uri, variables) => {
64
+ const evidenceId = variables.evidenceId;
65
+ if (typeof evidenceId !== "string")
66
+ throw new ResourceNotFoundError(uri.href);
67
+ const evidence = session.evidenceById(evidenceId);
68
+ if (evidence === undefined)
69
+ throw new ResourceNotFoundError(uri.href);
70
+ return {
71
+ contents: [
72
+ {
73
+ uri: uri.href,
74
+ mimeType: "application/json",
75
+ text: JSON.stringify(evidence, null, 2),
76
+ },
77
+ ],
78
+ };
79
+ });
80
+ server.registerResource("active-residual-unknowns", "rea://unknowns/active", {
81
+ title: "Active residual unknowns",
82
+ description: "Current unresolved session-owned residual unknown heads.",
83
+ mimeType: "application/json",
84
+ }, (uri) => jsonResource(uri.href, session.listUnknowns().filter(({ status }) => status !== "resolved")));
85
+ server.registerResource("analysis-snapshot", new ResourceTemplate("rea://snapshot/{snapshotDigest}", {
86
+ list: () => {
87
+ const snapshot = session.exportAnalysisSnapshot();
88
+ if (!snapshot.ok)
89
+ return { resources: [] };
90
+ const digest = contentDigest(snapshot.value);
91
+ return {
92
+ resources: [
93
+ {
94
+ uri: `rea://snapshot/${digest}`,
95
+ name: digest,
96
+ title: "Current analysis snapshot",
97
+ description: "Immutable projection of the current target cache.",
98
+ mimeType: "application/json",
99
+ },
100
+ ],
101
+ };
102
+ },
103
+ }), {
104
+ title: "Analysis snapshot",
105
+ description: "Current provider-neutral analysis snapshot by content digest.",
106
+ mimeType: "application/json",
107
+ }, (uri, variables) => {
108
+ const requested = stringVariable(variables.snapshotDigest, uri.href);
109
+ const snapshot = session.exportAnalysisSnapshot();
110
+ if (!snapshot.ok || contentDigest(snapshot.value) !== requested)
111
+ throw new ResourceNotFoundError(uri.href);
112
+ return jsonResource(uri.href, snapshot.value);
113
+ });
114
+ server.registerResource("artifact-page", new ResourceTemplate("rea://artifact/{manifestId}/{collection}", {
115
+ list: undefined,
116
+ complete: {
117
+ collection: (prefix) => ["nodes", "occurrences", "edges"].filter((value) => value.startsWith(prefix)),
118
+ },
119
+ }), {
120
+ title: "Artifact graph page",
121
+ description: "Deterministically paged artifact graph collection.",
122
+ mimeType: "application/json",
123
+ }, (uri, variables) => {
124
+ const manifestId = stringVariable(variables.manifestId, uri.href);
125
+ const collection = stringVariable(variables.collection, uri.href);
126
+ if (!ARTIFACT_COLLECTIONS.has(collection))
127
+ throw new ResourceNotFoundError(uri.href);
128
+ const evidence = session
129
+ .exportEvidenceBundle()
130
+ .records.find((record) => artifactManifestId(record.normalized_result) === manifestId);
131
+ if (evidence === undefined)
132
+ throw new ResourceNotFoundError(uri.href);
133
+ const result = objectValue(evidence.normalized_result);
134
+ const page = result?.[collection];
135
+ if (page === undefined)
136
+ throw new ResourceNotFoundError(uri.href);
137
+ return jsonResource(uri.href, {
138
+ evidence_id: evidence.evidence_id,
139
+ manifest_id: manifestId,
140
+ collection,
141
+ page,
142
+ provider: evidence.provider,
143
+ limitations: evidence.limitations,
144
+ });
145
+ });
146
+ server.registerResource("function-dossier", new ResourceTemplate("rea://function/{targetSha256}/{address}", {
147
+ list: undefined,
148
+ }), {
149
+ title: "Function dossier",
150
+ description: "Session-owned analyze_function dossier with provenance.",
151
+ mimeType: "application/json",
152
+ }, (uri, variables) => {
153
+ const targetSha256 = stringVariable(variables.targetSha256, uri.href);
154
+ const address = stringVariable(variables.address, uri.href);
155
+ const evidence = session
156
+ .exportEvidenceBundle()
157
+ .records.find((record) => record.operation === "analyze_function" &&
158
+ record.subject?.digest.sha256 === targetSha256 &&
159
+ functionAddress(record.normalized_result) === address);
160
+ if (evidence === undefined)
161
+ throw new ResourceNotFoundError(uri.href);
162
+ return jsonResource(uri.href, evidence);
163
+ });
164
+ server.registerResource("evidence-section", new ResourceTemplate("rea://evidence/{evidenceId}/section/{section}", {
165
+ list: undefined,
166
+ complete: {
167
+ evidenceId: (prefix) => evidenceIds(session, prefix),
168
+ section: (prefix) => EVIDENCE_SECTIONS.filter((section) => section.startsWith(prefix)),
169
+ },
170
+ }), {
171
+ title: "Evidence section",
172
+ description: "One independently useful bounded section of session Evidence v2.",
173
+ mimeType: "application/json",
174
+ }, (uri, variables) => {
175
+ const evidenceId = stringVariable(variables.evidenceId, uri.href);
176
+ const section = stringVariable(variables.section, uri.href);
177
+ const evidence = session.evidenceById(evidenceId);
178
+ if (evidence === undefined)
179
+ throw new ResourceNotFoundError(uri.href);
180
+ const value = evidenceSection(evidence.normalized_result, section);
181
+ if (value === undefined)
182
+ throw new ResourceNotFoundError(uri.href);
183
+ return jsonResource(uri.href, {
184
+ evidence_id: evidence.evidence_id,
185
+ operation: evidence.operation,
186
+ authority: evidence.authority,
187
+ confidence: evidence.confidence,
188
+ provider: evidence.provider,
189
+ limitations: evidence.limitations,
190
+ section,
191
+ value,
192
+ });
193
+ });
194
+ server.registerResource("residual-unknown", new ResourceTemplate("rea://unknown/{unknownId}", {
195
+ list: () => ({
196
+ resources: session.listUnknowns().map((unknown) => ({
197
+ uri: `rea://unknown/${unknown.unknown_id}`,
198
+ name: unknown.unknown_id,
199
+ title: unknown.question,
200
+ description: `Current ${unknown.status} residual unknown revision.`,
201
+ mimeType: "application/json",
202
+ })),
203
+ }),
204
+ complete: {
205
+ unknownId: (prefix) => session
206
+ .listUnknowns()
207
+ .map(({ unknown_id }) => unknown_id)
208
+ .filter((unknownId) => unknownId.startsWith(prefix)),
209
+ },
210
+ }), {
211
+ title: "Residual unknown",
212
+ description: "Current session-owned residual unknown head.",
213
+ mimeType: "application/json",
214
+ }, (uri, variables) => {
215
+ const unknownId = stringVariable(variables.unknownId, uri.href);
216
+ const unknown = session
217
+ .listUnknowns()
218
+ .find(({ unknown_id }) => unknown_id === unknownId);
219
+ if (unknown === undefined)
220
+ throw new ResourceNotFoundError(uri.href);
221
+ const history = session
222
+ .exportEvidenceBundle()
223
+ .unknowns.filter(({ unknown_id }) => unknown_id === unknownId);
224
+ return jsonResource(uri.href, { current: unknown, history });
225
+ });
226
+ };
227
+ const workspaceUri = (workspaceId, revision) => `rea://workspace/${workspaceId}/revision/${String(revision)}`;
228
+ const EVIDENCE_SECTIONS = [
229
+ "result",
230
+ "terminal",
231
+ "filesystem",
232
+ "process",
233
+ "protocol",
234
+ "nodes",
235
+ "occurrences",
236
+ "edges",
237
+ ];
238
+ const evidenceIds = (session, prefix) => session
239
+ .exportEvidenceBundle()
240
+ .records.map(({ evidence_id }) => evidence_id)
241
+ .filter((evidenceId) => evidenceId.startsWith(prefix));
242
+ const stringVariable = (value, uri) => {
243
+ if (typeof value !== "string")
244
+ throw new ResourceNotFoundError(uri);
245
+ return value;
246
+ };
247
+ const evidenceSection = (result, section) => {
248
+ if (section === "result")
249
+ return result;
250
+ if (typeof result !== "object" || result === null || Array.isArray(result))
251
+ return undefined;
252
+ if (section === "terminal")
253
+ return pick(result, ["frames", "rendered_frames", "interaction_events"]);
254
+ if (section === "filesystem")
255
+ return pick(result, [
256
+ "filesystem_checkpoints",
257
+ "filesystem_effects",
258
+ "files_before",
259
+ "files_after",
260
+ ]);
261
+ if (section === "process")
262
+ return pick(result, ["process_samples", "exit", "settlement", "cleanup"]);
263
+ if (section === "protocol")
264
+ return pick(result, ["protocol_events", "shim_events"]);
265
+ const value = result[section];
266
+ return value === undefined ? undefined : value;
267
+ };
268
+ const pick = (result, names) => {
269
+ const entries = names.flatMap((name) => result[name] === undefined ? [] : [[name, result[name]]]);
270
+ return entries.length === 0 ? undefined : Object.fromEntries(entries);
271
+ };
272
+ const jsonResource = (uri, value) => ({
273
+ contents: [
274
+ {
275
+ uri,
276
+ mimeType: "application/json",
277
+ text: JSON.stringify(value, null, 2),
278
+ },
279
+ ],
280
+ });
281
+ const ARTIFACT_COLLECTIONS = new Set(["nodes", "occurrences", "edges"]);
282
+ const objectValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
283
+ ? value
284
+ : undefined;
285
+ const artifactManifestId = (value) => {
286
+ const manifest = objectValue(objectValue(value)?.manifest ?? null);
287
+ return typeof manifest?.manifest_id === "string"
288
+ ? manifest.manifest_id
289
+ : undefined;
290
+ };
291
+ const functionAddress = (value) => {
292
+ const result = objectValue(value);
293
+ const procedure = objectValue(result?.procedure ?? null);
294
+ const candidate = procedure?.address ?? result?.address;
295
+ return typeof candidate === "string" ? candidate : undefined;
296
+ };
297
+ const contentDigest = (value) => {
298
+ const encoded = canonicalize(value);
299
+ if (encoded === undefined)
300
+ throw new TypeError("Resource is not canonical JSON");
301
+ return createHash("sha256").update(encoded).digest("hex");
302
+ };
@@ -3,14 +3,44 @@ import { jsonObjectSchema } from "../domain/jsonValue.js";
3
3
  import { logToolExecution } from "./toolLogging.js";
4
4
  import { toCallToolResult } from "./toolResult.js";
5
5
  import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
6
+ import { mcpProgressReporter } from "./mcpProgress.js";
7
+ import { AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
8
+ import { err } from "../domain/result.js";
6
9
  /** Register provider-backed contracts that return atomic Evidence v2 observations. */
7
10
  export const registerEvidenceTools = (server, analysis, contracts, options) => {
8
11
  for (const contract of contracts) {
9
12
  server.registerTool(contract.name, toolRegistrationOptions(contract), async (input, context) => {
13
+ const progress = mcpProgressReporter(context);
14
+ await progress.report({
15
+ phase: contract.name,
16
+ completed: 0,
17
+ total: 1,
18
+ message: "started",
19
+ });
10
20
  const parameters = jsonObjectSchema.parse(contract.inputSchema.parse(input));
21
+ if (options.permissionAuthority !== undefined) {
22
+ const request = permissionRequest(contract.name, parameters);
23
+ if (request !== undefined) {
24
+ const authorized = await options.permissionAuthority.authorize(request, contract.name === "extract_artifact" ? "write" : "read");
25
+ if (!authorized.ok)
26
+ return toCallToolResult(err(authorized.error instanceof PermissionRequiredError
27
+ ? authorized.error
28
+ : new AnalysisProtocolError(authorized.error.message, {
29
+ cause: authorized.error,
30
+ })), contract);
31
+ }
32
+ }
11
33
  const execution = await logToolExecution(options.logger, contract.name, () => analysis.execute(contract.name, parameters, {
12
34
  signal: context.mcpReq.signal,
35
+ progress,
13
36
  }));
37
+ await progress.report({
38
+ phase: contract.name,
39
+ completed: 1,
40
+ total: 1,
41
+ message: execution.ok ? "completed" : "failed",
42
+ terminal: true,
43
+ });
14
44
  if (!execution.ok)
15
45
  return toCallToolResult(execution, contract);
16
46
  const evidence = createEvidence(execution.value.subject ?? options.activeTarget?.(), execution.value.provider, {
@@ -24,7 +54,34 @@ export const registerEvidenceTools = (server, analysis, contracts, options) => {
24
54
  const recorded = options.recordEvidence?.(evidence);
25
55
  return recorded !== undefined && !recorded.ok
26
56
  ? toCallToolResult(recorded, contract)
27
- : toCallToolResult({ ok: true, value: evidence }, contract);
57
+ : toCallToolResult({ ok: true, value: evidence }, contract, {
58
+ evidenceResourcesAvailable: recorded !== undefined,
59
+ });
28
60
  });
29
61
  }
30
62
  };
63
+ const permissionRequest = (operation, parameters) => {
64
+ if (operation === "extract_artifact" &&
65
+ typeof parameters.output_root === "string")
66
+ return {
67
+ capability: "artifact_extract",
68
+ roots: [parameters.output_root],
69
+ executables: [],
70
+ environment_names: [],
71
+ network: "none",
72
+ mount: false,
73
+ operation_identity: `extract_artifact:${parameters.output_root}`,
74
+ };
75
+ if (operation === "inventory_artifact" &&
76
+ parameters.native_mount_approved === true)
77
+ return {
78
+ capability: "native_mount",
79
+ roots: [],
80
+ executables: [],
81
+ environment_names: [],
82
+ network: "none",
83
+ mount: true,
84
+ operation_identity: "inventory_artifact:native_mount",
85
+ };
86
+ return undefined;
87
+ };
@@ -6,9 +6,10 @@ import { toCallToolResult } from "./toolResult.js";
6
6
  import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
7
7
  import { resolveSessionEvidence } from "./sessionEvidence.js";
8
8
  import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
9
+ import { runDerivedOperation } from "./runDerivedOperation.js";
9
10
  /** Register explicit Evidence-backed function comparison. */
10
11
  export const registerFunctionComparisonTool = (server, session, contract) => {
11
- server.registerTool(contract.name, toolRegistrationOptions(contract), (input) => {
12
+ server.registerTool(contract.name, toolRegistrationOptions(contract), async (input, context) => {
12
13
  const parsed = functionComparisonInputSchema.parse(input);
13
14
  const left = resolveSessionEvidence(session, parsed.left);
14
15
  if (!left.ok)
@@ -18,7 +19,10 @@ export const registerFunctionComparisonTool = (server, session, contract) => {
18
19
  return toCallToolResult(right, contract);
19
20
  const leftIds = evidenceIds(left.value);
20
21
  const rightIds = evidenceIds(right.value);
21
- const comparison = compareFunctions(left.value, right.value, parsed.offset, parsed.limit);
22
+ const computed = await runDerivedOperation(context, contract.name, () => compareFunctions(left.value, right.value, parsed.offset, parsed.limit));
23
+ if (!computed.ok)
24
+ return toCallToolResult(computed, contract);
25
+ const comparison = computed.value;
22
26
  const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, {
23
27
  predicateType: "rea.function-comparison/v1",
24
28
  operation: contract.name,
@@ -10,6 +10,9 @@ import { err, ok } from "../domain/result.js";
10
10
  import { CALL_PATH_PROVIDER, CHANGED_BEHAVIOR_PROVIDER, RECONSTRUCTION_PROVIDER, STATIC_RUNTIME_PROVIDER, } from "./sessionToolPolicies.js";
11
11
  import { toCallToolResult } from "./toolResult.js";
12
12
  import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
13
+ import { AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
14
+ import { mcpProgressReporter } from "./mcpProgress.js";
15
+ import { runDerivedOperation } from "./runDerivedOperation.js";
13
16
  /** Register Evidence-composed differential investigation workflows. */
14
17
  export const registerInvestigationTools = (server, session, contracts, policies) => {
15
18
  registerChangedBehavior(server, session, contracts[0], policies);
@@ -17,17 +20,62 @@ export const registerInvestigationTools = (server, session, contracts, policies)
17
20
  registerStaticRuntime(server, session, contracts[2]);
18
21
  registerReconstruction(server, session, contracts[3]);
19
22
  };
20
- const investigationExecution = (session, signal, inputRoots) => ({ session, signal, inputRoots });
23
+ const investigationExecution = (session, signal, inputRoots, integrityContinueEnabled, progress) => ({
24
+ session,
25
+ signal,
26
+ inputRoots,
27
+ integrityContinueEnabled,
28
+ ...(progress === undefined ? {} : { progress }),
29
+ });
21
30
  const isIncomplete = (status) => status === "unknown" || status === "truncated";
22
31
  const registerChangedBehavior = (server, session, contract, policies) => {
23
32
  server.registerTool(contract.name, contractOptions(contract), async (input, context) => {
24
33
  const parsed = changedBehaviorInputSchema.parse(input);
25
34
  if (parsed.investigation_run !== undefined) {
26
- const investigated = await runCrossVersionInvestigation(parsed.investigation_run, policies.evidenceFiles, investigationExecution(session, context.mcpReq.signal, policies.inputRoots));
35
+ const progress = mcpProgressReporter(context);
36
+ if (policies.permissionAuthority !== undefined) {
37
+ const scopes = [
38
+ {
39
+ capability: "investigation_workspace_read",
40
+ roots: [parsed.investigation_run.workspace_path],
41
+ },
42
+ {
43
+ capability: "investigation_workspace_write",
44
+ roots: [parsed.investigation_run.workspace_path],
45
+ },
46
+ {
47
+ capability: "investigation_input",
48
+ roots: [
49
+ parsed.investigation_run.left_path,
50
+ parsed.investigation_run.right_path,
51
+ ],
52
+ },
53
+ ];
54
+ for (const scope of scopes) {
55
+ const authorized = await policies.permissionAuthority.authorize({
56
+ ...scope,
57
+ executables: [],
58
+ environment_names: [],
59
+ network: "none",
60
+ mount: false,
61
+ operation_identity: `${contract.name}:${scope.capability}:${scope.roots.join(":")}`,
62
+ }, scope.capability === "investigation_input" ? "read" : "write");
63
+ if (!authorized.ok)
64
+ return toCallToolResult(err(authorized.error instanceof PermissionRequiredError
65
+ ? authorized.error
66
+ : new AnalysisProtocolError(authorized.error.message, {
67
+ cause: authorized.error,
68
+ })), contract);
69
+ }
70
+ }
71
+ const investigated = await runCrossVersionInvestigation(parsed.investigation_run, policies.evidenceFiles, investigationExecution(session, context.mcpReq.signal, policies.inputRoots, policies.integrityContinueEnabled?.() ?? false, progress));
27
72
  if (!investigated.ok)
28
73
  return toCallToolResult(investigated, contract);
74
+ const workspace = investigated.value.workspace;
75
+ if (session.retainInvestigationWorkspace(workspace) === "added")
76
+ server.sendResourceListChanged();
29
77
  const result = changedBehaviorResultSchema.parse(investigated.value.evidence.normalized_result);
30
- return toCallToolResult(recordWorkflowEvidence(session, investigated.value.evidence, parsed.unknown_registry_approved, isIncomplete(result.behavior_status), {
78
+ const toolResult = toCallToolResult(recordWorkflowEvidence(session, investigated.value.evidence, parsed.unknown_registry_approved, isIncomplete(result.behavior_status), {
31
79
  question: `Automatic changed behavior run ${result.investigation_run?.run_id ?? "unknown"} remains ${result.behavior_status}`,
32
80
  domain: "changed-behavior",
33
81
  requiredAuthority: "controlled-replay",
@@ -39,12 +87,28 @@ const registerChangedBehavior = (server, session, contract, policies) => {
39
87
  },
40
88
  ],
41
89
  }), contract);
90
+ return {
91
+ ...toolResult,
92
+ content: [
93
+ ...toolResult.content,
94
+ {
95
+ type: "resource_link",
96
+ uri: `rea://workspace/${workspace.workspace_id}/revision/${String(workspace.revision)}`,
97
+ name: `${workspace.workspace_id} revision ${String(workspace.revision)}`,
98
+ description: "Immutable CAS-linked investigation workspace revision",
99
+ mimeType: "application/json",
100
+ },
101
+ ],
102
+ };
42
103
  }
43
104
  const closure = evidenceClosure(session, comparisonClosure(parsed.comparisons));
44
105
  if (!closure.ok)
45
106
  return toCallToolResult(closure, contract);
46
107
  const links = closure.value;
47
- const result = findChangedBehavior(parsed.comparisons, parsed.offset, parsed.limit);
108
+ const computed = await runDerivedOperation(context, contract.name, () => findChangedBehavior(parsed.comparisons, parsed.offset, parsed.limit));
109
+ if (!computed.ok)
110
+ return toCallToolResult(computed, contract);
111
+ const result = computed.value;
48
112
  const evidence = createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
49
113
  predicateType: "rea.changed-behavior/v1",
50
114
  operation: contract.name,
@@ -75,13 +139,16 @@ const registerChangedBehavior = (server, session, contract, policies) => {
75
139
  });
76
140
  };
77
141
  const registerCallPath = (server, session, contract) => {
78
- server.registerTool(contract.name, contractOptions(contract), (input) => {
142
+ server.registerTool(contract.name, contractOptions(contract), async (input, context) => {
79
143
  const parsed = callPathInputSchema.parse(input);
80
144
  const closure = evidenceClosure(session, functionEvidenceIds(parsed.functions));
81
145
  if (!closure.ok)
82
146
  return toCallToolResult(closure, contract);
83
147
  const links = closure.value;
84
- const result = buildCallPath(parsed);
148
+ const computed = await runDerivedOperation(context, contract.name, () => buildCallPath(parsed));
149
+ if (!computed.ok)
150
+ return toCallToolResult(computed, contract);
151
+ const result = computed.value;
85
152
  const evidence = createEvidence(undefined, CALL_PATH_PROVIDER, {
86
153
  predicateType: "rea.call-path/v1",
87
154
  operation: contract.name,
@@ -115,7 +182,7 @@ const registerCallPath = (server, session, contract) => {
115
182
  });
116
183
  };
117
184
  const registerStaticRuntime = (server, session, contract) => {
118
- server.registerTool(contract.name, contractOptions(contract), (input) => {
185
+ server.registerTool(contract.name, contractOptions(contract), async (input, context) => {
119
186
  const parsed = staticRuntimeCorrelationInputSchema.parse(input);
120
187
  const closure = evidenceClosure(session, comparisonClosure([
121
188
  ...parsed.static_comparisons,
@@ -124,7 +191,10 @@ const registerStaticRuntime = (server, session, contract) => {
124
191
  if (!closure.ok)
125
192
  return toCallToolResult(closure, contract);
126
193
  const links = closure.value;
127
- const result = correlateStaticAndRuntime(parsed);
194
+ const computed = await runDerivedOperation(context, contract.name, () => correlateStaticAndRuntime(parsed));
195
+ if (!computed.ok)
196
+ return toCallToolResult(computed, contract);
197
+ const result = computed.value;
128
198
  const evidence = createEvidence(undefined, STATIC_RUNTIME_PROVIDER, {
129
199
  predicateType: "rea.static-runtime-correlation/v1",
130
200
  operation: contract.name,
@@ -154,7 +224,7 @@ const registerStaticRuntime = (server, session, contract) => {
154
224
  });
155
225
  };
156
226
  const registerReconstruction = (server, session, contract) => {
157
- server.registerTool(contract.name, contractOptions(contract), (input) => {
227
+ server.registerTool(contract.name, contractOptions(contract), async (input, context) => {
158
228
  const parsed = reconstructionVerificationInputSchema.parse(input);
159
229
  const owned = session.exportEvidenceBundle();
160
230
  const ownedUnknowns = new Set(owned.unknowns.map(({ revision_digest: digest }) => digest));
@@ -162,7 +232,10 @@ const registerReconstruction = (server, session, contract) => {
162
232
  if (parsed.evidence_bundle.records.some(({ evidence_id: id }) => !ownedEvidenceIds.has(id)) ||
163
233
  parsed.evidence_bundle.unknowns.some(({ revision_digest: digest }) => !ownedUnknowns.has(digest)))
164
234
  return toCallToolResult(err(new EvidenceIntegrityError("Reconstruction input unknown history is not present in this session")), contract);
165
- const result = verifyReconstruction(parsed.specification, owned, parsed.offset, parsed.limit);
235
+ const computed = await runDerivedOperation(context, contract.name, () => verifyReconstruction(parsed.specification, owned, parsed.offset, parsed.limit));
236
+ if (!computed.ok)
237
+ return toCallToolResult(computed, contract);
238
+ const result = computed.value;
166
239
  const closure = evidenceClosure(session, result.evidence_links);
167
240
  if (!closure.ok)
168
241
  return toCallToolResult(closure, contract);
@@ -4,6 +4,7 @@ import { toCallToolResult } from "./toolResult.js";
4
4
  import { logToolExecution } from "./toolLogging.js";
5
5
  import { createEvidence } from "../domain/evidence.js";
6
6
  import { AnalysisCapabilityUnavailableError, UnknownRegistryError, } from "../domain/errors.js";
7
+ import { mcpProgressReporter } from "./mcpProgress.js";
7
8
  /** Register direct bridge proxies, preserving MCP cancellation and typed errors. */
8
9
  export const registerOfficialTools = (server, analysis, options) => {
9
10
  for (const contract of OFFICIAL_TOOL_CONTRACTS) {
@@ -23,9 +24,24 @@ const registerOfficialTool = (server, analysis, contract, registration) => {
23
24
  annotations: contract.annotations,
24
25
  }, async (input, context) => {
25
26
  const arguments_ = projectOfficialArguments(contract, input);
27
+ const progress = mcpProgressReporter(context);
28
+ await progress.report({
29
+ phase: contract.name,
30
+ completed: 0,
31
+ total: 1,
32
+ message: "started",
33
+ });
26
34
  const result = await logToolExecution(registration.logger, contract.name, () => analysis.execute(contract.name, arguments_, {
27
35
  signal: context.mcpReq.signal,
36
+ progress,
28
37
  }));
38
+ await progress.report({
39
+ phase: contract.name,
40
+ completed: 1,
41
+ total: 1,
42
+ message: result.ok ? "completed" : "failed",
43
+ terminal: true,
44
+ });
29
45
  if (result.ok) {
30
46
  const evidence = createEvidence(result.value.subject ?? registration.activeTarget?.(), result.value.provider, {
31
47
  operation: contract.name,
@@ -38,7 +54,9 @@ const registerOfficialTool = (server, analysis, contract, registration) => {
38
54
  const recorded = registration.recordEvidence?.(evidence);
39
55
  if (recorded !== undefined && !recorded.ok)
40
56
  return toCallToolResult(recorded, contract);
41
- return toCallToolResult({ ok: true, value: evidence }, contract);
57
+ return toCallToolResult({ ok: true, value: evidence }, contract, {
58
+ evidenceResourcesAvailable: recorded !== undefined,
59
+ });
42
60
  }
43
61
  if (result.error instanceof AnalysisCapabilityUnavailableError &&
44
62
  approvedUnknownTracking(input) &&
@@ -10,9 +10,10 @@ import { err } from "../domain/result.js";
10
10
  import { PROCESS_PROVIDER } from "./sessionToolPolicies.js";
11
11
  import { toCallToolResult } from "./toolResult.js";
12
12
  import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
13
+ import { runDerivedOperation } from "./runDerivedOperation.js";
13
14
  /** Register deterministic process-capture comparison and contradiction tracking. */
14
15
  export const registerProcessComparisonTool = (server, session, contract, now = Date.now) => {
15
- server.registerTool(contract.name, toolRegistrationOptions(contract), (input) => {
16
+ server.registerTool(contract.name, toolRegistrationOptions(contract), async (input, context) => {
16
17
  const parsed = z
17
18
  .object({
18
19
  left_evidence_id: z.string(),
@@ -28,12 +29,15 @@ export const registerProcessComparisonTool = (server, session, contract, now = D
28
29
  return toCallToolResult(validated, contract);
29
30
  let comparison;
30
31
  try {
31
- comparison = compareProcessCaptures(parsed.left, parsed.right, {
32
+ const computed = await runDerivedOperation(context, contract.name, () => compareProcessCaptures(parsed.left, parsed.right, {
32
33
  ...(parsed.max_capture_age_ms === undefined
33
34
  ? {}
34
35
  : { maxCaptureAgeMs: parsed.max_capture_age_ms }),
35
36
  now,
36
- });
37
+ }));
38
+ if (!computed.ok)
39
+ return toCallToolResult(computed, contract);
40
+ comparison = computed.value;
37
41
  }
38
42
  catch (cause) {
39
43
  return toCallToolResult(err(new EvidenceIntegrityError(cause instanceof Error