@atrib/emit 0.4.1 → 0.4.3

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
@@ -112,7 +112,7 @@ Three files do the work:
112
112
  - `src/submit.ts` — wraps `@atrib/mcp`'s `createSubmissionQueue`. Same priority semantics as the wrapper (cognitive events use 'normal' priority).
113
113
  - `src/storage.ts` — Best-effort JSONL mirror of full record + proof, for local recall.
114
114
 
115
- Per §5.8 degradation contract: nothing in `atrib-emit` throws to the agent. Missing key → warning in the response. Sign failure → warning. Network failure → submission queued for retry.
115
+ Per [§5.8](../../atrib-spec.md#58-degradation-contract) degradation contract: nothing in `atrib-emit` throws to the agent. Missing key → warning in the response. Sign failure → warning. Network failure → submission queued for retry.
116
116
 
117
117
  ## autoChain inheritance from the wrapper
118
118
 
package/dist/index.d.ts CHANGED
@@ -14,21 +14,21 @@ declare const EmitInput: z.ZodObject<{
14
14
  }, "strip", z.ZodTypeAny, {
15
15
  event_type: string;
16
16
  content: Record<string, unknown>;
17
- chain_root?: string | undefined;
18
- context_id?: string | undefined;
19
- annotates?: string | undefined;
20
- revises?: string | undefined;
21
17
  informed_by?: string[] | undefined;
18
+ annotates?: string | undefined;
22
19
  provenance_token?: string | undefined;
20
+ revises?: string | undefined;
21
+ context_id?: string | undefined;
22
+ chain_root?: string | undefined;
23
23
  }, {
24
24
  event_type: string;
25
25
  content: Record<string, unknown>;
26
- chain_root?: string | undefined;
27
- context_id?: string | undefined;
28
- annotates?: string | undefined;
29
- revises?: string | undefined;
30
26
  informed_by?: string[] | undefined;
27
+ annotates?: string | undefined;
31
28
  provenance_token?: string | undefined;
29
+ revises?: string | undefined;
30
+ context_id?: string | undefined;
31
+ chain_root?: string | undefined;
32
32
  }>;
33
33
  type EmitOutput = {
34
34
  record_hash: string;
package/dist/index.js CHANGED
@@ -12,11 +12,23 @@
12
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
13
  import { z } from 'zod';
14
14
  import { randomBytes } from 'node:crypto';
15
- import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, canonicalRecord, createSubmissionQueue, genesisChainRoot, hexEncode, isValidEventTypeUri, sha256, } from '@atrib/mcp';
16
- import { resolveChainContext } from './auto-chain.js';
15
+ import { homedir } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, canonicalRecord, createSubmissionQueue, genesisChainRoot, hexEncode, inheritChainContext, isValidEventTypeUri, sha256, } from '@atrib/mcp';
17
18
  import { resolveKey } from './keys.js';
18
19
  import { buildAndSignEmitRecord } from './sign.js';
19
20
  import { mirrorRecord } from './storage.js';
21
+ // Read-side mirror inheritance: ATRIB_AUTOCHAIN_SOURCE points at the file
22
+ // atrib-emit reads to inherit cross-producer chain state (typically the
23
+ // wrapper's mirror). Falls back to ATRIB_MIRROR_FILE (where emit writes —
24
+ // rarely useful as a read source, but kept for backward compatibility),
25
+ // then to the per-agent default. Distinct from ATRIB_MIRROR_FILE which is
26
+ // where emit's own records are persisted.
27
+ function readMirrorPath() {
28
+ return (process.env['ATRIB_AUTOCHAIN_SOURCE'] ??
29
+ process.env['ATRIB_MIRROR_FILE'] ??
30
+ join(homedir(), '.atrib', 'records', `${process.env['ATRIB_AGENT'] ?? 'claude-code'}.jsonl`));
31
+ }
20
32
  const SHA256_REF_PATTERN = /^sha256:[0-9a-f]{64}$/;
21
33
  const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
22
34
  // 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
@@ -151,23 +163,25 @@ async function handleEmit({ input, key, queue }) {
151
163
  `received event_type=${input.event_type}`,
152
164
  ]);
153
165
  }
154
- // autoChain inheritance: when the caller omits context_id, read the
155
- // wrapper's local mirror and inherit its most-recent record's context_id
156
- // (chaining on top of that record's hash). Falls back to a fresh genesis
157
- // when no mirror is present. The inheritance source is surfaced to the
158
- // caller in the warnings array so the agent knows which session this
159
- // emit landed in. When the caller supplies BOTH context_id and chain_root,
160
- // resolveChainContext uses them verbatim the path needed by consumers
161
- // that thread chain state themselves.
162
- const chain = await resolveChainContext({
166
+ // Multi-producer chain composition per spec §1.2.3 / D067. Single source
167
+ // of truth in @atrib/mcp's inheritChainContext: caller-supplied verbatim
168
+ // when both fields supplied, else cascade through env-tail (cross-producer
169
+ // handoff) and mirror-file inheritance (filtered to the same context_id),
170
+ // falling back to genesis. When caller omits context_id entirely, the
171
+ // helper inherits BOTH context_id and chain_root from the mirror's most
172
+ // recent record. The cognitive-extractor hook spawning atrib-emit with
173
+ // ATRIB_CHAIN_TAIL_<context_id> + the agent's context_id is the primary
174
+ // load-bearing case; pre-fix this produced isolated genesis records
175
+ // because atrib-emit's local resolver short-circuited on caller context.
176
+ const chain = await inheritChainContext({
163
177
  callerContextId: input.context_id,
164
178
  callerChainRoot: input.chain_root,
165
- genesisChainRoot,
179
+ mirrorPath: readMirrorPath(),
166
180
  randomContextId,
167
181
  });
168
182
  const contextId = chain.contextId;
169
183
  const chainRoot = chain.chainRoot;
170
- if (chain.inheritedFrom === 'wrapper-mirror') {
184
+ if (chain.inheritedFrom === 'mirror-context-and-tail') {
171
185
  warnings.push(`inherited context_id from wrapper mirror: ${contextId}`);
172
186
  }
173
187
  let record;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atrib/emit",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "MCP server for atrib. The producer-side cognitive primitive: lets agents sign explicit observations, annotations, and revisions beyond what middleware auto-signs.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "@noble/ed25519": "^2.3.0",
14
14
  "@noble/hashes": "^1.8.0",
15
15
  "zod": "^3.25.76",
16
- "@atrib/mcp": "0.4.0"
16
+ "@atrib/mcp": "0.6.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^22.19.17",
@@ -1,61 +0,0 @@
1
- import { type AtribRecord } from '@atrib/mcp';
2
- export interface ChainContext {
3
- contextId: string;
4
- chainRoot: string;
5
- inheritedFrom: 'caller-supplied' | 'wrapper-mirror' | 'fresh';
6
- }
7
- /**
8
- * Decide what context_id + chain_root the next emit record should use.
9
- *
10
- * When the caller supplies both context_id and chain_root, atrib-emit
11
- * uses them verbatim (inheritedFrom: 'caller-supplied'). This path is
12
- * used by consumers that manage chain state themselves, such as nightly
13
- * observation pipelines emitting a sequence of records under one
14
- * context_id with explicit chain_root threading.
15
- *
16
- * When the caller supplies only context_id, atrib-emit generates a
17
- * genesis record for that context_id (chain_root = genesisChainRoot(
18
- * context_id), inheritedFrom: 'fresh').
19
- *
20
- * When the caller supplies neither, atrib-emit inherits both fields
21
- * from the most recent record in the wrapper's mirror file. If no
22
- * mirror is available, it falls back to a fresh genesis context_id.
23
- *
24
- * Caller passes a chainRootForCallerContext callback that knows how to
25
- * compute the genesis chain_root for a given context_id (we accept it as
26
- * a parameter rather than depending on @atrib/mcp's genesisChainRoot
27
- * directly, so this module stays trivially testable without pulling in
28
- * the rest of the signing surface).
29
- */
30
- export declare function resolveChainContext(opts: {
31
- callerContextId?: string | undefined;
32
- /**
33
- * Caller-managed chain_root. Only honored when callerContextId is also
34
- * supplied; chain_root without a context_id is meaningless and is treated
35
- * as undefined here (the index.ts handler validates this case earlier and
36
- * returns a warnings-only response).
37
- */
38
- callerChainRoot?: string | undefined;
39
- /** Path override. Defaults to ATRIB_MIRROR_FILE env, then the wrapper's default. */
40
- mirrorPath?: string | undefined;
41
- /** Function returning genesis chain_root for a given context_id (spec §1.2.3). */
42
- genesisChainRoot: (contextId: string) => string;
43
- /** Random context_id generator (16 bytes hex). Injected for determinism in tests. */
44
- randomContextId: () => string;
45
- }): Promise<ChainContext>;
46
- /**
47
- * Read the JSONL mirror's last line and parse it as an AtribRecord.
48
- * Returns null on any failure (missing file, empty file, malformed JSON,
49
- * line missing required fields). Per §5.8 degradation: never throws.
50
- *
51
- * Implementation note: we read the whole file rather than seeking to the
52
- * end. Mirror files are bounded (one entry per tool call within a session
53
- * lifetime, single-digit MB at worst). If volume grows enough that this
54
- * matters, switch to a tail read. Until then, simplicity wins.
55
- */
56
- declare function readMostRecentRecord(path: string): Promise<AtribRecord | null>;
57
- export declare const __test_only__: {
58
- readMostRecentRecord: typeof readMostRecentRecord;
59
- DEFAULT_MIRROR: string;
60
- };
61
- export {};
@@ -1,135 +0,0 @@
1
- // autoChain inheritance from the wrapper's local JSONL mirror.
2
- //
3
- // The wrapper service persists every signed record to a JSONL file under
4
- // ~/.atrib/records/. Each line is a bare AtribRecord, newest at EOF. When
5
- // atrib-emit runs in the same agent process, it can inherit the wrapper's
6
- // active context_id by reading the most-recent line and chain its emit on
7
- // top of that record (chain_root = sha256:<that record's hash>).
8
- //
9
- // This is the cognitive-feedback-loop convention: explicit observations
10
- // chain seamlessly with the agent's mechanical tool calls in the same
11
- // session, so the verifier sees one coherent chain per context_id.
12
- //
13
- // Per the scope doc design-question #2: same file as wrapper. Default path
14
- // is the wrapper's default; override with ATRIB_MIRROR_FILE.
15
- //
16
- // Failure mode: never throws. Missing file → no inheritance → genesis
17
- // record. Malformed last line → no inheritance → genesis record. The
18
- // wrapper's autoChain across restarts uses the same file with the same
19
- // silent-degradation contract.
20
- import { readFile, stat } from 'node:fs/promises';
21
- import { homedir } from 'node:os';
22
- import { join } from 'node:path';
23
- import { canonicalRecord, hexEncode, sha256 } from '@atrib/mcp';
24
- // Default path is parameterized by ATRIB_AGENT so each agent gets its own
25
- // mirror file under ~/.atrib/records/. Wrappers that follow the same
26
- // convention will write to the same file and atrib-emit's autoChain picks
27
- // up inheritance for free. Wrappers that use a different filename should
28
- // have the operator set ATRIB_AUTOCHAIN_SOURCE explicitly.
29
- const DEFAULT_MIRROR = join(homedir(), '.atrib', 'records', `${process.env.ATRIB_AGENT ?? 'claude-code'}.jsonl`);
30
- /**
31
- * Decide what context_id + chain_root the next emit record should use.
32
- *
33
- * When the caller supplies both context_id and chain_root, atrib-emit
34
- * uses them verbatim (inheritedFrom: 'caller-supplied'). This path is
35
- * used by consumers that manage chain state themselves, such as nightly
36
- * observation pipelines emitting a sequence of records under one
37
- * context_id with explicit chain_root threading.
38
- *
39
- * When the caller supplies only context_id, atrib-emit generates a
40
- * genesis record for that context_id (chain_root = genesisChainRoot(
41
- * context_id), inheritedFrom: 'fresh').
42
- *
43
- * When the caller supplies neither, atrib-emit inherits both fields
44
- * from the most recent record in the wrapper's mirror file. If no
45
- * mirror is available, it falls back to a fresh genesis context_id.
46
- *
47
- * Caller passes a chainRootForCallerContext callback that knows how to
48
- * compute the genesis chain_root for a given context_id (we accept it as
49
- * a parameter rather than depending on @atrib/mcp's genesisChainRoot
50
- * directly, so this module stays trivially testable without pulling in
51
- * the rest of the signing surface).
52
- */
53
- export async function resolveChainContext(opts) {
54
- if (opts.callerContextId) {
55
- if (opts.callerChainRoot) {
56
- return {
57
- contextId: opts.callerContextId,
58
- chainRoot: opts.callerChainRoot,
59
- inheritedFrom: 'caller-supplied',
60
- };
61
- }
62
- return {
63
- contextId: opts.callerContextId,
64
- chainRoot: opts.genesisChainRoot(opts.callerContextId),
65
- inheritedFrom: 'fresh',
66
- };
67
- }
68
- // Reads from ATRIB_AUTOCHAIN_SOURCE first, falling back to the wrapper's
69
- // mirror path (NOT emit's own mirror — they serve different concerns).
70
- // ATRIB_MIRROR_FILE controls where emit writes; ATRIB_AUTOCHAIN_SOURCE
71
- // controls what emit reads to inherit context. In a typical setup they
72
- // point at different files: emit writes its own mirror, but inherits the
73
- // wrapper's session context.
74
- const path = opts.mirrorPath ??
75
- process.env['ATRIB_AUTOCHAIN_SOURCE'] ??
76
- process.env['ATRIB_MIRROR_FILE'] ??
77
- DEFAULT_MIRROR;
78
- const inherited = await readMostRecentRecord(path);
79
- if (inherited) {
80
- const recordHashHex = hexEncode(sha256(canonicalRecord(inherited)));
81
- return {
82
- contextId: inherited.context_id,
83
- chainRoot: `sha256:${recordHashHex}`,
84
- inheritedFrom: 'wrapper-mirror',
85
- };
86
- }
87
- const fresh = opts.randomContextId();
88
- return {
89
- contextId: fresh,
90
- chainRoot: opts.genesisChainRoot(fresh),
91
- inheritedFrom: 'fresh',
92
- };
93
- }
94
- /**
95
- * Read the JSONL mirror's last line and parse it as an AtribRecord.
96
- * Returns null on any failure (missing file, empty file, malformed JSON,
97
- * line missing required fields). Per §5.8 degradation: never throws.
98
- *
99
- * Implementation note: we read the whole file rather than seeking to the
100
- * end. Mirror files are bounded (one entry per tool call within a session
101
- * lifetime, single-digit MB at worst). If volume grows enough that this
102
- * matters, switch to a tail read. Until then, simplicity wins.
103
- */
104
- async function readMostRecentRecord(path) {
105
- try {
106
- const stats = await stat(path).catch(() => null);
107
- if (!stats || stats.size === 0)
108
- return null;
109
- const contents = await readFile(path, 'utf-8');
110
- const lines = contents.split('\n').filter((l) => l.trim().length > 0);
111
- if (lines.length === 0)
112
- return null;
113
- const last = lines[lines.length - 1];
114
- // Accept BOTH conventions:
115
- // (a) bare AtribRecord — the wrapper service's mirror writes one
116
- // record per line.
117
- // (b) envelope { record, proof?, written_at? } — atrib-emit's own
118
- // mirror writes this shape so it can preserve proof + timestamp
119
- // metadata for local recall. autoChain inheritance only needs the
120
- // record itself.
121
- // Each line could come from either producer in a session that uses both.
122
- const parsed = JSON.parse(last);
123
- const candidate = 'record' in parsed && parsed.record ? parsed.record : parsed;
124
- if (typeof candidate.context_id !== 'string' ||
125
- typeof candidate.creator_key !== 'string' ||
126
- typeof candidate.signature !== 'string') {
127
- return null;
128
- }
129
- return candidate;
130
- }
131
- catch {
132
- return null;
133
- }
134
- }
135
- export const __test_only__ = { readMostRecentRecord, DEFAULT_MIRROR };