@fitlab-ai/agent-infra 0.11.13 → 0.11.15

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 (115) 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 +7 -5
  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/pr-change-report.js +10 -3
  14. package/dist/lib/platform/release-notes.js +2 -2
  15. package/dist/lib/process-data/index.js +15 -4
  16. package/dist/lib/sandbox/commands/control-status.js +0 -1
  17. package/dist/lib/sandbox/commands/create.js +5 -4
  18. package/dist/lib/sandbox/config.js +12 -0
  19. package/dist/lib/sandbox/control/audit.js +1 -2
  20. package/dist/lib/sandbox/control/client.js +12 -47
  21. package/dist/lib/sandbox/control/executor.js +6 -10
  22. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  23. package/dist/lib/sandbox/control/lifecycle.js +22 -37
  24. package/dist/lib/sandbox/control/protocol.js +8 -36
  25. package/dist/lib/sandbox/control/server.js +2 -21
  26. package/dist/lib/sandbox/environment.js +7 -8
  27. package/dist/lib/sandbox/mounts.js +71 -59
  28. package/dist/lib/sandbox/recovery.js +2 -3
  29. package/dist/lib/sandbox/removal.js +19 -4
  30. package/dist/lib/sandbox/workspace-identity.js +30 -15
  31. package/dist/lib/sandbox/workspace-view.js +41 -4
  32. package/dist/lib/task/control-authority.js +39 -17
  33. package/dist/lib/task/control-recovery.js +1 -2
  34. package/dist/lib/task/finalization.js +13 -2
  35. package/dist/lib/task/lifecycle.js +122 -35
  36. package/dist/lib/task/short-id.js +2 -2
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapter.ts +0 -27
  39. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  40. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  41. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  42. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  43. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
  44. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  45. package/lib/agent-clients/adapters/codex.ts +0 -2
  46. package/lib/internal/cli-route-inventory.ts +0 -1
  47. package/lib/internal/task-finalization.ts +8 -6
  48. package/lib/internal/task-lifecycle.ts +3 -1
  49. package/lib/internal/task-operation-registry.ts +1 -3
  50. package/lib/internal/task-validate.ts +13 -127
  51. package/lib/platform/pr-change-report.ts +9 -3
  52. package/lib/platform/release-notes.ts +2 -2
  53. package/lib/process-data/index.ts +15 -4
  54. package/lib/sandbox/commands/control-status.ts +0 -1
  55. package/lib/sandbox/commands/create.ts +5 -4
  56. package/lib/sandbox/config.ts +13 -0
  57. package/lib/sandbox/control/audit.ts +1 -2
  58. package/lib/sandbox/control/client.ts +13 -61
  59. package/lib/sandbox/control/executor.ts +7 -13
  60. package/lib/sandbox/control/host-recovery.ts +94 -21
  61. package/lib/sandbox/control/lifecycle.ts +22 -37
  62. package/lib/sandbox/control/protocol.ts +9 -45
  63. package/lib/sandbox/control/server.ts +2 -17
  64. package/lib/sandbox/environment.ts +7 -8
  65. package/lib/sandbox/mounts.ts +92 -75
  66. package/lib/sandbox/recovery.ts +2 -3
  67. package/lib/sandbox/removal.ts +16 -4
  68. package/lib/sandbox/workspace-identity.ts +36 -13
  69. package/lib/sandbox/workspace-view.ts +40 -5
  70. package/lib/task/control-authority.ts +40 -26
  71. package/lib/task/control-recovery.ts +2 -3
  72. package/lib/task/finalization.ts +15 -2
  73. package/lib/task/lifecycle.ts +135 -33
  74. package/lib/task/short-id.ts +5 -2
  75. package/package.json +1 -1
  76. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  77. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  78. package/templates/.agents/rules/task-short-id.en.md +18 -12
  79. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  80. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
  81. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
  82. package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
  83. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
  84. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  85. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  86. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
  87. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
  88. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
  89. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
  90. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  91. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  92. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  93. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  94. package/templates/.agents/skills/watch-pr/SKILL.en.md +19 -2
  95. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
  96. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
  97. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
  98. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  99. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  100. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  101. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  102. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  103. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  104. package/dist/lib/runtime/agent-runtime.js +0 -33
  105. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  106. package/dist/lib/task/runtime-paths.js +0 -10
  107. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  108. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  109. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  110. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  111. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  112. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  113. package/lib/runtime/agent-runtime.ts +0 -41
  114. package/lib/sandbox/control/agent-operation.ts +0 -49
  115. package/lib/task/runtime-paths.ts +0 -12
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
5
- import { isAgentClientId } from '../../agent-clients/types.js';
4
+ import { SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
6
5
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.js';
7
6
  import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
8
7
  import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
@@ -29,7 +28,7 @@ function sleep(milliseconds) {
29
28
  function clientError(code, message, retryable, accepted = false, requestId = null) {
30
29
  throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted, requestId);
31
30
  }
32
- function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(), env = process.env) {
31
+ function preflight(statusDir, taskViewEffect = null, now = Date.now(), env = process.env) {
33
32
  if (fs.existsSync(statusDir)) {
34
33
  let identity;
35
34
  try {
@@ -41,9 +40,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
41
40
  : 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
42
41
  clientError(code, 'sandbox control identity is missing or invalid', false);
43
42
  }
44
- if (identity.generation !== generation) {
45
- clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
46
- }
47
43
  if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
48
44
  clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
49
45
  }
@@ -55,9 +51,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
55
51
  catch {
56
52
  clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
57
53
  }
58
- if (status.generation !== generation) {
59
- clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
60
- }
61
54
  if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
62
55
  clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
63
56
  }
@@ -79,6 +72,7 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
79
72
  clientError(access.reasonCode ?? 'SANDBOX_TASK_VIEW_DENIED', access.message ?? 'task view denied', false);
80
73
  }
81
74
  }
75
+ return status.generation;
82
76
  }
83
77
  function taskViewEffectForRequest(request) {
84
78
  if (request.family === 'task-lifecycle')
@@ -165,7 +159,7 @@ function exchangeSandboxControl(request, params) {
165
159
  const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
166
160
  const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
167
161
  const taskViewEffect = taskViewEffectForRequest(request);
168
- preflight(statusDir, request.generation, taskViewEffect);
162
+ const hostGeneration = preflight(statusDir, taskViewEffect);
169
163
  const identityPath = path.join(statusDir, 'identity.json');
170
164
  if (fs.existsSync(statusDir)) {
171
165
  const identity = readSandboxControlIdentitySentinel(statusDir);
@@ -213,7 +207,7 @@ function exchangeSandboxControl(request, params) {
213
207
  try {
214
208
  // The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
215
209
  accepted = true;
216
- response = readPublishedResponse(raw, request.id, channelDir, accepted, request.generation);
210
+ response = readPublishedResponse(raw, request.id, channelDir, accepted, hostGeneration);
217
211
  malformedResponseRaw = null;
218
212
  }
219
213
  catch (error) {
@@ -239,7 +233,7 @@ function exchangeSandboxControl(request, params) {
239
233
  }
240
234
  if (!accepted) {
241
235
  try {
242
- preflight(statusDir, request.generation, taskViewEffect);
236
+ preflight(statusDir, taskViewEffect);
243
237
  }
244
238
  catch (error) {
245
239
  if (cancelPendingRequest(requestPath))
@@ -266,6 +260,9 @@ export function recoverSandboxControl(requestId, params = {}) {
266
260
  const config = loadConfig();
267
261
  const response = recoverSandboxControlFromHost(requestId, {
268
262
  managedRoot: path.join(config.controlBase, config.project),
263
+ controlBase: config.controlBase,
264
+ project: config.project,
265
+ repoRoot: config.repoRoot,
269
266
  timeoutMs: params.timeoutMs
270
267
  });
271
268
  if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
@@ -352,56 +349,24 @@ export function recoverSandboxControlFromChannel(requestId, params) {
352
349
  }
353
350
  clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request did not produce a final result; inspect domain state before retrying', false, accepted, requestId);
354
351
  }
355
- function authority(params) {
356
- const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
357
- const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
358
- if (!token || !generation)
359
- clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
360
- return { token, generation };
361
- }
362
352
  export function requestSandboxControl(params) {
363
353
  if (!isSandboxControlFamily(params.family))
364
354
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
365
- if (params.family === 'task-create' || params.family === 'agent-client') {
355
+ if (params.family === 'task-create') {
366
356
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
367
357
  }
368
358
  if (params.args[1] === 'complete') {
369
359
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
370
360
  }
371
- const auth = authority(params);
372
- const issuedAt = Date.now();
373
361
  const request = {
374
- version: 4, id: randomUUID(), ...auth, issuedAt,
375
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
362
+ version: 5, id: randomUUID(),
376
363
  family: params.family, args: params.args
377
364
  };
378
365
  return exchangeSandboxControl(request, params);
379
366
  }
380
- export function requestSandboxAgentOperation(params) {
381
- if (!isAgentClientId(params.agentClient)) {
382
- clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
383
- }
384
- const auth = authority(params);
385
- const issuedAt = Date.now();
386
- const request = {
387
- version: 4,
388
- id: randomUUID(),
389
- ...auth,
390
- issuedAt,
391
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
392
- family: 'agent-client',
393
- agentClient: params.agentClient,
394
- operation: params.operation,
395
- payload: params.payload
396
- };
397
- return exchangeSandboxControl(request, params);
398
- }
399
367
  export function requestSandboxTaskCreate(params) {
400
- const auth = authority(params);
401
- const issuedAt = Date.now();
402
368
  const request = {
403
- version: 4, id: randomUUID(), ...auth, issuedAt,
404
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
369
+ version: 5, id: randomUUID(),
405
370
  family: 'task-create', candidate: params.candidate,
406
371
  };
407
372
  return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
@@ -10,7 +10,6 @@ import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSan
10
10
  import { validateSandboxControlIdentity } from './identity-sentinel.js';
11
11
  import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../../task/control-authority.js';
12
12
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
13
- import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.js';
14
13
  function sameBrokerOwner(left, right) {
15
14
  return left.version === right.version
16
15
  && left.pid === right.pid
@@ -206,10 +205,6 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
206
205
  return taskCreateExecutionFailure(request.id, code, detail);
207
206
  }
208
207
  }
209
- if (request.family === 'agent-client') {
210
- validateAgentClientOperation(request, manifest);
211
- return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
212
- }
213
208
  const boundArgs = bindSandboxControlTask(request, manifest.taskId);
214
209
  let operation;
215
210
  try {
@@ -269,10 +264,11 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
269
264
  const gate = await waitForGate(nonce);
270
265
  const gateOwner = gate.owner;
271
266
  process.disconnect?.();
272
- const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
273
- if (!manifestPath)
267
+ const requestedManifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
268
+ if (!requestedManifestPath)
274
269
  throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
275
- const manifest = readSandboxControlManifest(manifestPath);
270
+ const manifest = readSandboxControlManifest(requestedManifestPath);
271
+ const manifestPath = fs.realpathSync.native(requestedManifestPath);
276
272
  const identity = validateSandboxControlIdentity({
277
273
  publicStatusDir: manifest.publicStatusDir,
278
274
  root: path.dirname(path.resolve(manifestPath)),
@@ -324,13 +320,13 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
324
320
  return null;
325
321
  }
326
322
  })()
327
- : request.family === 'agent-client' ? request.operation : null;
323
+ : null;
328
324
  const context = createSandboxControlAuditContext(manifest, {
329
325
  requestId: request.id,
330
326
  family: request.family,
331
327
  operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
332
328
  ? operation.request.intent
333
- : request.family === 'agent-client' ? request.operation : null,
329
+ : null,
334
330
  phase: 'started-committed',
335
331
  outcome: 'in-progress'
336
332
  });
@@ -19,30 +19,92 @@ function manifestDigest(manifest) {
19
19
  generation: manifest.generation, controlRootId: manifest.controlRootId
20
20
  });
21
21
  }
22
- function managedManifestPaths(managedRoot) {
23
- if (!fs.existsSync(managedRoot))
24
- return [];
25
- const rootStat = fs.lstatSync(managedRoot);
26
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
22
+ function readManagedManifest(manifestPath) {
23
+ try {
24
+ return readSandboxControlManifest(manifestPath);
25
+ }
26
+ catch {
27
27
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
28
- const result = [];
29
- for (const container of fs.readdirSync(managedRoot, { withFileTypes: true })) {
30
- if (!container.isDirectory() || container.isSymbolicLink())
28
+ }
29
+ }
30
+ function branchOnlyManifests(parentPath) {
31
+ const manifests = [];
32
+ for (const identity of fs.readdirSync(parentPath, { withFileTypes: true })) {
33
+ if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
31
34
  continue;
32
- const containerPath = path.join(managedRoot, container.name);
33
- for (const identity of fs.readdirSync(containerPath, { withFileTypes: true })) {
34
- if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
35
- continue;
36
- result.push(path.join(containerPath, identity.name, 'manifest.json'));
35
+ manifests.push(readManagedManifest(path.join(parentPath, identity.name, 'manifest.json')));
36
+ }
37
+ return manifests;
38
+ }
39
+ function taskBoundManifests(params) {
40
+ assertDirectory(params.tasksRoot);
41
+ const canonicalManagedRoot = fs.realpathSync.native(path.dirname(params.tasksRoot));
42
+ const canonicalTasksRoot = fs.realpathSync.native(params.tasksRoot);
43
+ if (path.dirname(canonicalTasksRoot) !== canonicalManagedRoot)
44
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
45
+ const canonicalRepoRoot = fs.realpathSync.native(params.repoRoot);
46
+ const manifests = [];
47
+ for (const task of fs.readdirSync(params.tasksRoot, { withFileTypes: true })) {
48
+ if (!/^TASK-\d{8}-\d{6}$/u.test(task.name))
49
+ continue;
50
+ if (!task.isDirectory() || task.isSymbolicLink())
51
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
52
+ const taskRoot = path.join(params.tasksRoot, task.name);
53
+ assertNoSymlinkPath(params.controlBase, taskRoot);
54
+ const canonicalTaskRoot = fs.realpathSync.native(taskRoot);
55
+ if (path.dirname(canonicalTaskRoot) !== canonicalTasksRoot)
56
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
57
+ const manifestPath = path.join(taskRoot, 'manifest.json');
58
+ if (!fs.existsSync(manifestPath))
59
+ continue;
60
+ const manifest = readManagedManifest(manifestPath);
61
+ if (manifest.mode !== 'task-bound' || manifest.taskId !== task.name || manifest.project !== params.project
62
+ || fs.realpathSync.native(manifest.repoRoot) !== canonicalRepoRoot
63
+ || path.dirname(fs.realpathSync.native(manifestPath)) !== canonicalTaskRoot) {
64
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
37
65
  }
66
+ manifests.push(manifest);
67
+ }
68
+ return manifests;
69
+ }
70
+ function managedManifestCandidates(params) {
71
+ const managedRoot = params.managedRoot;
72
+ const { controlBase, project, repoRoot } = params;
73
+ if (controlBase && project && path.resolve(managedRoot) !== path.resolve(controlBase, project)) {
74
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
38
75
  }
39
- return result;
76
+ if (!fs.existsSync(managedRoot))
77
+ return [];
78
+ assertDirectory(managedRoot);
79
+ return fs.readdirSync(managedRoot, { withFileTypes: true })
80
+ .flatMap((parent) => {
81
+ const parentPath = path.join(managedRoot, parent.name);
82
+ if (parent.name === 'tasks' && controlBase && project && repoRoot) {
83
+ return taskBoundManifests({ tasksRoot: parentPath, controlBase, project, repoRoot });
84
+ }
85
+ if (!parent.isDirectory() || parent.isSymbolicLink())
86
+ return [];
87
+ return branchOnlyManifests(parentPath);
88
+ });
40
89
  }
41
90
  function assertDirectory(directory) {
42
91
  const stat = fs.lstatSync(directory);
43
92
  if (!stat.isDirectory() || stat.isSymbolicLink())
44
93
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
45
94
  }
95
+ function assertNoSymlinkPath(root, target) {
96
+ const relative = path.relative(root, target);
97
+ if (relative.startsWith('..') || path.isAbsolute(relative))
98
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
99
+ let current = path.resolve(root);
100
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
101
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
102
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
103
+ current = path.join(current, segment);
104
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
105
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
106
+ }
107
+ }
46
108
  function readAudit(manifest) {
47
109
  const root = path.dirname(path.resolve(manifest.publicStatusDir));
48
110
  const records = [];
@@ -80,12 +142,16 @@ function auditProof(manifest, requestId) {
80
142
  return [];
81
143
  return related;
82
144
  }
83
- function publishedEvidence(records, generation) {
145
+ function publishedEvidence(records, manifest) {
84
146
  const publications = records.filter((record) => record.event === 'executor-result-published');
85
147
  let selected = null;
86
148
  let selectedSummary = null;
149
+ const digest = manifestDigest(manifest);
87
150
  for (const record of publications) {
88
- if (record.version !== 2 || record.requestGeneration !== generation
151
+ if (record.version !== 2 || record.generation !== manifest.generation || record.identityDigest !== digest
152
+ || typeof record.requestFamily !== 'string'
153
+ || !['task-lifecycle', 'task-create'].includes(record.requestFamily)
154
+ || (record.sandboxTaskId !== null && typeof record.sandboxTaskId !== 'string')
89
155
  || !Number.isSafeInteger(record.exitCode)
90
156
  || !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
91
157
  || typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
@@ -93,7 +159,7 @@ function publishedEvidence(records, generation) {
93
159
  throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
94
160
  }
95
161
  const summary = JSON.stringify([
96
- record.requestFamily ?? null, record.requestGeneration, record.exitCode,
162
+ record.requestFamily, record.sandboxTaskId, record.generation, record.identityDigest, record.exitCode,
97
163
  record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
98
164
  ]);
99
165
  if (selectedSummary !== null && summary !== selectedSummary) {
@@ -102,6 +168,9 @@ function publishedEvidence(records, generation) {
102
168
  selected ??= record;
103
169
  selectedSummary ??= summary;
104
170
  }
171
+ if (selected && selected.sandboxTaskId !== manifest.taskId) {
172
+ throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
173
+ }
105
174
  return selected;
106
175
  }
107
176
  function verifiedResponse(manifest, requestId, records) {
@@ -131,7 +200,7 @@ function verifiedResponse(manifest, requestId, records) {
131
200
  if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
132
201
  return unknown(requestId);
133
202
  const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
134
- const published = publishedEvidence(records, manifest.generation);
203
+ const published = publishedEvidence(records, manifest);
135
204
  if (!published)
136
205
  return unknown(requestId);
137
206
  let evidence = null;
@@ -187,15 +256,8 @@ export function recoverSandboxControlFromHost(requestId, params) {
187
256
  let candidates = [];
188
257
  while (Date.now() < deadline) {
189
258
  candidates = [];
190
- for (const manifestPath of managedManifestPaths(params.managedRoot)) {
191
- let manifest;
192
- try {
193
- manifest = readSandboxControlManifest(manifestPath);
194
- }
195
- catch {
196
- throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
197
- }
198
- if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
259
+ for (const manifest of managedManifestCandidates(params)) {
260
+ if (manifest.project !== (params.project ?? path.basename(path.resolve(params.managedRoot))) || !manifest.generation) {
199
261
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
200
262
  }
201
263
  const records = auditProof(manifest, requestId);
@@ -518,27 +518,8 @@ function readTaskCompletionManifest(root, binding) {
518
518
  }
519
519
  return manifest;
520
520
  }
521
- export async function quiesceSandboxControlForTaskCompletion(root, binding, timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS) {
522
- const resolvedRoot = path.resolve(root);
523
- const manifest = readTaskCompletionManifest(resolvedRoot, binding);
524
- markSandboxControlRootQuiescing(resolvedRoot);
525
- const deadline = Date.now() + timeoutMs;
526
- const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
527
- const brokerPath = path.join(resolvedRoot, 'broker.json');
528
- while (true) {
529
- const startup = readStartupOwner(startupPath);
530
- if (fs.existsSync(startupPath) && !startup)
531
- throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
532
- const broker = readBrokerOwner(brokerPath);
533
- if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
534
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
535
- }
536
- if (!startup && !broker)
537
- return;
538
- if (Date.now() >= deadline)
539
- throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
540
- await new Promise((resolve) => setTimeout(resolve, 25));
541
- }
521
+ export function validateSandboxControlTaskIdentity(root, binding) {
522
+ readTaskCompletionManifest(path.resolve(root), binding);
542
523
  }
543
524
  function recordSandboxRemovalPending(root, manifest, phase, journal) {
544
525
  const pendingPath = path.join(root, 'removal-pending.json');
@@ -975,35 +956,39 @@ function readSandboxControlManifestValue(manifestPath) {
975
956
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
976
957
  const root = path.dirname(path.resolve(manifestPath));
977
958
  const canonicalRoot = fs.realpathSync.native(root);
978
- const matchesControlChildPath = (candidatePath, name) => {
979
- const resolved = path.resolve(candidatePath);
980
- if (path.basename(resolved) !== name)
981
- return false;
959
+ const resolveControlChildPath = (candidatePath, name) => {
960
+ if (path.isAbsolute(candidatePath) || candidatePath !== name)
961
+ return null;
962
+ const resolved = path.resolve(root, candidatePath);
982
963
  const expected = path.join(canonicalRoot, name);
983
964
  try {
984
- return fs.realpathSync.native(resolved) === expected;
965
+ const stat = fs.lstatSync(resolved);
966
+ if (!stat.isDirectory() || stat.isSymbolicLink())
967
+ return null;
968
+ return fs.realpathSync.native(resolved) === expected ? expected : null;
985
969
  }
986
970
  catch (error) {
987
971
  if (error.code !== 'ENOENT')
988
972
  throw error;
989
973
  try {
990
- return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot;
974
+ return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot ? expected : null;
991
975
  }
992
976
  catch (parentError) {
993
977
  if (parentError.code !== 'ENOENT')
994
978
  throw parentError;
995
- return false;
979
+ return null;
996
980
  }
997
981
  }
998
982
  };
999
- if (!matchesControlChildPath(candidate.channelDir, 'channel')
1000
- || !matchesControlChildPath(candidate.publicStatusDir, 'public')
1001
- || !matchesControlChildPath(candidate.processingDir, 'processing')) {
983
+ const channelDir = resolveControlChildPath(candidate.channelDir, 'channel');
984
+ const publicStatusDir = resolveControlChildPath(candidate.publicStatusDir, 'public');
985
+ const processingDir = resolveControlChildPath(candidate.processingDir, 'processing');
986
+ const runtimeDir = resolveControlChildPath(candidate.runtimeDir, 'runtime');
987
+ if (!channelDir || !publicStatusDir || !processingDir) {
1002
988
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
1003
989
  }
1004
- if (!matchesControlChildPath(candidate.runtimeDir, 'runtime')) {
990
+ if (!runtimeDir)
1005
991
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
1006
- }
1007
992
  const normalized = {
1008
993
  engine: candidate.engine,
1009
994
  repoRoot: candidate.repoRoot,
@@ -1021,10 +1006,10 @@ function readSandboxControlManifestValue(manifestPath) {
1021
1006
  token: candidate.token,
1022
1007
  generation: candidate.generation,
1023
1008
  controlRootId: candidate.controlRootId,
1024
- channelDir: candidate.channelDir,
1025
- publicStatusDir: candidate.publicStatusDir,
1026
- processingDir: candidate.processingDir,
1027
- runtimeDir: candidate.runtimeDir,
1009
+ channelDir,
1010
+ publicStatusDir,
1011
+ processingDir,
1012
+ runtimeDir,
1028
1013
  };
1029
1014
  return normalized;
1030
1015
  }
@@ -1,16 +1,14 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { validateTaskCreateCandidate } from '../../task/create.js';
3
- import { isAgentClientId } from '../../agent-clients/types.js';
4
3
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
5
4
  export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
6
5
  export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
7
6
  export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
8
7
  export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
9
- export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
10
8
  export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
11
9
  export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
12
10
  export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
13
- export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create', 'agent-client'];
11
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'];
14
12
  export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
15
13
  controlTickMs: 250,
16
14
  parkedBindingInitialMs: 1_000,
@@ -106,51 +104,26 @@ export function parseSandboxControlReservation(value) {
106
104
  export function isSandboxControlFamily(value) {
107
105
  return SANDBOX_CONTROL_FAMILIES.includes(value);
108
106
  }
109
- export function validateSandboxControlRequest(value, manifest, options = {}) {
107
+ export function validateSandboxControlRequest(value, manifest) {
110
108
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
111
109
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
112
110
  }
113
111
  const request = value;
114
- if (request.version !== 4 || typeof request.id !== 'string'
115
- || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
116
- || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
117
- || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
112
+ if (request.version !== 5 || typeof request.id !== 'string'
113
+ || !/^[a-f0-9-]{16,64}$/.test(request.id)
114
+ || typeof request.family !== 'string' || !isSandboxControlFamily(request.family))
118
115
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
119
- if (request.generation !== manifest.generation) {
120
- fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
121
- }
122
- const issuedAt = request.issuedAt;
123
- const expiresAt = request.expiresAt;
124
- const now = options.now ?? Date.now();
125
- if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
126
- fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
127
- }
128
- if (now > expiresAt)
129
- fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
130
116
  if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
131
117
  fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
132
118
  }
133
119
  if (request.family === 'task-create') {
134
- const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
120
+ const expected = ['candidate', 'family', 'id', 'version'];
135
121
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
136
122
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
137
123
  }
138
124
  return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
139
125
  }
140
- if (request.family === 'agent-client') {
141
- const expected = ['agentClient', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'payload', 'token', 'version'];
142
- if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
143
- || !isAgentClientId(request.agentClient)
144
- || typeof request.operation !== 'string' || !/^[a-z][a-z0-9.-]{0,63}$/u.test(request.operation)
145
- || !request.payload || typeof request.payload !== 'object' || Array.isArray(request.payload)) {
146
- fail('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client request schema is invalid');
147
- }
148
- if (manifest.mode !== 'task-bound' || !manifest.taskId) {
149
- fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot coordinate agent clients');
150
- }
151
- return request;
152
- }
153
- const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
126
+ const expected = ['args', 'family', 'id', 'version'];
154
127
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
155
128
  || !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
156
129
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
@@ -164,7 +137,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
164
137
  return request;
165
138
  }
166
139
  export function bindSandboxControlTask(request, taskId) {
167
- if (request.family === 'task-create' || request.family === 'agent-client') {
140
+ if (request.family === 'task-create') {
168
141
  fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
169
142
  }
170
143
  if (request.args.length === 0)
@@ -180,7 +153,6 @@ export function controlError(error) {
180
153
  const retryable = new Set([
181
154
  'SANDBOX_CONTROL_BUSY',
182
155
  'SANDBOX_CONTROL_HANDOFF_ACTIVE',
183
- 'SANDBOX_CONTROL_REQUEST_EXPIRED',
184
156
  'SANDBOX_WORKTREE_BINDING_LOST'
185
157
  ]).has(code);
186
158
  return { code, message, retryable };
@@ -18,7 +18,6 @@ import { inspectSandboxControlContainer } from './container-identity.js';
18
18
  import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
19
19
  import { nextSandboxControlBackoff } from './timing.js';
20
20
  import { readTaskFinalizationReceipt } from '../../task/finalization.js';
21
- import { recoverAgentClientOperation, validateAgentClientOperation } from './agent-operation.js';
22
21
  import { validateSandboxControlIdentity } from './identity-sentinel.js';
23
22
  import { mergeSandboxTaskView, taskViewForManifest } from './task-view.js';
24
23
  import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
@@ -28,10 +27,6 @@ function appendBrokerAudit(manifest, event, fields = {}) {
28
27
  function operationKey(request) {
29
28
  if (request.family === 'task-create')
30
29
  return 'create';
31
- if (request.family === 'agent-client')
32
- return request.operation;
33
- if (request.family !== 'task-lifecycle')
34
- return null;
35
30
  try {
36
31
  const operation = parseTaskControlOperation(request.family, request.args);
37
32
  return operation.family === 'task-lifecycle' ? operation.request.intent : null;
@@ -193,7 +188,7 @@ async function readRecoveryDomain(manifest, manifestPath, request, operation, te
193
188
  return { domain: null };
194
189
  const taskRef = 'args' in request ? request.args[0] ?? null : manifest.taskId;
195
190
  const output = parseControlOutput(payloadOutput);
196
- if (!taskRef && operation.family !== 'task-create' && operation.family !== 'agent-client') {
191
+ if (!taskRef && operation.family !== 'task-create') {
197
192
  return { domain: null };
198
193
  }
199
194
  if (operation.family === 'task-lifecycle') {
@@ -221,14 +216,6 @@ async function readRecoveryDomain(manifest, manifestPath, request, operation, te
221
216
  if (operation.family === 'task-create') {
222
217
  return { domain: taskCreateDomainEvidence(manifest, output) };
223
218
  }
224
- if (operation.family === 'agent-client' && request.family === 'agent-client') {
225
- return { domain: recoverAgentClientOperation({
226
- request,
227
- manifest,
228
- manifestPath,
229
- stdout: payloadOutput
230
- }) };
231
- }
232
219
  return { domain: null };
233
220
  }
234
221
  function readCommittedCriticalPhases(manifest, requestId) {
@@ -443,11 +430,7 @@ async function recoverProcessing(manifest, manifestPath, broker, brokerOwns) {
443
430
  let terminalReconciled = false;
444
431
  try {
445
432
  const rawRequest = JSON.parse(fs.readFileSync(path.join(processingDirectory, 'request.json'), 'utf8'));
446
- request = validateSandboxControlRequest(rawRequest, manifest, {
447
- now: typeof rawRequest.issuedAt === 'number' ? rawRequest.issuedAt : undefined
448
- });
449
- if (request.family === 'agent-client')
450
- validateAgentClientOperation(request, manifest);
433
+ request = validateSandboxControlRequest(rawRequest, manifest);
451
434
  }
452
435
  catch {
453
436
  // Missing or malformed request evidence remains fail-closed below.
@@ -926,8 +909,6 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
926
909
  if (active)
927
910
  throw new Error('SANDBOX_CONTROL_BUSY');
928
911
  const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
929
- if (request.family === 'agent-client')
930
- validateAgentClientOperation(request, manifest);
931
912
  validatedRequest = request;
932
913
  criticalRequestPhase(manifest, request, 'validated', 'in-progress');
933
914
  appendBrokerAudit(manifest, 'request-validated', {