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,501 +1,501 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { chmod, lstat, mkdir, realpath, stat } from "node:fs/promises";
3
- import { basename, dirname, resolve } from "node:path";
4
- import { verifyEnvelope } from "./authentication.js";
5
- import { buildCatalog } from "./catalog.js";
6
- import { buildHttpsSnapshot, importHttpsSnapshot, validateHttpsSnapshot } from "./https-transport.js";
7
- import { isInside } from "./paths.js";
8
- import { readDirectoryExchange } from "./sharing.js";
9
-
10
- const SCHEMA = "agentspine.sqlite/v1";
11
- const REVISION_SCHEMA = "agentspine.sqlite-revision/v1";
12
- const CONFIRMATION = "local-share-confirmed";
13
- const MAX_DATABASE_BYTES = 128 * 1024 * 1024;
14
- const MAX_SNAPSHOT_BYTES = 21 * 1024 * 1024;
15
- const MAX_REVISIONS = 1000;
16
- const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
17
- const DIGEST_RE = /^[a-f0-9]{64}$/;
18
- const TABLES = ["agentspine_head", "agentspine_meta", "agentspine_revisions"];
19
- const COLUMNS = {
20
- agentspine_meta: [
21
- ["id", "INTEGER", 0, 1], ["schema", "TEXT", 1, 0], ["scope_id", "TEXT", 1, 0],
22
- ["adapter_id", "TEXT", 1, 0], ["manifest_json", "TEXT", 1, 0],
23
- ["manifest_digest", "TEXT", 1, 0], ["signer_id", "TEXT", 1, 0],
24
- ["key_id", "TEXT", 1, 0], ["created_at", "TEXT", 1, 0], ["authority", "TEXT", 1, 0]
25
- ],
26
- agentspine_revisions: [
27
- ["sequence", "INTEGER", 0, 1], ["snapshot_id", "TEXT", 1, 0],
28
- ["snapshot_digest", "TEXT", 1, 0], ["previous_revision_digest", "TEXT", 0, 0],
29
- ["snapshot_json", "TEXT", 1, 0], ["created_at", "TEXT", 1, 0],
30
- ["authority", "TEXT", 1, 0], ["revision_digest", "TEXT", 1, 0]
31
- ],
32
- agentspine_head: [
33
- ["id", "INTEGER", 0, 1], ["sequence", "INTEGER", 1, 0],
34
- ["revision_digest", "TEXT", 1, 0], ["snapshot_digest", "TEXT", 1, 0],
35
- ["authority", "TEXT", 1, 0]
36
- ]
37
- };
38
-
39
- function canonical(value) {
40
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
41
- if (value && typeof value === "object") {
42
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
43
- }
44
- return JSON.stringify(value);
45
- }
46
-
47
- function digest(value) {
48
- return createHash("sha256").update(canonical(value)).digest("hex");
49
- }
50
-
51
- function exactKeys(value, expected) {
52
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
53
- const actual = Object.keys(value).sort();
54
- const wanted = [...expected].sort();
55
- return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
56
- }
57
-
58
- function validDate(value) {
59
- return typeof value === "string" && value.length > 0 && Number.isFinite(new Date(value).getTime());
60
- }
61
-
62
- async function sqliteModule() {
63
- const [major, minor] = process.versions.node.split(".").map(Number);
64
- if (major < 22 || (major === 22 && minor < 13)) {
65
- throw new Error("SQLite transport requires Node.js 22.13 or newer with node:sqlite support");
66
- }
67
- try {
68
- return await import("node:sqlite");
69
- } catch (error) {
70
- throw new Error("SQLite transport requires Node.js 22.13 or newer with node:sqlite support", {
71
- cause: error
72
- });
73
- }
74
- }
75
-
76
- async function prospectiveCanonicalPath(path) {
77
- let ancestor = path;
78
- const missing = [];
79
- while (true) {
80
- try {
81
- const canonicalAncestor = await realpath(ancestor);
82
- return resolve(canonicalAncestor, ...missing.reverse());
83
- } catch (error) {
84
- if (error.code !== "ENOENT") throw error;
85
- const parent = dirname(ancestor);
86
- if (parent === ancestor) throw error;
87
- missing.push(basename(ancestor));
88
- ancestor = parent;
89
- }
90
- }
91
- }
92
-
93
- async function databasePath(root, database, { create = false } = {}) {
94
- if (!database || typeof database !== "string" || database === ":memory:") {
95
- throw new Error("a file-backed SQLite database path is required");
96
- }
97
- const catalog = await buildCatalog(root);
98
- const target = resolve(database);
99
- const canonicalTarget = await prospectiveCanonicalPath(target);
100
- if (isInside(catalog.root, canonicalTarget)) {
101
- throw new Error("SQLite transport database must remain outside the scanned project");
102
- }
103
- if (create) await mkdir(dirname(target), { recursive: true, mode: 0o700 });
104
- let metadata = null;
105
- try {
106
- metadata = await lstat(target);
107
- } catch (error) {
108
- if (error.code !== "ENOENT" || !create) throw error;
109
- }
110
- if (metadata && (!metadata.isFile() || metadata.isSymbolicLink())) {
111
- throw new Error("SQLite transport path must be a real regular file");
112
- }
113
- if (metadata && metadata.nlink !== 1) {
114
- throw new Error("SQLite transport database must not be a hard link");
115
- }
116
- if (metadata && metadata.size > MAX_DATABASE_BYTES) {
117
- throw new Error("SQLite transport database exceeds the 128 MiB limit");
118
- }
119
- for (const suffix of ["-journal", "-wal", "-shm"]) {
120
- try {
121
- const sidecar = await lstat(`${target}${suffix}`);
122
- if (!metadata) throw new Error("SQLite transport sidecar exists without its database");
123
- if (!sidecar.isFile() || sidecar.isSymbolicLink() || sidecar.nlink !== 1) {
124
- throw new Error("SQLite transport sidecars must be real, single-link regular files");
125
- }
126
- if (sidecar.size > MAX_DATABASE_BYTES) throw new Error("SQLite transport sidecar exceeds the 128 MiB limit");
127
- } catch (error) {
128
- if (error.code !== "ENOENT") throw error;
129
- }
130
- }
131
- const parent = await realpath(dirname(target));
132
- if (isInside(catalog.root, parent)) {
133
- throw new Error("SQLite transport database must remain outside the scanned project");
134
- }
135
- return { catalog, target: resolve(parent, basename(target)), exists: Boolean(metadata) };
136
- }
137
-
138
- async function openDatabase(path, { readOnly = false } = {}) {
139
- const { DatabaseSync } = await sqliteModule();
140
- let database;
141
- try {
142
- database = new DatabaseSync(path, {
143
- readOnly, enableForeignKeyConstraints: true,
144
- enableDoubleQuotedStringLiterals: false, allowExtension: false
145
- });
146
- } catch (error) {
147
- throw new Error(`SQLite transport database could not be opened: ${error.message}`, { cause: error });
148
- }
149
- try {
150
- database.exec("PRAGMA trusted_schema = OFF; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
151
- if (typeof database.enableDefensive === "function") database.enableDefensive(true);
152
- if (readOnly) database.exec("PRAGMA query_only = ON;");
153
- else database.exec("PRAGMA synchronous = FULL;");
154
- return database;
155
- } catch (error) {
156
- database.close();
157
- throw error;
158
- }
159
- }
160
-
161
- function createSchema(database) {
162
- database.exec(`
163
- CREATE TABLE IF NOT EXISTS agentspine_meta (
164
- id INTEGER PRIMARY KEY CHECK (id = 1),
165
- schema TEXT NOT NULL CHECK (schema = 'agentspine.sqlite/v1'),
166
- scope_id TEXT NOT NULL,
167
- adapter_id TEXT NOT NULL,
168
- manifest_json TEXT NOT NULL,
169
- manifest_digest TEXT NOT NULL,
170
- signer_id TEXT NOT NULL,
171
- key_id TEXT NOT NULL,
172
- created_at TEXT NOT NULL,
173
- authority TEXT NOT NULL CHECK (authority = 'context-only')
174
- ) STRICT;
175
- CREATE TABLE IF NOT EXISTS agentspine_revisions (
176
- sequence INTEGER PRIMARY KEY CHECK (sequence > 0),
177
- snapshot_id TEXT NOT NULL,
178
- snapshot_digest TEXT NOT NULL UNIQUE,
179
- previous_revision_digest TEXT,
180
- snapshot_json TEXT NOT NULL,
181
- created_at TEXT NOT NULL,
182
- authority TEXT NOT NULL CHECK (authority = 'context-only'),
183
- revision_digest TEXT NOT NULL UNIQUE
184
- ) STRICT;
185
- CREATE TABLE IF NOT EXISTS agentspine_head (
186
- id INTEGER PRIMARY KEY CHECK (id = 1),
187
- sequence INTEGER NOT NULL,
188
- revision_digest TEXT NOT NULL,
189
- snapshot_digest TEXT NOT NULL,
190
- authority TEXT NOT NULL CHECK (authority = 'context-only')
191
- ) STRICT;
192
- PRAGMA user_version = 1;
193
- `);
194
- }
195
-
196
- function parseJson(value, label, maximum = MAX_SNAPSHOT_BYTES) {
197
- if (typeof value !== "string" || Buffer.byteLength(value) > maximum) {
198
- throw new Error(`${label} exceeds its validation limit`);
199
- }
200
- try {
201
- return JSON.parse(value);
202
- } catch {
203
- throw new Error(`${label} is not valid JSON`);
204
- }
205
- }
206
-
207
- function validateSchema(database) {
208
- const integrity = database.prepare("PRAGMA integrity_check").get();
209
- if (!integrity || Object.values(integrity)[0] !== "ok") throw new Error("SQLite transport integrity check failed");
210
- const objects = database.prepare(`
211
- SELECT type, name FROM sqlite_master
212
- WHERE name NOT LIKE 'sqlite_%'
213
- ORDER BY type, name
214
- `).all();
215
- if (objects.length !== TABLES.length
216
- || objects.some((item) => item.type !== "table")
217
- || objects.map((item) => item.name).sort().some((name, index) => name !== TABLES[index])) {
218
- throw new Error("SQLite transport schema contains missing or unexpected objects");
219
- }
220
- for (const table of TABLES) {
221
- const columns = database.prepare(`PRAGMA table_info(${table})`).all();
222
- const expected = COLUMNS[table];
223
- if (columns.length !== expected.length || columns.some((column, index) => {
224
- const wanted = expected[index];
225
- return column.name !== wanted[0] || String(column.type).toUpperCase() !== wanted[1]
226
- || Number(column.notnull) !== wanted[2] || Number(column.pk) !== wanted[3]
227
- || column.dflt_value !== null;
228
- })) throw new Error(`SQLite transport table layout is invalid: ${table}`);
229
- }
230
- for (const table of ["agentspine_meta", "agentspine_head"]) {
231
- if (database.prepare(`PRAGMA index_list(${table})`).all().length !== 0) {
232
- throw new Error(`SQLite transport table has an unexpected index: ${table}`);
233
- }
234
- }
235
- const revisionIndexes = database.prepare("PRAGMA index_list(agentspine_revisions)").all();
236
- if (revisionIndexes.length !== 2 || revisionIndexes.some((index) => (
237
- Number(index.unique) !== 1 || index.origin !== "u" || Number(index.partial) !== 0
238
- ))) throw new Error("SQLite transport revision indexes are invalid");
239
- const expectedIndexes = new Map([
240
- ["sqlite_autoindex_agentspine_revisions_1", "snapshot_digest"],
241
- ["sqlite_autoindex_agentspine_revisions_2", "revision_digest"]
242
- ]);
243
- for (const index of revisionIndexes) {
244
- const expectedColumn = expectedIndexes.get(index.name);
245
- if (!expectedColumn) throw new Error("SQLite transport revision indexes are invalid");
246
- const columns = database.prepare(`PRAGMA index_info(${index.name})`).all();
247
- if (columns.length !== 1 || columns[0].name !== expectedColumn) {
248
- throw new Error("SQLite transport revision indexes are invalid");
249
- }
250
- }
251
- const version = database.prepare("PRAGMA user_version").get();
252
- if (!version || Number(Object.values(version)[0]) !== 1) throw new Error("SQLite transport schema version is unsupported");
253
- }
254
-
255
- function validateManifestMetadata(row) {
256
- if (!row || row.schema !== SCHEMA || !ID_RE.test(row.scope_id || "")
257
- || !ID_RE.test(row.adapter_id || "") || !ID_RE.test(row.signer_id || "")
258
- || !ID_RE.test(row.key_id || "") || !DIGEST_RE.test(row.manifest_digest || "")
259
- || !validDate(row.created_at) || row.authority !== "context-only") {
260
- throw new Error("SQLite transport metadata is invalid");
261
- }
262
- const manifest = parseJson(row.manifest_json, "SQLite signed manifest", 64 * 1024);
263
- const verified = verifyEnvelope(manifest, "manifest");
264
- if (digest(manifest) !== row.manifest_digest
265
- || verified.payload?.scopeId !== row.scope_id || verified.payload?.adapterId !== row.adapter_id
266
- || verified.payload?.adapter !== "directory" || verified.publicIdentity.signerId !== row.signer_id
267
- || verified.publicIdentity.keyId !== row.key_id) {
268
- throw new Error("SQLite transport metadata does not match its signed manifest");
269
- }
270
- return { manifest, verified };
271
- }
272
-
273
- function revisionBody(row) {
274
- return {
275
- schema: REVISION_SCHEMA, sequence: row.sequence, snapshotId: row.snapshot_id,
276
- snapshotDigest: row.snapshot_digest, previousRevisionDigest: row.previous_revision_digest,
277
- createdAt: row.created_at, authority: row.authority
278
- };
279
- }
280
-
281
- function readValidatedState(database) {
282
- validateSchema(database);
283
- const metadata = database.prepare("SELECT * FROM agentspine_meta WHERE id = 1").get();
284
- const { manifest, verified } = validateManifestMetadata(metadata);
285
- const rows = database.prepare("SELECT * FROM agentspine_revisions ORDER BY sequence").all();
286
- if (rows.length > MAX_REVISIONS) throw new Error(`SQLite transport exceeds ${MAX_REVISIONS} retained revisions`);
287
- let previousDigest = null;
288
- let totalBytes = 0;
289
- const revisions = rows.map((row, index) => {
290
- totalBytes += Buffer.byteLength(row.snapshot_json || "");
291
- if (totalBytes > MAX_DATABASE_BYTES) throw new Error("SQLite transport retained snapshots exceed the 128 MiB limit");
292
- if (Number(row.sequence) !== index + 1 || !ID_RE.test(row.snapshot_id || "")
293
- || !DIGEST_RE.test(row.snapshot_digest || "")
294
- || row.previous_revision_digest !== previousDigest || !validDate(row.created_at)
295
- || row.authority !== "context-only" || !DIGEST_RE.test(row.revision_digest || "")
296
- || digest(revisionBody(row)) !== row.revision_digest) {
297
- throw new Error("SQLite transport revision chain is invalid");
298
- }
299
- const snapshot = validateHttpsSnapshot(parseJson(row.snapshot_json, "SQLite snapshot"));
300
- if (snapshot.digest !== row.snapshot_digest || snapshot.snapshotId !== row.snapshot_id
301
- || snapshot.scopeId !== metadata.scope_id || snapshot.adapterId !== metadata.adapter_id
302
- || digest(snapshot.manifest) !== metadata.manifest_digest) {
303
- throw new Error("SQLite transport revision does not match its authenticated metadata");
304
- }
305
- previousDigest = row.revision_digest;
306
- return { ...row, snapshot };
307
- });
308
- const head = database.prepare("SELECT * FROM agentspine_head WHERE id = 1").get();
309
- if (revisions.length === 0) {
310
- if (head) throw new Error("SQLite transport head exists without a revision");
311
- } else {
312
- const latest = revisions.at(-1);
313
- if (!head || !exactKeys(head, ["id", "sequence", "revision_digest", "snapshot_digest", "authority"])
314
- || Number(head.sequence) !== latest.sequence || head.revision_digest !== latest.revision_digest
315
- || head.snapshot_digest !== latest.snapshot_digest || head.authority !== "context-only") {
316
- throw new Error("SQLite transport head does not match the append-only revision chain");
317
- }
318
- }
319
- return { metadata, manifest, verified, revisions, head };
320
- }
321
-
322
- async function secureFile(path) {
323
- await chmod(path, 0o600);
324
- const metadata = await stat(path);
325
- if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size > MAX_DATABASE_BYTES) {
326
- throw new Error("SQLite transport database is not a bounded regular file");
327
- }
328
- }
329
-
330
- export async function initSqliteAdapter({
331
- root = process.cwd(), directory, database, confirmation, now = new Date()
332
- }) {
333
- if (confirmation !== CONFIRMATION) throw new Error("SQLite adapter initialization requires explicit local owner confirmation");
334
- const exchange = await readDirectoryExchange({ root, directory, requireAuthenticated: true });
335
- const manifestVerification = verifyEnvelope(exchange.manifest, "manifest");
336
- const location = await databasePath(root, database, { create: true });
337
- const connection = await openDatabase(location.target);
338
- let completed = false;
339
- let created = false;
340
- try {
341
- connection.exec("BEGIN IMMEDIATE");
342
- try {
343
- createSchema(connection);
344
- const existing = connection.prepare("SELECT * FROM agentspine_meta WHERE id = 1").get();
345
- if (existing) {
346
- const validated = validateManifestMetadata(existing);
347
- if (existing.scope_id !== exchange.scopeId || existing.adapter_id !== exchange.adapterId
348
- || existing.manifest_digest !== digest(exchange.manifest)
349
- || validated.verified.publicIdentity.keyId !== manifestVerification.publicIdentity.keyId) {
350
- throw new Error("SQLite transport already belongs to a different signed adapter");
351
- }
352
- } else {
353
- created = true;
354
- connection.prepare(`
355
- INSERT INTO agentspine_meta
356
- (id, schema, scope_id, adapter_id, manifest_json, manifest_digest, signer_id, key_id, created_at, authority)
357
- VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, 'context-only')
358
- `).run(
359
- SCHEMA, exchange.scopeId, exchange.adapterId, JSON.stringify(exchange.manifest), digest(exchange.manifest),
360
- manifestVerification.publicIdentity.signerId, manifestVerification.publicIdentity.keyId,
361
- new Date(now).toISOString()
362
- );
363
- }
364
- connection.exec("COMMIT");
365
- } catch (error) {
366
- try { connection.exec("ROLLBACK"); } catch {}
367
- throw error;
368
- }
369
- const state = readValidatedState(connection);
370
- completed = true;
371
- return {
372
- created, database: location.target, schema: SCHEMA,
373
- scopeId: state.metadata.scope_id, adapterId: state.metadata.adapter_id,
374
- signer: { signerId: state.metadata.signer_id, keyId: state.metadata.key_id },
375
- revisions: state.revisions.length, authority: "context-only"
376
- };
377
- } finally {
378
- connection.close();
379
- if (completed) await secureFile(location.target);
380
- }
381
- }
382
-
383
- export async function publishSqliteSnapshot({
384
- root = process.cwd(), directory, database, snapshotId = `snapshot:${randomUUID()}`,
385
- confirmation, now = new Date()
386
- }) {
387
- if (confirmation !== CONFIRMATION) throw new Error("SQLite snapshot publication requires explicit local owner confirmation");
388
- const snapshot = await buildHttpsSnapshot({ root, directory, snapshotId, now });
389
- const location = await databasePath(root, database);
390
- const connection = await openDatabase(location.target);
391
- let completed = false;
392
- try {
393
- connection.exec("BEGIN IMMEDIATE");
394
- try {
395
- const state = readValidatedState(connection);
396
- if (snapshot.scopeId !== state.metadata.scope_id || snapshot.adapterId !== state.metadata.adapter_id
397
- || digest(snapshot.manifest) !== state.metadata.manifest_digest) {
398
- throw new Error("SQLite snapshot does not belong to this authenticated adapter");
399
- }
400
- const existing = connection.prepare("SELECT * FROM agentspine_revisions WHERE snapshot_digest = ?").get(snapshot.digest);
401
- if (existing) {
402
- connection.exec("COMMIT");
403
- completed = true;
404
- return {
405
- created: false, database: location.target, sequence: existing.sequence,
406
- snapshotId: existing.snapshot_id, snapshotDigest: existing.snapshot_digest,
407
- revisionDigest: existing.revision_digest, events: snapshot.events.length,
408
- authority: "context-only"
409
- };
410
- }
411
- const sequence = state.revisions.length + 1;
412
- const previousRevisionDigest = state.revisions.at(-1)?.revision_digest || null;
413
- const createdAt = new Date(now).toISOString();
414
- if (!validDate(createdAt)) throw new Error("now must be a valid date");
415
- const body = {
416
- schema: REVISION_SCHEMA, sequence, snapshotId: snapshot.snapshotId,
417
- snapshotDigest: snapshot.digest, previousRevisionDigest, createdAt,
418
- authority: "context-only"
419
- };
420
- const revisionDigest = digest(body);
421
- connection.prepare(`
422
- INSERT INTO agentspine_revisions
423
- (sequence, snapshot_id, snapshot_digest, previous_revision_digest, snapshot_json, created_at, authority, revision_digest)
424
- VALUES (?, ?, ?, ?, ?, ?, 'context-only', ?)
425
- `).run(sequence, snapshot.snapshotId, snapshot.digest, previousRevisionDigest, JSON.stringify(snapshot), createdAt, revisionDigest);
426
- if (state.head) {
427
- const changed = connection.prepare(`
428
- UPDATE agentspine_head SET sequence = ?, revision_digest = ?, snapshot_digest = ?
429
- WHERE id = 1 AND sequence = ? AND revision_digest = ? AND authority = 'context-only'
430
- `).run(sequence, revisionDigest, snapshot.digest, state.head.sequence, state.head.revision_digest);
431
- if (Number(changed.changes) !== 1) throw new Error("SQLite transport head changed concurrently");
432
- } else {
433
- connection.prepare(`
434
- INSERT INTO agentspine_head (id, sequence, revision_digest, snapshot_digest, authority)
435
- VALUES (1, ?, ?, ?, 'context-only')
436
- `).run(sequence, revisionDigest, snapshot.digest);
437
- }
438
- connection.exec("COMMIT");
439
- completed = true;
440
- return {
441
- created: true, database: location.target, sequence, snapshotId: snapshot.snapshotId,
442
- snapshotDigest: snapshot.digest, revisionDigest, previousRevisionDigest,
443
- events: snapshot.events.length, authority: "context-only"
444
- };
445
- } catch (error) {
446
- try { connection.exec("ROLLBACK"); } catch {}
447
- throw error;
448
- }
449
- } finally {
450
- connection.close();
451
- if (completed) await secureFile(location.target);
452
- }
453
- }
454
-
455
- export async function inspectSqliteAdapter({ root = process.cwd(), database }) {
456
- const location = await databasePath(root, database);
457
- const connection = await openDatabase(location.target, { readOnly: true });
458
- try {
459
- const state = readValidatedState(connection);
460
- return {
461
- database: location.target, schema: SCHEMA, scopeId: state.metadata.scope_id,
462
- adapterId: state.metadata.adapter_id,
463
- signer: { signerId: state.metadata.signer_id, keyId: state.metadata.key_id },
464
- revisions: state.revisions.length,
465
- head: state.head ? {
466
- sequence: state.head.sequence, revisionDigest: state.head.revision_digest,
467
- snapshotDigest: state.head.snapshot_digest
468
- } : null,
469
- snapshots: state.revisions.map((item) => ({
470
- sequence: item.sequence, snapshotId: item.snapshot_id,
471
- snapshotDigest: item.snapshot_digest, revisionDigest: item.revision_digest,
472
- previousRevisionDigest: item.previous_revision_digest, createdAt: item.created_at,
473
- events: item.snapshot.events.length, authority: "context-only"
474
- })),
475
- authority: "context-only"
476
- };
477
- } finally {
478
- connection.close();
479
- }
480
- }
481
-
482
- export async function pullSqliteSnapshot({ root = process.cwd(), database, now = new Date() }) {
483
- const location = await databasePath(root, database);
484
- const connection = await openDatabase(location.target, { readOnly: true });
485
- let snapshot;
486
- let revision;
487
- try {
488
- const state = readValidatedState(connection);
489
- revision = state.revisions.at(-1);
490
- if (!revision) throw new Error("SQLite transport has no published snapshot");
491
- snapshot = structuredClone(revision.snapshot);
492
- } finally {
493
- connection.close();
494
- }
495
- const imported = await importHttpsSnapshot({ root, snapshot, now });
496
- return {
497
- ...imported, transport: "sqlite", database: location.target,
498
- sequence: revision.sequence, revisionDigest: revision.revision_digest,
499
- authority: "context-only"
500
- };
501
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmod, lstat, mkdir, realpath, stat } from "node:fs/promises";
3
+ import { basename, dirname, resolve } from "node:path";
4
+ import { verifyEnvelope } from "./authentication.js";
5
+ import { buildCatalog } from "./catalog.js";
6
+ import { buildHttpsSnapshot, importHttpsSnapshot, validateHttpsSnapshot } from "./https-transport.js";
7
+ import { isInside } from "./paths.js";
8
+ import { readDirectoryExchange } from "./sharing.js";
9
+
10
+ const SCHEMA = "agentspine.sqlite/v1";
11
+ const REVISION_SCHEMA = "agentspine.sqlite-revision/v1";
12
+ const CONFIRMATION = "local-share-confirmed";
13
+ const MAX_DATABASE_BYTES = 128 * 1024 * 1024;
14
+ const MAX_SNAPSHOT_BYTES = 21 * 1024 * 1024;
15
+ const MAX_REVISIONS = 1000;
16
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
17
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
18
+ const TABLES = ["agentspine_head", "agentspine_meta", "agentspine_revisions"];
19
+ const COLUMNS = {
20
+ agentspine_meta: [
21
+ ["id", "INTEGER", 0, 1], ["schema", "TEXT", 1, 0], ["scope_id", "TEXT", 1, 0],
22
+ ["adapter_id", "TEXT", 1, 0], ["manifest_json", "TEXT", 1, 0],
23
+ ["manifest_digest", "TEXT", 1, 0], ["signer_id", "TEXT", 1, 0],
24
+ ["key_id", "TEXT", 1, 0], ["created_at", "TEXT", 1, 0], ["authority", "TEXT", 1, 0]
25
+ ],
26
+ agentspine_revisions: [
27
+ ["sequence", "INTEGER", 0, 1], ["snapshot_id", "TEXT", 1, 0],
28
+ ["snapshot_digest", "TEXT", 1, 0], ["previous_revision_digest", "TEXT", 0, 0],
29
+ ["snapshot_json", "TEXT", 1, 0], ["created_at", "TEXT", 1, 0],
30
+ ["authority", "TEXT", 1, 0], ["revision_digest", "TEXT", 1, 0]
31
+ ],
32
+ agentspine_head: [
33
+ ["id", "INTEGER", 0, 1], ["sequence", "INTEGER", 1, 0],
34
+ ["revision_digest", "TEXT", 1, 0], ["snapshot_digest", "TEXT", 1, 0],
35
+ ["authority", "TEXT", 1, 0]
36
+ ]
37
+ };
38
+
39
+ function canonical(value) {
40
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
41
+ if (value && typeof value === "object") {
42
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
43
+ }
44
+ return JSON.stringify(value);
45
+ }
46
+
47
+ function digest(value) {
48
+ return createHash("sha256").update(canonical(value)).digest("hex");
49
+ }
50
+
51
+ function exactKeys(value, expected) {
52
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
53
+ const actual = Object.keys(value).sort();
54
+ const wanted = [...expected].sort();
55
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
56
+ }
57
+
58
+ function validDate(value) {
59
+ return typeof value === "string" && value.length > 0 && Number.isFinite(new Date(value).getTime());
60
+ }
61
+
62
+ async function sqliteModule() {
63
+ const [major, minor] = process.versions.node.split(".").map(Number);
64
+ if (major < 22 || (major === 22 && minor < 13)) {
65
+ throw new Error("SQLite transport requires Node.js 22.13 or newer with node:sqlite support");
66
+ }
67
+ try {
68
+ return await import("node:sqlite");
69
+ } catch (error) {
70
+ throw new Error("SQLite transport requires Node.js 22.13 or newer with node:sqlite support", {
71
+ cause: error
72
+ });
73
+ }
74
+ }
75
+
76
+ async function prospectiveCanonicalPath(path) {
77
+ let ancestor = path;
78
+ const missing = [];
79
+ while (true) {
80
+ try {
81
+ const canonicalAncestor = await realpath(ancestor);
82
+ return resolve(canonicalAncestor, ...missing.reverse());
83
+ } catch (error) {
84
+ if (error.code !== "ENOENT") throw error;
85
+ const parent = dirname(ancestor);
86
+ if (parent === ancestor) throw error;
87
+ missing.push(basename(ancestor));
88
+ ancestor = parent;
89
+ }
90
+ }
91
+ }
92
+
93
+ async function databasePath(root, database, { create = false } = {}) {
94
+ if (!database || typeof database !== "string" || database === ":memory:") {
95
+ throw new Error("a file-backed SQLite database path is required");
96
+ }
97
+ const catalog = await buildCatalog(root);
98
+ const target = resolve(database);
99
+ const canonicalTarget = await prospectiveCanonicalPath(target);
100
+ if (isInside(catalog.root, canonicalTarget)) {
101
+ throw new Error("SQLite transport database must remain outside the scanned project");
102
+ }
103
+ if (create) await mkdir(dirname(target), { recursive: true, mode: 0o700 });
104
+ let metadata = null;
105
+ try {
106
+ metadata = await lstat(target);
107
+ } catch (error) {
108
+ if (error.code !== "ENOENT" || !create) throw error;
109
+ }
110
+ if (metadata && (!metadata.isFile() || metadata.isSymbolicLink())) {
111
+ throw new Error("SQLite transport path must be a real regular file");
112
+ }
113
+ if (metadata && metadata.nlink !== 1) {
114
+ throw new Error("SQLite transport database must not be a hard link");
115
+ }
116
+ if (metadata && metadata.size > MAX_DATABASE_BYTES) {
117
+ throw new Error("SQLite transport database exceeds the 128 MiB limit");
118
+ }
119
+ for (const suffix of ["-journal", "-wal", "-shm"]) {
120
+ try {
121
+ const sidecar = await lstat(`${target}${suffix}`);
122
+ if (!metadata) throw new Error("SQLite transport sidecar exists without its database");
123
+ if (!sidecar.isFile() || sidecar.isSymbolicLink() || sidecar.nlink !== 1) {
124
+ throw new Error("SQLite transport sidecars must be real, single-link regular files");
125
+ }
126
+ if (sidecar.size > MAX_DATABASE_BYTES) throw new Error("SQLite transport sidecar exceeds the 128 MiB limit");
127
+ } catch (error) {
128
+ if (error.code !== "ENOENT") throw error;
129
+ }
130
+ }
131
+ const parent = await realpath(dirname(target));
132
+ if (isInside(catalog.root, parent)) {
133
+ throw new Error("SQLite transport database must remain outside the scanned project");
134
+ }
135
+ return { catalog, target: resolve(parent, basename(target)), exists: Boolean(metadata) };
136
+ }
137
+
138
+ async function openDatabase(path, { readOnly = false } = {}) {
139
+ const { DatabaseSync } = await sqliteModule();
140
+ let database;
141
+ try {
142
+ database = new DatabaseSync(path, {
143
+ readOnly, enableForeignKeyConstraints: true,
144
+ enableDoubleQuotedStringLiterals: false, allowExtension: false
145
+ });
146
+ } catch (error) {
147
+ throw new Error(`SQLite transport database could not be opened: ${error.message}`, { cause: error });
148
+ }
149
+ try {
150
+ database.exec("PRAGMA trusted_schema = OFF; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
151
+ if (typeof database.enableDefensive === "function") database.enableDefensive(true);
152
+ if (readOnly) database.exec("PRAGMA query_only = ON;");
153
+ else database.exec("PRAGMA synchronous = FULL;");
154
+ return database;
155
+ } catch (error) {
156
+ database.close();
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ function createSchema(database) {
162
+ database.exec(`
163
+ CREATE TABLE IF NOT EXISTS agentspine_meta (
164
+ id INTEGER PRIMARY KEY CHECK (id = 1),
165
+ schema TEXT NOT NULL CHECK (schema = 'agentspine.sqlite/v1'),
166
+ scope_id TEXT NOT NULL,
167
+ adapter_id TEXT NOT NULL,
168
+ manifest_json TEXT NOT NULL,
169
+ manifest_digest TEXT NOT NULL,
170
+ signer_id TEXT NOT NULL,
171
+ key_id TEXT NOT NULL,
172
+ created_at TEXT NOT NULL,
173
+ authority TEXT NOT NULL CHECK (authority = 'context-only')
174
+ ) STRICT;
175
+ CREATE TABLE IF NOT EXISTS agentspine_revisions (
176
+ sequence INTEGER PRIMARY KEY CHECK (sequence > 0),
177
+ snapshot_id TEXT NOT NULL,
178
+ snapshot_digest TEXT NOT NULL UNIQUE,
179
+ previous_revision_digest TEXT,
180
+ snapshot_json TEXT NOT NULL,
181
+ created_at TEXT NOT NULL,
182
+ authority TEXT NOT NULL CHECK (authority = 'context-only'),
183
+ revision_digest TEXT NOT NULL UNIQUE
184
+ ) STRICT;
185
+ CREATE TABLE IF NOT EXISTS agentspine_head (
186
+ id INTEGER PRIMARY KEY CHECK (id = 1),
187
+ sequence INTEGER NOT NULL,
188
+ revision_digest TEXT NOT NULL,
189
+ snapshot_digest TEXT NOT NULL,
190
+ authority TEXT NOT NULL CHECK (authority = 'context-only')
191
+ ) STRICT;
192
+ PRAGMA user_version = 1;
193
+ `);
194
+ }
195
+
196
+ function parseJson(value, label, maximum = MAX_SNAPSHOT_BYTES) {
197
+ if (typeof value !== "string" || Buffer.byteLength(value) > maximum) {
198
+ throw new Error(`${label} exceeds its validation limit`);
199
+ }
200
+ try {
201
+ return JSON.parse(value);
202
+ } catch {
203
+ throw new Error(`${label} is not valid JSON`);
204
+ }
205
+ }
206
+
207
+ function validateSchema(database) {
208
+ const integrity = database.prepare("PRAGMA integrity_check").get();
209
+ if (!integrity || Object.values(integrity)[0] !== "ok") throw new Error("SQLite transport integrity check failed");
210
+ const objects = database.prepare(`
211
+ SELECT type, name FROM sqlite_master
212
+ WHERE name NOT LIKE 'sqlite_%'
213
+ ORDER BY type, name
214
+ `).all();
215
+ if (objects.length !== TABLES.length
216
+ || objects.some((item) => item.type !== "table")
217
+ || objects.map((item) => item.name).sort().some((name, index) => name !== TABLES[index])) {
218
+ throw new Error("SQLite transport schema contains missing or unexpected objects");
219
+ }
220
+ for (const table of TABLES) {
221
+ const columns = database.prepare(`PRAGMA table_info(${table})`).all();
222
+ const expected = COLUMNS[table];
223
+ if (columns.length !== expected.length || columns.some((column, index) => {
224
+ const wanted = expected[index];
225
+ return column.name !== wanted[0] || String(column.type).toUpperCase() !== wanted[1]
226
+ || Number(column.notnull) !== wanted[2] || Number(column.pk) !== wanted[3]
227
+ || column.dflt_value !== null;
228
+ })) throw new Error(`SQLite transport table layout is invalid: ${table}`);
229
+ }
230
+ for (const table of ["agentspine_meta", "agentspine_head"]) {
231
+ if (database.prepare(`PRAGMA index_list(${table})`).all().length !== 0) {
232
+ throw new Error(`SQLite transport table has an unexpected index: ${table}`);
233
+ }
234
+ }
235
+ const revisionIndexes = database.prepare("PRAGMA index_list(agentspine_revisions)").all();
236
+ if (revisionIndexes.length !== 2 || revisionIndexes.some((index) => (
237
+ Number(index.unique) !== 1 || index.origin !== "u" || Number(index.partial) !== 0
238
+ ))) throw new Error("SQLite transport revision indexes are invalid");
239
+ const expectedIndexes = new Map([
240
+ ["sqlite_autoindex_agentspine_revisions_1", "snapshot_digest"],
241
+ ["sqlite_autoindex_agentspine_revisions_2", "revision_digest"]
242
+ ]);
243
+ for (const index of revisionIndexes) {
244
+ const expectedColumn = expectedIndexes.get(index.name);
245
+ if (!expectedColumn) throw new Error("SQLite transport revision indexes are invalid");
246
+ const columns = database.prepare(`PRAGMA index_info(${index.name})`).all();
247
+ if (columns.length !== 1 || columns[0].name !== expectedColumn) {
248
+ throw new Error("SQLite transport revision indexes are invalid");
249
+ }
250
+ }
251
+ const version = database.prepare("PRAGMA user_version").get();
252
+ if (!version || Number(Object.values(version)[0]) !== 1) throw new Error("SQLite transport schema version is unsupported");
253
+ }
254
+
255
+ function validateManifestMetadata(row) {
256
+ if (!row || row.schema !== SCHEMA || !ID_RE.test(row.scope_id || "")
257
+ || !ID_RE.test(row.adapter_id || "") || !ID_RE.test(row.signer_id || "")
258
+ || !ID_RE.test(row.key_id || "") || !DIGEST_RE.test(row.manifest_digest || "")
259
+ || !validDate(row.created_at) || row.authority !== "context-only") {
260
+ throw new Error("SQLite transport metadata is invalid");
261
+ }
262
+ const manifest = parseJson(row.manifest_json, "SQLite signed manifest", 64 * 1024);
263
+ const verified = verifyEnvelope(manifest, "manifest");
264
+ if (digest(manifest) !== row.manifest_digest
265
+ || verified.payload?.scopeId !== row.scope_id || verified.payload?.adapterId !== row.adapter_id
266
+ || verified.payload?.adapter !== "directory" || verified.publicIdentity.signerId !== row.signer_id
267
+ || verified.publicIdentity.keyId !== row.key_id) {
268
+ throw new Error("SQLite transport metadata does not match its signed manifest");
269
+ }
270
+ return { manifest, verified };
271
+ }
272
+
273
+ function revisionBody(row) {
274
+ return {
275
+ schema: REVISION_SCHEMA, sequence: row.sequence, snapshotId: row.snapshot_id,
276
+ snapshotDigest: row.snapshot_digest, previousRevisionDigest: row.previous_revision_digest,
277
+ createdAt: row.created_at, authority: row.authority
278
+ };
279
+ }
280
+
281
+ function readValidatedState(database) {
282
+ validateSchema(database);
283
+ const metadata = database.prepare("SELECT * FROM agentspine_meta WHERE id = 1").get();
284
+ const { manifest, verified } = validateManifestMetadata(metadata);
285
+ const rows = database.prepare("SELECT * FROM agentspine_revisions ORDER BY sequence").all();
286
+ if (rows.length > MAX_REVISIONS) throw new Error(`SQLite transport exceeds ${MAX_REVISIONS} retained revisions`);
287
+ let previousDigest = null;
288
+ let totalBytes = 0;
289
+ const revisions = rows.map((row, index) => {
290
+ totalBytes += Buffer.byteLength(row.snapshot_json || "");
291
+ if (totalBytes > MAX_DATABASE_BYTES) throw new Error("SQLite transport retained snapshots exceed the 128 MiB limit");
292
+ if (Number(row.sequence) !== index + 1 || !ID_RE.test(row.snapshot_id || "")
293
+ || !DIGEST_RE.test(row.snapshot_digest || "")
294
+ || row.previous_revision_digest !== previousDigest || !validDate(row.created_at)
295
+ || row.authority !== "context-only" || !DIGEST_RE.test(row.revision_digest || "")
296
+ || digest(revisionBody(row)) !== row.revision_digest) {
297
+ throw new Error("SQLite transport revision chain is invalid");
298
+ }
299
+ const snapshot = validateHttpsSnapshot(parseJson(row.snapshot_json, "SQLite snapshot"));
300
+ if (snapshot.digest !== row.snapshot_digest || snapshot.snapshotId !== row.snapshot_id
301
+ || snapshot.scopeId !== metadata.scope_id || snapshot.adapterId !== metadata.adapter_id
302
+ || digest(snapshot.manifest) !== metadata.manifest_digest) {
303
+ throw new Error("SQLite transport revision does not match its authenticated metadata");
304
+ }
305
+ previousDigest = row.revision_digest;
306
+ return { ...row, snapshot };
307
+ });
308
+ const head = database.prepare("SELECT * FROM agentspine_head WHERE id = 1").get();
309
+ if (revisions.length === 0) {
310
+ if (head) throw new Error("SQLite transport head exists without a revision");
311
+ } else {
312
+ const latest = revisions.at(-1);
313
+ if (!head || !exactKeys(head, ["id", "sequence", "revision_digest", "snapshot_digest", "authority"])
314
+ || Number(head.sequence) !== latest.sequence || head.revision_digest !== latest.revision_digest
315
+ || head.snapshot_digest !== latest.snapshot_digest || head.authority !== "context-only") {
316
+ throw new Error("SQLite transport head does not match the append-only revision chain");
317
+ }
318
+ }
319
+ return { metadata, manifest, verified, revisions, head };
320
+ }
321
+
322
+ async function secureFile(path) {
323
+ await chmod(path, 0o600);
324
+ const metadata = await stat(path);
325
+ if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size > MAX_DATABASE_BYTES) {
326
+ throw new Error("SQLite transport database is not a bounded regular file");
327
+ }
328
+ }
329
+
330
+ export async function initSqliteAdapter({
331
+ root = process.cwd(), directory, database, confirmation, now = new Date()
332
+ }) {
333
+ if (confirmation !== CONFIRMATION) throw new Error("SQLite adapter initialization requires explicit local owner confirmation");
334
+ const exchange = await readDirectoryExchange({ root, directory, requireAuthenticated: true });
335
+ const manifestVerification = verifyEnvelope(exchange.manifest, "manifest");
336
+ const location = await databasePath(root, database, { create: true });
337
+ const connection = await openDatabase(location.target);
338
+ let completed = false;
339
+ let created = false;
340
+ try {
341
+ connection.exec("BEGIN IMMEDIATE");
342
+ try {
343
+ createSchema(connection);
344
+ const existing = connection.prepare("SELECT * FROM agentspine_meta WHERE id = 1").get();
345
+ if (existing) {
346
+ const validated = validateManifestMetadata(existing);
347
+ if (existing.scope_id !== exchange.scopeId || existing.adapter_id !== exchange.adapterId
348
+ || existing.manifest_digest !== digest(exchange.manifest)
349
+ || validated.verified.publicIdentity.keyId !== manifestVerification.publicIdentity.keyId) {
350
+ throw new Error("SQLite transport already belongs to a different signed adapter");
351
+ }
352
+ } else {
353
+ created = true;
354
+ connection.prepare(`
355
+ INSERT INTO agentspine_meta
356
+ (id, schema, scope_id, adapter_id, manifest_json, manifest_digest, signer_id, key_id, created_at, authority)
357
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, 'context-only')
358
+ `).run(
359
+ SCHEMA, exchange.scopeId, exchange.adapterId, JSON.stringify(exchange.manifest), digest(exchange.manifest),
360
+ manifestVerification.publicIdentity.signerId, manifestVerification.publicIdentity.keyId,
361
+ new Date(now).toISOString()
362
+ );
363
+ }
364
+ connection.exec("COMMIT");
365
+ } catch (error) {
366
+ try { connection.exec("ROLLBACK"); } catch {}
367
+ throw error;
368
+ }
369
+ const state = readValidatedState(connection);
370
+ completed = true;
371
+ return {
372
+ created, database: location.target, schema: SCHEMA,
373
+ scopeId: state.metadata.scope_id, adapterId: state.metadata.adapter_id,
374
+ signer: { signerId: state.metadata.signer_id, keyId: state.metadata.key_id },
375
+ revisions: state.revisions.length, authority: "context-only"
376
+ };
377
+ } finally {
378
+ connection.close();
379
+ if (completed) await secureFile(location.target);
380
+ }
381
+ }
382
+
383
+ export async function publishSqliteSnapshot({
384
+ root = process.cwd(), directory, database, snapshotId = `snapshot:${randomUUID()}`,
385
+ confirmation, now = new Date()
386
+ }) {
387
+ if (confirmation !== CONFIRMATION) throw new Error("SQLite snapshot publication requires explicit local owner confirmation");
388
+ const snapshot = await buildHttpsSnapshot({ root, directory, snapshotId, now });
389
+ const location = await databasePath(root, database);
390
+ const connection = await openDatabase(location.target);
391
+ let completed = false;
392
+ try {
393
+ connection.exec("BEGIN IMMEDIATE");
394
+ try {
395
+ const state = readValidatedState(connection);
396
+ if (snapshot.scopeId !== state.metadata.scope_id || snapshot.adapterId !== state.metadata.adapter_id
397
+ || digest(snapshot.manifest) !== state.metadata.manifest_digest) {
398
+ throw new Error("SQLite snapshot does not belong to this authenticated adapter");
399
+ }
400
+ const existing = connection.prepare("SELECT * FROM agentspine_revisions WHERE snapshot_digest = ?").get(snapshot.digest);
401
+ if (existing) {
402
+ connection.exec("COMMIT");
403
+ completed = true;
404
+ return {
405
+ created: false, database: location.target, sequence: existing.sequence,
406
+ snapshotId: existing.snapshot_id, snapshotDigest: existing.snapshot_digest,
407
+ revisionDigest: existing.revision_digest, events: snapshot.events.length,
408
+ authority: "context-only"
409
+ };
410
+ }
411
+ const sequence = state.revisions.length + 1;
412
+ const previousRevisionDigest = state.revisions.at(-1)?.revision_digest || null;
413
+ const createdAt = new Date(now).toISOString();
414
+ if (!validDate(createdAt)) throw new Error("now must be a valid date");
415
+ const body = {
416
+ schema: REVISION_SCHEMA, sequence, snapshotId: snapshot.snapshotId,
417
+ snapshotDigest: snapshot.digest, previousRevisionDigest, createdAt,
418
+ authority: "context-only"
419
+ };
420
+ const revisionDigest = digest(body);
421
+ connection.prepare(`
422
+ INSERT INTO agentspine_revisions
423
+ (sequence, snapshot_id, snapshot_digest, previous_revision_digest, snapshot_json, created_at, authority, revision_digest)
424
+ VALUES (?, ?, ?, ?, ?, ?, 'context-only', ?)
425
+ `).run(sequence, snapshot.snapshotId, snapshot.digest, previousRevisionDigest, JSON.stringify(snapshot), createdAt, revisionDigest);
426
+ if (state.head) {
427
+ const changed = connection.prepare(`
428
+ UPDATE agentspine_head SET sequence = ?, revision_digest = ?, snapshot_digest = ?
429
+ WHERE id = 1 AND sequence = ? AND revision_digest = ? AND authority = 'context-only'
430
+ `).run(sequence, revisionDigest, snapshot.digest, state.head.sequence, state.head.revision_digest);
431
+ if (Number(changed.changes) !== 1) throw new Error("SQLite transport head changed concurrently");
432
+ } else {
433
+ connection.prepare(`
434
+ INSERT INTO agentspine_head (id, sequence, revision_digest, snapshot_digest, authority)
435
+ VALUES (1, ?, ?, ?, 'context-only')
436
+ `).run(sequence, revisionDigest, snapshot.digest);
437
+ }
438
+ connection.exec("COMMIT");
439
+ completed = true;
440
+ return {
441
+ created: true, database: location.target, sequence, snapshotId: snapshot.snapshotId,
442
+ snapshotDigest: snapshot.digest, revisionDigest, previousRevisionDigest,
443
+ events: snapshot.events.length, authority: "context-only"
444
+ };
445
+ } catch (error) {
446
+ try { connection.exec("ROLLBACK"); } catch {}
447
+ throw error;
448
+ }
449
+ } finally {
450
+ connection.close();
451
+ if (completed) await secureFile(location.target);
452
+ }
453
+ }
454
+
455
+ export async function inspectSqliteAdapter({ root = process.cwd(), database }) {
456
+ const location = await databasePath(root, database);
457
+ const connection = await openDatabase(location.target, { readOnly: true });
458
+ try {
459
+ const state = readValidatedState(connection);
460
+ return {
461
+ database: location.target, schema: SCHEMA, scopeId: state.metadata.scope_id,
462
+ adapterId: state.metadata.adapter_id,
463
+ signer: { signerId: state.metadata.signer_id, keyId: state.metadata.key_id },
464
+ revisions: state.revisions.length,
465
+ head: state.head ? {
466
+ sequence: state.head.sequence, revisionDigest: state.head.revision_digest,
467
+ snapshotDigest: state.head.snapshot_digest
468
+ } : null,
469
+ snapshots: state.revisions.map((item) => ({
470
+ sequence: item.sequence, snapshotId: item.snapshot_id,
471
+ snapshotDigest: item.snapshot_digest, revisionDigest: item.revision_digest,
472
+ previousRevisionDigest: item.previous_revision_digest, createdAt: item.created_at,
473
+ events: item.snapshot.events.length, authority: "context-only"
474
+ })),
475
+ authority: "context-only"
476
+ };
477
+ } finally {
478
+ connection.close();
479
+ }
480
+ }
481
+
482
+ export async function pullSqliteSnapshot({ root = process.cwd(), database, now = new Date() }) {
483
+ const location = await databasePath(root, database);
484
+ const connection = await openDatabase(location.target, { readOnly: true });
485
+ let snapshot;
486
+ let revision;
487
+ try {
488
+ const state = readValidatedState(connection);
489
+ revision = state.revisions.at(-1);
490
+ if (!revision) throw new Error("SQLite transport has no published snapshot");
491
+ snapshot = structuredClone(revision.snapshot);
492
+ } finally {
493
+ connection.close();
494
+ }
495
+ const imported = await importHttpsSnapshot({ root, snapshot, now });
496
+ return {
497
+ ...imported, transport: "sqlite", database: location.target,
498
+ sequence: revision.sequence, revisionDigest: revision.revision_digest,
499
+ authority: "context-only"
500
+ };
501
+ }