blun-king-cli 9.1.563 → 9.1.565

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.
Files changed (85) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  4. package/agent-spine-plugin/.mcp.json +8 -8
  5. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  6. package/agent-spine-plugin/LICENSE +186 -186
  7. package/agent-spine-plugin/README.md +394 -0
  8. package/agent-spine-plugin/SECURITY.md +47 -0
  9. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  10. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  11. package/agent-spine-plugin/bin/agentspine.js +7 -7
  12. package/agent-spine-plugin/blun.plugin.json +33 -33
  13. package/agent-spine-plugin/hooks/codex.json +47 -47
  14. package/agent-spine-plugin/hooks/hooks.json +106 -106
  15. package/agent-spine-plugin/hooks/version.json +5 -5
  16. package/agent-spine-plugin/package.json +69 -69
  17. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  18. package/agent-spine-plugin/skill/SKILL.md +76 -76
  19. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  20. package/agent-spine-plugin/src/cli.js +1488 -1442
  21. package/agent-spine-plugin/src/hook.js +886 -812
  22. package/agent-spine-plugin/src/index.js +93 -93
  23. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  24. package/agent-spine-plugin/src/lib/attention.js +755 -755
  25. package/agent-spine-plugin/src/lib/audit.js +351 -342
  26. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  27. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  28. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  29. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  30. package/agent-spine-plugin/src/lib/context.js +154 -154
  31. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  32. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  33. package/agent-spine-plugin/src/lib/documents.js +217 -217
  34. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  35. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  36. package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
  37. package/agent-spine-plugin/src/lib/graph.js +337 -337
  38. package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
  39. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  40. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  41. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  42. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  43. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  44. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  45. package/agent-spine-plugin/src/lib/paths.js +109 -109
  46. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  47. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  48. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  49. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  50. package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
  51. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  52. package/agent-spine-plugin/src/lib/source-roots.js +529 -482
  53. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  54. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  55. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  56. package/agent-spine-plugin/src/mcp.js +597 -572
  57. package/agent-spine-plugin/src/version.js +1 -1
  58. package/agent-spine-plugin/src/worker.js +202 -195
  59. package/bin/active-steer-priority-policy.cjs +24 -0
  60. package/bin/launcher-runtime.js +8 -1
  61. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  62. package/bin/thinking-activity-status-policy.cjs +132 -0
  63. package/bin/thinking-only-guard.cjs +75 -0
  64. package/bin/tool-call-loop-policy.cjs +53 -0
  65. package/bin/turn-thinking-policy.cjs +25 -1
  66. package/blun.mjs +554 -64
  67. package/package.json +4 -1
  68. package/standard-skills/translate-native/LICENSE +21 -21
  69. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  70. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  71. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  72. package/standard-skills/translate-native/references/structured-content.md +72 -72
  73. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  74. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  75. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  76. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  77. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  78. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  79. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  80. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  81. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  82. package/standard-tools/language-guard/check_diacritics.py +353 -353
  83. package/standard-tools/language-guard/guard_service_client.py +264 -264
  84. package/standard-tools/language-guard/language_quality.py +377 -377
  85. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,969 +1,969 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink, writeFile
4
- } from "node:fs/promises";
5
- import { basename, dirname, join, resolve } from "node:path";
6
- import { buildCatalog } from "./catalog.js";
7
- import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
8
- import { loadGraph } from "./graph.js";
9
- import { learningFindings, loadLearning } from "./learning.js";
10
- import { isInside, projectStateDir } from "./paths.js";
11
- import {
12
- assertTrustedIdentity, loadTrust, signEnvelope, trustFindings, verifyEnvelope
13
- } from "./authentication.js";
14
-
15
- const KINDS = new Set(["preference", "no-go", "goal", "correction", "personal-fact", "project-fact", "reference"]);
16
- const PRIVACY = new Set(["shared", "group"]);
17
- const STATUSES = new Set(["pending", "accepted", "rejected", "superseded", "rolled-back"]);
18
- const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
19
- const DIGEST_RE = /^[a-f0-9]{64}$/;
20
- const MAX_STATE_BYTES = 5 * 1024 * 1024;
21
- const MAX_EVENT_BYTES = 64 * 1024;
22
- const MAX_ADAPTER_EVENTS = 2000;
23
- const MAX_ADAPTER_BYTES = 20 * 1024 * 1024;
24
- const CONFIRMATION = "local-share-confirmed";
25
- const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/i;
26
- const AUTHORITY_RE = /\b(?:user|agent|person|they|he|she|i|ich|wir|nutzer|benutzer).{0,60}\b(?:may|can|is allowed|is authorized|has|have|darf|berechtigt|hat|haben).{0,50}\b(?:admin(?:istrator)?|permissions?|rights?|authorization|production access|deploy|billing|spending|policy exception|bypass|zugang|rechte|berechtigung|produktion|abrechnung|ausnahme|umgehen)\b/i;
27
-
28
- function canonical(value) {
29
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
30
- if (value && typeof value === "object") {
31
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
32
- }
33
- return JSON.stringify(value);
34
- }
35
-
36
- function digest(value) {
37
- return createHash("sha256").update(canonical(value)).digest("hex");
38
- }
39
-
40
- function withoutDigest(value) {
41
- const { digest: _digest, ...rest } = value;
42
- return rest;
43
- }
44
-
45
- function exactKeys(value, expected) {
46
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
47
- const actual = Object.keys(value).sort();
48
- const wanted = [...expected].sort();
49
- return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
50
- }
51
-
52
- function safeText(value, field, maximum) {
53
- if (!value || typeof value !== "string") throw new Error(`${field} is required`);
54
- const result = value.trim().slice(0, maximum);
55
- if (!result) throw new Error(`${field} is required`);
56
- if (SECRET_RE.test(result)) throw new Error(`${field} appears to contain a secret and cannot enter shared memory`);
57
- return result;
58
- }
59
-
60
- function safeClaim(value) {
61
- const result = safeText(value, "claim", 1000);
62
- if (AUTHORITY_RE.test(result)) throw new Error("authority and access claims cannot enter shared memory");
63
- return result;
64
- }
65
-
66
- function date(value, field = "date") {
67
- const parsed = value instanceof Date ? value : new Date(value);
68
- if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
69
- return parsed.toISOString();
70
- }
71
-
72
- function validDateValue(value) {
73
- return typeof value === "string" && value.length > 0 && Number.isFinite(new Date(value).getTime());
74
- }
75
-
76
- function integer(value, field, minimum, maximum) {
77
- const result = Number(value);
78
- if (!Number.isInteger(result) || result < minimum || result > maximum) throw new Error(`${field} must be an integer between ${minimum} and ${maximum}`);
79
- return result;
80
- }
81
-
82
- function emptySharing(root) {
83
- return {
84
- schema: "agentspine.sharing/v1",
85
- root,
86
- instanceId: `instance:${randomUUID()}`,
87
- config: { maxContextItems: 12 },
88
- records: [],
89
- history: []
90
- };
91
- }
92
-
93
- function normalizeSharing(value, root) {
94
- if (!value || typeof value !== "object" || Array.isArray(value)
95
- || value.schema !== "agentspine.sharing/v1" || value.root !== root
96
- || !ID_RE.test(value.instanceId || "") || !value.config || typeof value.config !== "object"
97
- || !Array.isArray(value.records) || !value.records.every((item) => item && typeof item === "object")
98
- || !Array.isArray(value.history) || !value.history.every((item) => item && typeof item === "object")) {
99
- throw new Error("sharing state structure is invalid; run the audit before using shared memory");
100
- }
101
- return value;
102
- }
103
-
104
- function validConfig(config) {
105
- return Number.isInteger(config?.maxContextItems) && config.maxContextItems >= 1 && config.maxContextItems <= 50;
106
- }
107
-
108
- async function readJson(path, root) {
109
- try {
110
- const metadata = await stat(path);
111
- if (metadata.size > MAX_STATE_BYTES) throw new Error("sharing state exceeds the 5 MiB read limit");
112
- return normalizeSharing(JSON.parse(await readFile(path, "utf8")), root);
113
- } catch (error) {
114
- if (error.code !== "ENOENT") throw error;
115
- return emptySharing(root);
116
- }
117
- }
118
-
119
- async function writeJson(path, state) {
120
- const content = `${JSON.stringify(state, null, 2)}\n`;
121
- if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("sharing state exceeds 5 MiB; reject or delete old imports first");
122
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
123
- await writeFile(temporary, content, { mode: 0o600 });
124
- try {
125
- await replaceFileWithRetry(temporary, path);
126
- } catch (error) {
127
- await unlink(temporary).catch((cleanupError) => {
128
- if (cleanupError.code !== "ENOENT") error.cleanupError = cleanupError;
129
- });
130
- throw error;
131
- }
132
- }
133
-
134
- async function acquireLock(path) {
135
- const lockPath = `${path}.lock`;
136
- for (let attempt = 0; attempt < 80; attempt += 1) {
137
- try {
138
- return { handle: await open(lockPath, "wx", 0o600), lockPath };
139
- } catch (error) {
140
- if (!isFileLockContention(error)) throw error;
141
- try {
142
- const metadata = await stat(lockPath);
143
- if (Date.now() - metadata.mtimeMs > 15000) await unlink(lockPath);
144
- } catch (lockError) {
145
- if (lockError.code !== "ENOENT") throw lockError;
146
- }
147
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
148
- }
149
- }
150
- throw new Error("sharing state is busy; retry shortly");
151
- }
152
-
153
- async function withLock(path, read, task, save = true) {
154
- const { handle, lockPath } = await acquireLock(path);
155
- try {
156
- const value = await read();
157
- const result = await task(value);
158
- if (save) await writeJson(path, value);
159
- return result;
160
- } finally {
161
- await handle.close();
162
- await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
163
- }
164
- }
165
-
166
- async function sharingPaths(root, providedCatalog = null) {
167
- const catalog = providedCatalog || await buildCatalog(root);
168
- const directory = await projectStateDir(catalog.root);
169
- return { catalog, sharingPath: join(directory, "sharing.json") };
170
- }
171
-
172
- export async function loadSharing(root = process.cwd(), providedCatalog = null) {
173
- const paths = await sharingPaths(root, providedCatalog);
174
- return { sharing: await readJson(paths.sharingPath, paths.catalog.root), ...paths };
175
- }
176
-
177
- export async function inspectSharing(root = process.cwd(), providedCatalog = null) {
178
- const paths = await sharingPaths(root, providedCatalog);
179
- try {
180
- return { sharing: await readJson(paths.sharingPath, paths.catalog.root), error: null, ...paths };
181
- } catch (error) {
182
- return { sharing: emptySharing(paths.catalog.root), error: error.message, ...paths };
183
- }
184
- }
185
-
186
- function preserve(state, value, now) {
187
- state.history.push({
188
- kind: "shared-record",
189
- recordId: value.event.id,
190
- supersededAt: now,
191
- privacy: value.event.privacy,
192
- value: { ...value, authority: "context-only" },
193
- authority: "context-only"
194
- });
195
- }
196
-
197
- async function mutation(root, operation) {
198
- const paths = await sharingPaths(root);
199
- const { graph } = await loadGraph(paths.catalog.root, paths.catalog);
200
- return withLock(
201
- paths.sharingPath,
202
- () => readJson(paths.sharingPath, paths.catalog.root),
203
- async (state) => {
204
- if (!validConfig(state.config)) throw new Error("sharing configuration is invalid; run the audit before using shared memory");
205
- const findings = sharingFindings(state, graph);
206
- if (findings.length) throw new Error(`sharing state failed closed: ${findings.join(", ")}`);
207
- const result = await operation(state, paths.catalog, paths.sharingPath, graph);
208
- const resultFindings = sharingFindings(state, graph);
209
- if (resultFindings.length) throw new Error(`sharing mutation failed closed: ${resultFindings.join(", ")}`);
210
- return result;
211
- }
212
- );
213
- }
214
-
215
- function requireConfirmation(confirmation) {
216
- if (confirmation !== CONFIRMATION) throw new Error("shared adapter writes require explicit local owner confirmation");
217
- }
218
-
219
- function manifestBody({ adapterId, scopeId, createdAt }) {
220
- return {
221
- schema: "agentspine.directory-adapter/v1",
222
- adapter: "directory",
223
- adapterId,
224
- scopeId,
225
- createdAt,
226
- authority: "context-only"
227
- };
228
- }
229
-
230
- function validateManifest(manifest) {
231
- if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)
232
- || !exactKeys(manifest, ["schema", "adapter", "adapterId", "scopeId", "createdAt", "authority", "digest"])
233
- || manifest.schema !== "agentspine.directory-adapter/v1" || manifest.adapter !== "directory"
234
- || !ID_RE.test(manifest.adapterId || "") || !ID_RE.test(manifest.scopeId || "")
235
- || manifest.authority !== "context-only" || !validDateValue(manifest.createdAt)
236
- || !DIGEST_RE.test(manifest.digest || "") || digest(withoutDigest(manifest)) !== manifest.digest) {
237
- throw new Error("directory adapter manifest is invalid or has failed its integrity check");
238
- }
239
- return manifest;
240
- }
241
-
242
- function validateAuthentication(authentication) {
243
- return authentication === null || authentication === undefined || (
244
- exactKeys(authentication, ["mode", "signerId", "keyId", "signedAt", "verifiedAt", "signature", "authority"])
245
- && authentication.mode === "signed" && ID_RE.test(authentication.signerId || "")
246
- && ID_RE.test(authentication.keyId || "") && validDateValue(authentication.signedAt)
247
- && validDateValue(authentication.verifiedAt) && typeof authentication.signature === "string"
248
- && authentication.signature.length >= 80 && authentication.signature.length <= 96
249
- && authentication.authority === "context-only"
250
- );
251
- }
252
-
253
- async function assertRegularFile(path, field) {
254
- const metadata = await lstat(path);
255
- if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`${field} must be a regular file, not a symlink`);
256
- return metadata;
257
- }
258
-
259
- async function adapterLocation(root, directory, create = false) {
260
- if (!directory || typeof directory !== "string") throw new Error("adapter directory is required");
261
- const target = resolve(directory);
262
- if (isInside(root, target)) throw new Error("shared adapter directory must remain outside the scanned project");
263
- if (create) {
264
- let ancestor = target;
265
- const missing = [];
266
- while (true) {
267
- try {
268
- ancestor = await realpath(ancestor);
269
- break;
270
- } catch (error) {
271
- if (error.code !== "ENOENT") throw error;
272
- const parent = dirname(ancestor);
273
- if (parent === ancestor) throw error;
274
- missing.push(basename(ancestor));
275
- ancestor = parent;
276
- }
277
- }
278
- const prospective = resolve(ancestor, ...missing.reverse());
279
- if (isInside(root, prospective)) throw new Error("shared adapter directory must remain outside the scanned project");
280
- await mkdir(target, { recursive: true, mode: 0o700 });
281
- }
282
- const canonicalDirectory = await realpath(target);
283
- if (isInside(root, canonicalDirectory)) throw new Error("shared adapter directory must remain outside the scanned project");
284
- return canonicalDirectory;
285
- }
286
-
287
- async function openAdapter(root, directory) {
288
- const adapterDirectory = await adapterLocation(root, directory, false);
289
- const manifestPath = join(adapterDirectory, ".agentspine-exchange.json");
290
- const metadata = await assertRegularFile(manifestPath, "adapter manifest");
291
- if (metadata.size > MAX_EVENT_BYTES) throw new Error("adapter manifest exceeds 64 KiB");
292
- const manifestDocument = JSON.parse(await readFile(manifestPath, "utf8"));
293
- let manifest;
294
- let authentication = null;
295
- let publicIdentity = null;
296
- if (manifestDocument?.schema === "agentspine.signed-envelope/v1") {
297
- const verified = verifyEnvelope(manifestDocument, "manifest");
298
- manifest = validateManifest(verified.payload);
299
- authentication = verified.authentication;
300
- publicIdentity = verified.publicIdentity;
301
- } else {
302
- manifest = validateManifest(manifestDocument);
303
- }
304
- const eventsDirectory = join(adapterDirectory, "events");
305
- const eventsMetadata = await lstat(eventsDirectory);
306
- if (!eventsMetadata.isDirectory() || eventsMetadata.isSymbolicLink()) throw new Error("adapter events path must be a real directory");
307
- return {
308
- adapterDirectory, manifestPath, manifest, manifestDocument,
309
- eventsDirectory, authentication, publicIdentity
310
- };
311
- }
312
-
313
- export async function initDirectoryAdapter({
314
- root = process.cwd(), directory, scopeId, adapterId = `adapter:${randomUUID()}`,
315
- signerId = null, confirmation, now = new Date()
316
- }) {
317
- requireConfirmation(confirmation);
318
- if (!ID_RE.test(scopeId || "")) throw new Error("scopeId must be a stable, whitespace-free identifier");
319
- if (!ID_RE.test(adapterId || "")) throw new Error("adapterId must be a stable, whitespace-free identifier");
320
- const catalog = await buildCatalog(root);
321
- const adapterDirectory = await adapterLocation(catalog.root, directory, true);
322
- const manifestPath = join(adapterDirectory, ".agentspine-exchange.json");
323
- const eventsDirectory = join(adapterDirectory, "events");
324
- await mkdir(eventsDirectory, { recursive: true, mode: 0o700 });
325
- const lockTarget = join(adapterDirectory, ".agentspine-adapter");
326
- return withLock(lockTarget, async () => ({}), async () => {
327
- try {
328
- await assertRegularFile(manifestPath, "adapter manifest");
329
- const existingDocument = JSON.parse(await readFile(manifestPath, "utf8"));
330
- const verified = existingDocument?.schema === "agentspine.signed-envelope/v1"
331
- ? verifyEnvelope(existingDocument, "manifest") : null;
332
- const existing = validateManifest(verified ? verified.payload : existingDocument);
333
- if (existing.scopeId !== scopeId) throw new Error(`adapter already belongs to scope ${existing.scopeId}`);
334
- if (signerId && verified?.publicIdentity.signerId !== signerId) throw new Error("adapter is signed by a different identity");
335
- return {
336
- created: false, manifest: existing, signed: Boolean(verified),
337
- signer: verified?.publicIdentity || null, adapterDirectory, manifestPath
338
- };
339
- } catch (error) {
340
- if (error.code !== "ENOENT") throw error;
341
- }
342
- const body = manifestBody({ adapterId, scopeId, createdAt: date(now, "now") });
343
- const manifest = { ...body, digest: digest(body) };
344
- const document = signerId
345
- ? await signEnvelope({ root: catalog.root, signerId, kind: "manifest", payload: manifest, now })
346
- : manifest;
347
- const temporary = `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
348
- await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
349
- await rename(temporary, manifestPath);
350
- return {
351
- created: true, manifest, signed: Boolean(signerId),
352
- signer: document.signer || null, adapterDirectory, manifestPath
353
- };
354
- }, false);
355
- }
356
-
357
- function eventBody({
358
- id, scopeId, originInstanceId, candidate, supersedesEventId, publishedAt
359
- }) {
360
- const proof = candidate.review || candidate.promotion;
361
- return {
362
- schema: "agentspine.shared-event/v1",
363
- id,
364
- scopeId,
365
- originInstanceId,
366
- kind: candidate.kind,
367
- claim: candidate.claim,
368
- subjectId: candidate.subjectId,
369
- privacy: candidate.privacy,
370
- groupId: candidate.groupId,
371
- confidence: candidate.confidence,
372
- source: {
373
- type: "accepted-learning",
374
- learningId: candidate.id,
375
- acceptedAt: candidate.acceptedAt,
376
- automatic: candidate.automatic,
377
- evidenceCount: candidate.evidence.length,
378
- reviewDigest: digest(proof || {})
379
- },
380
- supersedesEventId,
381
- publishedAt,
382
- authority: "context-only"
383
- };
384
- }
385
-
386
- export function validateSharedEvent(event, expectedScopeId = null) {
387
- if (!event || typeof event !== "object" || Array.isArray(event)
388
- || !exactKeys(event, [
389
- "schema", "id", "scopeId", "originInstanceId", "kind", "claim", "subjectId", "privacy",
390
- "groupId", "confidence", "source", "supersedesEventId", "publishedAt", "authority", "digest"
391
- ])
392
- || !exactKeys(event.source, ["type", "learningId", "acceptedAt", "automatic", "evidenceCount", "reviewDigest"])
393
- || event.schema !== "agentspine.shared-event/v1" || !ID_RE.test(event.id || "")
394
- || !ID_RE.test(event.scopeId || "") || !ID_RE.test(event.originInstanceId || "")
395
- || (expectedScopeId !== null && event.scopeId !== expectedScopeId)
396
- || !KINDS.has(event.kind) || !PRIVACY.has(event.privacy)
397
- || typeof event.claim !== "string" || !event.claim.trim() || event.claim.length > 1000
398
- || (event.subjectId !== null && !ID_RE.test(event.subjectId || ""))
399
- || (event.privacy === "group" && !ID_RE.test(event.groupId || ""))
400
- || (event.privacy !== "group" && event.groupId !== null)
401
- || !Number.isFinite(event.confidence) || event.confidence < 0 || event.confidence > 1
402
- || event.source?.type !== "accepted-learning" || !ID_RE.test(event.source?.learningId || "")
403
- || !validDateValue(event.source?.acceptedAt)
404
- || typeof event.source?.automatic !== "boolean" || !Number.isInteger(event.source?.evidenceCount) || event.source.evidenceCount < 1
405
- || !DIGEST_RE.test(event.source?.reviewDigest || "")
406
- || (event.supersedesEventId !== null && !ID_RE.test(event.supersedesEventId || ""))
407
- || !validDateValue(event.publishedAt) || event.authority !== "context-only"
408
- || !DIGEST_RE.test(event.digest || "") || digest(withoutDigest(event)) !== event.digest) {
409
- throw new Error(`shared event is invalid or has failed its integrity check: ${event?.id || "unknown"}`);
410
- }
411
- if (SECRET_RE.test(event.claim) || AUTHORITY_RE.test(event.claim)) throw new Error(`shared event contains unsafe context: ${event.id}`);
412
- return event;
413
- }
414
-
415
- function eventFilename(id) {
416
- return `${createHash("sha256").update(id).digest("hex")}.json`;
417
- }
418
-
419
- async function readEventFile(path, scopeId) {
420
- const metadata = await assertRegularFile(path, "shared event");
421
- if (metadata.size > MAX_EVENT_BYTES) throw new Error("shared event exceeds 64 KiB");
422
- const document = JSON.parse(await readFile(path, "utf8"));
423
- if (document?.schema === "agentspine.signed-envelope/v1") {
424
- const verified = verifyEnvelope(document, "event");
425
- return {
426
- event: validateSharedEvent(verified.payload, scopeId),
427
- authentication: verified.authentication,
428
- publicIdentity: verified.publicIdentity,
429
- document
430
- };
431
- }
432
- return { event: validateSharedEvent(document, scopeId), authentication: null, publicIdentity: null, document };
433
- }
434
-
435
- export async function publishLearning({
436
- root = process.cwd(), directory, learningId, eventId = `shared:${randomUUID()}`,
437
- supersedesEventId = null, signerId = null, confirmation, now = new Date()
438
- }) {
439
- requireConfirmation(confirmation);
440
- if (!ID_RE.test(learningId || "")) throw new Error("learningId is required");
441
- if (!ID_RE.test(eventId || "")) throw new Error("eventId must be a stable, whitespace-free identifier");
442
- if (supersedesEventId !== null && !ID_RE.test(supersedesEventId || "")) throw new Error("supersedesEventId is invalid");
443
- const catalog = await buildCatalog(root);
444
- const adapter = await openAdapter(catalog.root, directory);
445
- if (adapter.authentication && !signerId) throw new Error("signed adapters require an explicit local signer");
446
- if (!adapter.authentication && signerId) throw new Error("an unsigned adapter cannot accept signed events");
447
- const { learning } = await loadLearning(catalog.root, catalog);
448
- const { graph } = await loadGraph(catalog.root, catalog);
449
- const learningIssues = learningFindings(learning, graph);
450
- if (learningIssues.length) throw new Error(`learning state failed closed: ${learningIssues.join(", ")}`);
451
- const candidate = learning.candidates.find((item) => item.id === learningId);
452
- if (!candidate || candidate.status !== "accepted") throw new Error("only accepted learning can be published");
453
- if (!PRIVACY.has(candidate.privacy)) throw new Error("private learning can never be published to a shared adapter");
454
- safeClaim(candidate.claim);
455
- let instanceId;
456
- await mutation(catalog.root, (state) => { instanceId = state.instanceId; return { instanceId }; });
457
- const body = eventBody({
458
- id: eventId, scopeId: adapter.manifest.scopeId, originInstanceId: instanceId,
459
- candidate, supersedesEventId, publishedAt: date(now, "now")
460
- });
461
- const event = { ...body, digest: digest(body) };
462
- validateSharedEvent(event, adapter.manifest.scopeId);
463
- if (supersedesEventId !== null) {
464
- const previousPath = join(adapter.eventsDirectory, eventFilename(supersedesEventId));
465
- const previous = (await readEventFile(previousPath, adapter.manifest.scopeId)).event;
466
- if (previous.kind !== event.kind || previous.subjectId !== event.subjectId
467
- || previous.privacy !== event.privacy || previous.groupId !== event.groupId) {
468
- throw new Error("a superseding shared event must keep kind, subject, and privacy scope");
469
- }
470
- }
471
- const target = join(adapter.eventsDirectory, eventFilename(event.id));
472
- const lockTarget = join(adapter.adapterDirectory, ".agentspine-adapter");
473
- const document = signerId
474
- ? await signEnvelope({ root: catalog.root, signerId, kind: "event", payload: event, now })
475
- : event;
476
- return withLock(lockTarget, async () => ({}), async () => {
477
- try {
478
- const existing = await readEventFile(target, adapter.manifest.scopeId);
479
- if (existing.event.digest !== event.digest) throw new Error(`shared event ID collision: ${event.id}`);
480
- return {
481
- created: false, event: existing.event, authentication: existing.authentication,
482
- eventPath: target, manifest: adapter.manifest
483
- };
484
- } catch (error) {
485
- if (error.code !== "ENOENT") throw error;
486
- }
487
- const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
488
- await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
489
- await rename(temporary, target);
490
- return {
491
- created: true, event,
492
- authentication: document.schema === "agentspine.signed-envelope/v1"
493
- ? verifyEnvelope(document, "event").authentication : null,
494
- eventPath: target, manifest: adapter.manifest
495
- };
496
- }, false);
497
- }
498
-
499
- async function readAdapterEvents(adapter) {
500
- const entries = await readdir(adapter.eventsDirectory, { withFileTypes: true });
501
- const candidates = entries.filter((entry) => entry.name.endsWith(".json"));
502
- if (candidates.length > MAX_ADAPTER_EVENTS) throw new Error(`adapter exceeds ${MAX_ADAPTER_EVENTS} events`);
503
- if (candidates.some((entry) => !entry.isFile() || entry.isSymbolicLink())) throw new Error("adapter contains a non-regular shared event");
504
- const events = [];
505
- let totalBytes = 0;
506
- for (const entry of candidates.sort((a, b) => a.name.localeCompare(b.name))) {
507
- const path = join(adapter.eventsDirectory, entry.name);
508
- const metadata = await lstat(path);
509
- totalBytes += metadata.size;
510
- if (totalBytes > MAX_ADAPTER_BYTES) throw new Error("adapter event payload exceeds the 20 MiB pull limit");
511
- const item = await readEventFile(path, adapter.manifest.scopeId);
512
- if (entry.name !== eventFilename(item.event.id)) throw new Error(`shared event filename does not match its ID: ${item.event.id}`);
513
- events.push(item);
514
- }
515
- const byId = new Map();
516
- for (const item of events) {
517
- const previous = byId.get(item.event.id);
518
- if (previous && previous.event.digest !== item.event.digest) throw new Error(`shared event ID collision: ${item.event.id}`);
519
- byId.set(item.event.id, item);
520
- }
521
- return [...byId.values()].sort((a, b) => a.event.publishedAt.localeCompare(b.event.publishedAt) || a.event.id.localeCompare(b.event.id));
522
- }
523
-
524
- export async function readDirectoryExchange({
525
- root = process.cwd(), directory, requireAuthenticated = true
526
- }) {
527
- const catalog = await buildCatalog(root);
528
- const adapter = await openAdapter(catalog.root, directory);
529
- if (requireAuthenticated && !adapter.authentication) {
530
- throw new Error("HTTPS snapshots require an authenticated directory adapter");
531
- }
532
- const events = await readAdapterEvents(adapter);
533
- if (adapter.authentication && events.some((item) => !item.authentication)) {
534
- throw new Error("signed adapters cannot contain unsigned events");
535
- }
536
- if (!adapter.authentication && events.some((item) => item.authentication)) {
537
- throw new Error("unsigned adapters cannot mix signed events");
538
- }
539
- return {
540
- root: catalog.root,
541
- scopeId: adapter.manifest.scopeId,
542
- adapterId: adapter.manifest.adapterId,
543
- authenticated: Boolean(adapter.authentication),
544
- manifest: structuredClone(adapter.manifestDocument),
545
- events: events.map((item) => structuredClone(item.document))
546
- };
547
- }
548
-
549
- export async function pullShared({ root = process.cwd(), directory, requireAuthenticated = false, now = new Date() }) {
550
- const catalog = await buildCatalog(root);
551
- const adapter = await openAdapter(catalog.root, directory);
552
- if (requireAuthenticated && !adapter.authentication) throw new Error("adapter is not authenticated");
553
- const { trust } = await loadTrust(catalog.root, catalog);
554
- if (adapter.authentication) assertTrustedIdentity(adapter.publicIdentity, trust);
555
- const events = await readAdapterEvents(adapter);
556
- if (adapter.authentication && events.some((item) => !item.authentication)) {
557
- throw new Error("signed adapters cannot contain unsigned events");
558
- }
559
- if (!adapter.authentication && events.some((item) => item.authentication)) {
560
- throw new Error("unsigned adapters cannot mix signed events");
561
- }
562
- for (const item of events) if (item.authentication) assertTrustedIdentity(item.publicIdentity, trust);
563
- const receivedAt = date(now, "now");
564
- return mutation(catalog.root, (state, _catalog, sharingPath) => {
565
- const known = new Map([
566
- ...state.records.map((record) => [record.event.id, record.event.digest]),
567
- ...state.history.map((entry) => [entry.value?.event?.id, entry.value?.event?.digest]).filter(([id]) => id)
568
- ]);
569
- const imported = [];
570
- const skipped = [];
571
- for (const item of events) {
572
- const event = item.event;
573
- const knownDigest = known.get(event.id);
574
- if (knownDigest && knownDigest !== event.digest) throw new Error(`shared event ID collision in local state: ${event.id}`);
575
- if (knownDigest || event.originInstanceId === state.instanceId) {
576
- skipped.push(event.id);
577
- continue;
578
- }
579
- state.records.push({
580
- event,
581
- authentication: item.authentication ? { ...item.authentication, verifiedAt: receivedAt } : null,
582
- adapterId: adapter.manifest.adapterId,
583
- receivedAt,
584
- status: "pending",
585
- review: null,
586
- acceptedAt: null,
587
- supersededIds: [],
588
- authority: "context-only"
589
- });
590
- known.set(event.id, event.digest);
591
- imported.push(event.id);
592
- }
593
- state.records.sort((a, b) => a.event.id.localeCompare(b.event.id));
594
- return {
595
- imported, skipped, scopeId: adapter.manifest.scopeId, adapterId: adapter.manifest.adapterId,
596
- authenticated: Boolean(adapter.authentication),
597
- manifestSigner: adapter.authentication ? {
598
- signerId: adapter.authentication.signerId, keyId: adapter.authentication.keyId,
599
- authority: "context-only"
600
- } : null,
601
- eventSigners: [...new Map(events.filter((item) => item.authentication).map((item) => [
602
- item.authentication.keyId,
603
- { signerId: item.authentication.signerId, keyId: item.authentication.keyId, authority: "context-only" }
604
- ])).values()],
605
- sharingPath
606
- };
607
- });
608
- }
609
-
610
- export async function reviewShared({
611
- root = process.cwd(), id, decision, reason, confirmedByUser = false, now = new Date()
612
- }) {
613
- if (!ID_RE.test(id || "")) throw new Error("shared event id is required");
614
- if (!new Set(["accept", "reject"]).has(decision)) throw new Error("decision must be accept or reject");
615
- const reviewReason = safeText(reason, "reason", 500);
616
- const reviewedAt = date(now, "now");
617
- const acceptedTrust = decision === "accept" ? (await loadTrust(root)).trust : null;
618
- return mutation(root, (state, _catalog, sharingPath, graph) => {
619
- const record = state.records.find((item) => item.event.id === id);
620
- if (!record) throw new Error(`unknown shared event: ${id}`);
621
- if (record.status !== "pending") throw new Error("only a pending shared event can be reviewed");
622
- preserve(state, record, reviewedAt);
623
- if (decision === "reject") {
624
- const rejected = {
625
- ...record,
626
- status: "rejected",
627
- review: { decision, reason: reviewReason, confirmedByUser: false, reviewedAt, authority: "context-only" },
628
- authority: "context-only"
629
- };
630
- state.records = state.records.map((item) => item.event.id === id ? rejected : item);
631
- return { record: rejected, sharingPath };
632
- }
633
- if (!confirmedByUser) throw new Error("acceptance requires explicit local user confirmation");
634
- if (record.authentication) {
635
- const proofFindings = sharingAuthenticationFindings({ records: [record], history: [] }, acceptedTrust);
636
- if (proofFindings.length) throw new Error(`signed shared context failed verification: ${proofFindings.join(", ")}`);
637
- const trusted = acceptedTrust.records.find((item) => item.keyId === record.authentication.keyId);
638
- if (!trusted || trusted.status !== "trusted" || trusted.signerId !== record.authentication.signerId) {
639
- throw new Error("signed shared context must still have a trusted signer at local review time");
640
- }
641
- }
642
- validateLocalScope(record.event, graph);
643
- const newer = state.records.find((item) => item.status === "accepted" && item.event.supersedesEventId === id);
644
- if (newer) throw new Error(`shared event is already superseded by accepted event: ${newer.event.id}`);
645
- const prior = record.event.supersedesEventId
646
- ? state.records.find((item) => item.event.id === record.event.supersedesEventId)
647
- : null;
648
- if (record.event.supersedesEventId && prior?.status !== "accepted") {
649
- throw new Error("a shared predecessor must be locally accepted before its replacement");
650
- }
651
- if (prior && (prior.event.kind !== record.event.kind || prior.event.subjectId !== record.event.subjectId
652
- || prior.event.privacy !== record.event.privacy || prior.event.groupId !== record.event.groupId)) {
653
- throw new Error("a shared replacement must keep kind, subject, and privacy scope");
654
- }
655
- if (prior) {
656
- preserve(state, prior, reviewedAt);
657
- state.records = state.records.map((item) => item.event.id === prior.event.id
658
- ? { ...item, status: "superseded", authority: "context-only" }
659
- : item);
660
- }
661
- const accepted = {
662
- ...record,
663
- status: "accepted",
664
- review: { decision, reason: reviewReason, confirmedByUser: true, reviewedAt, authority: "context-only" },
665
- acceptedAt: reviewedAt,
666
- supersededIds: prior ? [prior.event.id] : [],
667
- authority: "context-only"
668
- };
669
- state.records = state.records.map((item) => item.event.id === id ? accepted : item);
670
- return { record: accepted, sharingPath };
671
- });
672
- }
673
-
674
- export async function rollbackShared({ root = process.cwd(), id, reason, now = new Date() }) {
675
- if (!ID_RE.test(id || "")) throw new Error("shared event id is required");
676
- const rollbackReason = safeText(reason, "reason", 500);
677
- const rolledBackAt = date(now, "now");
678
- return mutation(root, (state, _catalog, sharingPath) => {
679
- const record = state.records.find((item) => item.event.id === id);
680
- if (!record || record.status !== "accepted") throw new Error("only accepted shared context can be rolled back");
681
- preserve(state, record, rolledBackAt);
682
- const restored = [];
683
- for (const previousId of record.supersededIds || []) {
684
- const previous = state.records.find((item) => item.event.id === previousId && item.status === "superseded");
685
- if (!previous) continue;
686
- preserve(state, previous, rolledBackAt);
687
- state.records = state.records.map((item) => item.event.id === previousId
688
- ? { ...item, status: "accepted", authority: "context-only" }
689
- : item);
690
- restored.push(previousId);
691
- }
692
- const rolledBack = {
693
- ...record,
694
- status: "rolled-back",
695
- rollback: { reason: rollbackReason, rolledBackAt, authority: "context-only" },
696
- authority: "context-only"
697
- };
698
- state.records = state.records.map((item) => item.event.id === id ? rolledBack : item);
699
- return { record: rolledBack, restored, sharingPath };
700
- });
701
- }
702
-
703
- function groupAudience(graph, groupId, includePrivate) {
704
- const ids = new Set();
705
- if (!groupId) return ids;
706
- ids.add(groupId);
707
- for (const edge of graph.entityEdges) {
708
- if (edge.relation !== "member-of" || (!includePrivate && edge.privacy === "private")) continue;
709
- if (edge.to === groupId) ids.add(edge.from);
710
- if (edge.from === groupId) ids.add(edge.to);
711
- }
712
- return ids;
713
- }
714
-
715
- function visible(record, entities, audience, includePrivate, groupId) {
716
- const event = record.event;
717
- if (event.privacy === "group" && (!groupId || event.groupId !== groupId)) return false;
718
- const subject = event.subjectId ? entities.get(event.subjectId) : null;
719
- if (subject?.privacy === "private" && !includePrivate) return false;
720
- if (subject?.privacy === "group" && !audience.has(subject.id)) return false;
721
- if (event.privacy === "group" && event.subjectId && !audience.has(event.subjectId)) return false;
722
- return true;
723
- }
724
-
725
- export async function sharedContext({
726
- root = process.cwd(), scopeId = null, groupId = null, includePrivate = false,
727
- kinds = null, subjectIds = null, maxItems = null, catalog: providedCatalog = null
728
- } = {}) {
729
- const catalog = providedCatalog || await buildCatalog(root);
730
- const { sharing } = await loadSharing(catalog.root, catalog);
731
- const { graph } = await loadGraph(catalog.root, catalog);
732
- const { trust } = await loadTrust(catalog.root, catalog);
733
- const findings = sharingFindings(sharing, graph);
734
- if (findings.length) throw new Error(`sharing state failed closed: ${findings.join(", ")}`);
735
- const authenticationFindings = sharingAuthenticationFindings(sharing, trust);
736
- if (authenticationFindings.length) throw new Error(`shared authentication failed closed: ${authenticationFindings.join(", ")}`);
737
- if (scopeId !== null && !ID_RE.test(scopeId || "")) throw new Error("scopeId is invalid");
738
- const entities = new Map(graph.entities.map((entity) => [entity.id, entity]));
739
- if (groupId !== null) {
740
- const group = entities.get(groupId);
741
- if (!group || group.kind !== "group") throw new Error(`unknown group entity: ${groupId}`);
742
- }
743
- const kindFilter = kinds === null ? null : new Set(kinds);
744
- if (kindFilter && [...kindFilter].some((kind) => !KINDS.has(kind))) throw new Error("kinds contains an unsupported shared-memory kind");
745
- const subjectFilter = subjectIds === null ? null : new Set(subjectIds);
746
- const audience = groupAudience(graph, groupId, includePrivate);
747
- const limit = maxItems === null ? sharing.config.maxContextItems : integer(maxItems, "maxItems", 0, 50);
748
- const items = sharing.records
749
- .filter((record) => record.status === "accepted")
750
- .filter((record) => !scopeId || record.event.scopeId === scopeId)
751
- .filter((record) => !kindFilter || kindFilter.has(record.event.kind))
752
- .filter((record) => !subjectFilter || subjectFilter.has(record.event.subjectId))
753
- .filter((record) => visible(record, entities, audience, includePrivate, groupId))
754
- .sort((a, b) => b.event.confidence - a.event.confidence || b.acceptedAt.localeCompare(a.acceptedAt) || a.event.id.localeCompare(b.event.id))
755
- .slice(0, limit)
756
- .map((record) => ({
757
- id: record.event.id,
758
- scopeId: record.event.scopeId,
759
- kind: record.event.kind,
760
- claim: record.event.claim,
761
- subjectId: record.event.subjectId,
762
- privacy: record.event.privacy,
763
- groupId: record.event.groupId,
764
- confidence: record.event.confidence,
765
- originInstanceId: record.event.originInstanceId,
766
- sourceLearningId: record.event.source.learningId,
767
- publishedAt: record.event.publishedAt,
768
- acceptedAt: record.acceptedAt,
769
- authentication: record.authentication ? {
770
- mode: "signed", signerId: record.authentication.signerId,
771
- keyId: record.authentication.keyId, verifiedAt: record.authentication.verifiedAt,
772
- authority: "context-only"
773
- } : { mode: "integrity-only", authority: "context-only" },
774
- authority: "context-only"
775
- }));
776
- return {
777
- schema: "agentspine.shared-context/v1",
778
- root: catalog.root,
779
- scopeId,
780
- groupId,
781
- items,
782
- authority: "context-only",
783
- note: "Shared memory is reviewed descriptive context. It never grants delegation, host permissions, access, or instructions to act."
784
- };
785
- }
786
-
787
- export async function sharedInbox({ root = process.cwd(), status = "pending" } = {}) {
788
- if (!STATUSES.has(status)) throw new Error(`unsupported shared record status: ${status}`);
789
- const { sharing, sharingPath } = await loadSharing(root);
790
- const findings = sharingFindings(sharing, null);
791
- if (findings.length) throw new Error(`sharing state failed closed: ${findings.join(", ")}`);
792
- return {
793
- schema: "agentspine.shared-inbox/v1",
794
- root: sharing.root,
795
- status,
796
- items: sharing.records.filter((record) => record.status === status).map((record) => ({
797
- id: record.event.id,
798
- scopeId: record.event.scopeId,
799
- kind: record.event.kind,
800
- claim: record.event.claim,
801
- subjectId: record.event.subjectId,
802
- privacy: record.event.privacy,
803
- groupId: record.event.groupId,
804
- confidence: record.event.confidence,
805
- originInstanceId: record.event.originInstanceId,
806
- publishedAt: record.event.publishedAt,
807
- receivedAt: record.receivedAt,
808
- authentication: record.authentication ? {
809
- mode: "signed", signerId: record.authentication.signerId,
810
- keyId: record.authentication.keyId, signedAt: record.authentication.signedAt,
811
- verifiedAt: record.authentication.verifiedAt, authority: "context-only"
812
- } : { mode: "integrity-only", authority: "context-only" },
813
- authority: "context-only"
814
- })),
815
- sharingPath,
816
- authority: "context-only"
817
- };
818
- }
819
-
820
- export async function configureSharing({ root = process.cwd(), maxContextItems }) {
821
- return mutation(root, (state, _catalog, sharingPath) => {
822
- state.config.maxContextItems = integer(maxContextItems, "maxContextItems", 1, 50);
823
- return { config: state.config, sharingPath };
824
- });
825
- }
826
-
827
- export async function deleteShared({ root = process.cwd(), id, confirmation }) {
828
- requireConfirmation(confirmation);
829
- if (!ID_RE.test(id || "")) throw new Error("shared event id is required");
830
- return mutation(root, (state, _catalog, sharingPath) => {
831
- const record = state.records.find((item) => item.event.id === id);
832
- if (record?.status === "accepted" && record.supersededIds?.length) {
833
- throw new Error("roll back accepted superseding shared context before permanent deletion");
834
- }
835
- if (state.records.some((item) => item.status === "accepted" && item.supersededIds?.includes(id))) {
836
- throw new Error("roll back accepted superseding shared context before deleting its predecessor");
837
- }
838
- const existed = Boolean(record);
839
- state.records = state.records.filter((item) => item.event.id !== id);
840
- state.history = state.history.filter((entry) => entry.recordId !== id && entry.value?.event?.id !== id);
841
- return { deleted: existed, id, sharingPath };
842
- });
843
- }
844
-
845
- export function sharingFindings(sharing, graph = null) {
846
- const findings = [];
847
- if (!exactKeys(sharing, ["schema", "root", "instanceId", "config", "records", "history"])
848
- || !exactKeys(sharing.config, ["maxContextItems"])
849
- || sharing.schema !== "agentspine.sharing/v1" || !ID_RE.test(sharing.instanceId || "") || !validConfig(sharing.config)) findings.push("invalid-sharing-state");
850
- if (new Set(sharing.records.map((record) => record.event?.id)).size !== sharing.records.length) findings.push("duplicate-shared-event");
851
- const records = [
852
- ...sharing.records.map((record) => ({ record, current: true })),
853
- ...sharing.history.map((entry) => entry.value).filter(Boolean).map((record) => ({ record, current: false }))
854
- ];
855
- for (const { record, current } of records) {
856
- try { validateSharedEvent(record.event); } catch { findings.push(`invalid-shared-event:${record.event?.id || "unknown"}`); }
857
- const allowedRecordKeys = new Set(["event", "authentication", "adapterId", "receivedAt", "status", "review", "acceptedAt", "supersededIds", "rollback", "authority"]);
858
- if (Object.keys(record).some((key) => !allowedRecordKeys.has(key))
859
- || !STATUSES.has(record.status) || record.authority !== "context-only" || !ID_RE.test(record.adapterId || "")
860
- || !validDateValue(record.receivedAt) || !Array.isArray(record.supersededIds)
861
- || record.supersededIds.some((id) => !ID_RE.test(id || ""))) findings.push(`invalid-shared-record:${record.event?.id || "unknown"}`);
862
- if (!validateAuthentication(record.authentication)) findings.push(`invalid-shared-authentication:${record.event?.id || "unknown"}`);
863
- const nested = [record.review, record.rollback].filter(Boolean);
864
- if (nested.some((item) => item.authority !== "context-only")) findings.push(`shared-authority:${record.event?.id || "unknown"}`);
865
- if (nested.some((item) => SECRET_RE.test(item.reason || ""))) findings.push(`unsafe-shared-review:${record.event?.id || "unknown"}`);
866
- const validReview = record.review === null || (
867
- exactKeys(record.review, ["decision", "reason", "confirmedByUser", "reviewedAt", "authority"])
868
- && new Set(["accept", "reject"]).has(record.review.decision)
869
- && typeof record.review.reason === "string" && record.review.reason.length > 0
870
- && typeof record.review.confirmedByUser === "boolean"
871
- && validDateValue(record.review.reviewedAt)
872
- && record.review.authority === "context-only"
873
- );
874
- const validRollback = record.rollback === undefined || (
875
- exactKeys(record.rollback, ["reason", "rolledBackAt", "authority"])
876
- && typeof record.rollback.reason === "string" && record.rollback.reason.length > 0
877
- && validDateValue(record.rollback.rolledBackAt)
878
- && record.rollback.authority === "context-only"
879
- );
880
- if (!validReview || !validRollback) findings.push(`invalid-shared-review:${record.event?.id || "unknown"}`);
881
- if (record.status === "pending" && (record.review !== null || record.acceptedAt !== null || record.supersededIds.length)) findings.push(`invalid-shared-pending:${record.event?.id || "unknown"}`);
882
- if (record.status === "rejected" && (record.review?.decision !== "reject" || record.review?.confirmedByUser !== false || record.acceptedAt !== null)) findings.push(`invalid-shared-rejection:${record.event?.id || "unknown"}`);
883
- if (record.status === "accepted" && (record.review?.decision !== "accept" || record.review?.confirmedByUser !== true || !record.acceptedAt)) {
884
- findings.push(`invalid-shared-acceptance:${record.event?.id || "unknown"}`);
885
- }
886
- if (["accepted", "superseded", "rolled-back"].includes(record.status) && !validDateValue(record.acceptedAt)) findings.push(`invalid-shared-accepted-date:${record.event?.id || "unknown"}`);
887
- if (record.status === "rolled-back" && !record.rollback) findings.push(`invalid-shared-rollback:${record.event?.id || "unknown"}`);
888
- if (graph && current && record.status === "accepted") {
889
- const event = record.event || {};
890
- const subject = event.subjectId ? graph.entities.find((entity) => entity.id === event.subjectId) : null;
891
- if (event.subjectId && !subject) findings.push(`unknown-shared-subject:${event.id || "unknown"}`);
892
- if (event.privacy === "group") {
893
- const group = graph.entities.find((entity) => entity.id === event.groupId && entity.kind === "group");
894
- const member = !event.subjectId || graph.entityEdges.some((edge) => edge.relation === "member-of" && edge.privacy !== "private" && (
895
- (edge.from === event.subjectId && edge.to === event.groupId) || (edge.to === event.subjectId && edge.from === event.groupId)
896
- ));
897
- if (!group || !member) findings.push(`invalid-shared-group:${event.id || "unknown"}`);
898
- }
899
- }
900
- }
901
- const accepted = new Set(sharing.records.filter((record) => record.status === "accepted").map((record) => record.event.id));
902
- for (const record of sharing.records.filter((item) => item.status === "accepted" && item.event.supersedesEventId)) {
903
- if (accepted.has(record.event.supersedesEventId)) findings.push(`active-shared-supersession:${record.event.id}`);
904
- }
905
- for (const record of sharing.records) {
906
- if (record.status === "accepted") {
907
- const expected = record.event.supersedesEventId === null ? [] : [record.event.supersedesEventId];
908
- if (record.supersededIds.length !== expected.length
909
- || record.supersededIds.some((id, index) => id !== expected[index])) {
910
- findings.push(`invalid-shared-supersession:${record.event.id}`);
911
- }
912
- }
913
- if (record.status === "superseded" && !sharing.records.some(
914
- (item) => item.status === "accepted" && item.supersededIds.includes(record.event.id)
915
- )) findings.push(`orphaned-shared-supersession:${record.event.id}`);
916
- }
917
- if (sharing.history.some((entry) => !exactKeys(entry, ["kind", "recordId", "supersededAt", "privacy", "value", "authority"])
918
- || entry.kind !== "shared-record" || entry.recordId !== entry.value?.event?.id
919
- || entry.privacy !== entry.value?.event?.privacy || !validDateValue(entry.supersededAt)
920
- || entry.authority !== "context-only" || entry.value?.authority !== "context-only")) findings.push("invalid-sharing-history");
921
- return [...new Set(findings)];
922
- }
923
-
924
- export function sharingAuthenticationFindings(sharing, trust) {
925
- const findings = [];
926
- const trustIssues = trustFindings(trust);
927
- if (trustIssues.length) return trustIssues;
928
- const records = [
929
- ...sharing.records.map((record) => ({ record, current: true })),
930
- ...sharing.history.map((entry) => entry.value).filter(Boolean).map((record) => ({ record, current: false }))
931
- ];
932
- for (const { record, current } of records.filter((item) => item.record.authentication)) {
933
- const trusted = trust.records.find((item) => item.keyId === record.authentication.keyId);
934
- if (!trusted || trusted.signerId !== record.authentication.signerId) {
935
- findings.push(`missing-shared-signing-key:${record.event.id}`);
936
- continue;
937
- }
938
- try {
939
- verifyEnvelope({
940
- schema: "agentspine.signed-envelope/v1",
941
- kind: "event",
942
- signer: trusted.publicIdentity,
943
- payload: record.event,
944
- signedAt: record.authentication.signedAt,
945
- authority: "context-only",
946
- signature: record.authentication.signature
947
- }, "event");
948
- } catch {
949
- findings.push(`invalid-stored-shared-signature:${record.event.id}`);
950
- }
951
- if (current && record.status === "accepted" && trusted.status !== "trusted") {
952
- findings.push(`untrusted-accepted-shared-event:${record.event.id}`);
953
- }
954
- }
955
- return [...new Set(findings)];
956
- }
957
-
958
- function validateLocalScope(event, graph) {
959
- if (event.subjectId && !graph.entities.some((entity) => entity.id === event.subjectId)) {
960
- throw new Error(`unknown shared subject entity: ${event.subjectId}`);
961
- }
962
- if (event.privacy !== "group") return;
963
- const group = graph.entities.find((entity) => entity.id === event.groupId && entity.kind === "group");
964
- if (!group) throw new Error(`unknown shared group entity: ${event.groupId}`);
965
- const member = !event.subjectId || graph.entityEdges.some((edge) => edge.relation === "member-of" && edge.privacy !== "private" && (
966
- (edge.from === event.subjectId && edge.to === event.groupId) || (edge.to === event.subjectId && edge.from === event.groupId)
967
- ));
968
- if (!member) throw new Error(`shared subject is not a visible member of group: ${event.groupId}`);
969
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink, writeFile
4
+ } from "node:fs/promises";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { buildCatalog } from "./catalog.js";
7
+ import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
8
+ import { loadGraph } from "./graph.js";
9
+ import { learningFindings, loadLearning } from "./learning.js";
10
+ import { isInside, projectStateDir } from "./paths.js";
11
+ import {
12
+ assertTrustedIdentity, loadTrust, signEnvelope, trustFindings, verifyEnvelope
13
+ } from "./authentication.js";
14
+
15
+ const KINDS = new Set(["preference", "no-go", "goal", "correction", "personal-fact", "project-fact", "reference"]);
16
+ const PRIVACY = new Set(["shared", "group"]);
17
+ const STATUSES = new Set(["pending", "accepted", "rejected", "superseded", "rolled-back"]);
18
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
19
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
20
+ const MAX_STATE_BYTES = 5 * 1024 * 1024;
21
+ const MAX_EVENT_BYTES = 64 * 1024;
22
+ const MAX_ADAPTER_EVENTS = 2000;
23
+ const MAX_ADAPTER_BYTES = 20 * 1024 * 1024;
24
+ const CONFIRMATION = "local-share-confirmed";
25
+ const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/i;
26
+ const AUTHORITY_RE = /\b(?:user|agent|person|they|he|she|i|ich|wir|nutzer|benutzer).{0,60}\b(?:may|can|is allowed|is authorized|has|have|darf|berechtigt|hat|haben).{0,50}\b(?:admin(?:istrator)?|permissions?|rights?|authorization|production access|deploy|billing|spending|policy exception|bypass|zugang|rechte|berechtigung|produktion|abrechnung|ausnahme|umgehen)\b/i;
27
+
28
+ function canonical(value) {
29
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
30
+ if (value && typeof value === "object") {
31
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
32
+ }
33
+ return JSON.stringify(value);
34
+ }
35
+
36
+ function digest(value) {
37
+ return createHash("sha256").update(canonical(value)).digest("hex");
38
+ }
39
+
40
+ function withoutDigest(value) {
41
+ const { digest: _digest, ...rest } = value;
42
+ return rest;
43
+ }
44
+
45
+ function exactKeys(value, expected) {
46
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
47
+ const actual = Object.keys(value).sort();
48
+ const wanted = [...expected].sort();
49
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
50
+ }
51
+
52
+ function safeText(value, field, maximum) {
53
+ if (!value || typeof value !== "string") throw new Error(`${field} is required`);
54
+ const result = value.trim().slice(0, maximum);
55
+ if (!result) throw new Error(`${field} is required`);
56
+ if (SECRET_RE.test(result)) throw new Error(`${field} appears to contain a secret and cannot enter shared memory`);
57
+ return result;
58
+ }
59
+
60
+ function safeClaim(value) {
61
+ const result = safeText(value, "claim", 1000);
62
+ if (AUTHORITY_RE.test(result)) throw new Error("authority and access claims cannot enter shared memory");
63
+ return result;
64
+ }
65
+
66
+ function date(value, field = "date") {
67
+ const parsed = value instanceof Date ? value : new Date(value);
68
+ if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
69
+ return parsed.toISOString();
70
+ }
71
+
72
+ function validDateValue(value) {
73
+ return typeof value === "string" && value.length > 0 && Number.isFinite(new Date(value).getTime());
74
+ }
75
+
76
+ function integer(value, field, minimum, maximum) {
77
+ const result = Number(value);
78
+ if (!Number.isInteger(result) || result < minimum || result > maximum) throw new Error(`${field} must be an integer between ${minimum} and ${maximum}`);
79
+ return result;
80
+ }
81
+
82
+ function emptySharing(root) {
83
+ return {
84
+ schema: "agentspine.sharing/v1",
85
+ root,
86
+ instanceId: `instance:${randomUUID()}`,
87
+ config: { maxContextItems: 12 },
88
+ records: [],
89
+ history: []
90
+ };
91
+ }
92
+
93
+ function normalizeSharing(value, root) {
94
+ if (!value || typeof value !== "object" || Array.isArray(value)
95
+ || value.schema !== "agentspine.sharing/v1" || value.root !== root
96
+ || !ID_RE.test(value.instanceId || "") || !value.config || typeof value.config !== "object"
97
+ || !Array.isArray(value.records) || !value.records.every((item) => item && typeof item === "object")
98
+ || !Array.isArray(value.history) || !value.history.every((item) => item && typeof item === "object")) {
99
+ throw new Error("sharing state structure is invalid; run the audit before using shared memory");
100
+ }
101
+ return value;
102
+ }
103
+
104
+ function validConfig(config) {
105
+ return Number.isInteger(config?.maxContextItems) && config.maxContextItems >= 1 && config.maxContextItems <= 50;
106
+ }
107
+
108
+ async function readJson(path, root) {
109
+ try {
110
+ const metadata = await stat(path);
111
+ if (metadata.size > MAX_STATE_BYTES) throw new Error("sharing state exceeds the 5 MiB read limit");
112
+ return normalizeSharing(JSON.parse(await readFile(path, "utf8")), root);
113
+ } catch (error) {
114
+ if (error.code !== "ENOENT") throw error;
115
+ return emptySharing(root);
116
+ }
117
+ }
118
+
119
+ async function writeJson(path, state) {
120
+ const content = `${JSON.stringify(state, null, 2)}\n`;
121
+ if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("sharing state exceeds 5 MiB; reject or delete old imports first");
122
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
123
+ await writeFile(temporary, content, { mode: 0o600 });
124
+ try {
125
+ await replaceFileWithRetry(temporary, path);
126
+ } catch (error) {
127
+ await unlink(temporary).catch((cleanupError) => {
128
+ if (cleanupError.code !== "ENOENT") error.cleanupError = cleanupError;
129
+ });
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ async function acquireLock(path) {
135
+ const lockPath = `${path}.lock`;
136
+ for (let attempt = 0; attempt < 80; attempt += 1) {
137
+ try {
138
+ return { handle: await open(lockPath, "wx", 0o600), lockPath };
139
+ } catch (error) {
140
+ if (!isFileLockContention(error)) throw error;
141
+ try {
142
+ const metadata = await stat(lockPath);
143
+ if (Date.now() - metadata.mtimeMs > 15000) await unlink(lockPath);
144
+ } catch (lockError) {
145
+ if (lockError.code !== "ENOENT") throw lockError;
146
+ }
147
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 25));
148
+ }
149
+ }
150
+ throw new Error("sharing state is busy; retry shortly");
151
+ }
152
+
153
+ async function withLock(path, read, task, save = true) {
154
+ const { handle, lockPath } = await acquireLock(path);
155
+ try {
156
+ const value = await read();
157
+ const result = await task(value);
158
+ if (save) await writeJson(path, value);
159
+ return result;
160
+ } finally {
161
+ await handle.close();
162
+ await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
163
+ }
164
+ }
165
+
166
+ async function sharingPaths(root, providedCatalog = null) {
167
+ const catalog = providedCatalog || await buildCatalog(root);
168
+ const directory = await projectStateDir(catalog.root);
169
+ return { catalog, sharingPath: join(directory, "sharing.json") };
170
+ }
171
+
172
+ export async function loadSharing(root = process.cwd(), providedCatalog = null) {
173
+ const paths = await sharingPaths(root, providedCatalog);
174
+ return { sharing: await readJson(paths.sharingPath, paths.catalog.root), ...paths };
175
+ }
176
+
177
+ export async function inspectSharing(root = process.cwd(), providedCatalog = null) {
178
+ const paths = await sharingPaths(root, providedCatalog);
179
+ try {
180
+ return { sharing: await readJson(paths.sharingPath, paths.catalog.root), error: null, ...paths };
181
+ } catch (error) {
182
+ return { sharing: emptySharing(paths.catalog.root), error: error.message, ...paths };
183
+ }
184
+ }
185
+
186
+ function preserve(state, value, now) {
187
+ state.history.push({
188
+ kind: "shared-record",
189
+ recordId: value.event.id,
190
+ supersededAt: now,
191
+ privacy: value.event.privacy,
192
+ value: { ...value, authority: "context-only" },
193
+ authority: "context-only"
194
+ });
195
+ }
196
+
197
+ async function mutation(root, operation) {
198
+ const paths = await sharingPaths(root);
199
+ const { graph } = await loadGraph(paths.catalog.root, paths.catalog);
200
+ return withLock(
201
+ paths.sharingPath,
202
+ () => readJson(paths.sharingPath, paths.catalog.root),
203
+ async (state) => {
204
+ if (!validConfig(state.config)) throw new Error("sharing configuration is invalid; run the audit before using shared memory");
205
+ const findings = sharingFindings(state, graph);
206
+ if (findings.length) throw new Error(`sharing state failed closed: ${findings.join(", ")}`);
207
+ const result = await operation(state, paths.catalog, paths.sharingPath, graph);
208
+ const resultFindings = sharingFindings(state, graph);
209
+ if (resultFindings.length) throw new Error(`sharing mutation failed closed: ${resultFindings.join(", ")}`);
210
+ return result;
211
+ }
212
+ );
213
+ }
214
+
215
+ function requireConfirmation(confirmation) {
216
+ if (confirmation !== CONFIRMATION) throw new Error("shared adapter writes require explicit local owner confirmation");
217
+ }
218
+
219
+ function manifestBody({ adapterId, scopeId, createdAt }) {
220
+ return {
221
+ schema: "agentspine.directory-adapter/v1",
222
+ adapter: "directory",
223
+ adapterId,
224
+ scopeId,
225
+ createdAt,
226
+ authority: "context-only"
227
+ };
228
+ }
229
+
230
+ function validateManifest(manifest) {
231
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)
232
+ || !exactKeys(manifest, ["schema", "adapter", "adapterId", "scopeId", "createdAt", "authority", "digest"])
233
+ || manifest.schema !== "agentspine.directory-adapter/v1" || manifest.adapter !== "directory"
234
+ || !ID_RE.test(manifest.adapterId || "") || !ID_RE.test(manifest.scopeId || "")
235
+ || manifest.authority !== "context-only" || !validDateValue(manifest.createdAt)
236
+ || !DIGEST_RE.test(manifest.digest || "") || digest(withoutDigest(manifest)) !== manifest.digest) {
237
+ throw new Error("directory adapter manifest is invalid or has failed its integrity check");
238
+ }
239
+ return manifest;
240
+ }
241
+
242
+ function validateAuthentication(authentication) {
243
+ return authentication === null || authentication === undefined || (
244
+ exactKeys(authentication, ["mode", "signerId", "keyId", "signedAt", "verifiedAt", "signature", "authority"])
245
+ && authentication.mode === "signed" && ID_RE.test(authentication.signerId || "")
246
+ && ID_RE.test(authentication.keyId || "") && validDateValue(authentication.signedAt)
247
+ && validDateValue(authentication.verifiedAt) && typeof authentication.signature === "string"
248
+ && authentication.signature.length >= 80 && authentication.signature.length <= 96
249
+ && authentication.authority === "context-only"
250
+ );
251
+ }
252
+
253
+ async function assertRegularFile(path, field) {
254
+ const metadata = await lstat(path);
255
+ if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`${field} must be a regular file, not a symlink`);
256
+ return metadata;
257
+ }
258
+
259
+ async function adapterLocation(root, directory, create = false) {
260
+ if (!directory || typeof directory !== "string") throw new Error("adapter directory is required");
261
+ const target = resolve(directory);
262
+ if (isInside(root, target)) throw new Error("shared adapter directory must remain outside the scanned project");
263
+ if (create) {
264
+ let ancestor = target;
265
+ const missing = [];
266
+ while (true) {
267
+ try {
268
+ ancestor = await realpath(ancestor);
269
+ break;
270
+ } catch (error) {
271
+ if (error.code !== "ENOENT") throw error;
272
+ const parent = dirname(ancestor);
273
+ if (parent === ancestor) throw error;
274
+ missing.push(basename(ancestor));
275
+ ancestor = parent;
276
+ }
277
+ }
278
+ const prospective = resolve(ancestor, ...missing.reverse());
279
+ if (isInside(root, prospective)) throw new Error("shared adapter directory must remain outside the scanned project");
280
+ await mkdir(target, { recursive: true, mode: 0o700 });
281
+ }
282
+ const canonicalDirectory = await realpath(target);
283
+ if (isInside(root, canonicalDirectory)) throw new Error("shared adapter directory must remain outside the scanned project");
284
+ return canonicalDirectory;
285
+ }
286
+
287
+ async function openAdapter(root, directory) {
288
+ const adapterDirectory = await adapterLocation(root, directory, false);
289
+ const manifestPath = join(adapterDirectory, ".agentspine-exchange.json");
290
+ const metadata = await assertRegularFile(manifestPath, "adapter manifest");
291
+ if (metadata.size > MAX_EVENT_BYTES) throw new Error("adapter manifest exceeds 64 KiB");
292
+ const manifestDocument = JSON.parse(await readFile(manifestPath, "utf8"));
293
+ let manifest;
294
+ let authentication = null;
295
+ let publicIdentity = null;
296
+ if (manifestDocument?.schema === "agentspine.signed-envelope/v1") {
297
+ const verified = verifyEnvelope(manifestDocument, "manifest");
298
+ manifest = validateManifest(verified.payload);
299
+ authentication = verified.authentication;
300
+ publicIdentity = verified.publicIdentity;
301
+ } else {
302
+ manifest = validateManifest(manifestDocument);
303
+ }
304
+ const eventsDirectory = join(adapterDirectory, "events");
305
+ const eventsMetadata = await lstat(eventsDirectory);
306
+ if (!eventsMetadata.isDirectory() || eventsMetadata.isSymbolicLink()) throw new Error("adapter events path must be a real directory");
307
+ return {
308
+ adapterDirectory, manifestPath, manifest, manifestDocument,
309
+ eventsDirectory, authentication, publicIdentity
310
+ };
311
+ }
312
+
313
+ export async function initDirectoryAdapter({
314
+ root = process.cwd(), directory, scopeId, adapterId = `adapter:${randomUUID()}`,
315
+ signerId = null, confirmation, now = new Date()
316
+ }) {
317
+ requireConfirmation(confirmation);
318
+ if (!ID_RE.test(scopeId || "")) throw new Error("scopeId must be a stable, whitespace-free identifier");
319
+ if (!ID_RE.test(adapterId || "")) throw new Error("adapterId must be a stable, whitespace-free identifier");
320
+ const catalog = await buildCatalog(root);
321
+ const adapterDirectory = await adapterLocation(catalog.root, directory, true);
322
+ const manifestPath = join(adapterDirectory, ".agentspine-exchange.json");
323
+ const eventsDirectory = join(adapterDirectory, "events");
324
+ await mkdir(eventsDirectory, { recursive: true, mode: 0o700 });
325
+ const lockTarget = join(adapterDirectory, ".agentspine-adapter");
326
+ return withLock(lockTarget, async () => ({}), async () => {
327
+ try {
328
+ await assertRegularFile(manifestPath, "adapter manifest");
329
+ const existingDocument = JSON.parse(await readFile(manifestPath, "utf8"));
330
+ const verified = existingDocument?.schema === "agentspine.signed-envelope/v1"
331
+ ? verifyEnvelope(existingDocument, "manifest") : null;
332
+ const existing = validateManifest(verified ? verified.payload : existingDocument);
333
+ if (existing.scopeId !== scopeId) throw new Error(`adapter already belongs to scope ${existing.scopeId}`);
334
+ if (signerId && verified?.publicIdentity.signerId !== signerId) throw new Error("adapter is signed by a different identity");
335
+ return {
336
+ created: false, manifest: existing, signed: Boolean(verified),
337
+ signer: verified?.publicIdentity || null, adapterDirectory, manifestPath
338
+ };
339
+ } catch (error) {
340
+ if (error.code !== "ENOENT") throw error;
341
+ }
342
+ const body = manifestBody({ adapterId, scopeId, createdAt: date(now, "now") });
343
+ const manifest = { ...body, digest: digest(body) };
344
+ const document = signerId
345
+ ? await signEnvelope({ root: catalog.root, signerId, kind: "manifest", payload: manifest, now })
346
+ : manifest;
347
+ const temporary = `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
348
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
349
+ await rename(temporary, manifestPath);
350
+ return {
351
+ created: true, manifest, signed: Boolean(signerId),
352
+ signer: document.signer || null, adapterDirectory, manifestPath
353
+ };
354
+ }, false);
355
+ }
356
+
357
+ function eventBody({
358
+ id, scopeId, originInstanceId, candidate, supersedesEventId, publishedAt
359
+ }) {
360
+ const proof = candidate.review || candidate.promotion;
361
+ return {
362
+ schema: "agentspine.shared-event/v1",
363
+ id,
364
+ scopeId,
365
+ originInstanceId,
366
+ kind: candidate.kind,
367
+ claim: candidate.claim,
368
+ subjectId: candidate.subjectId,
369
+ privacy: candidate.privacy,
370
+ groupId: candidate.groupId,
371
+ confidence: candidate.confidence,
372
+ source: {
373
+ type: "accepted-learning",
374
+ learningId: candidate.id,
375
+ acceptedAt: candidate.acceptedAt,
376
+ automatic: candidate.automatic,
377
+ evidenceCount: candidate.evidence.length,
378
+ reviewDigest: digest(proof || {})
379
+ },
380
+ supersedesEventId,
381
+ publishedAt,
382
+ authority: "context-only"
383
+ };
384
+ }
385
+
386
+ export function validateSharedEvent(event, expectedScopeId = null) {
387
+ if (!event || typeof event !== "object" || Array.isArray(event)
388
+ || !exactKeys(event, [
389
+ "schema", "id", "scopeId", "originInstanceId", "kind", "claim", "subjectId", "privacy",
390
+ "groupId", "confidence", "source", "supersedesEventId", "publishedAt", "authority", "digest"
391
+ ])
392
+ || !exactKeys(event.source, ["type", "learningId", "acceptedAt", "automatic", "evidenceCount", "reviewDigest"])
393
+ || event.schema !== "agentspine.shared-event/v1" || !ID_RE.test(event.id || "")
394
+ || !ID_RE.test(event.scopeId || "") || !ID_RE.test(event.originInstanceId || "")
395
+ || (expectedScopeId !== null && event.scopeId !== expectedScopeId)
396
+ || !KINDS.has(event.kind) || !PRIVACY.has(event.privacy)
397
+ || typeof event.claim !== "string" || !event.claim.trim() || event.claim.length > 1000
398
+ || (event.subjectId !== null && !ID_RE.test(event.subjectId || ""))
399
+ || (event.privacy === "group" && !ID_RE.test(event.groupId || ""))
400
+ || (event.privacy !== "group" && event.groupId !== null)
401
+ || !Number.isFinite(event.confidence) || event.confidence < 0 || event.confidence > 1
402
+ || event.source?.type !== "accepted-learning" || !ID_RE.test(event.source?.learningId || "")
403
+ || !validDateValue(event.source?.acceptedAt)
404
+ || typeof event.source?.automatic !== "boolean" || !Number.isInteger(event.source?.evidenceCount) || event.source.evidenceCount < 1
405
+ || !DIGEST_RE.test(event.source?.reviewDigest || "")
406
+ || (event.supersedesEventId !== null && !ID_RE.test(event.supersedesEventId || ""))
407
+ || !validDateValue(event.publishedAt) || event.authority !== "context-only"
408
+ || !DIGEST_RE.test(event.digest || "") || digest(withoutDigest(event)) !== event.digest) {
409
+ throw new Error(`shared event is invalid or has failed its integrity check: ${event?.id || "unknown"}`);
410
+ }
411
+ if (SECRET_RE.test(event.claim) || AUTHORITY_RE.test(event.claim)) throw new Error(`shared event contains unsafe context: ${event.id}`);
412
+ return event;
413
+ }
414
+
415
+ function eventFilename(id) {
416
+ return `${createHash("sha256").update(id).digest("hex")}.json`;
417
+ }
418
+
419
+ async function readEventFile(path, scopeId) {
420
+ const metadata = await assertRegularFile(path, "shared event");
421
+ if (metadata.size > MAX_EVENT_BYTES) throw new Error("shared event exceeds 64 KiB");
422
+ const document = JSON.parse(await readFile(path, "utf8"));
423
+ if (document?.schema === "agentspine.signed-envelope/v1") {
424
+ const verified = verifyEnvelope(document, "event");
425
+ return {
426
+ event: validateSharedEvent(verified.payload, scopeId),
427
+ authentication: verified.authentication,
428
+ publicIdentity: verified.publicIdentity,
429
+ document
430
+ };
431
+ }
432
+ return { event: validateSharedEvent(document, scopeId), authentication: null, publicIdentity: null, document };
433
+ }
434
+
435
+ export async function publishLearning({
436
+ root = process.cwd(), directory, learningId, eventId = `shared:${randomUUID()}`,
437
+ supersedesEventId = null, signerId = null, confirmation, now = new Date()
438
+ }) {
439
+ requireConfirmation(confirmation);
440
+ if (!ID_RE.test(learningId || "")) throw new Error("learningId is required");
441
+ if (!ID_RE.test(eventId || "")) throw new Error("eventId must be a stable, whitespace-free identifier");
442
+ if (supersedesEventId !== null && !ID_RE.test(supersedesEventId || "")) throw new Error("supersedesEventId is invalid");
443
+ const catalog = await buildCatalog(root);
444
+ const adapter = await openAdapter(catalog.root, directory);
445
+ if (adapter.authentication && !signerId) throw new Error("signed adapters require an explicit local signer");
446
+ if (!adapter.authentication && signerId) throw new Error("an unsigned adapter cannot accept signed events");
447
+ const { learning } = await loadLearning(catalog.root, catalog);
448
+ const { graph } = await loadGraph(catalog.root, catalog);
449
+ const learningIssues = learningFindings(learning, graph);
450
+ if (learningIssues.length) throw new Error(`learning state failed closed: ${learningIssues.join(", ")}`);
451
+ const candidate = learning.candidates.find((item) => item.id === learningId);
452
+ if (!candidate || candidate.status !== "accepted") throw new Error("only accepted learning can be published");
453
+ if (!PRIVACY.has(candidate.privacy)) throw new Error("private learning can never be published to a shared adapter");
454
+ safeClaim(candidate.claim);
455
+ let instanceId;
456
+ await mutation(catalog.root, (state) => { instanceId = state.instanceId; return { instanceId }; });
457
+ const body = eventBody({
458
+ id: eventId, scopeId: adapter.manifest.scopeId, originInstanceId: instanceId,
459
+ candidate, supersedesEventId, publishedAt: date(now, "now")
460
+ });
461
+ const event = { ...body, digest: digest(body) };
462
+ validateSharedEvent(event, adapter.manifest.scopeId);
463
+ if (supersedesEventId !== null) {
464
+ const previousPath = join(adapter.eventsDirectory, eventFilename(supersedesEventId));
465
+ const previous = (await readEventFile(previousPath, adapter.manifest.scopeId)).event;
466
+ if (previous.kind !== event.kind || previous.subjectId !== event.subjectId
467
+ || previous.privacy !== event.privacy || previous.groupId !== event.groupId) {
468
+ throw new Error("a superseding shared event must keep kind, subject, and privacy scope");
469
+ }
470
+ }
471
+ const target = join(adapter.eventsDirectory, eventFilename(event.id));
472
+ const lockTarget = join(adapter.adapterDirectory, ".agentspine-adapter");
473
+ const document = signerId
474
+ ? await signEnvelope({ root: catalog.root, signerId, kind: "event", payload: event, now })
475
+ : event;
476
+ return withLock(lockTarget, async () => ({}), async () => {
477
+ try {
478
+ const existing = await readEventFile(target, adapter.manifest.scopeId);
479
+ if (existing.event.digest !== event.digest) throw new Error(`shared event ID collision: ${event.id}`);
480
+ return {
481
+ created: false, event: existing.event, authentication: existing.authentication,
482
+ eventPath: target, manifest: adapter.manifest
483
+ };
484
+ } catch (error) {
485
+ if (error.code !== "ENOENT") throw error;
486
+ }
487
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
488
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
489
+ await rename(temporary, target);
490
+ return {
491
+ created: true, event,
492
+ authentication: document.schema === "agentspine.signed-envelope/v1"
493
+ ? verifyEnvelope(document, "event").authentication : null,
494
+ eventPath: target, manifest: adapter.manifest
495
+ };
496
+ }, false);
497
+ }
498
+
499
+ async function readAdapterEvents(adapter) {
500
+ const entries = await readdir(adapter.eventsDirectory, { withFileTypes: true });
501
+ const candidates = entries.filter((entry) => entry.name.endsWith(".json"));
502
+ if (candidates.length > MAX_ADAPTER_EVENTS) throw new Error(`adapter exceeds ${MAX_ADAPTER_EVENTS} events`);
503
+ if (candidates.some((entry) => !entry.isFile() || entry.isSymbolicLink())) throw new Error("adapter contains a non-regular shared event");
504
+ const events = [];
505
+ let totalBytes = 0;
506
+ for (const entry of candidates.sort((a, b) => a.name.localeCompare(b.name))) {
507
+ const path = join(adapter.eventsDirectory, entry.name);
508
+ const metadata = await lstat(path);
509
+ totalBytes += metadata.size;
510
+ if (totalBytes > MAX_ADAPTER_BYTES) throw new Error("adapter event payload exceeds the 20 MiB pull limit");
511
+ const item = await readEventFile(path, adapter.manifest.scopeId);
512
+ if (entry.name !== eventFilename(item.event.id)) throw new Error(`shared event filename does not match its ID: ${item.event.id}`);
513
+ events.push(item);
514
+ }
515
+ const byId = new Map();
516
+ for (const item of events) {
517
+ const previous = byId.get(item.event.id);
518
+ if (previous && previous.event.digest !== item.event.digest) throw new Error(`shared event ID collision: ${item.event.id}`);
519
+ byId.set(item.event.id, item);
520
+ }
521
+ return [...byId.values()].sort((a, b) => a.event.publishedAt.localeCompare(b.event.publishedAt) || a.event.id.localeCompare(b.event.id));
522
+ }
523
+
524
+ export async function readDirectoryExchange({
525
+ root = process.cwd(), directory, requireAuthenticated = true
526
+ }) {
527
+ const catalog = await buildCatalog(root);
528
+ const adapter = await openAdapter(catalog.root, directory);
529
+ if (requireAuthenticated && !adapter.authentication) {
530
+ throw new Error("HTTPS snapshots require an authenticated directory adapter");
531
+ }
532
+ const events = await readAdapterEvents(adapter);
533
+ if (adapter.authentication && events.some((item) => !item.authentication)) {
534
+ throw new Error("signed adapters cannot contain unsigned events");
535
+ }
536
+ if (!adapter.authentication && events.some((item) => item.authentication)) {
537
+ throw new Error("unsigned adapters cannot mix signed events");
538
+ }
539
+ return {
540
+ root: catalog.root,
541
+ scopeId: adapter.manifest.scopeId,
542
+ adapterId: adapter.manifest.adapterId,
543
+ authenticated: Boolean(adapter.authentication),
544
+ manifest: structuredClone(adapter.manifestDocument),
545
+ events: events.map((item) => structuredClone(item.document))
546
+ };
547
+ }
548
+
549
+ export async function pullShared({ root = process.cwd(), directory, requireAuthenticated = false, now = new Date() }) {
550
+ const catalog = await buildCatalog(root);
551
+ const adapter = await openAdapter(catalog.root, directory);
552
+ if (requireAuthenticated && !adapter.authentication) throw new Error("adapter is not authenticated");
553
+ const { trust } = await loadTrust(catalog.root, catalog);
554
+ if (adapter.authentication) assertTrustedIdentity(adapter.publicIdentity, trust);
555
+ const events = await readAdapterEvents(adapter);
556
+ if (adapter.authentication && events.some((item) => !item.authentication)) {
557
+ throw new Error("signed adapters cannot contain unsigned events");
558
+ }
559
+ if (!adapter.authentication && events.some((item) => item.authentication)) {
560
+ throw new Error("unsigned adapters cannot mix signed events");
561
+ }
562
+ for (const item of events) if (item.authentication) assertTrustedIdentity(item.publicIdentity, trust);
563
+ const receivedAt = date(now, "now");
564
+ return mutation(catalog.root, (state, _catalog, sharingPath) => {
565
+ const known = new Map([
566
+ ...state.records.map((record) => [record.event.id, record.event.digest]),
567
+ ...state.history.map((entry) => [entry.value?.event?.id, entry.value?.event?.digest]).filter(([id]) => id)
568
+ ]);
569
+ const imported = [];
570
+ const skipped = [];
571
+ for (const item of events) {
572
+ const event = item.event;
573
+ const knownDigest = known.get(event.id);
574
+ if (knownDigest && knownDigest !== event.digest) throw new Error(`shared event ID collision in local state: ${event.id}`);
575
+ if (knownDigest || event.originInstanceId === state.instanceId) {
576
+ skipped.push(event.id);
577
+ continue;
578
+ }
579
+ state.records.push({
580
+ event,
581
+ authentication: item.authentication ? { ...item.authentication, verifiedAt: receivedAt } : null,
582
+ adapterId: adapter.manifest.adapterId,
583
+ receivedAt,
584
+ status: "pending",
585
+ review: null,
586
+ acceptedAt: null,
587
+ supersededIds: [],
588
+ authority: "context-only"
589
+ });
590
+ known.set(event.id, event.digest);
591
+ imported.push(event.id);
592
+ }
593
+ state.records.sort((a, b) => a.event.id.localeCompare(b.event.id));
594
+ return {
595
+ imported, skipped, scopeId: adapter.manifest.scopeId, adapterId: adapter.manifest.adapterId,
596
+ authenticated: Boolean(adapter.authentication),
597
+ manifestSigner: adapter.authentication ? {
598
+ signerId: adapter.authentication.signerId, keyId: adapter.authentication.keyId,
599
+ authority: "context-only"
600
+ } : null,
601
+ eventSigners: [...new Map(events.filter((item) => item.authentication).map((item) => [
602
+ item.authentication.keyId,
603
+ { signerId: item.authentication.signerId, keyId: item.authentication.keyId, authority: "context-only" }
604
+ ])).values()],
605
+ sharingPath
606
+ };
607
+ });
608
+ }
609
+
610
+ export async function reviewShared({
611
+ root = process.cwd(), id, decision, reason, confirmedByUser = false, now = new Date()
612
+ }) {
613
+ if (!ID_RE.test(id || "")) throw new Error("shared event id is required");
614
+ if (!new Set(["accept", "reject"]).has(decision)) throw new Error("decision must be accept or reject");
615
+ const reviewReason = safeText(reason, "reason", 500);
616
+ const reviewedAt = date(now, "now");
617
+ const acceptedTrust = decision === "accept" ? (await loadTrust(root)).trust : null;
618
+ return mutation(root, (state, _catalog, sharingPath, graph) => {
619
+ const record = state.records.find((item) => item.event.id === id);
620
+ if (!record) throw new Error(`unknown shared event: ${id}`);
621
+ if (record.status !== "pending") throw new Error("only a pending shared event can be reviewed");
622
+ preserve(state, record, reviewedAt);
623
+ if (decision === "reject") {
624
+ const rejected = {
625
+ ...record,
626
+ status: "rejected",
627
+ review: { decision, reason: reviewReason, confirmedByUser: false, reviewedAt, authority: "context-only" },
628
+ authority: "context-only"
629
+ };
630
+ state.records = state.records.map((item) => item.event.id === id ? rejected : item);
631
+ return { record: rejected, sharingPath };
632
+ }
633
+ if (!confirmedByUser) throw new Error("acceptance requires explicit local user confirmation");
634
+ if (record.authentication) {
635
+ const proofFindings = sharingAuthenticationFindings({ records: [record], history: [] }, acceptedTrust);
636
+ if (proofFindings.length) throw new Error(`signed shared context failed verification: ${proofFindings.join(", ")}`);
637
+ const trusted = acceptedTrust.records.find((item) => item.keyId === record.authentication.keyId);
638
+ if (!trusted || trusted.status !== "trusted" || trusted.signerId !== record.authentication.signerId) {
639
+ throw new Error("signed shared context must still have a trusted signer at local review time");
640
+ }
641
+ }
642
+ validateLocalScope(record.event, graph);
643
+ const newer = state.records.find((item) => item.status === "accepted" && item.event.supersedesEventId === id);
644
+ if (newer) throw new Error(`shared event is already superseded by accepted event: ${newer.event.id}`);
645
+ const prior = record.event.supersedesEventId
646
+ ? state.records.find((item) => item.event.id === record.event.supersedesEventId)
647
+ : null;
648
+ if (record.event.supersedesEventId && prior?.status !== "accepted") {
649
+ throw new Error("a shared predecessor must be locally accepted before its replacement");
650
+ }
651
+ if (prior && (prior.event.kind !== record.event.kind || prior.event.subjectId !== record.event.subjectId
652
+ || prior.event.privacy !== record.event.privacy || prior.event.groupId !== record.event.groupId)) {
653
+ throw new Error("a shared replacement must keep kind, subject, and privacy scope");
654
+ }
655
+ if (prior) {
656
+ preserve(state, prior, reviewedAt);
657
+ state.records = state.records.map((item) => item.event.id === prior.event.id
658
+ ? { ...item, status: "superseded", authority: "context-only" }
659
+ : item);
660
+ }
661
+ const accepted = {
662
+ ...record,
663
+ status: "accepted",
664
+ review: { decision, reason: reviewReason, confirmedByUser: true, reviewedAt, authority: "context-only" },
665
+ acceptedAt: reviewedAt,
666
+ supersededIds: prior ? [prior.event.id] : [],
667
+ authority: "context-only"
668
+ };
669
+ state.records = state.records.map((item) => item.event.id === id ? accepted : item);
670
+ return { record: accepted, sharingPath };
671
+ });
672
+ }
673
+
674
+ export async function rollbackShared({ root = process.cwd(), id, reason, now = new Date() }) {
675
+ if (!ID_RE.test(id || "")) throw new Error("shared event id is required");
676
+ const rollbackReason = safeText(reason, "reason", 500);
677
+ const rolledBackAt = date(now, "now");
678
+ return mutation(root, (state, _catalog, sharingPath) => {
679
+ const record = state.records.find((item) => item.event.id === id);
680
+ if (!record || record.status !== "accepted") throw new Error("only accepted shared context can be rolled back");
681
+ preserve(state, record, rolledBackAt);
682
+ const restored = [];
683
+ for (const previousId of record.supersededIds || []) {
684
+ const previous = state.records.find((item) => item.event.id === previousId && item.status === "superseded");
685
+ if (!previous) continue;
686
+ preserve(state, previous, rolledBackAt);
687
+ state.records = state.records.map((item) => item.event.id === previousId
688
+ ? { ...item, status: "accepted", authority: "context-only" }
689
+ : item);
690
+ restored.push(previousId);
691
+ }
692
+ const rolledBack = {
693
+ ...record,
694
+ status: "rolled-back",
695
+ rollback: { reason: rollbackReason, rolledBackAt, authority: "context-only" },
696
+ authority: "context-only"
697
+ };
698
+ state.records = state.records.map((item) => item.event.id === id ? rolledBack : item);
699
+ return { record: rolledBack, restored, sharingPath };
700
+ });
701
+ }
702
+
703
+ function groupAudience(graph, groupId, includePrivate) {
704
+ const ids = new Set();
705
+ if (!groupId) return ids;
706
+ ids.add(groupId);
707
+ for (const edge of graph.entityEdges) {
708
+ if (edge.relation !== "member-of" || (!includePrivate && edge.privacy === "private")) continue;
709
+ if (edge.to === groupId) ids.add(edge.from);
710
+ if (edge.from === groupId) ids.add(edge.to);
711
+ }
712
+ return ids;
713
+ }
714
+
715
+ function visible(record, entities, audience, includePrivate, groupId) {
716
+ const event = record.event;
717
+ if (event.privacy === "group" && (!groupId || event.groupId !== groupId)) return false;
718
+ const subject = event.subjectId ? entities.get(event.subjectId) : null;
719
+ if (subject?.privacy === "private" && !includePrivate) return false;
720
+ if (subject?.privacy === "group" && !audience.has(subject.id)) return false;
721
+ if (event.privacy === "group" && event.subjectId && !audience.has(event.subjectId)) return false;
722
+ return true;
723
+ }
724
+
725
+ export async function sharedContext({
726
+ root = process.cwd(), scopeId = null, groupId = null, includePrivate = false,
727
+ kinds = null, subjectIds = null, maxItems = null, catalog: providedCatalog = null
728
+ } = {}) {
729
+ const catalog = providedCatalog || await buildCatalog(root);
730
+ const { sharing } = await loadSharing(catalog.root, catalog);
731
+ const { graph } = await loadGraph(catalog.root, catalog);
732
+ const { trust } = await loadTrust(catalog.root, catalog);
733
+ const findings = sharingFindings(sharing, graph);
734
+ if (findings.length) throw new Error(`sharing state failed closed: ${findings.join(", ")}`);
735
+ const authenticationFindings = sharingAuthenticationFindings(sharing, trust);
736
+ if (authenticationFindings.length) throw new Error(`shared authentication failed closed: ${authenticationFindings.join(", ")}`);
737
+ if (scopeId !== null && !ID_RE.test(scopeId || "")) throw new Error("scopeId is invalid");
738
+ const entities = new Map(graph.entities.map((entity) => [entity.id, entity]));
739
+ if (groupId !== null) {
740
+ const group = entities.get(groupId);
741
+ if (!group || group.kind !== "group") throw new Error(`unknown group entity: ${groupId}`);
742
+ }
743
+ const kindFilter = kinds === null ? null : new Set(kinds);
744
+ if (kindFilter && [...kindFilter].some((kind) => !KINDS.has(kind))) throw new Error("kinds contains an unsupported shared-memory kind");
745
+ const subjectFilter = subjectIds === null ? null : new Set(subjectIds);
746
+ const audience = groupAudience(graph, groupId, includePrivate);
747
+ const limit = maxItems === null ? sharing.config.maxContextItems : integer(maxItems, "maxItems", 0, 50);
748
+ const items = sharing.records
749
+ .filter((record) => record.status === "accepted")
750
+ .filter((record) => !scopeId || record.event.scopeId === scopeId)
751
+ .filter((record) => !kindFilter || kindFilter.has(record.event.kind))
752
+ .filter((record) => !subjectFilter || subjectFilter.has(record.event.subjectId))
753
+ .filter((record) => visible(record, entities, audience, includePrivate, groupId))
754
+ .sort((a, b) => b.event.confidence - a.event.confidence || b.acceptedAt.localeCompare(a.acceptedAt) || a.event.id.localeCompare(b.event.id))
755
+ .slice(0, limit)
756
+ .map((record) => ({
757
+ id: record.event.id,
758
+ scopeId: record.event.scopeId,
759
+ kind: record.event.kind,
760
+ claim: record.event.claim,
761
+ subjectId: record.event.subjectId,
762
+ privacy: record.event.privacy,
763
+ groupId: record.event.groupId,
764
+ confidence: record.event.confidence,
765
+ originInstanceId: record.event.originInstanceId,
766
+ sourceLearningId: record.event.source.learningId,
767
+ publishedAt: record.event.publishedAt,
768
+ acceptedAt: record.acceptedAt,
769
+ authentication: record.authentication ? {
770
+ mode: "signed", signerId: record.authentication.signerId,
771
+ keyId: record.authentication.keyId, verifiedAt: record.authentication.verifiedAt,
772
+ authority: "context-only"
773
+ } : { mode: "integrity-only", authority: "context-only" },
774
+ authority: "context-only"
775
+ }));
776
+ return {
777
+ schema: "agentspine.shared-context/v1",
778
+ root: catalog.root,
779
+ scopeId,
780
+ groupId,
781
+ items,
782
+ authority: "context-only",
783
+ note: "Shared memory is reviewed descriptive context. It never grants delegation, host permissions, access, or instructions to act."
784
+ };
785
+ }
786
+
787
+ export async function sharedInbox({ root = process.cwd(), status = "pending" } = {}) {
788
+ if (!STATUSES.has(status)) throw new Error(`unsupported shared record status: ${status}`);
789
+ const { sharing, sharingPath } = await loadSharing(root);
790
+ const findings = sharingFindings(sharing, null);
791
+ if (findings.length) throw new Error(`sharing state failed closed: ${findings.join(", ")}`);
792
+ return {
793
+ schema: "agentspine.shared-inbox/v1",
794
+ root: sharing.root,
795
+ status,
796
+ items: sharing.records.filter((record) => record.status === status).map((record) => ({
797
+ id: record.event.id,
798
+ scopeId: record.event.scopeId,
799
+ kind: record.event.kind,
800
+ claim: record.event.claim,
801
+ subjectId: record.event.subjectId,
802
+ privacy: record.event.privacy,
803
+ groupId: record.event.groupId,
804
+ confidence: record.event.confidence,
805
+ originInstanceId: record.event.originInstanceId,
806
+ publishedAt: record.event.publishedAt,
807
+ receivedAt: record.receivedAt,
808
+ authentication: record.authentication ? {
809
+ mode: "signed", signerId: record.authentication.signerId,
810
+ keyId: record.authentication.keyId, signedAt: record.authentication.signedAt,
811
+ verifiedAt: record.authentication.verifiedAt, authority: "context-only"
812
+ } : { mode: "integrity-only", authority: "context-only" },
813
+ authority: "context-only"
814
+ })),
815
+ sharingPath,
816
+ authority: "context-only"
817
+ };
818
+ }
819
+
820
+ export async function configureSharing({ root = process.cwd(), maxContextItems }) {
821
+ return mutation(root, (state, _catalog, sharingPath) => {
822
+ state.config.maxContextItems = integer(maxContextItems, "maxContextItems", 1, 50);
823
+ return { config: state.config, sharingPath };
824
+ });
825
+ }
826
+
827
+ export async function deleteShared({ root = process.cwd(), id, confirmation }) {
828
+ requireConfirmation(confirmation);
829
+ if (!ID_RE.test(id || "")) throw new Error("shared event id is required");
830
+ return mutation(root, (state, _catalog, sharingPath) => {
831
+ const record = state.records.find((item) => item.event.id === id);
832
+ if (record?.status === "accepted" && record.supersededIds?.length) {
833
+ throw new Error("roll back accepted superseding shared context before permanent deletion");
834
+ }
835
+ if (state.records.some((item) => item.status === "accepted" && item.supersededIds?.includes(id))) {
836
+ throw new Error("roll back accepted superseding shared context before deleting its predecessor");
837
+ }
838
+ const existed = Boolean(record);
839
+ state.records = state.records.filter((item) => item.event.id !== id);
840
+ state.history = state.history.filter((entry) => entry.recordId !== id && entry.value?.event?.id !== id);
841
+ return { deleted: existed, id, sharingPath };
842
+ });
843
+ }
844
+
845
+ export function sharingFindings(sharing, graph = null) {
846
+ const findings = [];
847
+ if (!exactKeys(sharing, ["schema", "root", "instanceId", "config", "records", "history"])
848
+ || !exactKeys(sharing.config, ["maxContextItems"])
849
+ || sharing.schema !== "agentspine.sharing/v1" || !ID_RE.test(sharing.instanceId || "") || !validConfig(sharing.config)) findings.push("invalid-sharing-state");
850
+ if (new Set(sharing.records.map((record) => record.event?.id)).size !== sharing.records.length) findings.push("duplicate-shared-event");
851
+ const records = [
852
+ ...sharing.records.map((record) => ({ record, current: true })),
853
+ ...sharing.history.map((entry) => entry.value).filter(Boolean).map((record) => ({ record, current: false }))
854
+ ];
855
+ for (const { record, current } of records) {
856
+ try { validateSharedEvent(record.event); } catch { findings.push(`invalid-shared-event:${record.event?.id || "unknown"}`); }
857
+ const allowedRecordKeys = new Set(["event", "authentication", "adapterId", "receivedAt", "status", "review", "acceptedAt", "supersededIds", "rollback", "authority"]);
858
+ if (Object.keys(record).some((key) => !allowedRecordKeys.has(key))
859
+ || !STATUSES.has(record.status) || record.authority !== "context-only" || !ID_RE.test(record.adapterId || "")
860
+ || !validDateValue(record.receivedAt) || !Array.isArray(record.supersededIds)
861
+ || record.supersededIds.some((id) => !ID_RE.test(id || ""))) findings.push(`invalid-shared-record:${record.event?.id || "unknown"}`);
862
+ if (!validateAuthentication(record.authentication)) findings.push(`invalid-shared-authentication:${record.event?.id || "unknown"}`);
863
+ const nested = [record.review, record.rollback].filter(Boolean);
864
+ if (nested.some((item) => item.authority !== "context-only")) findings.push(`shared-authority:${record.event?.id || "unknown"}`);
865
+ if (nested.some((item) => SECRET_RE.test(item.reason || ""))) findings.push(`unsafe-shared-review:${record.event?.id || "unknown"}`);
866
+ const validReview = record.review === null || (
867
+ exactKeys(record.review, ["decision", "reason", "confirmedByUser", "reviewedAt", "authority"])
868
+ && new Set(["accept", "reject"]).has(record.review.decision)
869
+ && typeof record.review.reason === "string" && record.review.reason.length > 0
870
+ && typeof record.review.confirmedByUser === "boolean"
871
+ && validDateValue(record.review.reviewedAt)
872
+ && record.review.authority === "context-only"
873
+ );
874
+ const validRollback = record.rollback === undefined || (
875
+ exactKeys(record.rollback, ["reason", "rolledBackAt", "authority"])
876
+ && typeof record.rollback.reason === "string" && record.rollback.reason.length > 0
877
+ && validDateValue(record.rollback.rolledBackAt)
878
+ && record.rollback.authority === "context-only"
879
+ );
880
+ if (!validReview || !validRollback) findings.push(`invalid-shared-review:${record.event?.id || "unknown"}`);
881
+ if (record.status === "pending" && (record.review !== null || record.acceptedAt !== null || record.supersededIds.length)) findings.push(`invalid-shared-pending:${record.event?.id || "unknown"}`);
882
+ if (record.status === "rejected" && (record.review?.decision !== "reject" || record.review?.confirmedByUser !== false || record.acceptedAt !== null)) findings.push(`invalid-shared-rejection:${record.event?.id || "unknown"}`);
883
+ if (record.status === "accepted" && (record.review?.decision !== "accept" || record.review?.confirmedByUser !== true || !record.acceptedAt)) {
884
+ findings.push(`invalid-shared-acceptance:${record.event?.id || "unknown"}`);
885
+ }
886
+ if (["accepted", "superseded", "rolled-back"].includes(record.status) && !validDateValue(record.acceptedAt)) findings.push(`invalid-shared-accepted-date:${record.event?.id || "unknown"}`);
887
+ if (record.status === "rolled-back" && !record.rollback) findings.push(`invalid-shared-rollback:${record.event?.id || "unknown"}`);
888
+ if (graph && current && record.status === "accepted") {
889
+ const event = record.event || {};
890
+ const subject = event.subjectId ? graph.entities.find((entity) => entity.id === event.subjectId) : null;
891
+ if (event.subjectId && !subject) findings.push(`unknown-shared-subject:${event.id || "unknown"}`);
892
+ if (event.privacy === "group") {
893
+ const group = graph.entities.find((entity) => entity.id === event.groupId && entity.kind === "group");
894
+ const member = !event.subjectId || graph.entityEdges.some((edge) => edge.relation === "member-of" && edge.privacy !== "private" && (
895
+ (edge.from === event.subjectId && edge.to === event.groupId) || (edge.to === event.subjectId && edge.from === event.groupId)
896
+ ));
897
+ if (!group || !member) findings.push(`invalid-shared-group:${event.id || "unknown"}`);
898
+ }
899
+ }
900
+ }
901
+ const accepted = new Set(sharing.records.filter((record) => record.status === "accepted").map((record) => record.event.id));
902
+ for (const record of sharing.records.filter((item) => item.status === "accepted" && item.event.supersedesEventId)) {
903
+ if (accepted.has(record.event.supersedesEventId)) findings.push(`active-shared-supersession:${record.event.id}`);
904
+ }
905
+ for (const record of sharing.records) {
906
+ if (record.status === "accepted") {
907
+ const expected = record.event.supersedesEventId === null ? [] : [record.event.supersedesEventId];
908
+ if (record.supersededIds.length !== expected.length
909
+ || record.supersededIds.some((id, index) => id !== expected[index])) {
910
+ findings.push(`invalid-shared-supersession:${record.event.id}`);
911
+ }
912
+ }
913
+ if (record.status === "superseded" && !sharing.records.some(
914
+ (item) => item.status === "accepted" && item.supersededIds.includes(record.event.id)
915
+ )) findings.push(`orphaned-shared-supersession:${record.event.id}`);
916
+ }
917
+ if (sharing.history.some((entry) => !exactKeys(entry, ["kind", "recordId", "supersededAt", "privacy", "value", "authority"])
918
+ || entry.kind !== "shared-record" || entry.recordId !== entry.value?.event?.id
919
+ || entry.privacy !== entry.value?.event?.privacy || !validDateValue(entry.supersededAt)
920
+ || entry.authority !== "context-only" || entry.value?.authority !== "context-only")) findings.push("invalid-sharing-history");
921
+ return [...new Set(findings)];
922
+ }
923
+
924
+ export function sharingAuthenticationFindings(sharing, trust) {
925
+ const findings = [];
926
+ const trustIssues = trustFindings(trust);
927
+ if (trustIssues.length) return trustIssues;
928
+ const records = [
929
+ ...sharing.records.map((record) => ({ record, current: true })),
930
+ ...sharing.history.map((entry) => entry.value).filter(Boolean).map((record) => ({ record, current: false }))
931
+ ];
932
+ for (const { record, current } of records.filter((item) => item.record.authentication)) {
933
+ const trusted = trust.records.find((item) => item.keyId === record.authentication.keyId);
934
+ if (!trusted || trusted.signerId !== record.authentication.signerId) {
935
+ findings.push(`missing-shared-signing-key:${record.event.id}`);
936
+ continue;
937
+ }
938
+ try {
939
+ verifyEnvelope({
940
+ schema: "agentspine.signed-envelope/v1",
941
+ kind: "event",
942
+ signer: trusted.publicIdentity,
943
+ payload: record.event,
944
+ signedAt: record.authentication.signedAt,
945
+ authority: "context-only",
946
+ signature: record.authentication.signature
947
+ }, "event");
948
+ } catch {
949
+ findings.push(`invalid-stored-shared-signature:${record.event.id}`);
950
+ }
951
+ if (current && record.status === "accepted" && trusted.status !== "trusted") {
952
+ findings.push(`untrusted-accepted-shared-event:${record.event.id}`);
953
+ }
954
+ }
955
+ return [...new Set(findings)];
956
+ }
957
+
958
+ function validateLocalScope(event, graph) {
959
+ if (event.subjectId && !graph.entities.some((entity) => entity.id === event.subjectId)) {
960
+ throw new Error(`unknown shared subject entity: ${event.subjectId}`);
961
+ }
962
+ if (event.privacy !== "group") return;
963
+ const group = graph.entities.find((entity) => entity.id === event.groupId && entity.kind === "group");
964
+ if (!group) throw new Error(`unknown shared group entity: ${event.groupId}`);
965
+ const member = !event.subjectId || graph.entityEdges.some((edge) => edge.relation === "member-of" && edge.privacy !== "private" && (
966
+ (edge.from === event.subjectId && edge.to === event.groupId) || (edge.to === event.subjectId && edge.from === event.groupId)
967
+ ));
968
+ if (!member) throw new Error(`shared subject is not a visible member of group: ${event.groupId}`);
969
+ }