@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
@@ -2,27 +2,9 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { randomUUID } from 'node:crypto';
6
5
  import { loadConfig } from '../sandbox/config.ts';
7
- import { detectEngine } from '../sandbox/engine.ts';
8
- import { runEngine } from '../sandbox/shell.ts';
9
- import { sandboxBranchLabel, sandboxLabel } from '../sandbox/constants.ts';
10
- import { fetchSandboxRows, selectSandboxContainer } from '../sandbox/commands/list-running.ts';
11
- import { containerNameCandidates } from '../sandbox/constants.ts';
12
6
  import { resolveSandboxTarget } from '../sandbox/workspace-identity.ts';
13
- import { sandboxControlPaths } from '../sandbox/workspace-view.ts';
14
- import {
15
- appendSandboxControlAudit,
16
- readActiveLease,
17
- readSandboxControlStatus
18
- } from '../sandbox/control/state.ts';
19
- import type { SandboxControlLease } from '../sandbox/control/protocol.ts';
20
- import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
21
- import {
22
- SANDBOX_CONTROL_FUTURE_SKEW_MS,
23
- SANDBOX_CONTROL_STATUS_STALE_MS
24
- } from '../sandbox/control/protocol.ts';
25
- import { getProcessStartTime } from '../server/process-state.ts';
7
+ import { resolveBranchWorktree } from '../git/branch-worktree.ts';
26
8
  import { assertGitWorktreeBinding } from '../git/worktree-identity.ts';
27
9
  import { createManualValidationEvidence, manualValidationEvidenceSummary } from '../task/manual-validation-evidence.ts';
28
10
  import type { ManualValidationCleanup } from '../task/manual-validation-evidence.ts';
@@ -31,7 +13,6 @@ import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-in
31
13
 
32
14
  const USAGE = `Usage: agent-infra-internal task-validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] [--evidence-file <path>] -- <command> [args...]`;
33
15
  const MAX_TIMEOUT_MS = 60 * 60 * 1000;
34
- const RECOVERY_GRACE_MS = 30_000;
35
16
 
36
17
  type ValidateOptions = {
37
18
  target: string;
@@ -108,20 +89,6 @@ function git(root: string, args: string[]): string {
108
89
  return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
109
90
  }
110
91
 
111
- function waitForState(statusDir: string, predicate: (state: ReturnType<typeof readSandboxControlStatus>) => boolean, timeoutMs = 5_000): void {
112
- const deadline = Date.now() + timeoutMs;
113
- while (Date.now() < deadline) {
114
- try {
115
- const status = readSandboxControlStatus(statusDir);
116
- if (predicate(status)) return;
117
- } catch {
118
- // Atomic status publication may be between renames.
119
- }
120
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
121
- }
122
- throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
123
- }
124
-
125
92
  function snapshotValidation(repoRoot: string, commit: string, options: ValidateOptions) {
126
93
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
127
94
  const worktree = path.join(tempRoot, 'worktree');
@@ -150,109 +117,28 @@ function snapshotValidation(repoRoot: string, commit: string, options: ValidateO
150
117
  }
151
118
 
152
119
  function inplaceValidation(config: ReturnType<typeof loadConfig>, target: ReturnType<typeof resolveSandboxTarget>, options: ValidateOptions) {
153
- const engine = detectEngine(config);
154
- const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
155
- const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
156
- if (!row) throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
157
- const control = sandboxControlPaths({
158
- base: config.controlBase, repoRoot: config.repoRoot, project: config.project,
159
- container: row.name, identity: target.workspace
160
- });
161
- const manifest = readSandboxControlManifest(control.manifestPath);
162
- const initial = readSandboxControlStatus(control.statusDir);
163
- const now = Date.now();
164
- if (initial.generation !== manifest.generation
165
- || now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
166
- || initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
167
- throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
168
- }
169
- if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
170
- throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
171
- }
172
- const startTime = getProcessStartTime(process.pid);
173
- if (!startTime) throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
174
- const leasePath = path.join(control.root, 'lease.json');
175
- const nonce = randomUUID();
176
- const issuedAt = Date.now();
177
- const lease: SandboxControlLease = {
178
- version: 2, generation: manifest.generation, nonce,
179
- owner: { pid: process.pid, startTime }, issuedAt,
180
- expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
181
- taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
182
- branch: target.branch, reason: 'manual-validation'
183
- };
184
- const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
185
- const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
186
- fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
187
- let stopped = false;
120
+ const targetRoot = resolveBranchWorktree(config.repoRoot, target.branch);
121
+ if (!targetRoot) throw new Error(`TASK_VALIDATION_WORKTREE_NOT_FOUND: ${target.branch}`);
122
+ const worktree = assertGitWorktreeBinding(config.repoRoot, targetRoot, target.branch);
123
+ const originalBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
124
+ const originalHead = git(worktree.worktreeRoot, ['rev-parse', 'HEAD']);
188
125
  let restored = false;
189
126
  try {
190
- appendSandboxControlAudit(manifest, 'lease-acquire', { ownerPid: process.pid });
191
- waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
192
- if (row.running) {
193
- runEngine(engine, 'docker', ['stop', row.name]);
194
- stopped = true;
195
- }
196
- const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
197
- const refreshed = selectSandboxContainer(
198
- [...refreshedRows.running, ...refreshedRows.nonRunning],
199
- containerNameCandidates(config, target.branch)
200
- );
201
- if (!refreshed || refreshed.running) throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
202
- const activeLease = readActiveLease(manifest);
203
- if (!activeLease || activeLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
204
- assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
205
- const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
127
+ const result = runValidationCommand(options.command, worktree.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
206
128
  return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
207
129
  } finally {
208
- const recoveryErrors: string[] = [];
209
130
  try {
210
- const currentBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
211
- if (currentBranch !== originalBranch) git(manifest.worktreeRoot, ['switch', originalBranch]);
131
+ const currentBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
132
+ if (currentBranch !== originalBranch) git(worktree.worktreeRoot, ['switch', originalBranch]);
212
133
  } catch {
213
134
  try {
214
- git(manifest.worktreeRoot, ['checkout', '--detach', originalHead]);
215
- git(manifest.worktreeRoot, ['switch', originalBranch]);
216
- } catch (fallbackError) {
217
- recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
218
- }
219
- }
220
- if (stopped) {
221
- try {
222
- runEngine(engine, 'docker', ['start', row.name]);
223
- } catch (error) {
224
- recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
225
- }
226
- }
227
- try {
228
- const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { nonce?: unknown };
229
- if (currentLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
230
- } catch (error) {
231
- recoveryErrors.push(error instanceof Error ? error.message : String(error));
232
- }
233
- if (recoveryErrors.length === 0) {
234
- try {
235
- fs.unlinkSync(leasePath);
236
- appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
237
- waitForState(control.statusDir, (status) => status.state === 'healthy');
238
- restored = true;
135
+ git(worktree.worktreeRoot, ['checkout', '--detach', originalHead]);
136
+ git(worktree.worktreeRoot, ['switch', originalBranch]);
239
137
  } catch (error) {
240
- try {
241
- if (!fs.existsSync(leasePath)) {
242
- fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
243
- }
244
- appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
245
- } catch (leaseError) {
246
- recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
247
- }
248
- recoveryErrors.push(error instanceof Error ? error.message : String(error));
138
+ throw new Error(`TASK_VALIDATION_WORKTREE_RESTORE_FAILED: ${error instanceof Error ? error.message : String(error)}`);
249
139
  }
250
140
  }
251
- if (recoveryErrors.length > 0) {
252
- throw new Error(
253
- `SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`
254
- );
255
- }
141
+ restored = true;
256
142
  }
257
143
  }
258
144
 
@@ -429,12 +429,15 @@ function runMechanicalChangeReport(repoRoot: string, baseSha: string, headSha: s
429
429
  return validated.value;
430
430
  }
431
431
 
432
+ const DOCUMENTATION_CATEGORY = { label: '文档', prefixes: ['docs/'] } as const;
433
+
432
434
  const CHANGE_CATEGORIES = [
433
435
  { label: '运行时代码', prefixes: ['bin/', 'lib/', 'src/', 'scripts/'] },
434
436
  { label: '测试与校验', prefixes: ['test/', 'tests/'] },
435
437
  { label: '模板与生成内容', prefixes: ['templates/'] },
436
438
  { label: '技能与协作配置', prefixes: ['.agents/'] },
437
- { label: '文档与其他', prefixes: [] }
439
+ DOCUMENTATION_CATEGORY,
440
+ { label: '其他', prefixes: [] }
438
441
  ] as const;
439
442
 
440
443
  function emptyChangeTotals(): ChangeTotals {
@@ -443,7 +446,10 @@ function emptyChangeTotals(): ChangeTotals {
443
446
 
444
447
  function categoryForChangeFile(file: ChangeFile): (typeof CHANGE_CATEGORIES)[number] {
445
448
  const filePath = file.newPath || file.oldPath || '';
446
- return CHANGE_CATEGORIES.find((category) => category.prefixes.some((prefix) => filePath.startsWith(prefix))) || CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1]!;
449
+ const category = CHANGE_CATEGORIES.find((entry) => entry.prefixes.some((prefix) => prefix && filePath.startsWith(prefix)));
450
+ if (category) return category;
451
+ if (/\.(?:md|mdx|markdown|rst)$/i.test(filePath)) return DOCUMENTATION_CATEGORY;
452
+ return CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1]!;
447
453
  }
448
454
 
449
455
  function summarizeChangeCategories(files: ChangeFile[]): Array<{ label: string; totals: ChangeTotals }> {
@@ -553,7 +559,7 @@ function renderEvidence(evidence: Evidence): string {
553
559
  function renderPrechecks(checks: Precheck[]): string[] {
554
560
  return checks.flatMap((check) => [
555
561
  `- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeReportText(check.rationale)}`,
556
- ...check.evidence.slice(0, 1).map(renderEvidence)
562
+ ...check.evidence.map(renderEvidence)
557
563
  ]);
558
564
  }
559
565
 
@@ -136,8 +136,8 @@ async function releaseNoteContext(
136
136
  options: ReleaseNoteOptions = {}
137
137
  ) {
138
138
  const historyLimit = input.historyLimit ?? 3;
139
- if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 20) {
140
- return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 20 are required', retryable: false } };
139
+ if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 100) {
140
+ return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 100 are required', retryable: false } };
141
141
  }
142
142
  const cwd = path.resolve(options.cwd || process.cwd());
143
143
  const platformType = options.platformType ?? configuredPlatform(cwd);
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
 
4
5
  import { detectRepoRoot } from '../task/resolve-ref.ts';
@@ -52,16 +53,26 @@ function parseArgs(args: string[], valueOptions: Set<string>, booleanOptions: Se
52
53
  }
53
54
 
54
55
  function dataRoot(repoRoot: string, requested?: string): string {
55
- const workspace = path.resolve(repoRoot, '.agents', 'workspace');
56
- const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
56
+ let boundary = repoRoot;
57
+ let input = requested;
58
+ if (input === undefined) {
59
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
60
+ if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
61
+ throw new Error('.agents/.airc.json must contain a valid project name in its project field');
62
+ }
63
+ boundary = path.resolve(os.homedir());
64
+ input = path.join(boundary, '.agent-infra', 'data-store', config.project);
65
+ }
66
+ const resolved = path.resolve(repoRoot, input);
57
67
  const relative = path.relative(repoRoot, resolved);
58
- if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('data root must be inside the repository');
68
+ if (requested !== undefined && (!relative || relative.startsWith('..') || path.isAbsolute(relative))) throw new Error('data root must be inside the repository');
69
+ const workspace = path.resolve(repoRoot, '.agents', 'workspace');
59
70
  const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
60
71
  if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
61
72
  throw new Error('data root cannot be the workspace root or a task directory');
62
73
  }
63
74
  let current = resolved;
64
- while (current !== repoRoot) {
75
+ while (current !== boundary) {
65
76
  if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('data root cannot traverse a symlink');
66
77
  current = path.dirname(current);
67
78
  }
@@ -14,7 +14,6 @@ export function readSandboxControlStatusForRow(
14
14
  : { mode: 'branch-only' as const };
15
15
  const control = sandboxControlPaths({
16
16
  base: config.controlBase,
17
- repoRoot: config.repoRoot,
18
17
  project: config.project,
19
18
  container: row.name,
20
19
  identity
@@ -1108,7 +1108,6 @@ export async function create(
1108
1108
  const container = containerName(effectiveConfig, branch);
1109
1109
  const controlPaths = sandboxControlPaths({
1110
1110
  base: effectiveConfig.controlBase,
1111
- repoRoot: effectiveConfig.repoRoot,
1112
1111
  project: effectiveConfig.project,
1113
1112
  container,
1114
1113
  identity: target.workspace
@@ -1493,6 +1492,7 @@ export async function create(
1493
1492
  shellConfigHostDir: hostShellConfig.hostDir,
1494
1493
  workspaceViewRoot: workspaceView.root,
1495
1494
  controlDir: control.channelDir,
1495
+ ...(target.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
1496
1496
  controlStatusDir: control.statusDir,
1497
1497
  ...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
1498
1498
  ...(target.workspace.mode === 'task-bound'
@@ -1588,8 +1588,6 @@ export async function create(
1588
1588
  ]
1589
1589
  : []),
1590
1590
  '-e',
1591
- `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1592
- '-e',
1593
1591
  `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1594
1592
  '-e',
1595
1593
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
@@ -1598,7 +1596,10 @@ export async function create(
1598
1596
  '-e',
1599
1597
  `AGENT_INFRA_CONTROL_ROOT_ID=${control.controlRootId}`,
1600
1598
  ...(target.workspace.mode === 'task-bound'
1601
- ? ['-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime']
1599
+ ? [
1600
+ '-e', 'AGENT_INFRA_CONTROL_ROOT_DIR=/run/agent-infra/control-root',
1601
+ '-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime'
1602
+ ]
1602
1603
  : []),
1603
1604
  '--label',
1604
1605
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
@@ -237,3 +237,16 @@ export function loadConfig({
237
237
  }
238
238
  };
239
239
  }
240
+
241
+ export function loadSandboxControlLocation(repoRoot = detectRepoRoot()): Readonly<{
242
+ controlBase: string;
243
+ project: string;
244
+ }> | null {
245
+ const configPath = path.join(repoRoot, '.agents', '.airc.json');
246
+ if (!fs.existsSync(configPath)) return null;
247
+ const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
248
+ if (typeof airc.project !== 'string' || !airc.project) return null;
249
+ const home = homedir();
250
+ if (!home) throw new Error('sandbox: home directory is required');
251
+ return { controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'), project: airc.project };
252
+ }
@@ -41,8 +41,7 @@ const AUDIT_MAX_BYTES = 1024 * 1024;
41
41
 
42
42
  export function requestAuditFields(manifest: SandboxControlManifest, request: SandboxControlRequest) {
43
43
  return {
44
- requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId,
45
- requestGeneration: request.generation, requestIssuedAt: request.issuedAt, requestExpiresAt: request.expiresAt
44
+ requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
46
45
  };
47
46
  }
48
47
 
@@ -2,17 +2,14 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import {
5
- SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
6
5
  SANDBOX_CONTROL_MAX_BYTES,
7
6
  SANDBOX_CONTROL_STATUS_STALE_MS,
8
7
  isSandboxControlFamily,
9
8
  type SandboxControlError,
10
9
  type SandboxControlRequest,
11
10
  type SandboxControlResponse,
12
- type SandboxAgentClientRequest,
13
11
  type SandboxTaskCreateRequest
14
12
  } from './protocol.ts';
15
- import { isAgentClientId } from '../../agent-clients/types.ts';
16
13
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
17
14
  import type { TaskCreateCandidateV1 } from '../../task/create.ts';
18
15
  import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
@@ -53,11 +50,10 @@ function clientError(
53
50
 
54
51
  function preflight(
55
52
  statusDir: string,
56
- generation: string,
57
53
  taskViewEffect: TaskViewAccessEffect | null = null,
58
54
  now = Date.now(),
59
55
  env: NodeJS.ProcessEnv = process.env
60
- ): void {
56
+ ): string {
61
57
  if (fs.existsSync(statusDir)) {
62
58
  let identity;
63
59
  try {
@@ -68,9 +64,6 @@ function preflight(
68
64
  : 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
69
65
  clientError(code, 'sandbox control identity is missing or invalid', false);
70
66
  }
71
- if (identity.generation !== generation) {
72
- clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
73
- }
74
67
  if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
75
68
  clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
76
69
  }
@@ -81,9 +74,6 @@ function preflight(
81
74
  } catch {
82
75
  clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
83
76
  }
84
- if (status.generation !== generation) {
85
- clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
86
- }
87
77
  if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
88
78
  clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
89
79
  }
@@ -107,6 +97,7 @@ function preflight(
107
97
  );
108
98
  }
109
99
  }
100
+ return status.generation;
110
101
  }
111
102
 
112
103
  function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
@@ -201,7 +192,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
201
192
  const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
202
193
  const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
203
194
  const taskViewEffect = taskViewEffectForRequest(request);
204
- preflight(statusDir, request.generation, taskViewEffect);
195
+ const hostGeneration = preflight(statusDir, taskViewEffect);
205
196
  const identityPath = path.join(statusDir, 'identity.json');
206
197
  if (fs.existsSync(statusDir)) {
207
198
  const identity = readSandboxControlIdentitySentinel(statusDir);
@@ -246,7 +237,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
246
237
  try {
247
238
  // The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
248
239
  accepted = true;
249
- response = readPublishedResponse(raw, request.id, channelDir, accepted, request.generation);
240
+ response = readPublishedResponse(raw, request.id, channelDir, accepted, hostGeneration);
250
241
  malformedResponseRaw = null;
251
242
  } catch (error) {
252
243
  if (!(error instanceof SyntaxError)) throw error;
@@ -268,7 +259,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
268
259
  }
269
260
  if (!accepted) {
270
261
  try {
271
- preflight(statusDir, request.generation, taskViewEffect);
262
+ preflight(statusDir, taskViewEffect);
272
263
  } catch (error) {
273
264
  if (cancelPendingRequest(requestPath)) throw error;
274
265
  clientError(
@@ -314,6 +305,9 @@ export function recoverSandboxControl(requestId: string, params: Readonly<{
314
305
  const config = loadConfig();
315
306
  const response = recoverSandboxControlFromHost(requestId, {
316
307
  managedRoot: path.join(config.controlBase, config.project),
308
+ controlBase: config.controlBase,
309
+ project: config.project,
310
+ repoRoot: config.repoRoot,
317
311
  timeoutMs: params.timeoutMs
318
312
  });
319
313
  if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
@@ -402,72 +396,30 @@ export function recoverSandboxControlFromChannel(requestId: string, params: Read
402
396
  );
403
397
  }
404
398
 
405
- function authority(params: { token?: string; generation?: string }): { token: string; generation: string } {
406
- const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
407
- const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
408
- if (!token || !generation) clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
409
- return { token, generation };
410
- }
411
-
412
399
  export function requestSandboxControl(params: Readonly<{
413
400
  family: string; args: string[]; channelDir?: string; statusDir?: string;
414
- token?: string; generation?: string; timeoutMs?: number;
401
+ timeoutMs?: number;
415
402
  }>): SandboxControlResponse {
416
403
  if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
417
- if (params.family === 'task-create' || params.family === 'agent-client') {
404
+ if (params.family === 'task-create') {
418
405
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
419
406
  }
420
407
  if (params.args[1] === 'complete') {
421
408
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
422
409
  }
423
- const auth = authority(params);
424
- const issuedAt = Date.now();
425
410
  const request: SandboxControlRequest = {
426
- version: 4, id: randomUUID(), ...auth, issuedAt,
427
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
411
+ version: 5, id: randomUUID(),
428
412
  family: params.family as 'task-lifecycle', args: params.args
429
413
  };
430
414
  return exchangeSandboxControl(request, params);
431
415
  }
432
416
 
433
- export function requestSandboxAgentOperation(params: Readonly<{
434
- agentClient: string;
435
- operation: string;
436
- payload: Record<string, unknown>;
437
- channelDir?: string;
438
- statusDir?: string;
439
- token?: string;
440
- generation?: string;
441
- timeoutMs?: number;
442
- }>): SandboxControlResponse {
443
- if (!isAgentClientId(params.agentClient)) {
444
- clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
445
- }
446
- const auth = authority(params);
447
- const issuedAt = Date.now();
448
- const request: SandboxAgentClientRequest = {
449
- version: 4,
450
- id: randomUUID(),
451
- ...auth,
452
- issuedAt,
453
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
454
- family: 'agent-client',
455
- agentClient: params.agentClient,
456
- operation: params.operation,
457
- payload: params.payload
458
- };
459
- return exchangeSandboxControl(request, params);
460
- }
461
-
462
417
  export function requestSandboxTaskCreate(params: Readonly<{
463
418
  candidate: TaskCreateCandidateV1; channelDir?: string; statusDir?: string;
464
- token?: string; generation?: string; timeoutMs?: number;
419
+ timeoutMs?: number;
465
420
  }>): SandboxControlResponse {
466
- const auth = authority(params);
467
- const issuedAt = Date.now();
468
421
  const request: SandboxTaskCreateRequest = {
469
- version: 4, id: randomUUID(), ...auth, issuedAt,
470
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
422
+ version: 5, id: randomUUID(),
471
423
  family: 'task-create', candidate: params.candidate,
472
424
  };
473
425
  return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
@@ -33,8 +33,6 @@ import {
33
33
  type TaskControlOperation
34
34
  } from '../../task/control-authority.ts';
35
35
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
36
- import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.ts';
37
- import type { AgentClientSandboxControlOperation } from '../../agent-clients/adapter.ts';
38
36
 
39
37
  export type SandboxControlExecutionResult = {
40
38
  exitCode: number;
@@ -256,7 +254,6 @@ function taskCreateExecutionFailure(requestId: string, code: string, message: st
256
254
 
257
255
  type ExecuteRequestOptions = Readonly<{
258
256
  createTask?: typeof createTask;
259
- agentClientOperation?: AgentClientSandboxControlOperation;
260
257
  }>;
261
258
 
262
259
  async function executeRequestInner(
@@ -279,10 +276,6 @@ async function executeRequestInner(
279
276
  return taskCreateExecutionFailure(request.id, code, detail);
280
277
  }
281
278
  }
282
- if (request.family === 'agent-client') {
283
- validateAgentClientOperation(request, manifest);
284
- return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
285
- }
286
279
  const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
287
280
  let operation: TaskControlOperation;
288
281
  try {
@@ -346,9 +339,10 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
346
339
  const gate = await waitForGate(nonce);
347
340
  const gateOwner = gate.owner;
348
341
  process.disconnect?.();
349
- const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
350
- if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
351
- const manifest = readSandboxControlManifest(manifestPath);
342
+ const requestedManifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
343
+ if (!requestedManifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
344
+ const manifest = readSandboxControlManifest(requestedManifestPath);
345
+ const manifestPath = fs.realpathSync.native(requestedManifestPath);
352
346
  const identity = validateSandboxControlIdentity({
353
347
  publicStatusDir: manifest.publicStatusDir,
354
348
  root: path.dirname(path.resolve(manifestPath)),
@@ -390,17 +384,17 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
390
384
  let result: SandboxControlExecutionResult;
391
385
  try {
392
386
  assertSandboxControlExecutorAuthority(manifest, gateOwner);
393
- const operation = request.family === 'task-lifecycle'
387
+ const operation = request.family === 'task-lifecycle'
394
388
  ? (() => {
395
389
  try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
396
390
  })()
397
- : request.family === 'agent-client' ? request.operation : null;
391
+ : null;
398
392
  const context = createSandboxControlAuditContext(manifest, {
399
393
  requestId: request.id,
400
394
  family: request.family,
401
395
  operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
402
396
  ? operation.request.intent
403
- : request.family === 'agent-client' ? request.operation : null,
397
+ : null,
404
398
  phase: 'started-committed',
405
399
  outcome: 'in-progress'
406
400
  });