@fitlab-ai/agent-infra 0.11.13 → 0.11.14
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/dist/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/task-finalization.js +6 -4
- package/dist/lib/internal/task-lifecycle.js +3 -1
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +5 -4
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +12 -47
- package/dist/lib/sandbox/control/executor.js +6 -10
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +20 -16
- package/dist/lib/sandbox/control/protocol.js +8 -36
- package/dist/lib/sandbox/control/server.js +2 -21
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -59
- package/dist/lib/sandbox/recovery.js +2 -3
- package/dist/lib/sandbox/removal.js +19 -4
- package/dist/lib/sandbox/workspace-view.js +41 -4
- package/dist/lib/task/control-authority.js +39 -17
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +13 -2
- package/dist/lib/task/lifecycle.js +92 -33
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/task-finalization.ts +6 -4
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/release-notes.ts +2 -2
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +5 -4
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +13 -61
- package/lib/sandbox/control/executor.ts +7 -13
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +18 -16
- package/lib/sandbox/control/protocol.ts +9 -45
- package/lib/sandbox/control/server.ts +2 -17
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +92 -75
- package/lib/sandbox/recovery.ts +2 -3
- package/lib/sandbox/removal.ts +16 -4
- package/lib/sandbox/workspace-view.ts +40 -5
- package/lib/task/control-authority.ts +40 -26
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +15 -2
- package/lib/task/lifecycle.ts +103 -31
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/sandbox/control/agent-operation.ts +0 -49
|
@@ -2,17 +2,14 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import {
|
|
5
|
-
SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
6
5
|
SANDBOX_CONTROL_MAX_BYTES,
|
|
7
6
|
SANDBOX_CONTROL_STATUS_STALE_MS,
|
|
8
7
|
isSandboxControlFamily,
|
|
9
8
|
type SandboxControlError,
|
|
10
9
|
type SandboxControlRequest,
|
|
11
10
|
type SandboxControlResponse,
|
|
12
|
-
type SandboxAgentClientRequest,
|
|
13
11
|
type SandboxTaskCreateRequest
|
|
14
12
|
} from './protocol.ts';
|
|
15
|
-
import { isAgentClientId } from '../../agent-clients/types.ts';
|
|
16
13
|
import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
|
|
17
14
|
import type { TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
18
15
|
import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
|
|
@@ -53,11 +50,10 @@ function clientError(
|
|
|
53
50
|
|
|
54
51
|
function preflight(
|
|
55
52
|
statusDir: string,
|
|
56
|
-
generation: string,
|
|
57
53
|
taskViewEffect: TaskViewAccessEffect | null = null,
|
|
58
54
|
now = Date.now(),
|
|
59
55
|
env: NodeJS.ProcessEnv = process.env
|
|
60
|
-
):
|
|
56
|
+
): string {
|
|
61
57
|
if (fs.existsSync(statusDir)) {
|
|
62
58
|
let identity;
|
|
63
59
|
try {
|
|
@@ -68,9 +64,6 @@ function preflight(
|
|
|
68
64
|
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
|
|
69
65
|
clientError(code, 'sandbox control identity is missing or invalid', false);
|
|
70
66
|
}
|
|
71
|
-
if (identity.generation !== generation) {
|
|
72
|
-
clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
|
|
73
|
-
}
|
|
74
67
|
if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
75
68
|
clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
|
|
76
69
|
}
|
|
@@ -81,9 +74,6 @@ function preflight(
|
|
|
81
74
|
} catch {
|
|
82
75
|
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
|
|
83
76
|
}
|
|
84
|
-
if (status.generation !== generation) {
|
|
85
|
-
clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
|
|
86
|
-
}
|
|
87
77
|
if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
|
|
88
78
|
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
|
|
89
79
|
}
|
|
@@ -107,6 +97,7 @@ function preflight(
|
|
|
107
97
|
);
|
|
108
98
|
}
|
|
109
99
|
}
|
|
100
|
+
return status.generation;
|
|
110
101
|
}
|
|
111
102
|
|
|
112
103
|
function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
|
|
@@ -201,7 +192,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
201
192
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
202
193
|
const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
|
|
203
194
|
const taskViewEffect = taskViewEffectForRequest(request);
|
|
204
|
-
preflight(statusDir,
|
|
195
|
+
const hostGeneration = preflight(statusDir, taskViewEffect);
|
|
205
196
|
const identityPath = path.join(statusDir, 'identity.json');
|
|
206
197
|
if (fs.existsSync(statusDir)) {
|
|
207
198
|
const identity = readSandboxControlIdentitySentinel(statusDir);
|
|
@@ -246,7 +237,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
246
237
|
try {
|
|
247
238
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
248
239
|
accepted = true;
|
|
249
|
-
response = readPublishedResponse(raw, request.id, channelDir, accepted,
|
|
240
|
+
response = readPublishedResponse(raw, request.id, channelDir, accepted, hostGeneration);
|
|
250
241
|
malformedResponseRaw = null;
|
|
251
242
|
} catch (error) {
|
|
252
243
|
if (!(error instanceof SyntaxError)) throw error;
|
|
@@ -268,7 +259,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
268
259
|
}
|
|
269
260
|
if (!accepted) {
|
|
270
261
|
try {
|
|
271
|
-
preflight(statusDir,
|
|
262
|
+
preflight(statusDir, taskViewEffect);
|
|
272
263
|
} catch (error) {
|
|
273
264
|
if (cancelPendingRequest(requestPath)) throw error;
|
|
274
265
|
clientError(
|
|
@@ -314,6 +305,9 @@ export function recoverSandboxControl(requestId: string, params: Readonly<{
|
|
|
314
305
|
const config = loadConfig();
|
|
315
306
|
const response = recoverSandboxControlFromHost(requestId, {
|
|
316
307
|
managedRoot: path.join(config.controlBase, config.project),
|
|
308
|
+
controlBase: config.controlBase,
|
|
309
|
+
project: config.project,
|
|
310
|
+
repoRoot: config.repoRoot,
|
|
317
311
|
timeoutMs: params.timeoutMs
|
|
318
312
|
});
|
|
319
313
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
@@ -402,72 +396,30 @@ export function recoverSandboxControlFromChannel(requestId: string, params: Read
|
|
|
402
396
|
);
|
|
403
397
|
}
|
|
404
398
|
|
|
405
|
-
function authority(params: { token?: string; generation?: string }): { token: string; generation: string } {
|
|
406
|
-
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
407
|
-
const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
408
|
-
if (!token || !generation) clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
|
|
409
|
-
return { token, generation };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
399
|
export function requestSandboxControl(params: Readonly<{
|
|
413
400
|
family: string; args: string[]; channelDir?: string; statusDir?: string;
|
|
414
|
-
|
|
401
|
+
timeoutMs?: number;
|
|
415
402
|
}>): SandboxControlResponse {
|
|
416
403
|
if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
417
|
-
if (params.family === 'task-create'
|
|
404
|
+
if (params.family === 'task-create') {
|
|
418
405
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
|
|
419
406
|
}
|
|
420
407
|
if (params.args[1] === 'complete') {
|
|
421
408
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
|
|
422
409
|
}
|
|
423
|
-
const auth = authority(params);
|
|
424
|
-
const issuedAt = Date.now();
|
|
425
410
|
const request: SandboxControlRequest = {
|
|
426
|
-
version:
|
|
427
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
411
|
+
version: 5, id: randomUUID(),
|
|
428
412
|
family: params.family as 'task-lifecycle', args: params.args
|
|
429
413
|
};
|
|
430
414
|
return exchangeSandboxControl(request, params);
|
|
431
415
|
}
|
|
432
416
|
|
|
433
|
-
export function requestSandboxAgentOperation(params: Readonly<{
|
|
434
|
-
agentClient: string;
|
|
435
|
-
operation: string;
|
|
436
|
-
payload: Record<string, unknown>;
|
|
437
|
-
channelDir?: string;
|
|
438
|
-
statusDir?: string;
|
|
439
|
-
token?: string;
|
|
440
|
-
generation?: string;
|
|
441
|
-
timeoutMs?: number;
|
|
442
|
-
}>): SandboxControlResponse {
|
|
443
|
-
if (!isAgentClientId(params.agentClient)) {
|
|
444
|
-
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
|
|
445
|
-
}
|
|
446
|
-
const auth = authority(params);
|
|
447
|
-
const issuedAt = Date.now();
|
|
448
|
-
const request: SandboxAgentClientRequest = {
|
|
449
|
-
version: 4,
|
|
450
|
-
id: randomUUID(),
|
|
451
|
-
...auth,
|
|
452
|
-
issuedAt,
|
|
453
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
454
|
-
family: 'agent-client',
|
|
455
|
-
agentClient: params.agentClient,
|
|
456
|
-
operation: params.operation,
|
|
457
|
-
payload: params.payload
|
|
458
|
-
};
|
|
459
|
-
return exchangeSandboxControl(request, params);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
417
|
export function requestSandboxTaskCreate(params: Readonly<{
|
|
463
418
|
candidate: TaskCreateCandidateV1; channelDir?: string; statusDir?: string;
|
|
464
|
-
|
|
419
|
+
timeoutMs?: number;
|
|
465
420
|
}>): SandboxControlResponse {
|
|
466
|
-
const auth = authority(params);
|
|
467
|
-
const issuedAt = Date.now();
|
|
468
421
|
const request: SandboxTaskCreateRequest = {
|
|
469
|
-
version:
|
|
470
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
422
|
+
version: 5, id: randomUUID(),
|
|
471
423
|
family: 'task-create', candidate: params.candidate,
|
|
472
424
|
};
|
|
473
425
|
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
@@ -33,8 +33,6 @@ import {
|
|
|
33
33
|
type TaskControlOperation
|
|
34
34
|
} from '../../task/control-authority.ts';
|
|
35
35
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
|
|
36
|
-
import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.ts';
|
|
37
|
-
import type { AgentClientSandboxControlOperation } from '../../agent-clients/adapter.ts';
|
|
38
36
|
|
|
39
37
|
export type SandboxControlExecutionResult = {
|
|
40
38
|
exitCode: number;
|
|
@@ -256,7 +254,6 @@ function taskCreateExecutionFailure(requestId: string, code: string, message: st
|
|
|
256
254
|
|
|
257
255
|
type ExecuteRequestOptions = Readonly<{
|
|
258
256
|
createTask?: typeof createTask;
|
|
259
|
-
agentClientOperation?: AgentClientSandboxControlOperation;
|
|
260
257
|
}>;
|
|
261
258
|
|
|
262
259
|
async function executeRequestInner(
|
|
@@ -279,10 +276,6 @@ async function executeRequestInner(
|
|
|
279
276
|
return taskCreateExecutionFailure(request.id, code, detail);
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
|
-
if (request.family === 'agent-client') {
|
|
283
|
-
validateAgentClientOperation(request, manifest);
|
|
284
|
-
return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
|
|
285
|
-
}
|
|
286
279
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
|
|
287
280
|
let operation: TaskControlOperation;
|
|
288
281
|
try {
|
|
@@ -346,9 +339,10 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
346
339
|
const gate = await waitForGate(nonce);
|
|
347
340
|
const gateOwner = gate.owner;
|
|
348
341
|
process.disconnect?.();
|
|
349
|
-
const
|
|
350
|
-
if (!
|
|
351
|
-
const manifest = readSandboxControlManifest(
|
|
342
|
+
const requestedManifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
343
|
+
if (!requestedManifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
344
|
+
const manifest = readSandboxControlManifest(requestedManifestPath);
|
|
345
|
+
const manifestPath = fs.realpathSync.native(requestedManifestPath);
|
|
352
346
|
const identity = validateSandboxControlIdentity({
|
|
353
347
|
publicStatusDir: manifest.publicStatusDir,
|
|
354
348
|
root: path.dirname(path.resolve(manifestPath)),
|
|
@@ -390,17 +384,17 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
390
384
|
let result: SandboxControlExecutionResult;
|
|
391
385
|
try {
|
|
392
386
|
assertSandboxControlExecutorAuthority(manifest, gateOwner);
|
|
393
|
-
|
|
387
|
+
const operation = request.family === 'task-lifecycle'
|
|
394
388
|
? (() => {
|
|
395
389
|
try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
|
|
396
390
|
})()
|
|
397
|
-
:
|
|
391
|
+
: null;
|
|
398
392
|
const context = createSandboxControlAuditContext(manifest, {
|
|
399
393
|
requestId: request.id,
|
|
400
394
|
family: request.family,
|
|
401
395
|
operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
|
|
402
396
|
? operation.request.intent
|
|
403
|
-
:
|
|
397
|
+
: null,
|
|
404
398
|
phase: 'started-committed',
|
|
405
399
|
outcome: 'in-progress'
|
|
406
400
|
});
|
|
@@ -24,20 +24,73 @@ function manifestDigest(manifest: SandboxControlManifest): string {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
function readManagedManifest(manifestPath: string): SandboxControlManifest {
|
|
28
|
+
try { return readSandboxControlManifest(manifestPath); } catch { throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID'); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function branchOnlyManifests(parentPath: string): SandboxControlManifest[] {
|
|
32
|
+
const manifests: SandboxControlManifest[] = [];
|
|
33
|
+
for (const identity of fs.readdirSync(parentPath, { withFileTypes: true })) {
|
|
34
|
+
if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name)) continue;
|
|
35
|
+
manifests.push(readManagedManifest(path.join(parentPath, identity.name, 'manifest.json')));
|
|
36
|
+
}
|
|
37
|
+
return manifests;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function taskBoundManifests(params: Readonly<{
|
|
41
|
+
tasksRoot: string;
|
|
42
|
+
controlBase: string;
|
|
43
|
+
project: string;
|
|
44
|
+
repoRoot: string;
|
|
45
|
+
}>): SandboxControlManifest[] {
|
|
46
|
+
assertDirectory(params.tasksRoot);
|
|
47
|
+
const canonicalManagedRoot = fs.realpathSync.native(path.dirname(params.tasksRoot));
|
|
48
|
+
const canonicalTasksRoot = fs.realpathSync.native(params.tasksRoot);
|
|
49
|
+
if (path.dirname(canonicalTasksRoot) !== canonicalManagedRoot) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
50
|
+
const canonicalRepoRoot = fs.realpathSync.native(params.repoRoot);
|
|
51
|
+
const manifests: SandboxControlManifest[] = [];
|
|
52
|
+
for (const task of fs.readdirSync(params.tasksRoot, { withFileTypes: true })) {
|
|
53
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(task.name)) continue;
|
|
54
|
+
if (!task.isDirectory() || task.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
55
|
+
const taskRoot = path.join(params.tasksRoot, task.name);
|
|
56
|
+
assertNoSymlinkPath(params.controlBase, taskRoot);
|
|
57
|
+
const canonicalTaskRoot = fs.realpathSync.native(taskRoot);
|
|
58
|
+
if (path.dirname(canonicalTaskRoot) !== canonicalTasksRoot) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
59
|
+
const manifestPath = path.join(taskRoot, 'manifest.json');
|
|
60
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
61
|
+
const manifest = readManagedManifest(manifestPath);
|
|
62
|
+
if (manifest.mode !== 'task-bound' || manifest.taskId !== task.name || manifest.project !== params.project
|
|
63
|
+
|| fs.realpathSync.native(manifest.repoRoot) !== canonicalRepoRoot
|
|
64
|
+
|| path.dirname(fs.realpathSync.native(manifestPath)) !== canonicalTaskRoot) {
|
|
65
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
38
66
|
}
|
|
67
|
+
manifests.push(manifest);
|
|
68
|
+
}
|
|
69
|
+
return manifests;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function managedManifestCandidates(params: Readonly<{
|
|
73
|
+
managedRoot: string;
|
|
74
|
+
controlBase?: string;
|
|
75
|
+
project?: string;
|
|
76
|
+
repoRoot?: string;
|
|
77
|
+
}>): SandboxControlManifest[] {
|
|
78
|
+
const managedRoot = params.managedRoot;
|
|
79
|
+
const { controlBase, project, repoRoot } = params;
|
|
80
|
+
if (controlBase && project && path.resolve(managedRoot) !== path.resolve(controlBase, project)) {
|
|
81
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
39
82
|
}
|
|
40
|
-
return
|
|
83
|
+
if (!fs.existsSync(managedRoot)) return [];
|
|
84
|
+
assertDirectory(managedRoot);
|
|
85
|
+
return fs.readdirSync(managedRoot, { withFileTypes: true })
|
|
86
|
+
.flatMap((parent) => {
|
|
87
|
+
const parentPath = path.join(managedRoot, parent.name);
|
|
88
|
+
if (parent.name === 'tasks' && controlBase && project && repoRoot) {
|
|
89
|
+
return taskBoundManifests({ tasksRoot: parentPath, controlBase, project, repoRoot });
|
|
90
|
+
}
|
|
91
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) return [];
|
|
92
|
+
return branchOnlyManifests(parentPath);
|
|
93
|
+
});
|
|
41
94
|
}
|
|
42
95
|
|
|
43
96
|
function assertDirectory(directory: string): void {
|
|
@@ -45,6 +98,17 @@ function assertDirectory(directory: string): void {
|
|
|
45
98
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
46
99
|
}
|
|
47
100
|
|
|
101
|
+
function assertNoSymlinkPath(root: string, target: string): void {
|
|
102
|
+
const relative = path.relative(root, target);
|
|
103
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
104
|
+
let current = path.resolve(root);
|
|
105
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
106
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
107
|
+
current = path.join(current, segment);
|
|
108
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
48
112
|
type AuditRecord = Record<string, unknown>;
|
|
49
113
|
|
|
50
114
|
function readAudit(manifest: SandboxControlManifest): AuditRecord[] {
|
|
@@ -77,12 +141,16 @@ function auditProof(manifest: SandboxControlManifest, requestId: string): AuditR
|
|
|
77
141
|
return related;
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
function publishedEvidence(records: AuditRecord[],
|
|
144
|
+
function publishedEvidence(records: AuditRecord[], manifest: SandboxControlManifest): AuditRecord | null {
|
|
81
145
|
const publications = records.filter((record) => record.event === 'executor-result-published');
|
|
82
146
|
let selected: AuditRecord | null = null;
|
|
83
147
|
let selectedSummary: string | null = null;
|
|
148
|
+
const digest = manifestDigest(manifest);
|
|
84
149
|
for (const record of publications) {
|
|
85
|
-
if (record.version !== 2 || record.
|
|
150
|
+
if (record.version !== 2 || record.generation !== manifest.generation || record.identityDigest !== digest
|
|
151
|
+
|| typeof record.requestFamily !== 'string'
|
|
152
|
+
|| !['task-lifecycle', 'task-create'].includes(record.requestFamily)
|
|
153
|
+
|| (record.sandboxTaskId !== null && typeof record.sandboxTaskId !== 'string')
|
|
86
154
|
|| !Number.isSafeInteger(record.exitCode)
|
|
87
155
|
|| !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
|
|
88
156
|
|| typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
|
|
@@ -90,7 +158,7 @@ function publishedEvidence(records: AuditRecord[], generation: string): AuditRec
|
|
|
90
158
|
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
91
159
|
}
|
|
92
160
|
const summary = JSON.stringify([
|
|
93
|
-
record.requestFamily
|
|
161
|
+
record.requestFamily, record.sandboxTaskId, record.generation, record.identityDigest, record.exitCode,
|
|
94
162
|
record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
|
|
95
163
|
]);
|
|
96
164
|
if (selectedSummary !== null && summary !== selectedSummary) {
|
|
@@ -99,6 +167,9 @@ function publishedEvidence(records: AuditRecord[], generation: string): AuditRec
|
|
|
99
167
|
selected ??= record;
|
|
100
168
|
selectedSummary ??= summary;
|
|
101
169
|
}
|
|
170
|
+
if (selected && selected.sandboxTaskId !== manifest.taskId) {
|
|
171
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
172
|
+
}
|
|
102
173
|
return selected;
|
|
103
174
|
}
|
|
104
175
|
|
|
@@ -118,7 +189,7 @@ function verifiedResponse(manifest: SandboxControlManifest, requestId: string, r
|
|
|
118
189
|
if (!['completed', 'rejected'].includes(response.phase)) throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
119
190
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return unknown(requestId);
|
|
120
191
|
const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
|
|
121
|
-
const published = publishedEvidence(records, manifest
|
|
192
|
+
const published = publishedEvidence(records, manifest);
|
|
122
193
|
if (!published) return unknown(requestId);
|
|
123
194
|
let evidence = null;
|
|
124
195
|
if (fs.existsSync(evidencePath)) {
|
|
@@ -167,17 +238,19 @@ function verifiedResponse(manifest: SandboxControlManifest, requestId: string, r
|
|
|
167
238
|
}
|
|
168
239
|
|
|
169
240
|
export function recoverSandboxControlFromHost(requestId: string, params: Readonly<{
|
|
170
|
-
managedRoot: string;
|
|
241
|
+
managedRoot: string;
|
|
242
|
+
controlBase?: string;
|
|
243
|
+
project?: string;
|
|
244
|
+
repoRoot?: string;
|
|
245
|
+
timeoutMs?: number;
|
|
171
246
|
}>): SandboxControlResponse {
|
|
172
247
|
if (!REQUEST_ID.test(requestId)) throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
|
|
173
248
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
174
249
|
let candidates: Array<{ manifest: SandboxControlManifest; records: AuditRecord[] }> = [];
|
|
175
250
|
while (Date.now() < deadline) {
|
|
176
251
|
candidates = [];
|
|
177
|
-
for (const
|
|
178
|
-
|
|
179
|
-
try { manifest = readSandboxControlManifest(manifestPath); } catch { throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID'); }
|
|
180
|
-
if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
|
|
252
|
+
for (const manifest of managedManifestCandidates(params)) {
|
|
253
|
+
if (manifest.project !== (params.project ?? path.basename(path.resolve(params.managedRoot))) || !manifest.generation) {
|
|
181
254
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
182
255
|
}
|
|
183
256
|
const records = auditProof(manifest, requestId);
|
|
@@ -1155,30 +1155,32 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1155
1155
|
|| !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
1156
1156
|
const root = path.dirname(path.resolve(manifestPath));
|
|
1157
1157
|
const canonicalRoot = fs.realpathSync.native(root);
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1158
|
+
const resolveControlChildPath = (candidatePath: string, name: string): string | null => {
|
|
1159
|
+
if (path.isAbsolute(candidatePath) || candidatePath !== name) return null;
|
|
1160
|
+
const resolved = path.resolve(root, candidatePath);
|
|
1161
1161
|
const expected = path.join(canonicalRoot, name);
|
|
1162
1162
|
try {
|
|
1163
|
-
|
|
1163
|
+
const stat = fs.lstatSync(resolved);
|
|
1164
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
1165
|
+
return fs.realpathSync.native(resolved) === expected ? expected : null;
|
|
1164
1166
|
} catch (error) {
|
|
1165
1167
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
1166
1168
|
try {
|
|
1167
|
-
return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot;
|
|
1169
|
+
return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot ? expected : null;
|
|
1168
1170
|
} catch (parentError) {
|
|
1169
1171
|
if ((parentError as NodeJS.ErrnoException).code !== 'ENOENT') throw parentError;
|
|
1170
|
-
return
|
|
1172
|
+
return null;
|
|
1171
1173
|
}
|
|
1172
1174
|
}
|
|
1173
1175
|
};
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (!matchesControlChildPath(candidate.runtimeDir, 'runtime')) {
|
|
1176
|
+
const channelDir = resolveControlChildPath(candidate.channelDir, 'channel');
|
|
1177
|
+
const publicStatusDir = resolveControlChildPath(candidate.publicStatusDir, 'public');
|
|
1178
|
+
const processingDir = resolveControlChildPath(candidate.processingDir, 'processing');
|
|
1179
|
+
const runtimeDir = resolveControlChildPath(candidate.runtimeDir, 'runtime');
|
|
1180
|
+
if (!channelDir || !publicStatusDir || !processingDir) {
|
|
1180
1181
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
1181
1182
|
}
|
|
1183
|
+
if (!runtimeDir) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
1182
1184
|
const normalized: SandboxControlManifest = {
|
|
1183
1185
|
engine: candidate.engine,
|
|
1184
1186
|
repoRoot: candidate.repoRoot,
|
|
@@ -1196,10 +1198,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
|
|
|
1196
1198
|
token: candidate.token,
|
|
1197
1199
|
generation: candidate.generation,
|
|
1198
1200
|
controlRootId: candidate.controlRootId,
|
|
1199
|
-
channelDir
|
|
1200
|
-
publicStatusDir
|
|
1201
|
-
processingDir
|
|
1202
|
-
runtimeDir
|
|
1201
|
+
channelDir,
|
|
1202
|
+
publicStatusDir,
|
|
1203
|
+
processingDir,
|
|
1204
|
+
runtimeDir,
|
|
1203
1205
|
};
|
|
1204
1206
|
return normalized;
|
|
1205
1207
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
3
|
-
import { isAgentClientId } from '../../agent-clients/types.ts';
|
|
4
3
|
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
5
4
|
import type { SandboxAuthorityEvidenceV1 } from '../engines/authority.ts';
|
|
6
5
|
import type { SandboxTaskView } from './task-view.ts';
|
|
@@ -10,11 +9,10 @@ export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
|
|
|
10
9
|
export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
11
10
|
export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
|
|
12
11
|
export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
|
|
13
|
-
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
14
12
|
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
15
13
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
16
14
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
17
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'
|
|
15
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'] as const;
|
|
18
16
|
export type SandboxControlTimingPolicy = Readonly<{
|
|
19
17
|
controlTickMs: number;
|
|
20
18
|
parkedBindingInitialMs: number;
|
|
@@ -55,22 +53,14 @@ export type SandboxControlExecutorGateV2 = Readonly<{
|
|
|
55
53
|
owner: SandboxControlBrokerOwner;
|
|
56
54
|
requestId: string;
|
|
57
55
|
}>;
|
|
58
|
-
type RequestBase = Readonly<{
|
|
59
|
-
version: 4; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
|
|
60
|
-
}>;
|
|
56
|
+
type RequestBase = Readonly<{ version: 5; id: string }>;
|
|
61
57
|
export type SandboxTaskCommandRequest = RequestBase & Readonly<{
|
|
62
58
|
family: 'task-lifecycle'; args: string[];
|
|
63
59
|
}>;
|
|
64
60
|
export type SandboxTaskCreateRequest = RequestBase & Readonly<{
|
|
65
61
|
family: 'task-create'; candidate: TaskCreateCandidateV1;
|
|
66
62
|
}>;
|
|
67
|
-
export type
|
|
68
|
-
family: 'agent-client';
|
|
69
|
-
agentClient: string;
|
|
70
|
-
operation: string;
|
|
71
|
-
payload: Record<string, unknown>;
|
|
72
|
-
}>;
|
|
73
|
-
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest | SandboxAgentClientRequest;
|
|
63
|
+
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest;
|
|
74
64
|
export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
|
|
75
65
|
export type SandboxControlResultEvidence = Readonly<{
|
|
76
66
|
version: 1;
|
|
@@ -220,53 +210,28 @@ export function isSandboxControlFamily(value: string): value is SandboxControlFa
|
|
|
220
210
|
|
|
221
211
|
export function validateSandboxControlRequest(
|
|
222
212
|
value: unknown,
|
|
223
|
-
manifest: SandboxControlManifest
|
|
224
|
-
options: { now?: number } = {}
|
|
213
|
+
manifest: SandboxControlManifest
|
|
225
214
|
): SandboxControlRequest {
|
|
226
215
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
227
216
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
228
217
|
}
|
|
229
218
|
const request = value as Record<string, unknown>;
|
|
230
219
|
if (
|
|
231
|
-
request.version !==
|
|
232
|
-
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
220
|
+
request.version !== 5 || typeof request.id !== 'string'
|
|
221
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id)
|
|
233
222
|
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
234
|
-
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt)
|
|
235
223
|
) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
236
|
-
if (request.generation !== manifest.generation) {
|
|
237
|
-
fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
|
|
238
|
-
}
|
|
239
|
-
const issuedAt = request.issuedAt as number;
|
|
240
|
-
const expiresAt = request.expiresAt as number;
|
|
241
|
-
const now = options.now ?? Date.now();
|
|
242
|
-
if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
243
|
-
fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
|
|
244
|
-
}
|
|
245
|
-
if (now > expiresAt) fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
|
|
246
224
|
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
247
225
|
fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
|
|
248
226
|
}
|
|
249
227
|
if (request.family === 'task-create') {
|
|
250
|
-
const expected = ['candidate', '
|
|
228
|
+
const expected = ['candidate', 'family', 'id', 'version'];
|
|
251
229
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
|
|
252
230
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
253
231
|
}
|
|
254
232
|
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) } as SandboxTaskCreateRequest;
|
|
255
233
|
}
|
|
256
|
-
|
|
257
|
-
const expected = ['agentClient', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'payload', 'token', 'version'];
|
|
258
|
-
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
259
|
-
|| !isAgentClientId(request.agentClient)
|
|
260
|
-
|| typeof request.operation !== 'string' || !/^[a-z][a-z0-9.-]{0,63}$/u.test(request.operation)
|
|
261
|
-
|| !request.payload || typeof request.payload !== 'object' || Array.isArray(request.payload)) {
|
|
262
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client request schema is invalid');
|
|
263
|
-
}
|
|
264
|
-
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
265
|
-
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot coordinate agent clients');
|
|
266
|
-
}
|
|
267
|
-
return request as SandboxAgentClientRequest;
|
|
268
|
-
}
|
|
269
|
-
const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
234
|
+
const expected = ['args', 'family', 'id', 'version'];
|
|
270
235
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
271
236
|
|| !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
|
|
272
237
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
@@ -284,7 +249,7 @@ export function validateSandboxControlRequest(
|
|
|
284
249
|
}
|
|
285
250
|
|
|
286
251
|
export function bindSandboxControlTask(request: SandboxControlRequest, taskId: string): string[] {
|
|
287
|
-
if (request.family === 'task-create'
|
|
252
|
+
if (request.family === 'task-create') {
|
|
288
253
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
|
|
289
254
|
}
|
|
290
255
|
if (request.args.length === 0) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
|
|
@@ -300,7 +265,6 @@ export function controlError(error: unknown): SandboxControlError {
|
|
|
300
265
|
const retryable = new Set([
|
|
301
266
|
'SANDBOX_CONTROL_BUSY',
|
|
302
267
|
'SANDBOX_CONTROL_HANDOFF_ACTIVE',
|
|
303
|
-
'SANDBOX_CONTROL_REQUEST_EXPIRED',
|
|
304
268
|
'SANDBOX_WORKTREE_BINDING_LOST'
|
|
305
269
|
]).has(code);
|
|
306
270
|
return { code, message, retryable };
|
|
@@ -72,7 +72,6 @@ import {
|
|
|
72
72
|
import type { BrokerOwner } from './lifecycle.ts';
|
|
73
73
|
import { nextSandboxControlBackoff } from './timing.ts';
|
|
74
74
|
import { readTaskFinalizationReceipt } from '../../task/finalization.ts';
|
|
75
|
-
import { recoverAgentClientOperation, validateAgentClientOperation } from './agent-operation.ts';
|
|
76
75
|
import { validateSandboxControlIdentity } from './identity-sentinel.ts';
|
|
77
76
|
import {
|
|
78
77
|
mergeSandboxTaskView,
|
|
@@ -100,8 +99,6 @@ function appendBrokerAudit(
|
|
|
100
99
|
|
|
101
100
|
function operationKey(request: SandboxControlRequest): string | null {
|
|
102
101
|
if (request.family === 'task-create') return 'create';
|
|
103
|
-
if (request.family === 'agent-client') return request.operation;
|
|
104
|
-
if (request.family !== 'task-lifecycle') return null;
|
|
105
102
|
try {
|
|
106
103
|
const operation = parseTaskControlOperation(request.family, request.args);
|
|
107
104
|
return operation.family === 'task-lifecycle' ? operation.request.intent : null;
|
|
@@ -299,7 +296,7 @@ async function readRecoveryDomain(
|
|
|
299
296
|
if (!operation) return { domain: null };
|
|
300
297
|
const taskRef = 'args' in request ? request.args[0] ?? null : manifest.taskId;
|
|
301
298
|
const output = parseControlOutput(payloadOutput);
|
|
302
|
-
if (!taskRef && operation.family !== 'task-create'
|
|
299
|
+
if (!taskRef && operation.family !== 'task-create') {
|
|
303
300
|
return { domain: null };
|
|
304
301
|
}
|
|
305
302
|
|
|
@@ -327,14 +324,6 @@ async function readRecoveryDomain(
|
|
|
327
324
|
if (operation.family === 'task-create') {
|
|
328
325
|
return { domain: taskCreateDomainEvidence(manifest, output) };
|
|
329
326
|
}
|
|
330
|
-
if (operation.family === 'agent-client' && request.family === 'agent-client') {
|
|
331
|
-
return { domain: recoverAgentClientOperation({
|
|
332
|
-
request,
|
|
333
|
-
manifest,
|
|
334
|
-
manifestPath,
|
|
335
|
-
stdout: payloadOutput
|
|
336
|
-
}) };
|
|
337
|
-
}
|
|
338
327
|
return { domain: null };
|
|
339
328
|
}
|
|
340
329
|
|
|
@@ -568,10 +557,7 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
|
|
|
568
557
|
let terminalReconciled = false;
|
|
569
558
|
try {
|
|
570
559
|
const rawRequest = JSON.parse(fs.readFileSync(path.join(processingDirectory, 'request.json'), 'utf8')) as Record<string, unknown>;
|
|
571
|
-
request = validateSandboxControlRequest(rawRequest, manifest
|
|
572
|
-
now: typeof rawRequest.issuedAt === 'number' ? rawRequest.issuedAt : undefined
|
|
573
|
-
});
|
|
574
|
-
if (request.family === 'agent-client') validateAgentClientOperation(request, manifest);
|
|
560
|
+
request = validateSandboxControlRequest(rawRequest, manifest);
|
|
575
561
|
} catch {
|
|
576
562
|
// Missing or malformed request evidence remains fail-closed below.
|
|
577
563
|
}
|
|
@@ -1006,7 +992,6 @@ export async function serveSandboxControl(
|
|
|
1006
992
|
if (reasonCode) throw new Error(reasonCode);
|
|
1007
993
|
if (active) throw new Error('SANDBOX_CONTROL_BUSY');
|
|
1008
994
|
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
|
|
1009
|
-
if (request.family === 'agent-client') validateAgentClientOperation(request, manifest);
|
|
1010
995
|
validatedRequest = request;
|
|
1011
996
|
criticalRequestPhase(manifest, request, 'validated', 'in-progress');
|
|
1012
997
|
appendBrokerAudit(manifest, 'request-validated', {
|