@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.
Files changed (89) hide show
  1. package/dist/bin/internal-cli.js +2 -7
  2. package/dist/lib/agent-clients/adapter.js +0 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
  6. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
  7. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  8. package/dist/lib/internal/cli-route-inventory.js +0 -1
  9. package/dist/lib/internal/task-finalization.js +6 -4
  10. package/dist/lib/internal/task-lifecycle.js +3 -1
  11. package/dist/lib/internal/task-operation-registry.js +1 -3
  12. package/dist/lib/internal/task-validate.js +14 -124
  13. package/dist/lib/platform/release-notes.js +2 -2
  14. package/dist/lib/sandbox/commands/control-status.js +0 -1
  15. package/dist/lib/sandbox/commands/create.js +5 -4
  16. package/dist/lib/sandbox/config.js +12 -0
  17. package/dist/lib/sandbox/control/audit.js +1 -2
  18. package/dist/lib/sandbox/control/client.js +12 -47
  19. package/dist/lib/sandbox/control/executor.js +6 -10
  20. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  21. package/dist/lib/sandbox/control/lifecycle.js +20 -16
  22. package/dist/lib/sandbox/control/protocol.js +8 -36
  23. package/dist/lib/sandbox/control/server.js +2 -21
  24. package/dist/lib/sandbox/environment.js +7 -8
  25. package/dist/lib/sandbox/mounts.js +71 -59
  26. package/dist/lib/sandbox/recovery.js +2 -3
  27. package/dist/lib/sandbox/removal.js +19 -4
  28. package/dist/lib/sandbox/workspace-view.js +41 -4
  29. package/dist/lib/task/control-authority.js +39 -17
  30. package/dist/lib/task/control-recovery.js +1 -2
  31. package/dist/lib/task/finalization.js +13 -2
  32. package/dist/lib/task/lifecycle.js +92 -33
  33. package/dist/lifecycle-build-manifest.json +3 -3
  34. package/lib/agent-clients/adapter.ts +0 -27
  35. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  36. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  37. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  38. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  39. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
  40. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  41. package/lib/agent-clients/adapters/codex.ts +0 -2
  42. package/lib/internal/cli-route-inventory.ts +0 -1
  43. package/lib/internal/task-finalization.ts +6 -4
  44. package/lib/internal/task-lifecycle.ts +3 -1
  45. package/lib/internal/task-operation-registry.ts +1 -3
  46. package/lib/internal/task-validate.ts +13 -127
  47. package/lib/platform/release-notes.ts +2 -2
  48. package/lib/sandbox/commands/control-status.ts +0 -1
  49. package/lib/sandbox/commands/create.ts +5 -4
  50. package/lib/sandbox/config.ts +13 -0
  51. package/lib/sandbox/control/audit.ts +1 -2
  52. package/lib/sandbox/control/client.ts +13 -61
  53. package/lib/sandbox/control/executor.ts +7 -13
  54. package/lib/sandbox/control/host-recovery.ts +94 -21
  55. package/lib/sandbox/control/lifecycle.ts +18 -16
  56. package/lib/sandbox/control/protocol.ts +9 -45
  57. package/lib/sandbox/control/server.ts +2 -17
  58. package/lib/sandbox/environment.ts +7 -8
  59. package/lib/sandbox/mounts.ts +92 -75
  60. package/lib/sandbox/recovery.ts +2 -3
  61. package/lib/sandbox/removal.ts +16 -4
  62. package/lib/sandbox/workspace-view.ts +40 -5
  63. package/lib/task/control-authority.ts +40 -26
  64. package/lib/task/control-recovery.ts +2 -3
  65. package/lib/task/finalization.ts +15 -2
  66. package/lib/task/lifecycle.ts +103 -31
  67. package/package.json +1 -1
  68. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  69. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  70. package/templates/.agents/rules/task-short-id.en.md +18 -12
  71. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  72. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  73. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  74. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  75. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  76. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  77. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  78. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  79. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  80. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  81. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  82. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  83. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  84. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  85. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  86. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  87. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  88. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  89. 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
- ): void {
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, request.generation, taskViewEffect);
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, request.generation);
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, request.generation, taskViewEffect);
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
- token?: string; generation?: string; timeoutMs?: number;
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' || params.family === 'agent-client') {
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: 4, id: randomUUID(), ...auth, issuedAt,
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
- token?: string; generation?: string; timeoutMs?: number;
419
+ timeoutMs?: number;
465
420
  }>): SandboxControlResponse {
466
- const auth = authority(params);
467
- const issuedAt = Date.now();
468
421
  const request: SandboxTaskCreateRequest = {
469
- version: 4, id: randomUUID(), ...auth, issuedAt,
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 manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
350
- if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
351
- const manifest = readSandboxControlManifest(manifestPath);
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
- const operation = request.family === 'task-lifecycle'
387
+ const operation = request.family === 'task-lifecycle'
394
388
  ? (() => {
395
389
  try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
396
390
  })()
397
- : request.family === 'agent-client' ? request.operation : null;
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
- : request.family === 'agent-client' ? request.operation : null,
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 managedManifestPaths(managedRoot: string): string[] {
28
- if (!fs.existsSync(managedRoot)) return [];
29
- const rootStat = fs.lstatSync(managedRoot);
30
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
31
- const result: string[] = [];
32
- for (const container of fs.readdirSync(managedRoot, { withFileTypes: true })) {
33
- if (!container.isDirectory() || container.isSymbolicLink()) continue;
34
- const containerPath = path.join(managedRoot, container.name);
35
- for (const identity of fs.readdirSync(containerPath, { withFileTypes: true })) {
36
- if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name)) continue;
37
- result.push(path.join(containerPath, identity.name, 'manifest.json'));
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 result;
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[], generation: string): AuditRecord | null {
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.requestGeneration !== generation
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 ?? null, record.requestGeneration, record.exitCode,
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.generation);
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; timeoutMs?: number;
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 manifestPath of managedManifestPaths(params.managedRoot)) {
178
- let manifest: SandboxControlManifest;
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 matchesControlChildPath = (candidatePath: string, name: string): boolean => {
1159
- const resolved = path.resolve(candidatePath);
1160
- if (path.basename(resolved) !== name) return false;
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
- return fs.realpathSync.native(resolved) === expected;
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 false;
1172
+ return null;
1171
1173
  }
1172
1174
  }
1173
1175
  };
1174
- if (!matchesControlChildPath(candidate.channelDir, 'channel')
1175
- || !matchesControlChildPath(candidate.publicStatusDir, 'public')
1176
- || !matchesControlChildPath(candidate.processingDir, 'processing')) {
1177
- throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
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: candidate.channelDir,
1200
- publicStatusDir: candidate.publicStatusDir,
1201
- processingDir: candidate.processingDir,
1202
- runtimeDir: candidate.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', 'agent-client'] as const;
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 SandboxAgentClientRequest = RequestBase & Readonly<{
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 !== 4 || typeof request.id !== 'string'
232
- || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
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', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
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
- if (request.family === 'agent-client') {
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' || request.family === 'agent-client') {
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' && operation.family !== 'agent-client') {
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', {