@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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 (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  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/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -2,9 +2,10 @@ 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 type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
7
7
  import { parseArgs } from 'node:util';
8
+ import { fileURLToPath } from 'node:url';
8
9
  import * as p from '@clack/prompts';
9
10
  import pc from 'picocolors';
10
11
  import { listAgentClientAdapters } from '../../agent-clients/registry.ts';
@@ -20,6 +21,8 @@ import {
20
21
  sandboxImageRefreshLabel,
21
22
  sandboxLabel,
22
23
  sandboxRuntimeCapabilityLabel,
24
+ sandboxTaskIdLabel,
25
+ sandboxWorkspaceModeLabel,
23
26
  shareBranchDir,
24
27
  shareCommonDir,
25
28
  shellConfigDir,
@@ -44,7 +47,11 @@ import {
44
47
  runSafeEngine,
45
48
  runVerboseEngine
46
49
  } from '../shell.ts';
47
- import { resolveTaskBranch } from '../task-resolver.ts';
50
+ import {
51
+ parseSandboxWorkspaceIdentity,
52
+ resolveSandboxTarget,
53
+ sameSandboxWorkspaceIdentity
54
+ } from '../workspace-identity.ts';
48
55
  import {
49
56
  createSandboxCapabilityPlan,
50
57
  runSandboxHooks,
@@ -64,16 +71,17 @@ import {
64
71
  } from '../recovery.ts';
65
72
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
66
73
  import { sandboxCoreBindMounts } from '../mounts.ts';
74
+ import {
75
+ assertSandboxTaskSource,
76
+ materializeSandboxControl,
77
+ materializeSandboxWorkspaceView
78
+ } from '../workspace-view.ts';
67
79
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
68
80
  import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
69
81
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
70
82
  import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
71
83
  import { removeDirRecursive } from '../../remove-dir.ts';
72
- import {
73
- prepareClaudeCredentials,
74
- redactCommandError,
75
- validateClaudeCredentialsEnvOverride
76
- } from '../credentials.ts';
84
+ import { redactCommandError } from '../redaction.ts';
77
85
  import { detectHostTimezone } from '../host-timezone.ts';
78
86
  import {
79
87
  buildImageSignature,
@@ -84,10 +92,6 @@ import {
84
92
  parseRefreshTimestamp
85
93
  } from '../image-build.ts';
86
94
 
87
- export {
88
- ensureCodexModelInheritance,
89
- ensureCodexWorkspaceTrust
90
- } from '../../agent-clients/adapters/codex-sandbox.ts';
91
95
 
92
96
  const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
93
97
  const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
@@ -133,7 +137,6 @@ type TmpfsSeedPlanEntry = TmpfsSeedEntry & {
133
137
  volumeArgs: string[];
134
138
  };
135
139
  type RuntimeCheck = { name: string; cmd: string[] };
136
- type JsonObject = Record<string, unknown>;
137
140
  type GpgCache = { pub: Buffer; sec: Buffer } | null;
138
141
  type ExecSyncOptions = ExecFileSyncOptions & {
139
142
  input?: Buffer | string;
@@ -154,6 +157,12 @@ type HostShellConfig = {
154
157
  mounts: Array<{ hostPath: string; containerPath: string }>;
155
158
  };
156
159
 
160
+ function internalCliPath(): string {
161
+ const directory = path.dirname(fileURLToPath(import.meta.url));
162
+ const extension = path.extname(fileURLToPath(import.meta.url));
163
+ return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
164
+ }
165
+
157
166
  function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
158
167
  return tools.map((tool) => {
159
168
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -807,209 +816,6 @@ export function assertBranchAvailable(
807
816
  }
808
817
  }
809
818
 
810
- function readHostJsonSafe(filePath: string): JsonObject | null {
811
- if (!filePath || !fs.existsSync(filePath)) {
812
- return null;
813
- }
814
-
815
- try {
816
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
817
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as JsonObject : null;
818
- } catch {
819
- return null;
820
- }
821
- }
822
-
823
- const CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS = [
824
- 'model',
825
- 'fallbackModel',
826
- 'availableModels',
827
- 'modelOverrides',
828
- 'enforceAvailableModels',
829
- 'advisorModel',
830
- 'apiKeyHelper',
831
- 'effortLevel'
832
- ];
833
-
834
- function isJsonObjectRecord(value: unknown): value is JsonObject {
835
- return value !== null && typeof value === 'object' && !Array.isArray(value);
836
- }
837
-
838
- function mergeMissingStringEnvFields(target: JsonObject, source: JsonObject): boolean {
839
- if (!isJsonObjectRecord(source.env)) {
840
- return false;
841
- }
842
- if (Object.hasOwn(target, 'env') && !isJsonObjectRecord(target.env)) {
843
- return false;
844
- }
845
-
846
- let targetEnv = target.env as JsonObject | undefined;
847
- let changed = false;
848
- for (const [key, value] of Object.entries(source.env)) {
849
- if (typeof value !== 'string' || value === '') {
850
- continue;
851
- }
852
- if (!targetEnv) {
853
- targetEnv = {};
854
- target.env = targetEnv;
855
- }
856
- if (!Object.hasOwn(targetEnv, key)) {
857
- targetEnv[key] = value;
858
- changed = true;
859
- }
860
- }
861
- return changed;
862
- }
863
-
864
- function mergeMissingTopLevelSettings(target: JsonObject, source: JsonObject): boolean {
865
- let changed = false;
866
- for (const key of CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS) {
867
- if (!Object.hasOwn(source, key) || Object.hasOwn(target, key)) {
868
- continue;
869
- }
870
- const value = source[key];
871
- if (value === null || value === undefined || value === '') {
872
- continue;
873
- }
874
- target[key] = value;
875
- changed = true;
876
- }
877
- return changed;
878
- }
879
-
880
- export function ensureClaudeOnboarding(toolDir: string, hostHomeDir?: string): void {
881
- const claudeJsonPath = path.join(toolDir, '.claude.json');
882
- let data: JsonObject & {
883
- hasCompletedOnboarding?: boolean;
884
- projects?: Record<string, { hasTrustDialogAccepted?: boolean }>;
885
- model?: string;
886
- } = {};
887
- if (fs.existsSync(claudeJsonPath)) {
888
- try {
889
- data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')) as typeof data;
890
- } catch {
891
- // malformed JSON, start fresh
892
- }
893
- }
894
- let changed = false;
895
- if (!data.hasCompletedOnboarding) {
896
- data.hasCompletedOnboarding = true;
897
- changed = true;
898
- }
899
- if (!data.projects) {
900
- data.projects = {};
901
- changed = true;
902
- }
903
- if (!data.projects['/workspace']) {
904
- data.projects['/workspace'] = {};
905
- changed = true;
906
- }
907
- if (!data.projects['/workspace'].hasTrustDialogAccepted) {
908
- data.projects['/workspace'].hasTrustDialogAccepted = true;
909
- changed = true;
910
- }
911
- if (hostHomeDir) {
912
- const hostClaudeJson = readHostJsonSafe(path.join(hostHomeDir, '.claude.json'));
913
- if (
914
- hostClaudeJson
915
- && typeof hostClaudeJson.model === 'string'
916
- && hostClaudeJson.model !== ''
917
- && !Object.hasOwn(data, 'model')
918
- ) {
919
- data.model = hostClaudeJson.model;
920
- changed = true;
921
- }
922
- // Claude Code launch-pins a default effort per model generation (for
923
- // example xhigh for Opus 4.7). The saved effortLevel is honored only after
924
- // a top-level boolean `unpin*LaunchEffort: true` flag unlocks it, so mirror
925
- // those flags here with the existing first-write semantics.
926
- //
927
- // Pattern matching avoids one patch per future model generation. If
928
- // Anthropic changes the naming convention, this block will no-op and should
929
- // be revisited.
930
- if (hostClaudeJson) {
931
- for (const key of Object.keys(hostClaudeJson)) {
932
- if (
933
- /^unpin.*LaunchEffort$/.test(key)
934
- && hostClaudeJson[key] === true
935
- && !Object.hasOwn(data, key)
936
- ) {
937
- data[key] = true;
938
- changed = true;
939
- }
940
- }
941
- }
942
- }
943
- if (changed) {
944
- fs.writeFileSync(claudeJsonPath, JSON.stringify(data, null, 4), 'utf8');
945
- }
946
- }
947
-
948
- export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): void {
949
- const settingsPath = path.join(toolDir, 'settings.json');
950
- let data: JsonObject & { skipDangerousModePermissionPrompt?: boolean } = {};
951
- if (fs.existsSync(settingsPath)) {
952
- try {
953
- data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as typeof data;
954
- } catch {
955
- // malformed JSON, start fresh
956
- }
957
- }
958
- let changed = false;
959
- if (data.skipDangerousModePermissionPrompt !== true) {
960
- data.skipDangerousModePermissionPrompt = true;
961
- changed = true;
962
- }
963
- if (hostHomeDir) {
964
- const hostSettings = readHostJsonSafe(path.join(hostHomeDir, '.claude', 'settings.json'));
965
- if (hostSettings) {
966
- changed = mergeMissingStringEnvFields(data, hostSettings) || changed;
967
- changed = mergeMissingTopLevelSettings(data, hostSettings) || changed;
968
- }
969
- }
970
- if (changed) {
971
- fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
972
- }
973
- }
974
-
975
- export function ensureOpenCodeModelInheritance(toolDir: string, hostHomeDir?: string): void {
976
- if (!hostHomeDir) {
977
- return;
978
- }
979
-
980
- const hostConfigPath = path.join(hostHomeDir, '.config', 'opencode', 'opencode.json');
981
- const hostJson = readHostJsonSafe(hostConfigPath);
982
- if (!hostJson) {
983
- return;
984
- }
985
-
986
- const sandboxConfigPath = path.join(toolDir, 'opencode.json');
987
- let sandboxJson: JsonObject = {};
988
- if (fs.existsSync(sandboxConfigPath)) {
989
- const existing = readHostJsonSafe(sandboxConfigPath);
990
- if (!existing) {
991
- return;
992
- }
993
- sandboxJson = existing;
994
- }
995
- let changed = false;
996
- for (const key of ['model', 'small_model']) {
997
- if (Object.hasOwn(sandboxJson, key)) {
998
- continue;
999
- }
1000
- const value = hostJson[key];
1001
- if (typeof value !== 'string' || value === '') {
1002
- continue;
1003
- }
1004
- sandboxJson[key] = value;
1005
- changed = true;
1006
- }
1007
-
1008
- if (changed) {
1009
- fs.writeFileSync(sandboxConfigPath, JSON.stringify(sandboxJson, null, 2), 'utf8');
1010
- }
1011
- }
1012
-
1013
819
  export function sandboxAliasesPath(home: string): string {
1014
820
  return hostJoin(home, '.agent-infra', 'aliases', 'sandbox.sh');
1015
821
  }
@@ -1195,11 +1001,10 @@ export async function create(args: string[]): Promise<void> {
1195
1001
  }
1196
1002
 
1197
1003
  validateSelinuxDisableEnv();
1198
- validateClaudeCredentialsEnvOverride();
1199
-
1200
1004
  const config = loadConfig();
1201
1005
  const [branchOrTaskId = '', base] = positionals;
1202
- const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
1006
+ const target = resolveSandboxTarget(branchOrTaskId, config.repoRoot);
1007
+ const branch = target.branch;
1203
1008
  assertValidBranchName(branch);
1204
1009
  const effectiveConfig = {
1205
1010
  ...config,
@@ -1214,20 +1019,31 @@ export async function create(args: string[]): Promise<void> {
1214
1019
  const capabilityPlan = createSandboxCapabilityPlan(effectiveConfig);
1215
1020
  const tools = [...capabilityPlan.tools];
1216
1021
  const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
1217
- // Fatal credential states still fail before filesystem/docker side effects.
1218
- // Missing credentials leave the selected Claude Code capability installed
1219
- // and mounted; only the absent live credential file is omitted.
1220
- const credentialOutcome = prepareClaudeCredentials(
1221
- effectiveConfig.home,
1222
- effectiveConfig.project,
1223
- resolvedTools
1224
- );
1022
+ const prepareHookResults = await runSandboxHooks({
1023
+ hooks: capabilityPlan.hooksByPhase.prepare,
1024
+ phase: 'prepare',
1025
+ context: {
1026
+ config: effectiveConfig,
1027
+ plan: capabilityPlan,
1028
+ create: {
1029
+ hostHome: effectiveConfig.home,
1030
+ hostEnv: { ...process.env },
1031
+ project: effectiveConfig.project,
1032
+ resolvedTools
1033
+ }
1034
+ },
1035
+ runCommand: runBoundedSandboxHookCommand
1036
+ });
1037
+ const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
1038
+ if (prepareFailure) {
1039
+ throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
1040
+ }
1225
1041
  const effectiveResolvedTools = resolvedTools;
1226
1042
  const container = containerName(effectiveConfig, branch);
1227
1043
  const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
1228
1044
  const shareCommon = shareCommonDir(effectiveConfig);
1229
1045
  const shareBranch = shareBranchDir(effectiveConfig, branch);
1230
- const preparedDockerfile = prepareDockerfile(effectiveConfig);
1046
+ const preparedDockerfile = prepareDockerfile(effectiveConfig, capabilityPlan.image);
1231
1047
  const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
1232
1048
  const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
1233
1049
  const engine = detectEngine(effectiveConfig);
@@ -1237,26 +1053,12 @@ export async function create(args: string[]): Promise<void> {
1237
1053
  p.log.info(
1238
1054
  `Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`
1239
1055
  );
1240
- if (credentialOutcome.status === 'SKIPPED') {
1241
- p.log.warn(
1242
- 'Claude Code credentials not found on host - creating this sandbox WITHOUT Claude Code credentials.\n'
1243
- + ' Claude Code is still installed in the image but will not be authenticated.\n'
1244
- + ' To enable it: run "claude" once on the host to complete login, then re-run "ai sandbox create".'
1245
- );
1056
+ for (const result of prepareHookResults) {
1057
+ if (result.status === 'warning' && result.message) p.log.warn(result.message);
1246
1058
  }
1247
1059
 
1248
1060
  try {
1249
1061
  p.log.step('Checking container engine...');
1250
- const prepareHookResults = await runSandboxHooks({
1251
- hooks: capabilityPlan.hooksByPhase.prepare,
1252
- phase: 'prepare',
1253
- context: { config: effectiveConfig, plan: capabilityPlan },
1254
- runCommand: runBoundedSandboxHookCommand
1255
- });
1256
- const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
1257
- if (prepareFailure) {
1258
- throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
1259
- }
1260
1062
  await ensureDocker(effectiveConfig, (detail: string) => {
1261
1063
  p.log.info(` ${detail}`);
1262
1064
  });
@@ -1431,6 +1233,33 @@ export async function create(args: string[]): Promise<void> {
1431
1233
  .filter((name) => existing.includes(name));
1432
1234
 
1433
1235
  if (matchedContainers.length > 0) {
1236
+ for (const name of matchedContainers) {
1237
+ const rawLabels = runSafeEngine(engine, 'docker', [
1238
+ 'inspect', '--format', '{{json .Config.Labels}}', name
1239
+ ]).trim();
1240
+ let labels: Record<string, string> = {};
1241
+ try {
1242
+ labels = JSON.parse(rawLabels) as Record<string, string>;
1243
+ } catch {
1244
+ labels = {};
1245
+ }
1246
+ const existingIdentity = parseSandboxWorkspaceIdentity(labels, {
1247
+ mode: sandboxWorkspaceModeLabel(effectiveConfig),
1248
+ taskId: sandboxTaskIdLabel(effectiveConfig)
1249
+ });
1250
+ if (!sameSandboxWorkspaceIdentity(existingIdentity, target.workspace)) {
1251
+ const existingDescription = existingIdentity.mode === 'task-bound'
1252
+ ? `task-bound:${existingIdentity.taskId}`
1253
+ : existingIdentity.mode;
1254
+ const requestedDescription = target.workspace.mode === 'task-bound'
1255
+ ? `task-bound:${target.workspace.taskId}`
1256
+ : target.workspace.mode;
1257
+ throw new Error(
1258
+ `SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
1259
+ + `but this request is ${requestedDescription}. Run 'ai sandbox rm ${branch}' and retry.`
1260
+ );
1261
+ }
1262
+ }
1434
1263
  message('Removing old container instance...');
1435
1264
  for (const name of matchedContainers) {
1436
1265
  runSafeEngine(engine, 'docker', ['stop', name]);
@@ -1485,6 +1314,8 @@ export async function create(args: string[]): Promise<void> {
1485
1314
  plan: capabilityPlan,
1486
1315
  create: {
1487
1316
  hostHome: effectiveConfig.home,
1317
+ hostEnv: { ...process.env },
1318
+ project: effectiveConfig.project,
1488
1319
  resolvedTools: effectiveResolvedTools
1489
1320
  }
1490
1321
  },
@@ -1499,26 +1330,27 @@ export async function create(args: string[]): Promise<void> {
1499
1330
  ?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`
1500
1331
  );
1501
1332
  }
1502
- const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
1503
- if (claudeCodeEntry) {
1504
- ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
1505
- ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
1506
- // prepareClaudeCredentials wrote OAuth credentials or confirmed
1507
- // provider/API settings are enough for Claude Code. If no usable
1508
- // auth was present, the claude-code entry was removed above.
1509
- }
1510
- const opencodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'opencode');
1511
- if (opencodeEntry) {
1512
- // The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
1513
- ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
1514
- }
1515
1333
  const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
1516
1334
  tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]
1517
1335
  );
1518
1336
  const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) =>
1519
1337
  tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []
1520
1338
  );
1521
- const workspaceDir = path.join(effectiveConfig.repoRoot, '.agents', 'workspace');
1339
+ const workspaceView = materializeSandboxWorkspaceView({
1340
+ base: effectiveConfig.workspaceViewBase,
1341
+ project: effectiveConfig.project,
1342
+ container,
1343
+ identity: target.workspace
1344
+ });
1345
+ const control = materializeSandboxControl({
1346
+ base: effectiveConfig.controlBase,
1347
+ repoRoot: effectiveConfig.repoRoot,
1348
+ worktreeRoot: worktree,
1349
+ project: effectiveConfig.project,
1350
+ container,
1351
+ branch,
1352
+ identity: target.workspace
1353
+ });
1522
1354
  hostShellConfig = prepareHostShellConfig({
1523
1355
  home: effectiveConfig.home,
1524
1356
  project: effectiveConfig.project,
@@ -1527,7 +1359,18 @@ export async function create(args: string[]): Promise<void> {
1527
1359
  });
1528
1360
  const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
1529
1361
  worktree,
1530
- shellConfigHostDir: hostShellConfig.hostDir
1362
+ shellConfigHostDir: hostShellConfig.hostDir,
1363
+ workspaceViewRoot: workspaceView.root,
1364
+ controlDir: control.channelDir,
1365
+ ...(target.workspace.mode === 'task-bound'
1366
+ ? {
1367
+ taskSource: assertSandboxTaskSource(
1368
+ effectiveConfig.repoRoot,
1369
+ target.workspace.taskId
1370
+ ),
1371
+ taskId: target.workspace.taskId
1372
+ }
1373
+ : {})
1531
1374
  }).flatMap(({ hostPaths, containerPath, readOnly }) => [
1532
1375
  '-v',
1533
1376
  volumeArg(engine, hostPaths[0]!, containerPath, readOnly ? ':ro' : '')
@@ -1564,7 +1407,6 @@ export async function create(args: string[]): Promise<void> {
1564
1407
  ])
1565
1408
  );
1566
1409
 
1567
- fs.mkdirSync(workspaceDir, { recursive: true });
1568
1410
  fs.mkdirSync(shareCommon, { recursive: true });
1569
1411
  fs.mkdirSync(shareBranch, { recursive: true });
1570
1412
  fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
@@ -1591,6 +1433,20 @@ export async function create(args: string[]): Promise<void> {
1591
1433
  '--label',
1592
1434
  `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1593
1435
  '--label',
1436
+ `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1437
+ ...(target.workspace.mode === 'task-bound'
1438
+ ? [
1439
+ '--label',
1440
+ `${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
1441
+ '-e',
1442
+ `AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
1443
+ ]
1444
+ : []),
1445
+ '-e',
1446
+ `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1447
+ '-e',
1448
+ 'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1449
+ '--label',
1594
1450
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1595
1451
  ...coreVolumes,
1596
1452
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
@@ -1611,6 +1467,16 @@ export async function create(args: string[]): Promise<void> {
1611
1467
  '/workspace',
1612
1468
  effectiveConfig.imageName
1613
1469
  ]);
1470
+ const broker = spawn(
1471
+ process.execPath,
1472
+ [internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath],
1473
+ { cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' }
1474
+ );
1475
+ broker.once('error', () => {
1476
+ // Fresh readiness verification below remains the source of truth;
1477
+ // consume detached spawn errors so they cannot crash the command.
1478
+ });
1479
+ broker.unref();
1614
1480
  createdTmpfsSeedPlan = tmpfsSeedPlan;
1615
1481
  } finally {
1616
1482
  envFile.cleanup();
@@ -1713,10 +1579,11 @@ export async function create(args: string[]): Promise<void> {
1713
1579
  }
1714
1580
 
1715
1581
  p.log.step('Verifying setup...');
1716
- assertFreshSandboxReady({
1582
+ await assertFreshSandboxReady({
1717
1583
  config: effectiveConfig,
1718
1584
  engine,
1719
1585
  branch,
1586
+ workspace: target.workspace,
1720
1587
  container,
1721
1588
  copiedEntries: createdTmpfsSeedPlan
1722
1589
  });
@@ -12,23 +12,16 @@ import {
12
12
  runBoundedSandboxHookCommand,
13
13
  runSandboxHooks
14
14
  } from '../agent-client-reconciler.ts';
15
- import {
16
- formatCredentialWarnings,
17
- formatRemaining,
18
- hasClaudeProviderAuth,
19
- reconcileClaudeCredentials,
20
- redactCommandError,
21
- validateClaudeCredentialsEnvOverride
22
- } from '../credentials.ts';
23
15
  import { runInteractiveEngine } from '../shell.ts';
24
16
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
17
+ import { redactCommandError } from '../redaction.ts';
25
18
  import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
26
19
  import { detectHostTimezone } from '../host-timezone.ts';
27
20
  import {
28
21
  fetchSandboxRows,
29
- resolveBranchArg,
30
22
  selectSandboxContainer,
31
23
  } from './list-running.ts';
24
+ import { resolveSandboxTarget } from '../workspace-identity.ts';
32
25
 
33
26
  const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
34
27
 
@@ -98,37 +91,6 @@ export function runSandboxInteractive(params: {
98
91
  return runBridge({ engine, dockerArgs, container, home });
99
92
  }
100
93
 
101
- export function formatCredentialSyncStatus(
102
- result: ReturnType<typeof reconcileClaudeCredentials>,
103
- isTTY = process.stderr.isTTY,
104
- providerAuthAvailable = false
105
- ): string | null {
106
- if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING')) {
107
- return null;
108
- }
109
- if (result.status === 'STALE_ACCESS') {
110
- return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
111
- }
112
- if (result.status === 'MISSING') {
113
- return 'Warning: Claude Code credentials missing on host. Run "claude /login" to authenticate.\n';
114
- }
115
- if (result.status === 'KEYCHAIN_WRITE_FAILED') {
116
- return `Warning: A sandbox refresh produced newer credentials but host Keychain write failed (${formatCredentialWarnings(result.warnings)}). Run "ai sandbox refresh" again or "claude /status" on the host to retry.\n`;
117
- }
118
- if (result.status === 'KEYCHAIN_LOCKED' || result.status === 'KEYCHAIN_ERROR') {
119
- return 'Warning: Host keychain is unavailable; Claude credential sync skipped. Run "ai sandbox refresh" for details.\n';
120
- }
121
- if (result.status === 'OK' && result.authoritative !== 'host') {
122
- const message = `Synced Claude Code credentials from sandbox refresh back to host (expires in ${formatRemaining(result.expiresAt)})`;
123
- return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
124
- }
125
- if (result.status === 'OK' && result.filesWritten.length > 0) {
126
- const message = `Synced Claude Code credentials from host Keychain (expires in ${formatRemaining(result.expiresAt)})`;
127
- return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
128
- }
129
- return null;
130
- }
131
-
132
94
  export function parseEnterArgs(args: string[]): {
133
95
  target: string;
134
96
  command: string[];
@@ -157,10 +119,10 @@ export async function enter(args: string[]): Promise<number> {
157
119
  }
158
120
 
159
121
  const config = loadConfig();
160
- validateClaudeCredentialsEnvOverride();
161
122
  const engine = detectEngine(config);
162
123
  const parsed = parseEnterArgs(args);
163
- const branch = resolveBranchArg(parsed.target, { repoRoot: config.repoRoot });
124
+ const target = resolveSandboxTarget(parsed.target, config.repoRoot);
125
+ const branch = target.branch;
164
126
  assertValidBranchName(branch);
165
127
 
166
128
  const { running, nonRunning } = fetchSandboxRows(
@@ -182,11 +144,12 @@ export async function enter(args: string[]): Promise<number> {
182
144
  config,
183
145
  engine,
184
146
  branch,
147
+ workspace: target.workspace,
185
148
  row: found,
186
149
  allowRecreate: parsed.recreate,
187
- recreate: async (targetBranch) => {
150
+ recreate: async () => {
188
151
  const { create } = await import('./create.ts');
189
- await create([targetBranch, '--no-refresh']);
152
+ await create([target.requestedRef, '--no-refresh']);
190
153
  }
191
154
  });
192
155
  const container = ready.container;
@@ -195,7 +158,11 @@ export async function enter(args: string[]): Promise<number> {
195
158
  const enterHookResults = await runSandboxHooks({
196
159
  hooks: capabilityPlan.hooksByPhase['before-enter'],
197
160
  phase: 'before-enter',
198
- context: { config, plan: capabilityPlan },
161
+ context: {
162
+ config,
163
+ plan: capabilityPlan,
164
+ enter: { hostHome: config.home, hostEnv: { ...process.env } }
165
+ },
199
166
  runCommand: runBoundedSandboxHookCommand
200
167
  });
201
168
  for (const result of enterHookResults) {
@@ -207,20 +174,7 @@ export async function enter(args: string[]): Promise<number> {
207
174
  `Warning: ${result.message ?? `Sandbox hook '${result.hookId}' did not complete.`}\n`
208
175
  );
209
176
  }
210
- }
211
-
212
- if (capabilityPlan.selectedAgentClients.some((adapter) => adapter.id === 'claude-code')) {
213
- try {
214
- // Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
215
- const providerAuthAvailable = hasClaudeProviderAuth(config.home);
216
- const result = reconcileClaudeCredentials(config.home);
217
- const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
218
- if (message) {
219
- process.stderr.write(message);
220
- }
221
- } catch (error) {
222
- process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
223
- }
177
+ if (result.message && result.status !== 'warning') process.stderr.write(result.message);
224
178
  }
225
179
 
226
180
  const envFlags = [...terminalEnvFlags(), ...hostTimezoneEnvFlags()];