@atrib/attest 0.1.0 → 0.2.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 +17 -10
- package/dist/attest.d.ts +4 -0
- package/dist/attest.js +28 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +21 -9
- package/dist/sign.d.ts +4 -0
- package/dist/sign.js +9 -3
- 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`,
|
|
@@ -160,6 +162,11 @@ verb that contract is the most important thing to know:
|
|
|
160
162
|
carries only the replay-checkable `args_hash` commitment over it. The
|
|
161
163
|
public log stores the 90-byte commitment entry (record hash, creator key,
|
|
162
164
|
context_id, timestamp, event_type byte), never the content.
|
|
165
|
+
- **Salted direct commitments.** `args_salt` and `result_salt` let direct
|
|
166
|
+
attest callers carry the same salted commitment posture as middleware.
|
|
167
|
+
Each salt must be canonical base64url for exactly 16 bytes and requires its
|
|
168
|
+
matching explicit hash. The shared `@atrib/mcp` commitment helper computes
|
|
169
|
+
and verifies the bytes used by both paths.
|
|
163
170
|
- **Refused writes are loud, not silent.** A malformed call (unknown
|
|
164
171
|
`ref.kind`, missing revises reason, a `ref`/content contradiction, a
|
|
165
172
|
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({
|
|
@@ -85,11 +95,21 @@ export const AttestInput = z.object({
|
|
|
85
95
|
.optional()
|
|
86
96
|
.describe('Optional §8.3 args_hash commitment override. When omitted, attest signs ' +
|
|
87
97
|
'sha256(JCS(content)) per D099, identical to the legacy write names.'),
|
|
98
|
+
args_salt: z
|
|
99
|
+
.string()
|
|
100
|
+
.regex(/^[A-Za-z0-9_-]{22}$/)
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Optional base64url 16-byte salt paired with args_hash per §8.3.'),
|
|
88
103
|
result_hash: z
|
|
89
104
|
.string()
|
|
90
105
|
.regex(SHA256_REF_PATTERN)
|
|
91
106
|
.optional()
|
|
92
107
|
.describe('Optional §8.3 result_hash commitment, unchanged from the legacy emit tool.'),
|
|
108
|
+
result_salt: z
|
|
109
|
+
.string()
|
|
110
|
+
.regex(/^[A-Za-z0-9_-]{22}$/)
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('Optional base64url 16-byte salt paired with result_hash per §8.3.'),
|
|
93
113
|
producer: z
|
|
94
114
|
.string()
|
|
95
115
|
.min(1)
|
|
@@ -122,7 +142,9 @@ export function mapAttestInput(input) {
|
|
|
122
142
|
...(input.provenance_token ? { provenance_token: input.provenance_token } : {}),
|
|
123
143
|
...(input.tool_name ? { tool_name: input.tool_name } : {}),
|
|
124
144
|
...(input.args_hash ? { args_hash: input.args_hash } : {}),
|
|
145
|
+
...(input.args_salt ? { args_salt: input.args_salt } : {}),
|
|
125
146
|
...(input.result_hash ? { result_hash: input.result_hash } : {}),
|
|
147
|
+
...(input.result_salt ? { result_salt: input.result_salt } : {}),
|
|
126
148
|
};
|
|
127
149
|
if (!input.ref) {
|
|
128
150
|
const conflict = contentRefConflicts(input.content, undefined);
|
|
@@ -185,7 +207,7 @@ function contentRefConflicts(content, ref) {
|
|
|
185
207
|
if (!ref) {
|
|
186
208
|
if (contentAnnotates !== undefined || contentRevises !== undefined) {
|
|
187
209
|
return ('content carries a relationship field (annotates/revises) but no ref was declared; ' +
|
|
188
|
-
|
|
210
|
+
'declare ref: { kind, target } instead of embedding the relationship in content');
|
|
189
211
|
}
|
|
190
212
|
return null;
|
|
191
213
|
}
|
|
@@ -205,9 +227,7 @@ function contentRefConflicts(content, ref) {
|
|
|
205
227
|
if (contentRevises !== undefined && contentRevises !== ref.target) {
|
|
206
228
|
return `content.revises (${String(contentRevises)}) contradicts ref.target (${ref.target})`;
|
|
207
229
|
}
|
|
208
|
-
if (contentReason !== undefined &&
|
|
209
|
-
ref.reason !== undefined &&
|
|
210
|
-
contentReason !== ref.reason) {
|
|
230
|
+
if (contentReason !== undefined && ref.reason !== undefined && contentReason !== ref.reason) {
|
|
211
231
|
return 'content.reason contradicts ref.reason';
|
|
212
232
|
}
|
|
213
233
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,9 @@ declare const EmitInput: z.ZodObject<{
|
|
|
18
18
|
revises: z.ZodOptional<z.ZodString>;
|
|
19
19
|
tool_name: z.ZodOptional<z.ZodString>;
|
|
20
20
|
args_hash: z.ZodOptional<z.ZodString>;
|
|
21
|
+
args_salt: z.ZodOptional<z.ZodString>;
|
|
21
22
|
result_hash: z.ZodOptional<z.ZodString>;
|
|
23
|
+
result_salt: z.ZodOptional<z.ZodString>;
|
|
22
24
|
}, z.core.$strip>;
|
|
23
25
|
type EmitSignedOutput = {
|
|
24
26
|
signed: true;
|
|
@@ -308,5 +310,5 @@ export { AttestInput, AttestRef, mapAttestInput, isAttestMappingRefusal } from '
|
|
|
308
310
|
export type { AttestInputT, MappedAttestInput, AttestMappingRefusal } from './attest.js';
|
|
309
311
|
export { AnnotateInput, Importance, createAtribAnnotateServer, registerAnnotateTool, } from './annotate.js';
|
|
310
312
|
export type { AtribAnnotateServer, CreateAtribAnnotateServerOptions } from './annotate.js';
|
|
311
|
-
export { ReviseInput, createAtribReviseServer, registerReviseTool
|
|
313
|
+
export { ReviseInput, createAtribReviseServer, registerReviseTool } from './revise.js';
|
|
312
314
|
export type { AtribReviseServer, CreateAtribReviseServerOptions } from './revise.js';
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { resolveKey } from './keys.js';
|
|
|
24
24
|
import { filterResolvableInformedBy } from './reference-resolution.js';
|
|
25
25
|
import { buildAndSignEmitRecord } from './sign.js';
|
|
26
26
|
import { mirrorRecord } from './storage.js';
|
|
27
|
-
import { AttestInput, isAttestMappingRefusal, mapAttestInput
|
|
27
|
+
import { AttestInput, isAttestMappingRefusal, mapAttestInput } from './attest.js';
|
|
28
28
|
import { registerAnnotateTool } from './annotate.js';
|
|
29
29
|
import { registerReviseTool } from './revise.js';
|
|
30
30
|
// Read-side mirror inheritance: ATRIB_AUTOCHAIN_SOURCE points at the file
|
|
@@ -41,6 +41,7 @@ function readMirrorPath() {
|
|
|
41
41
|
const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
|
|
42
42
|
// 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
|
|
43
43
|
const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
44
|
+
const COMMITMENT_SALT_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
44
45
|
const DEFAULT_KEY_RESOLVE_RETRY_MS = 30_000;
|
|
45
46
|
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
46
47
|
function keyResolveRetryMs() {
|
|
@@ -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 ' +
|
|
@@ -475,11 +483,15 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
|
|
|
475
483
|
revises: input.revises,
|
|
476
484
|
toolName: input.tool_name,
|
|
477
485
|
argsHash: input.args_hash,
|
|
486
|
+
argsSalt: input.args_salt,
|
|
478
487
|
resultHash: input.result_hash,
|
|
488
|
+
resultSalt: input.result_salt,
|
|
479
489
|
});
|
|
480
490
|
}
|
|
481
491
|
catch (e) {
|
|
482
|
-
return refusalOutput(contextId, [
|
|
492
|
+
return refusalOutput(contextId, [
|
|
493
|
+
`signing failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
494
|
+
]);
|
|
483
495
|
}
|
|
484
496
|
const recordHash = hashRecord(record);
|
|
485
497
|
const unsignedRecordBody = unsignedRecordBodyFromSigned(record);
|
|
@@ -1025,4 +1037,4 @@ export { AttestInput, AttestRef, mapAttestInput, isAttestMappingRefusal } from '
|
|
|
1025
1037
|
// Legacy specialized writers, kept as permanent aliases over the attest
|
|
1026
1038
|
// funnel. The @atrib/annotate and @atrib/revise packages re-export these.
|
|
1027
1039
|
export { AnnotateInput, Importance, createAtribAnnotateServer, registerAnnotateTool, } from './annotate.js';
|
|
1028
|
-
export { ReviseInput, createAtribReviseServer, registerReviseTool
|
|
1040
|
+
export { ReviseInput, createAtribReviseServer, registerReviseTool } from './revise.js';
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atrib/attest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.22.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"
|