@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
@@ -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);
@@ -661,28 +661,11 @@ function readTaskCompletionManifest(
661
661
  return manifest;
662
662
  }
663
663
 
664
- export async function quiesceSandboxControlForTaskCompletion(
664
+ export function validateSandboxControlTaskIdentity(
665
665
  root: string,
666
- binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>,
667
- timeoutMs = DEFAULT_QUIESCE_TIMEOUT_MS
668
- ): Promise<void> {
669
- const resolvedRoot = path.resolve(root);
670
- const manifest = readTaskCompletionManifest(resolvedRoot, binding);
671
- markSandboxControlRootQuiescing(resolvedRoot);
672
- const deadline = Date.now() + timeoutMs;
673
- const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
674
- const brokerPath = path.join(resolvedRoot, 'broker.json');
675
- while (true) {
676
- const startup = readStartupOwner(startupPath);
677
- if (fs.existsSync(startupPath) && !startup) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
678
- const broker = readBrokerOwner(brokerPath);
679
- if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
680
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
681
- }
682
- if (!startup && !broker) return;
683
- if (Date.now() >= deadline) throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
684
- await new Promise((resolve) => setTimeout(resolve, 25));
685
- }
666
+ binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>
667
+ ): void {
668
+ readTaskCompletionManifest(path.resolve(root), binding);
686
669
  }
687
670
 
688
671
  function recordSandboxRemovalPending(
@@ -1155,30 +1138,32 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
1155
1138
  || !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
1156
1139
  const root = path.dirname(path.resolve(manifestPath));
1157
1140
  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;
1141
+ const resolveControlChildPath = (candidatePath: string, name: string): string | null => {
1142
+ if (path.isAbsolute(candidatePath) || candidatePath !== name) return null;
1143
+ const resolved = path.resolve(root, candidatePath);
1161
1144
  const expected = path.join(canonicalRoot, name);
1162
1145
  try {
1163
- return fs.realpathSync.native(resolved) === expected;
1146
+ const stat = fs.lstatSync(resolved);
1147
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
1148
+ return fs.realpathSync.native(resolved) === expected ? expected : null;
1164
1149
  } catch (error) {
1165
1150
  if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
1166
1151
  try {
1167
- return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot;
1152
+ return fs.realpathSync.native(path.dirname(resolved)) === canonicalRoot ? expected : null;
1168
1153
  } catch (parentError) {
1169
1154
  if ((parentError as NodeJS.ErrnoException).code !== 'ENOENT') throw parentError;
1170
- return false;
1155
+ return null;
1171
1156
  }
1172
1157
  }
1173
1158
  };
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')) {
1159
+ const channelDir = resolveControlChildPath(candidate.channelDir, 'channel');
1160
+ const publicStatusDir = resolveControlChildPath(candidate.publicStatusDir, 'public');
1161
+ const processingDir = resolveControlChildPath(candidate.processingDir, 'processing');
1162
+ const runtimeDir = resolveControlChildPath(candidate.runtimeDir, 'runtime');
1163
+ if (!channelDir || !publicStatusDir || !processingDir) {
1180
1164
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
1181
1165
  }
1166
+ if (!runtimeDir) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
1182
1167
  const normalized: SandboxControlManifest = {
1183
1168
  engine: candidate.engine,
1184
1169
  repoRoot: candidate.repoRoot,
@@ -1196,10 +1181,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
1196
1181
  token: candidate.token,
1197
1182
  generation: candidate.generation,
1198
1183
  controlRootId: candidate.controlRootId,
1199
- channelDir: candidate.channelDir,
1200
- publicStatusDir: candidate.publicStatusDir,
1201
- processingDir: candidate.processingDir,
1202
- runtimeDir: candidate.runtimeDir,
1184
+ channelDir,
1185
+ publicStatusDir,
1186
+ processingDir,
1187
+ runtimeDir,
1203
1188
  };
1204
1189
  return normalized;
1205
1190
  }
@@ -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', {
@@ -12,14 +12,13 @@ export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
12
12
  const TASK_MARKER_KEYS = [
13
13
  'AGENT_INFRA_SANDBOX',
14
14
  'AGENT_INFRA_TASK_ID',
15
- 'AGENT_INFRA_CONTROL_TOKEN',
16
15
  'AGENT_INFRA_CONTROL_GENERATION',
17
16
  'AGENT_INFRA_CONTROL_DIR',
17
+ 'AGENT_INFRA_CONTROL_ROOT_DIR',
18
18
  'AGENT_INFRA_CONTROL_STATUS_DIR',
19
19
  'AGENT_INFRA_RUNTIME_DIR'
20
20
  ] as const;
21
21
  const TASK_CONTROL_MARKER_KEYS = [
22
- 'AGENT_INFRA_CONTROL_TOKEN',
23
22
  'AGENT_INFRA_CONTROL_GENERATION',
24
23
  'AGENT_INFRA_CONTROL_DIR',
25
24
  'AGENT_INFRA_CONTROL_STATUS_DIR'
@@ -88,7 +87,6 @@ export function resolveSandboxControlTransport(
88
87
  ): SandboxControlTransportDecision {
89
88
  const markerState = sandboxTaskMarkerState(env);
90
89
  const hasAnyMarker = markerState !== 'none'
91
- || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
92
90
  || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
93
91
  const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
94
92
  const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
@@ -109,6 +107,10 @@ export function resolveSandboxControlTransport(
109
107
  if (!hasCompleteConfig) {
110
108
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
111
109
  }
110
+ if (markerState === 'task-bound'
111
+ && (!env.AGENT_INFRA_CONTROL_ROOT_DIR || !path.isAbsolute(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
112
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
113
+ }
112
114
  const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
113
115
  const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
114
116
  ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
@@ -119,7 +121,7 @@ export function resolveSandboxControlTransport(
119
121
  ? configuredStatusProbe === 'present' ? configuredStatusDir : null
120
122
  : fixedStatusMounted ? fixedStatusDir : null;
121
123
  const statusMounted = statusDir !== null;
122
- if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
124
+ if (env.AGENT_INFRA_EXECUTOR_MANIFEST) {
123
125
  return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
124
126
  }
125
127
  const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
@@ -134,9 +136,6 @@ export function resolveSandboxControlTransport(
134
136
  ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
135
137
  : 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
136
138
  }
137
- if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
138
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
139
- }
140
139
  if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
141
140
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
142
141
  }
@@ -144,7 +143,7 @@ export function resolveSandboxControlTransport(
144
143
  && (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
145
144
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
146
145
  }
147
- if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
146
+ if (sentinel.mode === 'branch-only' && (taskBound || runtime || Boolean(env.AGENT_INFRA_CONTROL_ROOT_DIR))) {
148
147
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
149
148
  }
150
149
  return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };