@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
package/lib/ledger.js CHANGED
@@ -1,10 +1,12 @@
1
+ import { markLinuxLedgerAdmissionRefusal } from './linux-repository-admission.js';
1
2
  import { validateGoalArtifactAdmission } from '@dsh-enhanced/task-acceptance-contract';
2
3
  import { chmodSync, mkdirSync } from 'node:fs';
3
- import { randomUUID } from 'node:crypto';
4
+ import { randomBytes, randomUUID } from 'node:crypto';
4
5
  import { dirname, isAbsolute, normalize } from 'node:path';
5
6
  import { DatabaseSync } from 'node:sqlite';
6
7
  import { processExited, sameDaemonWitness, validateCreationWitness } from './runtime-witness.js';
7
8
  import { receiptData } from './cleanup-receipt.js';
9
+ import { linuxExecutionContractDigest, linuxExecutionIntentDigest, linuxExecutionResourcesDigest, linuxJobResourceReservation, validateLinuxExecutionContract, validateLinuxExecutionIntent, validateLinuxExecutionResources, validateLinuxReceiptBinding, validateLinuxExecutionPartialCleanupProof } from './linux-execution-contract.js';
8
10
  import { pruneResult, validateRetention } from './storage-policy.js';
9
11
  export class IsolationLedgerError extends Error {
10
12
  code;
@@ -14,7 +16,7 @@ export class IsolationLedgerError extends Error {
14
16
  this.name = 'IsolationLedgerError';
15
17
  }
16
18
  }
17
- const schemaVersion = 7;
19
+ const schemaVersion = 9;
18
20
  const outputMaximum = 1_048_576;
19
21
  const artifactMaximum = 128;
20
22
  const textMaximum = 16_384;
@@ -50,7 +52,11 @@ function artifactBinding(value) {
50
52
  fail('invalid-input', 'invalid artifact binding');
51
53
  return frozen({ admission: validateGoalArtifactAdmission(value.admission), paths: [...value.paths] });
52
54
  }
53
- function grantDigest(value) { return JSON.stringify(value); }
55
+ /** Canonical freeze-order JSON of the ten-key grant document used as the grant
56
+ * equality proof. Despite the name it is a fingerprint, not a cryptographic
57
+ * hash: it is compared byte-for-byte, and the authorized backend handover
58
+ * rewrites the verifier grant with the exact same byte formula. */
59
+ export function grantDigest(value) { return JSON.stringify(value); }
54
60
  function result(value, allowRetention = false) {
55
61
  if (!value || typeof value !== 'object' || !text(value.jobId) || !['succeeded', 'failed', 'cancelled', 'timed-out', 'unknown'].includes(value.status)
56
62
  || typeof value.quiescent !== 'boolean' || typeof value.stdout !== 'string' || typeof value.stderr !== 'string'
@@ -121,11 +127,27 @@ function open(path) {
121
127
  CREATE INDEX isolation_jobs_grant ON isolation_jobs(grant_id);
122
128
  CREATE INDEX isolation_jobs_recoverable ON isolation_jobs(status, updated_at);
123
129
  UPDATE schema_meta SET value='7' WHERE key='schema-version'; PRAGMA user_version=7; COMMIT;`);
130
+ if ([0, 1, 2, 3, 4, 5, 6, 7].includes(version))
131
+ database.exec(`BEGIN IMMEDIATE;
132
+ CREATE TABLE isolation_jobs_v8 (id TEXT PRIMARY KEY, grant_id TEXT NOT NULL, grant_revision INTEGER NOT NULL, principal_digest TEXT NOT NULL, principal_record_id TEXT NOT NULL, principal_version INTEGER NOT NULL, workspace TEXT NOT NULL, agent_preset TEXT NOT NULL, session_id TEXT NOT NULL, idempotency_key TEXT NOT NULL, request_digest TEXT NOT NULL, container_name TEXT UNIQUE, backend_kind TEXT NOT NULL CHECK(backend_kind IN ('docker','macos-seatbelt','linux-native')), native_resource_id TEXT UNIQUE, native_nonce TEXT, deadline INTEGER NOT NULL, reserved_duration_ms INTEGER NOT NULL, reserved_memory_mib INTEGER NOT NULL DEFAULT 0, reserved_workspace_inodes INTEGER NOT NULL DEFAULT 0, reserved_storage_bytes INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL CHECK(status IN ('prepared','running','succeeded','failed','cancelled','timed-out','unknown')), version INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, result_json TEXT, dispatch_attempted INTEGER NOT NULL DEFAULT 0 CHECK(dispatch_attempted IN (0,1)), creation_witness_json TEXT, artifact_binding_json TEXT, linux_contract_json TEXT, linux_intent_json TEXT, linux_resources_json TEXT, CHECK((backend_kind='docker' AND container_name IS NOT NULL AND native_resource_id IS NULL AND native_nonce IS NULL) OR (backend_kind='macos-seatbelt' AND container_name IS NULL AND native_resource_id IS NOT NULL AND native_nonce IS NOT NULL AND reserved_memory_mib=0 AND reserved_workspace_inodes=0 AND linux_contract_json IS NULL AND linux_intent_json IS NULL AND linux_resources_json IS NULL) OR (backend_kind='linux-native' AND container_name IS NULL AND native_resource_id IS NOT NULL AND native_nonce IS NOT NULL AND reserved_memory_mib>0 AND reserved_workspace_inodes>0 AND reserved_storage_bytes>0 AND linux_contract_json IS NOT NULL)), CHECK(backend_kind='linux-native' OR (linux_contract_json IS NULL AND linux_intent_json IS NULL AND linux_resources_json IS NULL)), UNIQUE(principal_digest, principal_record_id, principal_version, workspace, agent_preset, session_id, grant_id, idempotency_key)) STRICT;
133
+ INSERT INTO isolation_jobs_v8 (id,grant_id,grant_revision,principal_digest,principal_record_id,principal_version,workspace,agent_preset,session_id,idempotency_key,request_digest,container_name,backend_kind,native_resource_id,native_nonce,deadline,reserved_duration_ms,reserved_memory_mib,reserved_workspace_inodes,reserved_storage_bytes,status,version,created_at,updated_at,result_json,dispatch_attempted,creation_witness_json,artifact_binding_json) SELECT id,grant_id,grant_revision,principal_digest,principal_record_id,principal_version,workspace,agent_preset,session_id,idempotency_key,request_digest,container_name,backend_kind,native_resource_id,native_nonce,deadline,reserved_duration_ms,reserved_memory_mib,reserved_workspace_inodes,reserved_storage_bytes,status,version,created_at,updated_at,result_json,dispatch_attempted,creation_witness_json,artifact_binding_json FROM isolation_jobs;
134
+ DROP TABLE isolation_jobs; ALTER TABLE isolation_jobs_v8 RENAME TO isolation_jobs;
135
+ CREATE INDEX isolation_jobs_grant ON isolation_jobs(grant_id);
136
+ CREATE INDEX isolation_jobs_recoverable ON isolation_jobs(status, updated_at);
137
+ UPDATE schema_meta SET value='8' WHERE key='schema-version'; PRAGMA user_version=8; COMMIT;`);
138
+ if ([0, 1, 2, 3, 4, 5, 6, 7, 8].includes(version))
139
+ database.exec(`BEGIN IMMEDIATE;
140
+ CREATE TABLE isolation_jobs_v9 (id TEXT PRIMARY KEY, grant_id TEXT NOT NULL, grant_revision INTEGER NOT NULL, principal_digest TEXT NOT NULL, principal_record_id TEXT NOT NULL, principal_version INTEGER NOT NULL, workspace TEXT NOT NULL, agent_preset TEXT NOT NULL, session_id TEXT NOT NULL, idempotency_key TEXT NOT NULL, request_digest TEXT NOT NULL, container_name TEXT UNIQUE, backend_kind TEXT NOT NULL CHECK(backend_kind IN ('docker','macos-seatbelt','linux-native')), native_resource_id TEXT UNIQUE, native_nonce TEXT, deadline INTEGER NOT NULL, reserved_duration_ms INTEGER NOT NULL, reserved_memory_mib INTEGER NOT NULL DEFAULT 0, reserved_workspace_inodes INTEGER NOT NULL DEFAULT 0, reserved_storage_bytes INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL CHECK(status IN ('prepared','running','succeeded','failed','cancelled','timed-out','unknown')), version INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, result_json TEXT, dispatch_attempted INTEGER NOT NULL DEFAULT 0 CHECK(dispatch_attempted IN (0,1)), creation_witness_json TEXT, artifact_binding_json TEXT, linux_contract_json TEXT, linux_intent_json TEXT, linux_resources_json TEXT, migrated INTEGER NOT NULL DEFAULT 0 CHECK(migrated IN (0,1)), CHECK((backend_kind='docker' AND container_name IS NOT NULL AND native_resource_id IS NULL AND native_nonce IS NULL) OR (backend_kind='macos-seatbelt' AND container_name IS NULL AND native_resource_id IS NOT NULL AND native_nonce IS NOT NULL AND reserved_memory_mib=0 AND reserved_workspace_inodes=0 AND linux_contract_json IS NULL AND linux_intent_json IS NULL AND linux_resources_json IS NULL) OR (backend_kind='linux-native' AND container_name IS NULL AND native_resource_id IS NOT NULL AND native_nonce IS NOT NULL AND reserved_memory_mib>0 AND reserved_workspace_inodes>0 AND reserved_storage_bytes>0 AND linux_contract_json IS NOT NULL)), CHECK(backend_kind='linux-native' OR (linux_contract_json IS NULL AND linux_intent_json IS NULL AND linux_resources_json IS NULL)), UNIQUE(principal_digest, principal_record_id, principal_version, workspace, agent_preset, session_id, grant_id, idempotency_key)) STRICT;
141
+ INSERT INTO isolation_jobs_v9 (id,grant_id,grant_revision,principal_digest,principal_record_id,principal_version,workspace,agent_preset,session_id,idempotency_key,request_digest,container_name,backend_kind,native_resource_id,native_nonce,deadline,reserved_duration_ms,reserved_memory_mib,reserved_workspace_inodes,reserved_storage_bytes,status,version,created_at,updated_at,result_json,dispatch_attempted,creation_witness_json,artifact_binding_json,linux_contract_json,linux_intent_json,linux_resources_json) SELECT id,grant_id,grant_revision,principal_digest,principal_record_id,principal_version,workspace,agent_preset,session_id,idempotency_key,request_digest,container_name,backend_kind,native_resource_id,native_nonce,deadline,reserved_duration_ms,reserved_memory_mib,reserved_workspace_inodes,reserved_storage_bytes,status,version,created_at,updated_at,result_json,dispatch_attempted,creation_witness_json,artifact_binding_json,linux_contract_json,linux_intent_json,linux_resources_json FROM isolation_jobs;
142
+ DROP TABLE isolation_jobs; ALTER TABLE isolation_jobs_v9 RENAME TO isolation_jobs;
143
+ CREATE INDEX isolation_jobs_grant ON isolation_jobs(grant_id);
144
+ CREATE INDEX isolation_jobs_recoverable ON isolation_jobs(status, updated_at);
145
+ UPDATE schema_meta SET value='9' WHERE key='schema-version'; PRAGMA user_version=9; COMMIT;`);
124
146
  database.exec("CREATE INDEX IF NOT EXISTS isolation_artifact_contract ON isolation_jobs(json_extract(artifact_binding_json, '$.admission.contractId')) WHERE artifact_binding_json IS NOT NULL");
125
- if (![0, 1, 2, 3, 4, 5, 6, schemaVersion].includes(version))
147
+ if (![0, 1, 2, 3, 4, 5, 6, 7, 8, schemaVersion].includes(version))
126
148
  fail('schema');
127
149
  const marker = database.prepare("SELECT value FROM schema_meta WHERE key='schema-version'").get();
128
- if (marker?.value !== '7' || database.prepare('PRAGMA user_version').get().user_version !== 7)
150
+ if (marker?.value !== '9' || database.prepare('PRAGMA user_version').get().user_version !== 9)
129
151
  fail('schema');
130
152
  if (path !== ':memory:')
131
153
  chmodSync(path, 0o600);
@@ -141,19 +163,29 @@ export class IsolationLedger {
141
163
  #database;
142
164
  #now;
143
165
  constructor(path, { now = Date.now } = {}) { this.#database = open(path); this.#now = now; }
144
- #transaction(operation) { this.#database.exec('BEGIN IMMEDIATE'); try {
145
- const value = operation();
146
- this.#database.exec('COMMIT');
147
- return value;
148
- }
149
- catch (error) {
166
+ #transaction(operation, rolledBack) {
167
+ this.#database.exec('BEGIN IMMEDIATE');
150
168
  try {
151
- this.#database.exec('ROLLBACK');
169
+ const value = operation();
170
+ this.#database.exec('COMMIT');
171
+ return value;
152
172
  }
153
- catch { }
154
- ;
155
- throw error;
156
- } }
173
+ catch (error) {
174
+ let confirmed = false;
175
+ try {
176
+ this.#database.exec('ROLLBACK');
177
+ confirmed = true;
178
+ }
179
+ catch { /* Preserve the original failure. */ }
180
+ if (confirmed) {
181
+ try {
182
+ rolledBack?.(error);
183
+ }
184
+ catch { /* Evidence must never change the original API error. */ }
185
+ }
186
+ throw error;
187
+ }
188
+ }
157
189
  #nowValue() { const value = this.#now(); if (!safeTime(value))
158
190
  fail('invalid-input', 'clock returned invalid time'); return value; }
159
191
  #grant(id) { return this.#database.prepare('SELECT * FROM isolation_grants WHERE id = ?').get(id); }
@@ -178,9 +210,10 @@ export class IsolationLedger {
178
210
  if (!text(row.id) || !text(row.grant_id) || !safePositive(row.grant_revision) || !text(row.session_id) || !text(row.idempotency_key) || !text(row.request_digest) || !positiveTime(row.deadline) || !safePositive(row.reserved_duration_ms) || !safeTime(row.reserved_memory_mib) || !safeTime(row.reserved_workspace_inodes) || !safeTime(row.reserved_storage_bytes) || !safePositive(row.version) || !safeTime(row.created_at) || !safeTime(row.updated_at) || row.updated_at < row.created_at || !['prepared', 'running', 'succeeded', 'failed', 'cancelled', 'timed-out', 'unknown'].includes(row.status))
179
211
  fail('schema');
180
212
  if (!((row.backend_kind === 'docker' && text(row.container_name) && row.native_resource_id === null && row.native_nonce === null)
181
- || (row.backend_kind === 'macos-seatbelt' && row.container_name === null && text(row.native_resource_id) && text(row.native_nonce) && row.reserved_memory_mib === 0 && row.reserved_workspace_inodes === 0)))
213
+ || (row.backend_kind === 'macos-seatbelt' && row.container_name === null && text(row.native_resource_id) && text(row.native_nonce) && row.reserved_memory_mib === 0 && row.reserved_workspace_inodes === 0)
214
+ || (row.backend_kind === 'linux-native' && row.container_name === null && row.native_resource_id === `linux-${row.id}` && /^[a-f0-9]{32}$/u.test(row.native_nonce ?? '') && row.reserved_memory_mib > 0 && row.reserved_workspace_inodes > 0 && row.reserved_storage_bytes > 0)))
182
215
  fail('schema');
183
- if (![0, 1].includes(row.dispatch_attempted))
216
+ if (![0, 1].includes(row.dispatch_attempted) || ![0, 1].includes(row.migrated))
184
217
  fail('schema');
185
218
  let witness;
186
219
  if (row.creation_witness_json !== null) {
@@ -202,6 +235,41 @@ export class IsolationLedger {
202
235
  fail('schema');
203
236
  }
204
237
  }
238
+ let linuxContract;
239
+ let linuxIntent;
240
+ let linuxResources;
241
+ try {
242
+ if (row.backend_kind === 'linux-native') {
243
+ linuxContract = validateLinuxExecutionContract(JSON.parse(row.linux_contract_json));
244
+ if (!/^[a-f0-9]{64}$/u.test(row.request_digest) || row.reserved_duration_ms > linuxContract.limits.maxDurationMs
245
+ || row.reserved_memory_mib < linuxContract.limits.memoryMiB + linuxContract.limits.workspaceMiB + linuxContract.keeperLimits.memoryMiB
246
+ || row.reserved_workspace_inodes < linuxContract.limits.workspaceInodes)
247
+ fail('schema');
248
+ if (linuxContract.schemaVersion !== 1) {
249
+ const minimum = linuxJobResourceReservation(linuxContract);
250
+ if (row.reserved_memory_mib < minimum.memoryMiB || row.reserved_workspace_inodes < minimum.workspaceInodes
251
+ || row.reserved_storage_bytes < minimum.storageBytes)
252
+ fail('schema');
253
+ }
254
+ if (row.linux_intent_json !== null) {
255
+ linuxIntent = validateLinuxExecutionIntent(JSON.parse(row.linux_intent_json), linuxContract);
256
+ this.#bindLinuxIntent(row, linuxIntent, linuxContract);
257
+ }
258
+ if (row.linux_resources_json !== null) {
259
+ if (!linuxIntent || !row.dispatch_attempted)
260
+ fail('schema');
261
+ linuxResources = validateLinuxExecutionResources(JSON.parse(row.linux_resources_json), linuxIntent, linuxContract);
262
+ }
263
+ if (row.dispatch_attempted && !linuxIntent || row.status === 'running' && !linuxResources
264
+ || ['running', 'succeeded'].includes(row.status) && linuxResources?.schemaVersion === 2 && !linuxResources.userNamespace)
265
+ fail('schema');
266
+ }
267
+ else if (row.linux_contract_json !== null || row.linux_intent_json !== null || row.linux_resources_json !== null)
268
+ fail('schema');
269
+ }
270
+ catch {
271
+ fail('schema', 'invalid persisted Linux execution binding');
272
+ }
205
273
  let parsed;
206
274
  if (row.result_json !== null) {
207
275
  try {
@@ -216,7 +284,20 @@ export class IsolationLedger {
216
284
  }
217
285
  else if (row.status !== 'prepared' && row.status !== 'running')
218
286
  fail('schema');
219
- return frozen({ ...(binding ? { artifactBinding: binding } : {}), id: row.id, grantId: row.grant_id, grantRevision: row.grant_revision, identity: jobIdentity, sessionId: row.session_id, idempotencyKey: row.idempotency_key, requestDigest: row.request_digest, containerName: row.container_name ?? '', backend: row.backend_kind, ...(row.native_resource_id ? { nativeResourceId: row.native_resource_id } : {}), ...(row.native_nonce ? { nativeNonce: row.native_nonce } : {}), deadline: row.deadline, reservedDurationMs: row.reserved_duration_ms, reservedMemoryMiB: row.reserved_memory_mib, reservedWorkspaceInodes: row.reserved_workspace_inodes, reservedStorageBytes: row.reserved_storage_bytes, dispatchAttempted: row.dispatch_attempted === 1, ...(witness ? { creationWitness: witness } : {}), status: row.status, version: row.version, createdAt: row.created_at, updatedAt: row.updated_at, ...(parsed ? { result: parsed } : {}) });
287
+ return frozen({ ...(linuxContract ? { linuxContract } : {}), ...(linuxIntent ? { linuxIntent } : {}), ...(linuxResources ? { linuxResources } : {}), ...(binding ? { artifactBinding: binding } : {}), id: row.id, grantId: row.grant_id, grantRevision: row.grant_revision, identity: jobIdentity, sessionId: row.session_id, idempotencyKey: row.idempotency_key, requestDigest: row.request_digest, containerName: row.container_name ?? '', backend: row.backend_kind, ...(row.native_resource_id ? { nativeResourceId: row.native_resource_id } : {}), ...(row.native_nonce ? { nativeNonce: row.native_nonce } : {}), deadline: row.deadline, reservedDurationMs: row.reserved_duration_ms, reservedMemoryMiB: row.reserved_memory_mib, reservedWorkspaceInodes: row.reserved_workspace_inodes, reservedStorageBytes: row.reserved_storage_bytes, dispatchAttempted: row.dispatch_attempted === 1, ...(witness ? { creationWitness: witness } : {}), status: row.status, version: row.version, createdAt: row.created_at, updatedAt: row.updated_at, ...(parsed ? { result: parsed } : {}) });
288
+ }
289
+ #bindLinuxIntent(row, intent, contract) {
290
+ if (intent.jobId !== row.id || intent.requestDigest !== row.request_digest || intent.nativeResourceId !== row.native_resource_id
291
+ || intent.nativeNonce !== row.native_nonce || intent.deadline !== row.deadline || row.reserved_duration_ms > contract.limits.maxDurationMs
292
+ || row.reserved_memory_mib < contract.limits.memoryMiB + contract.limits.workspaceMiB + contract.keeperLimits.memoryMiB
293
+ || row.reserved_workspace_inodes < contract.limits.workspaceInodes)
294
+ fail('invalid-input', 'Linux intent differs from job reservation');
295
+ if (contract.schemaVersion !== 1) {
296
+ const minimum = linuxJobResourceReservation(contract);
297
+ if (row.reserved_memory_mib < minimum.memoryMiB || row.reserved_workspace_inodes < minimum.workspaceInodes
298
+ || row.reserved_storage_bytes < minimum.storageBytes)
299
+ fail('invalid-input', 'Linux repository intent exceeds job reservation');
300
+ }
220
301
  }
221
302
  syncGrants(grants, authority) {
222
303
  if (!Array.isArray(grants))
@@ -254,7 +335,29 @@ export class IsolationLedger {
254
335
  });
255
336
  }
256
337
  prepare(input) {
257
- if (!input || typeof input !== 'object' || ![6, 7, 8, 9, 10, 11, 12].includes(Object.keys(input).length) || !onlyKeys(input, ['identity', 'sessionId', 'grantId', 'idempotencyKey', 'requestDigest', 'durationMs', 'maxActiveJobs', 'authority', 'resourceReservation', 'storageBudget', 'artifactBinding', 'backend']) || !text(input.sessionId) || !text(input.grantId) || !text(input.idempotencyKey) || !text(input.requestDigest) || !safePositive(input.durationMs) || (input.maxActiveJobs !== undefined && !safePositive(input.maxActiveJobs)) || (input.backend !== undefined && input.backend !== 'docker' && input.backend !== 'macos-seatbelt') || (input.backend === 'macos-seatbelt' && input.resourceReservation !== undefined))
338
+ if (!input || typeof input !== 'object' || ![6, 7, 8, 9, 10, 11, 12, 13].includes(Object.keys(input).length) || !onlyKeys(input, ['identity', 'sessionId', 'grantId', 'idempotencyKey', 'requestDigest', 'durationMs', 'maxActiveJobs', 'authority', 'resourceReservation', 'storageBudget', 'artifactBinding', 'backend', 'linuxContract']) || !text(input.sessionId) || !text(input.grantId) || !text(input.idempotencyKey) || !text(input.requestDigest) || !safePositive(input.durationMs) || (input.maxActiveJobs !== undefined && !safePositive(input.maxActiveJobs)) || (input.backend !== undefined && input.backend !== 'docker' && input.backend !== 'macos-seatbelt' && input.backend !== 'linux-native') || (input.backend === 'macos-seatbelt' && input.resourceReservation !== undefined))
339
+ fail('invalid-input');
340
+ let linuxContract;
341
+ if (input.backend === 'linux-native') {
342
+ try {
343
+ linuxContract = validateLinuxExecutionContract(input.linuxContract);
344
+ }
345
+ catch {
346
+ fail('invalid-input', 'Linux contract required');
347
+ }
348
+ if (!/^[a-f0-9]{64}$/u.test(input.requestDigest) || !input.resourceReservation || !input.storageBudget
349
+ || input.durationMs > linuxContract.limits.maxDurationMs
350
+ || input.resourceReservation.memoryMiB < linuxContract.limits.memoryMiB + linuxContract.limits.workspaceMiB + linuxContract.keeperLimits.memoryMiB
351
+ || input.resourceReservation.workspaceInodes < linuxContract.limits.workspaceInodes)
352
+ fail('invalid-input', 'Linux resources must be reserved');
353
+ if (linuxContract.schemaVersion !== 1) {
354
+ const minimum = linuxJobResourceReservation(linuxContract);
355
+ if (input.resourceReservation.memoryMiB < minimum.memoryMiB || input.resourceReservation.workspaceInodes < minimum.workspaceInodes
356
+ || input.storageBudget.reservedBytes < minimum.storageBytes)
357
+ fail('invalid-input', 'Linux repository resources must be reserved');
358
+ }
359
+ }
360
+ else if (input.linuxContract !== undefined)
258
361
  fail('invalid-input');
259
362
  const binding = input.artifactBinding === undefined ? undefined : artifactBinding(input.artifactBinding);
260
363
  const reservation = input.resourceReservation === undefined ? undefined : input.resourceReservation;
@@ -265,66 +368,151 @@ export class IsolationLedger {
265
368
  fail('invalid-input', 'invalid storage budget');
266
369
  const who = identity(input.identity);
267
370
  const controller = this.#authority(input.authority);
371
+ if (input.backend === 'linux-native' && !controller)
372
+ fail('invalid-input', 'Linux writes require controller authority');
373
+ let refusal, insertAttempted = false;
374
+ function refuse(code, message) { refusal = new IsolationLedgerError(code, message); throw refusal; }
268
375
  return this.#transaction(() => {
269
376
  const now = this.#nowValue();
270
377
  this.#requireController(controller, now);
271
378
  const existing = this.#database.prepare('SELECT * FROM isolation_jobs WHERE principal_digest=? AND principal_record_id=? AND principal_version=? AND workspace=? AND agent_preset=? AND session_id=? AND grant_id=? AND idempotency_key=?').get(who.principalDigest, who.principalRecordId, who.principalVersion, who.workspace, who.agentPreset, input.sessionId, input.grantId, input.idempotencyKey);
272
379
  if (existing) {
273
380
  const job = this.#decodeJob(existing);
274
- if (job.requestDigest !== input.requestDigest || job.backend !== (input.backend ?? 'docker'))
381
+ if (job.requestDigest !== input.requestDigest || job.backend !== (input.backend ?? 'docker') || (linuxContract && linuxExecutionContractDigest(job.linuxContract) !== linuxExecutionContractDigest(linuxContract)))
275
382
  fail('conflict', 'idempotency key has a different request or backend');
276
383
  return { job, created: false };
277
384
  }
278
385
  const current = this.#grant(input.grantId);
279
386
  if (!current || current.revoked || current.expires_at <= now || current.principal_digest !== who.principalDigest || current.principal_record_id !== who.principalRecordId || current.principal_version !== who.principalVersion || current.workspace !== who.workspace || current.agent_preset !== who.agentPreset)
280
- fail('unauthorized');
387
+ refuse('unauthorized');
281
388
  if (input.maxActiveJobs !== undefined) {
282
- const active = this.#database.prepare("SELECT COUNT(*) AS count FROM isolation_jobs WHERE status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)").get();
283
- if (!safeTime(active.count) || active.count >= input.maxActiveJobs)
389
+ const active = this.#database.prepare("SELECT COUNT(*) AS count FROM isolation_jobs WHERE migrated=0 AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0))").get();
390
+ if (!safeTime(active.count))
284
391
  fail('unauthorized', 'active isolation job limit reached');
392
+ if (active.count >= input.maxActiveJobs)
393
+ refuse('unauthorized', 'active isolation job limit reached');
285
394
  }
286
395
  const usage = this.#database.prepare('SELECT COUNT(*) AS runs, COALESCE(SUM(reserved_duration_ms), 0) AS duration FROM isolation_jobs WHERE grant_id=?').get(input.grantId);
287
- if (!safeTime(usage.runs) || !safeTime(usage.duration) || usage.runs >= current.max_runs || input.durationMs > current.max_total_duration_ms || usage.duration > current.max_total_duration_ms - input.durationMs)
396
+ if (!safeTime(usage.runs) || !safeTime(usage.duration))
288
397
  fail('unauthorized', 'grant budget exhausted');
398
+ if (usage.runs >= current.max_runs || input.durationMs > current.max_total_duration_ms || usage.duration > current.max_total_duration_ms - input.durationMs)
399
+ refuse('unauthorized', 'grant budget exhausted');
289
400
  if (reservation) {
290
- const occupied = "status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)";
401
+ const occupied = "migrated=0 AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0))";
291
402
  const legacy = this.#database.prepare(`SELECT COUNT(*) AS count FROM isolation_jobs WHERE (${occupied}) AND (reserved_memory_mib = 0 OR reserved_workspace_inodes = 0)`).get();
292
- if (!safeTime(legacy.count) || legacy.count > 0)
403
+ if (!safeTime(legacy.count))
293
404
  fail('unauthorized', 'active legacy job has no resource reservation');
405
+ if (legacy.count > 0)
406
+ refuse('unauthorized', 'active legacy job has no resource reservation');
294
407
  const resources = this.#database.prepare(`SELECT COALESCE(SUM(reserved_memory_mib), 0) AS memory, COALESCE(SUM(reserved_workspace_inodes), 0) AS inodes FROM isolation_jobs WHERE ${occupied}`).get();
295
- if (!safeTime(resources.memory) || !safeTime(resources.inodes) || resources.memory > reservation.maxMemoryMiB - reservation.memoryMiB || resources.inodes > reservation.maxWorkspaceInodes - reservation.workspaceInodes)
408
+ if (!safeTime(resources.memory) || !safeTime(resources.inodes))
296
409
  fail('unauthorized', 'isolation resource pool exhausted');
410
+ if (resources.memory > reservation.maxMemoryMiB - reservation.memoryMiB || resources.inodes > reservation.maxWorkspaceInodes - reservation.workspaceInodes)
411
+ refuse('unauthorized', 'isolation resource pool exhausted');
297
412
  }
298
413
  if (storage) {
299
414
  const observation = storage.observation;
300
415
  if (!observation)
301
- fail('unauthorized', 'storage observation unavailable');
416
+ refuse('unauthorized', 'storage observation unavailable');
302
417
  if (observation.observedAt > now || now - observation.observedAt > 5_000)
303
- fail('invalid-input', 'storage observation is stale');
418
+ refuse('invalid-input', 'storage observation is stale');
304
419
  const records = this.#database.prepare('SELECT COUNT(*) AS count FROM isolation_jobs').get();
305
- if (!safeTime(records.count) || records.count >= storage.maxJobRecords)
420
+ if (!safeTime(records.count))
306
421
  fail('unauthorized', 'isolation job record limit reached');
307
- const occupied = "status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)";
422
+ if (records.count >= storage.maxJobRecords)
423
+ refuse('unauthorized', 'isolation job record limit reached');
424
+ const occupied = "migrated=0 AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0))";
308
425
  const legacy = this.#database.prepare(`SELECT COUNT(*) AS count FROM isolation_jobs WHERE (${occupied}) AND reserved_storage_bytes = 0`).get();
309
- if (!safeTime(legacy.count) || legacy.count > 0)
426
+ if (!safeTime(legacy.count))
310
427
  fail('unauthorized', 'active legacy job has no storage reservation');
428
+ if (legacy.count > 0)
429
+ refuse('unauthorized', 'active legacy job has no storage reservation');
311
430
  const reservations = this.#database.prepare(`SELECT COALESCE(SUM(reserved_storage_bytes), 0) AS bytes FROM isolation_jobs WHERE ${occupied}`).get();
312
431
  if (!safeTime(reservations.bytes))
313
432
  fail('schema');
314
433
  const reserved = reservations.bytes + storage.reservedBytes;
315
434
  // SQLite reusable pages cannot cover staging growth outside the database.
316
435
  // Keep the whole future allocation even when that double-counts reusable space.
317
- if (!safeTime(reserved) || observation.bytes > storage.maxStateBytes || reserved > storage.maxStateBytes - observation.bytes)
436
+ if (!safeTime(reserved))
318
437
  fail('unauthorized', 'isolation storage budget exhausted');
438
+ if (observation.bytes > storage.maxStateBytes || reserved > storage.maxStateBytes - observation.bytes)
439
+ refuse('unauthorized', 'isolation storage budget exhausted');
319
440
  }
320
441
  const id = randomUUID();
321
442
  const deadline = Math.min(now + input.durationMs, current.expires_at);
322
- const native = input.backend === 'macos-seatbelt';
323
- this.#database.prepare("INSERT INTO isolation_jobs(id,grant_id,grant_revision,principal_digest,principal_record_id,principal_version,workspace,agent_preset,session_id,idempotency_key,request_digest,container_name,backend_kind,native_resource_id,native_nonce,deadline,reserved_duration_ms,reserved_memory_mib,reserved_workspace_inodes,reserved_storage_bytes,dispatch_attempted,status,version,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0, 'prepared',1,?,?)").run(id, input.grantId, current.revision, who.principalDigest, who.principalRecordId, who.principalVersion, who.workspace, who.agentPreset, input.sessionId, input.idempotencyKey, input.requestDigest, native ? null : `dsh-isolation-${id}`, native ? 'macos-seatbelt' : 'docker', native ? `native-${id}` : null, native ? randomUUID() : null, deadline, input.durationMs, reservation?.memoryMiB ?? 0, reservation?.workspaceInodes ?? 0, storage?.reservedBytes ?? 0, now, now);
443
+ const native = input.backend === 'macos-seatbelt' || input.backend === 'linux-native';
444
+ insertAttempted = true;
445
+ this.#database.prepare("INSERT INTO isolation_jobs(id,grant_id,grant_revision,principal_digest,principal_record_id,principal_version,workspace,agent_preset,session_id,idempotency_key,request_digest,container_name,backend_kind,native_resource_id,native_nonce,deadline,reserved_duration_ms,reserved_memory_mib,reserved_workspace_inodes,reserved_storage_bytes,linux_contract_json,dispatch_attempted,status,version,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0, 'prepared',1,?,?)").run(id, input.grantId, current.revision, who.principalDigest, who.principalRecordId, who.principalVersion, who.workspace, who.agentPreset, input.sessionId, input.idempotencyKey, input.requestDigest, native ? null : `dsh-isolation-${id}`, input.backend ?? 'docker', native ? `${input.backend === 'linux-native' ? 'linux' : 'native'}-${id}` : null, input.backend === 'linux-native' ? randomBytes(16).toString('hex') : native ? randomUUID() : null, deadline, input.durationMs, reservation?.memoryMiB ?? 0, reservation?.workspaceInodes ?? 0, storage?.reservedBytes ?? 0, linuxContract ? JSON.stringify(linuxContract) : null, now, now);
324
446
  if (binding)
325
447
  this.#database.prepare('UPDATE isolation_jobs SET artifact_binding_json=? WHERE id=?').run(JSON.stringify(binding), id);
326
448
  this.#audit(now, 'job-prepared', id, input.grantId, `revision:${current.revision}${reservation ? `;reservation:memoryMiB:${reservation.memoryMiB},workspaceInodes:${reservation.workspaceInodes}` : ''}${storage ? `;storageBytes:${storage.reservedBytes}` : ''}`);
327
449
  return { job: this.#job(id), created: true };
450
+ }, error => {
451
+ if (input.backend === 'linux-native' && controller && !insertAttempted && error === refusal) {
452
+ markLinuxLedgerAdmissionRefusal(refusal, { ledger: this, key: input.idempotencyKey, requestDigest: input.requestDigest, authority: controller });
453
+ }
454
+ });
455
+ }
456
+ markLinuxCreateIntent(jobId, expectedVersion, intent, authority) {
457
+ if (!text(jobId) || !safePositive(expectedVersion))
458
+ fail('invalid-input');
459
+ const controller = this.#authority(authority);
460
+ if (!controller)
461
+ fail('invalid-input');
462
+ return this.#transaction(() => {
463
+ const now = this.#nowValue();
464
+ this.#requireController(controller, now);
465
+ const job = this.#job(jobId);
466
+ if (!job)
467
+ fail('not-found');
468
+ if (job.version !== expectedVersion)
469
+ fail('conflict');
470
+ if (job.backend !== 'linux-native' || job.status !== 'prepared' || job.dispatchAttempted || job.linuxIntent || !this.usable(jobId))
471
+ fail('invalid-state');
472
+ let checked;
473
+ try {
474
+ checked = validateLinuxExecutionIntent(intent, job.linuxContract);
475
+ }
476
+ catch {
477
+ fail('invalid-input');
478
+ }
479
+ const row = this.#database.prepare('SELECT * FROM isolation_jobs WHERE id=?').get(jobId);
480
+ this.#bindLinuxIntent(row, checked, job.linuxContract);
481
+ const changed = this.#database.prepare("UPDATE isolation_jobs SET linux_intent_json=?, version=version+1, updated_at=? WHERE id=? AND version=? AND status='prepared' AND dispatch_attempted=0 AND linux_intent_json IS NULL").run(JSON.stringify(checked), now, jobId, expectedVersion);
482
+ if (changed.changes !== 1)
483
+ fail('conflict');
484
+ this.#audit(now, 'linux-create-intent', jobId, job.grantId, linuxExecutionIntentDigest(checked));
485
+ return this.#job(jobId);
486
+ });
487
+ }
488
+ recordLinuxResources(jobId, expectedVersion, resources, authority) {
489
+ if (!text(jobId) || !safePositive(expectedVersion))
490
+ fail('invalid-input');
491
+ const controller = this.#authority(authority);
492
+ if (!controller)
493
+ fail('invalid-input');
494
+ return this.#transaction(() => {
495
+ const now = this.#nowValue();
496
+ this.#requireController(controller, now);
497
+ const job = this.#job(jobId);
498
+ if (!job)
499
+ fail('not-found');
500
+ if (job.version !== expectedVersion)
501
+ fail('conflict');
502
+ if (job.backend !== 'linux-native' || job.status !== 'prepared' || !job.dispatchAttempted || !job.linuxIntent || job.linuxResources)
503
+ fail('invalid-state');
504
+ let checked;
505
+ try {
506
+ checked = validateLinuxExecutionResources(resources, job.linuxIntent, job.linuxContract);
507
+ }
508
+ catch {
509
+ fail('invalid-input');
510
+ }
511
+ const changed = this.#database.prepare("UPDATE isolation_jobs SET linux_resources_json=?, version=version+1, updated_at=? WHERE id=? AND version=? AND status='prepared' AND dispatch_attempted=1 AND linux_resources_json IS NULL").run(JSON.stringify(checked), now, jobId, expectedVersion);
512
+ if (changed.changes !== 1)
513
+ fail('conflict');
514
+ this.#audit(now, 'linux-resources-created', jobId, job.grantId, linuxExecutionResourcesDigest(checked));
515
+ return this.#job(jobId);
328
516
  });
329
517
  }
330
518
  /** Commit the crash boundary before a supervisor can receive create authority. */
@@ -342,7 +530,7 @@ export class IsolationLedger {
342
530
  fail('not-found');
343
531
  if (job.version !== expectedVersion)
344
532
  fail('conflict');
345
- if (job.status !== 'prepared' || job.dispatchAttempted || !this.usable(jobId))
533
+ if (job.status !== 'prepared' || job.dispatchAttempted || !this.usable(jobId) || job.backend === 'linux-native' && !job.linuxIntent)
346
534
  fail('invalid-state');
347
535
  const changed = this.#database.prepare("UPDATE isolation_jobs SET dispatch_attempted=1, version=version+1, updated_at=? WHERE id=? AND version=? AND status='prepared' AND dispatch_attempted=0").run(now, jobId, expectedVersion);
348
536
  if (changed.changes !== 1)
@@ -363,8 +551,10 @@ export class IsolationLedger {
363
551
  fail('not-found');
364
552
  if (job.version !== expectedVersion)
365
553
  fail('conflict', 'job version differs');
366
- if (job.status !== 'prepared')
554
+ if (job.status !== 'prepared' || job.backend === 'linux-native' && (!controller || !job.dispatchAttempted || !job.linuxIntent || !job.linuxResources))
367
555
  fail('invalid-state');
556
+ if (job.linuxResources?.schemaVersion === 2 && !job.linuxResources.userNamespace)
557
+ fail('invalid-state', 'Linux namespace admission is missing');
368
558
  const current = this.#grant(job.grantId);
369
559
  if (!current || current.revoked || current.revision !== job.grantRevision || current.expires_at <= now || job.deadline <= now || current.principal_digest !== job.identity.principalDigest || current.principal_record_id !== job.identity.principalRecordId || current.principal_version !== job.identity.principalVersion || current.workspace !== job.identity.workspace || current.agent_preset !== job.identity.agentPreset)
370
560
  fail('unauthorized');
@@ -403,9 +593,11 @@ export class IsolationLedger {
403
593
  fail('not-found');
404
594
  if (job.version !== expectedVersion)
405
595
  fail('conflict', 'job version differs');
596
+ if (job.backend === 'linux-native' && !controller)
597
+ fail('invalid-input', 'Linux writes require controller authority');
406
598
  if (witness && !job.dispatchAttempted)
407
599
  fail('invalid-state');
408
- if (job.backend === 'macos-seatbelt' && job.dispatchAttempted && value.status !== 'unknown')
600
+ if (job.backend !== 'docker' && job.dispatchAttempted && value.status !== 'unknown')
409
601
  fail('invalid-state', 'native dispatch requires a durable receipt');
410
602
  if (job.dispatchAttempted && value.status === 'unknown' && value.quiescent)
411
603
  fail('invalid-state', 'dispatch requires independent quiescence evidence');
@@ -460,6 +652,141 @@ export class IsolationLedger {
460
652
  return this.#job(jobId);
461
653
  });
462
654
  }
655
+ #linuxReceiptJob(jobId, expectedVersion, input, controller) {
656
+ const now = this.#nowValue();
657
+ this.#requireController(controller, now);
658
+ let binding;
659
+ try {
660
+ binding = validateLinuxReceiptBinding(input);
661
+ }
662
+ catch {
663
+ fail('invalid-input');
664
+ }
665
+ const job = this.#job(jobId);
666
+ if (!job)
667
+ fail('not-found');
668
+ if (job.version !== expectedVersion)
669
+ fail('conflict');
670
+ if (job.backend !== 'linux-native' || !job.dispatchAttempted || !job.linuxIntent || !job.linuxResources
671
+ || job.requestDigest !== binding.requestDigest || job.nativeNonce !== binding.nativeNonce || job.nativeResourceId !== binding.nativeResourceId
672
+ || linuxExecutionIntentDigest(job.linuxIntent) !== binding.intentDigest || linuxExecutionResourcesDigest(job.linuxResources) !== binding.resourcesDigest)
673
+ fail('invalid-state', 'Linux receipt binding differs');
674
+ return { job, binding, now };
675
+ }
676
+ /** Only the Host can attest receipt integrity and exact kernel quiescence. */
677
+ settleLinuxReceipt(jobId, expectedVersion, binding, input, authority) {
678
+ const value = result(input);
679
+ if (!text(jobId) || !safePositive(expectedVersion) || value.jobId !== jobId || !value.quiescent)
680
+ fail('invalid-input');
681
+ const controller = this.#authority(authority);
682
+ if (!controller)
683
+ fail('invalid-input');
684
+ return this.#transaction(() => {
685
+ const { job, binding: checked, now } = this.#linuxReceiptJob(jobId, expectedVersion, binding, controller);
686
+ if (!['prepared', 'running'].includes(job.status))
687
+ fail('invalid-state', 'existing unknown requires reconciliation');
688
+ if (value.status === 'succeeded' && job.linuxResources?.schemaVersion === 2 && !job.linuxResources.userNamespace)
689
+ fail('invalid-state', 'Linux namespace admission is missing');
690
+ const changed = this.#database.prepare("UPDATE isolation_jobs SET status=?, result_json=?, version=version+1, updated_at=? WHERE id=? AND version=? AND status IN ('prepared','running')").run(value.status, JSON.stringify(value), now, jobId, expectedVersion);
691
+ if (changed.changes !== 1)
692
+ fail('conflict');
693
+ this.#audit(now, 'linux-receipt-settled', jobId, job.grantId, JSON.stringify(checked));
694
+ return this.#job(jobId);
695
+ });
696
+ }
697
+ settleLinuxReconciledUnknown(jobId, expectedVersion, binding, authority) {
698
+ if (!text(jobId) || !safePositive(expectedVersion))
699
+ fail('invalid-input');
700
+ const controller = this.#authority(authority);
701
+ if (!controller)
702
+ fail('invalid-input');
703
+ return this.#transaction(() => {
704
+ const { job, binding: checked, now } = this.#linuxReceiptJob(jobId, expectedVersion, binding, controller);
705
+ if (job.status !== 'unknown' || !job.result || job.result.quiescent)
706
+ fail('invalid-state');
707
+ const changed = this.#database.prepare("UPDATE isolation_jobs SET result_json=?, version=version+1, updated_at=? WHERE id=? AND version=? AND status='unknown'").run(JSON.stringify({ ...job.result, quiescent: true }), now, jobId, expectedVersion);
708
+ if (changed.changes !== 1)
709
+ fail('conflict');
710
+ this.#audit(now, 'linux-unknown-quiesced', jobId, job.grantId, JSON.stringify(checked));
711
+ return this.#job(jobId);
712
+ });
713
+ }
714
+ /** Recover a lost resources acknowledgement; never reopens start admission. */
715
+ recordLinuxRecoveredResources(jobId, expectedVersion, resources, authority) {
716
+ if (!text(jobId) || !safePositive(expectedVersion))
717
+ fail('invalid-input');
718
+ const controller = this.#authority(authority);
719
+ if (!controller)
720
+ fail('invalid-input');
721
+ return this.#transaction(() => {
722
+ const now = this.#nowValue();
723
+ this.#requireController(controller, now);
724
+ const job = this.#job(jobId);
725
+ if (!job)
726
+ fail('not-found');
727
+ if (job.version !== expectedVersion)
728
+ fail('conflict');
729
+ if (job.backend !== 'linux-native' || !job.dispatchAttempted || job.status !== 'unknown' || job.result?.quiescent !== false
730
+ || !job.linuxIntent || !job.linuxContract || job.linuxResources)
731
+ fail('invalid-state');
732
+ let checked;
733
+ try {
734
+ checked = validateLinuxExecutionResources(resources, job.linuxIntent, job.linuxContract);
735
+ }
736
+ catch {
737
+ fail('invalid-input');
738
+ }
739
+ const changed = this.#database.prepare("UPDATE isolation_jobs SET linux_resources_json=?,version=version+1,updated_at=? WHERE id=? AND version=? AND status='unknown' AND linux_resources_json IS NULL").run(JSON.stringify(checked), now, jobId, expectedVersion);
740
+ if (changed.changes !== 1)
741
+ fail('conflict');
742
+ this.#audit(now, 'linux-resources-recovered', jobId, job.grantId, linuxExecutionResourcesDigest(checked));
743
+ return this.#job(jobId);
744
+ });
745
+ }
746
+ /** Exact pre-start absence proof releases capacity, never the original outcome or budget. */
747
+ settleLinuxPrestartCleanup(jobId, expectedVersion, proof, authority) {
748
+ if (!text(jobId) || !safePositive(expectedVersion))
749
+ fail('invalid-input');
750
+ let checked;
751
+ try {
752
+ checked = validateLinuxExecutionPartialCleanupProof(proof);
753
+ }
754
+ catch {
755
+ fail('invalid-input');
756
+ }
757
+ const controller = this.#authority(authority);
758
+ if (!controller)
759
+ fail('invalid-input');
760
+ return this.#transaction(() => {
761
+ const now = this.#nowValue();
762
+ this.#requireController(controller, now);
763
+ const job = this.#job(jobId);
764
+ if (!job)
765
+ fail('not-found');
766
+ if (job.version !== expectedVersion)
767
+ fail('conflict');
768
+ if (job.backend !== 'linux-native' || !job.dispatchAttempted || !job.linuxIntent || job.linuxResources
769
+ || !['prepared', 'unknown'].includes(job.status) || job.result?.quiescent === true
770
+ || checked.jobId !== job.id || checked.requestDigest !== job.requestDigest || checked.nativeNonce !== job.nativeNonce
771
+ || checked.nativeResourceId !== job.nativeResourceId || checked.intentDigest !== linuxExecutionIntentDigest(job.linuxIntent)
772
+ || checked.observedAt < job.createdAt || checked.observedAt > now)
773
+ fail('invalid-state');
774
+ const value = job.result ? { ...job.result, quiescent: true }
775
+ : { jobId, status: 'unknown', quiescent: true, stdout: '', stderr: '', artifacts: [], reason: 'linux-prestart-cleanup-confirmed' };
776
+ const changed = this.#database.prepare("UPDATE isolation_jobs SET status='unknown',result_json=?,version=version+1,updated_at=? WHERE id=? AND version=? AND status IN ('prepared','unknown')").run(JSON.stringify(value), now, jobId, expectedVersion);
777
+ if (changed.changes !== 1)
778
+ fail('conflict');
779
+ this.#audit(now, 'linux-prestart-quiesced', jobId, job.grantId, JSON.stringify(checked));
780
+ return this.#job(jobId);
781
+ });
782
+ }
783
+ findJobByKey(value, sessionId, grantId, key) {
784
+ const who = identity(value);
785
+ if (!text(sessionId) || !text(grantId) || !text(key))
786
+ fail('invalid-input');
787
+ const row = this.#database.prepare('SELECT * FROM isolation_jobs WHERE principal_digest=? AND principal_record_id=? AND principal_version=? AND workspace=? AND agent_preset=? AND session_id=? AND grant_id=? AND idempotency_key=?').get(who.principalDigest, who.principalRecordId, who.principalVersion, who.workspace, who.agentPreset, sessionId, grantId, key);
788
+ return row ? this.#decodeJob(row) : undefined;
789
+ }
463
790
  findNativeVerifierJob(principalDigest, stateRoot, key) {
464
791
  if (!text(principalDigest) || !text(stateRoot) || !text(key))
465
792
  fail('invalid-input');
@@ -575,11 +902,11 @@ export class IsolationLedger {
575
902
  return false; const row = this.#database.prepare('SELECT owner_id, fence, expires_at FROM isolation_controller WHERE singleton=1').get(); return !!row && row.owner_id === controller.ownerId && row.fence === controller.fence && safeTime(row.expires_at) && row.expires_at > this.#nowValue(); }
576
903
  get(jobId) { if (!text(jobId))
577
904
  fail('invalid-input'); return this.#job(jobId); }
578
- recoverable(afterId = '') { return this.#database.prepare("SELECT * FROM isolation_jobs WHERE id > ? AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)) ORDER BY id ASC LIMIT 1000").all(afterId).map(row => this.#decodeJob(row)).filter(job => job.status === 'prepared' || job.status === 'running' || job.result?.quiescent === false); }
579
- retentionCandidates(cutoff, afterId = '', limit = 16) {
580
- if (!safeTime(cutoff) || typeof afterId !== 'string' || afterId.length > textMaximum || !safePositive(limit) || limit > 1_000)
905
+ recoverable(afterId = '') { return this.#database.prepare("SELECT * FROM isolation_jobs WHERE id > ? AND migrated=0 AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)) ORDER BY id ASC LIMIT 1000").all(afterId).map(row => this.#decodeJob(row)).filter(job => job.status === 'prepared' || job.status === 'running' || job.result?.quiescent === false); }
906
+ retentionCandidates(cutoff, afterId = '', limit = 16, includeCompactedNative = false) {
907
+ if (typeof includeCompactedNative !== 'boolean' || !safeTime(cutoff) || typeof afterId !== 'string' || afterId.length > textMaximum || !safePositive(limit) || limit > 1_000)
581
908
  fail('invalid-input');
582
- const rows = this.#database.prepare("SELECT id, version FROM isolation_jobs WHERE id > ? AND updated_at <= ? AND status IN ('succeeded','failed','cancelled','timed-out') AND result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 1 AND json_extract(result_json, '$.retention') IS NULL ORDER BY id ASC LIMIT ?").all(afterId, cutoff, limit);
909
+ const rows = this.#database.prepare("SELECT id, version FROM isolation_jobs WHERE id > ? AND status IN ('succeeded','failed','cancelled','timed-out') AND result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 1 AND ((updated_at <= ? AND json_extract(result_json, '$.retention') IS NULL) OR (? = 1 AND backend_kind = 'linux-native' AND json_extract(result_json, '$.retention.kind') = 'pruned')) ORDER BY id ASC LIMIT ?").all(afterId, cutoff, includeCompactedNative ? 1 : 0, limit);
583
910
  if (!rows.every(row => text(row.id) && safePositive(row.version)))
584
911
  fail('schema');
585
912
  return rows.map(row => frozen({ id: row.id, version: row.version }));
@@ -615,7 +942,7 @@ export class IsolationLedger {
615
942
  });
616
943
  }
617
944
  storageStats() {
618
- const counts = this.#database.prepare("SELECT COUNT(*) AS jobRecords, COALESCE(SUM(CASE WHEN status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0) THEN reserved_storage_bytes ELSE 0 END), 0) AS activeReservedBytes, COUNT(CASE WHEN (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)) AND reserved_storage_bytes = 0 THEN 1 END) AS legacyActiveJobs FROM isolation_jobs").get();
945
+ const counts = this.#database.prepare("SELECT COUNT(*) AS jobRecords, COALESCE(SUM(CASE WHEN migrated=0 AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)) THEN reserved_storage_bytes ELSE 0 END), 0) AS activeReservedBytes, COUNT(CASE WHEN migrated=0 AND (status IN ('prepared','running') OR (result_json IS NOT NULL AND json_extract(result_json, '$.quiescent') = 0)) AND reserved_storage_bytes = 0 THEN 1 END) AS legacyActiveJobs FROM isolation_jobs").get();
619
946
  const pages = this.#database.prepare('PRAGMA page_size').get();
620
947
  const free = this.#database.prepare('PRAGMA freelist_count').get();
621
948
  if (!safeTime(counts.jobRecords) || !safeTime(counts.activeReservedBytes) || !safeTime(counts.legacyActiveJobs) || !safePositive(pages.page_size) || !safeTime(free.freelist_count) || free.freelist_count > Math.floor(Number.MAX_SAFE_INTEGER / pages.page_size))