@atrib/attest 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,6 +146,12 @@ Environment variables are unchanged from the legacy emit surface:
146
146
  table with descriptions; the values and resolution order carried over
147
147
  unchanged.
148
148
 
149
+ Programmatic callers can set `mirrorPath` and `autochainSource` in
150
+ `EmitInProcessOptions`. `mirrorPath` selects the write target for one call.
151
+ `autochainSource` selects its mirror-based chain inheritance source. When only
152
+ `mirrorPath` is set, it is also the read source. Calls that omit both options
153
+ retain the environment and per-agent default ladder above.
154
+
149
155
  ## Degradation and privacy
150
156
 
151
157
  atrib failures never affect the primary call ([spec §5.8](../../atrib-spec.md#58-degradation-contract)), and for the write
package/dist/attest.js CHANGED
@@ -86,9 +86,8 @@ export const AttestInput = z.object({
86
86
  tool_name: z
87
87
  .string()
88
88
  .min(1)
89
- .max(64)
90
89
  .optional()
91
- .describe('Optional §8.2 tool_name disclosure, unchanged from the legacy emit tool.'),
90
+ .describe('Optional §8.2 tool_name disclosure. The 64-character cap applies only to opaque labels; hashed and verbatim forms follow §8.2.'),
92
91
  args_hash: z
93
92
  .string()
94
93
  .regex(SHA256_REF_PATTERN)
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { type LocalSubstrateCoordinatorTransport, type LocalSubstrateDegradationPolicy, type LocalSubstrateHarnessClass, type LocalSubstrateProducer, type LocalSubstrateWalJoin, type ProofBundle, type SubmissionQueue, type TryLocalSubstrateCoordinatorResult } from '@atrib/mcp';
4
4
  import { type ResolvedKey } from './keys.js';
5
5
  import { type RecordReferenceResolver } from './reference-resolution.js';
6
+ import { resolveMirrorWritePath } from './storage.js';
6
7
  import { type AttestInputT } from './attest.js';
7
8
  declare function keyResolveRetryMs(): number;
8
9
  export declare function requiresExplicitContextId(env?: NodeJS.ProcessEnv): boolean;
@@ -175,7 +176,7 @@ declare function createServerKeyResolver(options: CreateAtribEmitServerOptions):
175
176
  interface HandleEmitInput {
176
177
  input: z.infer<typeof EmitInput>;
177
178
  key: ResolvedKey | null;
178
- queue: SubmissionQueue;
179
+ queue?: SubmissionQueue;
179
180
  /**
180
181
  * Producer label written to the sidecar's `_local.producer` field for
181
182
  * cross-source disambiguation in mirror queries. Defaults to
@@ -185,6 +186,13 @@ interface HandleEmitInput {
185
186
  */
186
187
  producer?: string;
187
188
  logEndpoint?: string | undefined;
189
+ /** Explicit write target for this call. Falls back to ATRIB_MIRROR_FILE. */
190
+ mirrorPath?: string | undefined;
191
+ /**
192
+ * Explicit read source for mirror-based chain inheritance. When omitted,
193
+ * mirrorPath wins before the legacy environment and default path ladder.
194
+ */
195
+ autochainSource?: string | undefined;
188
196
  recordReferenceResolver?: RecordReferenceResolver | undefined;
189
197
  localSubstrate?: EmitLocalSubstrateShadowOptions | undefined;
190
198
  localSubstrateCommit?: EmitLocalSubstrateCommitOptions | undefined;
@@ -193,7 +201,7 @@ interface HandleEmitInput {
193
201
  * Build, sign, submit, mirror. Refused writes return `signed: false`;
194
202
  * signed degradations stay `signed: true` and surface in `warnings`.
195
203
  */
196
- declare function handleEmit({ input, key, queue, producer, logEndpoint, recordReferenceResolver, localSubstrate, localSubstrateCommit, }: HandleEmitInput): Promise<EmitOutput>;
204
+ declare function handleEmit({ input, key, queue, producer, logEndpoint, mirrorPath, autochainSource, recordReferenceResolver, localSubstrate, localSubstrateCommit, }: HandleEmitInput): Promise<EmitOutput>;
197
205
  export declare function resolveEmitLocalSubstrateShadowFromEnv(options?: ResolveEmitLocalSubstrateShadowFromEnvOptions): EmitLocalSubstrateShadowOptions | undefined;
198
206
  export declare function resolveEmitLocalSubstrateCommitFromEnv(options?: ResolveEmitLocalSubstrateCommitFromEnvOptions): EmitLocalSubstrateCommitOptions | undefined;
199
207
  export interface EmitInProcessOptions {
@@ -201,6 +209,11 @@ export interface EmitInProcessOptions {
201
209
  key?: ResolvedKey;
202
210
  /** Override the log endpoint (defaults to ATRIB_LOG_ENDPOINT or @atrib/mcp default). */
203
211
  logEndpoint?: string | undefined;
212
+ /**
213
+ * Set false for local-only producers. The record is still signed and
214
+ * mirrored, but this process does not create a public-log queue.
215
+ */
216
+ submit?: boolean;
204
217
  /**
205
218
  * Producer label written to the sidecar's `_local.producer` field for
206
219
  * cross-source disambiguation. Defaults to `'atrib-emit'`. Callers that
@@ -210,6 +223,17 @@ export interface EmitInProcessOptions {
210
223
  * consumers can bucket records by emitter without inspecting envelopes.
211
224
  */
212
225
  producer?: string;
226
+ /**
227
+ * Explicit mirror write target for this call. When omitted,
228
+ * ATRIB_MIRROR_FILE and the per-agent default remain unchanged fallbacks.
229
+ */
230
+ mirrorPath?: string | undefined;
231
+ /**
232
+ * Explicit mirror read source for chain inheritance. When omitted, an
233
+ * explicit mirrorPath is also the read source before the environment
234
+ * fallback ladder.
235
+ */
236
+ autochainSource?: string | undefined;
213
237
  /**
214
238
  * Upper bound on the post-sign queue flush, in milliseconds. Default
215
239
  * 5000ms. The submission queue itself has a 30s retry budget against an
@@ -277,6 +301,8 @@ export interface HandleAttestInput {
277
301
  /** Sidecar label; defaults to 'atrib-attest'. input.producer wins. */
278
302
  producer?: string;
279
303
  logEndpoint?: string | undefined;
304
+ mirrorPath?: string | undefined;
305
+ autochainSource?: string | undefined;
280
306
  recordReferenceResolver?: RecordReferenceResolver | undefined;
281
307
  localSubstrate?: EmitLocalSubstrateShadowOptions | undefined;
282
308
  localSubstrateCommit?: EmitLocalSubstrateCommitOptions | undefined;
@@ -301,6 +327,7 @@ export declare const __test_only__: {
301
327
  keyResolveRetryMs: typeof keyResolveRetryMs;
302
328
  };
303
329
  export { handleEmit, EmitInput };
330
+ export { resolveMirrorWritePath };
304
331
  export type { EmitOutput };
305
332
  export { resolveKey } from './keys.js';
306
333
  export type { ResolvedKey } from './keys.js';
package/dist/index.js CHANGED
@@ -17,13 +17,11 @@
17
17
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
18
18
  import { z } from 'zod';
19
19
  import { randomBytes } from 'node:crypto';
20
- import { homedir } from 'node:os';
21
- import { join } from 'node:path';
22
20
  import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, LOCAL_SUBSTRATE_DEFAULT_TIMEOUT_MS, LOCAL_SUBSTRATE_REQUEST_SCHEMA, canonicalRecord, createHttpLocalSubstrateTransport, createSubmissionQueue, genesisChainRoot, hexEncode, inheritChainContext, isValidEventTypeUri, normalizeEventType, parentRecordHashFromEnv, resolveEnvContextId, SHA256_REF_PATTERN, sha256, tryLocalSubstrateCoordinator, } from '@atrib/mcp';
23
21
  import { resolveKey } from './keys.js';
24
22
  import { filterResolvableInformedBy } from './reference-resolution.js';
25
23
  import { buildAndSignEmitRecord } from './sign.js';
26
- import { mirrorRecord } from './storage.js';
24
+ import { mirrorRecord, resolveMirrorWritePath } from './storage.js';
27
25
  import { AttestInput, isAttestMappingRefusal, mapAttestInput } from './attest.js';
28
26
  import { registerAnnotateTool } from './annotate.js';
29
27
  import { registerReviseTool } from './revise.js';
@@ -33,10 +31,11 @@ import { registerReviseTool } from './revise.js';
33
31
  // rarely useful as a read source, but kept for backward compatibility),
34
32
  // then to the per-agent default. Distinct from ATRIB_MIRROR_FILE which is
35
33
  // where emit's own records are persisted.
36
- function readMirrorPath() {
37
- return (process.env['ATRIB_AUTOCHAIN_SOURCE'] ??
38
- process.env['ATRIB_MIRROR_FILE'] ??
39
- join(homedir(), '.atrib', 'records', `atrib-emit-${process.env['ATRIB_AGENT'] ?? 'claude-code'}.jsonl`));
34
+ function readMirrorPath(options = {}) {
35
+ return (options.autochainSource ??
36
+ options.mirrorPath ??
37
+ process.env['ATRIB_AUTOCHAIN_SOURCE'] ??
38
+ resolveMirrorWritePath());
40
39
  }
41
40
  const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
42
41
  // 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
@@ -127,10 +126,11 @@ const EmitInput = z.object({
127
126
  tool_name: z
128
127
  .string()
129
128
  .min(1)
130
- .max(64)
131
129
  .optional()
132
130
  .describe('Optional §8.2 tool_name disclosure. Verbatim or transformed name (verbatim, opaque, ' +
133
- 'or hashed per §8.2). Lets emit-signed records carry the tool name for downstream ' +
131
+ 'or hashed per §8.2). The 64-character cap applies only to opaque-label form; ' +
132
+ 'the hashed form is sha256:<64 lowercase hex>, and verbatim names have no ' +
133
+ 'protocol-wide length cap. Lets emit-signed records carry the tool name for downstream ' +
134
134
  'consumers (e.g. recall_my_attribution_history filtering by tool_name). Absence ' +
135
135
  'indicates the §8.1 default posture (no disclosure).'),
136
136
  args_hash: z
@@ -339,7 +339,7 @@ function createServerKeyResolver(options) {
339
339
  * Build, sign, submit, mirror. Refused writes return `signed: false`;
340
340
  * signed degradations stay `signed: true` and surface in `warnings`.
341
341
  */
342
- async function handleEmit({ input, key, queue, producer, logEndpoint, recordReferenceResolver, localSubstrate, localSubstrateCommit, }) {
342
+ async function handleEmit({ input, key, queue, producer, logEndpoint, mirrorPath, autochainSource, recordReferenceResolver, localSubstrate, localSubstrateCommit, }) {
343
343
  const warnings = [];
344
344
  const eventType = normalizeEventType(input.event_type);
345
345
  if (!isValidEventTypeUri(eventType)) {
@@ -430,10 +430,14 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
430
430
  // is the primary case that needs preserving; pre-fix this produced isolated
431
431
  // genesis records because atrib-emit's local resolver short-circuited on
432
432
  // caller context.
433
+ const autochainMirrorPath = readMirrorPath({ autochainSource, mirrorPath });
433
434
  const chain = await inheritChainContext({
434
435
  callerContextId,
435
436
  callerChainRoot: input.chain_root,
436
- mirrorPath: readMirrorPath(),
437
+ mirrorPath: autochainMirrorPath,
438
+ ...((mirrorPath !== undefined || autochainSource !== undefined) && {
439
+ mirrorScope: 'file',
440
+ }),
437
441
  randomContextId,
438
442
  });
439
443
  const contextId = chain.contextId;
@@ -464,6 +468,9 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
464
468
  allowUnresolved: input.allow_unresolved_informed_by,
465
469
  resolver: recordReferenceResolver,
466
470
  logEndpoint,
471
+ ...((mirrorPath !== undefined || autochainSource !== undefined) && {
472
+ localMirrorPaths: [resolveMirrorWritePath(mirrorPath), autochainMirrorPath],
473
+ }),
467
474
  warnings,
468
475
  });
469
476
  const effectiveInformedBy = validParentHash
@@ -528,7 +535,7 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
528
535
  // Submit asynchronously; the queue handles retry + degradation per §5.8.
529
536
  // Cognitive events default to normal priority, annotations/observations
530
537
  // never need to block the agent.
531
- if (!localSubstrateCommitted) {
538
+ if (!localSubstrateCommitted && queue) {
532
539
  queue.submit(record, 'normal');
533
540
  }
534
541
  // Best-effort mirror; mirrorRecord internally swallows errors per §5.8.
@@ -539,7 +546,7 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
539
546
  await mirrorRecord(record, getProofFor(queue, recordHash) ?? null, {
540
547
  content: input.content,
541
548
  producer: producer ?? 'atrib-emit',
542
- });
549
+ }, mirrorPath);
543
550
  // Try to read a proof if the queue submitted synchronously and the log
544
551
  // returned one within the same tick. Most submissions return null here
545
552
  // and the proof shows up on a later poll via getProof.
@@ -547,7 +554,7 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
547
554
  if (!proof && localSubstrateCommitted) {
548
555
  warnings.push('submission delegated to local substrate coordinator; proof not available in this process');
549
556
  }
550
- else if (!proof) {
557
+ else if (!proof && queue) {
551
558
  warnings.push('submission queued; proof not yet available (poll the log later if needed)');
552
559
  }
553
560
  if (localSubstrate?.waitForAttempt && localSubstrateShadow) {
@@ -874,6 +881,8 @@ function notifyLocalSubstrateWarning(localSubstrate, message, detail) {
874
881
  * warning, even when the record had already landed on the log.
875
882
  */
876
883
  function getProofFor(queue, recordHash) {
884
+ if (!queue)
885
+ return undefined;
877
886
  return queue.getProof(recordHash.startsWith('sha256:') ? recordHash.slice('sha256:'.length) : recordHash);
878
887
  }
879
888
  const DEFAULT_FLUSH_DEADLINE_MS = 5000;
@@ -910,13 +919,15 @@ export async function emitInProcess(rawInput, options = {}) {
910
919
  const key = options.key ?? (await resolveKey());
911
920
  const logEndpoint = options.logEndpoint ?? process.env['ATRIB_LOG_ENDPOINT'];
912
921
  const flushDeadlineMs = options.flushDeadlineMs ?? DEFAULT_FLUSH_DEADLINE_MS;
913
- const queue = createSubmissionQueue(logEndpoint);
922
+ const queue = options.submit === false ? undefined : createSubmissionQueue(logEndpoint);
914
923
  const result = await handleEmit({
915
924
  input,
916
925
  key,
917
- queue,
926
+ ...(queue !== undefined ? { queue } : {}),
918
927
  producer: options.producer,
919
928
  logEndpoint,
929
+ mirrorPath: options.mirrorPath,
930
+ autochainSource: options.autochainSource,
920
931
  recordReferenceResolver: options.recordReferenceResolver,
921
932
  localSubstrate: resolveLocalSubstrateOption(options.localSubstrate, {
922
933
  producer: options.producer ?? 'atrib-emit',
@@ -932,7 +943,7 @@ export async function emitInProcess(rawInput, options = {}) {
932
943
  transport: 'emit-in-process',
933
944
  }),
934
945
  });
935
- if (!result.signed) {
946
+ if (!result.signed || queue === undefined) {
936
947
  return result;
937
948
  }
938
949
  // Drain before returning, bounded by flushDeadlineMs. The typical caller
@@ -979,6 +990,8 @@ export async function handleAttest(args) {
979
990
  queue: args.queue,
980
991
  producer: args.input.producer ?? args.producer ?? 'atrib-attest',
981
992
  logEndpoint: args.logEndpoint,
993
+ mirrorPath: args.mirrorPath,
994
+ autochainSource: args.autochainSource,
982
995
  recordReferenceResolver: args.recordReferenceResolver,
983
996
  localSubstrate: args.localSubstrate,
984
997
  localSubstrateCommit: args.localSubstrateCommit,
@@ -1025,6 +1038,7 @@ export const __test_only__ = { createServerKeyResolver, handleEmit, keyResolveRe
1025
1038
  //
1026
1039
  // Stable as of @atrib/emit@0.8.0. Breaking changes here require a major bump.
1027
1040
  export { handleEmit, EmitInput };
1041
+ export { resolveMirrorWritePath };
1028
1042
  export { resolveKey } from './keys.js';
1029
1043
  // Session-checkpoint emission (§1.2.10 / D139): reads the ordered record
1030
1044
  // hashes for a context from the local mirror per §5.9 and emits the
@@ -4,6 +4,7 @@ interface FilterResolvableInformedByOptions {
4
4
  allowUnresolved?: boolean | undefined;
5
5
  resolver?: RecordReferenceResolver | undefined;
6
6
  logEndpoint?: string | undefined;
7
+ localMirrorPaths?: readonly string[] | undefined;
7
8
  warnings: string[];
8
9
  }
9
10
  export declare const defaultRecordReferenceResolver: typeof sharedDefaultRecordReferenceResolver;
@@ -8,7 +8,9 @@ export async function filterResolvableInformedBy(refs, options) {
8
8
  return unique;
9
9
  const kept = [];
10
10
  const resolver = options.resolver ??
11
- ((recordHash) => defaultRecordReferenceResolver(recordHash, options.logEndpoint));
11
+ ((recordHash) => defaultRecordReferenceResolver(recordHash, options.logEndpoint, {
12
+ localMirrorPaths: options.localMirrorPaths,
13
+ }));
12
14
  for (const ref of unique) {
13
15
  const resolution = await resolver(ref);
14
16
  if (resolution === 'found') {
package/dist/storage.d.ts CHANGED
@@ -18,10 +18,11 @@ export interface MirrorLine {
18
18
  /** Optional local-only sidecar; absent on legacy entries. */
19
19
  _local?: LocalSidecar;
20
20
  }
21
+ export declare function resolveMirrorWritePath(explicitPath?: string): string;
21
22
  /**
22
23
  * Append one record + optional proof + optional local sidecar to the
23
24
  * mirror file. Failures log with the atrib-emit prefix and otherwise
24
25
  * no-op, per §5.8 the mirror is best-effort and never blocks the
25
26
  * agent.
26
27
  */
27
- export declare function mirrorRecord(record: AtribRecord, proof: ProofBundle | null, localSidecar?: LocalSidecar): Promise<void>;
28
+ export declare function mirrorRecord(record: AtribRecord, proof: ProofBundle | null, localSidecar?: LocalSidecar, explicitPath?: string): Promise<void>;
package/dist/storage.js CHANGED
@@ -30,8 +30,9 @@ import { appendFile, mkdir } from 'node:fs/promises';
30
30
  import { homedir } from 'node:os';
31
31
  import { dirname, join } from 'node:path';
32
32
  let ensuredDirs = new Set();
33
- function mirrorPath() {
34
- return (process.env['ATRIB_MIRROR_FILE'] ??
33
+ export function resolveMirrorWritePath(explicitPath) {
34
+ return (explicitPath ??
35
+ process.env['ATRIB_MIRROR_FILE'] ??
35
36
  join(homedir(), '.atrib', 'records', `atrib-emit-${process.env['ATRIB_AGENT'] ?? 'claude-code'}.jsonl`));
36
37
  }
37
38
  /**
@@ -40,8 +41,8 @@ function mirrorPath() {
40
41
  * no-op, per §5.8 the mirror is best-effort and never blocks the
41
42
  * agent.
42
43
  */
43
- export async function mirrorRecord(record, proof, localSidecar) {
44
- const path = mirrorPath();
44
+ export async function mirrorRecord(record, proof, localSidecar, explicitPath) {
45
+ const path = resolveMirrorWritePath(explicitPath);
45
46
  const line = { record, proof, written_at: Date.now() };
46
47
  if (localSidecar) {
47
48
  line._local = localSidecar;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atrib/attest",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "MCP server for atrib's write verb. One attest tool signs observations, annotations, and revisions; the legacy emit, atrib-annotate, and atrib-revise tool names stay mounted as aliases over the same handler.",
5
5
  "keywords": [
6
6
  "atrib",
@@ -45,7 +45,7 @@
45
45
  "@noble/hashes": "^2.2.0",
46
46
  "canonicalize": "^3.0.0",
47
47
  "zod": "^3.25.76",
48
- "@atrib/mcp": "0.22.0"
48
+ "@atrib/mcp": "0.23.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^26.1.1",