@atrib/attest 0.0.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/LICENSE +216 -0
- package/README.md +187 -0
- package/dist/annotate.d.ts +56 -0
- package/dist/annotate.js +135 -0
- package/dist/attest.d.ts +69 -0
- package/dist/attest.js +214 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +729 -0
- package/dist/index.d.ts +312 -0
- package/dist/index.js +1028 -0
- package/dist/keys.d.ts +21 -0
- package/dist/keys.js +134 -0
- package/dist/local-substrate-host.d.ts +93 -0
- package/dist/local-substrate-host.js +467 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +18 -0
- package/dist/reference-resolution.d.ts +12 -0
- package/dist/reference-resolution.js +31 -0
- package/dist/revise.d.ts +43 -0
- package/dist/revise.js +133 -0
- package/dist/session-checkpoint.d.ts +103 -0
- package/dist/session-checkpoint.js +285 -0
- package/dist/sign.d.ts +69 -0
- package/dist/sign.js +74 -0
- package/dist/storage.d.ts +27 -0
- package/dist/storage.js +59 -0
- package/package.json +72 -0
package/dist/attest.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The `attest` tool: atrib's write verb. One handler signs observations,
|
|
3
|
+
// annotations, and revisions; the declared relationship moves from the
|
|
4
|
+
// tool-name axis to the `ref` argument. Every call delegates to handleEmit,
|
|
5
|
+
// so attest-signed records are byte-identical in canonical form to records
|
|
6
|
+
// signed through the legacy emit / atrib-annotate / atrib-revise names.
|
|
7
|
+
//
|
|
8
|
+
// Exact ref -> record mapping (exhaustive; anything else is a typed refusal
|
|
9
|
+
// and nothing is signed):
|
|
10
|
+
//
|
|
11
|
+
// ref absent -> observation (0x03), no relationship field
|
|
12
|
+
// { kind: 'annotates', target } -> annotation (0x05), signed annotates = target
|
|
13
|
+
// { kind: 'revises', target, reason } -> revision (0x06), signed revises = target
|
|
14
|
+
//
|
|
15
|
+
// The relationship target is also composed into the local content body
|
|
16
|
+
// (matching what atrib-annotate / atrib-revise compose today), so the
|
|
17
|
+
// D099 default args_hash commitment is byte-identical across both names.
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, SHA256_REF_PATTERN, } from '@atrib/mcp';
|
|
20
|
+
const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
|
|
21
|
+
// 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
|
|
22
|
+
const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
23
|
+
const OBSERVATION_URI = 'https://atrib.dev/v1/types/observation';
|
|
24
|
+
export const AttestRef = z.object({
|
|
25
|
+
kind: z.enum(['annotates', 'revises']).describe("Declared-relationship kind. 'annotates' marks a past record's importance " +
|
|
26
|
+
"and meaning (spec §1.2.7 / D058); 'revises' supersedes a prior position " +
|
|
27
|
+
'with a stated reason (spec §1.2.9 / D059). Omit `ref` entirely for a ' +
|
|
28
|
+
'plain observation.'),
|
|
29
|
+
target: z.string().regex(SHA256_REF_PATTERN).describe("'sha256:<64-hex>' record_hash the relationship points at. The target can " +
|
|
30
|
+
"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 " +
|
|
32
|
+
'the content body exactly as atrib-revise composes it today.'),
|
|
33
|
+
});
|
|
34
|
+
export const AttestInput = z.object({
|
|
35
|
+
content: z
|
|
36
|
+
.record(z.string(), z.unknown())
|
|
37
|
+
.describe('Semantic content of the cognitive event. For a plain observation: ' +
|
|
38
|
+
'{ what: string, why_noted?: string, topics?: string[] }. ' +
|
|
39
|
+
"For ref.kind='annotates': { importance: 'critical'|'high'|'medium'|'low'|'noise', summary: string, topics?: string[] }. " +
|
|
40
|
+
"For ref.kind='revises': { prior_position: string, new_position: string }. " +
|
|
41
|
+
'The relationship target (and reason, for revises) is composed in from `ref`; ' +
|
|
42
|
+
'do not duplicate it here unless it matches `ref` exactly.'),
|
|
43
|
+
ref: AttestRef.optional().describe('Optional declared relationship. Absent -> observation. ' +
|
|
44
|
+
"{ kind: 'annotates', target } -> annotation. " +
|
|
45
|
+
"{ kind: 'revises', target, reason } -> revision. " +
|
|
46
|
+
'The event_type vocabulary, required args, and graph effects (ANNOTATES / ' +
|
|
47
|
+
'REVISES edges per spec §3.2.4) are unchanged from the legacy tool names.'),
|
|
48
|
+
context_id: z
|
|
49
|
+
.string()
|
|
50
|
+
.regex(HEX_32_PATTERN)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('32-hex context_id. If omitted, resolution follows the same D078/D083 ladder ' +
|
|
53
|
+
'as the legacy write names; a fresh genesis context_id is the final fallback.'),
|
|
54
|
+
informed_by: z
|
|
55
|
+
.array(z.string().regex(SHA256_REF_PATTERN))
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Array of 'sha256:<64-hex>' record_hashes that informed this event. Composes " +
|
|
58
|
+
'freely with any `ref` and keeps D113 omit-unvalidated defaults. Sorted ' +
|
|
59
|
+
'lexicographically before signing per §1.2.5.'),
|
|
60
|
+
allow_unresolved_informed_by: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Set true only when the caller deliberately wants to sign dangling informed_by refs.'),
|
|
64
|
+
chain_root: z
|
|
65
|
+
.string()
|
|
66
|
+
.regex(SHA256_REF_PATTERN)
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Caller-managed chain_root of the immediately preceding record in this context_id. ' +
|
|
69
|
+
'Same semantics as the legacy emit tool; requires context_id.'),
|
|
70
|
+
provenance_token: z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(PROVENANCE_TOKEN_PATTERN)
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('22-char base64url cross-session causal anchor per spec §1.2.6 / D044. ' +
|
|
75
|
+
'Genesis-record-only; refused otherwise.'),
|
|
76
|
+
tool_name: z
|
|
77
|
+
.string()
|
|
78
|
+
.min(1)
|
|
79
|
+
.max(64)
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Optional §8.2 tool_name disclosure, unchanged from the legacy emit tool.'),
|
|
82
|
+
args_hash: z
|
|
83
|
+
.string()
|
|
84
|
+
.regex(SHA256_REF_PATTERN)
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Optional §8.3 args_hash commitment override. When omitted, attest signs ' +
|
|
87
|
+
'sha256(JCS(content)) per D099, identical to the legacy write names.'),
|
|
88
|
+
result_hash: z
|
|
89
|
+
.string()
|
|
90
|
+
.regex(SHA256_REF_PATTERN)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Optional §8.3 result_hash commitment, unchanged from the legacy emit tool.'),
|
|
93
|
+
producer: z
|
|
94
|
+
.string()
|
|
95
|
+
.min(1)
|
|
96
|
+
.max(128)
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Optional sidecar label override routed to `_local.producer`. Defaults to 'atrib-attest'. " +
|
|
99
|
+
'Sidecar metadata only; the signed record bytes are unchanged (§5.9.3).'),
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Map an AttestInput onto the legacy EmitInput shape. Pure; no signing.
|
|
103
|
+
* Returns refusals instead of throwing so the tool surface can return the
|
|
104
|
+
* same isError shape the legacy write names use (§5.8: refuse loudly,
|
|
105
|
+
* never sign a malformed record).
|
|
106
|
+
*
|
|
107
|
+
* Content composition matches the legacy specialized writers exactly so
|
|
108
|
+
* the D099 default args_hash is byte-identical:
|
|
109
|
+
* annotates: content := { annotates: target, ...content }
|
|
110
|
+
* revises: content := { revises: target, ...content, reason }
|
|
111
|
+
* A caller-supplied content.annotates / content.revises / content.reason
|
|
112
|
+
* that contradicts `ref` is a refusal, not a silent overwrite.
|
|
113
|
+
*/
|
|
114
|
+
export function mapAttestInput(input) {
|
|
115
|
+
const base = {
|
|
116
|
+
...(input.context_id ? { context_id: input.context_id } : {}),
|
|
117
|
+
...(input.informed_by ? { informed_by: input.informed_by } : {}),
|
|
118
|
+
...(input.allow_unresolved_informed_by !== undefined
|
|
119
|
+
? { allow_unresolved_informed_by: input.allow_unresolved_informed_by }
|
|
120
|
+
: {}),
|
|
121
|
+
...(input.chain_root ? { chain_root: input.chain_root } : {}),
|
|
122
|
+
...(input.provenance_token ? { provenance_token: input.provenance_token } : {}),
|
|
123
|
+
...(input.tool_name ? { tool_name: input.tool_name } : {}),
|
|
124
|
+
...(input.args_hash ? { args_hash: input.args_hash } : {}),
|
|
125
|
+
...(input.result_hash ? { result_hash: input.result_hash } : {}),
|
|
126
|
+
};
|
|
127
|
+
if (!input.ref) {
|
|
128
|
+
const conflict = contentRefConflicts(input.content, undefined);
|
|
129
|
+
if (conflict)
|
|
130
|
+
return { refusals: [conflict] };
|
|
131
|
+
return {
|
|
132
|
+
event_type: OBSERVATION_URI,
|
|
133
|
+
emitInput: { event_type: OBSERVATION_URI, content: input.content, ...base },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (input.ref.kind === 'annotates') {
|
|
137
|
+
const conflict = contentRefConflicts(input.content, input.ref);
|
|
138
|
+
if (conflict)
|
|
139
|
+
return { refusals: [conflict] };
|
|
140
|
+
const content = { annotates: input.ref.target, ...input.content };
|
|
141
|
+
return {
|
|
142
|
+
event_type: EVENT_TYPE_ANNOTATION_URI,
|
|
143
|
+
emitInput: {
|
|
144
|
+
event_type: EVENT_TYPE_ANNOTATION_URI,
|
|
145
|
+
content,
|
|
146
|
+
annotates: input.ref.target,
|
|
147
|
+
...base,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// input.ref.kind === 'revises'
|
|
152
|
+
if (!input.ref.reason) {
|
|
153
|
+
return {
|
|
154
|
+
refusals: [
|
|
155
|
+
"ref.reason is required when ref.kind is 'revises' (the reason lives in content, as atrib-revise composes it today)",
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const conflict = contentRefConflicts(input.content, input.ref);
|
|
160
|
+
if (conflict)
|
|
161
|
+
return { refusals: [conflict] };
|
|
162
|
+
const content = { revises: input.ref.target, ...input.content, reason: input.ref.reason };
|
|
163
|
+
return {
|
|
164
|
+
event_type: EVENT_TYPE_REVISION_URI,
|
|
165
|
+
emitInput: {
|
|
166
|
+
event_type: EVENT_TYPE_REVISION_URI,
|
|
167
|
+
content,
|
|
168
|
+
revises: input.ref.target,
|
|
169
|
+
...base,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function isAttestMappingRefusal(mapped) {
|
|
174
|
+
return 'refusals' in mapped;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Detect contradictions between caller-supplied content fields and the
|
|
178
|
+
* declared `ref`. Duplicates that MATCH the ref are tolerated (the caller
|
|
179
|
+
* pre-composed the legacy content shape); contradictions refuse.
|
|
180
|
+
*/
|
|
181
|
+
function contentRefConflicts(content, ref) {
|
|
182
|
+
const contentAnnotates = content['annotates'];
|
|
183
|
+
const contentRevises = content['revises'];
|
|
184
|
+
const contentReason = content['reason'];
|
|
185
|
+
if (!ref) {
|
|
186
|
+
if (contentAnnotates !== undefined || contentRevises !== undefined) {
|
|
187
|
+
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");
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (ref.kind === 'annotates') {
|
|
193
|
+
if (contentRevises !== undefined) {
|
|
194
|
+
return "content.revises is FORBIDDEN when ref.kind is 'annotates'";
|
|
195
|
+
}
|
|
196
|
+
if (contentAnnotates !== undefined && contentAnnotates !== ref.target) {
|
|
197
|
+
return `content.annotates (${String(contentAnnotates)}) contradicts ref.target (${ref.target})`;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// revises
|
|
202
|
+
if (contentAnnotates !== undefined) {
|
|
203
|
+
return "content.annotates is FORBIDDEN when ref.kind is 'revises'";
|
|
204
|
+
}
|
|
205
|
+
if (contentRevises !== undefined && contentRevises !== ref.target) {
|
|
206
|
+
return `content.revises (${String(contentRevises)}) contradicts ref.target (${ref.target})`;
|
|
207
|
+
}
|
|
208
|
+
if (contentReason !== undefined &&
|
|
209
|
+
ref.reason !== undefined &&
|
|
210
|
+
contentReason !== ref.reason) {
|
|
211
|
+
return 'content.reason contradicts ref.reason';
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
package/dist/cli.d.ts
ADDED