@fitlab-ai/agent-infra 0.11.12 → 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 (120) 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-lifecycle/store.js +11 -25
  7. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
  8. package/dist/lib/agent-clients/adapters/codex.js +0 -2
  9. package/dist/lib/internal/cli-route-inventory.js +0 -1
  10. package/dist/lib/internal/sandbox-control.js +1 -81
  11. package/dist/lib/internal/task-finalization.js +29 -20
  12. package/dist/lib/internal/task-lifecycle.js +4 -2
  13. package/dist/lib/internal/task-operation-registry.js +1 -3
  14. package/dist/lib/internal/task-validate.js +14 -124
  15. package/dist/lib/platform/github-client.js +33 -3
  16. package/dist/lib/platform/release-notes.js +2 -2
  17. package/dist/lib/sandbox/commands/control-status.js +0 -1
  18. package/dist/lib/sandbox/commands/create.js +11 -6
  19. package/dist/lib/sandbox/commands/ls.js +4 -5
  20. package/dist/lib/sandbox/commands/rm.js +1 -1
  21. package/dist/lib/sandbox/config.js +12 -0
  22. package/dist/lib/sandbox/control/audit.js +1 -2
  23. package/dist/lib/sandbox/control/client.js +15 -110
  24. package/dist/lib/sandbox/control/executor.js +9 -59
  25. package/dist/lib/sandbox/control/host-recovery.js +89 -27
  26. package/dist/lib/sandbox/control/lifecycle.js +75 -9
  27. package/dist/lib/sandbox/control/protocol.js +11 -50
  28. package/dist/lib/sandbox/control/server.js +203 -253
  29. package/dist/lib/sandbox/control/task-view.js +0 -13
  30. package/dist/lib/sandbox/environment.js +7 -8
  31. package/dist/lib/sandbox/mounts.js +71 -52
  32. package/dist/lib/sandbox/recovery.js +30 -12
  33. package/dist/lib/sandbox/removal.js +37 -3
  34. package/dist/lib/sandbox/workspace-identity.js +18 -6
  35. package/dist/lib/sandbox/workspace-view.js +89 -6
  36. package/dist/lib/task/commands/ls.js +3 -3
  37. package/dist/lib/task/control-authority.js +41 -17
  38. package/dist/lib/task/control-recovery.js +2 -4
  39. package/dist/lib/task/finalization.js +27 -2
  40. package/dist/lib/task/lifecycle.js +294 -63
  41. package/dist/lib/task/orchestration.js +26 -72
  42. package/dist/lib/task/short-id.js +37 -27
  43. package/dist/lib/task/verification-engine.js +4 -11
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/agent-clients/adapter.ts +0 -27
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
  47. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
  48. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
  51. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
  52. package/lib/agent-clients/adapters/codex.ts +0 -2
  53. package/lib/internal/cli-route-inventory.ts +0 -1
  54. package/lib/internal/sandbox-control.ts +0 -97
  55. package/lib/internal/task-finalization.ts +33 -19
  56. package/lib/internal/task-lifecycle.ts +4 -2
  57. package/lib/internal/task-operation-registry.ts +1 -3
  58. package/lib/internal/task-validate.ts +13 -127
  59. package/lib/platform/github-client.ts +34 -3
  60. package/lib/platform/release-notes.ts +2 -2
  61. package/lib/sandbox/commands/control-status.ts +0 -1
  62. package/lib/sandbox/commands/create.ts +11 -5
  63. package/lib/sandbox/commands/ls.ts +4 -5
  64. package/lib/sandbox/commands/rm.ts +1 -1
  65. package/lib/sandbox/config.ts +13 -0
  66. package/lib/sandbox/control/audit.ts +1 -2
  67. package/lib/sandbox/control/client.ts +17 -138
  68. package/lib/sandbox/control/executor.ts +8 -65
  69. package/lib/sandbox/control/host-recovery.ts +94 -21
  70. package/lib/sandbox/control/lifecycle.ts +75 -10
  71. package/lib/sandbox/control/protocol.ts +12 -62
  72. package/lib/sandbox/control/server.ts +11 -61
  73. package/lib/sandbox/control/task-view.ts +0 -19
  74. package/lib/sandbox/environment.ts +7 -8
  75. package/lib/sandbox/mounts.ts +93 -68
  76. package/lib/sandbox/recovery.ts +37 -19
  77. package/lib/sandbox/removal.ts +36 -5
  78. package/lib/sandbox/workspace-identity.ts +16 -6
  79. package/lib/sandbox/workspace-view.ts +81 -7
  80. package/lib/task/commands/ls.ts +3 -3
  81. package/lib/task/control-authority.ts +41 -26
  82. package/lib/task/control-recovery.ts +3 -5
  83. package/lib/task/finalization.ts +29 -2
  84. package/lib/task/lifecycle.ts +282 -57
  85. package/lib/task/orchestration.ts +30 -68
  86. package/lib/task/short-id.ts +33 -40
  87. package/lib/task/verification-engine.ts +3 -9
  88. package/package.json +1 -1
  89. package/templates/.agents/rules/README.en.md +1 -0
  90. package/templates/.agents/rules/README.zh-CN.md +1 -0
  91. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
  92. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
  93. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  94. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  95. package/templates/.agents/rules/task-short-id.en.md +18 -12
  96. package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
  97. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  98. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  99. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
  100. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
  101. package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
  102. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
  103. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  104. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  105. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  106. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  107. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
  108. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
  109. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
  110. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
  111. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
  112. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
  113. package/dist/lib/sandbox/control/agent-operation.js +0 -34
  114. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
  115. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
  116. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
  117. package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
  118. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
  119. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
  120. package/lib/sandbox/control/agent-operation.ts +0 -49
@@ -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);
@@ -9,6 +9,7 @@ import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
9
9
  import { inspectSandboxControlContainer } from './container-identity.js';
10
10
  import { acquireSandboxResourceLock, resolveSandboxLockNamespace } from './native-file-lock.js';
11
11
  import { isSandboxAuthorityEvidence } from '../engines/authority.js';
12
+ import { validateSandboxControlIdentity } from './identity-sentinel.js';
12
13
  import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
13
14
  const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
14
15
  const QUIESCING_FILE = 'quiescing.json';
@@ -500,6 +501,45 @@ function markSandboxControlRootQuiescing(root) {
500
501
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
501
502
  }
502
503
  }
504
+ function readTaskCompletionManifest(root, binding) {
505
+ const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
506
+ const identity = validateSandboxControlIdentity({
507
+ publicStatusDir: binding.statusDir,
508
+ root,
509
+ mode: manifest.mode,
510
+ taskId: manifest.taskId,
511
+ generation: manifest.generation,
512
+ controlRootId: manifest.controlRootId
513
+ });
514
+ if (identity.state !== 'valid' || manifest.mode !== 'task-bound'
515
+ || manifest.taskId !== binding.taskId || manifest.generation !== binding.generation
516
+ || manifest.controlRootId !== binding.controlRootId) {
517
+ throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
518
+ }
519
+ return manifest;
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
+ }
542
+ }
503
543
  function recordSandboxRemovalPending(root, manifest, phase, journal) {
504
544
  const pendingPath = path.join(root, 'removal-pending.json');
505
545
  const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
@@ -934,14 +974,40 @@ function readSandboxControlManifestValue(manifestPath) {
934
974
  || !isSandboxAuthorityEvidence(candidate.authorityEvidence))
935
975
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
936
976
  const root = path.dirname(path.resolve(manifestPath));
937
- if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
938
- || path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
939
- || path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
977
+ const canonicalRoot = fs.realpathSync.native(root);
978
+ const resolveControlChildPath = (candidatePath, name) => {
979
+ if (path.isAbsolute(candidatePath) || candidatePath !== name)
980
+ return null;
981
+ const resolved = path.resolve(root, candidatePath);
982
+ const expected = path.join(canonicalRoot, name);
983
+ try {
984
+ const stat = fs.lstatSync(resolved);
985
+ if (!stat.isDirectory() || stat.isSymbolicLink())
986
+ return null;
987
+ return fs.realpathSync.native(resolved) === expected ? expected : null;
988
+ }
989
+ catch (error) {
990
+ if (error.code !== 'ENOENT')
991
+ throw error;
992
+ try {
993
+ return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot ? expected : null;
994
+ }
995
+ catch (parentError) {
996
+ if (parentError.code !== 'ENOENT')
997
+ throw parentError;
998
+ return null;
999
+ }
1000
+ }
1001
+ };
1002
+ const channelDir = resolveControlChildPath(candidate.channelDir, 'channel');
1003
+ const publicStatusDir = resolveControlChildPath(candidate.publicStatusDir, 'public');
1004
+ const processingDir = resolveControlChildPath(candidate.processingDir, 'processing');
1005
+ const runtimeDir = resolveControlChildPath(candidate.runtimeDir, 'runtime');
1006
+ if (!channelDir || !publicStatusDir || !processingDir) {
940
1007
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
941
1008
  }
942
- if (path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
1009
+ if (!runtimeDir)
943
1010
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
944
- }
945
1011
  const normalized = {
946
1012
  engine: candidate.engine,
947
1013
  repoRoot: candidate.repoRoot,
@@ -959,10 +1025,10 @@ function readSandboxControlManifestValue(manifestPath) {
959
1025
  token: candidate.token,
960
1026
  generation: candidate.generation,
961
1027
  controlRootId: candidate.controlRootId,
962
- channelDir: candidate.channelDir,
963
- publicStatusDir: candidate.publicStatusDir,
964
- processingDir: candidate.processingDir,
965
- runtimeDir: candidate.runtimeDir,
1028
+ channelDir,
1029
+ publicStatusDir,
1030
+ processingDir,
1031
+ runtimeDir,
966
1032
  };
967
1033
  return normalized;
968
1034
  }
@@ -1,17 +1,14 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { validateTaskCreateCandidate } from '../../task/create.js';
3
- import { normalizeAgentToken } from '../../agent-clients/tokens.js';
4
- import { isAgentClientId } from '../../agent-clients/types.js';
5
3
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
6
4
  export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
7
5
  export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
8
6
  export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
9
7
  export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
10
- export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
11
8
  export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
12
9
  export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
13
10
  export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
14
- export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-finalization', 'task-create', 'agent-client'];
11
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-create'];
15
12
  export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
16
13
  controlTickMs: 250,
17
14
  parkedBindingInitialMs: 1_000,
@@ -107,75 +104,40 @@ export function parseSandboxControlReservation(value) {
107
104
  export function isSandboxControlFamily(value) {
108
105
  return SANDBOX_CONTROL_FAMILIES.includes(value);
109
106
  }
110
- export function validateSandboxControlRequest(value, manifest, options = {}) {
107
+ export function validateSandboxControlRequest(value, manifest) {
111
108
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
112
109
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
113
110
  }
114
111
  const request = value;
115
- if (request.version !== 4 || typeof request.id !== 'string'
116
- || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
117
- || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
118
- || !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))
119
115
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
120
- if (request.generation !== manifest.generation) {
121
- fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
122
- }
123
- const issuedAt = request.issuedAt;
124
- const expiresAt = request.expiresAt;
125
- const now = options.now ?? Date.now();
126
- if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
127
- fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
128
- }
129
- if (now > expiresAt)
130
- fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
131
116
  if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
132
117
  fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
133
118
  }
134
119
  if (request.family === 'task-create') {
135
- const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
120
+ const expected = ['candidate', 'family', 'id', 'version'];
136
121
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
137
122
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
138
123
  }
139
124
  return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
140
125
  }
141
- if (request.family === 'agent-client') {
142
- const expected = ['agentClient', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'payload', 'token', 'version'];
143
- if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
144
- || !isAgentClientId(request.agentClient)
145
- || typeof request.operation !== 'string' || !/^[a-z][a-z0-9.-]{0,63}$/u.test(request.operation)
146
- || !request.payload || typeof request.payload !== 'object' || Array.isArray(request.payload)) {
147
- fail('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client request schema is invalid');
148
- }
149
- if (manifest.mode !== 'task-bound' || !manifest.taskId) {
150
- fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot coordinate agent clients');
151
- }
152
- return request;
153
- }
154
- if (request.family === 'task-finalization') {
155
- const expected = ['agent', 'args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
156
- if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
157
- || request.operation !== 'complete'
158
- || !Array.isArray(request.args) || request.args.length !== 0
159
- || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent) {
160
- fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
161
- }
162
- if (manifest.mode !== 'task-bound' || !manifest.taskId) {
163
- fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks');
164
- }
165
- return request;
166
- }
167
- const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
126
+ const expected = ['args', 'family', 'id', 'version'];
168
127
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
169
128
  || !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
170
129
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
171
130
  }
131
+ if (request.args[1] === 'complete') {
132
+ fail('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization');
133
+ }
172
134
  if (manifest.mode !== 'task-bound' || !manifest.taskId) {
173
135
  fail('SANDBOX_CONTROL_BRANCH_ONLY', "branch-only sandboxes cannot coordinate tasks; return to the host and run 'ai sandbox start --recreate <task-ref-or-correct-branch>'");
174
136
  }
175
137
  return request;
176
138
  }
177
139
  export function bindSandboxControlTask(request, taskId) {
178
- if (request.family === 'task-create' || request.family === 'agent-client' || request.family === 'task-finalization') {
140
+ if (request.family === 'task-create') {
179
141
  fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
180
142
  }
181
143
  if (request.args.length === 0)
@@ -191,7 +153,6 @@ export function controlError(error) {
191
153
  const retryable = new Set([
192
154
  'SANDBOX_CONTROL_BUSY',
193
155
  'SANDBOX_CONTROL_HANDOFF_ACTIVE',
194
- 'SANDBOX_CONTROL_REQUEST_EXPIRED',
195
156
  'SANDBOX_WORKTREE_BINDING_LOST'
196
157
  ]).has(code);
197
158
  return { code, message, retryable };