@dsh-enhanced/assistant-isolation 0.1.65 → 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 (116) hide show
  1. package/README.md +287 -9
  2. package/lib/audit-archive.d.ts +1 -1
  3. package/lib/audit-archive.d.ts.map +1 -1
  4. package/lib/audit-archive.js +31 -13
  5. package/lib/audit-archive.js.map +1 -1
  6. package/lib/cli.d.ts +7 -3
  7. package/lib/cli.d.ts.map +1 -1
  8. package/lib/cli.js +104 -23
  9. package/lib/cli.js.map +1 -1
  10. package/lib/config.d.ts +15 -2
  11. package/lib/config.d.ts.map +1 -1
  12. package/lib/config.js +49 -9
  13. package/lib/config.js.map +1 -1
  14. package/lib/diagnostics.js +2 -2
  15. package/lib/diagnostics.js.map +1 -1
  16. package/lib/index.d.ts +17 -1
  17. package/lib/index.d.ts.map +1 -1
  18. package/lib/index.js +17 -1
  19. package/lib/index.js.map +1 -1
  20. package/lib/ledger.d.ts +19 -2
  21. package/lib/ledger.d.ts.map +1 -1
  22. package/lib/ledger.js +371 -44
  23. package/lib/ledger.js.map +1 -1
  24. package/lib/linux-cgroups.d.ts +75 -0
  25. package/lib/linux-cgroups.d.ts.map +1 -0
  26. package/lib/linux-cgroups.js +517 -0
  27. package/lib/linux-cgroups.js.map +1 -0
  28. package/lib/linux-execution-contract.d.ts +123 -0
  29. package/lib/linux-execution-contract.d.ts.map +1 -0
  30. package/lib/linux-execution-contract.js +279 -0
  31. package/lib/linux-execution-contract.js.map +1 -0
  32. package/lib/linux-execution.d.ts +123 -0
  33. package/lib/linux-execution.d.ts.map +1 -0
  34. package/lib/linux-execution.js +1264 -0
  35. package/lib/linux-execution.js.map +1 -0
  36. package/lib/linux-job-recovery.d.ts +5 -0
  37. package/lib/linux-job-recovery.d.ts.map +1 -0
  38. package/lib/linux-job-recovery.js +63 -0
  39. package/lib/linux-job-recovery.js.map +1 -0
  40. package/lib/linux-namespace.d.ts +80 -0
  41. package/lib/linux-namespace.d.ts.map +1 -0
  42. package/lib/linux-namespace.js +471 -0
  43. package/lib/linux-namespace.js.map +1 -0
  44. package/lib/linux-repository-admission.d.ts +26 -0
  45. package/lib/linux-repository-admission.d.ts.map +1 -0
  46. package/lib/linux-repository-admission.js +64 -0
  47. package/lib/linux-repository-admission.js.map +1 -0
  48. package/lib/linux-repository-runner.d.ts +54 -0
  49. package/lib/linux-repository-runner.d.ts.map +1 -0
  50. package/lib/linux-repository-runner.js +614 -0
  51. package/lib/linux-repository-runner.js.map +1 -0
  52. package/lib/linux-runner-state-migration.d.ts +94 -0
  53. package/lib/linux-runner-state-migration.d.ts.map +1 -0
  54. package/lib/linux-runner-state-migration.js +463 -0
  55. package/lib/linux-runner-state-migration.js.map +1 -0
  56. package/lib/linux-runner-state.d.ts +53 -0
  57. package/lib/linux-runner-state.d.ts.map +1 -0
  58. package/lib/linux-runner-state.js +332 -0
  59. package/lib/linux-runner-state.js.map +1 -0
  60. package/lib/linux-runtime-source.d.ts +28 -0
  61. package/lib/linux-runtime-source.d.ts.map +1 -0
  62. package/lib/linux-runtime-source.js +237 -0
  63. package/lib/linux-runtime-source.js.map +1 -0
  64. package/lib/linux-runtime.d.ts +37 -0
  65. package/lib/linux-runtime.d.ts.map +1 -0
  66. package/lib/linux-runtime.js +192 -0
  67. package/lib/linux-runtime.js.map +1 -0
  68. package/lib/linux-system-snapshot.d.ts +47 -0
  69. package/lib/linux-system-snapshot.d.ts.map +1 -0
  70. package/lib/linux-system-snapshot.js +704 -0
  71. package/lib/linux-system-snapshot.js.map +1 -0
  72. package/lib/linux-user-namespace.d.ts +79 -0
  73. package/lib/linux-user-namespace.d.ts.map +1 -0
  74. package/lib/linux-user-namespace.js +234 -0
  75. package/lib/linux-user-namespace.js.map +1 -0
  76. package/lib/linux-verifier-runner.d.ts +27 -0
  77. package/lib/linux-verifier-runner.d.ts.map +1 -0
  78. package/lib/linux-verifier-runner.js +346 -0
  79. package/lib/linux-verifier-runner.js.map +1 -0
  80. package/lib/native-cgroup-roots.d.ts +20 -0
  81. package/lib/native-cgroup-roots.d.ts.map +1 -0
  82. package/lib/native-cgroup-roots.js +121 -0
  83. package/lib/native-cgroup-roots.js.map +1 -0
  84. package/lib/native-resources.d.ts +44 -0
  85. package/lib/native-resources.d.ts.map +1 -0
  86. package/lib/native-resources.js +354 -0
  87. package/lib/native-resources.js.map +1 -0
  88. package/lib/probe.d.ts +10 -0
  89. package/lib/probe.d.ts.map +1 -1
  90. package/lib/probe.js +52 -2
  91. package/lib/probe.js.map +1 -1
  92. package/lib/service.d.ts +1 -1
  93. package/lib/service.d.ts.map +1 -1
  94. package/lib/service.js +122 -9
  95. package/lib/service.js.map +1 -1
  96. package/lib/storage.d.ts.map +1 -1
  97. package/lib/storage.js +36 -6
  98. package/lib/storage.js.map +1 -1
  99. package/lib/tools.js +1 -1
  100. package/lib/tools.js.map +1 -1
  101. package/lib/types.d.ts +5 -1
  102. package/lib/types.d.ts.map +1 -1
  103. package/lib/verifier-runner.d.ts +4 -2
  104. package/lib/verifier-runner.d.ts.map +1 -1
  105. package/lib/verifier-runner.js +22 -1
  106. package/lib/verifier-runner.js.map +1 -1
  107. package/lib/version.d.ts +1 -1
  108. package/lib/version.js +1 -1
  109. package/package.json +15 -10
  110. package/runtime/linux-candidate-entry.py +82 -0
  111. package/runtime/linux-namespace-worker.mjs +561 -0
  112. package/runtime/linux-seccomp-policy.LICENSE +202 -0
  113. package/runtime/linux-seccomp-policy.json +886 -0
  114. package/runtime/linux-seccomp.py +106 -0
  115. package/runtime/linux-userns-launcher.py +149 -0
  116. package/runtime/linux-userns-proof.py +299 -0
@@ -0,0 +1,1264 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, statfs, unlink } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
5
+ import { LinuxCgroupLease } from './linux-cgroups.js';
6
+ import { linuxExecutionContractDigest, linuxExecutionIntentDigest, linuxExecutionResourcesDigest, linuxExecutionPartialCleanupProofDigest, validateLinuxExecutionPartialCleanupProof, validateLinuxExecutionContract, validateLinuxExecutionIntent, validateLinuxExecutionResources } from './linux-execution-contract.js';
7
+ import { runLinuxNamespace, runLinuxRepositoryNamespace } from './linux-namespace.js';
8
+ import { pruneResult, validateRetention } from './storage-policy.js';
9
+ import { verifyLinuxSystemSnapshot } from './linux-system-snapshot.js';
10
+ import { inspectLinuxRepositoryCandidate, inspectLinuxUserNamespace, linuxRepositoryImplementationDigest, validateLinuxRepositoryCandidateWitness, validateLinuxUserNamespaceWitness, verifyLinuxUserNamespaceMapping } from './linux-user-namespace.js';
11
+ const active = new Set();
12
+ // This is a drain witness, never an execution grant. It permits only cleanup
13
+ // after this exact Host has awaited every internal create/admit continuation.
14
+ // Failure to retain a witness is conservative: durable recovery waits for death.
15
+ const completedRuns = new Map();
16
+ const MAX_COMPLETED_RUNS = 4096;
17
+ const controllers = ['cpu', 'memory', 'pids', 'freezer'];
18
+ const MAX_RECORD_BYTES = 14 * 1024 * 1024;
19
+ const phases = ['prepared', 'creating-worker', 'creating-keeper', 'recorded', 'keeper-admitted', 'user-namespace-ready', 'worker-admitted', 'candidate-ready', 'running', 'settled', 'prestart-quiesced', 'unknown'];
20
+ const canonical = (value) => Array.isArray(value) ? `[${value.map(canonical).join(',')}]`
21
+ : value && typeof value === 'object' ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
22
+ : JSON.stringify(value);
23
+ const digest = (value) => createHash('sha256').update(canonical(value)).digest('hex');
24
+ function fail(message) { throw new Error(`linux execution: ${message}`); }
25
+ function keys(value, expected) {
26
+ if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype
27
+ || Object.keys(value).sort().join() !== [...expected].sort().join())
28
+ fail('record fields differ');
29
+ }
30
+ const positive = (value) => Number.isSafeInteger(value) && value > 0;
31
+ const missing = (error) => error.code === 'ENOENT';
32
+ const boot = async () => (await readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim().toLowerCase();
33
+ async function exists(path) { try {
34
+ await lstat(path);
35
+ return true;
36
+ }
37
+ catch (error) {
38
+ if (missing(error))
39
+ return false;
40
+ throw error;
41
+ } }
42
+ function platform() {
43
+ const uid = process.getuid?.();
44
+ if (process.platform !== 'linux' || !['x64', 'arm64'].includes(process.arch) || !uid)
45
+ fail('requires non-root Linux x64/arm64');
46
+ return uid;
47
+ }
48
+ async function privateDirectory(path) {
49
+ if (!isAbsolute(path) || resolve(path) !== path || /[\p{Cc}\\]/u.test(path) || await realpath(path) !== path)
50
+ fail('state path is not canonical');
51
+ const info = await lstat(path);
52
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== platform() || (info.mode & 0o077) !== 0)
53
+ fail('state directory is not owner-private');
54
+ }
55
+ async function syncDirectory(path) {
56
+ const handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
57
+ try {
58
+ await handle.sync();
59
+ }
60
+ finally {
61
+ await handle.close();
62
+ }
63
+ }
64
+ async function childDirectory(parent, name) {
65
+ await privateDirectory(parent);
66
+ try {
67
+ await mkdir(join(parent, name), { mode: 0o700 });
68
+ await syncDirectory(parent);
69
+ }
70
+ catch (error) {
71
+ if (error.code !== 'EEXIST')
72
+ throw error;
73
+ }
74
+ await privateDirectory(join(parent, name));
75
+ }
76
+ async function setup(stateRoot) {
77
+ await privateDirectory(stateRoot);
78
+ for (const name of ['jobs', 'receipts', 'namespaces'])
79
+ await childDirectory(stateRoot, name);
80
+ }
81
+ function paths(ctx) {
82
+ const name = `${ctx.intent.jobId}-${ctx.intent.nativeNonce}.json`;
83
+ return { journal: join(ctx.stateRoot, 'jobs', name), receipt: join(ctx.stateRoot, 'receipts', name),
84
+ partial: join(ctx.stateRoot, 'receipts', name.replace(/\.json$/u, '.partial.json')) };
85
+ }
86
+ async function readRecord(path) {
87
+ await privateDirectory(dirname(path));
88
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
89
+ try {
90
+ const info = await handle.stat(), named = await lstat(path);
91
+ if (!info.isFile() || info.uid !== platform() || (info.mode & 0o7777) !== 0o600 || info.nlink !== 1
92
+ || info.size <= 0 || info.size > MAX_RECORD_BYTES || info.dev !== named.dev || info.ino !== named.ino || named.isSymbolicLink())
93
+ fail('private record identity differs');
94
+ const bytes = await handle.readFile();
95
+ const after = await handle.stat();
96
+ if (bytes.length !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs || after.ctimeMs !== info.ctimeMs)
97
+ fail('private record changed while reading');
98
+ return JSON.parse(bytes.toString('utf8'));
99
+ }
100
+ finally {
101
+ await handle.close();
102
+ }
103
+ }
104
+ async function emptyPlaceholder(path) {
105
+ await privateDirectory(dirname(path));
106
+ let handle;
107
+ try {
108
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
109
+ }
110
+ catch (error) {
111
+ if (missing(error))
112
+ return undefined;
113
+ throw error;
114
+ }
115
+ try {
116
+ const info = await handle.stat(), named = await lstat(path);
117
+ if (!info.isFile() || info.uid !== platform() || (info.mode & 0o7777) !== 0o600 || info.nlink !== 1
118
+ || named.isSymbolicLink() || named.dev !== info.dev || named.ino !== info.ino || named.nlink !== 1
119
+ || named.uid !== info.uid || named.mode !== info.mode || named.size !== info.size)
120
+ fail('receipt placeholder identity differs');
121
+ const after = await handle.stat();
122
+ if (after.nlink !== 1 || after.size !== info.size || after.ctimeMs !== info.ctimeMs || after.mode !== info.mode || after.uid !== info.uid)
123
+ fail('receipt placeholder changed');
124
+ return info.size === 0 ? { path, dev: info.dev, ino: info.ino } : undefined;
125
+ }
126
+ finally {
127
+ await handle.close();
128
+ }
129
+ }
130
+ async function repairPlaceholder(path) {
131
+ if (!await exists(path))
132
+ return;
133
+ const identity = await emptyPlaceholder(path);
134
+ if (!identity)
135
+ fail('refusing to replace nonempty receipt');
136
+ const info = await lstat(path);
137
+ if (!info.isFile() || info.dev !== identity.dev || info.ino !== identity.ino || info.size !== 0 || info.nlink !== 1
138
+ || info.uid !== platform() || (info.mode & 0o7777) !== 0o600)
139
+ fail('receipt placeholder changed');
140
+ await unlink(path);
141
+ await syncDirectory(dirname(path));
142
+ }
143
+ async function writeRecord(path, value, fresh = false) {
144
+ await privateDirectory(dirname(path));
145
+ const data = JSON.stringify(value);
146
+ if (Buffer.byteLength(data) > MAX_RECORD_BYTES)
147
+ fail('private record exceeds bounds');
148
+ // Reserve initial identity exclusively. A crash before rename leaves an invalid
149
+ // record, which fails closed; no external creation has been authorized yet.
150
+ if (fresh) {
151
+ const reservation = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
152
+ try {
153
+ await reservation.sync();
154
+ }
155
+ finally {
156
+ await reservation.close();
157
+ }
158
+ await syncDirectory(dirname(path));
159
+ }
160
+ else
161
+ await readRecord(path);
162
+ const temporary = `${path}.${randomBytes(16).toString('hex')}.tmp`;
163
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
164
+ try {
165
+ await handle.writeFile(data);
166
+ await handle.sync();
167
+ }
168
+ finally {
169
+ await handle.close();
170
+ }
171
+ try {
172
+ await rename(temporary, path);
173
+ await syncDirectory(dirname(path));
174
+ }
175
+ finally {
176
+ await unlink(temporary).catch(error => { if (!missing(error))
177
+ throw error; });
178
+ }
179
+ }
180
+ async function currentIdentity(pid) {
181
+ try {
182
+ const [raw, status, bootId] = await Promise.all([readFile(`/proc/${pid}/stat`, 'utf8'), readFile(`/proc/${pid}/status`, 'utf8'), boot()]);
183
+ const fields = raw.slice(raw.lastIndexOf(') ') + 2).trim().split(/\s+/u);
184
+ const uid = /^Uid:\s+(\d+)/mu.exec(status);
185
+ if (!uid || !/^[1-9][0-9]*$/u.test(fields[19] ?? ''))
186
+ fail('process identity unavailable');
187
+ if (fields[0] === 'Z' || fields[0] === 'X')
188
+ return undefined;
189
+ return { pid, startTicks: fields[19], uid: Number(uid[1]), bootId };
190
+ }
191
+ catch (error) {
192
+ if (missing(error) || error.code === 'ESRCH')
193
+ return undefined;
194
+ throw error;
195
+ }
196
+ }
197
+ function validateIdentity(value, intent) {
198
+ keys(value, ['pid', 'startTicks', 'uid', 'bootId']);
199
+ if (!positive(value.pid) || typeof value.startTicks !== 'string' || !/^[1-9][0-9]{0,63}$/u.test(value.startTicks)
200
+ || value.uid !== intent.uid || value.bootId !== intent.bootId)
201
+ fail('journal process identity differs');
202
+ }
203
+ async function rootsIdentity(contract, expected) {
204
+ if (contract.cgroupRoots.mode !== 'v1')
205
+ fail('cgroup v2 process placement is not yet verified');
206
+ const entries = {};
207
+ const mountinfo = (await readFile('/proc/self/mountinfo', 'utf8')).split('\n');
208
+ for (const key of controllers) {
209
+ const path = contract.cgroupRoots[key], info = await lstat(path);
210
+ const mount = path.split('/').slice(0, 5).join('/');
211
+ if (await realpath(path) !== path || !info.isDirectory() || info.isSymbolicLink() || info.uid !== platform() || (info.mode & 0o022) !== 0
212
+ || (await statfs(path)).type !== 0x27e0eb || mountinfo.filter(line => {
213
+ const [before, after] = line.split(' - '), fields = before?.split(' '), fs = after?.split(' ');
214
+ return fields?.[4] === mount && fs?.[0] === 'cgroup' && fs[2]?.split(',').includes(key);
215
+ }).length !== 1)
216
+ fail('delegated root identity differs');
217
+ entries[key] = { path, dev: info.dev, ino: info.ino };
218
+ if (expected && canonical(entries[key]) !== canonical(expected.roots[key]))
219
+ fail('delegated root changed');
220
+ }
221
+ return entries;
222
+ }
223
+ async function context(input) {
224
+ platform();
225
+ const contract = validateLinuxExecutionContract(input.contract), intent = validateLinuxExecutionIntent(input.intent, contract);
226
+ await privateDirectory(input.stateRoot);
227
+ for (const name of ['jobs', 'receipts', 'namespaces'])
228
+ await privateDirectory(join(input.stateRoot, name));
229
+ if (intent.uid !== platform() || intent.namespacePath !== join(input.stateRoot, 'namespaces', `namespace-${intent.jobId}-${intent.nativeNonce}`))
230
+ fail('intent state path differs');
231
+ if (await boot() !== intent.bootId)
232
+ fail('boot changed; recovery remains unknown');
233
+ await rootsIdentity(contract, intent);
234
+ return { stateRoot: input.stateRoot, contract, intent };
235
+ }
236
+ function validateSnapshot(value, kind, ctx) {
237
+ keys(value, ['schemaVersion', 'mode', 'bootId', 'jobId', 'nonce', 'uid', 'deadline', 'limits', 'directories']);
238
+ const expectedLimits = kind === 'worker' ? { memoryMiB: ctx.contract.limits.memoryMiB, pidsLimit: ctx.contract.limits.pidsLimit, cpus: ctx.contract.limits.cpus } : ctx.contract.keeperLimits;
239
+ const nonce = kind === 'worker' ? ctx.intent.workerNonce : ctx.intent.keeperNonce;
240
+ if (value.schemaVersion !== 1 || value.mode !== 'v1' || value.bootId !== ctx.intent.bootId || value.jobId !== ctx.intent.jobId
241
+ || value.nonce !== nonce || value.uid !== ctx.intent.uid || value.deadline !== ctx.intent.deadline || canonical(value.limits) !== canonical(expectedLimits))
242
+ fail('snapshot binding differs');
243
+ keys(value.directories, controllers);
244
+ for (const key of controllers) {
245
+ const item = value.directories[key], parent = ctx.intent.roots[key];
246
+ keys(item, ['path', 'dev', 'ino']);
247
+ if (item.path !== join(parent.path, `job-${ctx.intent.jobId}-${nonce}`) || item.dev !== parent.dev || !positive(item.ino) || item.ino === parent.ino)
248
+ fail('snapshot directory differs');
249
+ }
250
+ }
251
+ function validateResult(value, ctx) {
252
+ if (!value || typeof value !== 'object')
253
+ fail('invalid result');
254
+ const item = value;
255
+ keys(item, ['jobId', 'status', 'quiescent', 'stdout', 'stderr', 'artifacts', ...(Object.hasOwn(item, 'exitCode') ? ['exitCode'] : []), ...(Object.hasOwn(item, 'reason') ? ['reason'] : [])]);
256
+ if (item.jobId !== ctx.intent.jobId || !['succeeded', 'failed', 'cancelled', 'timed-out', 'unknown'].includes(item.status)
257
+ || typeof item.quiescent !== 'boolean' || typeof item.stdout !== 'string' || typeof item.stderr !== 'string'
258
+ || !item.stdout.isWellFormed() || !item.stderr.isWellFormed() || Buffer.byteLength(item.stdout) + Buffer.byteLength(item.stderr) > ctx.contract.limits.maxOutputBytes
259
+ || !Array.isArray(item.artifacts) || item.artifacts.length > ctx.contract.limits.maxFiles
260
+ || Object.hasOwn(item, 'reason') && (typeof item.reason !== 'string' || item.reason.length > 8192)
261
+ || Object.hasOwn(item, 'exitCode') && (!Number.isSafeInteger(item.exitCode) || item.exitCode < 0 || item.exitCode > 255))
262
+ fail('result bounds differ');
263
+ let bytes = 0;
264
+ const names = new Set();
265
+ for (const artifact of item.artifacts) {
266
+ keys(artifact, ['path', 'content']);
267
+ if (typeof artifact.path !== 'string' || !safePath(artifact.path) || names.has(artifact.path) || typeof artifact.content !== 'string' || !artifact.content.isWellFormed())
268
+ fail('invalid result artifact');
269
+ names.add(artifact.path);
270
+ bytes += Buffer.byteLength(artifact.content);
271
+ }
272
+ if (bytes > ctx.contract.limits.maxArtifactBytes || ctx.contract.schemaVersion !== 1 && item.artifacts.length !== 0)
273
+ fail('result artifacts exceed bounds');
274
+ }
275
+ async function readJournal(ctx) {
276
+ const value = await readRecord(paths(ctx).journal);
277
+ keys(value, ['schemaVersion', 'intent', 'creator', 'phase', 'worker', 'keeper', 'workerIdentity', 'keeperIdentity', 'namespace', 'result', ...(ctx.intent.schemaVersion !== 1 ? ['repositoryArtifacts'] : []), ...(ctx.intent.schemaVersion === 3 ? ['userNamespace', 'candidateIdentity'] : [])]);
278
+ if (value.schemaVersion !== ctx.intent.schemaVersion || canonical(value.intent) !== canonical(ctx.intent) || !phases.includes(value.phase))
279
+ fail('journal intent or phase differs');
280
+ validateIdentity(value.creator, ctx.intent);
281
+ for (const kind of ['worker', 'keeper']) {
282
+ if (value[kind] !== null)
283
+ validateSnapshot(value[kind], kind, ctx);
284
+ const identity = value[`${kind}Identity`];
285
+ if (identity !== null)
286
+ validateIdentity(identity, ctx.intent);
287
+ }
288
+ if (value.namespace !== null) {
289
+ keys(value.namespace, ['path', 'dev', 'ino']);
290
+ if (value.namespace.path !== ctx.intent.namespacePath || !Number.isSafeInteger(value.namespace.dev) || !positive(value.namespace.ino))
291
+ fail('namespace journal identity differs');
292
+ }
293
+ if (value.result !== null)
294
+ validateResult(value.result, ctx);
295
+ if (ctx.intent.schemaVersion !== 1) {
296
+ if (value.phase === 'settled')
297
+ validateArtifactSummaries(value.repositoryArtifacts, ctx);
298
+ else if (value.repositoryArtifacts !== null)
299
+ fail('repository journal artifact phase differs');
300
+ }
301
+ if (ctx.contract.schemaVersion === 3) {
302
+ if (value.userNamespace !== null) {
303
+ const witness = validateLinuxUserNamespaceWitness(value.userNamespace);
304
+ if (witness.bootId !== ctx.intent.bootId || canonical(witness.mapping) !== canonical(ctx.contract.userNamespace)
305
+ || !value.keeperIdentity || !value.namespace)
306
+ fail('journal namespace witness differs');
307
+ }
308
+ if (value.candidateIdentity !== null) {
309
+ if (!value.userNamespace || !value.workerIdentity)
310
+ fail('candidate witness lacks namespace admission');
311
+ validateLinuxRepositoryCandidateWitness(value.candidateIdentity, value.userNamespace);
312
+ }
313
+ if (['prepared', 'creating-worker', 'creating-keeper', 'recorded', 'keeper-admitted', 'prestart-quiesced'].includes(value.phase)
314
+ && (value.userNamespace || value.candidateIdentity)
315
+ || ['user-namespace-ready', 'worker-admitted'].includes(value.phase) && value.candidateIdentity
316
+ || ['user-namespace-ready', 'worker-admitted', 'candidate-ready', 'running'].includes(value.phase) && !value.userNamespace
317
+ || ['candidate-ready', 'running'].includes(value.phase) && !value.candidateIdentity
318
+ || value.result && value.result.status === 'succeeded' && (!value.candidateIdentity || !value.userNamespace))
319
+ fail('journal candidate admission differs');
320
+ }
321
+ else if (['user-namespace-ready', 'candidate-ready'].includes(value.phase))
322
+ fail('legacy journal used a mapped admission phase');
323
+ if (value.keeper && !value.worker || value.phase === 'prepared' && (value.worker || value.keeper || value.namespace || value.result)
324
+ || ['recorded', 'keeper-admitted', 'user-namespace-ready', 'worker-admitted', 'candidate-ready', 'running', 'settled'].includes(value.phase) && (!value.worker || !value.keeper)
325
+ || value.phase === 'settled' && (!value.result || !value.result.quiescent))
326
+ fail('journal phase fields differ');
327
+ if (value.workerIdentity && (!value.worker || !value.keeperIdentity) || value.keeperIdentity && (!value.keeper || !value.namespace)
328
+ || value.namespace && !value.keeper
329
+ || ['prepared', 'creating-worker', 'creating-keeper', 'recorded'].includes(value.phase) && (value.workerIdentity || value.keeperIdentity || value.namespace || value.result)
330
+ || value.phase === 'creating-worker' && (value.worker || value.keeper)
331
+ || value.phase === 'creating-keeper' && (!value.worker || value.keeper)
332
+ || ['keeper-admitted', 'user-namespace-ready'].includes(value.phase) && (!value.keeperIdentity || value.workerIdentity || value.result)
333
+ || ['worker-admitted', 'candidate-ready', 'running'].includes(value.phase) && (!value.workerIdentity || value.result))
334
+ fail('journal phase fields differ');
335
+ if (value.phase === 'prestart-quiesced' && (value.worker && value.keeper || value.workerIdentity || value.keeperIdentity || value.namespace
336
+ || !value.result || !value.result.quiescent || value.result.status === 'succeeded'))
337
+ fail('partial cleanup journal differs');
338
+ return value;
339
+ }
340
+ function resources(journal, ctx) {
341
+ return journal.worker && journal.keeper ? validateLinuxExecutionResources({ schemaVersion: ctx.contract.schemaVersion === 3 ? 2 : 1,
342
+ intentDigest: linuxExecutionIntentDigest(ctx.intent), worker: journal.worker, keeper: journal.keeper,
343
+ ...(ctx.contract.schemaVersion === 3 ? { userNamespace: journal.userNamespace && journal.keeperIdentity
344
+ ? { witness: journal.userNamespace, keeperGate: journal.keeperIdentity } : null } : {}) }, ctx.intent, ctx.contract) : undefined;
345
+ }
346
+ async function creatorCanReconcile(ctx, journal) {
347
+ if (active.has(paths(ctx).journal))
348
+ return false;
349
+ const creator = await currentIdentity(journal.creator.pid);
350
+ if (!creator || canonical(creator) !== canonical(journal.creator))
351
+ return true;
352
+ return creator.pid === process.pid && canonical(await currentIdentity(process.pid)) === canonical(journal.creator)
353
+ && completedRuns.get(paths(ctx).journal) === drainWitness(ctx, journal);
354
+ }
355
+ function drainWitness(ctx, journal) {
356
+ return digest({ intentDigest: linuxExecutionIntentDigest(ctx.intent), creator: journal.creator });
357
+ }
358
+ async function readOptionalReceipt(ctx, journal, path) {
359
+ try {
360
+ return await readRecord(path);
361
+ }
362
+ catch (error) {
363
+ if (missing(error))
364
+ return undefined;
365
+ // The exclusive fresh-file reservation is the sole recoverable malformed
366
+ // record. Nonempty/corrupt, linked or writable receipts stay untouched.
367
+ if (await emptyPlaceholder(path) && await creatorCanReconcile(ctx, journal))
368
+ return undefined;
369
+ throw error;
370
+ }
371
+ }
372
+ function safePath(path) {
373
+ return path.length > 0 && path.length <= 4096 && !isAbsolute(path) && !/[\p{Cc}\\]/u.test(path)
374
+ && path.split('/').every(part => part !== '' && part !== '.' && part !== '..');
375
+ }
376
+ async function checkLive(ctx, signal) {
377
+ if (signal?.aborted || Date.now() >= ctx.intent.deadline)
378
+ fail('execution authority ended');
379
+ if (await boot() !== ctx.intent.bootId)
380
+ fail('boot changed');
381
+ await rootsIdentity(ctx.contract, ctx.intent);
382
+ if (signal?.aborted || Date.now() >= ctx.intent.deadline)
383
+ fail('execution authority ended');
384
+ }
385
+ async function boundedAdmission(operation, signal, deadline) {
386
+ if (signal.aborted || Date.now() >= deadline)
387
+ fail('execution authority ended');
388
+ let abort = () => { };
389
+ let timer;
390
+ const ended = new Promise((_resolve, reject) => {
391
+ abort = () => reject(new Error('linux execution: execution authority ended'));
392
+ timer = setTimeout(abort, Math.max(1, deadline - Date.now()));
393
+ signal.addEventListener('abort', abort, { once: true });
394
+ if (signal.aborted)
395
+ abort();
396
+ });
397
+ try {
398
+ return await Promise.race([Promise.resolve().then(operation), ended]);
399
+ }
400
+ finally {
401
+ clearTimeout(timer);
402
+ signal.removeEventListener('abort', abort);
403
+ }
404
+ }
405
+ export async function prepareLinuxExecutionIntent(input) {
406
+ if (input.contract.schemaVersion !== 1)
407
+ fail('repository authority requires the repository API');
408
+ return await prepareIntent(input);
409
+ }
410
+ async function prepareIntent(input, repository) {
411
+ const uid = platform(), contract = validateLinuxExecutionContract(input.contract);
412
+ if (contract.cgroupRoots.mode !== 'v1')
413
+ fail('cgroup v2 process placement is not yet verified');
414
+ if (input.job.deadline <= Date.now() || !positive(input.job.deadline) || contract.limits.workspaceMiB > (contract.schemaVersion !== 1 ? 8192 : 4096) || contract.limits.maxDurationMs > 1_800_000)
415
+ fail('invalid execution deadline or namespace limits');
416
+ const intent = validateLinuxExecutionIntent({ schemaVersion: contract.schemaVersion, ...(repository ? { repository } : {}), jobId: input.job.id, requestDigest: input.job.requestDigest,
417
+ nativeResourceId: input.job.nativeResourceId, nativeNonce: input.job.nativeNonce,
418
+ contractDigest: linuxExecutionContractDigest(contract), bootId: await boot(), uid, deadline: input.job.deadline,
419
+ namespacePath: join(input.stateRoot, 'namespaces', `namespace-${input.job.id}-${input.job.nativeNonce}`),
420
+ workerNonce: randomBytes(16).toString('hex'), keeperNonce: randomBytes(16).toString('hex'), roots: await rootsIdentity(contract) }, contract);
421
+ await setup(input.stateRoot);
422
+ if (contract.schemaVersion !== 1)
423
+ await childDirectory(input.stateRoot, 'artifacts');
424
+ const creator = await currentIdentity(process.pid);
425
+ if (!creator)
426
+ fail('Host identity unavailable');
427
+ const ctx = { stateRoot: input.stateRoot, contract, intent };
428
+ await checkLive(ctx);
429
+ if (await exists(intent.namespacePath) || await exists(paths(ctx).receipt) || await exists(paths(ctx).partial))
430
+ fail('execution identity already exists');
431
+ await writeRecord(paths(ctx).journal, { schemaVersion: intent.schemaVersion, ...(intent.schemaVersion !== 1 ? { repositoryArtifacts: null } : {}), intent, creator, phase: 'prepared', worker: null, keeper: null,
432
+ workerIdentity: null, keeperIdentity: null, namespace: null, result: null,
433
+ ...(intent.schemaVersion === 3 ? { userNamespace: null, candidateIdentity: null } : {}) }, true);
434
+ return intent;
435
+ }
436
+ async function absentGroups(ctx) {
437
+ for (const nonce of [ctx.intent.workerNonce, ctx.intent.keeperNonce])
438
+ for (const root of Object.values(ctx.intent.roots)) {
439
+ if (await exists(join(root.path, `job-${ctx.intent.jobId}-${nonce}`)))
440
+ return false;
441
+ }
442
+ return true;
443
+ }
444
+ async function removeUnrecorded(ctx, nonce) {
445
+ // An interrupted create may lack a snapshot. Its durable nonce authorizes only
446
+ // absence/empty-directory proof, never signaling an unidentified process.
447
+ for (const root of Object.values(ctx.intent.roots).reverse()) {
448
+ await rootsIdentity(ctx.contract, ctx.intent);
449
+ const path = join(root.path, `job-${ctx.intent.jobId}-${nonce}`);
450
+ if (!(await exists(path)))
451
+ continue;
452
+ const info = await lstat(path);
453
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== ctx.intent.uid || info.dev !== root.dev
454
+ || (info.mode & 0o022) !== 0 || await realpath(path) !== path || (await statfs(path)).type !== 0x27e0eb)
455
+ fail('unrecorded cgroup identity differs');
456
+ const entries = await readdir(path, { withFileTypes: true });
457
+ if (entries.some(entry => entry.isDirectory() || entry.isSymbolicLink()) || (await readFile(join(path, 'cgroup.procs'), 'utf8')).trim() !== '')
458
+ fail('unrecorded cgroup is not provably empty');
459
+ const after = await lstat(path);
460
+ if (info.dev !== after.dev || info.ino !== after.ino)
461
+ fail('unrecorded cgroup changed');
462
+ await rmdir(path);
463
+ }
464
+ }
465
+ class LinuxImplementationChangedError extends Error {
466
+ }
467
+ function checkImplementation(ctx) {
468
+ if (ctx.contract.schemaVersion !== 3)
469
+ return;
470
+ let matches = false;
471
+ try {
472
+ matches = linuxRepositoryImplementationDigest() === ctx.contract.implementationDigest;
473
+ }
474
+ catch { /* Unavailable packaged bytes cannot grant cleanup authority. */ }
475
+ if (!matches)
476
+ throw new LinuxImplementationChangedError('linux execution: repository identity implementation changed');
477
+ }
478
+ async function cleanLease(lease, ctx) {
479
+ // Recovery and a still-running Host use the same frozen implementation
480
+ // authority as dispatch. Changed helpers cannot inherit an old job's kill grant.
481
+ checkImplementation(ctx);
482
+ // A v1 pidfd helper has its own three-second subprocess cap. Leave room for
483
+ // that helper inside the namespace primitive's five-second stop callback.
484
+ if (!await lease.terminateAndProve(1_000) || !await lease.removeWhenEmpty())
485
+ fail('cgroup cleanup unproven');
486
+ }
487
+ async function cleanNamespace(ctx, journal) {
488
+ const path = ctx.intent.namespacePath;
489
+ if (!await exists(path))
490
+ return;
491
+ await privateDirectory(path);
492
+ const info = await lstat(path);
493
+ if (journal.namespace && (info.dev !== journal.namespace.dev || info.ino !== journal.namespace.ino))
494
+ fail('namespace directory changed');
495
+ // After every cgroup has gone, private mounts are gone too. Only the fixed
496
+ // keeper's small staging skeleton may remain on the Host filesystem.
497
+ let count = 0, bytes = 0;
498
+ const walk = async (directory, relative) => {
499
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
500
+ const name = relative ? `${relative}/${entry.name}` : entry.name;
501
+ if (++count > 80)
502
+ fail('namespace staging inventory exceeds bounds');
503
+ const target = join(directory, entry.name), item = await lstat(target);
504
+ if (item.isSymbolicLink() || item.uid !== ctx.intent.uid || (item.mode & 0o022) !== 0 || item.dev !== info.dev)
505
+ fail('namespace staging identity differs');
506
+ if (item.isDirectory()) {
507
+ if (!/^(?:workspace|tmp|shm|input|identity|immutable(?:\/(?:usr|lib|lib64|runtime(?:\/(?:[0-9]|1[0-5]))?))?)$/u.test(name))
508
+ fail('unexpected namespace directory');
509
+ await walk(target, name);
510
+ }
511
+ else {
512
+ if (!item.isFile() || item.nlink !== 1 || !/^(?:seccomp\.bpf|identity\/(?:passwd|group|nsswitch\.conf|hosts))$/u.test(name))
513
+ fail('unexpected namespace file');
514
+ bytes += item.size;
515
+ if (bytes > 70_000)
516
+ fail('namespace staging files exceed bounds');
517
+ }
518
+ }
519
+ };
520
+ await walk(path, '');
521
+ await rm(path, { recursive: true });
522
+ await syncDirectory(dirname(path));
523
+ if (await exists(path))
524
+ fail('namespace cleanup unproven');
525
+ }
526
+ function receiptValue(ctx, journal, value) {
527
+ keys(value, ['schemaVersion', 'intent', 'resources', 'result', 'completedAt', 'receiptDigest', ...(ctx.intent.schemaVersion !== 1 ? ['repositoryArtifacts'] : [])]);
528
+ if (value.schemaVersion !== ctx.intent.schemaVersion || canonical(value.intent) !== canonical(ctx.intent) || !positive(value.completedAt)
529
+ || value.completedAt < 1 || value.completedAt > Date.now() + 1000 || typeof value.receiptDigest !== 'string')
530
+ fail('receipt binding differs');
531
+ const complete = validateLinuxExecutionResources(value.resources, ctx.intent, ctx.contract);
532
+ validateResult(value.result, ctx);
533
+ if (ctx.intent.schemaVersion !== 1) {
534
+ validateArtifactSummaries(value.repositoryArtifacts, ctx);
535
+ if ((value.result.status !== 'succeeded' && value.repositoryArtifacts.length !== 0)
536
+ || value.result.status === 'succeeded' && canonical(value.repositoryArtifacts.map(item => item.path)) !== canonical(ctx.intent.repository.artifacts))
537
+ fail('receipt artifact outcome differs');
538
+ if (canonical(value.repositoryArtifacts) !== canonical(journal.repositoryArtifacts))
539
+ fail('receipt artifact summaries differ');
540
+ }
541
+ const { receiptDigest, ...body } = value;
542
+ if (!value.result.quiescent || journal.phase !== 'settled' || canonical(value.result) !== canonical(journal.result)
543
+ || canonical(complete) !== canonical(resources(journal, ctx)) || receiptDigest !== digest(body))
544
+ fail('receipt content differs');
545
+ return value;
546
+ }
547
+ export function linuxExecutionReceiptBinding(receipt) {
548
+ const { receiptDigest, ...body } = receipt;
549
+ if (receiptDigest !== digest(body))
550
+ fail('receipt digest differs');
551
+ return { requestDigest: receipt.intent.requestDigest, nativeNonce: receipt.intent.nativeNonce,
552
+ nativeResourceId: receipt.intent.nativeResourceId, intentDigest: linuxExecutionIntentDigest(receipt.intent),
553
+ resourcesDigest: linuxExecutionResourcesDigest(receipt.resources), receiptDigest };
554
+ }
555
+ /** A private receipt alone never proves cleanup: recheck roots, boot and actual absence. */
556
+ export async function readLinuxExecutionReceipt(input) {
557
+ const ctx = await context(input), journal = await readJournal(ctx);
558
+ const raw = await readOptionalReceipt(ctx, journal, paths(ctx).receipt);
559
+ if (raw === undefined)
560
+ return undefined;
561
+ const receipt = receiptValue(ctx, journal, raw);
562
+ if (!await absentGroups(ctx) || await exists(ctx.intent.namespacePath))
563
+ fail('receipt resources still present');
564
+ return receipt;
565
+ }
566
+ export async function readLinuxExecutionPartialCleanupProof(input) {
567
+ const ctx = await context(input), journal = await readJournal(ctx);
568
+ const raw = await readOptionalReceipt(ctx, journal, paths(ctx).partial);
569
+ if (raw === undefined)
570
+ return undefined;
571
+ const proof = validateLinuxExecutionPartialCleanupProof(raw);
572
+ if (journal.phase !== 'prestart-quiesced' || resources(journal, ctx) || proof.jobId !== ctx.intent.jobId
573
+ || proof.requestDigest !== ctx.intent.requestDigest || proof.nativeResourceId !== ctx.intent.nativeResourceId
574
+ || proof.nativeNonce !== ctx.intent.nativeNonce || proof.intentDigest !== linuxExecutionIntentDigest(ctx.intent)
575
+ || proof.observedAt > Date.now() + 1000 || !await absentGroups(ctx) || await exists(ctx.intent.namespacePath))
576
+ fail('partial cleanup proof differs');
577
+ return proof;
578
+ }
579
+ async function persistPartialProof(ctx, journal, result, repairIncomplete = false) {
580
+ if (!result.quiescent || resources(journal, ctx) || journal.workerIdentity || journal.keeperIdentity || journal.namespace
581
+ || !await absentGroups(ctx) || await exists(ctx.intent.namespacePath))
582
+ fail('cannot prove prestart cleanup');
583
+ journal.phase = 'prestart-quiesced';
584
+ journal.result = result;
585
+ await writeRecord(paths(ctx).journal, journal);
586
+ const body = { schemaVersion: 1, jobId: ctx.intent.jobId, requestDigest: ctx.intent.requestDigest,
587
+ nativeResourceId: ctx.intent.nativeResourceId, nativeNonce: ctx.intent.nativeNonce, intentDigest: linuxExecutionIntentDigest(ctx.intent),
588
+ observedAt: Date.now(), workerAbsent: true, keeperAbsent: true, namespaceAbsent: true };
589
+ const proof = validateLinuxExecutionPartialCleanupProof({ ...body, proofDigest: linuxExecutionPartialCleanupProofDigest(body) });
590
+ if (repairIncomplete)
591
+ await repairPlaceholder(paths(ctx).partial);
592
+ await writeRecord(paths(ctx).partial, proof, true);
593
+ return proof;
594
+ }
595
+ async function persistReceipt(ctx, journal, complete, result, repairIncomplete = false) {
596
+ if (!result.quiescent || !await absentGroups(ctx) || await exists(ctx.intent.namespacePath))
597
+ fail('cannot receipt unproven cleanup');
598
+ if (canonical(complete) !== canonical(resources(journal, ctx))
599
+ || ctx.contract.schemaVersion === 3 && result.status === 'succeeded' && (!journal.userNamespace || !journal.candidateIdentity))
600
+ fail('cannot receipt missing admission');
601
+ if (ctx.intent.schemaVersion !== 1 && journal.repositoryArtifacts == null)
602
+ journal.repositoryArtifacts = [];
603
+ journal.phase = 'settled';
604
+ journal.result = result;
605
+ await writeRecord(paths(ctx).journal, journal);
606
+ const body = { schemaVersion: ctx.intent.schemaVersion, intent: ctx.intent, resources: complete, result, completedAt: Date.now(),
607
+ ...(ctx.intent.schemaVersion !== 1 ? { repositoryArtifacts: journal.repositoryArtifacts } : {}) };
608
+ const receipt = { ...body, receiptDigest: digest(body) };
609
+ if (repairIncomplete)
610
+ await repairPlaceholder(paths(ctx).receipt);
611
+ await writeRecord(paths(ctx).receipt, receipt, true);
612
+ return receipt;
613
+ }
614
+ /** Host-only result retention. The synchronous callback must persist the ledger
615
+ * marker before either private evidence copy is replaced. A mixed pair after a
616
+ * crash is independently verifiable against that marker and can be retried. */
617
+ export async function retainLinuxExecutionResult(input) {
618
+ const ctx = await context(input), locations = paths(ctx);
619
+ if (ctx.contract.schemaVersion !== 1)
620
+ fail('repository receipts require repository retention');
621
+ const complete = validateLinuxExecutionResources(input.resources, ctx.intent, ctx.contract);
622
+ const intentDigest = linuxExecutionIntentDigest(ctx.intent), resourcesDigest = linuxExecutionResourcesDigest(complete);
623
+ if (!input.fenced() || active.has(locations.journal) || input.result.jobId !== ctx.intent.jobId || !input.result.quiescent
624
+ || !['succeeded', 'failed', 'cancelled', 'timed-out'].includes(input.result.status))
625
+ fail('retention authority differs');
626
+ if (!await absentGroups(ctx) || await exists(ctx.intent.namespacePath))
627
+ fail('retention resources still present');
628
+ const originals = await Promise.all([readRecord(locations.journal), readRecord(locations.receipt)]);
629
+ if (originals[0].kind !== 'result-pruned'
630
+ && originals[1].kind !== 'result-pruned') {
631
+ receiptValue(ctx, await readJournal(ctx), originals[1]);
632
+ }
633
+ const resultMatches = (value, expected) => {
634
+ validateResult(value, ctx);
635
+ const matched = expected.retention ? pruneResult(value, expected.retention.prunedAt) : value;
636
+ if (!matched || canonical(matched) !== canonical(expected))
637
+ fail('retention result differs');
638
+ };
639
+ let receiptDigest;
640
+ for (let index = 0; index < originals.length; index++) {
641
+ const value = originals[index];
642
+ if (value.kind === 'result-pruned') {
643
+ keys(value, ['schemaVersion', 'kind', 'record', 'intentDigest', 'resourcesDigest', 'receiptDigest', 'originalRecordDigest', 'result', 'tombstoneDigest']);
644
+ const { tombstoneDigest, ...body } = value;
645
+ if (value.schemaVersion !== 1 || value.record !== (index === 0 ? 'journal' : 'receipt')
646
+ || value.intentDigest !== intentDigest || value.resourcesDigest !== resourcesDigest
647
+ || !input.result.retention || !validateRetention(input.result.retention)
648
+ || canonical(value.result) !== canonical(input.result) || tombstoneDigest !== digest(body)
649
+ || typeof value.receiptDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.receiptDigest)
650
+ || typeof value.originalRecordDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.originalRecordDigest))
651
+ fail('retention tombstone differs');
652
+ }
653
+ else if (index === 0) {
654
+ const journal = await readJournal(ctx);
655
+ if (digest(journal) !== digest(value) || journal.phase !== 'settled'
656
+ || canonical(resources(journal, ctx)) !== canonical(complete))
657
+ fail('retention journal differs');
658
+ resultMatches(journal.result, input.result);
659
+ }
660
+ else {
661
+ keys(value, ['schemaVersion', 'intent', 'resources', 'result', 'completedAt', 'receiptDigest']);
662
+ const { receiptDigest: recordedDigest, ...body } = value;
663
+ if (value.schemaVersion !== 1 || canonical(value.intent) !== canonical(ctx.intent)
664
+ || canonical(validateLinuxExecutionResources(value.resources, ctx.intent, ctx.contract)) !== canonical(complete)
665
+ || !positive(value.completedAt) || value.completedAt > Date.now() + 1000 || recordedDigest !== digest(body))
666
+ fail('retention receipt differs');
667
+ resultMatches(value.result, input.result);
668
+ }
669
+ if (index === 1 || value.kind === 'result-pruned') {
670
+ if (receiptDigest && receiptDigest !== value.receiptDigest)
671
+ fail('retention receipt identity differs');
672
+ receiptDigest = value.receiptDigest;
673
+ }
674
+ }
675
+ if (!input.fenced())
676
+ fail('retention authority ended');
677
+ const retained = input.compact();
678
+ if (!retained?.retention || !validateRetention(retained.retention)
679
+ || canonical(retained) !== canonical(input.result.retention ? input.result : pruneResult(input.result, retained.retention.prunedAt)))
680
+ fail('retention marker not persisted');
681
+ let removedBytes = 0;
682
+ for (let index = 0; index < originals.length; index++) {
683
+ const original = originals[index];
684
+ if (original.kind === 'result-pruned')
685
+ continue;
686
+ const path = index === 0 ? locations.journal : locations.receipt;
687
+ const body = { schemaVersion: 1, kind: 'result-pruned', record: index === 0 ? 'journal' : 'receipt',
688
+ intentDigest, resourcesDigest, receiptDigest, originalRecordDigest: digest(original), result: retained };
689
+ const tombstone = { ...body, tombstoneDigest: digest(body) };
690
+ const temporary = `${path}.${randomBytes(16).toString('hex')}.tmp`;
691
+ try {
692
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
693
+ try {
694
+ await handle.writeFile(JSON.stringify(tombstone));
695
+ await handle.sync();
696
+ }
697
+ finally {
698
+ await handle.close();
699
+ }
700
+ // Recheck the exact file and controller/version immediately before rename.
701
+ if (digest(await readRecord(path)) !== digest(original) || !input.fenced())
702
+ fail('retention evidence changed');
703
+ await rename(temporary, path);
704
+ await syncDirectory(dirname(path));
705
+ removedBytes += Math.max(0, Buffer.byteLength(JSON.stringify(original)) - Buffer.byteLength(JSON.stringify(tombstone)));
706
+ }
707
+ finally {
708
+ await unlink(temporary).catch(error => { if (!missing(error))
709
+ throw error; });
710
+ }
711
+ }
712
+ return removedBytes;
713
+ }
714
+ export async function runLinuxExecution(input) {
715
+ if (input.contract.schemaVersion !== 1)
716
+ fail('repository authority requires the repository API');
717
+ return await runExecution(input);
718
+ }
719
+ async function runExecution(input, archive) {
720
+ const ctx = await context(input), key = paths(ctx).journal;
721
+ if (completedRuns.has(key))
722
+ fail('execution is single-use; drained runs are recovery-only');
723
+ if (active.has(key))
724
+ fail('execution is already owned');
725
+ active.add(key);
726
+ let ownedJournal, drained = false, finalized = false;
727
+ try {
728
+ const journal = await readJournal(ctx), creator = await currentIdentity(process.pid);
729
+ if (journal.phase !== 'prepared' || canonical(creator) !== canonical(journal.creator))
730
+ fail('execution is single-use and bound to its creating Host');
731
+ ownedJournal = journal;
732
+ let worker, keeper, complete;
733
+ const executionDeadline = Math.min(ctx.intent.deadline, Date.now() + ctx.contract.limits.maxDurationMs);
734
+ const admissionAbort = new AbortController();
735
+ const admissionSignal = AbortSignal.any([input.signal, admissionAbort.signal]);
736
+ const admissions = new Set();
737
+ let result = { jobId: ctx.intent.jobId, status: 'unknown', quiescent: false, stdout: '', stderr: '', artifacts: [] };
738
+ let binaryArtifacts = [];
739
+ let cleanup;
740
+ const stopGroups = () => {
741
+ admissionAbort.abort();
742
+ return cleanup ??= (async () => {
743
+ const outcomes = await Promise.allSettled([worker ? cleanLease(worker, ctx) : Promise.resolve(), keeper ? cleanLease(keeper, ctx) : Promise.resolve()]);
744
+ const errors = outcomes.filter((item) => item.status === 'rejected');
745
+ if (errors.length)
746
+ throw new AggregateError(errors.map(item => item.reason), 'cgroup cleanup unproven');
747
+ })();
748
+ };
749
+ try {
750
+ const args = structuredClone({ argv: input.argv, files: input.files, artifacts: input.artifacts ?? [], readOnlyRoots: input.readOnlyRoots ?? [] });
751
+ if (typeof input.recordResources !== 'function' || typeof input.authorizeStart !== 'function')
752
+ fail('missing Host admission callbacks');
753
+ if (!Array.isArray(args.argv) || args.argv.length < 1 || args.argv.length > 64
754
+ || !args.argv.every((arg, index) => typeof arg === 'string' && arg.length <= 65_536 && !arg.includes('\0')
755
+ && (index !== 0 || isAbsolute(arg) && resolve(arg) === arg && !/[\r\n]/u.test(arg))))
756
+ fail('invalid command');
757
+ if (!Array.isArray(args.files) || args.files.length > ctx.contract.limits.maxFiles
758
+ || args.files.some(file => !file || typeof file.path !== 'string' || !safePath(file.path) || typeof file.content !== 'string' || !file.content.isWellFormed())
759
+ || args.files.reduce((sum, file) => sum + Buffer.byteLength(file.content), 0) > ctx.contract.limits.maxInputBytes
760
+ || !Array.isArray(args.artifacts) || args.artifacts.length > ctx.contract.limits.maxFiles
761
+ || args.artifacts.some(path => typeof path !== 'string' || !safePath(path))
762
+ || new Set(args.artifacts).size !== args.artifacts.length)
763
+ fail('input exceeds contract bounds');
764
+ const names = args.files.map(file => file.path);
765
+ if (new Set(names).size !== names.length || names.some(name => names.some(other => other !== name && other.startsWith(`${name}/`))))
766
+ fail('input paths collide');
767
+ if (!Array.isArray(args.readOnlyRoots) || args.readOnlyRoots.length > 16)
768
+ fail('invalid runtime roots');
769
+ for (const root of args.readOnlyRoots) {
770
+ if (typeof root !== 'string' || !isAbsolute(root) || root === '/' || resolve(root) !== root || /[\p{Cc}\\]/u.test(root)
771
+ || await realpath(root) !== root)
772
+ fail('unsafe runtime root');
773
+ if (root === ctx.stateRoot || root.startsWith(ctx.stateRoot + sep) || ctx.stateRoot.startsWith(root + sep))
774
+ fail('runtime root overlaps private state');
775
+ const info = await lstat(root);
776
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== ctx.intent.uid || (info.mode & 0o022) !== 0)
777
+ fail('unsafe runtime root');
778
+ }
779
+ if (ctx.contract.schemaVersion === 3) {
780
+ checkImplementation(ctx);
781
+ const system = ctx.contract.systemSnapshot.root.path;
782
+ if (system === ctx.stateRoot || system.startsWith(ctx.stateRoot + sep) || ctx.stateRoot.startsWith(system + sep))
783
+ fail('system snapshot overlaps private state');
784
+ await verifyLinuxUserNamespaceMapping(ctx.contract.userNamespace);
785
+ await verifyLinuxSystemSnapshot(ctx.contract.systemSnapshot);
786
+ }
787
+ await checkLive(ctx, input.signal);
788
+ journal.phase = 'creating-worker';
789
+ await writeRecord(key, journal);
790
+ await checkLive(ctx, input.signal);
791
+ worker = await LinuxCgroupLease.create({ roots: ctx.contract.cgroupRoots, jobId: ctx.intent.jobId, nonce: ctx.intent.workerNonce,
792
+ limits: { memoryMiB: ctx.contract.limits.memoryMiB, pidsLimit: ctx.contract.limits.pidsLimit, cpus: ctx.contract.limits.cpus }, deadline: ctx.intent.deadline });
793
+ journal.worker = worker.snapshot;
794
+ journal.phase = 'creating-keeper';
795
+ await writeRecord(key, journal);
796
+ await checkLive(ctx, input.signal);
797
+ keeper = await LinuxCgroupLease.create({ roots: ctx.contract.cgroupRoots, jobId: ctx.intent.jobId, nonce: ctx.intent.keeperNonce,
798
+ limits: ctx.contract.keeperLimits, deadline: ctx.intent.deadline });
799
+ journal.keeper = keeper.snapshot;
800
+ journal.phase = 'recorded';
801
+ await writeRecord(key, journal);
802
+ complete = resources(journal, ctx);
803
+ // Version 3 publishes its immutable resource binding only after the Host
804
+ // observes the kernel namespace anchor. The private journal already owns
805
+ // both groups; pre-anchor failures can retain a negative null-anchor receipt.
806
+ if (ctx.contract.schemaVersion !== 3)
807
+ await boundedAdmission(() => input.recordResources(complete), input.signal, executionDeadline);
808
+ await checkLive(ctx, input.signal);
809
+ if (Date.now() >= executionDeadline)
810
+ fail('execution authority ended');
811
+ const admit = async (kind, candidate) => {
812
+ await checkLive(ctx, admissionSignal);
813
+ const expected = kind === 'keeper' ? 'recorded' : ctx.contract.schemaVersion === 3 ? 'user-namespace-ready' : 'keeper-admitted';
814
+ if ((await readJournal(ctx)).phase !== expected || cleanup)
815
+ fail('admission phase closed');
816
+ const identity = await currentIdentity(candidate.pid);
817
+ if (!identity || identity.startTicks !== String(candidate.startTicks) || identity.bootId !== ctx.intent.bootId || identity.uid !== ctx.intent.uid)
818
+ fail('candidate gate identity differs');
819
+ if (kind === 'keeper') {
820
+ const info = await lstat(ctx.intent.namespacePath);
821
+ await privateDirectory(ctx.intent.namespacePath);
822
+ journal.namespace = { path: ctx.intent.namespacePath, dev: info.dev, ino: info.ino };
823
+ }
824
+ if (cleanup)
825
+ fail('admission phase closed');
826
+ journal[`${kind}Identity`] = identity;
827
+ journal.phase = kind === 'keeper' ? 'keeper-admitted' : 'worker-admitted';
828
+ await writeRecord(key, journal);
829
+ await checkLive(ctx, admissionSignal);
830
+ if (cleanup)
831
+ fail('admission phase closed');
832
+ await (kind === 'keeper' ? keeper : worker).admit(identity);
833
+ await checkLive(ctx, admissionSignal);
834
+ if (kind === 'worker' && ctx.contract.schemaVersion !== 3) {
835
+ const authorized = await boundedAdmission(async () => await input.authorizeStart(), admissionSignal, executionDeadline);
836
+ if (authorized !== true)
837
+ fail('Host start authorization withdrawn');
838
+ await checkLive(ctx, admissionSignal);
839
+ if (cleanup || (await readJournal(ctx)).phase !== 'worker-admitted')
840
+ fail('start phase closed');
841
+ journal.phase = 'running';
842
+ await writeRecord(key, journal);
843
+ await checkLive(ctx, admissionSignal);
844
+ }
845
+ };
846
+ const trackedAdmit = (kind, identity) => {
847
+ const pending = admit(kind, identity);
848
+ admissions.add(pending);
849
+ void pending.then(() => admissions.delete(pending), () => admissions.delete(pending));
850
+ return pending;
851
+ };
852
+ const track = (operation) => {
853
+ const pending = operation();
854
+ admissions.add(pending);
855
+ void pending.then(() => admissions.delete(pending), () => admissions.delete(pending));
856
+ return pending;
857
+ };
858
+ const mapped = ctx.contract.schemaVersion === 3 ? {
859
+ userNamespace: ctx.contract.userNamespace, systemSnapshot: ctx.contract.systemSnapshot,
860
+ onUserNamespaceReady: (identity) => track(async () => {
861
+ await checkLive(ctx, admissionSignal);
862
+ if (ctx.contract.schemaVersion !== 3 || cleanup || (await readJournal(ctx)).phase !== 'keeper-admitted'
863
+ || !journal.keeperIdentity)
864
+ fail('namespace admission phase closed');
865
+ const witness = await inspectLinuxUserNamespace(ctx.contract.userNamespace, identity);
866
+ const observed = await keeper.observe();
867
+ if (Object.values(observed.processes).some(pids => !pids.includes(identity.pid)))
868
+ fail('keeper namespace escaped resource groups');
869
+ await checkLive(ctx, admissionSignal);
870
+ if (cleanup)
871
+ fail('namespace admission phase closed');
872
+ worker.bindUserNamespace({ witness });
873
+ keeper.bindUserNamespace({ witness, outsideGate: journal.keeperIdentity });
874
+ journal.userNamespace = witness;
875
+ journal.phase = 'user-namespace-ready';
876
+ await writeRecord(key, journal);
877
+ complete = resources(journal, ctx);
878
+ await boundedAdmission(() => input.recordResources(complete), admissionSignal, executionDeadline);
879
+ await checkLive(ctx, admissionSignal);
880
+ if (cleanup || (await readJournal(ctx)).phase !== 'user-namespace-ready')
881
+ fail('namespace admission phase closed');
882
+ }),
883
+ beforeCandidateExec: (identity) => track(async () => {
884
+ await checkLive(ctx, admissionSignal);
885
+ if (cleanup || (await readJournal(ctx)).phase !== 'worker-admitted' || !journal.userNamespace)
886
+ fail('candidate admission phase closed');
887
+ const witness = await inspectLinuxRepositoryCandidate(journal.userNamespace, identity);
888
+ const observed = await worker.observe();
889
+ if (Object.values(observed.processes).some(pids => !pids.includes(identity.pid)))
890
+ fail('candidate escaped resource groups');
891
+ await checkLive(ctx, admissionSignal);
892
+ if (cleanup)
893
+ fail('candidate admission phase closed');
894
+ journal.candidateIdentity = witness;
895
+ journal.phase = 'candidate-ready';
896
+ await writeRecord(key, journal);
897
+ const authorized = await boundedAdmission(async () => await input.authorizeStart(), admissionSignal, executionDeadline);
898
+ if (authorized !== true)
899
+ fail('Host start authorization withdrawn');
900
+ await checkLive(ctx, admissionSignal);
901
+ if (cleanup || (await readJournal(ctx)).phase !== 'candidate-ready')
902
+ fail('candidate admission phase closed');
903
+ journal.phase = 'running';
904
+ await writeRecord(key, journal);
905
+ await checkLive(ctx, admissionSignal);
906
+ }),
907
+ } : {};
908
+ const namespaceInput = { stateRoot: join(ctx.stateRoot, 'namespaces'), namespaceId: `${ctx.intent.jobId}-${ctx.intent.nativeNonce}`,
909
+ argv: args.argv, artifacts: args.artifacts, readOnlyRoots: args.readOnlyRoots,
910
+ workspaceBytes: ctx.contract.limits.workspaceMiB * 1024 * 1024, workspaceInodes: ctx.contract.limits.workspaceInodes,
911
+ tmpBytes: (ctx.contract.schemaVersion !== 1 ? ctx.contract.limits.tmpMiB : Math.max(1, Math.min(64, ctx.contract.limits.memoryMiB / 4))) * 1024 * 1024,
912
+ tmpInodes: ctx.contract.schemaVersion !== 1 ? ctx.contract.limits.tmpInodes : ctx.contract.limits.workspaceInodes,
913
+ timeoutMs: Math.max(1, executionDeadline - Date.now()),
914
+ maxOutputBytes: ctx.contract.limits.maxOutputBytes, maxArtifactBytes: ctx.contract.limits.maxArtifactBytes,
915
+ signal: input.signal, beforeKeeperStart: (identity) => trackedAdmit('keeper', identity),
916
+ beforeStart: (identity) => trackedAdmit('worker', identity), afterExit: async () => { await cleanLease(worker, ctx); }, onStop: stopGroups };
917
+ const raw = archive ? await runLinuxRepositoryNamespace({ ...namespaceInput, ...mapped, archive })
918
+ : await runLinuxNamespace({ ...namespaceInput, files: args.files, profile: ctx.contract.profile });
919
+ if (archive)
920
+ binaryArtifacts = raw.artifacts.map(item => ({ ...item, bytes: Buffer.from(item.bytes) }));
921
+ result = { jobId: ctx.intent.jobId, status: raw.status, quiescent: false, stdout: raw.stdout, stderr: raw.stderr,
922
+ artifacts: archive ? [] : raw.artifacts, ...(raw.exitCode === undefined ? {} : { exitCode: raw.exitCode }), ...(raw.reason ? { reason: raw.reason.slice(0, 8192) } : {}) };
923
+ }
924
+ catch (error) {
925
+ result.reason = (error instanceof Error ? error.message : 'execution failed').slice(0, 8192);
926
+ result.status = input.signal.aborted ? 'cancelled' : Date.now() >= executionDeadline ? 'timed-out' : 'unknown';
927
+ }
928
+ try {
929
+ await stopGroups();
930
+ await rootsIdentity(ctx.contract, ctx.intent);
931
+ if (!journal.worker)
932
+ await removeUnrecorded(ctx, ctx.intent.workerNonce);
933
+ if (!journal.keeper)
934
+ await removeUnrecorded(ctx, ctx.intent.keeperNonce);
935
+ if (!await absentGroups(ctx))
936
+ fail('cgroup absence unproven');
937
+ await cleanNamespace(ctx, journal);
938
+ result.quiescent = true;
939
+ }
940
+ catch (error) {
941
+ result = { ...result, status: 'unknown', quiescent: false, artifacts: [],
942
+ reason: `${result.reason ?? ''}; cleanup unproven: ${error instanceof Error ? error.message : 'unknown'}`.slice(0, 8192) };
943
+ }
944
+ // No internal create/admit continuation may survive the terminal journal.
945
+ // boundedAdmission detaches only the Host callback, never a gate operation.
946
+ await Promise.allSettled(admissions);
947
+ drained = true;
948
+ // A failed namespace journal sync can occur after its atomic rename. Build
949
+ // the terminal resource binding from the owned journal, never the earlier
950
+ // pre-anchor snapshot. No null-anchor binding can authorize candidate START.
951
+ complete = resources(journal, ctx);
952
+ validateResult(result, ctx);
953
+ let receipt;
954
+ let partialProof;
955
+ if (result.quiescent && complete) {
956
+ if (ctx.intent.schemaVersion !== 1) {
957
+ if (result.status === 'succeeded' && canonical(binaryArtifacts.map(item => item.path)) !== canonical(ctx.intent.repository.artifacts))
958
+ fail('repository artifacts differ from declared paths');
959
+ journal.repositoryArtifacts = await writeArtifactSpool(ctx, result.status === 'succeeded' ? binaryArtifacts : []);
960
+ }
961
+ receipt = await persistReceipt(ctx, journal, complete, result);
962
+ }
963
+ else if (result.quiescent)
964
+ partialProof = await persistPartialProof(ctx, journal, result);
965
+ else {
966
+ journal.phase = 'unknown';
967
+ journal.result = result;
968
+ await writeRecord(key, journal);
969
+ }
970
+ finalized = !!receipt || !!partialProof;
971
+ return { result, ...(complete && (complete.schemaVersion !== 2 || complete.userNamespace || result.quiescent) ? { resources: complete } : {}),
972
+ ...(receipt ? { receipt } : {}), ...(partialProof ? { partialProof } : {}) };
973
+ }
974
+ finally {
975
+ if (finalized)
976
+ completedRuns.delete(key);
977
+ else if (ownedJournal && drained && (completedRuns.has(key) || completedRuns.size < MAX_COMPLETED_RUNS))
978
+ completedRuns.set(key, drainWitness(ctx, ownedJournal));
979
+ active.delete(key);
980
+ }
981
+ }
982
+ /** Recovery never dispatches. A still-live creator may resume a pending create,
983
+ * irrespective of controller lease expiry, so it cannot be reclaimed here. */
984
+ export async function reconcileLinuxExecution(input) {
985
+ const ctx = await context(input), journal = await readJournal(ctx), complete = resources(journal, ctx);
986
+ // A null anchor is only a negative, quiescent outcome. Publishing it while a
987
+ // creator can still establish its anchor would freeze the wrong ledger binding.
988
+ const admitted = complete && (complete.schemaVersion !== 2 || complete.userNamespace) ? complete : undefined;
989
+ const receipt = await readLinuxExecutionReceipt(ctx);
990
+ if (receipt) {
991
+ completedRuns.delete(paths(ctx).journal);
992
+ return { quiescent: true, resources: receipt.resources, receipt };
993
+ }
994
+ const partialProof = await readLinuxExecutionPartialCleanupProof(ctx);
995
+ if (partialProof) {
996
+ completedRuns.delete(paths(ctx).journal);
997
+ return { quiescent: true, partialProof };
998
+ }
999
+ // Keep the original journal, witness and uncertain body untouched when a
1000
+ // replacement implementation cannot exercise this job's cleanup authority.
1001
+ try {
1002
+ checkImplementation(ctx);
1003
+ }
1004
+ catch {
1005
+ return { quiescent: false, ...(admitted ? { resources: admitted } : {}) };
1006
+ }
1007
+ const key = paths(ctx).journal;
1008
+ if (!await creatorCanReconcile(ctx, journal))
1009
+ return { quiescent: false, ...(admitted ? { resources: admitted } : {}) };
1010
+ // creatorCanReconcile awaits /proc. Claim again synchronously after that
1011
+ // await, so two same-Host recoveries cannot both enter cleanup/persistence.
1012
+ if (active.has(key))
1013
+ return { quiescent: false, ...(admitted ? { resources: admitted } : {}) };
1014
+ active.add(key);
1015
+ let finalizing = false;
1016
+ try {
1017
+ const outcomes = await Promise.allSettled(['worker', 'keeper'].map(async (kind) => {
1018
+ const snapshot = journal[kind];
1019
+ if (snapshot)
1020
+ await cleanLease(await LinuxCgroupLease.recover(snapshot, journal.userNamespace ? {
1021
+ witness: journal.userNamespace, ...(kind === 'keeper' && journal.keeperIdentity ? { outsideGate: journal.keeperIdentity } : {}),
1022
+ } : undefined), ctx);
1023
+ else
1024
+ await removeUnrecorded(ctx, kind === 'worker' ? ctx.intent.workerNonce : ctx.intent.keeperNonce);
1025
+ }));
1026
+ const errors = outcomes.filter((item) => item.status === 'rejected');
1027
+ if (errors.length)
1028
+ throw new AggregateError(errors.map(item => item.reason), errors.map(item => item.reason instanceof Error ? item.reason.message : 'unknown cleanup failure').join('; '));
1029
+ await rootsIdentity(ctx.contract, ctx.intent);
1030
+ if (!await absentGroups(ctx))
1031
+ fail('recovered resource absence unproven');
1032
+ await cleanNamespace(ctx, journal);
1033
+ finalizing = true;
1034
+ const result = { jobId: ctx.intent.jobId, status: 'unknown', quiescent: true, stdout: '', stderr: '', artifacts: [], reason: 'Host execution interrupted; task outcome remains unknown' };
1035
+ if (!complete) {
1036
+ const proof = await persistPartialProof(ctx, journal, result, true);
1037
+ completedRuns.delete(key);
1038
+ return { quiescent: true, partialProof: proof };
1039
+ }
1040
+ if (ctx.intent.schemaVersion !== 1) {
1041
+ await removeArtifactSpool(ctx);
1042
+ journal.repositoryArtifacts = [];
1043
+ }
1044
+ const recovered = await persistReceipt(ctx, journal, complete, result, true);
1045
+ completedRuns.delete(key);
1046
+ return { quiescent: true, resources: complete, receipt: recovered };
1047
+ }
1048
+ catch (error) {
1049
+ if (error instanceof LinuxImplementationChangedError || error instanceof AggregateError
1050
+ && error.errors.some(item => item instanceof LinuxImplementationChangedError))
1051
+ return { quiescent: false, ...(admitted ? { resources: admitted } : {}) };
1052
+ // A terminal rename may already be visible even when its directory fsync
1053
+ // failed. Preserve that journal/receipt pair for strict validation on the
1054
+ // next attempt; overwriting either would destroy an otherwise valid proof.
1055
+ if (finalizing)
1056
+ return { quiescent: false, ...(admitted ? { resources: admitted } : {}) };
1057
+ journal.phase = 'unknown';
1058
+ journal.result = { jobId: ctx.intent.jobId, status: 'unknown', quiescent: false, stdout: '', stderr: '', artifacts: [],
1059
+ reason: `recovery cleanup unproven: ${error instanceof Error ? error.message : 'unknown failure'}`.slice(0, 8192) };
1060
+ await writeRecord(key, journal);
1061
+ return { quiescent: false, ...(admitted ? { resources: admitted } : {}) };
1062
+ }
1063
+ finally {
1064
+ active.delete(key);
1065
+ }
1066
+ }
1067
+ function validateArtifactSummaries(value, ctx) {
1068
+ if (ctx.intent.schemaVersion === 1 || !Array.isArray(value) || value.length > ctx.contract.limits.maxFiles)
1069
+ fail('invalid repository summaries');
1070
+ let total = 0;
1071
+ const names = new Set();
1072
+ for (const item of value) {
1073
+ keys(item, ['path', 'bytes', 'sha256']);
1074
+ if (typeof item.path !== 'string' || !ctx.intent.repository.artifacts.includes(item.path) || names.has(item.path)
1075
+ || !Number.isSafeInteger(item.bytes) || item.bytes < 0
1076
+ || typeof item.sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(item.sha256))
1077
+ fail('repository artifact binding differs');
1078
+ total += item.bytes;
1079
+ names.add(item.path);
1080
+ }
1081
+ if (total > ctx.contract.limits.maxArtifactBytes)
1082
+ fail('repository artifacts exceed bounds');
1083
+ }
1084
+ function spoolPath(ctx) { return join(ctx.stateRoot, 'artifacts', `${ctx.intent.jobId}-${ctx.intent.nativeNonce}`); }
1085
+ async function writeArtifactSpool(ctx, artifacts) {
1086
+ const summaries = artifacts.map(item => ({ path: item.path, bytes: item.bytes.length, sha256: item.sha256 }));
1087
+ validateArtifactSummaries(summaries, ctx);
1088
+ for (const item of artifacts)
1089
+ if (!Buffer.isBuffer(item.bytes) || createHash('sha256').update(item.bytes).digest('hex') !== item.sha256)
1090
+ fail('repository artifact digest differs');
1091
+ if (!artifacts.length)
1092
+ return [];
1093
+ await privateDirectory(join(ctx.stateRoot, 'artifacts'));
1094
+ const directory = spoolPath(ctx);
1095
+ await mkdir(directory, { mode: 0o700 });
1096
+ await syncDirectory(dirname(directory));
1097
+ await privateDirectory(directory);
1098
+ for (let index = 0; index < artifacts.length; index++) {
1099
+ const target = join(directory, `${index}.bin`), temporary = `${target}.${randomBytes(16).toString('hex')}.tmp`;
1100
+ try {
1101
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
1102
+ try {
1103
+ await handle.writeFile(artifacts[index].bytes);
1104
+ await handle.sync();
1105
+ }
1106
+ finally {
1107
+ await handle.close();
1108
+ }
1109
+ if (await exists(target))
1110
+ fail('repository spool identity already exists');
1111
+ await rename(temporary, target);
1112
+ await syncDirectory(directory);
1113
+ }
1114
+ finally {
1115
+ await unlink(temporary).catch(error => { if (!missing(error))
1116
+ throw error; });
1117
+ }
1118
+ }
1119
+ return summaries;
1120
+ }
1121
+ async function readArtifactSpool(ctx, summaries) {
1122
+ validateArtifactSummaries(summaries, ctx);
1123
+ if (!summaries.length)
1124
+ return [];
1125
+ const directory = spoolPath(ctx);
1126
+ await privateDirectory(join(ctx.stateRoot, 'artifacts'));
1127
+ await privateDirectory(directory);
1128
+ const entries = await readdir(directory);
1129
+ if (canonical(entries.sort()) !== canonical(summaries.map((_, index) => `${index}.bin`).sort()))
1130
+ fail('repository spool inventory differs');
1131
+ const result = [];
1132
+ for (let index = 0; index < summaries.length; index++) {
1133
+ const item = summaries[index], path = join(directory, `${index}.bin`);
1134
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1135
+ try {
1136
+ const info = await handle.stat(), named = await lstat(path);
1137
+ if (!info.isFile() || named.isSymbolicLink() || info.uid !== ctx.intent.uid || (info.mode & 0o7777) !== 0o600
1138
+ || info.nlink !== 1 || info.size !== item.bytes || named.dev !== info.dev || named.ino !== info.ino)
1139
+ fail('repository spool identity differs');
1140
+ const bytes = Buffer.alloc(item.bytes);
1141
+ let offset = 0;
1142
+ while (offset < bytes.length) {
1143
+ const part = await handle.read(bytes, offset, bytes.length - offset, offset);
1144
+ if (part.bytesRead === 0)
1145
+ fail('repository spool truncated');
1146
+ offset += part.bytesRead;
1147
+ }
1148
+ if ((await handle.read(Buffer.alloc(1), 0, 1, offset)).bytesRead !== 0)
1149
+ fail('repository spool grew');
1150
+ const after = await handle.stat();
1151
+ if (bytes.length !== item.bytes || after.size !== info.size || after.mtimeMs !== info.mtimeMs || after.ctimeMs !== info.ctimeMs
1152
+ || after.nlink !== 1 || createHash('sha256').update(bytes).digest('hex') !== item.sha256)
1153
+ fail('repository spool digest differs');
1154
+ result.push({ path: item.path, bytes, sha256: item.sha256 });
1155
+ }
1156
+ finally {
1157
+ await handle.close();
1158
+ }
1159
+ }
1160
+ return result;
1161
+ }
1162
+ /** Only recovery after proven cgroup/namespace absence can discard an orphan
1163
+ * spool. The inventory is bounded and never follows a caller-controlled link. */
1164
+ async function removeArtifactSpool(ctx) {
1165
+ await privateDirectory(join(ctx.stateRoot, 'artifacts'));
1166
+ const directory = spoolPath(ctx);
1167
+ if (!await exists(directory))
1168
+ return;
1169
+ await privateDirectory(directory);
1170
+ const entries = await readdir(directory);
1171
+ let total = 0;
1172
+ if (entries.length > 256)
1173
+ fail('orphan spool inventory exceeds bounds');
1174
+ for (const name of entries) {
1175
+ if (!/^(?:[0-9]|[1-9][0-9]|1[01][0-9]|12[0-7])\.bin(?:\.[a-f0-9]{32}\.tmp)?$/u.test(name))
1176
+ fail('orphan spool entry differs');
1177
+ const info = await lstat(join(directory, name));
1178
+ if (!info.isFile() || info.isSymbolicLink() || info.uid !== ctx.intent.uid || (info.mode & 0o7777) !== 0o600
1179
+ || info.nlink !== 1 || info.size > ctx.contract.limits.maxArtifactBytes)
1180
+ fail('orphan spool identity differs');
1181
+ total += info.size;
1182
+ if (total > 2 * ctx.contract.limits.maxArtifactBytes)
1183
+ fail('orphan spool exceeds bounds');
1184
+ }
1185
+ for (const name of entries)
1186
+ await unlink(join(directory, name));
1187
+ await rmdir(directory);
1188
+ await syncDirectory(dirname(directory));
1189
+ }
1190
+ function repositoryRequest(input, contract) {
1191
+ if (!input.archive || !(input.archive.bytes instanceof Uint8Array) || input.archive.bytes.byteLength < 1
1192
+ || input.archive.bytes.byteLength > contract.limits.maxInputBytes)
1193
+ fail('repository archive exceeds bounds');
1194
+ // Snapshot all caller-owned values before the first asynchronous operation.
1195
+ const request = { argv: [...input.argv], artifacts: [...input.artifacts], readOnlyRoots: [...(input.readOnlyRoots ?? [])],
1196
+ archive: { bytes: Buffer.from(input.archive.bytes), sha256: input.archive.sha256 } };
1197
+ if (!/^[a-f0-9]{64}$/u.test(request.archive.sha256)
1198
+ || createHash('sha256').update(request.archive.bytes).digest('hex') !== request.archive.sha256
1199
+ || request.argv.length < 1 || request.argv.length > 64
1200
+ || request.argv.some((arg, index) => typeof arg !== 'string' || arg.length > 65_536 || arg.includes('\0')
1201
+ || index === 0 && (!isAbsolute(arg) || resolve(arg) !== arg || /[\r\n]/u.test(arg)))
1202
+ || request.artifacts.length > contract.limits.maxFiles || request.artifacts.some(item => typeof item !== 'string' || !safePath(item))
1203
+ || new Set(request.artifacts).size !== request.artifacts.length || request.readOnlyRoots.length > 16
1204
+ || request.readOnlyRoots.some(root => typeof root !== 'string' || !isAbsolute(root) || root === '/' || resolve(root) !== root || /[\p{Cc}\\]/u.test(root)))
1205
+ fail('repository request differs');
1206
+ const archive = { bytes: request.archive.bytes.length, sha256: request.archive.sha256 };
1207
+ const binding = { archive, artifacts: request.artifacts,
1208
+ executionDigest: digest({ argv: request.argv, archive, artifacts: request.artifacts, readOnlyRoots: request.readOnlyRoots }) };
1209
+ return { request, binding };
1210
+ }
1211
+ export async function prepareLinuxRepositoryIntent(input) {
1212
+ const contract = validateLinuxExecutionContract(input.contract);
1213
+ if (contract.schemaVersion === 1)
1214
+ fail('repository contract required');
1215
+ const { binding } = repositoryRequest(input, contract);
1216
+ return await prepareIntent({ ...input, contract }, binding);
1217
+ }
1218
+ /** Binary bytes are returned only after successful receipt validation and fresh
1219
+ * kernel absence checks. Reopening a completed identity only reads its receipt. */
1220
+ export async function readLinuxRepositoryReceipt(input) {
1221
+ if (![2, 3].includes(input.contract.schemaVersion) || ![2, 3].includes(input.intent.schemaVersion))
1222
+ fail('repository context required');
1223
+ const ctx = await context(input), receipt = await readLinuxExecutionReceipt(ctx);
1224
+ if (!receipt)
1225
+ return undefined;
1226
+ if (receipt.schemaVersion === 1)
1227
+ fail('repository receipt required');
1228
+ if (receipt.result.status !== 'succeeded' && receipt.repositoryArtifacts.length)
1229
+ fail('unsuccessful receipt contains artifacts');
1230
+ const artifacts = receipt.result.status === 'succeeded' ? await readArtifactSpool(ctx, receipt.repositoryArtifacts) : [];
1231
+ const verified = await readLinuxExecutionReceipt(ctx);
1232
+ if (verified?.receiptDigest !== receipt.receiptDigest)
1233
+ fail('repository receipt changed');
1234
+ return { receipt, artifacts };
1235
+ }
1236
+ export async function runLinuxRepositoryExecution(input) {
1237
+ const contract = validateLinuxExecutionContract(input.contract);
1238
+ if (contract.schemaVersion === 1 || ![2, 3].includes(input.intent.schemaVersion))
1239
+ fail('repository context required');
1240
+ const { request, binding } = repositoryRequest(input, contract);
1241
+ if (canonical(binding) !== canonical(input.intent.repository))
1242
+ fail('repository request binding differs');
1243
+ const ctx = { stateRoot: input.stateRoot, contract, intent: input.intent };
1244
+ const existing = await readLinuxRepositoryReceipt(ctx);
1245
+ if (existing)
1246
+ return { result: existing.receipt.result, resources: existing.receipt.resources, ...existing };
1247
+ const outcome = await runExecution({ ...input, ...ctx, ...request, files: [] }, request.archive);
1248
+ const settled = outcome.receipt ? await readLinuxRepositoryReceipt(ctx) : undefined;
1249
+ const { receipt, ...rest } = outcome;
1250
+ if (receipt && receipt.schemaVersion === 1)
1251
+ fail('repository receipt required');
1252
+ return { ...rest, ...(receipt ? { receipt } : {}), artifacts: settled?.artifacts ?? [] };
1253
+ }
1254
+ export async function reconcileLinuxRepositoryExecution(input) {
1255
+ if (![2, 3].includes(input.contract.schemaVersion) || ![2, 3].includes(input.intent.schemaVersion))
1256
+ fail('repository context required');
1257
+ const outcome = await reconcileLinuxExecution(input);
1258
+ const settled = outcome.receipt ? await readLinuxRepositoryReceipt(input) : undefined;
1259
+ const { receipt, ...rest } = outcome;
1260
+ if (receipt && receipt.schemaVersion === 1)
1261
+ fail('repository receipt required');
1262
+ return { ...rest, ...(receipt ? { receipt } : {}), artifacts: settled?.artifacts ?? [] };
1263
+ }
1264
+ //# sourceMappingURL=linux-execution.js.map