@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 +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +27 -13
- package/package.json +2 -2
- package/dist/auto-chain.d.ts +0 -61
- package/dist/auto-chain.js +0 -135
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 {
|
|
16
|
-
import {
|
|
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
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
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
|
-
|
|
179
|
+
mirrorPath: readMirrorPath(),
|
|
166
180
|
randomContextId,
|
|
167
181
|
});
|
|
168
182
|
const contextId = chain.contextId;
|
|
169
183
|
const chainRoot = chain.chainRoot;
|
|
170
|
-
if (chain.inheritedFrom === '
|
|
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.
|
|
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.
|
|
16
|
+
"@atrib/mcp": "0.6.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^22.19.17",
|
package/dist/auto-chain.d.ts
DELETED
|
@@ -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 {};
|
package/dist/auto-chain.js
DELETED
|
@@ -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 };
|