@atrib/daemon 0.1.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/LICENSE +216 -0
- package/README.md +153 -0
- package/dist/backend.d.ts +117 -0
- package/dist/backend.js +732 -0
- package/dist/context-policy.d.ts +19 -0
- package/dist/context-policy.js +76 -0
- package/dist/http-host.d.ts +93 -0
- package/dist/http-host.js +476 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +238 -0
- package/dist/stdio.d.ts +22 -0
- package/dist/stdio.js +73 -0
- package/dist/transport-adapter.d.ts +40 -0
- package/dist/transport-adapter.js +28 -0
- package/package.json +56 -0
package/dist/backend.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* atribd backend: mounts the seven cognitive-primitive MCP servers in
|
|
4
|
+
* process over `InMemoryTransport` and routes their fifteen physical tools
|
|
5
|
+
* through two internal handlers (write, read). Each mounted tool name is a
|
|
6
|
+
* thin alias over the standalone package's own server, so a call through
|
|
7
|
+
* the daemon produces the same canonical record bytes and the same
|
|
8
|
+
* `_local.producer` sidecar label as a call through the standalone binary.
|
|
9
|
+
*
|
|
10
|
+
* Write-primitive calls are serialized per resolved context_id. The
|
|
11
|
+
* primitives resolve their chain tail through mirror inheritance
|
|
12
|
+
* (`resolveChainRoot` in @atrib/mcp; the daemon never reimplements chain
|
|
13
|
+
* selection), and `handleEmit` awaits its mirror write before returning,
|
|
14
|
+
* so serializing read-tail, sign, append per context through one process
|
|
15
|
+
* yields a linear chain for every writer routed through the daemon.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { resolveEnvContextId } from '@atrib/mcp';
|
|
21
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
22
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
23
|
+
import { ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
24
|
+
export const DEFAULT_TOOL_TIMEOUT_MS = 45_000;
|
|
25
|
+
const MCP_REQUEST_TIMEOUT_CODE = -32001;
|
|
26
|
+
const EXPECTED_RECALL_COVERAGE_VERSION = 'coverage-v1';
|
|
27
|
+
const EXPECTED_RECALL_CONTENT_INDEX_VERSION = 'content-index-v1';
|
|
28
|
+
const HEALTH_PROBE_ABSENT_HASH = `sha256:${'f'.repeat(64)}`;
|
|
29
|
+
const HEALTH_PROBE_ABSENT_CONTEXT_ID = 'f'.repeat(32);
|
|
30
|
+
const CONTEXT_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
31
|
+
const runtimeRequire = createRequire(import.meta.url);
|
|
32
|
+
export const PRIMITIVE_SPECS = [
|
|
33
|
+
{
|
|
34
|
+
name: 'emit',
|
|
35
|
+
packageName: '@atrib/emit',
|
|
36
|
+
kind: 'write',
|
|
37
|
+
expectedTools: ['emit'],
|
|
38
|
+
mutatesLogOnCall: true,
|
|
39
|
+
probeMode: 'package-and-tool-surface',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'annotate',
|
|
43
|
+
packageName: '@atrib/annotate',
|
|
44
|
+
kind: 'write',
|
|
45
|
+
expectedTools: ['atrib-annotate'],
|
|
46
|
+
mutatesLogOnCall: true,
|
|
47
|
+
probeMode: 'package-and-tool-surface',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'revise',
|
|
51
|
+
packageName: '@atrib/revise',
|
|
52
|
+
kind: 'write',
|
|
53
|
+
expectedTools: ['atrib-revise'],
|
|
54
|
+
mutatesLogOnCall: true,
|
|
55
|
+
probeMode: 'package-and-tool-surface',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'recall',
|
|
59
|
+
packageName: '@atrib/recall',
|
|
60
|
+
kind: 'read',
|
|
61
|
+
expectedTools: [
|
|
62
|
+
'recall_annotations',
|
|
63
|
+
'recall_by_content',
|
|
64
|
+
'recall_by_signer',
|
|
65
|
+
'recall_my_attribution_history',
|
|
66
|
+
'recall_orphans',
|
|
67
|
+
'recall_revisions',
|
|
68
|
+
'recall_session_chain',
|
|
69
|
+
'recall_walk',
|
|
70
|
+
],
|
|
71
|
+
mutatesLogOnCall: false,
|
|
72
|
+
probeMode: 'read-only-behavioral-probe',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'trace',
|
|
76
|
+
packageName: '@atrib/trace',
|
|
77
|
+
kind: 'read',
|
|
78
|
+
expectedTools: ['trace', 'trace_forward'],
|
|
79
|
+
mutatesLogOnCall: false,
|
|
80
|
+
probeMode: 'package-and-tool-surface',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'summarize',
|
|
84
|
+
packageName: '@atrib/summarize',
|
|
85
|
+
kind: 'read',
|
|
86
|
+
expectedTools: ['summarize'],
|
|
87
|
+
mutatesLogOnCall: false,
|
|
88
|
+
probeMode: 'package-and-tool-surface',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'verify',
|
|
92
|
+
packageName: '@atrib/verify-mcp',
|
|
93
|
+
kind: 'read',
|
|
94
|
+
expectedTools: ['atrib-verify'],
|
|
95
|
+
mutatesLogOnCall: false,
|
|
96
|
+
probeMode: 'package-and-tool-surface',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
/** Physical tool names served by the write handler. */
|
|
100
|
+
export const WRITE_TOOL_NAMES = new Set(PRIMITIVE_SPECS.filter((spec) => spec.kind === 'write').flatMap((spec) => [
|
|
101
|
+
...spec.expectedTools,
|
|
102
|
+
]));
|
|
103
|
+
const PRIMITIVES = [
|
|
104
|
+
['emit', async () => (await import('@atrib/emit')).createAtribEmitServer()],
|
|
105
|
+
['annotate', async () => (await import('@atrib/annotate')).createAtribAnnotateServer()],
|
|
106
|
+
['revise', async () => (await import('@atrib/revise')).createAtribReviseServer()],
|
|
107
|
+
['recall', async () => (await import('@atrib/recall')).createAtribRecallServer()],
|
|
108
|
+
['trace', async () => (await import('@atrib/trace')).createAtribTraceServer()],
|
|
109
|
+
['summarize', async () => (await import('@atrib/summarize')).createAtribSummarizeServer()],
|
|
110
|
+
['verify', async () => (await import('@atrib/verify-mcp')).createAtribVerifyServer()],
|
|
111
|
+
];
|
|
112
|
+
export function readPackageVersion() {
|
|
113
|
+
try {
|
|
114
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
115
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return '0.0.0';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function readDependencyPackageVersion(packageName) {
|
|
122
|
+
try {
|
|
123
|
+
const packagePath = runtimeRequire.resolve(`${packageName}/package.json`);
|
|
124
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
125
|
+
return stringValue(pkg.version);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function logDaemonEvent(event) {
|
|
132
|
+
try {
|
|
133
|
+
process.stderr.write(`${JSON.stringify({
|
|
134
|
+
ts: new Date().toISOString(),
|
|
135
|
+
component: 'atribd',
|
|
136
|
+
...event,
|
|
137
|
+
})}\n`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Diagnostics must not interfere with the MCP transport.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function toolTimeoutError(tool, timeoutMs) {
|
|
144
|
+
return new McpError(MCP_REQUEST_TIMEOUT_CODE, `atrib primitive tool ${tool} timed out after ${timeoutMs}ms`);
|
|
145
|
+
}
|
|
146
|
+
export function errorMessage(error) {
|
|
147
|
+
return error instanceof Error ? error.message : String(error);
|
|
148
|
+
}
|
|
149
|
+
export async function callWithToolTimeout(tool, timeoutMs, run) {
|
|
150
|
+
let timeoutHandle;
|
|
151
|
+
let timedOut = false;
|
|
152
|
+
const startedAt = Date.now();
|
|
153
|
+
const call = run();
|
|
154
|
+
const timeout = new Promise((_, reject) => {
|
|
155
|
+
timeoutHandle = setTimeout(() => {
|
|
156
|
+
timedOut = true;
|
|
157
|
+
reject(toolTimeoutError(tool, timeoutMs));
|
|
158
|
+
}, timeoutMs);
|
|
159
|
+
timeoutHandle.unref?.();
|
|
160
|
+
});
|
|
161
|
+
try {
|
|
162
|
+
return await Promise.race([call, timeout]);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (timeoutHandle)
|
|
166
|
+
clearTimeout(timeoutHandle);
|
|
167
|
+
if (timedOut) {
|
|
168
|
+
void call.catch((error) => {
|
|
169
|
+
logDaemonEvent({
|
|
170
|
+
event: 'proxy_tool_call_failed_after_timeout',
|
|
171
|
+
tool,
|
|
172
|
+
elapsed_ms: Date.now() - startedAt,
|
|
173
|
+
error: errorMessage(error),
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function serializeInFlightToolCall(call, now = Date.now()) {
|
|
180
|
+
const serialized = {
|
|
181
|
+
id: call.id,
|
|
182
|
+
primitive: call.primitive,
|
|
183
|
+
tool: call.tool,
|
|
184
|
+
started_at: new Date(call.startedAt).toISOString(),
|
|
185
|
+
elapsed_ms: Math.max(0, now - call.startedAt),
|
|
186
|
+
timed_out: call.timedOutAt !== undefined,
|
|
187
|
+
};
|
|
188
|
+
if (call.timedOutAt !== undefined) {
|
|
189
|
+
serialized.timed_out_at = new Date(call.timedOutAt).toISOString();
|
|
190
|
+
}
|
|
191
|
+
return serialized;
|
|
192
|
+
}
|
|
193
|
+
export function toolCallDiagnosticsDegraded(diagnostics) {
|
|
194
|
+
return diagnostics.in_flight_tool_calls.some((call) => call.timed_out || call.elapsed_ms >= diagnostics.tool_timeout_ms);
|
|
195
|
+
}
|
|
196
|
+
function stringValue(value) {
|
|
197
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Serialization key for a write-primitive call. Mirrors the context_id
|
|
201
|
+
* derivation the write primitives themselves apply before mirror-tail
|
|
202
|
+
* inheritance: an explicit 32-hex `context_id` argument wins, else the
|
|
203
|
+
* D078/D083 env ladder through `resolveEnvContextId`. When neither
|
|
204
|
+
* resolves, the primitive synthesizes a fresh orphan context (D072), which
|
|
205
|
+
* cannot race another writer, so no lock is taken.
|
|
206
|
+
*/
|
|
207
|
+
export function writeSerializationKey(params, env = process.env) {
|
|
208
|
+
const args = params.arguments;
|
|
209
|
+
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
210
|
+
const explicit = args['context_id'];
|
|
211
|
+
if (typeof explicit === 'string' && CONTEXT_ID_PATTERN.test(explicit)) {
|
|
212
|
+
return explicit;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
return resolveEnvContextId(env);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Per-key promise-chain mutex. Write calls against the same context_id run
|
|
224
|
+
* strictly one after another; different contexts do not block each other.
|
|
225
|
+
*/
|
|
226
|
+
export class ContextWriteLocks {
|
|
227
|
+
tails = new Map();
|
|
228
|
+
async run(key, fn) {
|
|
229
|
+
const entry = this.tails.get(key) ?? { chain: Promise.resolve(), depth: 0 };
|
|
230
|
+
entry.depth += 1;
|
|
231
|
+
const prior = entry.chain;
|
|
232
|
+
let release;
|
|
233
|
+
entry.chain = new Promise((resolveGate) => {
|
|
234
|
+
release = resolveGate;
|
|
235
|
+
});
|
|
236
|
+
this.tails.set(key, entry);
|
|
237
|
+
await prior;
|
|
238
|
+
try {
|
|
239
|
+
return await fn();
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
release();
|
|
243
|
+
entry.depth -= 1;
|
|
244
|
+
if (entry.depth === 0 && this.tails.get(key) === entry) {
|
|
245
|
+
this.tails.delete(key);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function failedRecallContract(reason) {
|
|
251
|
+
return {
|
|
252
|
+
status: 'fail',
|
|
253
|
+
package: '@atrib/recall',
|
|
254
|
+
runtime_metadata_available: false,
|
|
255
|
+
expected_coverage_version: EXPECTED_RECALL_COVERAGE_VERSION,
|
|
256
|
+
expected_content_index_version: EXPECTED_RECALL_CONTENT_INDEX_VERSION,
|
|
257
|
+
reason,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function validateRecallRuntimeContract(raw) {
|
|
261
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
262
|
+
return failedRecallContract('getAtribRecallRuntimeContract did not return an object');
|
|
263
|
+
}
|
|
264
|
+
const contract = raw;
|
|
265
|
+
const pkg = stringValue(contract.package) ?? '@atrib/recall';
|
|
266
|
+
const version = stringValue(contract.version);
|
|
267
|
+
const coverageVersion = stringValue(contract.coverage_version);
|
|
268
|
+
const contentIndexVersion = stringValue(contract.content_index_version);
|
|
269
|
+
const ok = pkg === '@atrib/recall' &&
|
|
270
|
+
coverageVersion === EXPECTED_RECALL_COVERAGE_VERSION &&
|
|
271
|
+
contentIndexVersion === EXPECTED_RECALL_CONTENT_INDEX_VERSION;
|
|
272
|
+
const diagnostic = {
|
|
273
|
+
status: ok ? 'pass' : 'fail',
|
|
274
|
+
package: pkg,
|
|
275
|
+
runtime_metadata_available: true,
|
|
276
|
+
expected_coverage_version: EXPECTED_RECALL_COVERAGE_VERSION,
|
|
277
|
+
expected_content_index_version: EXPECTED_RECALL_CONTENT_INDEX_VERSION,
|
|
278
|
+
};
|
|
279
|
+
if (version)
|
|
280
|
+
diagnostic.version = version;
|
|
281
|
+
if (coverageVersion)
|
|
282
|
+
diagnostic.coverage_version = coverageVersion;
|
|
283
|
+
if (contentIndexVersion)
|
|
284
|
+
diagnostic.content_index_version = contentIndexVersion;
|
|
285
|
+
if (!ok) {
|
|
286
|
+
diagnostic.reason =
|
|
287
|
+
`expected @atrib/recall ${EXPECTED_RECALL_COVERAGE_VERSION} and ` +
|
|
288
|
+
`${EXPECTED_RECALL_CONTENT_INDEX_VERSION}`;
|
|
289
|
+
}
|
|
290
|
+
return diagnostic;
|
|
291
|
+
}
|
|
292
|
+
function behavioralProbeSkipped(spec, reason) {
|
|
293
|
+
return {
|
|
294
|
+
status: 'skipped',
|
|
295
|
+
primitive: spec.name,
|
|
296
|
+
tool_names: [...spec.expectedTools],
|
|
297
|
+
probe_kind: 'not-available',
|
|
298
|
+
mutates_log_on_call: spec.mutatesLogOnCall,
|
|
299
|
+
reason,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function behavioralProbePassed(spec, probeKind, observed) {
|
|
303
|
+
return {
|
|
304
|
+
status: 'pass',
|
|
305
|
+
primitive: spec.name,
|
|
306
|
+
tool_names: [...spec.expectedTools],
|
|
307
|
+
probe_kind: probeKind,
|
|
308
|
+
mutates_log_on_call: spec.mutatesLogOnCall,
|
|
309
|
+
observed,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function behavioralProbeFailed(spec, reason) {
|
|
313
|
+
return {
|
|
314
|
+
status: 'fail',
|
|
315
|
+
primitive: spec.name,
|
|
316
|
+
tool_names: [...spec.expectedTools],
|
|
317
|
+
probe_kind: spec.mutatesLogOnCall ? 'not-available' : 'read-only',
|
|
318
|
+
mutates_log_on_call: spec.mutatesLogOnCall,
|
|
319
|
+
reason,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function parseToolJsonResult(toolName, result) {
|
|
323
|
+
const text = result.content?.find((item) => item.type === 'text' && typeof item.text === 'string')?.text;
|
|
324
|
+
if (!text)
|
|
325
|
+
throw new Error(`${toolName} returned no text content`);
|
|
326
|
+
return JSON.parse(text);
|
|
327
|
+
}
|
|
328
|
+
function assertObject(value, label) {
|
|
329
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
330
|
+
throw new Error(`${label} did not return an object`);
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
function assertArray(value, label) {
|
|
335
|
+
if (!Array.isArray(value))
|
|
336
|
+
throw new Error(`${label} did not return an array`);
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
async function callProbeTool(primitive, toolName, args, timeoutMs) {
|
|
340
|
+
const result = await callWithToolTimeout(`${primitive.name}.${toolName}`, Math.min(timeoutMs, 5_000), () => primitive.client.callTool({
|
|
341
|
+
name: toolName,
|
|
342
|
+
arguments: args,
|
|
343
|
+
}));
|
|
344
|
+
return assertObject(parseToolJsonResult(toolName, result), toolName);
|
|
345
|
+
}
|
|
346
|
+
async function probeRecallBehavior(primitive, spec, timeoutMs) {
|
|
347
|
+
const payload = await callProbeTool(primitive, 'recall_by_content', {
|
|
348
|
+
query: 'atrib primitive runtime behavioral health probe',
|
|
349
|
+
k: 1,
|
|
350
|
+
max_records: 10,
|
|
351
|
+
evidence_mode: 'bounded',
|
|
352
|
+
}, timeoutMs);
|
|
353
|
+
const runtime = assertObject(payload.runtime, 'recall_by_content.runtime');
|
|
354
|
+
const coverage = assertObject(payload.coverage, 'recall_by_content.coverage');
|
|
355
|
+
const index = assertObject(coverage.index, 'recall_by_content.coverage.index');
|
|
356
|
+
if (runtime.coverage_version !== EXPECTED_RECALL_COVERAGE_VERSION) {
|
|
357
|
+
throw new Error(`unexpected recall coverage_version ${String(runtime.coverage_version)}`);
|
|
358
|
+
}
|
|
359
|
+
if (runtime.content_index_version !== EXPECTED_RECALL_CONTENT_INDEX_VERSION) {
|
|
360
|
+
throw new Error(`unexpected recall content_index_version ${String(runtime.content_index_version)}`);
|
|
361
|
+
}
|
|
362
|
+
if (index.version !== EXPECTED_RECALL_CONTENT_INDEX_VERSION) {
|
|
363
|
+
throw new Error(`unexpected recall coverage.index.version ${String(index.version)}`);
|
|
364
|
+
}
|
|
365
|
+
return behavioralProbePassed(spec, 'read-only', {
|
|
366
|
+
tool: 'recall_by_content',
|
|
367
|
+
content_index_version: runtime.content_index_version,
|
|
368
|
+
coverage_version: runtime.coverage_version,
|
|
369
|
+
coverage_index_status: index.status,
|
|
370
|
+
searched_records: payload.searched_records,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async function probeTraceBehavior(primitive, spec, timeoutMs) {
|
|
374
|
+
const backward = await callProbeTool(primitive, 'trace', { record_hash: HEALTH_PROBE_ABSENT_HASH, depth: 0, compact: true }, timeoutMs);
|
|
375
|
+
const forward = await callProbeTool(primitive, 'trace_forward', { record_hash: HEALTH_PROBE_ABSENT_HASH, depth: 0, compact: true }, timeoutMs);
|
|
376
|
+
for (const [label, payload, direction] of [
|
|
377
|
+
['trace', backward, 'backward'],
|
|
378
|
+
['trace_forward', forward, 'forward'],
|
|
379
|
+
]) {
|
|
380
|
+
if (payload.start_hash !== HEALTH_PROBE_ABSENT_HASH) {
|
|
381
|
+
throw new Error(`${label} returned unexpected start_hash ${String(payload.start_hash)}`);
|
|
382
|
+
}
|
|
383
|
+
if (payload.direction !== direction) {
|
|
384
|
+
throw new Error(`${label} returned unexpected direction ${String(payload.direction)}`);
|
|
385
|
+
}
|
|
386
|
+
const dangling = assertArray(payload.dangling, `${label}.dangling`);
|
|
387
|
+
if (!dangling.includes(HEALTH_PROBE_ABSENT_HASH)) {
|
|
388
|
+
throw new Error(`${label} did not surface the absent probe hash as dangling`);
|
|
389
|
+
}
|
|
390
|
+
const visited = assertArray(payload.visited, `${label}.visited`);
|
|
391
|
+
if (visited.length !== 0) {
|
|
392
|
+
throw new Error(`${label} visited records for an absent probe hash`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return behavioralProbePassed(spec, 'read-only', {
|
|
396
|
+
tools: ['trace', 'trace_forward'],
|
|
397
|
+
absent_hash_dangling: true,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
async function probeSummarizeBehavior(primitive, spec, timeoutMs) {
|
|
401
|
+
const payload = await callProbeTool(primitive, 'summarize', { context_id: HEALTH_PROBE_ABSENT_CONTEXT_ID, max_records: 1 }, timeoutMs);
|
|
402
|
+
if (payload.narrative !== null) {
|
|
403
|
+
throw new Error('summarize produced a narrative for the absent health-probe context');
|
|
404
|
+
}
|
|
405
|
+
if (payload.records_summarized !== 0) {
|
|
406
|
+
throw new Error(`summarize matched ${String(payload.records_summarized)} probe record(s)`);
|
|
407
|
+
}
|
|
408
|
+
const warnings = assertArray(payload.warnings, 'summarize.warnings').map(String);
|
|
409
|
+
const expectedWarning = warnings.find((warning) => warning.includes('no records matched') || warning.includes('no LLM API key resolved'));
|
|
410
|
+
if (!expectedWarning) {
|
|
411
|
+
throw new Error('summarize did not report a deterministic zero-record health-probe path');
|
|
412
|
+
}
|
|
413
|
+
return behavioralProbePassed(spec, 'schema-only', {
|
|
414
|
+
tool: 'summarize',
|
|
415
|
+
records_summarized: payload.records_summarized,
|
|
416
|
+
warning: expectedWarning,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async function probeVerifyBehavior(primitive, spec, timeoutMs) {
|
|
420
|
+
const payload = await callProbeTool(primitive, 'atrib-verify', { records: [], required_record_hashes: [HEALTH_PROBE_ABSENT_HASH] }, timeoutMs);
|
|
421
|
+
if (payload.all_accepted !== false) {
|
|
422
|
+
throw new Error('atrib-verify accepted an intentionally missing required record');
|
|
423
|
+
}
|
|
424
|
+
const rejected = assertArray(payload.rejected, 'atrib-verify.rejected');
|
|
425
|
+
const missing = rejected.find((entry) => {
|
|
426
|
+
const claim = assertObject(entry, 'atrib-verify.rejected[]');
|
|
427
|
+
return (claim.record_hash === HEALTH_PROBE_ABSENT_HASH &&
|
|
428
|
+
Array.isArray(claim.rejection_reasons) &&
|
|
429
|
+
claim.rejection_reasons.includes('record_missing'));
|
|
430
|
+
});
|
|
431
|
+
if (!missing) {
|
|
432
|
+
throw new Error('atrib-verify did not reject the absent probe hash as record_missing');
|
|
433
|
+
}
|
|
434
|
+
return behavioralProbePassed(spec, 'read-only', {
|
|
435
|
+
tool: 'atrib-verify',
|
|
436
|
+
missing_required_record_rejected: true,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function inspectPrimitiveBehavioralProbes(mounted, timeoutMs) {
|
|
440
|
+
const byName = new Map(mounted.map((primitive) => [primitive.name, primitive]));
|
|
441
|
+
const entries = [];
|
|
442
|
+
for (const spec of PRIMITIVE_SPECS) {
|
|
443
|
+
const primitive = byName.get(spec.name);
|
|
444
|
+
if (!primitive) {
|
|
445
|
+
entries.push([spec.name, behavioralProbeFailed(spec, 'primitive did not mount')]);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (spec.mutatesLogOnCall) {
|
|
449
|
+
entries.push([
|
|
450
|
+
spec.name,
|
|
451
|
+
behavioralProbeSkipped(spec, 'write primitive has no validate-only contract; health checks must not sign records'),
|
|
452
|
+
]);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
if (spec.name === 'recall') {
|
|
457
|
+
entries.push([spec.name, await probeRecallBehavior(primitive, spec, timeoutMs)]);
|
|
458
|
+
}
|
|
459
|
+
else if (spec.name === 'trace') {
|
|
460
|
+
entries.push([spec.name, await probeTraceBehavior(primitive, spec, timeoutMs)]);
|
|
461
|
+
}
|
|
462
|
+
else if (spec.name === 'summarize') {
|
|
463
|
+
entries.push([spec.name, await probeSummarizeBehavior(primitive, spec, timeoutMs)]);
|
|
464
|
+
}
|
|
465
|
+
else if (spec.name === 'verify') {
|
|
466
|
+
entries.push([spec.name, await probeVerifyBehavior(primitive, spec, timeoutMs)]);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
entries.push([spec.name, behavioralProbeSkipped(spec, 'no behavioral probe defined')]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
entries.push([spec.name, behavioralProbeFailed(spec, errorMessage(error))]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return Object.fromEntries(entries);
|
|
477
|
+
}
|
|
478
|
+
function inspectPrimitiveSurfaceContracts(mounted) {
|
|
479
|
+
return Object.fromEntries(PRIMITIVE_SPECS.map((spec) => {
|
|
480
|
+
const primitive = mounted.find((candidate) => candidate.name === spec.name);
|
|
481
|
+
const mountedTools = primitive
|
|
482
|
+
? primitive.tools.map((tool) => tool.name).sort((a, b) => a.localeCompare(b))
|
|
483
|
+
: [];
|
|
484
|
+
const expectedTools = [...spec.expectedTools].sort((a, b) => a.localeCompare(b));
|
|
485
|
+
const missingTools = expectedTools.filter((tool) => !mountedTools.includes(tool));
|
|
486
|
+
const unexpectedTools = mountedTools.filter((tool) => !expectedTools.includes(tool));
|
|
487
|
+
const version = readDependencyPackageVersion(spec.packageName);
|
|
488
|
+
const issues = [];
|
|
489
|
+
if (!primitive)
|
|
490
|
+
issues.push('primitive did not mount');
|
|
491
|
+
if (!version)
|
|
492
|
+
issues.push('package version could not be read');
|
|
493
|
+
if (missingTools.length)
|
|
494
|
+
issues.push(`missing tool(s): ${missingTools.join(', ')}`);
|
|
495
|
+
if (unexpectedTools.length)
|
|
496
|
+
issues.push(`unexpected tool(s): ${unexpectedTools.join(', ')}`);
|
|
497
|
+
const diagnostic = {
|
|
498
|
+
status: issues.length ? 'fail' : 'pass',
|
|
499
|
+
primitive: spec.name,
|
|
500
|
+
package: spec.packageName,
|
|
501
|
+
expected_tools: expectedTools,
|
|
502
|
+
mounted_tools: mountedTools,
|
|
503
|
+
missing_tools: missingTools,
|
|
504
|
+
unexpected_tools: unexpectedTools,
|
|
505
|
+
mutates_log_on_call: spec.mutatesLogOnCall,
|
|
506
|
+
probe_mode: spec.probeMode,
|
|
507
|
+
};
|
|
508
|
+
if (version)
|
|
509
|
+
diagnostic.version = version;
|
|
510
|
+
if (issues.length)
|
|
511
|
+
diagnostic.reason = issues.join('; ');
|
|
512
|
+
return [spec.name, diagnostic];
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
async function inspectRuntimeContracts(mounted, timeoutMs) {
|
|
516
|
+
const primitives = inspectPrimitiveSurfaceContracts(mounted);
|
|
517
|
+
const behavioralProbes = await inspectPrimitiveBehavioralProbes(mounted, timeoutMs);
|
|
518
|
+
try {
|
|
519
|
+
const recall = (await import('@atrib/recall'));
|
|
520
|
+
const contractFn = recall.getAtribRecallRuntimeContract;
|
|
521
|
+
if (typeof contractFn !== 'function') {
|
|
522
|
+
return {
|
|
523
|
+
primitives,
|
|
524
|
+
behavioral_probes: behavioralProbes,
|
|
525
|
+
recall_content: failedRecallContract('@atrib/recall does not export getAtribRecallRuntimeContract'),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
primitives,
|
|
530
|
+
behavioral_probes: behavioralProbes,
|
|
531
|
+
recall_content: validateRecallRuntimeContract(contractFn()),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return {
|
|
536
|
+
primitives,
|
|
537
|
+
behavioral_probes: behavioralProbes,
|
|
538
|
+
recall_content: failedRecallContract(errorMessage(error)),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
export function runtimeContractsDegraded(contracts) {
|
|
543
|
+
return (contracts.recall_content.status !== 'pass' ||
|
|
544
|
+
Object.values(contracts.primitives).some((contract) => contract.status !== 'pass') ||
|
|
545
|
+
Object.values(contracts.behavioral_probes).some((probe) => probe.status === 'fail'));
|
|
546
|
+
}
|
|
547
|
+
async function mountPrimitive(name, factory) {
|
|
548
|
+
const handle = await factory();
|
|
549
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
550
|
+
await handle.mcp.connect(serverTransport);
|
|
551
|
+
const client = new Client({
|
|
552
|
+
name: `atribd-${name}`,
|
|
553
|
+
version: readPackageVersion(),
|
|
554
|
+
});
|
|
555
|
+
await client.connect(clientTransport);
|
|
556
|
+
const listed = await client.listTools();
|
|
557
|
+
return { name, handle, client, tools: listed.tools };
|
|
558
|
+
}
|
|
559
|
+
export async function createAtribdBackend(options = {}) {
|
|
560
|
+
const toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
561
|
+
const primitives = options.primitives ?? PRIMITIVES;
|
|
562
|
+
const specByName = new Map(PRIMITIVE_SPECS.map((spec) => [spec.name, spec]));
|
|
563
|
+
const mounted = [];
|
|
564
|
+
for (const [name, factory] of primitives) {
|
|
565
|
+
mounted.push(await mountPrimitive(name, factory));
|
|
566
|
+
}
|
|
567
|
+
const routeByTool = new Map();
|
|
568
|
+
const tools = [];
|
|
569
|
+
const inFlightToolCalls = new Map();
|
|
570
|
+
const writeLocks = new ContextWriteLocks();
|
|
571
|
+
const runtimeContracts = await inspectRuntimeContracts(mounted, toolTimeoutMs);
|
|
572
|
+
let callsStarted = 0;
|
|
573
|
+
let callsSucceeded = 0;
|
|
574
|
+
let callsFailed = 0;
|
|
575
|
+
let callsTimedOut = 0;
|
|
576
|
+
let callsSettledAfterTimeout = 0;
|
|
577
|
+
for (const primitive of mounted) {
|
|
578
|
+
// Unknown mount names default to the read handler; only tools served by
|
|
579
|
+
// a registered write primitive go through the write handler's lock.
|
|
580
|
+
const kind = specByName.get(primitive.name)?.kind ?? 'read';
|
|
581
|
+
for (const tool of primitive.tools) {
|
|
582
|
+
const existing = routeByTool.get(tool.name);
|
|
583
|
+
if (existing) {
|
|
584
|
+
throw new Error(`duplicate atrib primitive tool ${tool.name}: ${existing.primitive} and ${primitive.name}`);
|
|
585
|
+
}
|
|
586
|
+
routeByTool.set(tool.name, { primitive: primitive.name, kind, client: primitive.client });
|
|
587
|
+
tools.push(tool);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
591
|
+
const routeToolCall = async (route, request) => {
|
|
592
|
+
const id = randomUUID();
|
|
593
|
+
const startedAt = Date.now();
|
|
594
|
+
const call = {
|
|
595
|
+
id,
|
|
596
|
+
primitive: route.primitive,
|
|
597
|
+
tool: request.name,
|
|
598
|
+
startedAt,
|
|
599
|
+
};
|
|
600
|
+
callsStarted += 1;
|
|
601
|
+
inFlightToolCalls.set(id, call);
|
|
602
|
+
logDaemonEvent({
|
|
603
|
+
event: 'tool_call_started',
|
|
604
|
+
id,
|
|
605
|
+
primitive: route.primitive,
|
|
606
|
+
tool: request.name,
|
|
607
|
+
});
|
|
608
|
+
let timeoutHandle;
|
|
609
|
+
let timedOut = false;
|
|
610
|
+
const toolCall = route.client.callTool(request);
|
|
611
|
+
const timeout = new Promise((_, reject) => {
|
|
612
|
+
timeoutHandle = setTimeout(() => {
|
|
613
|
+
timedOut = true;
|
|
614
|
+
call.timedOutAt = Date.now();
|
|
615
|
+
callsTimedOut += 1;
|
|
616
|
+
logDaemonEvent({
|
|
617
|
+
event: 'tool_call_timed_out',
|
|
618
|
+
id,
|
|
619
|
+
primitive: route.primitive,
|
|
620
|
+
tool: request.name,
|
|
621
|
+
timeout_ms: toolTimeoutMs,
|
|
622
|
+
elapsed_ms: call.timedOutAt - startedAt,
|
|
623
|
+
});
|
|
624
|
+
reject(toolTimeoutError(request.name, toolTimeoutMs));
|
|
625
|
+
}, toolTimeoutMs);
|
|
626
|
+
timeoutHandle.unref?.();
|
|
627
|
+
});
|
|
628
|
+
try {
|
|
629
|
+
const result = await Promise.race([toolCall, timeout]);
|
|
630
|
+
if (timeoutHandle)
|
|
631
|
+
clearTimeout(timeoutHandle);
|
|
632
|
+
callsSucceeded += 1;
|
|
633
|
+
inFlightToolCalls.delete(id);
|
|
634
|
+
logDaemonEvent({
|
|
635
|
+
event: 'tool_call_completed',
|
|
636
|
+
id,
|
|
637
|
+
primitive: route.primitive,
|
|
638
|
+
tool: request.name,
|
|
639
|
+
elapsed_ms: Date.now() - startedAt,
|
|
640
|
+
});
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
if (timeoutHandle)
|
|
645
|
+
clearTimeout(timeoutHandle);
|
|
646
|
+
if (timedOut) {
|
|
647
|
+
void toolCall
|
|
648
|
+
.then(() => {
|
|
649
|
+
callsSettledAfterTimeout += 1;
|
|
650
|
+
logDaemonEvent({
|
|
651
|
+
event: 'tool_call_settled_after_timeout',
|
|
652
|
+
id,
|
|
653
|
+
primitive: route.primitive,
|
|
654
|
+
tool: request.name,
|
|
655
|
+
outcome: 'succeeded',
|
|
656
|
+
elapsed_ms: Date.now() - startedAt,
|
|
657
|
+
});
|
|
658
|
+
}, (lateError) => {
|
|
659
|
+
callsSettledAfterTimeout += 1;
|
|
660
|
+
logDaemonEvent({
|
|
661
|
+
event: 'tool_call_settled_after_timeout',
|
|
662
|
+
id,
|
|
663
|
+
primitive: route.primitive,
|
|
664
|
+
tool: request.name,
|
|
665
|
+
outcome: 'failed',
|
|
666
|
+
elapsed_ms: Date.now() - startedAt,
|
|
667
|
+
error: errorMessage(lateError),
|
|
668
|
+
});
|
|
669
|
+
})
|
|
670
|
+
.finally(() => {
|
|
671
|
+
inFlightToolCalls.delete(id);
|
|
672
|
+
});
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
callsFailed += 1;
|
|
676
|
+
inFlightToolCalls.delete(id);
|
|
677
|
+
logDaemonEvent({
|
|
678
|
+
event: 'tool_call_failed',
|
|
679
|
+
id,
|
|
680
|
+
primitive: route.primitive,
|
|
681
|
+
tool: request.name,
|
|
682
|
+
elapsed_ms: Date.now() - startedAt,
|
|
683
|
+
error: errorMessage(error),
|
|
684
|
+
});
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
// The two internal handlers of the daemon. The write handler serializes
|
|
689
|
+
// per resolved context so concurrent writers cannot race read-tail, sign,
|
|
690
|
+
// append against the same chain. The read handler routes directly.
|
|
691
|
+
const callWriteTool = async (route, request) => {
|
|
692
|
+
const key = writeSerializationKey(request);
|
|
693
|
+
if (!key)
|
|
694
|
+
return routeToolCall(route, request);
|
|
695
|
+
return writeLocks.run(key, () => routeToolCall(route, request));
|
|
696
|
+
};
|
|
697
|
+
const callReadTool = (route, request) => routeToolCall(route, request);
|
|
698
|
+
return {
|
|
699
|
+
tools,
|
|
700
|
+
toolNames: tools.map((tool) => tool.name),
|
|
701
|
+
mountedPrimitiveCount: mounted.length,
|
|
702
|
+
callTool: async (request) => {
|
|
703
|
+
const route = routeByTool.get(request.name);
|
|
704
|
+
if (!route) {
|
|
705
|
+
throw new McpError(ErrorCode.MethodNotFound, `unknown atrib primitive tool: ${request.name}`);
|
|
706
|
+
}
|
|
707
|
+
return route.kind === 'write' ? callWriteTool(route, request) : callReadTool(route, request);
|
|
708
|
+
},
|
|
709
|
+
diagnostics: () => {
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
return {
|
|
712
|
+
tool_timeout_ms: toolTimeoutMs,
|
|
713
|
+
active_tool_calls: inFlightToolCalls.size,
|
|
714
|
+
calls_started: callsStarted,
|
|
715
|
+
calls_succeeded: callsSucceeded,
|
|
716
|
+
calls_failed: callsFailed,
|
|
717
|
+
calls_timed_out: callsTimedOut,
|
|
718
|
+
calls_settled_after_timeout: callsSettledAfterTimeout,
|
|
719
|
+
in_flight_tool_calls: [...inFlightToolCalls.values()].map((call) => serializeInFlightToolCall(call, now)),
|
|
720
|
+
};
|
|
721
|
+
},
|
|
722
|
+
runtimeContracts: () => runtimeContracts,
|
|
723
|
+
flush: async () => {
|
|
724
|
+
await Promise.all(mounted.map((primitive) => primitive.handle.flush?.() ?? Promise.resolve()));
|
|
725
|
+
},
|
|
726
|
+
close: async () => {
|
|
727
|
+
await Promise.allSettled(mounted.map((primitive) => primitive.handle.flush?.() ?? Promise.resolve()));
|
|
728
|
+
await Promise.allSettled(mounted.map((primitive) => primitive.client.close()));
|
|
729
|
+
await Promise.allSettled(mounted.map((primitive) => primitive.handle.mcp.close()));
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|