@dsh-enhanced/plugin-control-plane 0.1.66 → 0.1.67

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 (43) hide show
  1. package/README.md +42 -2
  2. package/lib/creation-capability-execution.d.ts +5 -1
  3. package/lib/creation-capability-execution.d.ts.map +1 -1
  4. package/lib/creation-capability-execution.js +42 -0
  5. package/lib/creation-capability-execution.js.map +1 -1
  6. package/lib/creation-capability-runtime.d.ts +47 -1
  7. package/lib/creation-capability-runtime.d.ts.map +1 -1
  8. package/lib/creation-capability-runtime.js +103 -1
  9. package/lib/creation-capability-runtime.js.map +1 -1
  10. package/lib/index.d.ts +3 -0
  11. package/lib/index.d.ts.map +1 -1
  12. package/lib/index.js +2 -0
  13. package/lib/index.js.map +1 -1
  14. package/lib/revision-capability-journal.d.ts +103 -0
  15. package/lib/revision-capability-journal.d.ts.map +1 -0
  16. package/lib/revision-capability-journal.js +872 -0
  17. package/lib/revision-capability-journal.js.map +1 -0
  18. package/lib/revision-capability-runtime.d.ts +74 -0
  19. package/lib/revision-capability-runtime.d.ts.map +1 -0
  20. package/lib/revision-capability-runtime.js +562 -0
  21. package/lib/revision-capability-runtime.js.map +1 -0
  22. package/lib/revision-capability-types.d.ts +289 -0
  23. package/lib/revision-capability-types.d.ts.map +1 -0
  24. package/lib/revision-capability-types.js +2 -0
  25. package/lib/revision-capability-types.js.map +1 -0
  26. package/lib/service.d.ts +25 -1
  27. package/lib/service.d.ts.map +1 -1
  28. package/lib/service.js +194 -3
  29. package/lib/service.js.map +1 -1
  30. package/lib/source-jobs.d.ts +5 -0
  31. package/lib/source-jobs.d.ts.map +1 -1
  32. package/lib/source-jobs.js +25 -4
  33. package/lib/source-jobs.js.map +1 -1
  34. package/lib/source-workspace.d.ts.map +1 -1
  35. package/lib/source-workspace.js +0 -1
  36. package/lib/source-workspace.js.map +1 -1
  37. package/lib/store.d.ts +1 -1
  38. package/lib/store.d.ts.map +1 -1
  39. package/lib/store.js +7 -3
  40. package/lib/store.js.map +1 -1
  41. package/lib/version.d.ts +1 -1
  42. package/lib/version.js +1 -1
  43. package/package.json +8 -8
@@ -0,0 +1,872 @@
1
+ import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { dirname, isAbsolute, resolve } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { canonicalGrowthJson, validatePluginCreationVerificationCertificate, validatePluginRevisionVerificationCertificate } from '@dsh-enhanced/assistant-growth-contract';
6
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/u;
7
+ const SHA = /^[a-f0-9]{64}$/u;
8
+ const ALIAS = /^[a-z][a-z0-9_-]{0,95}$/u;
9
+ const APP_ID = 0x44535241;
10
+ const SEMANTICS = 'dsh-created-capability-revision-journal/v1:reserve-before-dispatch:unknown-no-retry:parent-cas:append-only-versions';
11
+ const DOMAIN = 'dsh-created-capability-revision-adoption-v1\0';
12
+ const MAX_ARTIFACT = 512 * 1024;
13
+ const MAX_JSON = 65_536;
14
+ const TABLES = ['adoptions', 'authority', 'calls', 'entries', 'rollbacks', 'versions'];
15
+ function fail() { throw new Error('revision capability journal rejected input or state'); }
16
+ function obj(value, expected) {
17
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype
18
+ || Reflect.ownKeys(value).length !== expected.length)
19
+ fail();
20
+ const descriptors = Object.getOwnPropertyDescriptors(value);
21
+ if (Object.keys(descriptors).sort().join('\0') !== [...expected].sort().join('\0')
22
+ || !Object.values(descriptors).every(descriptor => descriptor.enumerable && 'value' in descriptor))
23
+ fail();
24
+ return value;
25
+ }
26
+ function str(value, pattern = ID, max = 512) {
27
+ if (typeof value !== 'string' || !pattern.test(value) || value.normalize('NFC') !== value
28
+ || Buffer.byteLength(value) > max || /[\p{Cc}]/u.test(value))
29
+ fail();
30
+ return value;
31
+ }
32
+ function int(value, min, max) {
33
+ if (!Number.isSafeInteger(value) || Number(value) < min || Number(value) > max)
34
+ fail();
35
+ return Number(value);
36
+ }
37
+ function path(value) {
38
+ const result = str(value, /^[\s\S]+$/u, 4096);
39
+ if (!isAbsolute(result) || result === '/' || resolve(result) !== result)
40
+ fail();
41
+ return result;
42
+ }
43
+ function privateStat(file, kind) {
44
+ const stat = lstatSync(file);
45
+ if (realpathSync(file) !== file || (kind === 'file' ? !stat.isFile() || stat.nlink !== 1 : !stat.isDirectory())
46
+ || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)
47
+ fail();
48
+ }
49
+ function keyBytes(file) {
50
+ privateStat(dirname(file), 'directory');
51
+ privateStat(file, 'file');
52
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
53
+ try {
54
+ const before = fstatSync(fd, { bigint: true }), named = lstatSync(file, { bigint: true });
55
+ if (before.size > 32768n || before.ino !== named.ino || before.dev !== named.dev)
56
+ fail();
57
+ const bytes = readFileSync(fd), after = fstatSync(fd, { bigint: true });
58
+ if (bytes.length > 32768 || before.ino !== after.ino || before.dev !== after.dev
59
+ || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs)
60
+ fail();
61
+ return bytes;
62
+ }
63
+ finally {
64
+ closeSync(fd);
65
+ }
66
+ }
67
+ function json(value, max = MAX_JSON) {
68
+ const seen = new Set();
69
+ let nodes = 0;
70
+ const walk = (item, depth) => {
71
+ if (++nodes > 8192 || depth > 32)
72
+ fail();
73
+ if (item === null || typeof item === 'string' || typeof item === 'boolean')
74
+ return;
75
+ if (typeof item === 'number' && Number.isFinite(item))
76
+ return;
77
+ if (!item || typeof item !== 'object' || seen.has(item))
78
+ fail();
79
+ seen.add(item);
80
+ if (Array.isArray(item)) {
81
+ if (Object.getPrototypeOf(item) !== Array.prototype || Reflect.ownKeys(item).length !== item.length + 1)
82
+ fail();
83
+ for (let index = 0; index < item.length; index++) {
84
+ if (!Object.hasOwn(item, index))
85
+ fail();
86
+ walk(item[index], depth + 1);
87
+ }
88
+ }
89
+ else {
90
+ const descriptors = Object.getOwnPropertyDescriptors(item);
91
+ if ((Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null)
92
+ || Reflect.ownKeys(item).length !== Object.keys(descriptors).length
93
+ || !Object.values(descriptors).every(descriptor => descriptor.enumerable && 'value' in descriptor))
94
+ fail();
95
+ for (const [name, descriptor] of Object.entries(descriptors)) {
96
+ if (name === '__proto__' || name === 'constructor' || name === 'prototype')
97
+ fail();
98
+ walk(descriptor.value, depth + 1);
99
+ }
100
+ }
101
+ seen.delete(item);
102
+ };
103
+ walk(value, 0);
104
+ const encoded = canonicalGrowthJson(value);
105
+ if (Buffer.byteLength(encoded) > max)
106
+ fail();
107
+ return encoded;
108
+ }
109
+ const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
110
+ function digest(value) { return sha(json(value)); }
111
+ function model(value) {
112
+ const item = obj(value, Object.hasOwn(value, 'reasoningEffort') ? ['provider', 'model', 'reasoningEffort'] : ['provider', 'model']);
113
+ str(item.provider, /^[\s\S]+$/u, 500);
114
+ str(item.model, /^[\s\S]+$/u, 500);
115
+ if (Object.hasOwn(item, 'reasoningEffort'))
116
+ str(item.reasoningEffort, /^[\s\S]+$/u, 500);
117
+ return value;
118
+ }
119
+ function sourceRef(value) {
120
+ const item = obj(value, ['referenceDigest', 'ownerDigest', 'growthRunDigest']);
121
+ for (const name of ['referenceDigest', 'ownerDigest', 'growthRunDigest'])
122
+ str(item[name], SHA, 64);
123
+ return value;
124
+ }
125
+ function aliases(value) {
126
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length < 1 || value.length > 16
127
+ || Reflect.ownKeys(value).length !== value.length + 1)
128
+ fail();
129
+ const names = new Set();
130
+ for (let index = 0; index < value.length; index++) {
131
+ if (!Object.hasOwn(value, index))
132
+ fail();
133
+ const alias = str(value[index], ALIAS, 96);
134
+ if (names.has(alias))
135
+ fail();
136
+ names.add(alias);
137
+ }
138
+ return Object.freeze([...value]);
139
+ }
140
+ function tools(value) {
141
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length < 1 || value.length > 16
142
+ || Reflect.ownKeys(value).length !== value.length + 1)
143
+ fail();
144
+ const names = new Set();
145
+ for (const entry of value) {
146
+ const tool = obj(entry, ['originalName', 'name', 'description', 'parameters']);
147
+ str(tool.originalName, ALIAS, 96);
148
+ str(tool.name, ALIAS, 96);
149
+ str(tool.description, /^[\s\S]+$/u, 2048);
150
+ if (names.has(tool.name))
151
+ fail();
152
+ names.add(tool.name);
153
+ if (!tool.parameters || typeof tool.parameters !== 'object' || Array.isArray(tool.parameters))
154
+ fail();
155
+ json(tool.parameters);
156
+ }
157
+ json(value);
158
+ return value;
159
+ }
160
+ /** Read-only preflight; it never creates a signing key or ledger. */
161
+ export function validateRevisionCapabilityConfig(value) {
162
+ const v = obj(value, ['authorityId', 'keyId', 'keyPath', 'owner', 'namePrefix', 'expiresAt', 'maxAdoptions',
163
+ 'maxInputBytes', 'maxCallRecords', 'rollback', ...(Object.hasOwn(value, 'retention') ? ['retention'] : [])]);
164
+ str(v.authorityId, ID);
165
+ str(v.keyId, ID);
166
+ path(v.keyPath);
167
+ const owner = obj(v.owner, ['authorityId', 'authorityHash', 'principalId', 'principalRecordId', 'principalVersion', 'workspace', 'agentPreset']);
168
+ str(owner.authorityId, ID);
169
+ str(owner.authorityHash, SHA, 64);
170
+ str(owner.principalId, /^[\s\S]+$/u, 512);
171
+ str(owner.principalRecordId, /^[\s\S]+$/u, 512);
172
+ int(owner.principalVersion, 1, Number.MAX_SAFE_INTEGER);
173
+ path(owner.workspace);
174
+ str(owner.agentPreset, ID);
175
+ str(v.namePrefix, /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-$/u, 48);
176
+ int(v.expiresAt, 1, 8_640_000_000_000_000);
177
+ int(v.maxAdoptions, 1, 32);
178
+ int(v.maxInputBytes, 1, 65_536);
179
+ int(v.maxCallRecords, 1, 1_024);
180
+ const rollback = obj(v.rollback, ['authorityId', 'expiresAt', 'maxRollbacks']);
181
+ str(rollback.authorityId, ID);
182
+ int(rollback.expiresAt, 1, 8_640_000_000_000_000);
183
+ int(rollback.maxRollbacks, 1, 32);
184
+ if (Object.hasOwn(v, 'retention')) {
185
+ const retention = obj(v.retention, ['maximumLifetimeMs']);
186
+ int(retention.maximumLifetimeMs, 1, 30 * 86_400_000);
187
+ }
188
+ const privateKey = createPrivateKey(keyBytes(v.keyPath));
189
+ if (privateKey.asymmetricKeyType !== 'ed25519')
190
+ fail();
191
+ json(value);
192
+ }
193
+ function canonicalConfig(config) {
194
+ return structuredClone(config);
195
+ }
196
+ /** Stable public identity for preflight separation from acceptance and creation adoption keys. */
197
+ export function revisionCapabilityPublicKey(config) {
198
+ validateRevisionCapabilityConfig(config);
199
+ return createPublicKey(createPrivateKey(keyBytes(config.keyPath))).export({ type: 'spki', format: 'pem' }).toString();
200
+ }
201
+ /**
202
+ * Stable journal authority identity: canonical journal path, the full grant and the signing key file
203
+ * fingerprint. A missing, non-canonical or non-private key fails closed before any ledger exists.
204
+ */
205
+ export function revisionCapabilityAuthorityDigest(input) {
206
+ return digest({ semantics: SEMANTICS, path: path(input.path), config: canonicalConfig(input.config),
207
+ keyFingerprint: sha(keyBytes(input.config.keyPath)) });
208
+ }
209
+ const receiptKeys = ['protocol', 'authorityId', 'authorityDigest', 'keyId', 'planId', 'planDigest', 'verificationDigest',
210
+ 'parentPlanId', 'parentCertificateDigest', 'parentReceiptDigest', 'parentArtifactSha256', 'entryId', 'aliasesDigest',
211
+ 'artifactSha256', 'artifactBytes', 'schemaDigest', 'source', 'model', 'inFlightParentCalls', 'expiresAt', 'adoptedAt', 'signature'];
212
+ function receiptKeysFor(value) {
213
+ const regression = value && typeof value === 'object' ? Object.hasOwn(value, 'regressionDigest') : false;
214
+ return regression ? [...receiptKeys.slice(0, 18), 'regressionDigest', ...receiptKeys.slice(18)] : receiptKeys;
215
+ }
216
+ function receiptShape(value) {
217
+ const v = obj(value, receiptKeysFor(value));
218
+ if (v.protocol !== 'dsh-created-capability-revision-adoption/v1')
219
+ fail();
220
+ for (const name of ['authorityId', 'keyId', 'planId', 'parentPlanId', 'entryId'])
221
+ str(v[name], ID);
222
+ for (const name of ['authorityDigest', 'planDigest', 'verificationDigest', 'parentCertificateDigest', 'parentReceiptDigest',
223
+ 'parentArtifactSha256', 'aliasesDigest', 'artifactSha256', 'schemaDigest'])
224
+ str(v[name], SHA, 64);
225
+ int(v.artifactBytes, 1, MAX_ARTIFACT);
226
+ int(v.inFlightParentCalls, 0, Number.MAX_SAFE_INTEGER);
227
+ int(v.expiresAt, 1, 8_640_000_000_000_000);
228
+ int(v.adoptedAt, 1, 8_640_000_000_000_000);
229
+ sourceRef(v.source);
230
+ model(v.model);
231
+ if (Object.hasOwn(v, 'regressionDigest'))
232
+ str(v.regressionDigest, SHA, 64);
233
+ const signature = str(v.signature, /^[A-Za-z0-9_-]{86}$/u, 86);
234
+ if (Buffer.from(signature, 'base64url').length !== 64 || Buffer.from(signature, 'base64url').toString('base64url') !== signature)
235
+ fail();
236
+ return value;
237
+ }
238
+ function receiptBody(receipt) {
239
+ const { signature: _signature, ...body } = receipt;
240
+ return body;
241
+ }
242
+ export function verifyRevisionCapabilityAdoptionReceipt(value, authorityDigest, publicKey, now = Date.now()) {
243
+ try {
244
+ const receipt = receiptShape(value);
245
+ if (!SHA.test(authorityDigest) || receipt.authorityDigest !== authorityDigest || !Number.isSafeInteger(now)
246
+ || receipt.adoptedAt > now || receipt.expiresAt <= now || receipt.expiresAt <= receipt.adoptedAt)
247
+ return false;
248
+ const key = createPublicKey(publicKey);
249
+ return key.asymmetricKeyType === 'ed25519'
250
+ && verify(null, Buffer.from(DOMAIN + json(receiptBody(receipt), MAX_JSON)), key, Buffer.from(receipt.signature, 'base64url'));
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ }
256
+ /**
257
+ * Parent authenticity is inherited from the independently verified creation adoption record the Host
258
+ * port re-reads; this journal only pins its exact bytes and digest so a rollback cannot substitute
259
+ * another version.
260
+ */
261
+ function parentReceiptShape(value, planId, artifactSha256) {
262
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
263
+ fail();
264
+ const item = value;
265
+ if (item.protocol !== 'dsh-created-capability-adoption/v1' && item.protocol !== 'dsh-created-capability-adoption/v2')
266
+ fail();
267
+ for (const name of ['authorityId', 'keyId', 'planId'])
268
+ str(item[name], ID);
269
+ for (const name of ['authorityDigest', 'planDigest', 'verificationDigest', 'artifactSha256', 'schemaDigest', 'toolsDigest'])
270
+ str(item[name], SHA, 64);
271
+ int(item.artifactBytes, 1, MAX_ARTIFACT);
272
+ int(item.expiresAt, 1, 8_640_000_000_000_000);
273
+ int(item.adoptedAt, 1, 8_640_000_000_000_000);
274
+ if (item.planId !== planId || item.artifactSha256 !== artifactSha256)
275
+ fail();
276
+ const signature = str(item.signature, /^[A-Za-z0-9_-]{86}$/u, 86);
277
+ if (Buffer.from(signature, 'base64url').length !== 64 || Buffer.from(signature, 'base64url').toString('base64url') !== signature)
278
+ fail();
279
+ const encoded = json(value, MAX_JSON);
280
+ return { json: encoded, digest: sha(encoded) };
281
+ }
282
+ /** Reservations consume quota before any external action; recovery only marks uncertainty. */
283
+ export class RevisionCapabilityJournal {
284
+ authorityDigest;
285
+ publicKey;
286
+ db;
287
+ key;
288
+ identity;
289
+ ledgerPath;
290
+ config;
291
+ constructor(input) {
292
+ validateRevisionCapabilityConfig(input.config);
293
+ this.config = canonicalConfig(input.config);
294
+ this.ledgerPath = path(input.path);
295
+ if (!this.ledgerPath.endsWith('/revision-adoptions.sqlite') || this.ledgerPath === this.config.keyPath)
296
+ fail();
297
+ this.key = createPrivateKey(keyBytes(this.config.keyPath));
298
+ this.publicKey = createPublicKey(this.key).export({ type: 'spki', format: 'pem' }).toString();
299
+ this.authorityDigest = revisionCapabilityAuthorityDigest({ path: this.ledgerPath, config: this.config });
300
+ privateStat(dirname(this.ledgerPath), 'directory');
301
+ try {
302
+ const fd = openSync(this.ledgerPath, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
303
+ try {
304
+ fsyncSync(fd);
305
+ }
306
+ finally {
307
+ closeSync(fd);
308
+ }
309
+ }
310
+ catch (error) {
311
+ if (error.code !== 'EEXIST')
312
+ throw error;
313
+ }
314
+ privateStat(this.ledgerPath, 'file');
315
+ const stat = lstatSync(this.ledgerPath);
316
+ this.identity = { dev: stat.dev, ino: stat.ino };
317
+ this.db = new DatabaseSync(this.ledgerPath);
318
+ try {
319
+ this.checkFiles();
320
+ this.db.exec('PRAGMA busy_timeout=1000; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; BEGIN IMMEDIATE');
321
+ try {
322
+ const app = this.db.prepare('PRAGMA application_id').get().application_id;
323
+ const version = this.db.prepare('PRAGMA user_version').get().user_version;
324
+ const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all().map(row => row.name).sort();
325
+ if (app === 0 && version === 0 && tables.length === 0) {
326
+ this.db.exec(schemaSql());
327
+ }
328
+ else {
329
+ // Append-only evolution: a known older version may only add tables/columns in a
330
+ // forward migration; any unknown application id or schema version fails closed.
331
+ if (app !== APP_ID || version !== 1)
332
+ fail();
333
+ const expected = [...TABLES].sort().join('\0');
334
+ if (tables.join('\0') !== expected)
335
+ fail();
336
+ }
337
+ const row = this.db.prepare('SELECT * FROM authority WHERE authority_id=?').get(this.config.authorityId);
338
+ const count = this.db.prepare('SELECT COUNT(*) AS n FROM authority').get().n;
339
+ if (row) {
340
+ if (count !== 1 || row.authority_digest !== this.authorityDigest || row.key_id !== this.config.keyId
341
+ || row.public_key !== this.publicKey || row.config_json !== json(this.config))
342
+ fail();
343
+ }
344
+ else if (count !== 0)
345
+ fail();
346
+ else
347
+ this.db.prepare('INSERT INTO authority VALUES (?,?,?,?,?)').run(this.config.authorityId, this.authorityDigest, this.config.keyId, this.publicKey, json(this.config));
348
+ this.db.exec('COMMIT; PRAGMA journal_mode=WAL');
349
+ }
350
+ catch (error) {
351
+ try {
352
+ this.db.exec('ROLLBACK');
353
+ }
354
+ catch { /* keep the original error */ }
355
+ throw error;
356
+ }
357
+ if (this.db.prepare('PRAGMA foreign_key_check').all().length)
358
+ fail();
359
+ this.checkFiles();
360
+ this.syncDirectory();
361
+ this.listEntries();
362
+ }
363
+ catch (error) {
364
+ this.db.close();
365
+ throw error;
366
+ }
367
+ }
368
+ checkFiles() {
369
+ privateStat(dirname(this.ledgerPath), 'directory');
370
+ privateStat(this.ledgerPath, 'file');
371
+ const stat = lstatSync(this.ledgerPath);
372
+ if (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)
373
+ fail();
374
+ for (const suffix of ['-wal', '-shm', '-journal']) {
375
+ try {
376
+ privateStat(this.ledgerPath + suffix, 'file');
377
+ }
378
+ catch (error) {
379
+ if (error.code !== 'ENOENT')
380
+ throw error;
381
+ }
382
+ }
383
+ }
384
+ syncDirectory() {
385
+ this.checkFiles();
386
+ const fd = openSync(dirname(this.ledgerPath), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
387
+ try {
388
+ fsyncSync(fd);
389
+ }
390
+ finally {
391
+ closeSync(fd);
392
+ }
393
+ }
394
+ transaction(callback) {
395
+ this.checkFiles();
396
+ this.db.exec('BEGIN IMMEDIATE');
397
+ try {
398
+ const result = callback();
399
+ this.db.exec('COMMIT');
400
+ this.syncDirectory();
401
+ return result;
402
+ }
403
+ catch (error) {
404
+ try {
405
+ this.db.exec('ROLLBACK');
406
+ }
407
+ catch { /* keep the original error */ }
408
+ throw error;
409
+ }
410
+ }
411
+ count(table) {
412
+ return this.db.prepare('SELECT COUNT(*) AS n FROM ' + table).get().n;
413
+ }
414
+ usage() {
415
+ this.checkFiles();
416
+ return { adoptions: this.count('adoptions'), rollbacks: this.count('rollbacks'), calls: this.count('calls') };
417
+ }
418
+ row(sql, ...args) {
419
+ return this.db.prepare(sql).get(...args);
420
+ }
421
+ versionRow(planId) {
422
+ return this.row('SELECT * FROM versions WHERE plan_id=?', str(planId, ID));
423
+ }
424
+ decodeEntry(row) {
425
+ str(row.entry_id, ID);
426
+ str(row.name, /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u, 64);
427
+ const list = aliases(JSON.parse(row.aliases_json));
428
+ if (json([...list]) !== row.aliases_json || digest([...list]) !== row.aliases_digest)
429
+ fail();
430
+ str(row.parent_plan_id, ID);
431
+ str(row.current_plan_id, ID);
432
+ int(row.revision, 1, Number.MAX_SAFE_INTEGER);
433
+ int(row.created_at, 1, 8_640_000_000_000_000);
434
+ int(row.updated_at, 1, 8_640_000_000_000_000);
435
+ return { entryId: row.entry_id, name: row.name, aliases: list, aliasesDigest: row.aliases_digest,
436
+ parentPlanId: row.parent_plan_id, currentPlanId: row.current_plan_id,
437
+ revision: row.revision, createdAt: row.created_at, updatedAt: row.updated_at };
438
+ }
439
+ decodeVersion(row) {
440
+ str(row.plan_id, ID);
441
+ str(row.entry_id, ID);
442
+ if (!(row.artifact instanceof Uint8Array) || row.artifact.length < 1 || row.artifact.length > MAX_ARTIFACT
443
+ || sha(Buffer.from(row.artifact)) !== row.artifact_sha)
444
+ fail();
445
+ if (Buffer.byteLength(row.certificate_json) > MAX_JSON || sha(row.certificate_json) !== row.certificate_digest
446
+ || Buffer.byteLength(row.receipt_json) > MAX_JSON || sha(row.receipt_json) !== row.receipt_digest)
447
+ fail();
448
+ const certificate = JSON.parse(row.certificate_json);
449
+ if (row.role === 'parent')
450
+ validatePluginCreationVerificationCertificate(certificate);
451
+ else {
452
+ validatePluginRevisionVerificationCertificate(certificate);
453
+ const receipt = receiptShape(JSON.parse(row.receipt_json));
454
+ if (!verifyRevisionCapabilityAdoptionReceipt(receipt, this.authorityDigest, this.publicKey, receipt.adoptedAt)
455
+ || receipt.authorityId !== this.config.authorityId || receipt.keyId !== this.config.keyId)
456
+ fail();
457
+ if (receipt.planId !== row.plan_id || receipt.planDigest !== certificate.plan.digest
458
+ || receipt.verificationDigest !== sha(row.certificate_json) || receipt.artifactSha256 !== row.artifact_sha
459
+ || receipt.artifactBytes !== row.artifact.length || receipt.schemaDigest !== certificate.schemaDigest
460
+ || receipt.entryId !== row.entry_id || json(receipt.source) !== json(certificate.source)
461
+ || json(receipt.model) !== json(certificate.model)
462
+ || receipt.expiresAt !== row.expires_at || receipt.adoptedAt !== row.adopted_at
463
+ || (receipt.regressionDigest === undefined) !== (row.regression_digest === null)
464
+ || (receipt.regressionDigest !== undefined && receipt.regressionDigest !== row.regression_digest)
465
+ || receipt.parentPlanId !== certificate.parent.planId
466
+ || receipt.parentCertificateDigest !== certificate.parent.certificateDigest
467
+ || receipt.parentArtifactSha256 !== certificate.parent.artifactSha256)
468
+ fail();
469
+ }
470
+ if (certificate.plan.id !== row.plan_id || certificate.plan.artifactSha256 !== row.artifact_sha
471
+ || certificate.plan.artifactBytes !== row.artifact.length || certificate.schemaDigest !== row.schema_digest
472
+ || json(certificate) !== row.certificate_json)
473
+ fail();
474
+ if (row.role === 'parent') {
475
+ const parentReceipt = parentReceiptShape(JSON.parse(row.receipt_json), row.plan_id, row.artifact_sha);
476
+ if (parentReceipt.json !== row.receipt_json || parentReceipt.digest !== row.receipt_digest)
477
+ fail();
478
+ }
479
+ const versionTools = tools(JSON.parse(row.tools_json));
480
+ if (json(versionTools) !== row.tools_json)
481
+ fail();
482
+ const versionSource = sourceRef(JSON.parse(row.source_json));
483
+ if (json(versionSource) !== row.source_json || json(versionSource) !== json(certificate.source))
484
+ fail();
485
+ const versionModel = model(JSON.parse(row.model_json));
486
+ if (json(versionModel) !== row.model_json || json(versionModel) !== json(certificate.model))
487
+ fail();
488
+ int(row.adopted_at, 1, 8_640_000_000_000_000);
489
+ int(row.expires_at, 1, 8_640_000_000_000_000);
490
+ if (row.expires_at <= row.adopted_at)
491
+ fail();
492
+ return { planId: row.plan_id, entryId: row.entry_id, role: row.role, name: certificate.plan.name,
493
+ certificate, certificateDigest: row.certificate_digest, receiptJson: row.receipt_json,
494
+ receiptDigest: row.receipt_digest, tools: versionTools, artifact: Buffer.from(row.artifact),
495
+ artifactSha256: row.artifact_sha, schemaDigest: row.schema_digest, source: versionSource, model: versionModel,
496
+ ...(row.regression_digest === null ? {} : { regressionDigest: row.regression_digest }),
497
+ adoptedAt: row.adopted_at, expiresAt: row.expires_at };
498
+ }
499
+ decodeAdoption(row) {
500
+ str(row.plan_id, ID);
501
+ str(row.parent_plan_id, ID);
502
+ str(row.certificate_digest, SHA, 64);
503
+ if (!['claimed', 'active', 'closed', 'unknown', 'rejected'].includes(row.status))
504
+ fail();
505
+ int(row.claimed_at, 1, 8_640_000_000_000_000);
506
+ const record = { planId: row.plan_id, status: row.status,
507
+ parentPlanId: row.parent_plan_id, certificateDigest: row.certificate_digest, claimedAt: row.claimed_at };
508
+ if (row.settled_at !== null)
509
+ record.settledAt = int(row.settled_at, 1, 8_640_000_000_000_000);
510
+ if (row.reason !== null)
511
+ record.reason = str(row.reason, /^[a-z][a-z0-9-]{0,63}$/u, 64);
512
+ if (row.entry_id !== null)
513
+ record.entryId = str(row.entry_id, ID);
514
+ if (row.in_flight_parent_calls !== null)
515
+ record.inFlightParentCalls = int(row.in_flight_parent_calls, 0, Number.MAX_SAFE_INTEGER);
516
+ if (row.status === 'active') {
517
+ if (record.entryId === undefined || record.inFlightParentCalls === undefined
518
+ || record.settledAt === undefined || record.reason !== undefined)
519
+ fail();
520
+ }
521
+ else if (row.status === 'claimed') {
522
+ if (record.settledAt !== undefined || record.reason !== undefined
523
+ || record.entryId !== undefined || record.inFlightParentCalls !== undefined)
524
+ fail();
525
+ }
526
+ else if (record.settledAt === undefined || record.reason === undefined
527
+ || record.entryId !== undefined || record.inFlightParentCalls !== undefined)
528
+ fail();
529
+ return record;
530
+ }
531
+ inspectAdoption(planId) {
532
+ this.checkFiles();
533
+ const row = this.row('SELECT * FROM adoptions WHERE plan_id=?', str(planId, ID));
534
+ return row === undefined ? undefined : this.decodeAdoption(row);
535
+ }
536
+ listEntries() {
537
+ this.checkFiles();
538
+ const rows = this.db.prepare('SELECT * FROM entries ORDER BY entry_id').all();
539
+ return rows.map(row => this.decodeEntry(row));
540
+ }
541
+ inspectVersion(planId) {
542
+ this.checkFiles();
543
+ const row = this.versionRow(planId);
544
+ return row === undefined ? undefined : this.decodeVersion(row);
545
+ }
546
+ inspectEntry(entryId) {
547
+ this.checkFiles();
548
+ const row = this.row('SELECT * FROM entries WHERE entry_id=?', str(entryId, ID));
549
+ if (!row)
550
+ return undefined;
551
+ const entry = this.decodeEntry(row);
552
+ const parentRow = this.versionRow(entry.parentPlanId), currentRow = this.versionRow(entry.currentPlanId);
553
+ if (!parentRow || !currentRow)
554
+ fail();
555
+ const parent = this.decodeVersion(parentRow), current = this.decodeVersion(currentRow);
556
+ if (parent.role !== 'parent' || parent.entryId !== entry.entryId || current.entryId !== entry.entryId)
557
+ fail();
558
+ return { entry, parent, current };
559
+ }
560
+ claim(input) {
561
+ str(input.planId, ID);
562
+ str(input.parentPlanId, ID);
563
+ str(input.certificateDigest, SHA, 64);
564
+ return this.transaction(() => {
565
+ const prior = this.row('SELECT * FROM adoptions WHERE plan_id=?', input.planId);
566
+ if (prior) {
567
+ const record = this.decodeAdoption(prior);
568
+ if (record.parentPlanId !== input.parentPlanId || record.certificateDigest !== input.certificateDigest)
569
+ fail();
570
+ return { created: false, record };
571
+ }
572
+ if (Date.now() >= this.config.expiresAt)
573
+ fail();
574
+ if (this.count('adoptions') >= this.config.maxAdoptions)
575
+ fail();
576
+ this.db.prepare("INSERT INTO adoptions(plan_id,status,parent_plan_id,certificate_digest,claimed_at) VALUES (?,'claimed',?,?,?)")
577
+ .run(input.planId, input.parentPlanId, input.certificateDigest, Date.now());
578
+ return { created: true, record: this.decodeAdoption(this.row('SELECT * FROM adoptions WHERE plan_id=?', input.planId)) };
579
+ });
580
+ }
581
+ settle(planId, status, reason) {
582
+ str(planId, ID);
583
+ if (!['closed', 'unknown', 'rejected'].includes(status))
584
+ fail();
585
+ str(reason, /^[a-z][a-z0-9-]{0,63}$/u, 64);
586
+ this.transaction(() => {
587
+ const prior = this.row('SELECT * FROM adoptions WHERE plan_id=?', planId);
588
+ if (!prior)
589
+ fail();
590
+ if (prior.status === status && prior.reason === reason)
591
+ return;
592
+ if (prior.status !== 'claimed')
593
+ fail();
594
+ this.db.prepare("UPDATE adoptions SET status=?,reason=?,settled_at=? WHERE plan_id=? AND status='claimed'")
595
+ .run(status, reason, Date.now(), planId);
596
+ });
597
+ }
598
+ commit(input) {
599
+ str(input.entry.entryId, ID);
600
+ str(input.entry.name, /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u, 64);
601
+ str(input.expectedCurrentPlanId, ID);
602
+ int(input.inFlightParentCalls, 0, Number.MAX_SAFE_INTEGER);
603
+ const receipt = receiptShape(input.receipt);
604
+ if (!verifyRevisionCapabilityAdoptionReceipt(receipt, this.authorityDigest, this.publicKey, receipt.adoptedAt)
605
+ || receipt.authorityId !== this.config.authorityId || receipt.keyId !== this.config.keyId
606
+ || receipt.entryId !== input.entry.entryId || receipt.aliasesDigest !== digest([...input.entry.aliases])
607
+ || receipt.planId !== input.revision.planId || input.revision.role !== 'revision'
608
+ || input.parent.role !== 'parent' || input.parent.entryId !== input.entry.entryId
609
+ || input.revision.entryId !== input.entry.entryId
610
+ || receipt.planDigest !== input.revision.certificate.plan.digest
611
+ || receipt.verificationDigest !== input.revision.certificateDigest
612
+ || receipt.artifactSha256 !== input.revision.artifactSha256 || receipt.schemaDigest !== input.revision.schemaDigest
613
+ || receipt.parentPlanId !== input.parent.planId || receipt.parentCertificateDigest !== input.parent.certificateDigest
614
+ || receipt.parentReceiptDigest !== input.parent.receiptDigest || receipt.parentArtifactSha256 !== input.parent.artifactSha256
615
+ || receipt.expiresAt !== input.revision.expiresAt || receipt.adoptedAt !== input.revision.adoptedAt
616
+ || receipt.inFlightParentCalls !== input.inFlightParentCalls
617
+ || (receipt.regressionDigest === undefined) !== (input.revision.regressionDigest === undefined)
618
+ || (receipt.regressionDigest !== undefined && receipt.regressionDigest !== input.revision.regressionDigest))
619
+ fail();
620
+ const expectedExpiry = this.config.retention === undefined
621
+ ? Math.min(this.config.expiresAt, input.revision.certificate.expiresAt)
622
+ : Math.min(this.config.expiresAt, input.revision.adoptedAt + this.config.retention.maximumLifetimeMs);
623
+ if (input.revision.expiresAt !== expectedExpiry
624
+ || input.revision.adoptedAt < input.revision.certificate.verifiedAt
625
+ || input.revision.adoptedAt >= input.revision.certificate.expiresAt
626
+ || input.revision.adoptedAt >= this.config.expiresAt)
627
+ fail();
628
+ return this.transaction(() => {
629
+ const adoption = this.row('SELECT * FROM adoptions WHERE plan_id=?', input.revision.planId);
630
+ if (!adoption || adoption.status !== 'claimed' || adoption.certificate_digest !== input.revision.certificateDigest
631
+ || adoption.parent_plan_id !== input.parent.planId)
632
+ fail();
633
+ const priorEntry = this.row('SELECT * FROM entries WHERE entry_id=?', input.entry.entryId);
634
+ let revisionCount;
635
+ if (!priorEntry) {
636
+ if (input.expectedCurrentPlanId !== input.parent.planId)
637
+ fail();
638
+ this.db.prepare('INSERT INTO entries(entry_id,name,aliases_json,aliases_digest,parent_plan_id,current_plan_id,revision,created_at,updated_at) VALUES (?,?,?,?,?,?,1,?,?)')
639
+ .run(input.entry.entryId, input.entry.name, json([...input.entry.aliases]), digest([...input.entry.aliases]), input.parent.planId, input.parent.planId, input.revision.adoptedAt, input.revision.adoptedAt);
640
+ this.insertVersion(input.parent);
641
+ revisionCount = 1;
642
+ }
643
+ else {
644
+ const entry = this.decodeEntry(priorEntry);
645
+ if (entry.parentPlanId !== input.parent.planId || entry.currentPlanId !== input.expectedCurrentPlanId
646
+ || entry.currentPlanId !== input.parent.planId || entry.name !== input.entry.name
647
+ || json([...entry.aliases]) !== json([...input.entry.aliases]))
648
+ fail();
649
+ revisionCount = entry.revision;
650
+ }
651
+ if (this.versionRow(input.revision.planId))
652
+ fail();
653
+ this.insertVersion(input.revision);
654
+ const updated = this.db.prepare('UPDATE entries SET current_plan_id=?, revision=revision+1, updated_at=? WHERE entry_id=? AND current_plan_id=? AND revision=?')
655
+ .run(input.revision.planId, input.revision.adoptedAt, input.entry.entryId, input.expectedCurrentPlanId, revisionCount);
656
+ if (Number(updated.changes) !== 1)
657
+ fail();
658
+ const settled = this.db.prepare("UPDATE adoptions SET status='active',entry_id=?,in_flight_parent_calls=?,settled_at=? WHERE plan_id=? AND status='claimed'")
659
+ .run(input.entry.entryId, input.inFlightParentCalls, input.revision.adoptedAt, input.revision.planId);
660
+ if (Number(settled.changes) !== 1)
661
+ fail();
662
+ return this.decodeAdoption(this.row('SELECT * FROM adoptions WHERE plan_id=?', input.revision.planId));
663
+ });
664
+ }
665
+ signReceipt(body) {
666
+ const canonicalBody = JSON.parse(json(body));
667
+ const signature = sign(null, Buffer.from(DOMAIN + json(canonicalBody, MAX_JSON)), this.key).toString('base64url');
668
+ return receiptShape({ ...canonicalBody, signature });
669
+ }
670
+ insertVersion(version) {
671
+ const certificateJson = json(version.certificate);
672
+ if (version.role === 'parent') {
673
+ const receipt = parentReceiptShape(JSON.parse(version.receiptJson), version.planId, version.artifactSha256);
674
+ if (receipt.digest !== version.receiptDigest)
675
+ fail();
676
+ }
677
+ else if (sha(version.receiptJson) !== version.receiptDigest)
678
+ fail();
679
+ this.db.prepare('INSERT INTO versions(plan_id,entry_id,role,certificate_json,certificate_digest,receipt_json,receipt_digest,tools_json,artifact,artifact_sha,schema_digest,source_json,model_json,regression_digest,adopted_at,expires_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
680
+ .run(version.planId, version.entryId, version.role, certificateJson, sha(certificateJson), version.receiptJson, version.receiptDigest, json([...version.tools]), version.artifact, version.artifactSha256, version.schemaDigest, json(version.source), json(version.model), version.regressionDigest ?? null, version.adoptedAt, version.expiresAt);
681
+ }
682
+ rollback(input) {
683
+ str(input.entryId, ID);
684
+ str(input.expectedCurrentPlanId, ID);
685
+ str(input.authorityId, ID);
686
+ const rollbackId = digest({ entryId: input.entryId, expectedCurrentPlanId: input.expectedCurrentPlanId, authorityId: input.authorityId });
687
+ return this.transaction(() => {
688
+ const prior = this.row('SELECT * FROM rollbacks WHERE rollback_id=?', rollbackId);
689
+ if (prior)
690
+ return { created: false, record: this.decodeRollback(prior) };
691
+ if (Date.now() >= this.config.rollback.expiresAt)
692
+ fail();
693
+ if (this.count('rollbacks') >= this.config.rollback.maxRollbacks)
694
+ fail();
695
+ const entryRow = this.row('SELECT * FROM entries WHERE entry_id=?', input.entryId);
696
+ if (!entryRow)
697
+ fail();
698
+ const entry = this.decodeEntry(entryRow);
699
+ if (entry.currentPlanId !== input.expectedCurrentPlanId || entry.currentPlanId === entry.parentPlanId)
700
+ fail();
701
+ const childRow = this.versionRow(entry.currentPlanId), parentRow = this.versionRow(entry.parentPlanId);
702
+ if (!childRow || !parentRow)
703
+ fail();
704
+ const child = this.decodeVersion(childRow), parent = this.decodeVersion(parentRow);
705
+ if (child.role !== 'revision' || parent.role !== 'parent' || child.entryId !== entry.entryId
706
+ || parent.entryId !== entry.entryId)
707
+ fail();
708
+ const childReceipt = receiptShape(JSON.parse(child.receiptJson));
709
+ if (childReceipt.parentPlanId !== entry.parentPlanId || childReceipt.parentReceiptDigest !== parent.receiptDigest)
710
+ fail();
711
+ const ordinal = this.count('rollbacks') + 1, at = Date.now();
712
+ const body = { rollbackId, entryId: entry.entryId, fromPlanId: entry.currentPlanId, toPlanId: entry.parentPlanId,
713
+ parentReceiptDigest: parent.receiptDigest, authorityId: input.authorityId, at, ordinal };
714
+ const recordDigest = digest(body);
715
+ this.db.prepare('INSERT INTO rollbacks(rollback_id,entry_id,from_plan_id,to_plan_id,parent_receipt_digest,authority_id,at,ordinal,digest) VALUES (?,?,?,?,?,?,?,?,?)')
716
+ .run(rollbackId, entry.entryId, entry.currentPlanId, entry.parentPlanId, parent.receiptDigest, input.authorityId, at, ordinal, recordDigest);
717
+ const updated = this.db.prepare('UPDATE entries SET current_plan_id=?, revision=revision+1, updated_at=? WHERE entry_id=? AND current_plan_id=? AND revision=?')
718
+ .run(entry.parentPlanId, at, entry.entryId, entry.currentPlanId, entry.revision);
719
+ if (Number(updated.changes) !== 1)
720
+ fail();
721
+ return { created: true, record: this.decodeRollback(this.row('SELECT * FROM rollbacks WHERE rollback_id=?', rollbackId)) };
722
+ });
723
+ }
724
+ decodeRollback(row) {
725
+ str(row.rollback_id, SHA, 64);
726
+ str(row.entry_id, ID);
727
+ str(row.from_plan_id, ID);
728
+ str(row.to_plan_id, ID);
729
+ str(row.parent_receipt_digest, SHA, 64);
730
+ str(row.authority_id, ID);
731
+ int(row.at, 1, 8_640_000_000_000_000);
732
+ int(row.ordinal, 1, 32);
733
+ str(row.digest, SHA, 64);
734
+ const body = { rollbackId: row.rollback_id, entryId: row.entry_id, fromPlanId: row.from_plan_id, toPlanId: row.to_plan_id,
735
+ parentReceiptDigest: row.parent_receipt_digest, authorityId: row.authority_id, at: row.at, ordinal: row.ordinal };
736
+ if (digest(body) !== row.digest)
737
+ fail();
738
+ return { protocol: 'dsh-created-capability-revision-rollback/v1', ...body, digest: row.digest };
739
+ }
740
+ claimCall(input) {
741
+ str(input.entryId, ID);
742
+ str(input.key, SHA, 64);
743
+ str(input.argumentsDigest, SHA, 64);
744
+ str(input.toolAlias, ALIAS, 96);
745
+ str(input.versionPlanId, ID);
746
+ return this.transaction(() => {
747
+ const prior = this.row('SELECT * FROM calls WHERE entry_id=? AND call_key=?', input.entryId, input.key);
748
+ if (prior) {
749
+ if (prior.arguments_digest !== input.argumentsDigest || prior.tool_alias !== input.toolAlias)
750
+ fail();
751
+ return { created: false, call: this.decodeCall(prior) };
752
+ }
753
+ const entryRow = this.row('SELECT * FROM entries WHERE entry_id=?', input.entryId);
754
+ if (!entryRow)
755
+ fail();
756
+ const entry = this.decodeEntry(entryRow);
757
+ if (entry.currentPlanId !== input.versionPlanId || !entry.aliases.includes(input.toolAlias))
758
+ fail();
759
+ const versionRow = this.versionRow(input.versionPlanId);
760
+ if (!versionRow)
761
+ fail();
762
+ const version = this.decodeVersion(versionRow);
763
+ if (version.entryId !== entry.entryId || Date.now() >= version.expiresAt)
764
+ fail();
765
+ if (this.count('calls') >= this.config.maxCallRecords)
766
+ fail();
767
+ this.db.prepare("INSERT INTO calls(entry_id,call_key,version_plan_id,tool_alias,arguments_digest,status,claimed_at) VALUES (?,?,?,?,?,'claimed',?)")
768
+ .run(input.entryId, input.key, input.versionPlanId, input.toolAlias, input.argumentsDigest, Date.now());
769
+ return { created: true, call: { key: input.key, versionPlanId: input.versionPlanId, status: 'claimed' } };
770
+ });
771
+ }
772
+ decodeCall(row) {
773
+ str(row.entry_id, ID);
774
+ str(row.call_key, SHA, 64);
775
+ str(row.version_plan_id, ID);
776
+ str(row.tool_alias, ALIAS, 96);
777
+ str(row.arguments_digest, SHA, 64);
778
+ int(row.claimed_at, 1, 8_640_000_000_000_000);
779
+ if (!['claimed', 'completed', 'unknown'].includes(row.status))
780
+ fail();
781
+ const call = { key: row.call_key, versionPlanId: row.version_plan_id, status: row.status };
782
+ if (row.result_json !== null) {
783
+ if (Buffer.byteLength(row.result_json) > MAX_JSON)
784
+ fail();
785
+ call.result = JSON.parse(row.result_json);
786
+ if (json(call.result) !== row.result_json)
787
+ fail();
788
+ }
789
+ if (row.settled_at !== null)
790
+ int(row.settled_at, 1, 8_640_000_000_000_000);
791
+ if ((row.status === 'claimed') !== (row.settled_at === null && row.result_json === null))
792
+ fail();
793
+ return call;
794
+ }
795
+ settleCall(input) {
796
+ str(input.entryId, ID);
797
+ str(input.key, SHA, 64);
798
+ if (input.status !== 'completed' && input.status !== 'unknown')
799
+ fail();
800
+ const resultJson = Object.hasOwn(input, 'result') ? json(input.result) : null;
801
+ this.transaction(() => {
802
+ const row = this.row('SELECT * FROM calls WHERE entry_id=? AND call_key=?', input.entryId, input.key);
803
+ if (!row || row.status !== 'claimed')
804
+ fail();
805
+ this.decodeCall(row);
806
+ this.db.prepare("UPDATE calls SET status=?,result_json=?,settled_at=? WHERE entry_id=? AND call_key=? AND status='claimed'")
807
+ .run(input.status, resultJson, Math.max(Date.now(), row.claimed_at), input.entryId, input.key);
808
+ });
809
+ }
810
+ listCallEvidence(entryId) {
811
+ this.checkFiles();
812
+ str(entryId, ID);
813
+ const rows = this.db.prepare('SELECT * FROM calls WHERE entry_id=? ORDER BY claimed_at, call_key').all(entryId);
814
+ if (rows.length > this.config.maxCallRecords)
815
+ fail();
816
+ const entry = this.row('SELECT * FROM entries WHERE entry_id=?', entryId);
817
+ if (!entry)
818
+ return [];
819
+ return rows.map(row => {
820
+ const call = this.decodeCall(row);
821
+ const versionRow = this.versionRow(call.versionPlanId);
822
+ if (!versionRow)
823
+ fail();
824
+ const version = this.decodeVersion(versionRow);
825
+ return { protocol: 'dsh-created-capability-revision-call/v1', entryId, key: call.key,
826
+ versionPlanId: call.versionPlanId, toolAlias: row.tool_alias, status: call.status,
827
+ receiptDigest: version.receiptDigest, claimedAt: row.claimed_at,
828
+ ...(row.settled_at === null ? {} : { settledAt: row.settled_at }) };
829
+ });
830
+ }
831
+ recoverClaims() {
832
+ this.transaction(() => {
833
+ this.db.prepare("UPDATE adoptions SET status='unknown',reason='recovered-claim',settled_at=? WHERE status='claimed'").run(Date.now());
834
+ this.db.prepare("UPDATE calls SET status='unknown',settled_at=CASE WHEN claimed_at IS NULL THEN NULL ELSE max(claimed_at,?) END WHERE status='claimed'").run(Date.now());
835
+ });
836
+ }
837
+ close() { this.db.close(); }
838
+ }
839
+ function schemaSql() {
840
+ return [
841
+ 'CREATE TABLE authority (authority_id TEXT PRIMARY KEY, authority_digest TEXT NOT NULL CHECK(length(authority_digest)=64),',
842
+ ' key_id TEXT NOT NULL, public_key TEXT NOT NULL, config_json TEXT NOT NULL CHECK(json_valid(config_json) AND length(config_json)<=65536)) STRICT, WITHOUT ROWID;',
843
+ 'CREATE TABLE entries (entry_id TEXT PRIMARY KEY, name TEXT NOT NULL, aliases_json TEXT NOT NULL CHECK(json_valid(aliases_json) AND length(aliases_json)<=16384),',
844
+ ' aliases_digest TEXT NOT NULL CHECK(length(aliases_digest)=64), parent_plan_id TEXT NOT NULL, current_plan_id TEXT NOT NULL,',
845
+ ' revision INTEGER NOT NULL CHECK(revision>=1), created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL) STRICT, WITHOUT ROWID;',
846
+ 'CREATE TABLE versions (plan_id TEXT PRIMARY KEY, entry_id TEXT NOT NULL REFERENCES entries(entry_id),',
847
+ " role TEXT NOT NULL CHECK(role IN ('parent','revision')), certificate_json TEXT NOT NULL CHECK(json_valid(certificate_json) AND length(certificate_json)<=65536),",
848
+ ' certificate_digest TEXT NOT NULL CHECK(length(certificate_digest)=64), receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json) AND length(receipt_json)<=65536),',
849
+ ' receipt_digest TEXT NOT NULL CHECK(length(receipt_digest)=64), tools_json TEXT NOT NULL CHECK(json_valid(tools_json) AND length(tools_json)<=65536),',
850
+ ' artifact BLOB NOT NULL CHECK(length(artifact)>0 AND length(artifact)<=524288), artifact_sha TEXT NOT NULL CHECK(length(artifact_sha)=64),',
851
+ ' schema_digest TEXT NOT NULL CHECK(length(schema_digest)=64), source_json TEXT NOT NULL CHECK(json_valid(source_json) AND length(source_json)<=4096),',
852
+ ' model_json TEXT NOT NULL CHECK(json_valid(model_json) AND length(model_json)<=4096), regression_digest TEXT CHECK(regression_digest IS NULL OR length(regression_digest)=64),',
853
+ ' adopted_at INTEGER NOT NULL, expires_at INTEGER NOT NULL) STRICT, WITHOUT ROWID;',
854
+ "CREATE TABLE adoptions (plan_id TEXT PRIMARY KEY, status TEXT NOT NULL CHECK(status IN ('claimed','active','closed','unknown','rejected')),",
855
+ ' parent_plan_id TEXT NOT NULL, certificate_digest TEXT NOT NULL CHECK(length(certificate_digest)=64), claimed_at INTEGER NOT NULL,',
856
+ " settled_at INTEGER, reason TEXT CHECK(reason IS NULL OR (length(reason)>0 AND length(reason)<=64)), entry_id TEXT,",
857
+ ' in_flight_parent_calls INTEGER, CHECK((status=\'claimed\' AND settled_at IS NULL AND reason IS NULL AND entry_id IS NULL AND in_flight_parent_calls IS NULL)',
858
+ ' OR (status=\'active\' AND settled_at IS NOT NULL AND reason IS NULL AND entry_id IS NOT NULL AND in_flight_parent_calls IS NOT NULL)',
859
+ " OR (status IN ('closed','unknown','rejected') AND settled_at IS NOT NULL AND reason IS NOT NULL AND entry_id IS NULL AND in_flight_parent_calls IS NULL))) STRICT, WITHOUT ROWID;",
860
+ 'CREATE TABLE calls (entry_id TEXT NOT NULL REFERENCES entries(entry_id), call_key TEXT NOT NULL, version_plan_id TEXT NOT NULL,',
861
+ ' tool_alias TEXT NOT NULL, arguments_digest TEXT NOT NULL CHECK(length(arguments_digest)=64),',
862
+ " status TEXT NOT NULL CHECK(status IN ('claimed','completed','unknown')), result_json TEXT CHECK(result_json IS NULL OR (json_valid(result_json) AND length(result_json)<=65536)),",
863
+ ' claimed_at INTEGER NOT NULL, settled_at INTEGER, PRIMARY KEY(entry_id, call_key),',
864
+ " CHECK((status='claimed' AND result_json IS NULL AND settled_at IS NULL) OR status IN ('completed','unknown'))) STRICT, WITHOUT ROWID;",
865
+ 'CREATE TABLE rollbacks (seq INTEGER PRIMARY KEY, rollback_id TEXT NOT NULL UNIQUE, entry_id TEXT NOT NULL REFERENCES entries(entry_id),',
866
+ ' from_plan_id TEXT NOT NULL, to_plan_id TEXT NOT NULL, parent_receipt_digest TEXT NOT NULL CHECK(length(parent_receipt_digest)=64),',
867
+ ' authority_id TEXT NOT NULL, at INTEGER NOT NULL, ordinal INTEGER NOT NULL CHECK(ordinal>=1), digest TEXT NOT NULL CHECK(length(digest)=64)) STRICT;',
868
+ 'PRAGMA application_id=' + APP_ID + ';',
869
+ 'PRAGMA user_version=1;',
870
+ ].join('\n');
871
+ }
872
+ //# sourceMappingURL=revision-capability-journal.js.map