@atrib/attest 0.1.0 → 0.3.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/README.md CHANGED
@@ -54,18 +54,20 @@ attest({
54
54
  provenance_token?: string,
55
55
  tool_name?: string,
56
56
  args_hash?: string,
57
+ args_salt?: string, // base64url 16-byte salt; requires args_hash
57
58
  result_hash?: string,
59
+ result_salt?: string, // base64url 16-byte salt; requires result_hash
58
60
  producer?: string, // sidecar label override; defaults to 'atrib-attest'
59
61
  })
60
62
  ```
61
63
 
62
64
  ### Ref mapping
63
65
 
64
- | `ref` argument | Signed record | Signed field carried |
65
- | --- | --- | --- |
66
- | absent | observation | none |
67
- | `{ kind: "annotates", target, ... }` | annotation | signed `annotates` field |
68
- | `{ kind: "revises", target, reason, ... }` (`reason` required) | revision | signed `revises` field |
66
+ | `ref` argument | Signed record | Signed field carried |
67
+ | -------------------------------------------------------------- | ------------- | ------------------------ |
68
+ | absent | observation | none |
69
+ | `{ kind: "annotates", target, ... }` | annotation | signed `annotates` field |
70
+ | `{ kind: "revises", target, reason, ... }` (`reason` required) | revision | signed `revises` field |
69
71
 
70
72
  The three shapes route through the same `handleEmit` funnel used by the
71
73
  legacy tools. There is no separate signing path for `attest`: the ref
@@ -110,10 +112,10 @@ re-export shims.
110
112
 
111
113
  ```typescript
112
114
  import {
113
- createAtribAttestServer, // mounts the four-tool write union
114
- createAtribEmitServer, // legacy factory; also mounts attest
115
- createAtribAnnotateServer, // legacy factory; also mounts attest
116
- createAtribReviseServer, // legacy factory; also mounts attest
115
+ createAtribAttestServer, // mounts the four-tool write union
116
+ createAtribEmitServer, // legacy factory; also mounts attest
117
+ createAtribAnnotateServer, // legacy factory; also mounts attest
118
+ createAtribReviseServer, // legacy factory; also mounts attest
117
119
  handleAttest,
118
120
  attestInProcess,
119
121
 
@@ -123,7 +125,7 @@ import {
123
125
  EmitInput,
124
126
  resolveKey,
125
127
  emitSessionCheckpoint,
126
- } from "@atrib/attest";
128
+ } from '@atrib/attest'
127
129
  ```
128
130
 
129
131
  Each legacy factory (`createAtribEmitServer`, `createAtribAnnotateServer`,
@@ -144,6 +146,12 @@ Environment variables are unchanged from the legacy emit surface:
144
146
  table with descriptions; the values and resolution order carried over
145
147
  unchanged.
146
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
+
147
155
  ## Degradation and privacy
148
156
 
149
157
  atrib failures never affect the primary call ([spec §5.8](../../atrib-spec.md#58-degradation-contract)), and for the write
@@ -160,6 +168,11 @@ verb that contract is the most important thing to know:
160
168
  carries only the replay-checkable `args_hash` commitment over it. The
161
169
  public log stores the 90-byte commitment entry (record hash, creator key,
162
170
  context_id, timestamp, event_type byte), never the content.
171
+ - **Salted direct commitments.** `args_salt` and `result_salt` let direct
172
+ attest callers carry the same salted commitment posture as middleware.
173
+ Each salt must be canonical base64url for exactly 16 bytes and requires its
174
+ matching explicit hash. The shared `@atrib/mcp` commitment helper computes
175
+ and verifies the bytes used by both paths.
163
176
  - **Refused writes are loud, not silent.** A malformed call (unknown
164
177
  `ref.kind`, missing revises reason, a `ref`/content contradiction, a
165
178
  provenance_token off genesis, or no resolvable signing key) returns
package/dist/attest.d.ts CHANGED
@@ -24,7 +24,9 @@ export declare const AttestInput: z.ZodObject<{
24
24
  provenance_token: z.ZodOptional<z.ZodString>;
25
25
  tool_name: z.ZodOptional<z.ZodString>;
26
26
  args_hash: z.ZodOptional<z.ZodString>;
27
+ args_salt: z.ZodOptional<z.ZodString>;
27
28
  result_hash: z.ZodOptional<z.ZodString>;
29
+ result_salt: z.ZodOptional<z.ZodString>;
28
30
  producer: z.ZodOptional<z.ZodString>;
29
31
  }, z.core.$strip>;
30
32
  export type AttestInputT = z.infer<typeof AttestInput>;
@@ -46,7 +48,9 @@ export interface MappedAttestInput {
46
48
  revises?: string;
47
49
  tool_name?: string;
48
50
  args_hash?: string;
51
+ args_salt?: string;
49
52
  result_hash?: string;
53
+ result_salt?: string;
50
54
  };
51
55
  }
52
56
  export interface AttestMappingRefusal {
package/dist/attest.js CHANGED
@@ -16,19 +16,29 @@
16
16
  // (matching what atrib-annotate / atrib-revise compose today), so the
17
17
  // D099 default args_hash commitment is byte-identical across both names.
18
18
  import { z } from 'zod';
19
- import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, SHA256_REF_PATTERN, } from '@atrib/mcp';
19
+ import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, SHA256_REF_PATTERN } from '@atrib/mcp';
20
20
  const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
21
21
  // 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
22
22
  const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
23
23
  const OBSERVATION_URI = 'https://atrib.dev/v1/types/observation';
24
24
  export const AttestRef = z.object({
25
- kind: z.enum(['annotates', 'revises']).describe("Declared-relationship kind. 'annotates' marks a past record's importance " +
25
+ kind: z
26
+ .enum(['annotates', 'revises'])
27
+ .describe("Declared-relationship kind. 'annotates' marks a past record's importance " +
26
28
  "and meaning (spec §1.2.7 / D058); 'revises' supersedes a prior position " +
27
29
  'with a stated reason (spec §1.2.9 / D059). Omit `ref` entirely for a ' +
28
30
  'plain observation.'),
29
- target: z.string().regex(SHA256_REF_PATTERN).describe("'sha256:<64-hex>' record_hash the relationship points at. The target can " +
31
+ target: z
32
+ .string()
33
+ .regex(SHA256_REF_PATTERN)
34
+ .describe("'sha256:<64-hex>' record_hash the relationship points at. The target can " +
30
35
  "be any prior record (yours or another agent's)."),
31
- reason: z.string().min(1).max(4096).optional().describe("Why the position changed. REQUIRED when kind is 'revises'; composed into " +
36
+ reason: z
37
+ .string()
38
+ .min(1)
39
+ .max(4096)
40
+ .optional()
41
+ .describe("Why the position changed. REQUIRED when kind is 'revises'; composed into " +
32
42
  'the content body exactly as atrib-revise composes it today.'),
33
43
  });
34
44
  export const AttestInput = z.object({
@@ -76,20 +86,29 @@ export const AttestInput = z.object({
76
86
  tool_name: z
77
87
  .string()
78
88
  .min(1)
79
- .max(64)
80
89
  .optional()
81
- .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.'),
82
91
  args_hash: z
83
92
  .string()
84
93
  .regex(SHA256_REF_PATTERN)
85
94
  .optional()
86
95
  .describe('Optional §8.3 args_hash commitment override. When omitted, attest signs ' +
87
96
  'sha256(JCS(content)) per D099, identical to the legacy write names.'),
97
+ args_salt: z
98
+ .string()
99
+ .regex(/^[A-Za-z0-9_-]{22}$/)
100
+ .optional()
101
+ .describe('Optional base64url 16-byte salt paired with args_hash per §8.3.'),
88
102
  result_hash: z
89
103
  .string()
90
104
  .regex(SHA256_REF_PATTERN)
91
105
  .optional()
92
106
  .describe('Optional §8.3 result_hash commitment, unchanged from the legacy emit tool.'),
107
+ result_salt: z
108
+ .string()
109
+ .regex(/^[A-Za-z0-9_-]{22}$/)
110
+ .optional()
111
+ .describe('Optional base64url 16-byte salt paired with result_hash per §8.3.'),
93
112
  producer: z
94
113
  .string()
95
114
  .min(1)
@@ -122,7 +141,9 @@ export function mapAttestInput(input) {
122
141
  ...(input.provenance_token ? { provenance_token: input.provenance_token } : {}),
123
142
  ...(input.tool_name ? { tool_name: input.tool_name } : {}),
124
143
  ...(input.args_hash ? { args_hash: input.args_hash } : {}),
144
+ ...(input.args_salt ? { args_salt: input.args_salt } : {}),
125
145
  ...(input.result_hash ? { result_hash: input.result_hash } : {}),
146
+ ...(input.result_salt ? { result_salt: input.result_salt } : {}),
126
147
  };
127
148
  if (!input.ref) {
128
149
  const conflict = contentRefConflicts(input.content, undefined);
@@ -185,7 +206,7 @@ function contentRefConflicts(content, ref) {
185
206
  if (!ref) {
186
207
  if (contentAnnotates !== undefined || contentRevises !== undefined) {
187
208
  return ('content carries a relationship field (annotates/revises) but no ref was declared; ' +
188
- "declare ref: { kind, target } instead of embedding the relationship in content");
209
+ 'declare ref: { kind, target } instead of embedding the relationship in content');
189
210
  }
190
211
  return null;
191
212
  }
@@ -205,9 +226,7 @@ function contentRefConflicts(content, ref) {
205
226
  if (contentRevises !== undefined && contentRevises !== ref.target) {
206
227
  return `content.revises (${String(contentRevises)}) contradicts ref.target (${ref.target})`;
207
228
  }
208
- if (contentReason !== undefined &&
209
- ref.reason !== undefined &&
210
- contentReason !== ref.reason) {
229
+ if (contentReason !== undefined && ref.reason !== undefined && contentReason !== ref.reason) {
211
230
  return 'content.reason contradicts ref.reason';
212
231
  }
213
232
  return null;
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;
@@ -18,7 +19,9 @@ declare const EmitInput: z.ZodObject<{
18
19
  revises: z.ZodOptional<z.ZodString>;
19
20
  tool_name: z.ZodOptional<z.ZodString>;
20
21
  args_hash: z.ZodOptional<z.ZodString>;
22
+ args_salt: z.ZodOptional<z.ZodString>;
21
23
  result_hash: z.ZodOptional<z.ZodString>;
24
+ result_salt: z.ZodOptional<z.ZodString>;
22
25
  }, z.core.$strip>;
23
26
  type EmitSignedOutput = {
24
27
  signed: true;
@@ -183,6 +186,13 @@ interface HandleEmitInput {
183
186
  */
184
187
  producer?: string;
185
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;
186
196
  recordReferenceResolver?: RecordReferenceResolver | undefined;
187
197
  localSubstrate?: EmitLocalSubstrateShadowOptions | undefined;
188
198
  localSubstrateCommit?: EmitLocalSubstrateCommitOptions | undefined;
@@ -191,7 +201,7 @@ interface HandleEmitInput {
191
201
  * Build, sign, submit, mirror. Refused writes return `signed: false`;
192
202
  * signed degradations stay `signed: true` and surface in `warnings`.
193
203
  */
194
- 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>;
195
205
  export declare function resolveEmitLocalSubstrateShadowFromEnv(options?: ResolveEmitLocalSubstrateShadowFromEnvOptions): EmitLocalSubstrateShadowOptions | undefined;
196
206
  export declare function resolveEmitLocalSubstrateCommitFromEnv(options?: ResolveEmitLocalSubstrateCommitFromEnvOptions): EmitLocalSubstrateCommitOptions | undefined;
197
207
  export interface EmitInProcessOptions {
@@ -208,6 +218,17 @@ export interface EmitInProcessOptions {
208
218
  * consumers can bucket records by emitter without inspecting envelopes.
209
219
  */
210
220
  producer?: string;
221
+ /**
222
+ * Explicit mirror write target for this call. When omitted,
223
+ * ATRIB_MIRROR_FILE and the per-agent default remain unchanged fallbacks.
224
+ */
225
+ mirrorPath?: string | undefined;
226
+ /**
227
+ * Explicit mirror read source for chain inheritance. When omitted, an
228
+ * explicit mirrorPath is also the read source before the environment
229
+ * fallback ladder.
230
+ */
231
+ autochainSource?: string | undefined;
211
232
  /**
212
233
  * Upper bound on the post-sign queue flush, in milliseconds. Default
213
234
  * 5000ms. The submission queue itself has a 30s retry budget against an
@@ -275,6 +296,8 @@ export interface HandleAttestInput {
275
296
  /** Sidecar label; defaults to 'atrib-attest'. input.producer wins. */
276
297
  producer?: string;
277
298
  logEndpoint?: string | undefined;
299
+ mirrorPath?: string | undefined;
300
+ autochainSource?: string | undefined;
278
301
  recordReferenceResolver?: RecordReferenceResolver | undefined;
279
302
  localSubstrate?: EmitLocalSubstrateShadowOptions | undefined;
280
303
  localSubstrateCommit?: EmitLocalSubstrateCommitOptions | undefined;
@@ -299,6 +322,7 @@ export declare const __test_only__: {
299
322
  keyResolveRetryMs: typeof keyResolveRetryMs;
300
323
  };
301
324
  export { handleEmit, EmitInput };
325
+ export { resolveMirrorWritePath };
302
326
  export type { EmitOutput };
303
327
  export { resolveKey } from './keys.js';
304
328
  export type { ResolvedKey } from './keys.js';
@@ -308,5 +332,5 @@ export { AttestInput, AttestRef, mapAttestInput, isAttestMappingRefusal } from '
308
332
  export type { AttestInputT, MappedAttestInput, AttestMappingRefusal } from './attest.js';
309
333
  export { AnnotateInput, Importance, createAtribAnnotateServer, registerAnnotateTool, } from './annotate.js';
310
334
  export type { AtribAnnotateServer, CreateAtribAnnotateServerOptions } from './annotate.js';
311
- export { ReviseInput, createAtribReviseServer, registerReviseTool, } from './revise.js';
335
+ export { ReviseInput, createAtribReviseServer, registerReviseTool } from './revise.js';
312
336
  export type { AtribReviseServer, CreateAtribReviseServerOptions } from './revise.js';
package/dist/index.js CHANGED
@@ -17,14 +17,12 @@
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';
27
- import { AttestInput, isAttestMappingRefusal, mapAttestInput, } from './attest.js';
24
+ import { mirrorRecord, resolveMirrorWritePath } from './storage.js';
25
+ import { AttestInput, isAttestMappingRefusal, mapAttestInput } from './attest.js';
28
26
  import { registerAnnotateTool } from './annotate.js';
29
27
  import { registerReviseTool } from './revise.js';
30
28
  // Read-side mirror inheritance: ATRIB_AUTOCHAIN_SOURCE points at the file
@@ -33,14 +31,16 @@ 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.
43
42
  const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
43
+ const COMMITMENT_SALT_PATTERN = /^[A-Za-z0-9_-]{22}$/;
44
44
  const DEFAULT_KEY_RESOLVE_RETRY_MS = 30_000;
45
45
  const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
46
46
  function keyResolveRetryMs() {
@@ -126,10 +126,11 @@ const EmitInput = z.object({
126
126
  tool_name: z
127
127
  .string()
128
128
  .min(1)
129
- .max(64)
130
129
  .optional()
131
130
  .describe('Optional §8.2 tool_name disclosure. Verbatim or transformed name (verbatim, opaque, ' +
132
- '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 ' +
133
134
  'consumers (e.g. recall_my_attribution_history filtering by tool_name). Absence ' +
134
135
  'indicates the §8.1 default posture (no disclosure).'),
135
136
  args_hash: z
@@ -138,17 +139,24 @@ const EmitInput = z.object({
138
139
  .optional()
139
140
  .describe('Optional §8.3 args_hash commitment. Format: "sha256:" + 64 lowercase hex. Lets ' +
140
141
  'emit-signed records carry a commitment to canonical args bytes for downstream ' +
141
- 'consumers (e.g. recall filtering by args_hash, or replay detection). Salted vs ' +
142
- 'plain forms hash identically on the wire; the salt (when used) is carried in the ' +
143
- 'separate args_salt field, which this surface does not yet expose.'),
142
+ 'consumers (e.g. recall filtering by args_hash, or replay detection).'),
143
+ args_salt: z
144
+ .string()
145
+ .regex(COMMITMENT_SALT_PATTERN)
146
+ .optional()
147
+ .describe('Optional base64url 16-byte salt paired with args_hash per §8.3.'),
144
148
  result_hash: z
145
149
  .string()
146
150
  .regex(SHA256_REF_PATTERN)
147
151
  .optional()
148
152
  .describe('Optional §8.3 result_hash commitment. Format: "sha256:" + 64 lowercase hex. Lets ' +
149
153
  'emit-signed records carry a commitment to canonical result bytes for downstream ' +
150
- 'consumers. Salted vs plain forms hash identically on the wire; the salt (when used) ' +
151
- 'is carried in the separate result_salt field, which this surface does not yet expose.'),
154
+ 'consumers.'),
155
+ result_salt: z
156
+ .string()
157
+ .regex(COMMITMENT_SALT_PATTERN)
158
+ .optional()
159
+ .describe('Optional base64url 16-byte salt paired with result_hash per §8.3.'),
152
160
  });
153
161
  const DEFAULT_LOCAL_SUBSTRATE_DEGRADATION = {
154
162
  if_unavailable: 'sign locally in producer and continue without coordinator receipt',
@@ -212,7 +220,7 @@ export function registerEmitTool(mcp, deps) {
212
220
  */
213
221
  export function registerAttestTool(mcp, deps) {
214
222
  mcp.registerTool('attest', {
215
- description: 'Make a signed statement now: atrib\'s write verb. Signs an observation by default; ' +
223
+ description: "Make a signed statement now: atrib's write verb. Signs an observation by default; " +
216
224
  'declare ref: { kind: "annotates", target } to mark a past record\'s importance, or ' +
217
225
  'ref: { kind: "revises", target, reason } to supersede a prior position. One handler ' +
218
226
  'behind the legacy emit / atrib-annotate / atrib-revise names; records are ' +
@@ -331,7 +339,7 @@ function createServerKeyResolver(options) {
331
339
  * Build, sign, submit, mirror. Refused writes return `signed: false`;
332
340
  * signed degradations stay `signed: true` and surface in `warnings`.
333
341
  */
334
- 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, }) {
335
343
  const warnings = [];
336
344
  const eventType = normalizeEventType(input.event_type);
337
345
  if (!isValidEventTypeUri(eventType)) {
@@ -422,10 +430,14 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
422
430
  // is the primary case that needs preserving; pre-fix this produced isolated
423
431
  // genesis records because atrib-emit's local resolver short-circuited on
424
432
  // caller context.
433
+ const autochainMirrorPath = readMirrorPath({ autochainSource, mirrorPath });
425
434
  const chain = await inheritChainContext({
426
435
  callerContextId,
427
436
  callerChainRoot: input.chain_root,
428
- mirrorPath: readMirrorPath(),
437
+ mirrorPath: autochainMirrorPath,
438
+ ...((mirrorPath !== undefined || autochainSource !== undefined) && {
439
+ mirrorScope: 'file',
440
+ }),
429
441
  randomContextId,
430
442
  });
431
443
  const contextId = chain.contextId;
@@ -456,6 +468,9 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
456
468
  allowUnresolved: input.allow_unresolved_informed_by,
457
469
  resolver: recordReferenceResolver,
458
470
  logEndpoint,
471
+ ...((mirrorPath !== undefined || autochainSource !== undefined) && {
472
+ localMirrorPaths: [resolveMirrorWritePath(mirrorPath), autochainMirrorPath],
473
+ }),
459
474
  warnings,
460
475
  });
461
476
  const effectiveInformedBy = validParentHash
@@ -475,11 +490,15 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
475
490
  revises: input.revises,
476
491
  toolName: input.tool_name,
477
492
  argsHash: input.args_hash,
493
+ argsSalt: input.args_salt,
478
494
  resultHash: input.result_hash,
495
+ resultSalt: input.result_salt,
479
496
  });
480
497
  }
481
498
  catch (e) {
482
- return refusalOutput(contextId, [`signing failed: ${e instanceof Error ? e.message : String(e)}`]);
499
+ return refusalOutput(contextId, [
500
+ `signing failed: ${e instanceof Error ? e.message : String(e)}`,
501
+ ]);
483
502
  }
484
503
  const recordHash = hashRecord(record);
485
504
  const unsignedRecordBody = unsignedRecordBodyFromSigned(record);
@@ -527,7 +546,7 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
527
546
  await mirrorRecord(record, getProofFor(queue, recordHash) ?? null, {
528
547
  content: input.content,
529
548
  producer: producer ?? 'atrib-emit',
530
- });
549
+ }, mirrorPath);
531
550
  // Try to read a proof if the queue submitted synchronously and the log
532
551
  // returned one within the same tick. Most submissions return null here
533
552
  // and the proof shows up on a later poll via getProof.
@@ -905,6 +924,8 @@ export async function emitInProcess(rawInput, options = {}) {
905
924
  queue,
906
925
  producer: options.producer,
907
926
  logEndpoint,
927
+ mirrorPath: options.mirrorPath,
928
+ autochainSource: options.autochainSource,
908
929
  recordReferenceResolver: options.recordReferenceResolver,
909
930
  localSubstrate: resolveLocalSubstrateOption(options.localSubstrate, {
910
931
  producer: options.producer ?? 'atrib-emit',
@@ -967,6 +988,8 @@ export async function handleAttest(args) {
967
988
  queue: args.queue,
968
989
  producer: args.input.producer ?? args.producer ?? 'atrib-attest',
969
990
  logEndpoint: args.logEndpoint,
991
+ mirrorPath: args.mirrorPath,
992
+ autochainSource: args.autochainSource,
970
993
  recordReferenceResolver: args.recordReferenceResolver,
971
994
  localSubstrate: args.localSubstrate,
972
995
  localSubstrateCommit: args.localSubstrateCommit,
@@ -1013,6 +1036,7 @@ export const __test_only__ = { createServerKeyResolver, handleEmit, keyResolveRe
1013
1036
  //
1014
1037
  // Stable as of @atrib/emit@0.8.0. Breaking changes here require a major bump.
1015
1038
  export { handleEmit, EmitInput };
1039
+ export { resolveMirrorWritePath };
1016
1040
  export { resolveKey } from './keys.js';
1017
1041
  // Session-checkpoint emission (§1.2.10 / D139): reads the ordered record
1018
1042
  // hashes for a context from the local mirror per §5.9 and emits the
@@ -1025,4 +1049,4 @@ export { AttestInput, AttestRef, mapAttestInput, isAttestMappingRefusal } from '
1025
1049
  // Legacy specialized writers, kept as permanent aliases over the attest
1026
1050
  // funnel. The @atrib/annotate and @atrib/revise packages re-export these.
1027
1051
  export { AnnotateInput, Importance, createAtribAnnotateServer, registerAnnotateTool, } from './annotate.js';
1028
- export { ReviseInput, createAtribReviseServer, registerReviseTool, } from './revise.js';
1052
+ export { ReviseInput, createAtribReviseServer, registerReviseTool } from './revise.js';
@@ -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/sign.d.ts CHANGED
@@ -46,11 +46,15 @@ export interface BuildEmitRecordInput {
46
46
  * replay-checkable without exposing the body to the public log.
47
47
  */
48
48
  argsHash?: string | undefined;
49
+ /** Optional 16-byte base64url salt paired with argsHash. */
50
+ argsSalt?: string | undefined;
49
51
  /**
50
52
  * Optional §8.3 result_hash commitment. Same posture semantics and wire
51
53
  * format as args_hash, but commits to the tool result bytes.
52
54
  */
53
55
  resultHash?: string | undefined;
56
+ /** Optional 16-byte base64url salt paired with resultHash. */
57
+ resultSalt?: string | undefined;
54
58
  }
55
59
  /**
56
60
  * Build, sign, and return a complete AtribRecord ready for submission.
package/dist/sign.js CHANGED
@@ -23,6 +23,12 @@ const encoder = new TextEncoder();
23
23
  * Pure aside from the signing primitive itself; no network I/O here.
24
24
  */
25
25
  export async function buildAndSignEmitRecord(input) {
26
+ if (input.argsSalt && !input.argsHash) {
27
+ throw new TypeError('args_salt requires an explicit args_hash');
28
+ }
29
+ if (input.resultSalt && !input.resultHash) {
30
+ throw new TypeError('result_salt requires result_hash');
31
+ }
26
32
  const publicKey = base64urlEncode(await getPublicKey(input.privateKey));
27
33
  const toolName = leafOfEventTypeUri(input.eventType);
28
34
  const contentId = computeContentId(SYNTHETIC_SERVER_URL, toolName);
@@ -30,9 +36,7 @@ export async function buildAndSignEmitRecord(input) {
30
36
  // informed_by must be sorted lexicographically per §1.2.5 to keep the
31
37
  // canonical form stable across emitters. Omitted entirely (not null,
32
38
  // not empty) when no references are given, presence affects JCS.
33
- const informedBySorted = input.informedBy && input.informedBy.length > 0
34
- ? [...input.informedBy].sort()
35
- : undefined;
39
+ const informedBySorted = input.informedBy && input.informedBy.length > 0 ? [...input.informedBy].sort() : undefined;
36
40
  const record = {
37
41
  spec_version: 'atrib/1.0',
38
42
  content_id: contentId,
@@ -45,8 +49,10 @@ export async function buildAndSignEmitRecord(input) {
45
49
  ...(informedBySorted ? { informed_by: informedBySorted } : {}),
46
50
  ...(input.annotates ? { annotates: input.annotates } : {}),
47
51
  ...(argsHash ? { args_hash: argsHash } : {}),
52
+ ...(input.argsSalt ? { args_salt: input.argsSalt } : {}),
48
53
  ...(input.provenanceToken ? { provenance_token: input.provenanceToken } : {}),
49
54
  ...(input.resultHash ? { result_hash: input.resultHash } : {}),
55
+ ...(input.resultSalt ? { result_salt: input.resultSalt } : {}),
50
56
  ...(input.revises ? { revises: input.revises } : {}),
51
57
  ...(input.toolName ? { tool_name: input.toolName } : {}),
52
58
  };
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.1.0",
3
+ "version": "0.3.0",
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,13 +45,13 @@
45
45
  "@noble/hashes": "^2.2.0",
46
46
  "canonicalize": "^3.0.0",
47
47
  "zod": "^3.25.76",
48
- "@atrib/mcp": "0.21.0"
48
+ "@atrib/mcp": "0.23.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^25.9.3",
52
- "tsx": "^4.22.4",
51
+ "@types/node": "^26.1.1",
52
+ "tsx": "^4.23.1",
53
53
  "typescript": "^6.0.3",
54
- "vitest": "^4.1.8"
54
+ "vitest": "^4.1.10"
55
55
  },
56
56
  "files": [
57
57
  "dist"