@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
@@ -18,6 +18,32 @@ function boundedFailureDetail(result) {
18
18
  ? diagnostic
19
19
  : `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
20
20
  }
21
+ function requestDiagnostics(args, request, attempt, errorMessage) {
22
+ if (process.env.AGENT_INFRA_GH_DIAGNOSTICS !== '1' && !/rate.?limit/iu.test(errorMessage))
23
+ return '';
24
+ const apiIndex = args.indexOf('api');
25
+ const endpoint = apiIndex < 0
26
+ ? args.slice(0, 2).filter((arg) => !arg.startsWith('-')).join(' ')
27
+ : args.slice(apiIndex + 1).find((arg) => !arg.startsWith('-') && !arg.includes('=')) || 'unknown';
28
+ const safeEndpoint = endpoint.startsWith('http')
29
+ ? (() => {
30
+ try {
31
+ const url = new URL(endpoint);
32
+ return `${url.host}${url.pathname}`;
33
+ }
34
+ catch {
35
+ return 'unknown';
36
+ }
37
+ })()
38
+ : endpoint.split('?')[0] || 'unknown';
39
+ const methodIndex = args.findIndex((arg) => arg === '-X' || arg === '--method');
40
+ const methodFromArgs = methodIndex >= 0 ? args[methodIndex + 1] : undefined;
41
+ const method = methodFromArgs || request.method || (safeEndpoint === 'graphql' ? 'POST' : 'GET');
42
+ const tokenEnvironment = ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']
43
+ .filter((name) => Boolean(process.env[name]));
44
+ const configDirOverride = Boolean(process.env.GH_CONFIG_DIR);
45
+ return ` [gh-diagnostic endpoint=${safeEndpoint} method=${method} attempt=${attempt} tokenEnv=${tokenEnvironment.join(',') || 'none'} ghConfigDirOverride=${configDirOverride}]`;
46
+ }
21
47
  function defaultRunner(args, options) {
22
48
  const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
23
49
  let prefix = [];
@@ -135,14 +161,18 @@ function createGitHubClient(options = {}) {
135
161
  if (result.status === 0)
136
162
  return { ok: true, value: result.stdout };
137
163
  const error = classifyGitHubFailure(result);
164
+ const requestDetail = requestDiagnostics(args, request, attempt + 1, error.message);
165
+ const contextualError = requestDetail
166
+ ? { ...error, message: `${error.message}${requestDetail}` }
167
+ : error;
138
168
  if (!retryableMethod || !error.requestRetryable)
139
- return { ok: false, error: clientError(error) };
169
+ return { ok: false, error: clientError(contextualError) };
140
170
  if (attempt >= delays.length) {
141
171
  return {
142
172
  ok: false,
143
173
  error: error.code === 'NETWORK_TRANSIENT'
144
- ? { ...clientError(error), code: 'NETWORK_RETRY_EXHAUSTED', message: `${error.message} (retry exhausted)` }
145
- : clientError(error)
174
+ ? { ...clientError(contextualError), code: 'NETWORK_RETRY_EXHAUSTED', message: `${contextualError.message} (retry exhausted)` }
175
+ : clientError(contextualError)
146
176
  };
147
177
  }
148
178
  sleep(delays[attempt]);
@@ -106,8 +106,8 @@ function configuredPlatform(cwd) {
106
106
  }
107
107
  async function releaseNoteContext(input, options = {}) {
108
108
  const historyLimit = input.historyLimit ?? 3;
109
- if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 20) {
110
- 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 } };
109
+ if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 100) {
110
+ 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 } };
111
111
  }
112
112
  const cwd = path.resolve(options.cwd || process.cwd());
113
113
  const platformType = options.platformType ?? configuredPlatform(cwd);
@@ -8,7 +8,6 @@ export function readSandboxControlStatusForRow(config, row) {
8
8
  : { mode: 'branch-only' };
9
9
  const control = sandboxControlPaths({
10
10
  base: config.controlBase,
11
- repoRoot: config.repoRoot,
12
11
  project: config.project,
13
12
  container: row.name,
14
13
  identity
@@ -23,7 +23,7 @@ import { inspectSandboxControlContainer } from '../control/container-identity.js
23
23
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
24
24
  import { sandboxCoreBindMounts } from '../mounts.js';
25
25
  import { materializeSandboxGitMetadata } from '../git-metadata.js';
26
- import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, sandboxControlPaths, } from '../workspace-view.js';
26
+ import { assertSandboxTaskSource, finalizeSandboxControlManifest, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets, prepareSandboxCompletedTaskMountSource, sandboxControlPaths, } from '../workspace-view.js';
27
27
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
28
28
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
29
29
  import { captureSandboxAuthority, commandForSandboxAuthority, verifySandboxAuthority } from '../engines/authority.js';
@@ -741,7 +741,6 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
741
741
  const container = containerName(effectiveConfig, branch);
742
742
  const controlPaths = sandboxControlPaths({
743
743
  base: effectiveConfig.controlBase,
744
- repoRoot: effectiveConfig.repoRoot,
745
744
  project: effectiveConfig.project,
746
745
  container,
747
746
  identity: target.workspace
@@ -1038,7 +1037,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1038
1037
  const taskSource = target.workspace.mode === 'task-bound'
1039
1038
  ? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
1040
1039
  : null;
1041
- prepareSandboxWorkspaceMountTargets(worktree);
1040
+ const completedTaskSource = target.workspace.mode === 'task-bound'
1041
+ ? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
1042
+ : null;
1043
+ prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
1042
1044
  if (previousCutoverSnapshot) {
1043
1045
  assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
1044
1046
  if (previousManifest) {
@@ -1076,11 +1078,13 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1076
1078
  shellConfigHostDir: hostShellConfig.hostDir,
1077
1079
  workspaceViewRoot: workspaceView.root,
1078
1080
  controlDir: control.channelDir,
1081
+ ...(target.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
1079
1082
  controlStatusDir: control.statusDir,
1080
1083
  ...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
1081
1084
  ...(target.workspace.mode === 'task-bound'
1082
1085
  ? {
1083
1086
  taskSources: [taskSource],
1087
+ completedTaskSource: completedTaskSource,
1084
1088
  taskId: target.workspace.taskId
1085
1089
  }
1086
1090
  : {})
@@ -1158,8 +1162,6 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1158
1162
  ]
1159
1163
  : []),
1160
1164
  '-e',
1161
- `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1162
- '-e',
1163
1165
  `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1164
1166
  '-e',
1165
1167
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
@@ -1168,7 +1170,10 @@ export async function create(args, { runProjectInitCommand = true, workspace: wo
1168
1170
  '-e',
1169
1171
  `AGENT_INFRA_CONTROL_ROOT_ID=${control.controlRootId}`,
1170
1172
  ...(target.workspace.mode === 'task-bound'
1171
- ? ['-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime']
1173
+ ? [
1174
+ '-e', 'AGENT_INFRA_CONTROL_ROOT_DIR=/run/agent-infra/control-root',
1175
+ '-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime'
1176
+ ]
1172
1177
  : []),
1173
1178
  '--label',
1174
1179
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
@@ -11,11 +11,10 @@ export { containerListFormat, parseLabels } from './list-running.js';
11
11
  const USAGE = `Usage: ai sandbox ls
12
12
 
13
13
  Lists all containers for the current project. The '#' column is a
14
- display-only row number; the 'SHORT' column shows the active task short
15
- id bound to each container's branch (via
16
- .agents/workspace/active/.short-ids.json), or '-' if no active task is
17
- bound. Pass the SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
18
- A SHORT value of '-' means this container has no active task short id and may
14
+ display-only row number; the 'SHORT' column shows the task short id bound to
15
+ each task-bound container, or '-' if no short id is registered. Pass the
16
+ SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
17
+ A SHORT value of '-' means this container has no task short id and may
19
18
  be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
20
19
  remove a specific task-bound sandbox.
21
20
 
@@ -7,7 +7,7 @@ import { sandboxBranchLabel, sandboxLabel, sandboxTaskIdLabel, sandboxWorkspaceM
7
7
  import { fetchSandboxRows } from './list-running.js';
8
8
  import { rmOne, rmPurge, rmUnbound } from '../removal.js';
9
9
  const USAGE = `Usage:
10
- ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
10
+ ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a retained short id or full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
11
11
  ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
12
12
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
13
13
  function resolveMissingTaskSandboxBranch(taskId, config) {
@@ -153,4 +153,16 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
153
153
  }
154
154
  };
155
155
  }
156
+ export function loadSandboxControlLocation(repoRoot = detectRepoRoot()) {
157
+ const configPath = path.join(repoRoot, '.agents', '.airc.json');
158
+ if (!fs.existsSync(configPath))
159
+ return null;
160
+ const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
161
+ if (typeof airc.project !== 'string' || !airc.project)
162
+ return null;
163
+ const home = homedir();
164
+ if (!home)
165
+ throw new Error('sandbox: home directory is required');
166
+ return { controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'), project: airc.project };
167
+ }
156
168
  //# sourceMappingURL=config.js.map
@@ -14,8 +14,7 @@ const SAFE_OPERATION = /^[a-z][a-z0-9.-]{0,96}$/u;
14
14
  const AUDIT_MAX_BYTES = 1024 * 1024;
15
15
  export function requestAuditFields(manifest, request) {
16
16
  return {
17
- requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId,
18
- requestGeneration: request.generation, requestIssuedAt: request.issuedAt, requestExpiresAt: request.expiresAt
17
+ requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
19
18
  };
20
19
  }
21
20
  export function resultAuditFields(result) {
@@ -1,9 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
5
- import { isAgentClientId } from '../../agent-clients/types.js';
6
- import { normalizeAgentToken } from '../../agent-clients/tokens.js';
4
+ import { SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
7
5
  import { readSandboxControlPayload, readSandboxControlStatus } from './state.js';
8
6
  import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
9
7
  import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
@@ -12,7 +10,6 @@ import { hasTaskBoundMarker, isSandbox } from '../environment.js';
12
10
  import { loadConfig } from '../config.js';
13
11
  import { recoverSandboxControlFromHost } from './host-recovery.js';
14
12
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
15
- const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
16
13
  export class SandboxControlClientError extends Error {
17
14
  detail;
18
15
  accepted;
@@ -31,7 +28,7 @@ function sleep(milliseconds) {
31
28
  function clientError(code, message, retryable, accepted = false, requestId = null) {
32
29
  throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted, requestId);
33
30
  }
34
- function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(), env = process.env) {
31
+ function preflight(statusDir, taskViewEffect = null, now = Date.now(), env = process.env) {
35
32
  if (fs.existsSync(statusDir)) {
36
33
  let identity;
37
34
  try {
@@ -43,9 +40,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
43
40
  : 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
44
41
  clientError(code, 'sandbox control identity is missing or invalid', false);
45
42
  }
46
- if (identity.generation !== generation) {
47
- clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
48
- }
49
43
  if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
50
44
  clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
51
45
  }
@@ -57,9 +51,6 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
57
51
  catch {
58
52
  clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
59
53
  }
60
- if (status.generation !== generation) {
61
- clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
62
- }
63
54
  if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
64
55
  clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
65
56
  }
@@ -81,12 +72,11 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
81
72
  clientError(access.reasonCode ?? 'SANDBOX_TASK_VIEW_DENIED', access.message ?? 'task view denied', false);
82
73
  }
83
74
  }
75
+ return status.generation;
84
76
  }
85
77
  function taskViewEffectForRequest(request) {
86
78
  if (request.family === 'task-lifecycle')
87
79
  return 'progress';
88
- if (request.family === 'task-finalization')
89
- return 'recovery';
90
80
  return null;
91
81
  }
92
82
  function parseResponse(raw, id, accepted = false) {
@@ -169,7 +159,7 @@ function exchangeSandboxControl(request, params) {
169
159
  const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
170
160
  const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
171
161
  const taskViewEffect = taskViewEffectForRequest(request);
172
- preflight(statusDir, request.generation, taskViewEffect);
162
+ const hostGeneration = preflight(statusDir, taskViewEffect);
173
163
  const identityPath = path.join(statusDir, 'identity.json');
174
164
  if (fs.existsSync(statusDir)) {
175
165
  const identity = readSandboxControlIdentitySentinel(statusDir);
@@ -180,9 +170,6 @@ function exchangeSandboxControl(request, params) {
180
170
  || (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
181
171
  clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
182
172
  }
183
- if (identity.mode === 'branch-only' && request.family === 'task-finalization') {
184
- clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
185
- }
186
173
  }
187
174
  const encoded = `${JSON.stringify(request)}\n`;
188
175
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
@@ -220,7 +207,7 @@ function exchangeSandboxControl(request, params) {
220
207
  try {
221
208
  // The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
222
209
  accepted = true;
223
- response = readPublishedResponse(raw, request.id, channelDir, accepted, request.generation);
210
+ response = readPublishedResponse(raw, request.id, channelDir, accepted, hostGeneration);
224
211
  malformedResponseRaw = null;
225
212
  }
226
213
  catch (error) {
@@ -246,7 +233,7 @@ function exchangeSandboxControl(request, params) {
246
233
  }
247
234
  if (!accepted) {
248
235
  try {
249
- preflight(statusDir, request.generation, taskViewEffect);
236
+ preflight(statusDir, taskViewEffect);
250
237
  }
251
238
  catch (error) {
252
239
  if (cancelPendingRequest(requestPath))
@@ -273,6 +260,9 @@ export function recoverSandboxControl(requestId, params = {}) {
273
260
  const config = loadConfig();
274
261
  const response = recoverSandboxControlFromHost(requestId, {
275
262
  managedRoot: path.join(config.controlBase, config.project),
263
+ controlBase: config.controlBase,
264
+ project: config.project,
265
+ repoRoot: config.repoRoot,
276
266
  timeoutMs: params.timeoutMs
277
267
  });
278
268
  if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
@@ -359,109 +349,24 @@ export function recoverSandboxControlFromChannel(requestId, params) {
359
349
  }
360
350
  clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request did not produce a final result; inspect domain state before retrying', false, accepted, requestId);
361
351
  }
362
- function authority(params) {
363
- const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
364
- const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
365
- if (!token || !generation)
366
- clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
367
- return { token, generation };
368
- }
369
352
  export function requestSandboxControl(params) {
370
353
  if (!isSandboxControlFamily(params.family))
371
354
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
372
- if (params.family === 'task-create' || params.family === 'agent-client' || params.family === 'task-finalization') {
355
+ if (params.family === 'task-create') {
373
356
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
374
357
  }
375
- const auth = authority(params);
376
- const issuedAt = Date.now();
377
- const request = {
378
- version: 4, id: randomUUID(), ...auth, issuedAt,
379
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
380
- family: params.family, args: params.args
381
- };
382
- return exchangeSandboxControl(request, params);
383
- }
384
- export function requestSandboxAgentOperation(params) {
385
- if (!isAgentClientId(params.agentClient)) {
386
- clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
358
+ if (params.args[1] === 'complete') {
359
+ clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
387
360
  }
388
- const auth = authority(params);
389
- const issuedAt = Date.now();
390
361
  const request = {
391
- version: 4,
392
- id: randomUUID(),
393
- ...auth,
394
- issuedAt,
395
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
396
- family: 'agent-client',
397
- agentClient: params.agentClient,
398
- operation: params.operation,
399
- payload: params.payload
362
+ version: 5, id: randomUUID(),
363
+ family: params.family, args: params.args
400
364
  };
401
365
  return exchangeSandboxControl(request, params);
402
366
  }
403
- export function requestSandboxTaskFinalization(params) {
404
- const agent = normalizeAgentToken(params.agent);
405
- if (!agent)
406
- clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
407
- const auth = authority(params);
408
- const issuedAt = Date.now();
409
- const id = randomUUID();
410
- const request = {
411
- version: 4,
412
- id,
413
- ...auth,
414
- issuedAt,
415
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
416
- family: 'task-finalization',
417
- operation: 'complete',
418
- agent,
419
- args: []
420
- };
421
- let response;
422
- try {
423
- response = exchangeSandboxControl(request, params);
424
- }
425
- catch (error) {
426
- if (!(error instanceof SandboxControlClientError)
427
- || error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
428
- || !error.accepted || !error.requestId)
429
- throw error;
430
- return recoverAcceptedTaskFinalization(error.requestId, params);
431
- }
432
- if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
433
- return recoverAcceptedTaskFinalization(response.id, params);
434
- }
435
- return response;
436
- }
437
- export function recoverAcceptedTaskFinalization(requestId, params, recover = recoverSandboxControl) {
438
- const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
439
- while (Date.now() < deadline) {
440
- const timeoutMs = Math.min(30_000, deadline - Date.now());
441
- if (timeoutMs <= 0)
442
- break;
443
- try {
444
- const response = recover(requestId, { timeoutMs });
445
- if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
446
- continue;
447
- return response;
448
- }
449
- catch (error) {
450
- if (error instanceof SandboxControlClientError
451
- && error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
452
- && error.requestId === requestId)
453
- continue;
454
- throw error;
455
- }
456
- }
457
- clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id', false, true, requestId);
458
- }
459
367
  export function requestSandboxTaskCreate(params) {
460
- const auth = authority(params);
461
- const issuedAt = Date.now();
462
368
  const request = {
463
- version: 4, id: randomUUID(), ...auth, issuedAt,
464
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
369
+ version: 5, id: randomUUID(),
465
370
  family: 'task-create', candidate: params.candidate,
466
371
  };
467
372
  return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
@@ -4,15 +4,12 @@ import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { getProcessStartTime } from '../../server/process-state.js';
6
6
  import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
7
- import { applyTaskFinalization } from '../../task/finalization.js';
8
- import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.js';
9
7
  import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
10
8
  import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
11
9
  import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.js';
12
10
  import { validateSandboxControlIdentity } from './identity-sentinel.js';
13
11
  import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../../task/control-authority.js';
14
12
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
15
- import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.js';
16
13
  function sameBrokerOwner(left, right) {
17
14
  return left.version === right.version
18
15
  && left.pid === right.pid
@@ -192,21 +189,6 @@ function taskCreateExecutionFailure(requestId, code, message) {
192
189
  recovery: 'inspect-domain-state'
193
190
  }));
194
191
  }
195
- function finalizationResult(result) {
196
- const payload = {
197
- version: 1,
198
- status: result.status,
199
- changed: result.changed,
200
- accepted: true,
201
- result,
202
- error: result.error
203
- };
204
- return {
205
- exitCode: result.status === 'completed' ? 0 : result.status === 'blocked' ? 2 : 1,
206
- stdout: `${JSON.stringify(payload)}\n`,
207
- stderr: ''
208
- };
209
- }
210
192
  async function executeRequestInner(manifest, manifestPath, request, options = {}) {
211
193
  if (request.family === 'task-create') {
212
194
  try {
@@ -223,26 +205,6 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
223
205
  return taskCreateExecutionFailure(request.id, code, detail);
224
206
  }
225
207
  }
226
- if (request.family === 'agent-client') {
227
- validateAgentClientOperation(request, manifest);
228
- return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
229
- }
230
- if (request.family === 'task-finalization') {
231
- const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
232
- if (parsed.family !== 'task-finalization')
233
- throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
234
- const operation = parsed;
235
- const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
236
- repoRoot: manifest.repoRoot,
237
- worktreeRoot: manifest.worktreeRoot,
238
- runtimeDir: manifest.runtimeDir,
239
- taskId: manifest.taskId,
240
- generation: manifest.generation,
241
- manifestPath,
242
- requestId: request.id
243
- }), operation);
244
- return finalizationResult(await result);
245
- }
246
208
  const boundArgs = bindSandboxControlTask(request, manifest.taskId);
247
209
  let operation;
248
210
  try {
@@ -302,10 +264,11 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
302
264
  const gate = await waitForGate(nonce);
303
265
  const gateOwner = gate.owner;
304
266
  process.disconnect?.();
305
- const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
306
- if (!manifestPath)
267
+ const requestedManifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
268
+ if (!requestedManifestPath)
307
269
  throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
308
- const manifest = readSandboxControlManifest(manifestPath);
270
+ const manifest = readSandboxControlManifest(requestedManifestPath);
271
+ const manifestPath = fs.realpathSync.native(requestedManifestPath);
309
272
  const identity = validateSandboxControlIdentity({
310
273
  publicStatusDir: manifest.publicStatusDir,
311
274
  root: path.dirname(path.resolve(manifestPath)),
@@ -357,13 +320,13 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
357
320
  return null;
358
321
  }
359
322
  })()
360
- : request.family === 'agent-client' ? request.operation : null;
323
+ : null;
361
324
  const context = createSandboxControlAuditContext(manifest, {
362
325
  requestId: request.id,
363
326
  family: request.family,
364
327
  operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
365
328
  ? operation.request.intent
366
- : request.family === 'task-finalization' || request.family === 'agent-client' ? request.operation : null,
329
+ : null,
367
330
  phase: 'started-committed',
368
331
  outcome: 'in-progress'
369
332
  });
@@ -380,22 +343,9 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
380
343
  });
381
344
  const detail = error instanceof Error ? error.message : String(error);
382
345
  const code = /^([A-Z][A-Z0-9_]+)/u.exec(detail)?.[1] ?? 'SANDBOX_CONTROL_EXECUTOR_FAILED';
383
- result = request.family === 'task-finalization'
384
- ? {
385
- exitCode: 1,
386
- stdout: serializeTaskFinalizationEnvelope({
387
- status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
388
- changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
389
- accepted: true,
390
- requestId: request.id,
391
- result: null,
392
- error: { code, message: detail, retryable: false }
393
- }),
394
- stderr: ''
395
- }
396
- : request.family === 'task-create'
397
- ? taskCreateExecutionFailure(request.id, code, detail)
398
- : lifecycleFailure(code, detail);
346
+ result = request.family === 'task-create'
347
+ ? taskCreateExecutionFailure(request.id, code, detail)
348
+ : lifecycleFailure(code, detail);
399
349
  }
400
350
  process.stdout.write(result.stdout);
401
351
  process.stderr.write(result.stderr);