@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 +23 -10
- package/dist/attest.d.ts +4 -0
- package/dist/attest.js +29 -10
- package/dist/index.d.ts +26 -2
- package/dist/index.js +45 -21
- package/dist/reference-resolution.d.ts +1 -0
- package/dist/reference-resolution.js +3 -1
- package/dist/sign.d.ts +4 -0
- package/dist/sign.js +9 -3
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +5 -4
- package/package.json +5 -5
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
|
|
65
|
-
|
|
|
66
|
-
| absent
|
|
67
|
-
| `{ kind: "annotates", target, ... }`
|
|
68
|
-
| `{ kind: "revises", target, reason, ... }` (`reason` required) | revision
|
|
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,
|
|
114
|
-
createAtribEmitServer,
|
|
115
|
-
createAtribAnnotateServer,
|
|
116
|
-
createAtribReviseServer,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
38
|
-
|
|
39
|
-
|
|
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).
|
|
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).
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
151
|
-
|
|
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:
|
|
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:
|
|
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, [
|
|
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
|
|
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
|
|
34
|
-
return (
|
|
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 =
|
|
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.
|
|
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.
|
|
48
|
+
"@atrib/mcp": "0.23.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"tsx": "^4.
|
|
51
|
+
"@types/node": "^26.1.1",
|
|
52
|
+
"tsx": "^4.23.1",
|
|
53
53
|
"typescript": "^6.0.3",
|
|
54
|
-
"vitest": "^4.1.
|
|
54
|
+
"vitest": "^4.1.10"
|
|
55
55
|
},
|
|
56
56
|
"files": [
|
|
57
57
|
"dist"
|