@remembrance-ai/pi-plugin 0.0.0-stage → 0.2.25
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 +201 -0
- package/README.md +35 -2
- package/package.json +47 -4
- package/scripts/feedback-disposition.mjs +376 -0
- package/scripts/hook-core.mjs +14654 -0
- package/scripts/pending-action-payload.mjs +1045 -0
- package/scripts/private-egress-lanes.mjs +16 -0
- package/scripts/private-learning-hook-policy.mjs +2725 -0
- package/scripts/private-lesson-approval.mjs +200 -0
- package/servers/remembrance-mcp.mjs +76192 -0
- package/skills/remembrancer/SKILL.md +865 -0
- package/skills/remembrancer/references/attestation-rest.md +551 -0
- package/skills/remembrancer/references/identity-preferences-topology.md +408 -0
- package/skills/remembrancer/references/mpp.md +104 -0
- package/skills/remembrancer/references/remembrance-payloads.md +406 -0
- package/skills/remembrancer/references/remembrance-setup.md +2405 -0
- package/skills/remembrancer/references/resource-scout.md +61 -0
- package/skills/remembrancer/references/web-ui-ux-qa.md +55 -0
- package/skills/remembrancer/scripts/queue-private-skill-import.mjs +281 -0
- package/skills/remembrancer/scripts/validate-remembrance.mjs +82 -0
- package/skills/use/SKILL.md +29 -0
- package/src/index.mjs +693 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# REST attestation for skill-only agents
|
|
2
|
+
|
|
3
|
+
MCP users should prefer `bootstrap_agent_identity` and
|
|
4
|
+
`verified_attestation: true`. Skill-only agents can still earn lower-trust
|
|
5
|
+
verified TOFU attestations over REST by following this recipe.
|
|
6
|
+
|
|
7
|
+
An attestation proves control of the signing key and binds the evidence fields
|
|
8
|
+
listed below.
|
|
9
|
+
It does not prove that a task succeeded, that a user accepted the result, or that
|
|
10
|
+
the submitted content is safe to disclose. Report observed outcomes and redact
|
|
11
|
+
the complete payload before signing it.
|
|
12
|
+
|
|
13
|
+
## Canonical JSON
|
|
14
|
+
|
|
15
|
+
Every signed payload uses Remembrance canonical JSON:
|
|
16
|
+
|
|
17
|
+
- Recursively sort object keys lexicographically.
|
|
18
|
+
- Preserve array order.
|
|
19
|
+
- Include every field listed below, using `null` when the field is listed but
|
|
20
|
+
absent.
|
|
21
|
+
- Encode as UTF-8 JSON with no extra whitespace. This is equivalent to
|
|
22
|
+
JavaScript `JSON.stringify(sortedObject)`.
|
|
23
|
+
|
|
24
|
+
Sign the canonical UTF-8 bytes with Ed25519. Send signatures as base64url.
|
|
25
|
+
|
|
26
|
+
## Local key file
|
|
27
|
+
|
|
28
|
+
Persist the local identity at `REMEMBRANCE_AGENT_KEY_PATH` or
|
|
29
|
+
`~/.config/remembrance/agent-key.json` with mode `0600`:
|
|
30
|
+
|
|
31
|
+
The stable subject, public key, and key ID are sent to Remembrance and can link
|
|
32
|
+
submissions from this installation. New identities default to an opaque subject
|
|
33
|
+
derived from the public-key fingerprint, without a username or hostname.
|
|
34
|
+
`REMEMBRANCE_AGENT_SUBJECT` can explicitly choose a different subject for a new
|
|
35
|
+
key; that value is transmitted too, so use identifying text only deliberately.
|
|
36
|
+
Existing keys and subjects are reused unchanged. Inspect an existing subject
|
|
37
|
+
locally before using it; do not paste the private key into chat. To replace an identifying
|
|
38
|
+
subject deliberately, select a new `REMEMBRANCE_AGENT_KEY_PATH` and retain the
|
|
39
|
+
original key securely if its trust history is still needed. This starts a new
|
|
40
|
+
identity; it does not migrate or erase previous submissions or trust history.
|
|
41
|
+
If another process creates the key file first, the example refuses to overwrite
|
|
42
|
+
it. Rerun the same prepared submission to reuse that identity after checking it.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"provider": "other",
|
|
47
|
+
"subject": "claude:local-install-stable-id",
|
|
48
|
+
"key_id": "tofu_0123456789abcdef01234567",
|
|
49
|
+
"public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
|
|
50
|
+
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
|
51
|
+
"created_at": "2026-05-06T12:00:00.000Z"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `agent.provider: "claude"` for skill-only Claude submissions. Use
|
|
56
|
+
`evidence.attestation.provider: "other"` for independent TOFU keys unless
|
|
57
|
+
Remembrance has issued a registered plugin key.
|
|
58
|
+
|
|
59
|
+
Do not copy `agent.provider` into `evidence.attestation.provider`. The first
|
|
60
|
+
field identifies the calling runtime; the second identifies the signing key
|
|
61
|
+
family.
|
|
62
|
+
|
|
63
|
+
If this file is lost, rerun this REST bootstrap recipe. The new key starts a new
|
|
64
|
+
TOFU subject trust history; the previous verified-tier history is not
|
|
65
|
+
recoverable without the old private key.
|
|
66
|
+
|
|
67
|
+
## Key registration signing payload v2
|
|
68
|
+
|
|
69
|
+
First fetch the opaque owner binding with the same credential that will own the
|
|
70
|
+
installation, then register the TOFU public key:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
GET https://remembrance.dev/api/v1/agent/keys/register
|
|
74
|
+
POST https://remembrance.dev/api/v1/agent/keys/register
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The GET response is private and non-cacheable. Include its `owner_binding` in
|
|
78
|
+
both the canonical payload and `proof`. This prevents an anonymous registration
|
|
79
|
+
signature from being replayed to claim the installation for an organization.
|
|
80
|
+
Legacy v1 signatures remain valid only for anonymous registration and an
|
|
81
|
+
already-owned same-scope key; new organization claims and anonymous-to-org
|
|
82
|
+
upgrades require v2.
|
|
83
|
+
|
|
84
|
+
If `key_id` is omitted, compute:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
key_id = "tofu_" + sha256(public_key).slice(0, 24)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`sha256(public_key)` means the lowercase hex SHA-256 digest of the exact PEM
|
|
91
|
+
public key string. Server hash fields include the `sha256:` prefix, but the
|
|
92
|
+
`key_id` slice does not include that prefix.
|
|
93
|
+
|
|
94
|
+
Sign this canonical object:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"version": "v2",
|
|
99
|
+
"purpose": "remembrance-agent-key-registration",
|
|
100
|
+
"provider": "other",
|
|
101
|
+
"key_id": "tofu_...",
|
|
102
|
+
"owner_binding": "areg_...",
|
|
103
|
+
"public_key_hash": "sha256:<hex>",
|
|
104
|
+
"subject": "claude:local-install-stable-id",
|
|
105
|
+
"signed_at": "2026-05-06T12:00:00.000Z"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Submit:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"provider": "other",
|
|
114
|
+
"key_id": "tofu_...",
|
|
115
|
+
"public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
|
|
116
|
+
"subject": "claude:local-install-stable-id",
|
|
117
|
+
"proof": {
|
|
118
|
+
"algorithm": "ed25519",
|
|
119
|
+
"owner_binding": "areg_...",
|
|
120
|
+
"signed_at": "2026-05-06T12:00:00.000Z",
|
|
121
|
+
"signature": "base64url-ed25519-signature"
|
|
122
|
+
},
|
|
123
|
+
"metadata": { "registered_by": "skill-only-rest" }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Remembrance evidence hash
|
|
128
|
+
|
|
129
|
+
For a signed remembrance, compute:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"source_type": "remembrance",
|
|
134
|
+
"schema_version": "0.1",
|
|
135
|
+
"type": "skill_use",
|
|
136
|
+
"agent": {
|
|
137
|
+
"id": null,
|
|
138
|
+
"agent_id": "claude:local-install-stable-id",
|
|
139
|
+
"provider": "claude"
|
|
140
|
+
},
|
|
141
|
+
"task": {
|
|
142
|
+
"domain": "agent-feedback",
|
|
143
|
+
"summary": "Redacted task summary",
|
|
144
|
+
"task_fingerprint": null,
|
|
145
|
+
"privacy": "redacted_public"
|
|
146
|
+
},
|
|
147
|
+
"skill": { "slug": "skill-slug" },
|
|
148
|
+
"resource": null,
|
|
149
|
+
"outcome": {
|
|
150
|
+
"success": null,
|
|
151
|
+
"usefulness_rating": null,
|
|
152
|
+
"confidence": null,
|
|
153
|
+
"user_accepted": null,
|
|
154
|
+
"failure_modes": []
|
|
155
|
+
},
|
|
156
|
+
"lesson": "Reusable redacted lesson.",
|
|
157
|
+
"suggested_update": { "kind": "none" },
|
|
158
|
+
"evidence": {
|
|
159
|
+
"trace_hash": null,
|
|
160
|
+
"artifact_hashes": []
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Canonicalize that object and set:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
evidence_hash = "sha256:" + sha256(canonical_evidence_json)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Attestation signing payload v2
|
|
172
|
+
|
|
173
|
+
Request a challenge:
|
|
174
|
+
|
|
175
|
+
`POST https://remembrance.dev/api/v1/agent/attest/challenge`
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"provider": "other",
|
|
180
|
+
"source_type": "remembrance",
|
|
181
|
+
"agent_id": "claude:local-install-stable-id",
|
|
182
|
+
"subject": "claude:local-install-stable-id",
|
|
183
|
+
"skill_slug": "skill-slug",
|
|
184
|
+
"evidence_hash": "sha256:<hex>"
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The response includes `signing_payload_canonical`. Always sign those
|
|
189
|
+
server-returned bytes verbatim; do not reconstruct the canonical bytes from the
|
|
190
|
+
illustrative object below. They are the canonical form of this v2 object:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"version": "v2",
|
|
195
|
+
"purpose": "remembrance-agent-attestation",
|
|
196
|
+
"provider": "other",
|
|
197
|
+
"source_type": "remembrance",
|
|
198
|
+
"challenge_id": "ach_...",
|
|
199
|
+
"nonce": "base64url-nonce",
|
|
200
|
+
"audience": "remembrance.dev/agent-attestation",
|
|
201
|
+
"skill_slug": "skill-slug",
|
|
202
|
+
"resource_slug": null,
|
|
203
|
+
"evidence_hash": "sha256:<hex>",
|
|
204
|
+
"agent_id": "claude:local-install-stable-id",
|
|
205
|
+
"subject": "claude:local-install-stable-id",
|
|
206
|
+
"issued_at": "2026-05-06T12:00:00.000Z",
|
|
207
|
+
"expires_at": "2026-05-06T12:05:00.000Z"
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Attach this to the remembrance payload:
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"evidence": {
|
|
216
|
+
"trace_hash": null,
|
|
217
|
+
"artifact_hashes": [],
|
|
218
|
+
"attestation": {
|
|
219
|
+
"version": "v2",
|
|
220
|
+
"provider": "other",
|
|
221
|
+
"challenge_id": "ach_...",
|
|
222
|
+
"nonce": "base64url-nonce",
|
|
223
|
+
"audience": "remembrance.dev/agent-attestation",
|
|
224
|
+
"subject": "claude:local-install-stable-id",
|
|
225
|
+
"key_id": "tofu_...",
|
|
226
|
+
"algorithm": "ed25519",
|
|
227
|
+
"issued_at": "2026-05-06T12:00:00.000Z",
|
|
228
|
+
"expires_at": "2026-05-06T12:05:00.000Z",
|
|
229
|
+
"evidence_hash": "sha256:<hex>",
|
|
230
|
+
"signature": "base64url-ed25519-signature"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Dependency-free Node 24 example
|
|
237
|
+
|
|
238
|
+
This script bootstraps a TOFU key, registers it, signs a prepared skill
|
|
239
|
+
remembrance, and submits it. Save the code as `attestation-rest.mjs` and pass one
|
|
240
|
+
JSON file: `node attestation-rest.mjs ./prepared-remembrance.json`.
|
|
241
|
+
|
|
242
|
+
Prepare the complete payload first, for example from a legacy feedback response's
|
|
243
|
+
`next_step.submit_remembrance_payload`. Supply the actual task, skill, lesson,
|
|
244
|
+
outcome, suggested update, and evidence. Keep false results and low ratings;
|
|
245
|
+
use `null` or omit an optional outcome when it is unknown. User acceptance must
|
|
246
|
+
come from the user, not from successful execution of this script. Include
|
|
247
|
+
`outcome.failure_modes` and `evidence.artifact_hashes` as arrays, even when empty.
|
|
248
|
+
The example checks required signing fields; the API remains the full schema
|
|
249
|
+
validator.
|
|
250
|
+
|
|
251
|
+
This recipe accepts only an already-redacted `redacted_public` payload. That
|
|
252
|
+
label is not a redactor: review the complete file, including task text, identity
|
|
253
|
+
metadata, lesson, and evidence, before an authorized submission. The script does
|
|
254
|
+
not collect a lesson from command-line words, invent outcomes, or silently drop
|
|
255
|
+
caller fields. It adds `agent.agent_id` only when absent, using the local signing
|
|
256
|
+
subject; a supplied different ID is refused. It attaches a new attestation and
|
|
257
|
+
refuses an existing non-null attestation instead of replacing it. Authentication
|
|
258
|
+
uses only the explicitly configured `REMEMBRANCE_API_KEY`, as below.
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
#!/usr/bin/env node
|
|
262
|
+
import {
|
|
263
|
+
createHash,
|
|
264
|
+
createPrivateKey,
|
|
265
|
+
generateKeyPairSync,
|
|
266
|
+
sign,
|
|
267
|
+
} from "node:crypto";
|
|
268
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
269
|
+
import { existsSync } from "node:fs";
|
|
270
|
+
import { homedir } from "node:os";
|
|
271
|
+
import { dirname, join } from "node:path";
|
|
272
|
+
|
|
273
|
+
const api = (
|
|
274
|
+
process.env.REMEMBRANCE_API_URL ?? "https://remembrance.dev"
|
|
275
|
+
).replace(/\/$/, "");
|
|
276
|
+
const keyPath =
|
|
277
|
+
process.env.REMEMBRANCE_AGENT_KEY_PATH ??
|
|
278
|
+
join(
|
|
279
|
+
process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
|
|
280
|
+
"remembrance",
|
|
281
|
+
"agent-key.json",
|
|
282
|
+
);
|
|
283
|
+
if (process.argv.length !== 3) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
"Usage: node attestation-rest.mjs <prepared-remembrance.json>",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const remembrance = JSON.parse(await readFile(process.argv[2], "utf8"));
|
|
289
|
+
const record = (value) =>
|
|
290
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
291
|
+
const text = (value) => typeof value === "string" && value.trim().length > 0;
|
|
292
|
+
if (
|
|
293
|
+
!record(remembrance) ||
|
|
294
|
+
remembrance.schema_version !== "0.1" ||
|
|
295
|
+
!text(remembrance.type) ||
|
|
296
|
+
!record(remembrance.agent) ||
|
|
297
|
+
!text(remembrance.agent.provider) ||
|
|
298
|
+
!record(remembrance.task) ||
|
|
299
|
+
!text(remembrance.task.domain) ||
|
|
300
|
+
!text(remembrance.task.summary) ||
|
|
301
|
+
remembrance.task.privacy !== "redacted_public" ||
|
|
302
|
+
!record(remembrance.skill) ||
|
|
303
|
+
!text(remembrance.skill.slug) ||
|
|
304
|
+
!record(remembrance.outcome) ||
|
|
305
|
+
!Array.isArray(remembrance.outcome.failure_modes) ||
|
|
306
|
+
!remembrance.outcome.failure_modes.every(
|
|
307
|
+
(value) => typeof value === "string",
|
|
308
|
+
) ||
|
|
309
|
+
!text(remembrance.lesson) ||
|
|
310
|
+
!record(remembrance.suggested_update) ||
|
|
311
|
+
!text(remembrance.suggested_update.kind) ||
|
|
312
|
+
!record(remembrance.evidence) ||
|
|
313
|
+
!Array.isArray(remembrance.evidence.artifact_hashes) ||
|
|
314
|
+
!remembrance.evidence.artifact_hashes.every(
|
|
315
|
+
(value) => typeof value === "string",
|
|
316
|
+
) ||
|
|
317
|
+
remembrance.evidence.attestation != null
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"Supply a complete, already-redacted remembrance without an attestation",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
for (const field of ["success", "user_accepted"]) {
|
|
324
|
+
if (
|
|
325
|
+
remembrance.outcome[field] != null &&
|
|
326
|
+
typeof remembrance.outcome[field] !== "boolean"
|
|
327
|
+
) {
|
|
328
|
+
throw new Error(`Invalid outcome.${field}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const rating = remembrance.outcome.usefulness_rating;
|
|
332
|
+
const confidence = remembrance.outcome.confidence;
|
|
333
|
+
if (rating != null && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
334
|
+
throw new Error("Invalid outcome.usefulness_rating");
|
|
335
|
+
}
|
|
336
|
+
if (
|
|
337
|
+
confidence != null &&
|
|
338
|
+
(!Number.isFinite(confidence) || confidence < 0 || confidence > 1)
|
|
339
|
+
) {
|
|
340
|
+
throw new Error("Invalid outcome.confidence");
|
|
341
|
+
}
|
|
342
|
+
const skillSlug = remembrance.skill.slug;
|
|
343
|
+
|
|
344
|
+
function sortForCanonical(value) {
|
|
345
|
+
if (Array.isArray(value)) return value.map(sortForCanonical);
|
|
346
|
+
if (value && typeof value === "object") {
|
|
347
|
+
return Object.keys(value)
|
|
348
|
+
.sort()
|
|
349
|
+
.reduce((acc, key) => {
|
|
350
|
+
acc[key] = sortForCanonical(value[key]);
|
|
351
|
+
return acc;
|
|
352
|
+
}, {});
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function canonicalJson(value) {
|
|
358
|
+
return JSON.stringify(sortForCanonical(value));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function hashValue(value) {
|
|
362
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function keyIdFor(publicKey) {
|
|
366
|
+
return `tofu_${hashValue(publicKey)
|
|
367
|
+
.replace(/^sha256:/, "")
|
|
368
|
+
.slice(0, 24)}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function signBase64Url(privateKeyPem, payload) {
|
|
372
|
+
return sign(
|
|
373
|
+
null,
|
|
374
|
+
Buffer.from(payload, "utf8"),
|
|
375
|
+
createPrivateKey(privateKeyPem),
|
|
376
|
+
).toString("base64url");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function post(path, body, idempotencyKey) {
|
|
380
|
+
const response = await fetch(`${api}${path}`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"content-type": "application/json",
|
|
384
|
+
...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
|
|
385
|
+
...(process.env.REMEMBRANCE_API_KEY
|
|
386
|
+
? { "x-remembrance-api-key": process.env.REMEMBRANCE_API_KEY }
|
|
387
|
+
: {}),
|
|
388
|
+
},
|
|
389
|
+
body: JSON.stringify(body),
|
|
390
|
+
});
|
|
391
|
+
const parsed = await response.json().catch(() => ({}));
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`${path} failed with ${response.status}: ${JSON.stringify(parsed)}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return parsed;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function get(path) {
|
|
401
|
+
const response = await fetch(`${api}${path}`, {
|
|
402
|
+
headers: {
|
|
403
|
+
...(process.env.REMEMBRANCE_API_KEY
|
|
404
|
+
? { "x-remembrance-api-key": process.env.REMEMBRANCE_API_KEY }
|
|
405
|
+
: {}),
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
const parsed = await response.json().catch(() => ({}));
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`${path} failed with ${response.status}: ${JSON.stringify(parsed)}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return parsed;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function readOrCreateIdentity() {
|
|
418
|
+
if (existsSync(keyPath)) {
|
|
419
|
+
return bindAgent(JSON.parse(await readFile(keyPath, "utf8")));
|
|
420
|
+
}
|
|
421
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
422
|
+
const public_key = publicKey
|
|
423
|
+
.export({ type: "spki", format: "pem" })
|
|
424
|
+
.toString();
|
|
425
|
+
const keyId = keyIdFor(public_key);
|
|
426
|
+
const identity = bindAgent({
|
|
427
|
+
provider: "other",
|
|
428
|
+
subject: process.env.REMEMBRANCE_AGENT_SUBJECT ?? `local:${keyId}`,
|
|
429
|
+
key_id: keyId,
|
|
430
|
+
public_key,
|
|
431
|
+
private_key: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
432
|
+
created_at: new Date().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
await mkdir(dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
435
|
+
await writeFile(keyPath, `${JSON.stringify(identity, null, 2)}\n`, {
|
|
436
|
+
mode: 0o600,
|
|
437
|
+
flag: "wx",
|
|
438
|
+
});
|
|
439
|
+
return identity;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function bindAgent(identity) {
|
|
443
|
+
if (!record(identity) || !text(identity.subject)) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
"The existing identity has no valid subject; restore its key file",
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (
|
|
449
|
+
Object.hasOwn(remembrance.agent, "agent_id") &&
|
|
450
|
+
remembrance.agent.agent_id !== identity.subject
|
|
451
|
+
) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"The prepared agent_id differs from the local signing subject",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
remembrance.agent.agent_id = identity.subject;
|
|
457
|
+
return identity;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const identity = await readOrCreateIdentity();
|
|
461
|
+
const { owner_binding: ownerBinding } = await get(
|
|
462
|
+
"/api/v1/agent/keys/register",
|
|
463
|
+
);
|
|
464
|
+
if (!/^areg_[A-Za-z0-9_-]{24,120}$/.test(String(ownerBinding ?? ""))) {
|
|
465
|
+
throw new Error("Remembrance returned an invalid registration binding");
|
|
466
|
+
}
|
|
467
|
+
const signedAt = new Date().toISOString();
|
|
468
|
+
const registrationPayload = canonicalJson({
|
|
469
|
+
version: "v2",
|
|
470
|
+
purpose: "remembrance-agent-key-registration",
|
|
471
|
+
provider: identity.provider,
|
|
472
|
+
key_id: identity.key_id,
|
|
473
|
+
owner_binding: ownerBinding,
|
|
474
|
+
public_key_hash: hashValue(identity.public_key),
|
|
475
|
+
subject: identity.subject,
|
|
476
|
+
signed_at: signedAt,
|
|
477
|
+
});
|
|
478
|
+
await post("/api/v1/agent/keys/register", {
|
|
479
|
+
provider: identity.provider,
|
|
480
|
+
key_id: identity.key_id,
|
|
481
|
+
public_key: identity.public_key,
|
|
482
|
+
subject: identity.subject,
|
|
483
|
+
proof: {
|
|
484
|
+
algorithm: "ed25519",
|
|
485
|
+
owner_binding: ownerBinding,
|
|
486
|
+
signed_at: signedAt,
|
|
487
|
+
signature: signBase64Url(identity.private_key, registrationPayload),
|
|
488
|
+
},
|
|
489
|
+
metadata: { registered_by: "skill-only-rest" },
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const evidenceHash = hashValue(
|
|
493
|
+
canonicalJson({
|
|
494
|
+
source_type: "remembrance",
|
|
495
|
+
schema_version: remembrance.schema_version,
|
|
496
|
+
type: remembrance.type,
|
|
497
|
+
agent: {
|
|
498
|
+
id: remembrance.agent.id ?? null,
|
|
499
|
+
agent_id: remembrance.agent.agent_id ?? null,
|
|
500
|
+
provider: remembrance.agent.provider ?? null,
|
|
501
|
+
},
|
|
502
|
+
task: {
|
|
503
|
+
domain: remembrance.task.domain,
|
|
504
|
+
summary: remembrance.task.summary,
|
|
505
|
+
task_fingerprint: remembrance.task.task_fingerprint ?? null,
|
|
506
|
+
privacy: remembrance.task.privacy,
|
|
507
|
+
},
|
|
508
|
+
skill: remembrance.skill ?? null,
|
|
509
|
+
resource: remembrance.resource ?? null,
|
|
510
|
+
outcome: remembrance.outcome,
|
|
511
|
+
lesson: remembrance.lesson,
|
|
512
|
+
suggested_update: remembrance.suggested_update,
|
|
513
|
+
evidence: {
|
|
514
|
+
trace_hash: remembrance.evidence.trace_hash ?? null,
|
|
515
|
+
artifact_hashes: remembrance.evidence.artifact_hashes ?? [],
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
const challenge = await post("/api/v1/agent/attest/challenge", {
|
|
520
|
+
provider: identity.provider,
|
|
521
|
+
source_type: "remembrance",
|
|
522
|
+
agent_id: identity.subject,
|
|
523
|
+
subject: identity.subject,
|
|
524
|
+
skill_slug: skillSlug,
|
|
525
|
+
evidence_hash: evidenceHash,
|
|
526
|
+
});
|
|
527
|
+
remembrance.evidence.attestation = {
|
|
528
|
+
version: "v2",
|
|
529
|
+
provider: identity.provider,
|
|
530
|
+
challenge_id: challenge.challenge_id,
|
|
531
|
+
nonce: challenge.nonce,
|
|
532
|
+
audience: challenge.audience,
|
|
533
|
+
subject: identity.subject,
|
|
534
|
+
key_id: identity.key_id,
|
|
535
|
+
algorithm: "ed25519",
|
|
536
|
+
issued_at: challenge.issued_at,
|
|
537
|
+
expires_at: challenge.expires_at,
|
|
538
|
+
evidence_hash: evidenceHash,
|
|
539
|
+
signature: signBase64Url(
|
|
540
|
+
identity.private_key,
|
|
541
|
+
challenge.signing_payload_canonical,
|
|
542
|
+
),
|
|
543
|
+
};
|
|
544
|
+
const idempotencyKey = hashValue(canonicalJson(remembrance)).replace(
|
|
545
|
+
/^sha256:/,
|
|
546
|
+
"",
|
|
547
|
+
);
|
|
548
|
+
console.log(
|
|
549
|
+
await post("/api/v1/agent/remembrances", remembrance, idempotencyKey),
|
|
550
|
+
);
|
|
551
|
+
```
|