@fitlab-ai/agent-infra 0.9.3 → 0.9.4

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 (175) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -123
  6. package/dist/lib/agent-clients/adapter.js +92 -2
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  12. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  13. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  14. package/dist/lib/agent-clients/registry.js +9 -1
  15. package/dist/lib/internal/sandbox-control.js +24 -0
  16. package/dist/lib/internal/task-activity.js +45 -13
  17. package/dist/lib/internal/task-ledger.js +12 -3
  18. package/dist/lib/internal/task-lifecycle.js +19 -1
  19. package/dist/lib/internal/task-orchestration.js +122 -18
  20. package/dist/lib/platform/github-client.js +19 -2
  21. package/dist/lib/platform/pr-checks.js +1 -1
  22. package/dist/lib/platform/pull-requests.js +47 -1
  23. package/dist/lib/platform/releases.js +13 -5
  24. package/dist/lib/process-data/index.js +239 -0
  25. package/dist/lib/process-data/normalize.js +198 -0
  26. package/dist/lib/process-data/privacy.js +33 -0
  27. package/dist/lib/process-data/reconcile.js +95 -0
  28. package/dist/lib/process-data/repair.js +58 -0
  29. package/dist/lib/process-data/sources.js +345 -0
  30. package/dist/lib/process-data/store.js +282 -0
  31. package/dist/lib/process-data/types.js +2 -0
  32. package/dist/lib/run/index.js +4 -3
  33. package/dist/lib/run/tui.js +7 -8
  34. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  35. package/dist/lib/sandbox/capture.js +7 -2
  36. package/dist/lib/sandbox/commands/create.js +109 -228
  37. package/dist/lib/sandbox/commands/enter.js +15 -47
  38. package/dist/lib/sandbox/commands/list-running.js +16 -6
  39. package/dist/lib/sandbox/commands/ls.js +10 -6
  40. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  41. package/dist/lib/sandbox/commands/refresh.js +4 -84
  42. package/dist/lib/sandbox/commands/rm.js +24 -3
  43. package/dist/lib/sandbox/commands/show.js +12 -3
  44. package/dist/lib/sandbox/commands/start.js +7 -4
  45. package/dist/lib/sandbox/config.js +2 -0
  46. package/dist/lib/sandbox/constants.js +6 -0
  47. package/dist/lib/sandbox/control/client.js +44 -0
  48. package/dist/lib/sandbox/control/protocol.js +35 -0
  49. package/dist/lib/sandbox/control/server.js +109 -0
  50. package/dist/lib/sandbox/dockerfile.js +36 -13
  51. package/dist/lib/sandbox/mounts.js +17 -4
  52. package/dist/lib/sandbox/recovery.js +128 -5
  53. package/dist/lib/sandbox/redaction.js +12 -0
  54. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  55. package/dist/lib/sandbox/workspace-identity.js +95 -0
  56. package/dist/lib/sandbox/workspace-view.js +103 -0
  57. package/dist/lib/task/activity-intent.js +257 -51
  58. package/dist/lib/task/commit-finalization.js +267 -0
  59. package/dist/lib/task/commit-intent.js +165 -0
  60. package/dist/lib/task/decision-intents.js +31 -17
  61. package/dist/lib/task/delegation-receipts.js +2 -2
  62. package/dist/lib/task/implementation-inputs.js +54 -7
  63. package/dist/lib/task/ledger-intents.js +35 -1
  64. package/dist/lib/task/orchestration.js +363 -28
  65. package/dist/lib/task/resolve-ref.js +41 -1
  66. package/lib/agent-clients/adapter.ts +126 -2
  67. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  68. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  69. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  70. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  71. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  72. package/lib/agent-clients/adapters/opencode.ts +18 -3
  73. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  74. package/lib/agent-clients/registry.ts +9 -1
  75. package/lib/internal/sandbox-control.ts +24 -0
  76. package/lib/internal/task-activity.ts +50 -14
  77. package/lib/internal/task-ledger.ts +12 -3
  78. package/lib/internal/task-lifecycle.ts +22 -1
  79. package/lib/internal/task-orchestration.ts +81 -11
  80. package/lib/platform/github-client.ts +20 -2
  81. package/lib/platform/pr-checks.ts +1 -1
  82. package/lib/platform/pull-requests.ts +49 -1
  83. package/lib/platform/releases.ts +16 -4
  84. package/lib/process-data/index.ts +224 -0
  85. package/lib/process-data/normalize.ts +202 -0
  86. package/lib/process-data/privacy.ts +41 -0
  87. package/lib/process-data/reconcile.ts +110 -0
  88. package/lib/process-data/repair.ts +64 -0
  89. package/lib/process-data/sources.ts +354 -0
  90. package/lib/process-data/store.ts +315 -0
  91. package/lib/process-data/types.ts +135 -0
  92. package/lib/run/index.ts +5 -3
  93. package/lib/run/tui.ts +11 -8
  94. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  95. package/lib/sandbox/capture.ts +9 -2
  96. package/lib/sandbox/commands/create.ts +126 -260
  97. package/lib/sandbox/commands/enter.ts +13 -59
  98. package/lib/sandbox/commands/list-running.ts +23 -6
  99. package/lib/sandbox/commands/ls.ts +22 -6
  100. package/lib/sandbox/commands/rebuild.ts +4 -3
  101. package/lib/sandbox/commands/refresh.ts +4 -126
  102. package/lib/sandbox/commands/rm.ts +24 -3
  103. package/lib/sandbox/commands/show.ts +29 -3
  104. package/lib/sandbox/commands/start.ts +6 -4
  105. package/lib/sandbox/config.ts +4 -0
  106. package/lib/sandbox/constants.ts +8 -0
  107. package/lib/sandbox/control/client.ts +55 -0
  108. package/lib/sandbox/control/protocol.ts +75 -0
  109. package/lib/sandbox/control/server.ts +117 -0
  110. package/lib/sandbox/dockerfile.ts +66 -13
  111. package/lib/sandbox/mounts.ts +24 -4
  112. package/lib/sandbox/recovery.ts +163 -6
  113. package/lib/sandbox/redaction.ts +14 -0
  114. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  115. package/lib/sandbox/tool-types.ts +16 -0
  116. package/lib/sandbox/workspace-identity.ts +123 -0
  117. package/lib/sandbox/workspace-view.ts +143 -0
  118. package/lib/task/activity-intent.ts +350 -61
  119. package/lib/task/commit-finalization.ts +356 -0
  120. package/lib/task/commit-intent.ts +230 -0
  121. package/lib/task/decision-intents.ts +32 -12
  122. package/lib/task/delegation-receipts.ts +2 -2
  123. package/lib/task/implementation-inputs.ts +62 -8
  124. package/lib/task/ledger-intents.ts +43 -3
  125. package/lib/task/orchestration.ts +501 -33
  126. package/lib/task/resolve-ref.ts +46 -1
  127. package/package.json +1 -1
  128. package/templates/.agents/rules/README.en.md +1 -1
  129. package/templates/.agents/rules/README.zh-CN.md +1 -1
  130. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  131. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  132. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  133. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  134. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  135. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  136. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  137. package/templates/.agents/rules/task-management.en.md +2 -0
  138. package/templates/.agents/rules/task-management.zh-CN.md +2 -0
  139. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.en.md +21 -11
  142. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
  143. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  144. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  149. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  150. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  151. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  152. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  153. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  156. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  157. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  158. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  159. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  160. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  161. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  162. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  163. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  164. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  165. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  166. package/templates/.agents/templates/task.en.md +1 -1
  167. package/templates/.agents/templates/task.zh-CN.md +1 -1
  168. package/templates/.claude/commands/watch-pr.en.md +1 -1
  169. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  170. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  171. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  172. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  173. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  174. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  175. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -1,6 +1,12 @@
1
1
  import { renderAgentClientInvocation } from '../agent-clients/invocation.js';
2
2
  import { getAgentClientAdapter } from '../agent-clients/registry.js';
3
3
  const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode']);
4
+ const TUI_AGENT_CLIENT_IDS = Object.freeze({
5
+ claude: 'claude-code',
6
+ codex: 'codex',
7
+ antigravity: 'antigravity-cli',
8
+ opencode: 'opencode'
9
+ });
4
10
  function isTuiName(value) {
5
11
  return typeof value === 'string' && TUI_NAMES.has(value);
6
12
  }
@@ -18,14 +24,7 @@ export function selectTui(skill, options) {
18
24
  return 'codex';
19
25
  }
20
26
  export function renderPrompt(params) {
21
- const suffix = [params.skill, ...params.args].join(' ').trim();
22
- if (params.tui === 'codex') {
23
- return renderAgentClientInvocation(getAgentClientAdapter('codex').invocation, {
24
- skillName: params.skill,
25
- args: params.args
26
- });
27
- }
28
- return `/${suffix}`;
27
+ return renderAgentClientInvocation(getAgentClientAdapter(TUI_AGENT_CLIENT_IDS[params.tui]).invocation, { skillName: params.skill, args: params.args });
29
28
  }
30
29
  export function buildTuiCommand(tui, prompt) {
31
30
  if (tui === 'claude')
@@ -62,6 +62,27 @@ function assertUniqueTools(tools) {
62
62
  seen.add(tool.id);
63
63
  }
64
64
  }
65
+ function unique(values) {
66
+ return Object.freeze([...new Set(values)]);
67
+ }
68
+ function sandboxImageContribution(adapters, tools) {
69
+ const homePrefix = '/home/devuser/';
70
+ const derivedExclusions = tools.map((tool) => {
71
+ if (!tool.containerMount.startsWith(homePrefix)) {
72
+ throw new Error(`Agent Client '${tool.id}' container mount must be below ${homePrefix.slice(0, -1)}`);
73
+ }
74
+ return tool.containerMount.slice(homePrefix.length);
75
+ });
76
+ return Object.freeze({
77
+ dockerfileFragments: unique(adapters.flatMap((adapter) => adapter.sandbox.image?.dockerfileFragment
78
+ ? [adapter.sandbox.image.dockerfileFragment]
79
+ : [])),
80
+ dotfilesExclusions: unique([
81
+ ...derivedExclusions,
82
+ ...adapters.flatMap((adapter) => adapter.sandbox.image?.dotfilesExclusions ?? [])
83
+ ])
84
+ });
85
+ }
65
86
  function hooksByPhase(adapters) {
66
87
  const phases = [
67
88
  'prepare',
@@ -108,6 +129,7 @@ function createSandboxCapabilityPlan(config) {
108
129
  }
109
130
  const tools = Object.freeze([...nonClientTools, ...selectedTools, ...selectedCustomTools]);
110
131
  assertUniqueTools(tools);
132
+ const image = sandboxImageContribution(selectedAgentClients, selectedTools);
111
133
  const phaseHooks = hooksByPhase(selectedAgentClients);
112
134
  const hookProjection = selectedAgentClients.flatMap((adapter) => adapter.sandbox.hooks.map((hook) => ({
113
135
  adapterId: adapter.id,
@@ -134,7 +156,8 @@ function createSandboxCapabilityPlan(config) {
134
156
  tools: Object.freeze(tools.map(runtimeToolProjection)),
135
157
  selectedAgentClients: Object.freeze(selectedAgentClients.map((adapter) => adapter.id)),
136
158
  hooks: Object.freeze(hookProjection),
137
- recoveryChecks: recoveryCheckProjection
159
+ recoveryChecks: recoveryCheckProjection,
160
+ image
138
161
  });
139
162
  const cleanupInventory = Object.freeze([
140
163
  agentInfraTool(config.home),
@@ -148,6 +171,7 @@ function createSandboxCapabilityPlan(config) {
148
171
  hooksByPhase: phaseHooks,
149
172
  recoveryChecks,
150
173
  aliases: Object.freeze(listAgentClientAdapters().flatMap((adapter) => adapter.sandbox.aliases)),
174
+ image,
151
175
  imageSignature: hash(tools.map(({ id, install }) => ({ id, install }))),
152
176
  runtimeSignature: hash(runtimeProjection),
153
177
  runtimeProjection,
@@ -230,5 +254,5 @@ function runBoundedSandboxHookCommand(command, options) {
230
254
  });
231
255
  });
232
256
  }
233
- export { DEFAULT_SANDBOX_HOOK_TIMEOUT_MS, createSandboxCapabilityPlan, runBoundedSandboxHookCommand, runSandboxHooks };
257
+ export { DEFAULT_SANDBOX_HOOK_TIMEOUT_MS, createSandboxCapabilityPlan, runBoundedSandboxHookCommand, runSandboxHooks, sandboxImageContribution };
234
258
  //# sourceMappingURL=agent-client-reconciler.js.map
@@ -6,6 +6,7 @@ import { commandForEngine } from './shell.js';
6
6
  import { ensureSandboxReady } from './recovery.js';
7
7
  import { hostTimezoneEnvFlags, terminalEnvFlags } from './commands/enter.js';
8
8
  import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
9
+ import { resolveSandboxTarget } from './workspace-identity.js';
9
10
  async function spawnCapture(file, args) {
10
11
  return new Promise((resolve, reject) => {
11
12
  const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] });
@@ -122,6 +123,8 @@ EOF
122
123
  export async function runInSandbox(request, options = {}) {
123
124
  const config = options.engine ? null : loadConfig();
124
125
  const engine = options.engine ?? detectEngine(config);
126
+ const workspace = request.workspace
127
+ ?? (config ? resolveSandboxTarget(request.taskRef, config.repoRoot).workspace : undefined);
125
128
  const rows = options.rows ??
126
129
  (() => {
127
130
  const fetched = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
@@ -136,6 +139,7 @@ export async function runInSandbox(request, options = {}) {
136
139
  if (options.ensureReady) {
137
140
  container = (await options.ensureReady({
138
141
  branch: request.branch,
142
+ workspace,
139
143
  row: found,
140
144
  allowRecreate: request.recreate ?? false
141
145
  })).container;
@@ -145,11 +149,12 @@ export async function runInSandbox(request, options = {}) {
145
149
  config,
146
150
  engine,
147
151
  branch: request.branch,
152
+ workspace,
148
153
  row: found,
149
154
  allowRecreate: request.recreate,
150
- recreate: async (targetBranch) => {
155
+ recreate: async () => {
151
156
  const { create } = await import('./commands/create.js');
152
- await create([targetBranch, '--no-refresh']);
157
+ await create([request.taskRef, '--no-refresh']);
153
158
  }
154
159
  })).container;
155
160
  }
@@ -2,32 +2,33 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
- import { execFileSync } from 'node:child_process';
5
+ import { execFileSync, spawn } from 'node:child_process';
6
6
  import { parseArgs } from 'node:util';
7
+ import { fileURLToPath } from 'node:url';
7
8
  import * as p from '@clack/prompts';
8
9
  import pc from 'picocolors';
9
10
  import { listAgentClientAdapters } from '../../agent-clients/registry.js';
10
11
  import { loadConfig } from '../config.js';
11
- import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, sandboxRuntimeCapabilityLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
12
+ import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, sandboxRuntimeCapabilityLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
12
13
  import { prepareDockerfile } from '../dockerfile.js';
13
14
  import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
14
15
  import { detectEngine, ensureDocker } from '../engine.js';
15
16
  import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from '../shell.js';
16
- import { resolveTaskBranch } from '../task-resolver.js';
17
+ import { parseSandboxWorkspaceIdentity, resolveSandboxTarget, sameSandboxWorkspaceIdentity } from '../workspace-identity.js';
17
18
  import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookCommand } from '../agent-client-reconciler.js';
18
19
  import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
19
20
  import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts } from '../recovery.js';
20
21
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
21
22
  import { sandboxCoreBindMounts } from '../mounts.js';
23
+ import { assertSandboxTaskSource, materializeSandboxControl, materializeSandboxWorkspaceView } from '../workspace-view.js';
22
24
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
23
25
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
24
26
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
25
27
  import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.js';
26
28
  import { removeDirRecursive } from '../../remove-dir.js';
27
- import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
29
+ import { redactCommandError } from '../redaction.js';
28
30
  import { detectHostTimezone } from '../host-timezone.js';
29
31
  import { buildImageSignature, buildSandboxImageArgs, isRefreshDisabled, isRefreshDue, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
30
- export { ensureCodexModelInheritance, ensureCodexWorkspaceTrust } from '../../agent-clients/adapters/codex-sandbox.js';
31
32
  const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
32
33
  const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
33
34
  const CONTAINER_HOME = '/home/devuser';
@@ -64,6 +65,11 @@ const MANAGED_GITHUB_GIT_CONFIG = [
64
65
  '[url "https://github.com/"]',
65
66
  '\tinsteadOf = git@github.com:'
66
67
  ];
68
+ function internalCliPath() {
69
+ const directory = path.dirname(fileURLToPath(import.meta.url));
70
+ const extension = path.extname(fileURLToPath(import.meta.url));
71
+ return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
72
+ }
67
73
  function resolveToolDirs(config, tools, branch) {
68
74
  return tools.map((tool) => {
69
75
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -544,191 +550,6 @@ export function assertBranchAvailable(repoRoot, branch, { allowedWorktrees = [],
544
550
  }
545
551
  }
546
552
  }
547
- function readHostJsonSafe(filePath) {
548
- if (!filePath || !fs.existsSync(filePath)) {
549
- return null;
550
- }
551
- try {
552
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
553
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
554
- }
555
- catch {
556
- return null;
557
- }
558
- }
559
- const CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS = [
560
- 'model',
561
- 'fallbackModel',
562
- 'availableModels',
563
- 'modelOverrides',
564
- 'enforceAvailableModels',
565
- 'advisorModel',
566
- 'apiKeyHelper',
567
- 'effortLevel'
568
- ];
569
- function isJsonObjectRecord(value) {
570
- return value !== null && typeof value === 'object' && !Array.isArray(value);
571
- }
572
- function mergeMissingStringEnvFields(target, source) {
573
- if (!isJsonObjectRecord(source.env)) {
574
- return false;
575
- }
576
- if (Object.hasOwn(target, 'env') && !isJsonObjectRecord(target.env)) {
577
- return false;
578
- }
579
- let targetEnv = target.env;
580
- let changed = false;
581
- for (const [key, value] of Object.entries(source.env)) {
582
- if (typeof value !== 'string' || value === '') {
583
- continue;
584
- }
585
- if (!targetEnv) {
586
- targetEnv = {};
587
- target.env = targetEnv;
588
- }
589
- if (!Object.hasOwn(targetEnv, key)) {
590
- targetEnv[key] = value;
591
- changed = true;
592
- }
593
- }
594
- return changed;
595
- }
596
- function mergeMissingTopLevelSettings(target, source) {
597
- let changed = false;
598
- for (const key of CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS) {
599
- if (!Object.hasOwn(source, key) || Object.hasOwn(target, key)) {
600
- continue;
601
- }
602
- const value = source[key];
603
- if (value === null || value === undefined || value === '') {
604
- continue;
605
- }
606
- target[key] = value;
607
- changed = true;
608
- }
609
- return changed;
610
- }
611
- export function ensureClaudeOnboarding(toolDir, hostHomeDir) {
612
- const claudeJsonPath = path.join(toolDir, '.claude.json');
613
- let data = {};
614
- if (fs.existsSync(claudeJsonPath)) {
615
- try {
616
- data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
617
- }
618
- catch {
619
- // malformed JSON, start fresh
620
- }
621
- }
622
- let changed = false;
623
- if (!data.hasCompletedOnboarding) {
624
- data.hasCompletedOnboarding = true;
625
- changed = true;
626
- }
627
- if (!data.projects) {
628
- data.projects = {};
629
- changed = true;
630
- }
631
- if (!data.projects['/workspace']) {
632
- data.projects['/workspace'] = {};
633
- changed = true;
634
- }
635
- if (!data.projects['/workspace'].hasTrustDialogAccepted) {
636
- data.projects['/workspace'].hasTrustDialogAccepted = true;
637
- changed = true;
638
- }
639
- if (hostHomeDir) {
640
- const hostClaudeJson = readHostJsonSafe(path.join(hostHomeDir, '.claude.json'));
641
- if (hostClaudeJson
642
- && typeof hostClaudeJson.model === 'string'
643
- && hostClaudeJson.model !== ''
644
- && !Object.hasOwn(data, 'model')) {
645
- data.model = hostClaudeJson.model;
646
- changed = true;
647
- }
648
- // Claude Code launch-pins a default effort per model generation (for
649
- // example xhigh for Opus 4.7). The saved effortLevel is honored only after
650
- // a top-level boolean `unpin*LaunchEffort: true` flag unlocks it, so mirror
651
- // those flags here with the existing first-write semantics.
652
- //
653
- // Pattern matching avoids one patch per future model generation. If
654
- // Anthropic changes the naming convention, this block will no-op and should
655
- // be revisited.
656
- if (hostClaudeJson) {
657
- for (const key of Object.keys(hostClaudeJson)) {
658
- if (/^unpin.*LaunchEffort$/.test(key)
659
- && hostClaudeJson[key] === true
660
- && !Object.hasOwn(data, key)) {
661
- data[key] = true;
662
- changed = true;
663
- }
664
- }
665
- }
666
- }
667
- if (changed) {
668
- fs.writeFileSync(claudeJsonPath, JSON.stringify(data, null, 4), 'utf8');
669
- }
670
- }
671
- export function ensureClaudeSettings(toolDir, hostHomeDir) {
672
- const settingsPath = path.join(toolDir, 'settings.json');
673
- let data = {};
674
- if (fs.existsSync(settingsPath)) {
675
- try {
676
- data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
677
- }
678
- catch {
679
- // malformed JSON, start fresh
680
- }
681
- }
682
- let changed = false;
683
- if (data.skipDangerousModePermissionPrompt !== true) {
684
- data.skipDangerousModePermissionPrompt = true;
685
- changed = true;
686
- }
687
- if (hostHomeDir) {
688
- const hostSettings = readHostJsonSafe(path.join(hostHomeDir, '.claude', 'settings.json'));
689
- if (hostSettings) {
690
- changed = mergeMissingStringEnvFields(data, hostSettings) || changed;
691
- changed = mergeMissingTopLevelSettings(data, hostSettings) || changed;
692
- }
693
- }
694
- if (changed) {
695
- fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
696
- }
697
- }
698
- export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
699
- if (!hostHomeDir) {
700
- return;
701
- }
702
- const hostConfigPath = path.join(hostHomeDir, '.config', 'opencode', 'opencode.json');
703
- const hostJson = readHostJsonSafe(hostConfigPath);
704
- if (!hostJson) {
705
- return;
706
- }
707
- const sandboxConfigPath = path.join(toolDir, 'opencode.json');
708
- let sandboxJson = {};
709
- if (fs.existsSync(sandboxConfigPath)) {
710
- const existing = readHostJsonSafe(sandboxConfigPath);
711
- if (!existing) {
712
- return;
713
- }
714
- sandboxJson = existing;
715
- }
716
- let changed = false;
717
- for (const key of ['model', 'small_model']) {
718
- if (Object.hasOwn(sandboxJson, key)) {
719
- continue;
720
- }
721
- const value = hostJson[key];
722
- if (typeof value !== 'string' || value === '') {
723
- continue;
724
- }
725
- sandboxJson[key] = value;
726
- changed = true;
727
- }
728
- if (changed) {
729
- fs.writeFileSync(sandboxConfigPath, JSON.stringify(sandboxJson, null, 2), 'utf8');
730
- }
731
- }
732
553
  export function sandboxAliasesPath(home) {
733
554
  return hostJoin(home, '.agent-infra', 'aliases', 'sandbox.sh');
734
555
  }
@@ -848,10 +669,10 @@ export async function create(args) {
848
669
  throw new Error(USAGE);
849
670
  }
850
671
  validateSelinuxDisableEnv();
851
- validateClaudeCredentialsEnvOverride();
852
672
  const config = loadConfig();
853
673
  const [branchOrTaskId = '', base] = positionals;
854
- const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
674
+ const target = resolveSandboxTarget(branchOrTaskId, config.repoRoot);
675
+ const branch = target.branch;
855
676
  assertValidBranchName(branch);
856
677
  const effectiveConfig = {
857
678
  ...config,
@@ -866,39 +687,43 @@ export async function create(args) {
866
687
  const capabilityPlan = createSandboxCapabilityPlan(effectiveConfig);
867
688
  const tools = [...capabilityPlan.tools];
868
689
  const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
869
- // Fatal credential states still fail before filesystem/docker side effects.
870
- // Missing credentials leave the selected Claude Code capability installed
871
- // and mounted; only the absent live credential file is omitted.
872
- const credentialOutcome = prepareClaudeCredentials(effectiveConfig.home, effectiveConfig.project, resolvedTools);
690
+ const prepareHookResults = await runSandboxHooks({
691
+ hooks: capabilityPlan.hooksByPhase.prepare,
692
+ phase: 'prepare',
693
+ context: {
694
+ config: effectiveConfig,
695
+ plan: capabilityPlan,
696
+ create: {
697
+ hostHome: effectiveConfig.home,
698
+ hostEnv: { ...process.env },
699
+ project: effectiveConfig.project,
700
+ resolvedTools
701
+ }
702
+ },
703
+ runCommand: runBoundedSandboxHookCommand
704
+ });
705
+ const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
706
+ if (prepareFailure) {
707
+ throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
708
+ }
873
709
  const effectiveResolvedTools = resolvedTools;
874
710
  const container = containerName(effectiveConfig, branch);
875
711
  const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
876
712
  const shareCommon = shareCommonDir(effectiveConfig);
877
713
  const shareBranch = shareBranchDir(effectiveConfig, branch);
878
- const preparedDockerfile = prepareDockerfile(effectiveConfig);
714
+ const preparedDockerfile = prepareDockerfile(effectiveConfig, capabilityPlan.image);
879
715
  const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
880
716
  const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
881
717
  const engine = detectEngine(effectiveConfig);
882
718
  let createdTmpfsSeedPlan = [];
883
719
  p.intro(pc.cyan('AI Sandbox'));
884
720
  p.log.info(`Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`);
885
- if (credentialOutcome.status === 'SKIPPED') {
886
- p.log.warn('Claude Code credentials not found on host - creating this sandbox WITHOUT Claude Code credentials.\n'
887
- + ' Claude Code is still installed in the image but will not be authenticated.\n'
888
- + ' To enable it: run "claude" once on the host to complete login, then re-run "ai sandbox create".');
721
+ for (const result of prepareHookResults) {
722
+ if (result.status === 'warning' && result.message)
723
+ p.log.warn(result.message);
889
724
  }
890
725
  try {
891
726
  p.log.step('Checking container engine...');
892
- const prepareHookResults = await runSandboxHooks({
893
- hooks: capabilityPlan.hooksByPhase.prepare,
894
- phase: 'prepare',
895
- context: { config: effectiveConfig, plan: capabilityPlan },
896
- runCommand: runBoundedSandboxHookCommand
897
- });
898
- const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
899
- if (prepareFailure) {
900
- throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
901
- }
902
727
  await ensureDocker(effectiveConfig, (detail) => {
903
728
  p.log.info(` ${detail}`);
904
729
  });
@@ -1048,6 +873,32 @@ export async function create(args) {
1048
873
  const matchedContainers = containerNameCandidates(effectiveConfig, branch)
1049
874
  .filter((name) => existing.includes(name));
1050
875
  if (matchedContainers.length > 0) {
876
+ for (const name of matchedContainers) {
877
+ const rawLabels = runSafeEngine(engine, 'docker', [
878
+ 'inspect', '--format', '{{json .Config.Labels}}', name
879
+ ]).trim();
880
+ let labels = {};
881
+ try {
882
+ labels = JSON.parse(rawLabels);
883
+ }
884
+ catch {
885
+ labels = {};
886
+ }
887
+ const existingIdentity = parseSandboxWorkspaceIdentity(labels, {
888
+ mode: sandboxWorkspaceModeLabel(effectiveConfig),
889
+ taskId: sandboxTaskIdLabel(effectiveConfig)
890
+ });
891
+ if (!sameSandboxWorkspaceIdentity(existingIdentity, target.workspace)) {
892
+ const existingDescription = existingIdentity.mode === 'task-bound'
893
+ ? `task-bound:${existingIdentity.taskId}`
894
+ : existingIdentity.mode;
895
+ const requestedDescription = target.workspace.mode === 'task-bound'
896
+ ? `task-bound:${target.workspace.taskId}`
897
+ : target.workspace.mode;
898
+ throw new Error(`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
899
+ + `but this request is ${requestedDescription}. Run 'ai sandbox rm ${branch}' and retry.`);
900
+ }
901
+ }
1051
902
  message('Removing old container instance...');
1052
903
  for (const name of matchedContainers) {
1053
904
  runSafeEngine(engine, 'docker', ['stop', name]);
@@ -1091,6 +942,8 @@ export async function create(args) {
1091
942
  plan: capabilityPlan,
1092
943
  create: {
1093
944
  hostHome: effectiveConfig.home,
945
+ hostEnv: { ...process.env },
946
+ project: effectiveConfig.project,
1094
947
  resolvedTools: effectiveResolvedTools
1095
948
  }
1096
949
  },
@@ -1101,22 +954,22 @@ export async function create(args) {
1101
954
  throw new Error(beforeCreateFailure.message
1102
955
  ?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`);
1103
956
  }
1104
- const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
1105
- if (claudeCodeEntry) {
1106
- ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
1107
- ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
1108
- // prepareClaudeCredentials wrote OAuth credentials or confirmed
1109
- // provider/API settings are enough for Claude Code. If no usable
1110
- // auth was present, the claude-code entry was removed above.
1111
- }
1112
- const opencodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'opencode');
1113
- if (opencodeEntry) {
1114
- // The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
1115
- ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
1116
- }
1117
957
  const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]);
1118
958
  const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) => tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []);
1119
- const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
959
+ const workspaceView = materializeSandboxWorkspaceView({
960
+ base: effectiveConfig.workspaceViewBase,
961
+ project: effectiveConfig.project,
962
+ container,
963
+ identity: target.workspace
964
+ });
965
+ const control = materializeSandboxControl({
966
+ base: effectiveConfig.controlBase,
967
+ repoRoot: effectiveConfig.repoRoot,
968
+ project: effectiveConfig.project,
969
+ container,
970
+ branch,
971
+ identity: target.workspace
972
+ });
1120
973
  hostShellConfig = prepareHostShellConfig({
1121
974
  home: effectiveConfig.home,
1122
975
  project: effectiveConfig.project,
@@ -1125,7 +978,15 @@ export async function create(args) {
1125
978
  });
1126
979
  const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
1127
980
  worktree,
1128
- shellConfigHostDir: hostShellConfig.hostDir
981
+ shellConfigHostDir: hostShellConfig.hostDir,
982
+ workspaceViewRoot: workspaceView.root,
983
+ controlDir: control.channelDir,
984
+ ...(target.workspace.mode === 'task-bound'
985
+ ? {
986
+ taskSource: assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId),
987
+ taskId: target.workspace.taskId
988
+ }
989
+ : {})
1129
990
  }).flatMap(({ hostPaths, containerPath, readOnly }) => [
1130
991
  '-v',
1131
992
  volumeArg(engine, hostPaths[0], containerPath, readOnly ? ':ro' : '')
@@ -1157,7 +1018,6 @@ export async function create(args) {
1157
1018
  '-v',
1158
1019
  volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
1159
1020
  ]));
1160
- fs.mkdirSync(workspaceDir, { recursive: true });
1161
1021
  fs.mkdirSync(shareCommon, { recursive: true });
1162
1022
  fs.mkdirSync(shareBranch, { recursive: true });
1163
1023
  fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
@@ -1179,6 +1039,20 @@ export async function create(args) {
1179
1039
  '--label',
1180
1040
  `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1181
1041
  '--label',
1042
+ `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1043
+ ...(target.workspace.mode === 'task-bound'
1044
+ ? [
1045
+ '--label',
1046
+ `${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
1047
+ '-e',
1048
+ `AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
1049
+ ]
1050
+ : []),
1051
+ '-e',
1052
+ `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1053
+ '-e',
1054
+ 'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1055
+ '--label',
1182
1056
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1183
1057
  ...coreVolumes,
1184
1058
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
@@ -1195,6 +1069,12 @@ export async function create(args) {
1195
1069
  '/workspace',
1196
1070
  effectiveConfig.imageName
1197
1071
  ]);
1072
+ const broker = spawn(process.execPath, [internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath], { cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' });
1073
+ broker.once('error', () => {
1074
+ // Fresh readiness verification below remains the source of truth;
1075
+ // consume detached spawn errors so they cannot crash the command.
1076
+ });
1077
+ broker.unref();
1198
1078
  createdTmpfsSeedPlan = tmpfsSeedPlan;
1199
1079
  }
1200
1080
  finally {
@@ -1277,10 +1157,11 @@ export async function create(args) {
1277
1157
  preparedDockerfile.cleanup();
1278
1158
  }
1279
1159
  p.log.step('Verifying setup...');
1280
- assertFreshSandboxReady({
1160
+ await assertFreshSandboxReady({
1281
1161
  config: effectiveConfig,
1282
1162
  engine,
1283
1163
  branch,
1164
+ workspace: target.workspace,
1284
1165
  container,
1285
1166
  copiedEntries: createdTmpfsSeedPlan
1286
1167
  });