@atrib/attest 0.0.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.
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Host-owned local substrate coordinator entrypoint.
4
+ //
5
+ // This is a supervised local process, not an MCP server. It reuses the
6
+ // existing @atrib/emit key resolver so the coordinator signs under the same
7
+ // creator identity as emit and the wrapper.
8
+ import { LOCAL_SUBSTRATE_HARNESS_CLASSES, LOCAL_SUBSTRATE_NODE_DEFAULT_HOST, LOCAL_SUBSTRATE_NODE_DEFAULT_MAX_BODY_BYTES, LOCAL_SUBSTRATE_NODE_DEFAULT_PORT, base64urlEncode, bindLocalSubstrateCoordinatorNodeServer, createInProcessLocalSubstrateCoordinator, } from '@atrib/mcp';
9
+ import { readFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { resolveKey } from './keys.js';
13
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2000;
14
+ const supportedHarnessClasses = new Set(LOCAL_SUBSTRATE_HARNESS_CLASSES);
15
+ function parseArgs(argv, env = process.env) {
16
+ const out = {
17
+ showVersion: false,
18
+ showHelp: false,
19
+ showDescribe: false,
20
+ jsonOutput: false,
21
+ };
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const a = argv[i];
24
+ if (a === '--version' || a === '-v') {
25
+ out.showVersion = true;
26
+ continue;
27
+ }
28
+ if (a === '--help' || a === '-h') {
29
+ out.showHelp = true;
30
+ continue;
31
+ }
32
+ if (a === '--describe') {
33
+ out.showDescribe = true;
34
+ continue;
35
+ }
36
+ if (a === '--json') {
37
+ out.jsonOutput = true;
38
+ continue;
39
+ }
40
+ if (a === '--host') {
41
+ out.host = requireValue(argv, ++i, '--host');
42
+ continue;
43
+ }
44
+ if (a === '--port') {
45
+ out.port = parsePort(requireValue(argv, ++i, '--port'), '--port');
46
+ continue;
47
+ }
48
+ if (a === '--max-body-bytes') {
49
+ out.maxBodyBytes = parsePositiveInt(requireValue(argv, ++i, '--max-body-bytes'), '--max-body-bytes');
50
+ continue;
51
+ }
52
+ if (a === '--log-endpoint') {
53
+ out.logEndpoint = requireValue(argv, ++i, '--log-endpoint');
54
+ continue;
55
+ }
56
+ if (a === '--log-submission') {
57
+ out.logSubmission = parseLogSubmission(requireValue(argv, ++i, '--log-submission'));
58
+ continue;
59
+ }
60
+ if (a === '--harness-classes') {
61
+ out.harnessClasses = parseHarnessClasses(requireValue(argv, ++i, '--harness-classes'));
62
+ continue;
63
+ }
64
+ if (a === '--shutdown-timeout-ms') {
65
+ out.shutdownTimeoutMs = parsePositiveInt(requireValue(argv, ++i, '--shutdown-timeout-ms'), '--shutdown-timeout-ms');
66
+ continue;
67
+ }
68
+ throw new Error(`unknown argument: ${a}`);
69
+ }
70
+ out.host = out.host ?? parseOptionalString(env['ATRIB_LOCAL_SUBSTRATE_HOST']);
71
+ out.port =
72
+ out.port ?? parseOptionalPort(env['ATRIB_LOCAL_SUBSTRATE_PORT'], 'ATRIB_LOCAL_SUBSTRATE_PORT');
73
+ out.maxBodyBytes =
74
+ out.maxBodyBytes ??
75
+ parseOptionalPositiveInt(env['ATRIB_LOCAL_SUBSTRATE_MAX_BODY_BYTES'], 'ATRIB_LOCAL_SUBSTRATE_MAX_BODY_BYTES');
76
+ out.logEndpoint = out.logEndpoint ?? parseOptionalString(env['ATRIB_LOG_ENDPOINT']);
77
+ out.logSubmission =
78
+ out.logSubmission ?? parseOptionalLogSubmission(env['ATRIB_LOCAL_SUBSTRATE_LOG_SUBMISSION']);
79
+ out.harnessClasses =
80
+ out.harnessClasses ?? parseOptionalHarnessClasses(env['ATRIB_LOCAL_SUBSTRATE_HARNESS_CLASSES']);
81
+ out.shutdownTimeoutMs =
82
+ out.shutdownTimeoutMs ??
83
+ parseOptionalPositiveInt(env['ATRIB_LOCAL_SUBSTRATE_SHUTDOWN_TIMEOUT_MS'], 'ATRIB_LOCAL_SUBSTRATE_SHUTDOWN_TIMEOUT_MS');
84
+ return out;
85
+ }
86
+ function requireValue(argv, index, flag) {
87
+ const value = argv[index];
88
+ if (value === undefined)
89
+ throw new Error(`${flag} requires a value`);
90
+ return value;
91
+ }
92
+ function parseOptionalString(raw) {
93
+ return raw === undefined || raw.trim() === '' ? undefined : raw;
94
+ }
95
+ function parsePort(raw, name) {
96
+ const n = Number(raw);
97
+ if (!Number.isInteger(n) || n < 0 || n > 65_535) {
98
+ throw new Error(`${name} must be an integer from 0 to 65535`);
99
+ }
100
+ return n;
101
+ }
102
+ function parseOptionalPort(raw, name) {
103
+ return raw === undefined || raw.trim() === '' ? undefined : parsePort(raw, name);
104
+ }
105
+ function parsePositiveInt(raw, name) {
106
+ const n = Number(raw);
107
+ if (!Number.isInteger(n) || n <= 0) {
108
+ throw new Error(`${name} must be a positive integer`);
109
+ }
110
+ return n;
111
+ }
112
+ function parseOptionalPositiveInt(raw, name) {
113
+ return raw === undefined || raw.trim() === '' ? undefined : parsePositiveInt(raw, name);
114
+ }
115
+ function parseLogSubmission(raw) {
116
+ if (raw === 'enabled' || raw === 'disabled')
117
+ return raw;
118
+ throw new Error('--log-submission must be enabled or disabled');
119
+ }
120
+ function parseOptionalLogSubmission(raw) {
121
+ return raw === undefined || raw.trim() === '' ? undefined : parseLogSubmission(raw);
122
+ }
123
+ function parseHarnessClasses(raw) {
124
+ const values = raw
125
+ .split(',')
126
+ .map((value) => value.trim())
127
+ .filter((value) => value.length > 0);
128
+ if (values.length === 0) {
129
+ throw new Error('harness classes must include at least one value');
130
+ }
131
+ for (const value of values) {
132
+ if (!supportedHarnessClasses.has(value)) {
133
+ throw new Error(`unsupported harness class ${value}; expected one of ${LOCAL_SUBSTRATE_HARNESS_CLASSES.join(', ')}`);
134
+ }
135
+ }
136
+ return [...new Set(values)];
137
+ }
138
+ function parseOptionalHarnessClasses(raw) {
139
+ return raw === undefined || raw.trim() === '' ? undefined : parseHarnessClasses(raw);
140
+ }
141
+ function readPackageVersion() {
142
+ try {
143
+ const here = dirname(fileURLToPath(import.meta.url));
144
+ const pkgPath = join(here, '..', 'package.json');
145
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
146
+ return pkg.version ?? 'unknown';
147
+ }
148
+ catch {
149
+ return 'unknown';
150
+ }
151
+ }
152
+ function printHelp() {
153
+ process.stderr.write(`atrib-local-substrate ${readPackageVersion()}
154
+ Run the host-owned P042 local substrate coordinator over loopback HTTP.
155
+
156
+ USAGE
157
+ atrib-local-substrate [--json] [--host <host>] [--port <n>]
158
+ atrib-local-substrate --describe
159
+ atrib-local-substrate --version | --help
160
+
161
+ OPTIONS
162
+ --host <host> Bind host. Defaults to 127.0.0.1.
163
+ --port <n> Bind port. Defaults to 8787. Use 0 in tests.
164
+ --max-body-bytes <n> Request body cap. Defaults to 1048576.
165
+ --log-endpoint <url> Override ATRIB_LOG_ENDPOINT.
166
+ --log-submission <mode> enabled or disabled. Defaults to enabled.
167
+ --harness-classes <csv> startup-spawn,long-lived-agent,watcher-wal by default.
168
+ --shutdown-timeout-ms <n> Best-effort flush bound on SIGTERM/SIGINT. Defaults to 2000.
169
+ --json Write the ready event as JSON on stdout.
170
+ --describe Emit a JSON description of this host binary.
171
+ --version Print package version.
172
+ --help Print this help.
173
+ `);
174
+ }
175
+ function buildDescription() {
176
+ return {
177
+ name: 'atrib-local-substrate',
178
+ version: readPackageVersion(),
179
+ description: 'Host-owned local substrate coordinator for P042. It signs exact unsigned record bodies through @atrib/mcp and exposes POST /atrib/local-substrate plus health probes over loopback HTTP.',
180
+ options: [
181
+ { flag: '--host', takes_value: true, description: 'Bind host. Defaults to 127.0.0.1.' },
182
+ { flag: '--port', takes_value: true, description: 'Bind port. Defaults to 8787.' },
183
+ {
184
+ flag: '--max-body-bytes',
185
+ takes_value: true,
186
+ description: 'Maximum request body size in bytes.',
187
+ },
188
+ { flag: '--log-endpoint', takes_value: true, description: 'Override ATRIB_LOG_ENDPOINT.' },
189
+ {
190
+ flag: '--log-submission',
191
+ takes_value: true,
192
+ description: 'enabled or disabled. Disable only for tests and local proofs.',
193
+ },
194
+ {
195
+ flag: '--harness-classes',
196
+ takes_value: true,
197
+ description: 'Comma-separated supported harness classes.',
198
+ },
199
+ {
200
+ flag: '--shutdown-timeout-ms',
201
+ takes_value: true,
202
+ description: 'Best-effort flush bound on shutdown.',
203
+ },
204
+ { flag: '--json', takes_value: false, description: 'Write the ready event as JSON.' },
205
+ ],
206
+ env_vars: [
207
+ {
208
+ name: 'ATRIB_PRIVATE_KEY',
209
+ description: 'base64url Ed25519 32-byte seed. First key source tried.',
210
+ required: false,
211
+ },
212
+ {
213
+ name: 'ATRIB_KEY_FILE',
214
+ description: 'Path to a file containing the seed. Second key source.',
215
+ required: false,
216
+ },
217
+ {
218
+ name: 'ATRIB_AGENT',
219
+ description: 'Agent label used for the agent-scoped Keychain service.',
220
+ required: false,
221
+ },
222
+ {
223
+ name: 'ATRIB_LOCAL_SUBSTRATE_HOST',
224
+ description: 'Bind host override.',
225
+ required: false,
226
+ },
227
+ {
228
+ name: 'ATRIB_LOCAL_SUBSTRATE_PORT',
229
+ description: 'Bind port override.',
230
+ required: false,
231
+ },
232
+ {
233
+ name: 'ATRIB_LOCAL_SUBSTRATE_HARNESS_CLASSES',
234
+ description: 'Comma-separated harness classes the host accepts.',
235
+ required: false,
236
+ },
237
+ {
238
+ name: 'ATRIB_LOCAL_SUBSTRATE_LOG_SUBMISSION',
239
+ description: 'enabled or disabled. Defaults to enabled.',
240
+ required: false,
241
+ },
242
+ ],
243
+ };
244
+ }
245
+ function keyScope(key) {
246
+ if (key.source === 'keychain' && key.keychainService)
247
+ return `keychain:${key.keychainService}`;
248
+ return key.source;
249
+ }
250
+ async function startLocalSubstrateHost(options) {
251
+ const harnessClasses = options.harnessClasses ?? [...LOCAL_SUBSTRATE_HARNESS_CLASSES];
252
+ const coordinator = createInProcessLocalSubstrateCoordinator({
253
+ creatorKey: base64urlEncode(options.key.privateKey),
254
+ supportedHarnessClasses: harnessClasses,
255
+ ...(options.logEndpoint !== undefined ? { logEndpoint: options.logEndpoint } : {}),
256
+ ...(options.logSubmission !== undefined ? { logSubmission: options.logSubmission } : {}),
257
+ health: {
258
+ pid: process.pid,
259
+ version: readPackageVersion(),
260
+ transport: 'node-http',
261
+ creatorKeyScope: keyScope(options.key),
262
+ activeWrappers: 0,
263
+ staleChildren: 0,
264
+ },
265
+ });
266
+ let server;
267
+ try {
268
+ server = await bindLocalSubstrateCoordinatorNodeServer(coordinator, {
269
+ host: options.host ?? LOCAL_SUBSTRATE_NODE_DEFAULT_HOST,
270
+ port: options.port ?? LOCAL_SUBSTRATE_NODE_DEFAULT_PORT,
271
+ maxBodyBytes: options.maxBodyBytes ?? LOCAL_SUBSTRATE_NODE_DEFAULT_MAX_BODY_BYTES,
272
+ });
273
+ }
274
+ catch (error) {
275
+ coordinator.destroy();
276
+ throw error;
277
+ }
278
+ let closed = false;
279
+ const close = async () => {
280
+ if (closed)
281
+ return;
282
+ closed = true;
283
+ await server.close();
284
+ await withTimeout(coordinator.flush(), options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS).catch(() => undefined);
285
+ coordinator.destroy();
286
+ };
287
+ return { coordinator, server, close };
288
+ }
289
+ async function withTimeout(promise, timeoutMs) {
290
+ let timer;
291
+ try {
292
+ return await Promise.race([
293
+ promise,
294
+ new Promise((_, reject) => {
295
+ timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
296
+ }),
297
+ ]);
298
+ }
299
+ finally {
300
+ if (timer)
301
+ clearTimeout(timer);
302
+ }
303
+ }
304
+ function writeReady(handle, jsonOutput) {
305
+ const event = {
306
+ event: 'ready',
307
+ name: 'atrib-local-substrate',
308
+ version: readPackageVersion(),
309
+ pid: process.pid,
310
+ url: handle.server.url,
311
+ endpoint: handle.server.endpoint,
312
+ health_endpoint: handle.server.healthEndpoint,
313
+ };
314
+ if (jsonOutput) {
315
+ process.stdout.write(`${JSON.stringify(event)}\n`);
316
+ }
317
+ else {
318
+ process.stderr.write(`atrib-local-substrate: listening at ${event.endpoint} (health ${event.health_endpoint})\n`);
319
+ }
320
+ }
321
+ /**
322
+ * Resolve the signing key, polling with exponential backoff instead of failing
323
+ * on the first miss.
324
+ *
325
+ * A login-managed host can start before the login Keychain is unlocked, so the
326
+ * first resolveKey() returns null. Exiting there makes the process crash-loop
327
+ * under a KeepAlive supervisor, and any dependent that waits on the substrate
328
+ * hangs. Instead we stay alive and retry until the Keychain unlocks and the key
329
+ * resolves, so a reboot self-heals with no manual restart.
330
+ *
331
+ * Returns null only when a finite ATRIB_KEY_RETRY_MAX_MS budget is exhausted
332
+ * (default 0 = wait indefinitely), preserving an explicit-failure escape hatch.
333
+ */
334
+ export async function resolveSigningKeyWithRetry(options = {}) {
335
+ const resolve = options.resolve ?? resolveKey;
336
+ const initialDelayMs = options.initialDelayMs ?? 2_000;
337
+ const maxDelayMs = Math.max(options.maxDelayMs ?? 30_000, initialDelayMs);
338
+ const maxWaitMs = options.maxWaitMs ?? 0;
339
+ const now = options.now ?? (() => Date.now());
340
+ const sleep = options.sleep ??
341
+ ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)));
342
+ const log = options.log ?? ((message) => void process.stderr.write(message));
343
+ const startedAt = now();
344
+ let attempt = 0;
345
+ let delay = initialDelayMs;
346
+ for (;;) {
347
+ attempt += 1;
348
+ let key = null;
349
+ try {
350
+ key = await resolve();
351
+ }
352
+ catch (error) {
353
+ // A throw here (e.g. a transient ATRIB_KEY_FILE read) is treated like a
354
+ // miss and retried, but logged each time so a genuine misconfiguration
355
+ // stays visible instead of silently looping.
356
+ log(`atrib-local-substrate: key resolution error on attempt ${attempt}: ${error instanceof Error ? error.message : String(error)}\n`);
357
+ }
358
+ if (key) {
359
+ if (attempt > 1) {
360
+ const waitedSec = Math.round((now() - startedAt) / 1_000);
361
+ log(`atrib-local-substrate: signing key resolved after ${attempt} attempts (~${waitedSec}s)\n`);
362
+ }
363
+ return key;
364
+ }
365
+ if (maxWaitMs > 0 && now() - startedAt >= maxWaitMs)
366
+ return null;
367
+ if (attempt === 1) {
368
+ log('atrib-local-substrate: no signing key yet (the login Keychain may be locked at boot); ' +
369
+ 'waiting and retrying with backoff. Set ATRIB_KEY_RETRY_MAX_MS to cap the wait.\n');
370
+ }
371
+ await sleep(delay);
372
+ delay = Math.min(delay * 2, maxDelayMs);
373
+ }
374
+ }
375
+ function keyRetryOptionsFromEnv() {
376
+ const options = {};
377
+ const initial = parseOptionalPositiveInt(process.env['ATRIB_KEY_RETRY_INTERVAL_MS'], 'ATRIB_KEY_RETRY_INTERVAL_MS');
378
+ if (initial !== undefined)
379
+ options.initialDelayMs = initial;
380
+ const maxDelay = parseOptionalPositiveInt(process.env['ATRIB_KEY_RETRY_MAX_INTERVAL_MS'], 'ATRIB_KEY_RETRY_MAX_INTERVAL_MS');
381
+ if (maxDelay !== undefined)
382
+ options.maxDelayMs = maxDelay;
383
+ const maxWaitRaw = process.env['ATRIB_KEY_RETRY_MAX_MS'];
384
+ if (maxWaitRaw !== undefined && maxWaitRaw.trim() !== '') {
385
+ const n = Number(maxWaitRaw);
386
+ if (!Number.isInteger(n) || n < 0) {
387
+ throw new Error('ATRIB_KEY_RETRY_MAX_MS must be a non-negative integer');
388
+ }
389
+ options.maxWaitMs = n;
390
+ }
391
+ return options;
392
+ }
393
+ async function main() {
394
+ let args;
395
+ try {
396
+ args = parseArgs(process.argv.slice(2));
397
+ }
398
+ catch (error) {
399
+ process.stderr.write(`atrib-local-substrate: ${error instanceof Error ? error.message : String(error)}\n`);
400
+ process.exit(1);
401
+ }
402
+ if (args.showVersion) {
403
+ process.stdout.write(`${readPackageVersion()}\n`);
404
+ return;
405
+ }
406
+ if (args.showHelp) {
407
+ printHelp();
408
+ return;
409
+ }
410
+ if (args.showDescribe) {
411
+ process.stdout.write(`${JSON.stringify(buildDescription(), null, 2)}\n`);
412
+ return;
413
+ }
414
+ const key = await resolveSigningKeyWithRetry(keyRetryOptionsFromEnv());
415
+ if (!key) {
416
+ process.stderr.write('atrib-local-substrate: no signing key resolved within ATRIB_KEY_RETRY_MAX_MS; set ATRIB_PRIVATE_KEY, ATRIB_KEY_FILE, or an atrib Keychain entry\n');
417
+ process.exit(1);
418
+ }
419
+ const handle = await startLocalSubstrateHost({
420
+ key,
421
+ ...(args.host !== undefined ? { host: args.host } : {}),
422
+ ...(args.port !== undefined ? { port: args.port } : {}),
423
+ ...(args.maxBodyBytes !== undefined ? { maxBodyBytes: args.maxBodyBytes } : {}),
424
+ ...(args.logEndpoint !== undefined ? { logEndpoint: args.logEndpoint } : {}),
425
+ ...(args.logSubmission !== undefined ? { logSubmission: args.logSubmission } : {}),
426
+ ...(args.harnessClasses !== undefined ? { harnessClasses: args.harnessClasses } : {}),
427
+ ...(args.shutdownTimeoutMs !== undefined ? { shutdownTimeoutMs: args.shutdownTimeoutMs } : {}),
428
+ });
429
+ writeReady(handle, args.jsonOutput);
430
+ let shuttingDown = false;
431
+ const shutdown = (signal) => {
432
+ if (shuttingDown)
433
+ return;
434
+ shuttingDown = true;
435
+ process.stderr.write(`atrib-local-substrate: received ${signal}, shutting down\n`);
436
+ handle
437
+ .close()
438
+ .then(() => process.exit(0))
439
+ .catch((error) => {
440
+ process.stderr.write(`atrib-local-substrate: shutdown error ${error instanceof Error ? error.message : String(error)}\n`);
441
+ process.exit(1);
442
+ });
443
+ };
444
+ process.on('SIGINT', shutdown);
445
+ process.on('SIGTERM', shutdown);
446
+ }
447
+ if (import.meta.url === `file://${process.argv[1]}`) {
448
+ main().catch((error) => {
449
+ process.stderr.write(`atrib-local-substrate: fatal ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
450
+ process.exit(1);
451
+ });
452
+ }
453
+ export const __test_only__ = {
454
+ buildDescription,
455
+ parseArgs,
456
+ startLocalSubstrateHost,
457
+ resolveSigningKeyWithRetry,
458
+ };
459
+ /**
460
+ * Run the local-substrate host entrypoint. Exists so forwarding bins (the
461
+ * @atrib/emit shim's `atrib-local-substrate`) can invoke the host without
462
+ * relying on the entrypoint guard above, which only fires when this file
463
+ * itself is process.argv[1].
464
+ */
465
+ export async function runLocalSubstrateHost() {
466
+ return main();
467
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // atrib-attest standalone binary. Serves the write-verb union (attest +
3
+ // emit + atrib-annotate + atrib-revise) over a stdio transport so it can be
4
+ // launched as a subprocess by an MCP host (Claude Code, Claude Desktop,
5
+ // etc.). All four names dispatch to one handleEmit funnel; records are
6
+ // byte-identical regardless of which name signed them.
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { createAtribAttestServer } from './index.js';
9
+ async function main() {
10
+ const { mcp } = await createAtribAttestServer();
11
+ const transport = new StdioServerTransport();
12
+ await mcp.connect(transport);
13
+ // Stays alive on the stdio transport until the host closes it.
14
+ }
15
+ main().catch((e) => {
16
+ console.error('atrib-attest: fatal', e instanceof Error ? e.stack ?? e.message : String(e));
17
+ process.exit(1);
18
+ });
@@ -0,0 +1,12 @@
1
+ import { defaultRecordReferenceResolver as sharedDefaultRecordReferenceResolver, type RecordReferenceResolution } from '@atrib/mcp';
2
+ export type RecordReferenceResolver = (recordHash: string) => RecordReferenceResolution | Promise<RecordReferenceResolution>;
3
+ interface FilterResolvableInformedByOptions {
4
+ allowUnresolved?: boolean | undefined;
5
+ resolver?: RecordReferenceResolver | undefined;
6
+ logEndpoint?: string | undefined;
7
+ warnings: string[];
8
+ }
9
+ export declare const defaultRecordReferenceResolver: typeof sharedDefaultRecordReferenceResolver;
10
+ export declare function filterResolvableInformedBy(refs: string[] | undefined, options: FilterResolvableInformedByOptions): Promise<string[] | undefined>;
11
+ export declare function clearRecordReferenceResolverCacheForTests(): void;
12
+ export {};
@@ -0,0 +1,31 @@
1
+ import { clearRecordReferenceResolverCacheForTests as clearSharedRecordReferenceResolverCacheForTests, defaultRecordReferenceResolver as sharedDefaultRecordReferenceResolver, } from '@atrib/mcp';
2
+ export const defaultRecordReferenceResolver = sharedDefaultRecordReferenceResolver;
3
+ export async function filterResolvableInformedBy(refs, options) {
4
+ if (!refs || refs.length === 0)
5
+ return undefined;
6
+ const unique = [...new Set(refs)];
7
+ if (options.allowUnresolved)
8
+ return unique;
9
+ const kept = [];
10
+ const resolver = options.resolver ??
11
+ ((recordHash) => defaultRecordReferenceResolver(recordHash, options.logEndpoint));
12
+ for (const ref of unique) {
13
+ const resolution = await resolver(ref);
14
+ if (resolution === 'found') {
15
+ kept.push(ref);
16
+ continue;
17
+ }
18
+ if (resolution === 'unknown') {
19
+ options.warnings.push(`dropped unvalidated informed_by reference ${shortHash(ref)}; validation unavailable`);
20
+ continue;
21
+ }
22
+ options.warnings.push(`dropped unresolved informed_by reference ${shortHash(ref)}; not found in local mirrors or log lookup`);
23
+ }
24
+ return kept.length > 0 ? kept : undefined;
25
+ }
26
+ function shortHash(recordHash) {
27
+ return `${recordHash.slice(0, 19)}...${recordHash.slice(-8)}`;
28
+ }
29
+ export function clearRecordReferenceResolverCacheForTests() {
30
+ clearSharedRecordReferenceResolverCacheForTests();
31
+ }
@@ -0,0 +1,43 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { type EmitLocalSubstrateShadowOptions, type WriteToolDeps } from './index.js';
4
+ import { type ResolvedKey } from './keys.js';
5
+ export declare const ReviseInput: z.ZodObject<{
6
+ revises: z.ZodString;
7
+ prior_position: z.ZodString;
8
+ new_position: z.ZodString;
9
+ reason: z.ZodString;
10
+ topics: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ context_id: z.ZodOptional<z.ZodString>;
12
+ informed_by: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
+ }, z.core.$strip>;
14
+ export interface AtribReviseServer {
15
+ /** Underlying McpServer; expose for testing or composition. */
16
+ mcp: McpServer;
17
+ /** Drain pending submissions (for tests/shutdown). */
18
+ flush(): Promise<void>;
19
+ }
20
+ export interface CreateAtribReviseServerOptions {
21
+ /** Override the resolved key (primarily for testing). */
22
+ key?: ResolvedKey | null | undefined;
23
+ /** Override the log endpoint (defaults to env or @atrib/mcp default). */
24
+ logEndpoint?: string | undefined;
25
+ /**
26
+ * Optional long-lived-agent local substrate shadow probe. `undefined` reads
27
+ * opt-in env config; `false` disables env config for this server.
28
+ */
29
+ localSubstrate?: EmitLocalSubstrateShadowOptions | false | undefined;
30
+ }
31
+ /**
32
+ * Register the legacy `atrib-revise` tool. The handler call shape is
33
+ * unchanged from the standalone @atrib/revise server, so historical
34
+ * behavior is preserved exactly; only the mounting surface moved.
35
+ */
36
+ export declare function registerReviseTool(mcp: McpServer, deps: WriteToolDeps): void;
37
+ /**
38
+ * Wire up the legacy atrib-revise MCP server. Mounts `atrib-revise` plus
39
+ * `attest` (alias-window rule W1). The underlying signing pipeline is
40
+ * shared via handleEmit so revision records are byte-identical regardless
41
+ * of which tool produced them.
42
+ */
43
+ export declare function createAtribReviseServer(options?: CreateAtribReviseServerOptions): Promise<AtribReviseServer>;