@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.
- package/README.md +287 -9
- package/lib/audit-archive.d.ts +1 -1
- package/lib/audit-archive.d.ts.map +1 -1
- package/lib/audit-archive.js +31 -13
- package/lib/audit-archive.js.map +1 -1
- package/lib/cli.d.ts +7 -3
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +104 -23
- package/lib/cli.js.map +1 -1
- package/lib/config.d.ts +15 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +49 -9
- package/lib/config.js.map +1 -1
- package/lib/diagnostics.js +2 -2
- package/lib/diagnostics.js.map +1 -1
- package/lib/index.d.ts +17 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -1
- package/lib/index.js.map +1 -1
- package/lib/ledger.d.ts +19 -2
- package/lib/ledger.d.ts.map +1 -1
- package/lib/ledger.js +371 -44
- package/lib/ledger.js.map +1 -1
- package/lib/linux-cgroups.d.ts +75 -0
- package/lib/linux-cgroups.d.ts.map +1 -0
- package/lib/linux-cgroups.js +517 -0
- package/lib/linux-cgroups.js.map +1 -0
- package/lib/linux-execution-contract.d.ts +123 -0
- package/lib/linux-execution-contract.d.ts.map +1 -0
- package/lib/linux-execution-contract.js +279 -0
- package/lib/linux-execution-contract.js.map +1 -0
- package/lib/linux-execution.d.ts +123 -0
- package/lib/linux-execution.d.ts.map +1 -0
- package/lib/linux-execution.js +1264 -0
- package/lib/linux-execution.js.map +1 -0
- package/lib/linux-job-recovery.d.ts +5 -0
- package/lib/linux-job-recovery.d.ts.map +1 -0
- package/lib/linux-job-recovery.js +63 -0
- package/lib/linux-job-recovery.js.map +1 -0
- package/lib/linux-namespace.d.ts +80 -0
- package/lib/linux-namespace.d.ts.map +1 -0
- package/lib/linux-namespace.js +471 -0
- package/lib/linux-namespace.js.map +1 -0
- package/lib/linux-repository-admission.d.ts +26 -0
- package/lib/linux-repository-admission.d.ts.map +1 -0
- package/lib/linux-repository-admission.js +64 -0
- package/lib/linux-repository-admission.js.map +1 -0
- package/lib/linux-repository-runner.d.ts +54 -0
- package/lib/linux-repository-runner.d.ts.map +1 -0
- package/lib/linux-repository-runner.js +614 -0
- package/lib/linux-repository-runner.js.map +1 -0
- package/lib/linux-runner-state-migration.d.ts +94 -0
- package/lib/linux-runner-state-migration.d.ts.map +1 -0
- package/lib/linux-runner-state-migration.js +463 -0
- package/lib/linux-runner-state-migration.js.map +1 -0
- package/lib/linux-runner-state.d.ts +53 -0
- package/lib/linux-runner-state.d.ts.map +1 -0
- package/lib/linux-runner-state.js +332 -0
- package/lib/linux-runner-state.js.map +1 -0
- package/lib/linux-runtime-source.d.ts +28 -0
- package/lib/linux-runtime-source.d.ts.map +1 -0
- package/lib/linux-runtime-source.js +237 -0
- package/lib/linux-runtime-source.js.map +1 -0
- package/lib/linux-runtime.d.ts +37 -0
- package/lib/linux-runtime.d.ts.map +1 -0
- package/lib/linux-runtime.js +192 -0
- package/lib/linux-runtime.js.map +1 -0
- package/lib/linux-system-snapshot.d.ts +47 -0
- package/lib/linux-system-snapshot.d.ts.map +1 -0
- package/lib/linux-system-snapshot.js +704 -0
- package/lib/linux-system-snapshot.js.map +1 -0
- package/lib/linux-user-namespace.d.ts +79 -0
- package/lib/linux-user-namespace.d.ts.map +1 -0
- package/lib/linux-user-namespace.js +234 -0
- package/lib/linux-user-namespace.js.map +1 -0
- package/lib/linux-verifier-runner.d.ts +27 -0
- package/lib/linux-verifier-runner.d.ts.map +1 -0
- package/lib/linux-verifier-runner.js +346 -0
- package/lib/linux-verifier-runner.js.map +1 -0
- package/lib/native-cgroup-roots.d.ts +20 -0
- package/lib/native-cgroup-roots.d.ts.map +1 -0
- package/lib/native-cgroup-roots.js +121 -0
- package/lib/native-cgroup-roots.js.map +1 -0
- package/lib/native-resources.d.ts +44 -0
- package/lib/native-resources.d.ts.map +1 -0
- package/lib/native-resources.js +354 -0
- package/lib/native-resources.js.map +1 -0
- package/lib/probe.d.ts +10 -0
- package/lib/probe.d.ts.map +1 -1
- package/lib/probe.js +52 -2
- package/lib/probe.js.map +1 -1
- package/lib/service.d.ts +1 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +122 -9
- package/lib/service.js.map +1 -1
- package/lib/storage.d.ts.map +1 -1
- package/lib/storage.js +36 -6
- package/lib/storage.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +5 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/verifier-runner.d.ts +4 -2
- package/lib/verifier-runner.d.ts.map +1 -1
- package/lib/verifier-runner.js +22 -1
- package/lib/verifier-runner.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +15 -10
- package/runtime/linux-candidate-entry.py +82 -0
- package/runtime/linux-namespace-worker.mjs +561 -0
- package/runtime/linux-seccomp-policy.LICENSE +202 -0
- package/runtime/linux-seccomp-policy.json +886 -0
- package/runtime/linux-seccomp.py +106 -0
- package/runtime/linux-userns-launcher.py +149 -0
- 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 =
|
|
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
|
-
|
|
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 !== '
|
|
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
|
|
145
|
-
|
|
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
|
-
|
|
169
|
+
const value = operation();
|
|
170
|
+
this.#database.exec('COMMIT');
|
|
171
|
+
return value;
|
|
152
172
|
}
|
|
153
|
-
catch {
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
416
|
+
refuse('unauthorized', 'storage observation unavailable');
|
|
302
417
|
if (observation.observedAt > now || now - observation.observedAt > 5_000)
|
|
303
|
-
|
|
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)
|
|
420
|
+
if (!safeTime(records.count))
|
|
306
421
|
fail('unauthorized', 'isolation job record limit reached');
|
|
307
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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))
|