@yeaft/webchat-agent 1.0.372 → 1.0.374

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 (34) hide show
  1. package/cli.js +8 -0
  2. package/connection/index.js +11 -1
  3. package/index.js +4 -0
  4. package/local-runtime/server/api.js +2 -0
  5. package/local-runtime/server/auth/login.js +1 -0
  6. package/local-runtime/server/auth/oauth-flow.js +2 -2
  7. package/local-runtime/server/auth/token.js +4 -0
  8. package/local-runtime/server/config.js +47 -1
  9. package/local-runtime/server/database.js +1 -0
  10. package/local-runtime/server/db/connection.js +346 -4
  11. package/local-runtime/server/db/sandbox-db.js +673 -0
  12. package/local-runtime/server/db/user-db.js +137 -7
  13. package/local-runtime/server/index.js +60 -2
  14. package/local-runtime/server/routes/sandbox-routes.js +124 -0
  15. package/local-runtime/server/routes/user-routes.js +30 -15
  16. package/local-runtime/server/sandbox-agent-auth.js +66 -0
  17. package/local-runtime/server/sandbox-attestation-listener.js +128 -0
  18. package/local-runtime/server/sandbox-config.js +42 -0
  19. package/local-runtime/server/sandbox-host-attestation.js +175 -0
  20. package/local-runtime/server/sandbox-reconciler.js +355 -0
  21. package/local-runtime/server/ws-agent.js +34 -11
  22. package/local-runtime/server/ws-client.js +18 -5
  23. package/local-runtime/version.json +1 -1
  24. package/local-runtime/web/app.bundle.js +90 -29
  25. package/local-runtime/web/app.bundle.js.gz +0 -0
  26. package/local-runtime/web/index.html +2 -2
  27. package/local-runtime/web/style.bundle.css +1 -1
  28. package/local-runtime/web/style.bundle.css.gz +0 -0
  29. package/managed-sandbox/agent-runtime.js +73 -0
  30. package/managed-sandbox/controller.js +118 -0
  31. package/managed-sandbox/helper.js +437 -0
  32. package/managed-sandbox/identity-store.js +9 -0
  33. package/managed-sandbox/runtime-executor.js +387 -0
  34. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { setManagedSandboxIdentity } from './identity-store.js';
4
+
5
+ function assertIdentity(value) {
6
+ const claims = value?.claims;
7
+ if (!value?.serverUrl || !claims?.sandboxId || !claims?.instanceId
8
+ || !Number.isInteger(claims.generation) || !claims.imageDigest) {
9
+ throw new Error('Managed Sandbox Agent rejected an invalid identity');
10
+ }
11
+ }
12
+
13
+ async function readJson(path) {
14
+ return JSON.parse(await readFile(path, 'utf8'));
15
+ }
16
+
17
+ async function persistCredential(path, identity) {
18
+ const temporary = join(dirname(path), `.managed-agent-credential-${process.pid}.tmp`);
19
+ await writeFile(temporary, JSON.stringify(identity), { mode: 0o600 });
20
+ await rename(temporary, path);
21
+ }
22
+
23
+ function exchangeUrl(serverUrl) {
24
+ const url = new URL(serverUrl);
25
+ if (url.protocol === 'wss:') url.protocol = 'https:';
26
+ else if (url.protocol === 'ws:') url.protocol = 'http:';
27
+ url.pathname = '/api/sandbox/bootstrap/exchange';
28
+ url.search = '';
29
+ return url;
30
+ }
31
+
32
+ export async function loadManagedSandboxIdentity({ bootstrapFile, credentialFile, fetchImpl = fetch }) {
33
+ try {
34
+ const saved = await readJson(credentialFile);
35
+ assertIdentity(saved);
36
+ if (!saved.credentialId || !saved.secret) throw new Error('Managed Sandbox Agent credential is incomplete');
37
+ return saved;
38
+ } catch (error) {
39
+ if (error.code !== 'ENOENT') throw error;
40
+ }
41
+
42
+ const bootstrap = await readJson(bootstrapFile);
43
+ assertIdentity(bootstrap);
44
+ if (!bootstrap.token) throw new Error('Managed Sandbox Agent bootstrap token is missing');
45
+ const response = await fetchImpl(exchangeUrl(bootstrap.serverUrl), {
46
+ method: 'POST',
47
+ headers: { 'content-type': 'application/json' },
48
+ body: JSON.stringify({ token: bootstrap.token, claims: bootstrap.claims })
49
+ });
50
+ if (!response.ok) throw new Error(`Managed Sandbox Agent bootstrap exchange failed (${response.status})`);
51
+ const credential = await response.json();
52
+ const identity = { ...bootstrap, token: undefined, ...credential };
53
+ if (!identity.credentialId || !identity.secret) throw new Error('Managed Sandbox Agent bootstrap returned no credential');
54
+ await persistCredential(credentialFile, identity);
55
+ await unlink(bootstrapFile);
56
+ return identity;
57
+ }
58
+
59
+ export async function runManagedSandboxAgent(args, options = {}) {
60
+ const bootstrapIndex = args.indexOf('--bootstrap-file');
61
+ if (bootstrapIndex < 0 || !args[bootstrapIndex + 1]) {
62
+ throw new Error('managed-sandbox requires --bootstrap-file');
63
+ }
64
+ const bootstrapFile = args[bootstrapIndex + 1];
65
+ const credentialFile = options.credentialFile || '/home/yeaft/.yeaft/managed-agent-credential';
66
+ const identity = await loadManagedSandboxIdentity({ bootstrapFile, credentialFile, fetchImpl: options.fetchImpl });
67
+ setManagedSandboxIdentity(identity);
68
+ process.env.SERVER_URL = identity.serverUrl;
69
+ process.env.AGENT_NAME = identity.claims.sandboxId;
70
+ process.env.YEAFT_AGENT_INSTANCE = identity.claims.instanceId;
71
+ process.env.WORK_DIR = '/workspace';
72
+ await (options.startAgent || (() => import('../index.js')))();
73
+ }
@@ -0,0 +1,118 @@
1
+ import { createServer as createHttpsServer } from 'node:https';
2
+ import { sign, timingSafeEqual } from 'node:crypto';
3
+
4
+ const MAX_REQUEST_BYTES = 64 * 1024;
5
+
6
+ function canonicalControllerResult(result) {
7
+ return JSON.stringify({
8
+ operationId: result.operationId,
9
+ action: result.action,
10
+ hostId: result.hostId,
11
+ sandboxId: result.sandboxId,
12
+ requestDigest: result.requestDigest,
13
+ generation: result.generation,
14
+ hostEpoch: result.hostEpoch,
15
+ requestNonce: result.requestNonce,
16
+ issuedAt: result.issuedAt,
17
+ success: result.success,
18
+ imageDigest: result.imageDigest || null,
19
+ helperAttestation: result.helperAttestation || null,
20
+ errorCode: result.errorCode || null
21
+ });
22
+ }
23
+
24
+ function tokenMatches(header, expectedToken) {
25
+ const prefix = 'Bearer ';
26
+ if (typeof header !== 'string' || !header.startsWith(prefix)) return false;
27
+ const actual = Buffer.from(header.slice(prefix.length));
28
+ const expected = Buffer.from(expectedToken);
29
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
30
+ }
31
+
32
+ function writeJson(response, status, body) {
33
+ response.writeHead(status, { 'content-type': 'application/json' });
34
+ response.end(JSON.stringify(body));
35
+ }
36
+
37
+ /**
38
+ * Dedicated-Host Controller boundary. Privileged work remains exclusively in
39
+ * the Helper; the Controller only authenticates Server requests and signs the
40
+ * Helper's durable result for transport back to the control plane.
41
+ */
42
+ export function createSandboxController({ config, helper, now = Date.now, createServer = createHttpsServer }) {
43
+ if (!config?.hostId || !config.token || !config.tlsCert || !config.tlsKey || !config.clientCa
44
+ || !config.resultSigningPrivateKey || !helper?.execute) {
45
+ throw new Error('Sandbox Controller requires complete dedicated Host configuration');
46
+ }
47
+
48
+ async function execute(operation) {
49
+ if (!operation || operation.hostId !== config.hostId) {
50
+ throw new Error('Sandbox Controller rejected an operation for another Host');
51
+ }
52
+ const helperResult = await helper.execute(operation);
53
+ const result = {
54
+ operationId: operation.operationId,
55
+ action: operation.action,
56
+ hostId: operation.hostId,
57
+ sandboxId: operation.sandboxId || null,
58
+ requestDigest: operation.requestDigest,
59
+ generation: operation.generation || null,
60
+ hostEpoch: operation.hostEpoch,
61
+ requestNonce: operation.nonce,
62
+ issuedAt: now(),
63
+ success: helperResult.success === true,
64
+ imageDigest: helperResult.helperAttestation?.imageDigest || null,
65
+ helperAttestation: helperResult.helperAttestation || null,
66
+ errorCode: helperResult.errorCode || null
67
+ };
68
+ result.signature = sign(
69
+ null,
70
+ Buffer.from(canonicalControllerResult(result)),
71
+ config.resultSigningPrivateKey
72
+ ).toString('base64url');
73
+ return result;
74
+ }
75
+
76
+ const server = createServer({
77
+ cert: config.tlsCert,
78
+ key: config.tlsKey,
79
+ ca: config.clientCa,
80
+ requestCert: true,
81
+ rejectUnauthorized: true
82
+ }, (request, response) => {
83
+ if (request.method !== 'POST' || request.url !== '/v1/operations') {
84
+ writeJson(response, 404, { code: 'NOT_FOUND' });
85
+ return;
86
+ }
87
+ if (!request.socket.authorized || !tokenMatches(request.headers.authorization, config.token)) {
88
+ writeJson(response, 401, { code: 'UNAUTHORIZED' });
89
+ request.resume();
90
+ return;
91
+ }
92
+
93
+ const chunks = [];
94
+ let size = 0;
95
+ request.on('data', chunk => {
96
+ size += chunk.length;
97
+ if (size > MAX_REQUEST_BYTES) request.destroy();
98
+ else chunks.push(chunk);
99
+ });
100
+ request.on('end', async () => {
101
+ if (size > MAX_REQUEST_BYTES) return;
102
+ try {
103
+ const operation = JSON.parse(Buffer.concat(chunks).toString('utf8'));
104
+ writeJson(response, 200, await execute(operation));
105
+ } catch {
106
+ writeJson(response, 400, { code: 'SANDBOX_OPERATION_REJECTED' });
107
+ }
108
+ });
109
+ });
110
+
111
+ return {
112
+ execute,
113
+ listen: (...args) => server.listen(...args),
114
+ close: () => new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
115
+ };
116
+ }
117
+
118
+ export { canonicalControllerResult };
@@ -0,0 +1,437 @@
1
+ import { createHash, sign, verify } from 'node:crypto';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+
6
+ const ALLOWED_ACTIONS = new Set(['ACTIVATE_EPOCH', 'create', 'start', 'stop', 'retry', 'remove']);
7
+
8
+ function canonicalOperation(operation) {
9
+ return JSON.stringify({
10
+ protocolVersion: operation.protocolVersion,
11
+ operationId: operation.operationId,
12
+ hostId: operation.hostId,
13
+ sandboxId: operation.sandboxId || null,
14
+ action: operation.action,
15
+ requestDigest: operation.requestDigest,
16
+ generation: operation.generation || null,
17
+ hostEpoch: operation.hostEpoch,
18
+ instanceId: operation.instanceId,
19
+ imageDigest: operation.imageDigest,
20
+ desiredState: operation.desiredState,
21
+ issuedAt: operation.issuedAt,
22
+ expiresAt: operation.expiresAt,
23
+ nonce: operation.nonce,
24
+ bootstrap: operation.bootstrap || null,
25
+ resources: operation.resources
26
+ });
27
+ }
28
+
29
+ function digestOperation(operation) {
30
+ return createHash('sha256').update(canonicalOperation(operation)).digest('hex');
31
+ }
32
+
33
+ function canonicalAttestation(attestation) {
34
+ return JSON.stringify({
35
+ protocolVersion: attestation.protocolVersion,
36
+ operationId: attestation.operationId,
37
+ hostId: attestation.hostId,
38
+ sandboxId: attestation.sandboxId,
39
+ action: attestation.action,
40
+ requestDigest: attestation.requestDigest,
41
+ generation: attestation.generation,
42
+ hostEpoch: attestation.hostEpoch,
43
+ requestNonce: attestation.requestNonce,
44
+ issuedAt: attestation.issuedAt,
45
+ imageDigest: attestation.imageDigest || null,
46
+ readinessProof: attestation.readinessProof || null,
47
+ absenceProof: attestation.absenceProof || null,
48
+ resourceInspection: attestation.resourceInspection || null
49
+ });
50
+ }
51
+
52
+ function assertProofShape(operation, result) {
53
+ if (!result || typeof result !== 'object' || typeof result.success !== 'boolean') {
54
+ throw new Error('Sandbox Helper executor returned an invalid result');
55
+ }
56
+ if (!result.success) return;
57
+ if (operation.action === 'remove') {
58
+ const proof = result.absenceProof;
59
+ if (!proof || proof.container !== true || proof.storage !== true || proof.quota !== true
60
+ || proof.network !== true || proof.credential !== true) {
61
+ throw new Error('Sandbox Helper executor did not prove resource absence');
62
+ }
63
+ return;
64
+ }
65
+ const proof = result.readinessProof;
66
+ const inspection = result.resourceInspection;
67
+ const requiredProofs = ['image', 'cpu', 'memory', 'pid', 'io', 'quota', 'network', 'credential'];
68
+ if (!proof || !requiredProofs.every(key => proof[key] === true)
69
+ || !inspection || inspection.cpuMillis !== operation.resources.cpuMillis
70
+ || inspection.memoryMiB !== operation.resources.memoryMiB
71
+ || inspection.diskGiB !== operation.resources.diskGiB
72
+ || !Number.isInteger(inspection.pidsLimit) || inspection.pidsLimit <= 0
73
+ || !Number.isInteger(inspection.ioWeight) || inspection.ioWeight <= 0
74
+ || inspection.quotaHard !== true
75
+ || inspection.networkPolicy !== 'public-egress-isolated') {
76
+ throw new Error('Sandbox Helper executor did not prove the requested resource policy');
77
+ }
78
+ }
79
+
80
+ function buildAttestation(operation, result, config, issuedAt) {
81
+ const attestation = {
82
+ protocolVersion: 1,
83
+ operationId: operation.operationId,
84
+ hostId: operation.hostId,
85
+ sandboxId: operation.sandboxId || null,
86
+ action: operation.action,
87
+ requestDigest: operation.requestDigest,
88
+ generation: operation.generation || null,
89
+ hostEpoch: operation.hostEpoch,
90
+ requestNonce: operation.nonce,
91
+ issuedAt,
92
+ imageDigest: ['remove', 'ACTIVATE_EPOCH'].includes(operation.action) ? null : operation.imageDigest,
93
+ readinessProof: result.success && !['remove', 'ACTIVATE_EPOCH'].includes(operation.action) ? result.readinessProof : null,
94
+ absenceProof: result.success && operation.action === 'remove' ? result.absenceProof : null,
95
+ resourceInspection: result.success && !['remove', 'ACTIVATE_EPOCH'].includes(operation.action) ? result.resourceInspection : null
96
+ };
97
+ attestation.signature = sign(
98
+ null,
99
+ Buffer.from(canonicalAttestation(attestation)),
100
+ config.attestationSigningPrivateKey
101
+ ).toString('base64url');
102
+ return { ...result, helperAttestation: attestation };
103
+ }
104
+
105
+ function validateOperation(operation, config, now) {
106
+ const activation = operation?.action === 'ACTIVATE_EPOCH';
107
+ if (!operation || operation.protocolVersion !== 1
108
+ || !operation.operationId || (!activation && (!operation.sandboxId || !operation.instanceId))
109
+ || operation.hostId !== config.hostId
110
+ || !ALLOWED_ACTIONS.has(operation.action)
111
+ || (!activation && (!Number.isInteger(operation.generation) || operation.generation < 1))
112
+ || !operation.requestDigest
113
+ || !Number.isSafeInteger(operation.hostEpoch) || operation.hostEpoch <= 0
114
+ || !operation.nonce
115
+ || !Number.isFinite(operation.issuedAt) || !Number.isFinite(operation.expiresAt)
116
+ || operation.issuedAt > now + config.maxClockSkewMs
117
+ || operation.expiresAt < now
118
+ || operation.expiresAt - operation.issuedAt > config.maxOperationTtlMs) {
119
+ throw new Error('Sandbox Helper rejected an invalid operation envelope');
120
+ }
121
+ if (!activation && operation.action !== 'remove') {
122
+ const resources = operation.resources;
123
+ if (!resources || !Number.isInteger(resources.cpuMillis) || resources.cpuMillis <= 0
124
+ || !Number.isInteger(resources.memoryMiB) || resources.memoryMiB <= 0
125
+ || !Number.isInteger(resources.diskGiB) || resources.diskGiB <= 0
126
+ || operation.imageDigest !== config.imageDigest) {
127
+ throw new Error('Sandbox Helper rejected an invalid resource policy');
128
+ }
129
+ }
130
+ let valid = false;
131
+ try {
132
+ valid = verify(
133
+ null,
134
+ Buffer.from(canonicalOperation(operation)),
135
+ config.operationSigningPublicKey,
136
+ Buffer.from(String(operation.signature || ''), 'base64url')
137
+ );
138
+ } catch {
139
+ valid = false;
140
+ }
141
+ if (!valid) throw new Error('Sandbox Helper rejected an invalid operation signature');
142
+ }
143
+
144
+ /**
145
+ * Root-only durable authorization boundary for a dedicated Sandbox Host.
146
+ * The injected executor is the only component allowed to perform runtime actions.
147
+ */
148
+ export function createSandboxHelper({ config, executor, now = Date.now }) {
149
+ if (!config?.hostId || !config.imageDigest || !config.operationSigningPublicKey
150
+ || !config.attestationSigningPrivateKey || !config.journalPath || !executor?.execute) {
151
+ throw new Error('Sandbox Helper requires a complete dedicated Host configuration');
152
+ }
153
+ const effectiveConfig = {
154
+ maxClockSkewMs: 30_000,
155
+ maxOperationTtlMs: 30_000,
156
+ ...config
157
+ };
158
+ mkdirSync(dirname(effectiveConfig.journalPath), { recursive: true, mode: 0o700 });
159
+ const db = new DatabaseSync(effectiveConfig.journalPath);
160
+ const epochLockDb = new DatabaseSync(`${effectiveConfig.journalPath}.epoch-lock`);
161
+ db.exec(`
162
+ PRAGMA journal_mode = WAL;
163
+ PRAGMA synchronous = FULL;
164
+ PRAGMA busy_timeout = 5000;
165
+ CREATE TABLE IF NOT EXISTS helper_state (
166
+ key TEXT PRIMARY KEY,
167
+ value TEXT NOT NULL
168
+ );
169
+ CREATE TABLE IF NOT EXISTS helper_operations (
170
+ operation_id TEXT PRIMARY KEY,
171
+ sandbox_id TEXT,
172
+ action TEXT,
173
+ request_digest TEXT NOT NULL,
174
+ host_epoch TEXT NOT NULL,
175
+ status TEXT NOT NULL CHECK(status IN ('in_progress', 'succeeded', 'failed', 'recovery_required')),
176
+ result_json TEXT,
177
+ updated_at INTEGER NOT NULL
178
+ );
179
+ `);
180
+ epochLockDb.exec(`
181
+ PRAGMA journal_mode = DELETE;
182
+ PRAGMA synchronous = FULL;
183
+ PRAGMA busy_timeout = 0;
184
+ CREATE TABLE IF NOT EXISTS helper_epoch_lock (
185
+ id INTEGER PRIMARY KEY CHECK(id = 1),
186
+ holder TEXT
187
+ );
188
+ INSERT OR IGNORE INTO helper_epoch_lock(id, holder) VALUES (1, NULL);
189
+ `);
190
+ const operationColumns = db.prepare('PRAGMA table_info(helper_operations)').all();
191
+ if (!operationColumns.some(column => column.name === 'sandbox_id')) {
192
+ db.exec('ALTER TABLE helper_operations ADD COLUMN sandbox_id TEXT');
193
+ }
194
+ if (!operationColumns.some(column => column.name === 'action')) {
195
+ db.exec('ALTER TABLE helper_operations ADD COLUMN action TEXT');
196
+ }
197
+
198
+ let executing = 0;
199
+ let executionWaiter = null;
200
+ let queuedActivations = 0;
201
+ let activationTail = Promise.resolve();
202
+
203
+ async function acquireEpochLock() {
204
+ while (true) {
205
+ try {
206
+ epochLockDb.exec('BEGIN IMMEDIATE');
207
+ return;
208
+ } catch (error) {
209
+ if (!String(error?.message || '').includes('database is locked')) throw error;
210
+ await new Promise(resolve => setTimeout(resolve, 10));
211
+ }
212
+ }
213
+ }
214
+
215
+ function releaseEpochLock() {
216
+ epochLockDb.exec('COMMIT');
217
+ }
218
+
219
+ function activeEpoch() {
220
+ const storedEpoch = db.prepare("SELECT value FROM helper_state WHERE key = 'active_epoch'").get()?.value;
221
+ const digest = db.prepare("SELECT value FROM helper_state WHERE key = 'active_epoch_digest'").get()?.value;
222
+ if (!storedEpoch && !digest) return null;
223
+ const epoch = Number(storedEpoch);
224
+ if (!Number.isSafeInteger(epoch) || epoch <= 0 || !digest) {
225
+ throw new Error('Sandbox Helper found invalid durable Host epoch state');
226
+ }
227
+ return { epoch, digest };
228
+ }
229
+
230
+ function writeActiveEpoch(epoch, digest) {
231
+ const write = db.prepare(`INSERT INTO helper_state(key, value) VALUES (?, ?)
232
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`);
233
+ write.run('active_epoch', epoch);
234
+ write.run('active_epoch_digest', digest);
235
+ write.run(`epoch:${epoch}`, digest);
236
+ }
237
+
238
+ function activationIntent() {
239
+ const storedEpoch = db.prepare("SELECT value FROM helper_state WHERE key = 'activation_intent_epoch'").get()?.value;
240
+ const digest = db.prepare("SELECT value FROM helper_state WHERE key = 'activation_intent_digest'").get()?.value;
241
+ if (!storedEpoch && !digest) return null;
242
+ const epoch = Number(storedEpoch);
243
+ if (!Number.isSafeInteger(epoch) || epoch <= 0 || !digest) {
244
+ throw new Error('Sandbox Helper found invalid durable Host epoch activation intent');
245
+ }
246
+ return { epoch, digest };
247
+ }
248
+
249
+ function writeActivationIntent(epoch, digest) {
250
+ db.exec('BEGIN IMMEDIATE');
251
+ try {
252
+ const current = activeEpoch();
253
+ const pending = activationIntent();
254
+ if (current?.epoch === epoch) {
255
+ if (current.digest !== digest) throw new Error('Sandbox Helper rejected conflicting epoch activation');
256
+ db.exec('COMMIT');
257
+ return false;
258
+ }
259
+ if ((current && epoch < current.epoch) || (pending && pending.epoch > epoch)) {
260
+ throw new Error('Sandbox Helper rejected epoch rollback');
261
+ }
262
+ if (pending?.epoch === epoch && pending.digest !== digest) {
263
+ throw new Error('Sandbox Helper rejected conflicting epoch activation');
264
+ }
265
+ const write = db.prepare(`INSERT INTO helper_state(key, value) VALUES (?, ?)
266
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`);
267
+ write.run('activation_intent_epoch', epoch);
268
+ write.run('activation_intent_digest', digest);
269
+ db.exec('COMMIT');
270
+ return true;
271
+ } catch (error) {
272
+ db.exec('ROLLBACK');
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ function activateEpoch(operation) {
278
+ validateOperation(operation, effectiveConfig, now());
279
+ if (operation.action !== 'ACTIVATE_EPOCH') {
280
+ throw new Error('Sandbox Helper rejected an invalid epoch activation');
281
+ }
282
+ const epoch = operation.hostEpoch;
283
+ // The durable epoch identity is the Server-authorized immutable activation
284
+ // digest. Per-request nonce/timestamps remain covered by the signature but
285
+ // must not make an idempotent activation look like a conflicting epoch.
286
+ const digest = operation.requestDigest;
287
+ let activationNeeded;
288
+ try {
289
+ activationNeeded = writeActivationIntent(epoch, digest);
290
+ } catch (error) {
291
+ return Promise.reject(error);
292
+ }
293
+ if (!activationNeeded) {
294
+ return Promise.resolve(buildAttestation(
295
+ operation, { success: true, activated: false }, effectiveConfig, now()
296
+ ));
297
+ }
298
+ queuedActivations++;
299
+ const activation = activationTail.then(async () => {
300
+ try {
301
+ if (executing > 0) await new Promise(resolve => { executionWaiter = resolve; });
302
+ await acquireEpochLock();
303
+ try {
304
+ const current = activeEpoch();
305
+ if (current?.epoch === epoch) {
306
+ if (current.digest !== digest) throw new Error('Sandbox Helper rejected conflicting epoch activation');
307
+ return buildAttestation(operation, { success: true, activated: false }, effectiveConfig, now());
308
+ }
309
+ if (current && epoch < current.epoch) {
310
+ throw new Error('Sandbox Helper rejected epoch rollback');
311
+ }
312
+ db.exec('BEGIN IMMEDIATE');
313
+ try {
314
+ writeActiveEpoch(epoch, digest);
315
+ const pending = activationIntent();
316
+ if (pending?.epoch === epoch && pending.digest === digest) {
317
+ db.prepare("DELETE FROM helper_state WHERE key IN ('activation_intent_epoch', 'activation_intent_digest')").run();
318
+ }
319
+ db.exec('COMMIT');
320
+ } catch (error) {
321
+ db.exec('ROLLBACK');
322
+ throw error;
323
+ }
324
+ return buildAttestation(operation, { success: true, activated: true }, effectiveConfig, now());
325
+ } finally {
326
+ releaseEpochLock();
327
+ }
328
+ } finally {
329
+ queuedActivations--;
330
+ }
331
+ });
332
+ activationTail = activation.catch(() => {});
333
+ return activation;
334
+ }
335
+
336
+ const interrupted = db.prepare("SELECT operation_id, sandbox_id FROM helper_operations WHERE status = 'in_progress'").all();
337
+ if (interrupted.length > 0) {
338
+ db.exec('BEGIN IMMEDIATE');
339
+ try {
340
+ db.prepare("UPDATE helper_operations SET status = 'recovery_required', updated_at = ? WHERE status = 'in_progress'")
341
+ .run(now());
342
+ const recordRecovery = db.prepare("INSERT INTO helper_state(key, value) VALUES (?, '1') ON CONFLICT(key) DO UPDATE SET value = excluded.value");
343
+ for (const operation of interrupted) {
344
+ recordRecovery.run(operation.sandbox_id ? `recovery:${operation.sandbox_id}` : 'recovery_required');
345
+ }
346
+ db.exec('COMMIT');
347
+ } catch (error) {
348
+ db.exec('ROLLBACK');
349
+ throw error;
350
+ }
351
+ }
352
+
353
+ function assertAvailable(operation) {
354
+ const legacyRecovery = db.prepare("SELECT value FROM helper_state WHERE key = 'recovery_required'").get();
355
+ const sandboxRecovery = db.prepare('SELECT value FROM helper_state WHERE key = ?')
356
+ .get(`recovery:${operation.sandboxId}`);
357
+ if (legacyRecovery?.value === '1' || (sandboxRecovery?.value === '1' && operation.action !== 'remove')) {
358
+ throw new Error('Sandbox Helper requires operator recovery');
359
+ }
360
+ }
361
+
362
+ async function execute(operation) {
363
+ if (operation?.action === 'ACTIVATE_EPOCH') return activateEpoch(operation);
364
+ validateOperation(operation, effectiveConfig, now());
365
+ while (queuedActivations > 0) await activationTail;
366
+ await acquireEpochLock();
367
+ const requestDigest = digestOperation(operation);
368
+ db.exec('BEGIN IMMEDIATE');
369
+ try {
370
+ const existing = db.prepare('SELECT * FROM helper_operations WHERE operation_id = ?').get(operation.operationId);
371
+ if (existing) {
372
+ if (existing.request_digest !== requestDigest) {
373
+ throw new Error('Sandbox Helper rejected operation ID reuse with a different request');
374
+ }
375
+ if (existing.status === 'succeeded' || existing.status === 'failed') {
376
+ db.exec('COMMIT');
377
+ releaseEpochLock();
378
+ return JSON.parse(existing.result_json);
379
+ }
380
+ throw new Error('Sandbox Helper operation is not safely replayable');
381
+ }
382
+ const epoch = activeEpoch();
383
+ const pending = activationIntent();
384
+ if (!epoch || operation.hostEpoch !== epoch.epoch
385
+ || (pending && operation.hostEpoch !== pending.epoch)) {
386
+ throw new Error('Sandbox Helper rejected an inactive Host epoch');
387
+ }
388
+ assertAvailable(operation);
389
+ db.prepare(`INSERT INTO helper_operations
390
+ (operation_id, sandbox_id, action, request_digest, host_epoch, status, updated_at)
391
+ VALUES (?, ?, ?, ?, ?, 'in_progress', ?)`)
392
+ .run(operation.operationId, operation.sandboxId, operation.action,
393
+ requestDigest, operation.hostEpoch, now());
394
+ db.exec('COMMIT');
395
+ } catch (error) {
396
+ db.exec('ROLLBACK');
397
+ releaseEpochLock();
398
+ throw error;
399
+ }
400
+
401
+ executing++;
402
+ let result;
403
+ let status;
404
+ try {
405
+ try {
406
+ const executorResult = await executor.execute(operation);
407
+ assertProofShape(operation, executorResult);
408
+ result = buildAttestation(operation, executorResult, effectiveConfig, now());
409
+ status = executorResult.success ? 'succeeded' : 'failed';
410
+ } catch (error) {
411
+ result = buildAttestation(operation, {
412
+ success: false,
413
+ errorCode: 'SANDBOX_HELPER_EXECUTION_FAILED'
414
+ }, effectiveConfig, now());
415
+ status = 'failed';
416
+ }
417
+ db.prepare('UPDATE helper_operations SET status = ?, result_json = ?, updated_at = ? WHERE operation_id = ?')
418
+ .run(status, JSON.stringify(result), now(), operation.operationId);
419
+ return result;
420
+ } finally {
421
+ releaseEpochLock();
422
+ executing--;
423
+ if (executing === 0 && executionWaiter) {
424
+ const resolve = executionWaiter;
425
+ executionWaiter = null;
426
+ resolve();
427
+ }
428
+ }
429
+ }
430
+
431
+ return { execute, activateEpoch, activeEpoch, close: () => {
432
+ db.close();
433
+ epochLockDb.close();
434
+ } };
435
+ }
436
+
437
+ export { canonicalOperation };
@@ -0,0 +1,9 @@
1
+ let managedSandboxIdentity = null;
2
+
3
+ export function setManagedSandboxIdentity(identity) {
4
+ managedSandboxIdentity = identity;
5
+ }
6
+
7
+ export function getManagedSandboxIdentity() {
8
+ return managedSandboxIdentity;
9
+ }