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

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 (121) hide show
  1. package/README.md +81 -5
  2. package/lib/attestation.d.ts.map +1 -1
  3. package/lib/attestation.js +33 -1
  4. package/lib/attestation.js.map +1 -1
  5. package/lib/creation-capability-empty-handoff.d.ts +57 -0
  6. package/lib/creation-capability-empty-handoff.d.ts.map +1 -0
  7. package/lib/creation-capability-empty-handoff.js +147 -0
  8. package/lib/creation-capability-empty-handoff.js.map +1 -0
  9. package/lib/creation-capability-execution.d.ts +9 -0
  10. package/lib/creation-capability-execution.d.ts.map +1 -0
  11. package/lib/creation-capability-execution.js +70 -0
  12. package/lib/creation-capability-execution.js.map +1 -0
  13. package/lib/creation-capability-feedback.d.ts +58 -1
  14. package/lib/creation-capability-feedback.d.ts.map +1 -1
  15. package/lib/creation-capability-feedback.js +532 -50
  16. package/lib/creation-capability-feedback.js.map +1 -1
  17. package/lib/creation-capability-handoff-common.d.ts +60 -0
  18. package/lib/creation-capability-handoff-common.d.ts.map +1 -0
  19. package/lib/creation-capability-handoff-common.js +195 -0
  20. package/lib/creation-capability-handoff-common.js.map +1 -0
  21. package/lib/creation-capability-journal.d.ts +19 -0
  22. package/lib/creation-capability-journal.d.ts.map +1 -1
  23. package/lib/creation-capability-journal.js +62 -21
  24. package/lib/creation-capability-journal.js.map +1 -1
  25. package/lib/creation-capability-runtime.d.ts +18 -0
  26. package/lib/creation-capability-runtime.d.ts.map +1 -1
  27. package/lib/creation-capability-runtime.js +76 -18
  28. package/lib/creation-capability-runtime.js.map +1 -1
  29. package/lib/creation-capability-sealed-audit.d.ts +19 -0
  30. package/lib/creation-capability-sealed-audit.d.ts.map +1 -0
  31. package/lib/creation-capability-sealed-audit.js +114 -0
  32. package/lib/creation-capability-sealed-audit.js.map +1 -0
  33. package/lib/creation-capability-sealed-handoff.d.ts +160 -0
  34. package/lib/creation-capability-sealed-handoff.d.ts.map +1 -0
  35. package/lib/creation-capability-sealed-handoff.js +695 -0
  36. package/lib/creation-capability-sealed-handoff.js.map +1 -0
  37. package/lib/creation-capability-trend-snapshot.d.ts +70 -0
  38. package/lib/creation-capability-trend-snapshot.d.ts.map +1 -0
  39. package/lib/creation-capability-trend-snapshot.js +117 -0
  40. package/lib/creation-capability-trend-snapshot.js.map +1 -0
  41. package/lib/creation-capability-types.d.ts +254 -4
  42. package/lib/creation-capability-types.d.ts.map +1 -1
  43. package/lib/in-process-reload.d.ts +191 -0
  44. package/lib/in-process-reload.d.ts.map +1 -0
  45. package/lib/in-process-reload.js +284 -0
  46. package/lib/in-process-reload.js.map +1 -0
  47. package/lib/index.d.ts +12 -4
  48. package/lib/index.d.ts.map +1 -1
  49. package/lib/index.js +8 -2
  50. package/lib/index.js.map +1 -1
  51. package/lib/linux-source-build.d.ts +16 -0
  52. package/lib/linux-source-build.d.ts.map +1 -0
  53. package/lib/linux-source-build.js +268 -0
  54. package/lib/linux-source-build.js.map +1 -0
  55. package/lib/linux-source-preflight.d.ts +31 -0
  56. package/lib/linux-source-preflight.d.ts.map +1 -0
  57. package/lib/linux-source-preflight.js +101 -0
  58. package/lib/linux-source-preflight.js.map +1 -0
  59. package/lib/live-qualification-runtime.d.ts +6 -2
  60. package/lib/live-qualification-runtime.d.ts.map +1 -1
  61. package/lib/live-qualification-runtime.js +65 -11
  62. package/lib/live-qualification-runtime.js.map +1 -1
  63. package/lib/owner-task-gap-types.d.ts +23 -0
  64. package/lib/owner-task-gap-types.d.ts.map +1 -1
  65. package/lib/owner-task-gaps.d.ts +4 -0
  66. package/lib/owner-task-gaps.d.ts.map +1 -1
  67. package/lib/owner-task-gaps.js +35 -1
  68. package/lib/owner-task-gaps.js.map +1 -1
  69. package/lib/scanner-owner-runtime.d.ts +34 -0
  70. package/lib/scanner-owner-runtime.d.ts.map +1 -0
  71. package/lib/scanner-owner-runtime.js +99 -0
  72. package/lib/scanner-owner-runtime.js.map +1 -0
  73. package/lib/service.d.ts +82 -5
  74. package/lib/service.d.ts.map +1 -1
  75. package/lib/service.js +450 -45
  76. package/lib/service.js.map +1 -1
  77. package/lib/source-approval-authority.d.ts.map +1 -1
  78. package/lib/source-approval-authority.js +25 -7
  79. package/lib/source-approval-authority.js.map +1 -1
  80. package/lib/source-build-diagnostics.d.ts +31 -0
  81. package/lib/source-build-diagnostics.d.ts.map +1 -0
  82. package/lib/source-build-diagnostics.js +110 -0
  83. package/lib/source-build-diagnostics.js.map +1 -0
  84. package/lib/source-build.d.ts +25 -2
  85. package/lib/source-build.d.ts.map +1 -1
  86. package/lib/source-build.js +145 -26
  87. package/lib/source-build.js.map +1 -1
  88. package/lib/source-creation.d.ts +6 -2
  89. package/lib/source-creation.d.ts.map +1 -1
  90. package/lib/source-creation.js +11 -2
  91. package/lib/source-creation.js.map +1 -1
  92. package/lib/source-job-types.d.ts +3 -1
  93. package/lib/source-job-types.d.ts.map +1 -1
  94. package/lib/source-jobs.d.ts +28 -1
  95. package/lib/source-jobs.d.ts.map +1 -1
  96. package/lib/source-jobs.js +117 -26
  97. package/lib/source-jobs.js.map +1 -1
  98. package/lib/sqlite.d.ts +1 -1
  99. package/lib/sqlite.d.ts.map +1 -1
  100. package/lib/sqlite.js +36 -1
  101. package/lib/sqlite.js.map +1 -1
  102. package/lib/store.d.ts +21 -1
  103. package/lib/store.d.ts.map +1 -1
  104. package/lib/store.js +470 -174
  105. package/lib/store.js.map +1 -1
  106. package/lib/systemd-host-authority.d.ts.map +1 -1
  107. package/lib/systemd-host-authority.js +4 -0
  108. package/lib/systemd-host-authority.js.map +1 -1
  109. package/lib/task-observation-runtime.d.ts +6 -2
  110. package/lib/task-observation-runtime.d.ts.map +1 -1
  111. package/lib/task-observation-runtime.js +69 -11
  112. package/lib/task-observation-runtime.js.map +1 -1
  113. package/lib/types.d.ts +47 -1
  114. package/lib/types.d.ts.map +1 -1
  115. package/lib/version.d.ts +1 -1
  116. package/lib/version.js +1 -1
  117. package/lib/work-item-creation-contract.d.ts +17 -0
  118. package/lib/work-item-creation-contract.d.ts.map +1 -0
  119. package/lib/work-item-creation-contract.js +61 -0
  120. package/lib/work-item-creation-contract.js.map +1 -0
  121. package/package.json +10 -10
@@ -0,0 +1,695 @@
1
+ import { createPublicKey, randomBytes, verify } from 'node:crypto';
2
+ import { chmodSync, closeSync, constants, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync, writeSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ import { validatePluginCreationVerificationCertificate } from '@dsh-enhanced/assistant-growth-contract';
7
+ import { SOURCE_ARCHIVE_DOMAIN, SOURCE_ARCHIVE_LIMIT, verifyCreationCapabilityReceipt } from './creation-capability-journal.js';
8
+ import { validateCreationCapabilitySource } from './creation-capability-source.js';
9
+ import { RESERVED_OUTPUTS, validateSourceCreationFiles } from './source-creation.js';
10
+ import { encode, freeze, handoffPrimitives, hash, sha } from './creation-capability-handoff-common.js';
11
+ import { SEALED_ADOPTION_AUDIT, SEALED_ADOPTION_AUDIT_STATEMENTS, SEALED_ADOPTION_HANDOFF_PROTOCOL as PROTOCOL, SEALED_PROOF_KEYS, readSealedAdoptionAudit, sealedAdoptionInheritedUsage, sealedAdoptionPlanIds } from './creation-capability-sealed-audit.js';
12
+ function fail(message) { throw new Error(`sealed adoption handoff: ${message}`); }
13
+ const common = handoffPrimitives(fail);
14
+ const { identity, directory, absolute, read, bytes, identities, text, authority, swapAuthority, businessCounts, openExisting, exact } = common;
15
+ const AUDIT_SCHEMA = { table: SEALED_ADOPTION_AUDIT, statements: SEALED_ADOPTION_AUDIT_STATEMENTS };
16
+ const schema = (db, audited) => common.schema(db, AUDIT_SCHEMA, audited);
17
+ const MAX_FILE = 64 * 1024 * 1024;
18
+ const SEALED_DIRECTORY = 'creation-adoptions.sealed';
19
+ const SHA = /^[a-f0-9]{64}$/u;
20
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/u;
21
+ const ADOPTION_STATUSES = ['claimed', 'authorized', 'active', 'closed', 'unknown', 'rejected'];
22
+ const FILE_KEYS = ['dev', 'ino', 'uid', 'gid', 'mode', 'nlink'];
23
+ const ZERO = { adoptions: 0, calls: 0, sourceArchives: 0 };
24
+ function archivePlacement(journal, authorityDigest) {
25
+ return join(dirname(journal), SEALED_DIRECTORY, `${authorityDigest}.sqlite`);
26
+ }
27
+ function nullable(raw) { return raw === null ? null : text(raw); }
28
+ function canonical(raw, label) {
29
+ let value;
30
+ try {
31
+ value = JSON.parse(raw);
32
+ }
33
+ catch {
34
+ fail(`invalid ${label} JSON`);
35
+ }
36
+ if (!value || typeof value !== 'object' || Array.isArray(value) || encode(value) !== raw)
37
+ fail(`noncanonical ${label} JSON`);
38
+ return value;
39
+ }
40
+ function readHistory(db, old) {
41
+ const config = old.config;
42
+ const adoptions = db.prepare(`SELECT CAST(plan_id AS BLOB) AS plan_id, CAST(status AS BLOB) AS status, CAST(reason AS BLOB) AS reason,
43
+ CAST(certificate_json AS BLOB) AS certificate_json, CAST(certificate_digest AS BLOB) AS certificate_digest, artifact,
44
+ CAST(artifact_sha AS BLOB) AS artifact_sha, CAST(tools_digest AS BLOB) AS tools_digest, CAST(receipt_json AS BLOB) AS receipt_json
45
+ FROM adoptions ORDER BY plan_id`).all();
46
+ const calls = db.prepare('SELECT CAST(plan_id AS BLOB) AS plan_id, CAST(status AS BLOB) AS status FROM calls').all();
47
+ const archives = db.prepare('SELECT CAST(plan_id AS BLOB) AS plan_id, CAST(archive_json AS BLOB) AS archive_json FROM source_archives').all();
48
+ const counts = { adoptions: adoptions.length, calls: calls.length, sourceArchives: archives.length };
49
+ if (!counts.adoptions && !counts.calls && !counts.sourceArchives)
50
+ fail('empty history requires the empty adoption handoff');
51
+ if (counts.adoptions > config.maxAdoptions || counts.calls > config.maxCallRecords)
52
+ fail('history exceeds the grant limits');
53
+ const tally = new Map();
54
+ for (const call of calls) {
55
+ const planId = text(call.plan_id), status = text(call.status);
56
+ const entry = tally.get(planId) ?? { callCount: 0, completedCalls: 0, unknownCalls: 0 };
57
+ entry.callCount++;
58
+ if (status === 'completed')
59
+ entry.completedCalls++;
60
+ else if (status === 'claimed' || status === 'unknown')
61
+ entry.unknownCalls++; // never replayed, never refunded
62
+ else
63
+ fail('invalid call status');
64
+ tally.set(planId, entry);
65
+ }
66
+ const sources = new Map();
67
+ for (const archive of archives)
68
+ sources.set(text(archive.plan_id), sha(text(archive.archive_json)));
69
+ const byStatus = { active: 0, closed: 0, rejected: 0, unknown: 0 };
70
+ const rows = [], pendingReverification = [];
71
+ for (const row of adoptions) {
72
+ const planId = text(row.plan_id), originalStatus = text(row.status);
73
+ const reason = nullable(row.reason), certificateJson = text(row.certificate_json), certificateDigest = text(row.certificate_digest);
74
+ const artifactSha256 = text(row.artifact_sha), toolsDigest = nullable(row.tools_digest), receiptJson = nullable(row.receipt_json);
75
+ const unsettled = ['claimed', 'authorized', 'active'].includes(originalStatus);
76
+ if (!ID.test(planId) || !ADOPTION_STATUSES.includes(originalStatus) || !SHA.test(certificateDigest)
77
+ || certificateDigest !== sha(certificateJson) || !SHA.test(artifactSha256) || !(row.artifact instanceof Uint8Array)
78
+ || sha(Buffer.from(row.artifact)) !== artifactSha256 || (reason === null) !== unsettled
79
+ || (['authorized', 'active'].includes(originalStatus) && (receiptJson === null || toolsDigest === null)))
80
+ fail('invalid adoption row');
81
+ const certificate = canonical(certificateJson, 'certificate');
82
+ const receipt = receiptJson === null ? undefined : canonical(receiptJson, 'receipt');
83
+ if (typeof certificate.protocol !== 'string')
84
+ fail('invalid certificate protocol');
85
+ const status = originalStatus === 'active' || originalStatus === 'closed' || originalStatus === 'rejected'
86
+ ? originalStatus : 'unknown';
87
+ const usage = tally.get(planId) ?? { callCount: 0, completedCalls: 0, unknownCalls: 0 };
88
+ tally.delete(planId);
89
+ if (usage.callCount > config.maxCallsPerAdoption)
90
+ fail('history exceeds the grant limits');
91
+ const sourceArchiveDigest = sources.get(planId) ?? null;
92
+ sources.delete(planId);
93
+ if (status === 'active') {
94
+ try {
95
+ validatePluginCreationVerificationCertificate(certificate);
96
+ }
97
+ catch {
98
+ fail('active adoption certificate is invalid');
99
+ }
100
+ if (certificate.protocol !== 'assistant-growth/creation-verification/v1')
101
+ fail('active adoption requires the Docker v1 certificate protocol');
102
+ const signed = receipt;
103
+ const plan = certificate.plan;
104
+ if (!verifyCreationCapabilityReceipt(signed, old.authorityDigest, old.publicKey, signed.adoptedAt)
105
+ || signed.authorityId !== config.authorityId || signed.keyId !== config.keyId || signed.planId !== planId
106
+ || plan.id !== planId || signed.planDigest !== plan.digest || signed.verificationDigest !== certificateDigest
107
+ || signed.artifactSha256 !== artifactSha256 || signed.toolsDigest !== toolsDigest)
108
+ fail('active adoption receipt does not verify under the old authority');
109
+ pendingReverification.push({ planId, certificateDigest, receiptDigest: sha(receiptJson), artifactSha256, toolsDigest: toolsDigest });
110
+ }
111
+ byStatus[status]++;
112
+ rows.push({ planId, status, originalStatus, reason, certificate: { protocol: certificate.protocol, digest: certificateDigest },
113
+ artifactSha256, receiptDigest: receiptJson === null ? null : sha(receiptJson), ...usage, sourceArchiveDigest });
114
+ }
115
+ if (tally.size || sources.size)
116
+ fail('orphan business rows');
117
+ return { history: { counts, byStatus, rows, rowsDigest: hash(rows) }, pendingReverification,
118
+ inheritedUsage: { adoptions: counts.adoptions, calls: counts.calls } };
119
+ }
120
+ function planBody(plan) { const { digest: _digest, ...body } = plan; return body; }
121
+ function validatePlan(plan) {
122
+ exact(plan, ['protocol', 'transactionId', 'hostAnchor', 'physicalPath', 'oldIdentity', 'nextIdentity', 'snapshot', 'archive',
123
+ 'history', 'pendingReverification', 'inheritedUsage', 'createdAt', 'expiresAt', 'digest']);
124
+ for (const identity of [plan.oldIdentity, plan.nextIdentity])
125
+ exact(identity, ['logicalPath', 'config', 'authorityDigest', 'publicKey', 'keyFingerprint']);
126
+ absolute(plan.physicalPath);
127
+ if (plan.physicalPath === plan.oldIdentity.logicalPath)
128
+ fail('staged copy must be separate from logical source');
129
+ exact(plan.snapshot, ['identity', 'bytes', 'digest']);
130
+ exact(plan.snapshot.identity, FILE_KEYS);
131
+ exact(plan.archive, ['logicalPath', 'physicalPath']);
132
+ exact(plan.history, ['counts', 'byStatus', 'rows', 'rowsDigest']);
133
+ exact(plan.history.counts, ['adoptions', 'calls', 'sourceArchives']);
134
+ exact(plan.history.byStatus, ['active', 'closed', 'rejected', 'unknown']);
135
+ exact(plan.inheritedUsage, ['adoptions', 'calls']);
136
+ if (!Array.isArray(plan.history.rows) || !Array.isArray(plan.pendingReverification))
137
+ fail('invalid plan');
138
+ if (plan.protocol !== PROTOCOL || plan.digest !== hash(planBody(plan))
139
+ || !SHA.test(plan.hostAnchor) || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(plan.transactionId)
140
+ || !Number.isSafeInteger(plan.createdAt) || !Number.isSafeInteger(plan.expiresAt)
141
+ || plan.createdAt < 1 || plan.expiresAt <= plan.createdAt || plan.expiresAt - plan.createdAt > 300_000
142
+ || plan.expiresAt > Math.min(plan.oldIdentity.config.expiresAt, plan.nextIdentity.config.expiresAt, plan.oldIdentity.config.runner.expiresAt, plan.nextIdentity.config.runner.expiresAt)
143
+ || plan.history.rowsDigest !== hash(plan.history.rows)
144
+ || plan.inheritedUsage.adoptions !== plan.history.counts.adoptions || plan.inheritedUsage.calls !== plan.history.counts.calls)
145
+ fail('invalid plan');
146
+ const actual = identities(plan.oldIdentity.logicalPath, plan.oldIdentity.config, plan.nextIdentity.config);
147
+ if (encode(actual) !== encode({ oldIdentity: plan.oldIdentity, nextIdentity: plan.nextIdentity }))
148
+ fail('public identity changed');
149
+ if (plan.archive.logicalPath !== archivePlacement(plan.oldIdentity.logicalPath, plan.oldIdentity.authorityDigest)
150
+ || plan.archive.physicalPath !== archivePlacement(plan.physicalPath, plan.oldIdentity.authorityDigest))
151
+ fail('archive path is not the sealed placement');
152
+ }
153
+ function archiveDirectory(dir) {
154
+ // directory() rejects links, aliases, foreign owners and group/other bits; also require exact 0700.
155
+ directory(dir);
156
+ if ((lstatSync(dir).mode & 0o777) !== 0o700)
157
+ fail('unsafe archive directory');
158
+ }
159
+ function syncDirectory(dir) {
160
+ const fd = openSync(dir, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
161
+ try {
162
+ fsyncSync(fd);
163
+ }
164
+ finally {
165
+ closeSync(fd);
166
+ }
167
+ }
168
+ /** Exact 0400 private copy of the planned snapshot bytes. */
169
+ function checkArchive(file, snapshot) {
170
+ archiveDirectory(dirname(file));
171
+ const archived = bytes(file, MAX_FILE);
172
+ if (archived.bytes !== snapshot.bytes || archived.digest !== snapshot.digest || (archived.identity.mode & 0o777) !== 0o400)
173
+ fail('archive mismatch');
174
+ return archived;
175
+ }
176
+ function writeArchive(plan, value) {
177
+ const file = plan.archive.physicalPath, dir = dirname(file);
178
+ directory(dirname(dir));
179
+ let created = false;
180
+ try {
181
+ mkdirSync(dir, { mode: 0o700 });
182
+ created = true;
183
+ }
184
+ catch (error) {
185
+ if (error.code !== 'EEXIST')
186
+ throw error;
187
+ }
188
+ if (created) {
189
+ chmodSync(dir, 0o700);
190
+ syncDirectory(dirname(dir));
191
+ }
192
+ archiveDirectory(dir);
193
+ // A crash may leave a private temp (possibly still hard-linked to the final name); drop it first.
194
+ removeArchiveTemps(dir, file);
195
+ if (present(file)) {
196
+ // Retry after a failure between archive link and commit: only the identical 0400 bytes are accepted.
197
+ const existing = read(file, MAX_FILE).snapshot;
198
+ if (existing.bytes !== plan.snapshot.bytes || existing.digest !== plan.snapshot.digest
199
+ || (existing.identity.mode & 0o777) !== 0o400)
200
+ fail('pre-existing archive differs');
201
+ }
202
+ else {
203
+ // The final name only ever appears complete: write+fsync+0400 under a private temp, then link().
204
+ const temp = join(dir, `${archiveTempPrefix(file)}${randomBytes(16).toString('hex')}`);
205
+ const fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
206
+ try {
207
+ try {
208
+ for (let offset = 0; offset < value.length;)
209
+ offset += writeSync(fd, value, offset, value.length - offset);
210
+ fsyncSync(fd);
211
+ fchmodSync(fd, 0o400);
212
+ fsyncSync(fd);
213
+ }
214
+ finally {
215
+ closeSync(fd);
216
+ }
217
+ linkSync(temp, file); // EEXIST when the final name appeared meanwhile
218
+ }
219
+ finally {
220
+ unlinkSync(temp);
221
+ }
222
+ }
223
+ syncDirectory(dir);
224
+ return checkArchive(file, plan.snapshot);
225
+ }
226
+ function present(file) {
227
+ try {
228
+ lstatSync(file);
229
+ return true;
230
+ }
231
+ catch (error) {
232
+ if (error.code === 'ENOENT')
233
+ return false;
234
+ throw error;
235
+ }
236
+ }
237
+ const archiveTempPrefix = (file) => `.${basename(file)}.tmp-`;
238
+ /** Remove crash leftovers named `.<archive>.tmp-<32 hex>`; anything else under that name fails closed. */
239
+ function removeArchiveTemps(dir, file) {
240
+ const prefix = archiveTempPrefix(file);
241
+ let removed = false;
242
+ for (const name of readdirSync(dir)) {
243
+ if (!name.startsWith(prefix))
244
+ continue;
245
+ const temp = join(dir, name), stat = lstatSync(temp);
246
+ if (!/^[a-f0-9]{32}$/u.test(name.slice(prefix.length)) || !stat.isFile() || stat.uid !== process.getuid?.())
247
+ fail('unsafe archive temp');
248
+ unlinkSync(temp);
249
+ removed = true;
250
+ }
251
+ if (removed)
252
+ syncDirectory(dir);
253
+ }
254
+ /** Stopped apply only: recover what a crash inside or right after BEGIN IMMEDIATE leaves behind, either a
255
+ * lone hot rollback `-journal` or a WAL pair (`-wal` + `-shm`; staged journals are WAL mode). One read-write
256
+ * open lets SQLite roll back the journal or discard uncommitted WAL frames; closing it checkpoints a committed
257
+ * WAL. Afterwards no sidecar may remain. Without the audit the file must equal the plan snapshot exactly; with
258
+ * it (the crash came after COMMIT) the normal retry branch validates the stored proof. Any other sidecar set
259
+ * fails closed. */
260
+ function recoverCrashedApply(plan) {
261
+ const file = plan.physicalPath;
262
+ const journal = present(`${file}-journal`), wal = present(`${file}-wal`), shm = present(`${file}-shm`);
263
+ if (!journal && !wal && !shm)
264
+ return;
265
+ if (journal ? wal || shm : !(wal && shm))
266
+ fail('sidecar requires stopped checkpointed database');
267
+ if (basename(file) !== 'creation-adoptions.sqlite')
268
+ fail('invalid physical journal');
269
+ directory(dirname(file));
270
+ for (const suffix of journal ? ['-journal'] : ['-wal', '-shm'])
271
+ identity(file + suffix);
272
+ const expected = identity(file), locator = pathToFileURL(file);
273
+ locator.searchParams.set('mode', 'rw');
274
+ const db = new DatabaseSync(locator);
275
+ let audited = false;
276
+ try {
277
+ if (encode(identity(file)) !== encode(expected))
278
+ fail('named identity changed');
279
+ // The first read under a shared lock rolls a hot journal back or recovers the WAL index.
280
+ db.exec('PRAGMA busy_timeout=0');
281
+ audited = db.prepare('SELECT name FROM sqlite_schema WHERE name=?').get(SEALED_ADOPTION_AUDIT) !== undefined;
282
+ if (wal)
283
+ db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
284
+ }
285
+ finally {
286
+ db.close();
287
+ }
288
+ for (const suffix of ['-wal', '-shm', '-journal'])
289
+ if (present(file + suffix))
290
+ fail('sidecar requires stopped checkpointed database');
291
+ if (encode(identity(file)) !== encode(expected))
292
+ fail('named identity changed');
293
+ if (!audited && encode(bytes(file, MAX_FILE)) !== encode(plan.snapshot))
294
+ fail('recovered journal differs from the plan snapshot');
295
+ }
296
+ function storedProof(db, plan) {
297
+ schema(db, true);
298
+ const audit = readSealedAdoptionAudit(db);
299
+ if (!audit)
300
+ fail('missing immutable proof');
301
+ const proof = audit.proof;
302
+ exact(proof, SEALED_PROOF_KEYS);
303
+ exact(proof.fileIdentity, FILE_KEYS);
304
+ exact(proof.archive, ['logicalPath', 'fileIdentity', 'bytes', 'digest', 'mode']);
305
+ exact(proof.archive.fileIdentity, FILE_KEYS);
306
+ exact(proof.liveBusiness, ['adoptions', 'calls', 'sourceArchives']);
307
+ exact(proof.inheritedUsage, ['adoptions', 'calls']);
308
+ if (encode(proof.plan) !== encode(plan) || encode(proof.fileIdentity) !== encode(plan.snapshot.identity)
309
+ || proof.archive.logicalPath !== plan.archive.logicalPath || proof.archive.bytes !== plan.snapshot.bytes
310
+ || proof.archive.digest !== plan.snapshot.digest || proof.archive.mode !== 0o400
311
+ || encode(proof.liveBusiness) !== encode(ZERO) || encode(proof.inheritedUsage) !== encode(plan.inheritedUsage))
312
+ fail('proof mismatch');
313
+ authority(db, plan.nextIdentity);
314
+ if (encode(sealedAdoptionInheritedUsage(db, plan.nextIdentity.authorityDigest)) !== encode(plan.inheritedUsage))
315
+ fail('proof mismatch');
316
+ if (encode(businessCounts(db)) !== encode(ZERO))
317
+ fail('nonempty business journal after sealed handoff');
318
+ return proof;
319
+ }
320
+ function sameArchive(file, proof) {
321
+ const archived = checkArchive(file, proof.plan.snapshot);
322
+ if (encode(archived.identity) !== encode(proof.archive.fileIdentity))
323
+ fail('archive identity mismatch');
324
+ }
325
+ /** Plan sealing a stopped staged Docker journal whose history is non-empty. Same caller duties as the
326
+ * empty handoff: independently verify the source copy and both configurations, bind them in hostAnchor
327
+ * and freeze the plan digest outside this file. */
328
+ export function planSealedAdoptionHandoff(scope) {
329
+ const pair = identities(scope.logicalPath, scope.oldConfig, scope.nextConfig);
330
+ absolute(scope.physicalPath);
331
+ absolute(scope.archiveLogicalPath);
332
+ absolute(scope.archivePhysicalPath);
333
+ if (scope.archiveLogicalPath !== archivePlacement(scope.logicalPath, pair.oldIdentity.authorityDigest)
334
+ || scope.archivePhysicalPath !== archivePlacement(scope.physicalPath, pair.oldIdentity.authorityDigest))
335
+ fail('archive path is not the sealed placement');
336
+ const deadline = Math.min(scope.oldConfig.expiresAt, scope.nextConfig.expiresAt, scope.oldConfig.runner.expiresAt, scope.nextConfig.runner.expiresAt);
337
+ if (scope.now >= deadline || scope.expiresAt > deadline)
338
+ fail('expired grant or plan');
339
+ const snapshot = bytes(scope.physicalPath, MAX_FILE);
340
+ const facts = openExisting(scope.physicalPath, db => {
341
+ db.exec('BEGIN');
342
+ try {
343
+ schema(db, false);
344
+ authority(db, pair.oldIdentity);
345
+ const result = readHistory(db, pair.oldIdentity);
346
+ if (encode(bytes(scope.physicalPath, MAX_FILE)) !== encode(snapshot))
347
+ fail('plan snapshot drift');
348
+ return result;
349
+ }
350
+ finally {
351
+ db.exec('ROLLBACK');
352
+ }
353
+ });
354
+ const body = { protocol: PROTOCOL, transactionId: scope.transactionId, hostAnchor: scope.hostAnchor,
355
+ physicalPath: scope.physicalPath, ...pair, snapshot,
356
+ archive: { logicalPath: scope.archiveLogicalPath, physicalPath: scope.archivePhysicalPath }, ...facts,
357
+ createdAt: scope.now, expiresAt: scope.expiresAt };
358
+ const plan = { ...body, digest: hash(body) };
359
+ validatePlan(plan);
360
+ return freeze(plan);
361
+ }
362
+ export function applySealedAdoptionHandoff(plan, input) {
363
+ validatePlan(plan);
364
+ if (plan.digest !== input.expectedPlanDigest || plan.hostAnchor !== input.expectedHostAnchor)
365
+ fail('external plan anchor mismatch');
366
+ const now = input.now;
367
+ if (!Number.isSafeInteger(now) || now < plan.createdAt)
368
+ fail('invalid apply time');
369
+ absolute(plan.physicalPath);
370
+ recoverCrashedApply(plan);
371
+ return openExisting(plan.physicalPath, db => {
372
+ db.exec('PRAGMA synchronous=FULL; BEGIN IMMEDIATE');
373
+ try {
374
+ if (db.prepare('SELECT name FROM sqlite_master WHERE name=?').get(SEALED_ADOPTION_AUDIT)) {
375
+ const proof = storedProof(db, plan); // acknowledgement only, never a new handoff
376
+ if (encode(identity(plan.physicalPath)) !== encode(proof.fileIdentity))
377
+ fail('retry file identity mismatch');
378
+ sameArchive(plan.archive.physicalPath, proof);
379
+ db.exec('ROLLBACK');
380
+ return freeze(proof);
381
+ }
382
+ if (now > plan.expiresAt)
383
+ fail('stale plan');
384
+ schema(db, false);
385
+ authority(db, plan.oldIdentity);
386
+ if (encode(readHistory(db, plan.oldIdentity)) !== encode({ history: plan.history,
387
+ pendingReverification: plan.pendingReverification, inheritedUsage: plan.inheritedUsage }))
388
+ fail('history drift');
389
+ const { value, snapshot } = read(plan.physicalPath, MAX_FILE);
390
+ if (encode(snapshot) !== encode(plan.snapshot))
391
+ fail('apply snapshot drift');
392
+ const archived = writeArchive(plan, value);
393
+ if (encode(bytes(plan.physicalPath, MAX_FILE)) !== encode(plan.snapshot))
394
+ fail('apply snapshot drift');
395
+ // Rows leave the live business tables only; their bytes stay in SQLite free pages until a
396
+ // later VACUUM. The 0400 archive, not this file, is the retained historical record.
397
+ db.exec('DELETE FROM calls; DELETE FROM source_archives; DELETE FROM adoptions');
398
+ swapAuthority(db, plan.oldIdentity, plan.nextIdentity);
399
+ if (encode(identity(plan.physicalPath)) !== encode(plan.snapshot.identity))
400
+ fail('apply file identity drift');
401
+ const body = { protocol: PROTOCOL, plan: structuredClone(plan), fileIdentity: identity(plan.physicalPath),
402
+ archive: { logicalPath: plan.archive.logicalPath, fileIdentity: archived.identity, bytes: archived.bytes,
403
+ digest: archived.digest, mode: 0o400 },
404
+ liveBusiness: { ...ZERO }, inheritedUsage: { ...plan.inheritedUsage } };
405
+ const proof = { ...body, digest: hash(body) };
406
+ db.exec(`${SEALED_ADOPTION_AUDIT_STATEMENTS.join(';')};`);
407
+ db.prepare(`INSERT INTO ${SEALED_ADOPTION_AUDIT} VALUES (1,?,?)`).run(encode(proof), proof.digest);
408
+ if (encode(storedProof(db, plan)) !== encode(proof))
409
+ fail('proof readback mismatch');
410
+ if (encode(identity(plan.physicalPath)) !== encode(plan.snapshot.identity))
411
+ fail('commit file identity drift');
412
+ db.exec('COMMIT');
413
+ return freeze(proof);
414
+ }
415
+ catch (error) {
416
+ try {
417
+ db.exec('ROLLBACK');
418
+ }
419
+ catch { }
420
+ throw error;
421
+ }
422
+ }, true);
423
+ }
424
+ /** Read-only Host cross-check of a possibly live journal (WAL sidecars allowed). Returns the stored
425
+ * sealed proof digest after the same audit validation the journal performs, or null when no audit
426
+ * object exists. The journal alone cannot detect a wholly dropped audit; the caller compares this
427
+ * result with its signed proof digest and refuses on any difference. */
428
+ export function readSealedAdoptionHandoffDigest(input) {
429
+ const file = input?.physicalPath;
430
+ return readJournal(file, db => {
431
+ const audit = readSealedAdoptionAudit(db);
432
+ if (!audit)
433
+ return null;
434
+ const rows = db.prepare('SELECT CAST(authority_digest AS BLOB) AS authority_digest FROM authority').all();
435
+ if (rows.length !== 1)
436
+ fail('authority singleton mismatch');
437
+ const authorityDigest = text(rows[0].authority_digest);
438
+ // Same checks as the journal constructor: authority binding, inherited usage and sealed plan ids.
439
+ sealedAdoptionInheritedUsage(db, authorityDigest);
440
+ sealedAdoptionPlanIds(db, authorityDigest);
441
+ return audit.proof.digest;
442
+ });
443
+ }
444
+ /** Read-only snapshot of a stopped or live journal. A stopped journal (no sidecars) is opened immutable so
445
+ * the read never creates -wal/-shm in a stage; a live Host's existing WAL sidecars need the ordinary
446
+ * read-only open to see committed frames. */
447
+ function readJournal(file, action) {
448
+ absolute(file);
449
+ if (basename(file) !== 'creation-adoptions.sqlite')
450
+ fail('invalid physical journal');
451
+ directory(dirname(file));
452
+ identity(file);
453
+ const wal = present(file + '-wal'), shm = present(file + '-shm'), hot = present(file + '-journal');
454
+ // A live WAL journal always has both sidecars. A lone -wal would make the read-only open create -shm in
455
+ // a stopped stage, a lone -shm is stale, and a hot rollback journal cannot be recovered read-only.
456
+ if (hot || wal !== shm)
457
+ fail('journal sidecars are incomplete; stop the Host cleanly or checkpoint the stopped database, then retry');
458
+ const live = wal;
459
+ const locator = pathToFileURL(file);
460
+ locator.searchParams.set('mode', 'ro');
461
+ if (!live)
462
+ locator.searchParams.set('immutable', '1');
463
+ const db = new DatabaseSync(locator, { readOnly: true });
464
+ try {
465
+ db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN');
466
+ try {
467
+ return action(db);
468
+ }
469
+ finally {
470
+ db.exec('ROLLBACK');
471
+ }
472
+ }
473
+ finally {
474
+ db.close();
475
+ }
476
+ }
477
+ /** Stopped-stage / post-cutover BEFORE START acceptance only. The live file may have been renamed on
478
+ * the same inode; the archive must sit in the same relative placement and keep its exact identity. */
479
+ export function verifySealedAdoptionHandoff(input) {
480
+ validatePlan(input.plan);
481
+ absolute(input.physicalPath);
482
+ absolute(input.archivePhysicalPath);
483
+ if (input.archivePhysicalPath !== join(dirname(input.physicalPath), SEALED_DIRECTORY, basename(input.plan.archive.physicalPath)))
484
+ fail('archive path is not the sealed placement');
485
+ return openExisting(input.physicalPath, db => {
486
+ db.exec('BEGIN');
487
+ try {
488
+ const proof = storedProof(db, input.plan);
489
+ if (!SHA.test(input.expectedProofDigest) || proof.digest !== input.expectedProofDigest
490
+ || encode(identity(input.physicalPath)) !== encode(proof.fileIdentity))
491
+ fail('external anchor or file identity mismatch');
492
+ sameArchive(input.archivePhysicalPath, proof);
493
+ return freeze(proof);
494
+ }
495
+ finally {
496
+ db.exec('ROLLBACK');
497
+ }
498
+ });
499
+ }
500
+ const SOURCE_ARCHIVE_KEYS = ['protocol', 'authorityId', 'authorityDigest', 'keyId', 'planId', 'certificateDigest', 'artifactSha256', 'source', 'signature'];
501
+ /** Immutable read of the exact 0400 archive the proof names. The binding is sameArchive() (dev/ino, mode,
502
+ * bytes and digest) before and after the read; SQLite reopens by path, so the held O_NOFOLLOW fd only refuses
503
+ * a symlink at open time and is not itself the binding. */
504
+ function readArchive(file, proof, action) {
505
+ sameArchive(file, proof);
506
+ for (const suffix of ['-wal', '-shm', '-journal'])
507
+ if (present(file + suffix))
508
+ fail('archive sidecar');
509
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
510
+ let db;
511
+ try {
512
+ const held = fstatSync(fd);
513
+ if (String(held.dev) !== proof.archive.fileIdentity.dev || String(held.ino) !== proof.archive.fileIdentity.ino)
514
+ fail('archive identity mismatch');
515
+ const locator = pathToFileURL(file);
516
+ locator.searchParams.set('mode', 'ro');
517
+ locator.searchParams.set('immutable', '1');
518
+ db = new DatabaseSync(locator, { readOnly: true });
519
+ db.exec('PRAGMA query_only=ON');
520
+ const result = action(db);
521
+ sameArchive(file, proof);
522
+ return result;
523
+ }
524
+ finally {
525
+ db?.close();
526
+ closeSync(fd);
527
+ }
528
+ }
529
+ /** The journal's immutable sealed proof, bound to the caller's signed proof digest and re-validated against
530
+ * its plan, archive, next authority, inherited usage and the journal's own file identity. */
531
+ function anchoredProof(db, file, expectedProofDigest) {
532
+ schema(db, true);
533
+ const audit = readSealedAdoptionAudit(db);
534
+ if (!audit)
535
+ fail('missing immutable proof');
536
+ const stored = audit.proof;
537
+ if (stored.digest !== expectedProofDigest)
538
+ fail('external proof anchor mismatch');
539
+ exact(stored, SEALED_PROOF_KEYS);
540
+ exact(stored.fileIdentity, FILE_KEYS);
541
+ exact(stored.archive, ['logicalPath', 'fileIdentity', 'bytes', 'digest', 'mode']);
542
+ exact(stored.archive.fileIdentity, FILE_KEYS);
543
+ validatePlan(stored.plan);
544
+ if (stored.archive.logicalPath !== stored.plan.archive.logicalPath || stored.archive.bytes !== stored.plan.snapshot.bytes
545
+ || stored.archive.digest !== stored.plan.snapshot.digest || stored.archive.mode !== 0o400)
546
+ fail('proof mismatch');
547
+ authority(db, stored.plan.nextIdentity);
548
+ if (encode(sealedAdoptionInheritedUsage(db, stored.plan.nextIdentity.authorityDigest)) !== encode(stored.plan.inheritedUsage))
549
+ fail('proof mismatch');
550
+ if (encode(identity(file)) !== encode(stored.fileIdentity))
551
+ fail('journal file identity mismatch');
552
+ return structuredClone(stored);
553
+ }
554
+ /** Comparison form of a plan id. Plan ids are ASCII (`ID`), so case folding is the only aliasing: SQLite
555
+ * compares them exactly, but two ids differing only in case would still read as the same tool name. */
556
+ function planIdNameForm(planId) {
557
+ return planId.toLowerCase();
558
+ }
559
+ /** S2 naming isolation for sealed tool replay (check and refuse only; nothing adopts with it yet).
560
+ *
561
+ * A replayed sealed tool must return under a NEW plan id. Bound to the caller's signed proof digest, this
562
+ * refuses `planId` when it names any sealed history row (every status, not only pending reverification),
563
+ * equals one ignoring case, or equals ignoring case any `adoptions` row of the live ledger, whatever its
564
+ * authority (stricter than the next authority alone, intentionally fail-closed).
565
+ * It reads the journal only and grants nothing: passing it is necessary for a replay plan, never sufficient.
566
+ * It runs in its own read-only snapshot and reserves nothing, and `CreationCapabilityJournal.claim` compares
567
+ * plan ids exactly without case folding, so whoever wires it must re-run this case-folded check inside the
568
+ * claim writer transaction; otherwise a concurrent `FRESH`/`fresh` pair both pass. */
569
+ export function assertSealedReplayPlanIdIsolated(input) {
570
+ const file = input?.physicalPath, planId = input?.planId;
571
+ if (typeof input?.expectedProofDigest !== 'string' || !SHA.test(input.expectedProofDigest))
572
+ fail('invalid expected proof digest');
573
+ if (typeof planId !== 'string' || !ID.test(planId))
574
+ fail('invalid plan id');
575
+ return readJournal(file, db => {
576
+ const proof = anchoredProof(db, file, input.expectedProofDigest);
577
+ const form = planIdNameForm(planId);
578
+ for (const row of proof.plan.history.rows) {
579
+ if (typeof row?.planId !== 'string')
580
+ fail('invalid sealed row');
581
+ if (row.planId === planId)
582
+ fail('plan id names a sealed row');
583
+ if (planIdNameForm(row.planId) === form)
584
+ fail('plan id collides with a sealed row');
585
+ }
586
+ for (const row of proof.plan.pendingReverification) {
587
+ if (typeof row?.planId !== 'string' || planIdNameForm(row.planId) === form)
588
+ fail('plan id collides with a sealed row');
589
+ }
590
+ const live = db.prepare('SELECT CAST(plan_id AS BLOB) AS plan_id FROM adoptions').all();
591
+ if (live.some(row => planIdNameForm(text(row.plan_id)) === form))
592
+ fail('plan id already names a live adoption');
593
+ return freeze({ protocol: 'dsh-sealed-replay-plan-id-isolation/v1', planId, proofDigest: proof.digest,
594
+ sealedRows: proof.plan.history.rows.length });
595
+ });
596
+ }
597
+ /** Read-only provenance reader for re-verifying one sealed `pendingReverification` tool. It binds the live
598
+ * or stopped journal's immutable proof to the caller's signed proof digest, reads the exact sealed 0400
599
+ * archive under the OLD authority and returns only candidate source files. It grants nothing: no
600
+ * certificate, artifact, receipt or tools leave this function, and re-verification itself is the caller's. */
601
+ export function readSealedReverificationSource(input) {
602
+ const file = input?.physicalPath, planId = input?.planId;
603
+ if (typeof input?.expectedProofDigest !== 'string' || !SHA.test(input.expectedProofDigest))
604
+ fail('invalid expected proof digest');
605
+ if (typeof planId !== 'string' || !ID.test(planId))
606
+ fail('invalid plan id');
607
+ const proof = readJournal(file, db => anchoredProof(db, file, input.expectedProofDigest));
608
+ const pending = proof.plan.pendingReverification.find(row => row?.planId === planId);
609
+ if (!pending)
610
+ fail('plan is not pending reverification');
611
+ const row = proof.plan.history.rows.find(row => row?.planId === planId);
612
+ if (!row || row.status !== 'active' || row.originalStatus !== 'active')
613
+ fail('sealed row is not active');
614
+ if (row.sourceArchiveDigest === null)
615
+ fail('no archived source');
616
+ if (typeof row.sourceArchiveDigest !== 'string' || !SHA.test(row.sourceArchiveDigest))
617
+ fail('invalid sealed row');
618
+ const old = proof.plan.oldIdentity;
619
+ return readArchive(join(dirname(file), SEALED_DIRECTORY, `${old.authorityDigest}.sqlite`), proof, db => {
620
+ schema(db, false);
621
+ authority(db, old);
622
+ const adoptions = db.prepare(`SELECT CAST(status AS BLOB) AS status, CAST(certificate_json AS BLOB) AS certificate_json,
623
+ CAST(certificate_digest AS BLOB) AS certificate_digest, CAST(artifact_sha AS BLOB) AS artifact_sha FROM adoptions WHERE plan_id=?`).all(planId);
624
+ if (adoptions.length !== 1)
625
+ fail('archived adoption mismatch');
626
+ const adoption = adoptions[0], certificateJson = text(adoption.certificate_json);
627
+ if (text(adoption.status) !== 'active' || sha(certificateJson) !== pending.certificateDigest
628
+ || text(adoption.certificate_digest) !== pending.certificateDigest || text(adoption.artifact_sha) !== pending.artifactSha256)
629
+ fail('archived adoption mismatch');
630
+ const certificate = canonical(certificateJson, 'certificate');
631
+ try {
632
+ validatePluginCreationVerificationCertificate(certificate);
633
+ }
634
+ catch {
635
+ fail('archived certificate is invalid');
636
+ }
637
+ const sources = db.prepare('SELECT CAST(archive_json AS BLOB) AS archive_json FROM source_archives WHERE plan_id=?').all(planId);
638
+ if (sources.length !== 1)
639
+ fail('no archived source');
640
+ const raw = text(sources[0].archive_json);
641
+ if (Buffer.byteLength(raw) > SOURCE_ARCHIVE_LIMIT)
642
+ fail('archived source too large');
643
+ const archiveRowDigest = sha(raw);
644
+ if (archiveRowDigest !== row.sourceArchiveDigest)
645
+ fail('archived source digest mismatch');
646
+ const item = canonical(raw, 'source archive');
647
+ exact(item, SOURCE_ARCHIVE_KEYS);
648
+ if (item.protocol !== 'dsh-created-capability-source-archive/v1' || item.authorityId !== old.config.authorityId
649
+ || item.authorityDigest !== old.authorityDigest || item.keyId !== old.config.keyId || item.planId !== planId
650
+ || item.certificateDigest !== pending.certificateDigest || item.artifactSha256 !== pending.artifactSha256
651
+ || certificate.plan.id !== planId || certificate.plan.artifactSha256 !== pending.artifactSha256)
652
+ fail('archived source authority mismatch');
653
+ const signature = item.signature;
654
+ const { signature: _signature, ...body } = item;
655
+ if (typeof signature !== 'string' || !/^[A-Za-z0-9_-]{86}$/u.test(signature)
656
+ || Buffer.from(signature, 'base64url').toString('base64url') !== signature
657
+ || !verify(null, Buffer.from(SOURCE_ARCHIVE_DOMAIN + encode(body)), createPublicKey(old.publicKey), Buffer.from(signature, 'base64url')))
658
+ fail('archived source signature mismatch');
659
+ try {
660
+ validateCreationCapabilitySource(item.source, certificate);
661
+ }
662
+ catch {
663
+ fail('archived source is invalid');
664
+ }
665
+ const name = certificate.plan.name, root = `plugins/${name}/`, files = [];
666
+ for (const entry of item.source.files) {
667
+ if (typeof entry?.path !== 'string' || typeof entry.content !== 'string' || !entry.path.startsWith(root))
668
+ fail('archived source file outside the plugin');
669
+ // Same order as source-revision parentFiles: a replayed file carries no mode, so only 100644 is admissible.
670
+ if (entry.mode !== '100644')
671
+ fail('archived source file mode');
672
+ const path = entry.path.slice(root.length);
673
+ // Host-generated metadata is regenerated by the source job, never taken from the archive.
674
+ if (RESERVED_OUTPUTS.includes(path))
675
+ continue;
676
+ try {
677
+ validateSourceCreationFiles([{ path, content: entry.content }]);
678
+ }
679
+ catch {
680
+ fail(`archived source file outside candidate authority: ${JSON.stringify(path)}`);
681
+ }
682
+ files.push({ path, content: entry.content });
683
+ }
684
+ if (!files.length)
685
+ fail('archived source has no candidate files');
686
+ try {
687
+ validateSourceCreationFiles(files);
688
+ }
689
+ catch {
690
+ fail('archived source exceeds candidate bounds');
691
+ }
692
+ return freeze({ planId, name, files, archiveRowDigest, filesDigest: hash(files), oldAuthorityDigest: old.authorityDigest });
693
+ });
694
+ }
695
+ //# sourceMappingURL=creation-capability-sealed-handoff.js.map