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.
Files changed (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +149 -2
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/referenceSourcePolicy.js +1 -0
  79. package/dist/domain/residualUnknown.js +239 -0
  80. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  81. package/dist/hopper/BridgeLauncher.js +40 -3
  82. package/dist/hopper/HopperClient.js +14 -5
  83. package/dist/hopper/HopperProvider.js +57 -22
  84. package/dist/identity.js +1 -0
  85. package/dist/main.js +5 -1
  86. package/dist/native/CommandRunner.js +156 -0
  87. package/dist/native/NativeMacOSProvider.js +306 -0
  88. package/dist/native/NativeMachoInspection.js +135 -0
  89. package/dist/native/parsers/codesign.js +55 -0
  90. package/dist/native/parsers/demangle.js +26 -0
  91. package/dist/native/parsers/dyldInfo.js +25 -0
  92. package/dist/native/parsers/lipo.js +67 -0
  93. package/dist/native/parsers/otool.js +193 -0
  94. package/dist/native/parsers/plist.js +23 -0
  95. package/dist/reference/ReferenceSourceReader.js +73 -0
  96. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  97. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  98. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  99. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  100. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  101. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  102. package/dist/server/createServer.js +31 -5
  103. package/dist/server/recordDerivedEvidence.js +10 -0
  104. package/dist/server/registerArtifactComparisonTool.js +62 -0
  105. package/dist/server/registerArtifactTools.js +6 -0
  106. package/dist/server/registerBundleComparisonTool.js +47 -0
  107. package/dist/server/registerEnhancedTools.js +64 -12
  108. package/dist/server/registerEvidenceTools.js +36 -0
  109. package/dist/server/registerFunctionComparisonTool.js +68 -0
  110. package/dist/server/registerInvestigationTools.js +224 -0
  111. package/dist/server/registerNativeTools.js +6 -0
  112. package/dist/server/registerOfficialTools.js +47 -14
  113. package/dist/server/registerProcessComparisonTool.js +106 -0
  114. package/dist/server/registerSessionTools.js +179 -70
  115. package/dist/server/sessionEvidence.js +28 -0
  116. package/dist/server/sessionToolPolicies.js +64 -0
  117. package/dist/server/toolRegistrationOptions.js +7 -0
  118. package/dist/server/toolResult.js +8 -5
  119. package/install.sh +198 -0
  120. package/package.json +18 -1
  121. package/scripts/rea.mjs +5 -1
  122. package/skills/rea-analysis/SKILL.md +77 -2
@@ -0,0 +1,206 @@
1
+ import { readdir, readlink, realpath } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { entryFailure, safeSize } from "./ReferenceSourceReaderErrors.js";
4
+ import { compareNames, pathFromRoot, withinRoot, } from "./ReferenceSourceReaderPaths.js";
5
+ import { readStableFile } from "./ReferenceSourceReaderFile.js";
6
+ import { isAborted, sameFile, validateDirectory, bigLstat, } from "./ReferenceSourceReaderValidate.js";
7
+ import {} from "./ReferenceSourceReaderTypes.js";
8
+ export const traverseDirectory = async (state, current) => {
9
+ const before = await validateDirectory(state.root, state.rootIdentity, current.path);
10
+ if (!before.ok) {
11
+ state.entries.push(entryFailure(pathFromRoot(state.root, current.path), "directory", before.code, before.message));
12
+ return { ok: true, value: undefined };
13
+ }
14
+ const names = await readDirectoryNames(current.path);
15
+ if (!names.ok) {
16
+ state.entries.push(entryFailure(pathFromRoot(state.root, current.path), "directory", "io", names.message));
17
+ return { ok: true, value: undefined };
18
+ }
19
+ if (isAborted(state.signal))
20
+ return {
21
+ ok: false,
22
+ error: {
23
+ tag: "reference-source-reader",
24
+ code: "cancelled",
25
+ message: "Reference source traversal cancelled",
26
+ },
27
+ };
28
+ const after = await validateDirectory(state.root, state.rootIdentity, current.path);
29
+ if (!after.ok || !sameFile(before.stats, after.stats)) {
30
+ state.entries.push(entryFailure(pathFromRoot(state.root, current.path), "directory", "changed", "Directory changed while it was read"));
31
+ return { ok: true, value: undefined };
32
+ }
33
+ const directories = [];
34
+ for (const name of names.value) {
35
+ const result = await processEntry(state, current, name, directories);
36
+ if (!result.ok)
37
+ return result;
38
+ if (state.stopped)
39
+ break;
40
+ }
41
+ directories.reverse();
42
+ state.pending.push(...directories);
43
+ return { ok: true, value: undefined };
44
+ };
45
+ const readDirectoryNames = async (path) => {
46
+ try {
47
+ return { ok: true, value: (await readdir(path)).sort(compareNames) };
48
+ }
49
+ catch {
50
+ return { ok: false, message: "Directory could not be read" };
51
+ }
52
+ };
53
+ const readMetadata = async (path) => {
54
+ try {
55
+ return { ok: true, value: await bigLstat(path) };
56
+ }
57
+ catch {
58
+ return { ok: false, message: "Entry metadata could not be read" };
59
+ }
60
+ };
61
+ const processEntry = async (state, current, name, directories) => {
62
+ if (isAborted(state.signal))
63
+ return {
64
+ ok: false,
65
+ error: {
66
+ tag: "reference-source-reader",
67
+ code: "cancelled",
68
+ message: "Reference source traversal cancelled",
69
+ },
70
+ };
71
+ const absolute = join(current.path, name);
72
+ const path = pathFromRoot(state.root, absolute);
73
+ const excluded = applyExclusion(state.shouldExclude, path);
74
+ if (!excluded.ok)
75
+ return {
76
+ ok: false,
77
+ error: {
78
+ tag: "reference-source-reader",
79
+ code: "io",
80
+ message: "Reference source exclusion check failed",
81
+ },
82
+ };
83
+ if (excluded.value)
84
+ return { ok: true, value: undefined };
85
+ if (!reserveEntry(state))
86
+ return { ok: true, value: undefined };
87
+ if (Buffer.byteLength(path) > state.limits.maxPathBytes) {
88
+ state.entries.push(entryFailure(path, "unknown", "limit", "Path exceeds maxPathBytes"));
89
+ state.truncated = true;
90
+ return { ok: true, value: undefined };
91
+ }
92
+ const metadata = await readMetadata(absolute);
93
+ if (!metadata.ok) {
94
+ state.entries.push(entryFailure(path, "unknown", "io", metadata.message));
95
+ return { ok: true, value: undefined };
96
+ }
97
+ if (isAborted(state.signal))
98
+ return {
99
+ ok: false,
100
+ error: {
101
+ tag: "reference-source-reader",
102
+ code: "cancelled",
103
+ message: "Reference source traversal cancelled",
104
+ },
105
+ };
106
+ if (metadata.value.isSymbolicLink())
107
+ state.entries.push(await describeSymlink(state.root, absolute, path));
108
+ else if (metadata.value.isDirectory()) {
109
+ if (current.depth < state.limits.maxDepth) {
110
+ state.entries.push({ status: "read", kind: "directory", path });
111
+ directories.push({ path: absolute, depth: current.depth + 1 });
112
+ }
113
+ else {
114
+ state.entries.push(entryFailure(path, "directory", "limit", "Directory exceeds maxDepth"));
115
+ state.truncated = true;
116
+ }
117
+ }
118
+ else if (!metadata.value.isFile())
119
+ state.entries.push(entryFailure(path, "other", "unsupported", "Entry is not a regular file", safeSize(metadata.value.size)));
120
+ else
121
+ await processFileEntry(state, absolute, path, metadata.value);
122
+ return { ok: true, value: undefined };
123
+ };
124
+ const processFileEntry = async (state, absolute, path, metadata) => {
125
+ const remaining = state.limits.maxBytes - state.bytesRead;
126
+ if (metadata.size > BigInt(remaining)) {
127
+ state.entries.push(entryFailure(path, "file", "limit", "File exceeds remaining maxBytes", safeSize(metadata.size)));
128
+ state.truncated = true;
129
+ return;
130
+ }
131
+ const result = await readStableFile({
132
+ root: state.root,
133
+ rootIdentity: state.rootIdentity,
134
+ absolute,
135
+ path,
136
+ expected: metadata,
137
+ remaining,
138
+ ...(state.signal === undefined ? {} : { signal: state.signal }),
139
+ });
140
+ if (result.status === "read" && result.kind === "file")
141
+ state.bytesRead += result.bytes.byteLength;
142
+ else if (result.status === "failed" && result.code === "limit")
143
+ state.truncated = true;
144
+ state.entries.push(result);
145
+ };
146
+ const describeSymlink = async (root, absolute, path) => {
147
+ try {
148
+ const rawTarget = await readlink(absolute);
149
+ const lexicalTarget = resolve(dirname(absolute), rawTarget);
150
+ if (!withinRoot(root, lexicalTarget))
151
+ return {
152
+ status: "read",
153
+ kind: "symlink",
154
+ path,
155
+ target: "<outside-root>",
156
+ targetState: "external",
157
+ };
158
+ try {
159
+ const canonicalTarget = await realpath(lexicalTarget);
160
+ return withinRoot(root, canonicalTarget)
161
+ ? {
162
+ status: "read",
163
+ kind: "symlink",
164
+ path,
165
+ target: pathFromRoot(root, canonicalTarget),
166
+ targetState: "internal",
167
+ }
168
+ : {
169
+ status: "read",
170
+ kind: "symlink",
171
+ path,
172
+ target: "<outside-root>",
173
+ targetState: "external",
174
+ };
175
+ }
176
+ catch {
177
+ return {
178
+ status: "read",
179
+ kind: "symlink",
180
+ path,
181
+ target: pathFromRoot(root, lexicalTarget),
182
+ targetState: "missing",
183
+ };
184
+ }
185
+ }
186
+ catch {
187
+ return entryFailure(path, "symlink", "io", "Symbolic link target could not be read");
188
+ }
189
+ };
190
+ const applyExclusion = (shouldExclude, path) => {
191
+ try {
192
+ return { ok: true, value: shouldExclude?.(path) === true };
193
+ }
194
+ catch {
195
+ return { ok: false };
196
+ }
197
+ };
198
+ const reserveEntry = (state) => {
199
+ if (state.filesSeen < state.limits.maxEntries) {
200
+ state.filesSeen += 1;
201
+ return true;
202
+ }
203
+ state.truncated = true;
204
+ state.stopped = true;
205
+ return false;
206
+ };
@@ -0,0 +1,19 @@
1
+ import {} from "./ReferenceSourceReaderTypes.js";
2
+ export const failure = (code, message) => ({
3
+ tag: "reference-source-reader",
4
+ code,
5
+ message,
6
+ });
7
+ export const cancelled = () => failure("cancelled", "Reference source traversal cancelled");
8
+ export const safeSize = (size) => {
9
+ const value = Number(size);
10
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
11
+ };
12
+ export const entryFailure = (...[path, kind, code, message, size]) => ({
13
+ status: "failed",
14
+ kind,
15
+ path,
16
+ code,
17
+ message,
18
+ ...(size === undefined ? {} : { size }),
19
+ });
@@ -0,0 +1,119 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { entryFailure, safeSize } from "./ReferenceSourceReaderErrors.js";
5
+ import { isAborted, sameFile, validateDirectory, } from "./ReferenceSourceReaderValidate.js";
6
+ import {} from "./ReferenceSourceReaderTypes.js";
7
+ const READ_CHUNK_BYTES = 64 * 1024;
8
+ const prepareFileRead = async (request) => {
9
+ const { root, rootIdentity, absolute, path, expected, signal } = request;
10
+ const parentBefore = await validateDirectory(root, rootIdentity, dirname(absolute));
11
+ if (!parentBefore.ok)
12
+ return {
13
+ status: "failed",
14
+ entry: entryFailure(path, "file", parentBefore.code, parentBefore.message, safeSize(expected.size)),
15
+ };
16
+ const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
17
+ if (isAborted(signal)) {
18
+ await handle.close().catch(() => undefined);
19
+ return {
20
+ status: "failed",
21
+ entry: entryFailure(path, "file", "cancelled", "File read cancelled", safeSize(expected.size)),
22
+ };
23
+ }
24
+ const before = await handle.stat({ bigint: true });
25
+ if (!sameFile(expected, before)) {
26
+ await handle.close().catch(() => undefined);
27
+ return {
28
+ status: "failed",
29
+ entry: entryFailure(path, "file", "changed", "File changed before it was read", safeSize(before.size)),
30
+ };
31
+ }
32
+ return { status: "ready", handle, before, parentBefore };
33
+ };
34
+ const readFileContents = async (request) => {
35
+ const { handle, path, remaining, signal } = request;
36
+ const chunks = [];
37
+ let total = 0;
38
+ for (;;) {
39
+ if (isAborted(signal))
40
+ return {
41
+ status: "failed",
42
+ entry: entryFailure(path, "file", "cancelled", "File read cancelled", total),
43
+ };
44
+ const capacity = Math.min(READ_CHUNK_BYTES, remaining + 1 - total);
45
+ if (capacity <= 0)
46
+ return {
47
+ status: "failed",
48
+ entry: entryFailure(path, "file", "limit", "File exceeds remaining maxBytes", total),
49
+ };
50
+ const chunk = Buffer.allocUnsafe(capacity);
51
+ const read = await handle.read(chunk, 0, capacity, null);
52
+ if (isAborted(signal))
53
+ return {
54
+ status: "failed",
55
+ entry: entryFailure(path, "file", "cancelled", "File read cancelled", total),
56
+ };
57
+ if (read.bytesRead === 0)
58
+ break;
59
+ total += read.bytesRead;
60
+ if (total > remaining)
61
+ return {
62
+ status: "failed",
63
+ entry: entryFailure(path, "file", "limit", "File exceeds remaining maxBytes", total),
64
+ };
65
+ chunks.push(chunk.subarray(0, read.bytesRead));
66
+ }
67
+ return { status: "ok", chunks, total };
68
+ };
69
+ const finalizeFileRead = async (request) => {
70
+ const { root, rootIdentity, absolute, path, handle, before, parentBefore, chunks, total, } = request;
71
+ const after = await handle.stat({ bigint: true });
72
+ const parentAfter = await validateDirectory(root, rootIdentity, dirname(absolute));
73
+ if (!parentAfter.ok || !sameFile(parentBefore.stats, parentAfter.stats))
74
+ return entryFailure(path, "file", "changed", "Parent directory changed while file was read", total);
75
+ if (!sameFile(before, after) || BigInt(total) !== after.size)
76
+ return entryFailure(path, "file", "changed", "File changed while it was read", total);
77
+ return {
78
+ status: "read",
79
+ kind: "file",
80
+ path,
81
+ bytes: Buffer.concat(chunks, total),
82
+ size: total,
83
+ };
84
+ };
85
+ export const readStableFile = async (request) => {
86
+ const { root, rootIdentity, absolute, path, expected, remaining, signal } = request;
87
+ let handle;
88
+ try {
89
+ const prepared = await prepareFileRead(request);
90
+ if (prepared.status === "failed")
91
+ return prepared.entry;
92
+ handle = prepared.handle;
93
+ const contents = await readFileContents({
94
+ handle,
95
+ path,
96
+ remaining,
97
+ ...(signal === undefined ? {} : { signal }),
98
+ });
99
+ if (contents.status === "failed")
100
+ return contents.entry;
101
+ return await finalizeFileRead({
102
+ root,
103
+ rootIdentity,
104
+ absolute,
105
+ path,
106
+ handle,
107
+ before: prepared.before,
108
+ parentBefore: prepared.parentBefore,
109
+ chunks: contents.chunks,
110
+ total: contents.total,
111
+ });
112
+ }
113
+ catch {
114
+ return entryFailure(path, "file", isAborted(signal) ? "cancelled" : "io", "File could not be read safely", safeSize(expected.size));
115
+ }
116
+ finally {
117
+ await handle?.close().catch(() => undefined);
118
+ }
119
+ };
@@ -0,0 +1,23 @@
1
+ import { isAbsolute, relative, sep } from "node:path";
2
+ export const withinRoot = (root, path) => {
3
+ const value = relative(root, path);
4
+ return (value === "" ||
5
+ (value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value)));
6
+ };
7
+ export const pathFromRoot = (root, path) => {
8
+ const value = relative(root, path);
9
+ return withinRoot(root, path)
10
+ ? value.split(sep).join("/") || "."
11
+ : "<outside-root>";
12
+ };
13
+ export const compareNames = (left, right) => {
14
+ const leftPoints = [...left].map((value) => value.codePointAt(0) ?? 0);
15
+ const rightPoints = [...right].map((value) => value.codePointAt(0) ?? 0);
16
+ const length = Math.min(leftPoints.length, rightPoints.length);
17
+ for (let index = 0; index < length; index += 1) {
18
+ const difference = (leftPoints[index] ?? 0) - (rightPoints[index] ?? 0);
19
+ if (difference !== 0)
20
+ return difference;
21
+ }
22
+ return leftPoints.length - rightPoints.length;
23
+ };
@@ -0,0 +1,2 @@
1
+ import {} from "node:fs";
2
+ import {} from "../domain/result.js";
@@ -0,0 +1,71 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ import { err, ok } from "../domain/result.js";
4
+ import { cancelled, failure } from "./ReferenceSourceReaderErrors.js";
5
+ import { withinRoot } from "./ReferenceSourceReaderPaths.js";
6
+ import {} from "./ReferenceSourceReaderTypes.js";
7
+ export const bigLstat = (path) => lstat(path, { bigint: true });
8
+ export const sameFile = (left, right) => left.dev === right.dev &&
9
+ left.ino === right.ino &&
10
+ left.size === right.size &&
11
+ left.mtimeNs === right.mtimeNs &&
12
+ left.ctimeNs === right.ctimeNs;
13
+ export const isAborted = (signal) => signal?.aborted === true;
14
+ const validLimits = (limits) => Object.values(limits).every((value) => Number.isSafeInteger(value) && value > 0);
15
+ export const noFollowOpenSupported = () => Number.isSafeInteger(constants.O_NOFOLLOW) && constants.O_NOFOLLOW !== 0;
16
+ export const validateDirectory = async (root, rootIdentity, path) => {
17
+ try {
18
+ const rootNow = await bigLstat(root);
19
+ if (!sameFile(rootIdentity, rootNow))
20
+ return { ok: false, code: "changed", message: "Reference root changed" };
21
+ const stats = await bigLstat(path);
22
+ if (stats.isSymbolicLink())
23
+ return {
24
+ ok: false,
25
+ code: "symlink",
26
+ message: "Symbolic links are not followed",
27
+ };
28
+ if (!stats.isDirectory())
29
+ return {
30
+ ok: false,
31
+ code: "changed",
32
+ message: "Directory identity changed",
33
+ };
34
+ const canonical = await realpath(path);
35
+ if (canonical !== path || !withinRoot(root, canonical))
36
+ return {
37
+ ok: false,
38
+ code: "changed",
39
+ message: "Directory escaped the reference root",
40
+ };
41
+ return { ok: true, stats };
42
+ }
43
+ catch {
44
+ return {
45
+ ok: false,
46
+ code: "io",
47
+ message: "Directory identity could not be verified",
48
+ };
49
+ }
50
+ };
51
+ export const prepareRoot = async (root, limits, signal) => {
52
+ if (!validLimits(limits))
53
+ return err(failure("invalid-limits", "Reader limits must be positive integers"));
54
+ if (isAborted(signal))
55
+ return err(cancelled());
56
+ try {
57
+ const metadata = await bigLstat(root);
58
+ if (metadata.isSymbolicLink() || !metadata.isDirectory())
59
+ return err(failure("invalid-root", "Reference source root must be a directory"));
60
+ const canonicalRoot = await realpath(root);
61
+ if (isAborted(signal))
62
+ return err(cancelled());
63
+ const canonicalMetadata = await bigLstat(canonicalRoot);
64
+ if (!sameFile(metadata, canonicalMetadata))
65
+ return err(failure("invalid-root", "Reference source root changed during resolution"));
66
+ return ok({ canonicalRoot, rootIdentity: canonicalMetadata });
67
+ }
68
+ catch {
69
+ return err(failure("invalid-root", "Reference source root could not be resolved"));
70
+ }
71
+ };
@@ -3,13 +3,16 @@ import { PRODUCT_IDENTITY } from "../identity.js";
3
3
  import { registerEnhancedTools } from "./registerEnhancedTools.js";
4
4
  import { registerOfficialTools } from "./registerOfficialTools.js";
5
5
  import { registerSessionTools } from "./registerSessionTools.js";
6
+ import { registerNativeTools } from "./registerNativeTools.js";
7
+ import { registerArtifactTools } from "./registerArtifactTools.js";
6
8
  import { silentLogger } from "../logger.js";
7
9
  /**
8
10
  * Construct one MCP server without acquiring subprocess resources.
9
11
  * Supplying a session adds target lifecycle tools; omitting it retains the
10
12
  * fixed-target seam used by focused tests and embedders.
11
13
  */
12
- export const createServer = (analysis, session, logger = silentLogger, processPolicy) => {
14
+ export const createServer = (analysis, session, options = {}) => {
15
+ const logger = options.logger ?? silentLogger;
13
16
  const server = new McpServer({ name: PRODUCT_IDENTITY.mcpServerKey, version: "0.1.0" }, {
14
17
  capabilities: { tools: {} },
15
18
  instructions: session === undefined
@@ -18,13 +21,36 @@ export const createServer = (analysis, session, logger = silentLogger, processPo
18
21
  });
19
22
  const toolLogger = logger.child({ layer: "server" });
20
23
  const activeTarget = session === undefined ? undefined : () => session.activeTarget();
21
- const provider = session?.providerIdentity();
22
24
  const recordEvidence = session === undefined
23
25
  ? undefined
24
26
  : (evidence) => session.recordEvidence(evidence);
25
- registerOfficialTools(server, analysis, toolLogger, activeTarget, provider, recordEvidence);
26
- registerEnhancedTools(server, analysis, toolLogger, activeTarget, provider, recordEvidence);
27
+ registerOfficialTools(server, analysis, {
28
+ logger: toolLogger,
29
+ activeTarget,
30
+ recordEvidence,
31
+ recordUnknown: session === undefined
32
+ ? undefined
33
+ : (input) => session.recordUnknown(input),
34
+ });
35
+ registerEnhancedTools(server, analysis, {
36
+ logger: toolLogger,
37
+ activeTarget,
38
+ recordEvidence,
39
+ recordUnknown: session === undefined
40
+ ? undefined
41
+ : (input) => session.recordUnknown(input),
42
+ });
43
+ registerNativeTools(server, analysis, {
44
+ logger: toolLogger,
45
+ activeTarget,
46
+ recordEvidence,
47
+ });
48
+ registerArtifactTools(server, analysis, {
49
+ logger: toolLogger,
50
+ activeTarget,
51
+ recordEvidence,
52
+ });
27
53
  if (session !== undefined)
28
- registerSessionTools(server, session, toolLogger, processPolicy);
54
+ registerSessionTools(server, session, toolLogger, options);
29
55
  return server;
30
56
  };
@@ -0,0 +1,10 @@
1
+ import { ok } from "../domain/result.js";
2
+ /** Atomically record derived Evidence with an optional approved unknown. */
3
+ export const recordDerivedEvidence = (session, evidence, unknown) => {
4
+ if (unknown !== undefined) {
5
+ const recorded = session.recordEvidenceWithUnknown(evidence, unknown);
6
+ return recorded.ok ? ok(evidence) : recorded;
7
+ }
8
+ const recorded = session.recordEvidence(evidence);
9
+ return recorded.ok ? ok(evidence) : recorded;
10
+ };
@@ -0,0 +1,62 @@
1
+ import { artifactComparisonInputSchema, compareArtifacts, } from "../domain/artifactComparison.js";
2
+ import { createEvidence } from "../domain/evidence.js";
3
+ import { jsonValueSchema } from "../domain/jsonValue.js";
4
+ import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
5
+ import { resolveSessionEvidence } from "./sessionEvidence.js";
6
+ import { ARTIFACT_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
7
+ import { toCallToolResult } from "./toolResult.js";
8
+ import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
9
+ /** Register Evidence-backed deterministic artifact comparison. */
10
+ export const registerArtifactComparisonTool = (server, session, contract) => {
11
+ server.registerTool(contract.name, toolRegistrationOptions(contract), (input) => {
12
+ const parsed = artifactComparisonInputSchema.parse(input);
13
+ const left = resolveSessionEvidence(session, parsed.left);
14
+ if (!left.ok)
15
+ return toCallToolResult(left, contract);
16
+ const right = resolveSessionEvidence(session, parsed.right);
17
+ if (!right.ok)
18
+ return toCallToolResult(right, contract);
19
+ const comparison = compareArtifacts(left.value, right.value, parsed.offset, parsed.limit);
20
+ const leftEvidenceIds = evidenceIds(parsed.left);
21
+ const rightEvidenceIds = evidenceIds(parsed.right);
22
+ const evidence = createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
23
+ predicateType: "rea.artifact-comparison/v1",
24
+ operation: contract.name,
25
+ parameters: {
26
+ left_evidence_ids: leftEvidenceIds,
27
+ right_evidence_ids: rightEvidenceIds,
28
+ offset: parsed.offset,
29
+ limit: parsed.limit,
30
+ },
31
+ result: jsonValueSchema.parse(comparison),
32
+ confidence: "derived",
33
+ authority: "analyst-inference",
34
+ limitations: comparison.limitations,
35
+ evidenceLinks: [...leftEvidenceIds, ...rightEvidenceIds],
36
+ });
37
+ return toCallToolResult(recordDerivedEvidence(session, evidence, artifactUnknownInput(parsed, comparison.status)), contract);
38
+ });
39
+ };
40
+ const artifactUnknownInput = (input, status) => {
41
+ if (input.unknown_registry_approved !== true || status === "unchanged")
42
+ return undefined;
43
+ return {
44
+ approved: true,
45
+ question: `Artifact comparison is ${status}`,
46
+ severity: status === "unknown" || status === "truncated" ? "high" : "medium",
47
+ domain: "artifact-comparison",
48
+ supporting_evidence_ids: evidenceIds(input.left),
49
+ contradicting_evidence_ids: evidenceIds(input.right),
50
+ required_authority: "shipped-artifact",
51
+ required_confidence: "observed",
52
+ required_environment: null,
53
+ recommended_probes: [
54
+ {
55
+ operation: "inventory_artifact",
56
+ rationale: "Capture both complete artifact graphs under equal limits.",
57
+ },
58
+ ],
59
+ relationships: [],
60
+ };
61
+ };
62
+ const evidenceIds = (input) => (Array.isArray(input) ? input : [input]).map(({ evidence_id: id }) => id);
@@ -0,0 +1,6 @@
1
+ import { ARTIFACT_TOOL_CONTRACTS } from "../contracts/artifactToolContracts.js";
2
+ import { registerEvidenceTools } from "./registerEvidenceTools.js";
3
+ /** Register deterministic artifact inventory and safe extraction operations. */
4
+ export const registerArtifactTools = (server, analysis, options) => {
5
+ registerEvidenceTools(server, analysis, ARTIFACT_TOOL_CONTRACTS, options);
6
+ };
@@ -0,0 +1,47 @@
1
+ import { bundleComparisonInputSchema, compareBundles, } from "../domain/bundleComparison.js";
2
+ import { createEvidence } from "../domain/evidence.js";
3
+ import { EvidenceIntegrityError } from "../domain/errors.js";
4
+ import { jsonValueSchema } from "../domain/jsonValue.js";
5
+ import { err, ok } from "../domain/result.js";
6
+ import { BUNDLE_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
7
+ import { toCallToolResult } from "./toolResult.js";
8
+ import { isSessionEvidence } from "./sessionEvidence.js";
9
+ import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
10
+ /** Register canonical Evidence bundle comparison. */
11
+ export const registerBundleComparisonTool = (server, session, contract) => {
12
+ server.registerTool(contract.name, toolRegistrationOptions(contract), (input) => {
13
+ const parsed = bundleComparisonInputSchema.parse(input);
14
+ const sourceIds = [
15
+ ...new Set([
16
+ ...parsed.left.records.map(({ evidence_id: id }) => id),
17
+ ...parsed.right.records.map(({ evidence_id: id }) => id),
18
+ ]),
19
+ ].sort();
20
+ if ([...parsed.left.records, ...parsed.right.records].some((evidence) => !isSessionEvidence(session, evidence)))
21
+ return toCallToolResult(err(new EvidenceIntegrityError("Bundle comparison input Evidence is not present in this session")), contract);
22
+ const ownedUnknownDigests = new Set(session
23
+ .exportEvidenceBundle()
24
+ .unknowns.map(({ revision_digest: digest }) => digest));
25
+ if ([...parsed.left.unknowns, ...parsed.right.unknowns].some(({ revision_digest: digest }) => !ownedUnknownDigests.has(digest)))
26
+ return toCallToolResult(err(new EvidenceIntegrityError("Bundle comparison input unknown history is not present in this session")), contract);
27
+ const comparison = compareBundles(parsed.left, parsed.right, parsed.record_pairs, parsed.offset, parsed.limit);
28
+ const evidence = createEvidence(undefined, BUNDLE_COMPARISON_PROVIDER, {
29
+ predicateType: "rea.bundle-comparison/v1",
30
+ operation: contract.name,
31
+ parameters: {
32
+ left_bundle_sha256: comparison.left_bundle_sha256,
33
+ right_bundle_sha256: comparison.right_bundle_sha256,
34
+ record_pairs: parsed.record_pairs,
35
+ offset: parsed.offset,
36
+ limit: parsed.limit,
37
+ },
38
+ result: jsonValueSchema.parse(comparison),
39
+ confidence: "derived",
40
+ authority: "analyst-inference",
41
+ limitations: comparison.limitations,
42
+ evidenceLinks: sourceIds,
43
+ });
44
+ const recorded = session.recordEvidence(evidence);
45
+ return toCallToolResult(recorded.ok ? ok(evidence) : recorded, contract);
46
+ });
47
+ };