@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
@@ -0,0 +1,144 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getProcessStartTime } from '../../server/process-state.js';
5
+ import { createTask } from '../../task/create-service.js';
6
+ import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
7
+ import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
8
+ function readManifest(manifestPath) {
9
+ const stat = fs.lstatSync(manifestPath);
10
+ if (!stat.isFile() || stat.isSymbolicLink())
11
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
12
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
13
+ if (manifest.version !== 2) {
14
+ throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
15
+ }
16
+ if (typeof manifest.repoRoot !== 'string'
17
+ || typeof manifest.worktreeRoot !== 'string'
18
+ || typeof manifest.channelDir !== 'string'
19
+ || typeof manifest.token !== 'string')
20
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
21
+ if (path.resolve(manifest.channelDir) !== path.join(path.dirname(path.resolve(manifestPath)), 'channel')) {
22
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
23
+ }
24
+ return manifest;
25
+ }
26
+ function assertRealDirectory(directory, parent) {
27
+ const stat = fs.lstatSync(directory);
28
+ if (!stat.isDirectory() || stat.isSymbolicLink())
29
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
30
+ if (parent && path.dirname(fs.realpathSync.native(directory)) !== fs.realpathSync.native(parent)) {
31
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
32
+ }
33
+ }
34
+ function writeResponse(directory, response) {
35
+ const temporary = path.join(directory, `.${response.id}.tmp`);
36
+ fs.writeFileSync(temporary, `${JSON.stringify(response)}\n`, { mode: 0o600, flag: 'wx' });
37
+ fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
38
+ }
39
+ export function sandboxControlSafeEnv(env = process.env) {
40
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
41
+ }
42
+ function consumeRequest(directory, id) {
43
+ try {
44
+ fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
45
+ }
46
+ catch (error) {
47
+ if (error.code === 'EEXIST') {
48
+ throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ export function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
54
+ const manifest = readManifest(manifestPath);
55
+ assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
56
+ const brokerPath = path.join(path.dirname(manifestPath), 'broker.json');
57
+ const startTime = getProcessStartTime(process.pid);
58
+ if (!startTime)
59
+ throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
60
+ fs.writeFileSync(brokerPath, `${JSON.stringify({
61
+ version: 1,
62
+ pid: process.pid,
63
+ startTime,
64
+ token: manifest.token
65
+ })}\n`, { mode: 0o600, flag: 'wx' });
66
+ const requestsDir = path.join(manifest.channelDir, 'requests');
67
+ const responsesDir = path.join(manifest.channelDir, 'responses');
68
+ const consumedDir = path.join(path.dirname(manifestPath), 'consumed');
69
+ assertRealDirectory(manifest.channelDir);
70
+ fs.mkdirSync(requestsDir, { recursive: true, mode: 0o700 });
71
+ fs.mkdirSync(responsesDir, { recursive: true, mode: 0o700 });
72
+ fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
73
+ assertRealDirectory(requestsDir, manifest.channelDir);
74
+ assertRealDirectory(responsesDir, manifest.channelDir);
75
+ assertRealDirectory(consumedDir, path.dirname(manifestPath));
76
+ while (!signal.aborted) {
77
+ const current = readManifest(manifestPath);
78
+ if (current.token !== manifest.token)
79
+ return;
80
+ assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
81
+ assertRealDirectory(requestsDir, manifest.channelDir);
82
+ assertRealDirectory(responsesDir, manifest.channelDir);
83
+ for (const name of fs.readdirSync(requestsDir).sort()) {
84
+ if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
85
+ continue;
86
+ const requestPath = path.join(requestsDir, name);
87
+ let id = name.slice(0, -5);
88
+ let response;
89
+ try {
90
+ const stat = fs.lstatSync(requestPath);
91
+ if (!stat.isFile() || stat.isSymbolicLink())
92
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
93
+ const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(requestPath, 'utf8')), manifest);
94
+ id = request.id;
95
+ consumeRequest(consumedDir, id);
96
+ if (request.family === 'task-create') {
97
+ const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
98
+ response = {
99
+ version: 1, id,
100
+ exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
101
+ stdout: `${JSON.stringify(result)}\n`, stderr: ''
102
+ };
103
+ }
104
+ else {
105
+ const boundArgs = bindSandboxControlTask(request, manifest.taskId);
106
+ if (request.family === 'task-orchestration') {
107
+ boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
108
+ }
109
+ const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...boundArgs], {
110
+ cwd: manifest.repoRoot,
111
+ encoding: 'utf8',
112
+ env: sandboxControlSafeEnv()
113
+ });
114
+ response = {
115
+ version: 1,
116
+ id,
117
+ exitCode: result.status ?? 1,
118
+ stdout: result.stdout ?? '',
119
+ stderr: result.stderr ?? ''
120
+ };
121
+ }
122
+ }
123
+ catch (error) {
124
+ response = {
125
+ version: 1,
126
+ id,
127
+ exitCode: 1,
128
+ stdout: '',
129
+ stderr: `${error instanceof Error ? error.message : String(error)}\n`
130
+ };
131
+ }
132
+ try {
133
+ fs.unlinkSync(requestPath);
134
+ writeResponse(responsesDir, response);
135
+ }
136
+ catch (error) {
137
+ if (error.code !== 'ENOENT')
138
+ throw error;
139
+ }
140
+ }
141
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
142
+ }
143
+ }
144
+ //# sourceMappingURL=server.js.map
@@ -5,6 +5,11 @@ import { createHash } from 'node:crypto';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { removeDirRecursive } from '../remove-dir.js';
7
7
  const RUNTIMES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'runtimes');
8
+ const AGENT_CLIENT_RUNTIMES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'agent-clients', 'adapters', 'runtimes');
9
+ const EMPTY_IMAGE_CONTRIBUTION = Object.freeze({
10
+ dockerfileFragments: Object.freeze([]),
11
+ dotfilesExclusions: Object.freeze([])
12
+ });
8
13
  function listRuntimeFragments() {
9
14
  return fs.readdirSync(RUNTIMES_DIR)
10
15
  .filter((file) => file.endsWith('.dockerfile'))
@@ -15,7 +20,24 @@ export function availableRuntimes() {
15
20
  .filter((name) => name !== 'base' && name !== 'ai-tools')
16
21
  .sort();
17
22
  }
18
- function dockerfileContent(config) {
23
+ function exclusionsFragment(exclusions) {
24
+ return [
25
+ "RUN mkdir -p /etc/agent-infra && cat > /etc/agent-infra/dotfiles-exclusions <<'EXCLUSIONS'",
26
+ ...exclusions,
27
+ 'EXCLUSIONS'
28
+ ].join('\n');
29
+ }
30
+ function adapterFragmentPath(fragment) {
31
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(fragment)) {
32
+ throw new Error(`Invalid Agent Client Dockerfile fragment: ${fragment}`);
33
+ }
34
+ const fragmentPath = path.join(AGENT_CLIENT_RUNTIMES_DIR, `${fragment}.dockerfile`);
35
+ if (!fs.existsSync(fragmentPath)) {
36
+ throw new Error(`Unknown Agent Client Dockerfile fragment: ${fragment}`);
37
+ }
38
+ return fragmentPath;
39
+ }
40
+ function dockerfileContent(config, image = EMPTY_IMAGE_CONTRIBUTION) {
19
41
  if (config.dockerfile) {
20
42
  const customPath = path.resolve(config.repoRoot, config.dockerfile);
21
43
  if (!fs.existsSync(customPath)) {
@@ -30,22 +52,23 @@ function dockerfileContent(config) {
30
52
  }
31
53
  }
32
54
  const fragments = [
33
- 'base.dockerfile',
34
- ...config.runtimes.map((runtime) => `${runtime}.dockerfile`),
35
- 'ai-tools.dockerfile'
55
+ fs.readFileSync(path.join(RUNTIMES_DIR, 'base.dockerfile'), 'utf8').trimEnd(),
56
+ exclusionsFragment(image.dotfilesExclusions),
57
+ ...image.dockerfileFragments.map((fragment) => fs.readFileSync(adapterFragmentPath(fragment), 'utf8').trimEnd()),
58
+ ...config.runtimes.map((runtime) => fs.readFileSync(path.join(RUNTIMES_DIR, `${runtime}.dockerfile`), 'utf8').trimEnd()),
59
+ fs.readFileSync(path.join(RUNTIMES_DIR, 'ai-tools.dockerfile'), 'utf8').trimEnd()
36
60
  ];
37
61
  const content = fragments
38
- .map((fragment) => fs.readFileSync(path.join(RUNTIMES_DIR, fragment), 'utf8').trimEnd())
39
62
  .join('\n\n');
40
63
  return `${content}\n`;
41
64
  }
42
- export function dockerfileSignature(config) {
65
+ export function dockerfileSignature(config, image = EMPTY_IMAGE_CONTRIBUTION) {
43
66
  return createHash('sha256')
44
- .update(dockerfileContent(config))
67
+ .update(dockerfileContent(config, image))
45
68
  .digest('hex')
46
69
  .slice(0, 12);
47
70
  }
48
- export function prepareDockerfile(config) {
71
+ export function prepareDockerfile(config, image = EMPTY_IMAGE_CONTRIBUTION) {
49
72
  if (config.dockerfile) {
50
73
  const customPath = path.resolve(config.repoRoot, config.dockerfile);
51
74
  if (!fs.existsSync(customPath)) {
@@ -53,23 +76,23 @@ export function prepareDockerfile(config) {
53
76
  }
54
77
  return {
55
78
  path: customPath,
56
- signature: dockerfileSignature(config),
79
+ signature: dockerfileSignature(config, image),
57
80
  cleanup() { }
58
81
  };
59
82
  }
60
83
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${config.project}-sandbox-`));
61
84
  const tempPath = path.join(tempDir, 'Dockerfile');
62
- fs.writeFileSync(tempPath, dockerfileContent(config), 'utf8');
85
+ fs.writeFileSync(tempPath, dockerfileContent(config, image), 'utf8');
63
86
  return {
64
87
  path: tempPath,
65
- signature: dockerfileSignature(config),
88
+ signature: dockerfileSignature(config, image),
66
89
  cleanup() {
67
90
  removeDirRecursive(tempDir);
68
91
  }
69
92
  };
70
93
  }
71
- export function composeDockerfile(config) {
72
- const content = dockerfileContent(config);
94
+ export function composeDockerfile(config, image = EMPTY_IMAGE_CONTRIBUTION) {
95
+ const content = dockerfileContent(config, image);
73
96
  const tempPath = path.join(os.tmpdir(), `${config.project}-sandbox.Dockerfile`);
74
97
  fs.writeFileSync(tempPath, content, 'utf8');
75
98
  return tempPath;
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
3
- export function sandboxCoreBindMounts(config, branch, overrides = {}) {
4
- return [
3
+ export function sandboxCoreBindMounts(config, branch, overrides) {
4
+ const mounts = [
5
5
  {
6
6
  hostPaths: overrides.worktree
7
7
  ? [overrides.worktree]
@@ -10,9 +10,9 @@ export function sandboxCoreBindMounts(config, branch, overrides = {}) {
10
10
  readOnly: false
11
11
  },
12
12
  {
13
- hostPaths: [path.join(config.repoRoot, '.agents', 'workspace')],
13
+ hostPaths: [overrides.workspaceViewRoot],
14
14
  containerPath: '/workspace/.agents/workspace',
15
- readOnly: false
15
+ readOnly: true
16
16
  },
17
17
  {
18
18
  hostPaths: [shareCommonDir(config)],
@@ -32,5 +32,18 @@ export function sandboxCoreBindMounts(config, branch, overrides = {}) {
32
32
  readOnly: true
33
33
  }
34
34
  ];
35
+ if (overrides.taskSource && overrides.taskId) {
36
+ mounts.splice(2, 0, {
37
+ hostPaths: [overrides.taskSource],
38
+ containerPath: path.posix.join('/workspace/.agents/workspace/active', overrides.taskId),
39
+ readOnly: false
40
+ });
41
+ }
42
+ mounts.push({
43
+ hostPaths: [overrides.controlDir],
44
+ containerPath: '/run/agent-infra/control',
45
+ readOnly: false
46
+ });
47
+ return mounts;
35
48
  }
36
49
  //# sourceMappingURL=mounts.js.map
@@ -1,13 +1,53 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxRuntimeCapabilityLabel } from './constants.js';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { containerNameCandidates, sandboxBranchLabel, sandboxLabel, sandboxRuntimeCapabilityLabel, sandboxTaskIdLabel, sandboxWorkspaceModeLabel } from './constants.js';
4
6
  import { isAgentClientId } from '../agent-clients/types.js';
5
7
  import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
6
8
  import { toEnginePath } from './engines/wsl2-paths.js';
7
9
  import { sandboxCoreBindMounts } from './mounts.js';
10
+ import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
11
+ import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
8
12
  import { runEngine, runOkEngine, runVerboseEngine } from './shell.js';
9
13
  import { declaredTmpfsSeedEntries, resolveTools, toolConfigDirCandidates } from './tools.js';
10
14
  import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
15
+ import { processIdentityMatches } from '../server/process-state.js';
16
+ function ensureSandboxControlBroker(params) {
17
+ const control = sandboxControlPaths({
18
+ base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
19
+ project: params.config.project,
20
+ container: params.container,
21
+ identity: params.workspace
22
+ });
23
+ if (!fs.existsSync(control.manifestPath))
24
+ return;
25
+ const manifest = JSON.parse(fs.readFileSync(control.manifestPath, 'utf8'));
26
+ const brokerPath = path.join(control.root, 'broker.json');
27
+ try {
28
+ const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8'));
29
+ if (broker.version === 1
30
+ && typeof broker.pid === 'number'
31
+ && typeof broker.startTime === 'string'
32
+ && broker.token === manifest.token
33
+ && processIdentityMatches({ version: 1, pid: broker.pid, startTime: broker.startTime }))
34
+ return;
35
+ }
36
+ catch {
37
+ // A missing, stale, or malformed broker record is replaced below.
38
+ }
39
+ fs.rmSync(brokerPath, { force: true });
40
+ const directory = path.dirname(fileURLToPath(import.meta.url));
41
+ const extension = path.extname(fileURLToPath(import.meta.url));
42
+ const internalCli = path.resolve(directory, '..', '..', 'bin', `internal-cli${extension}`);
43
+ const child = spawn(process.execPath, [internalCli, 'sandbox-control', 'serve', '--manifest', control.manifestPath], { cwd: params.config.repoRoot, detached: true, stdio: 'ignore' });
44
+ child.once('error', () => {
45
+ // Readiness remains fail-closed; a later control request reports the
46
+ // unavailable broker without turning this detached spawn into an
47
+ // unhandled process error.
48
+ });
49
+ child.unref();
50
+ }
11
51
  function findingKey(finding) {
12
52
  return `${finding.repairKind}:${finding.path ?? finding.seed?.targetPath ?? finding.message}`;
13
53
  }
@@ -156,7 +196,28 @@ function hostSourceAccessible(hostPath, writable) {
156
196
  }
157
197
  function expectedMounts(params) {
158
198
  const { config, branch, tools, actualMounts } = params;
159
- const core = sandboxCoreBindMounts(config, branch).map((mount) => ({
199
+ const view = sandboxWorkspaceViewPaths({
200
+ base: config.workspaceViewBase ?? path.join(config.home, '.agent-infra', 'workspace-views'),
201
+ project: config.project,
202
+ container: params.container,
203
+ identity: params.workspace
204
+ });
205
+ const control = sandboxControlPaths({
206
+ base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
207
+ project: config.project,
208
+ container: params.container,
209
+ identity: params.workspace
210
+ });
211
+ const core = sandboxCoreBindMounts(config, branch, {
212
+ workspaceViewRoot: view.root,
213
+ controlDir: control.channelDir,
214
+ ...(params.workspace.mode === 'task-bound'
215
+ ? {
216
+ taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
217
+ taskId: params.workspace.taskId
218
+ }
219
+ : {})
220
+ }).map((mount) => ({
160
221
  path: mount.containerPath,
161
222
  expectedType: 'bind',
162
223
  hostPaths: mount.hostPaths,
@@ -248,6 +309,7 @@ export function collectSandboxRecoverySnapshot(params) {
248
309
  }
249
310
  }
250
311
  const capabilityPlan = createSandboxCapabilityPlan(params.config);
312
+ const workspace = params.workspace ?? { mode: 'branch-only' };
251
313
  const tools = [...capabilityPlan.tools];
252
314
  const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
253
315
  const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
@@ -269,6 +331,10 @@ export function collectSandboxRecoverySnapshot(params) {
269
331
  }));
270
332
  const labels = inspection.Config?.Labels ?? {};
271
333
  const branchLabel = labels[sandboxBranchLabel(params.config)];
334
+ const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
335
+ mode: sandboxWorkspaceModeLabel(params.config),
336
+ taskId: sandboxTaskIdLabel(params.config)
337
+ });
272
338
  const selectedToolIds = new Set(tools.map((tool) => tool.id));
273
339
  const enforceCanonicalPlan = params.config.agentClientSource === 'canonical';
274
340
  const unexpectedCapabilityMounts = enforceCanonicalPlan
@@ -280,6 +346,8 @@ export function collectSandboxRecoverySnapshot(params) {
280
346
  const mountSnapshots = expectedMounts({
281
347
  config: params.config,
282
348
  branch: params.branch,
349
+ container: params.container,
350
+ workspace,
283
351
  tools,
284
352
  actualMounts: mountsByDestination
285
353
  }).map((expected) => {
@@ -313,7 +381,8 @@ export function collectSandboxRecoverySnapshot(params) {
313
381
  return {
314
382
  identityOk: typeof inspection.Id === 'string'
315
383
  && inspection.Id.length > 0
316
- && branchLabel === params.branch,
384
+ && branchLabel === params.branch
385
+ && sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
317
386
  runtimeCapabilityOk: enforceCanonicalPlan
318
387
  ? labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature
319
388
  : undefined,
@@ -424,14 +493,51 @@ function assess(params) {
424
493
  const snapshot = collectSandboxRecoverySnapshot(params);
425
494
  return { snapshot, findings: classifySandboxRecovery(snapshot) };
426
495
  }
427
- export function assertFreshSandboxReady(params) {
496
+ function canRetryFreshReadiness(findings) {
497
+ return findings.length > 0 && findings.every((finding) => finding.repairKind === 'hard-failure'
498
+ && finding.path !== undefined
499
+ && (finding.path.startsWith('/workspace/.agents/workspace')
500
+ || finding.path === '/home/devuser/.host-shell-config'
501
+ || finding.path === '/home/devuser/.bash_aliases'));
502
+ }
503
+ export async function assertFreshSandboxReady(params) {
428
504
  validateTmpfsSeedEntries({
429
505
  engine: params.engine,
430
506
  container: params.container,
431
507
  entries: params.copiedEntries,
432
508
  deps: params.deps
433
509
  });
434
- const { findings } = assess(params);
510
+ let { findings } = assess(params);
511
+ if (canRetryFreshReadiness(findings)) {
512
+ const runVerboseFn = params.deps?.runVerbose ?? runVerboseEngine;
513
+ runVerboseFn(params.engine, 'docker', ['restart', params.container]);
514
+ prepareTmpfsMounts({
515
+ engine: params.engine,
516
+ container: params.container,
517
+ mountPaths: resolveTools(params.config)
518
+ .filter((tool) => tool.tmpfs)
519
+ .map((tool) => tool.containerMount),
520
+ deps: params.deps
521
+ });
522
+ hydrateTmpfsSeedEntries({
523
+ engine: params.engine,
524
+ container: params.container,
525
+ entries: params.copiedEntries,
526
+ replace: true,
527
+ deps: params.deps
528
+ });
529
+ let retried = assess(params);
530
+ if (!retried.findings.some((finding) => finding.repairKind === 'hard-failure')) {
531
+ repairFindings({
532
+ engine: params.engine,
533
+ container: params.container,
534
+ findings: retried.findings,
535
+ deps: params.deps
536
+ });
537
+ retried = assess(params);
538
+ }
539
+ findings = retried.findings;
540
+ }
435
541
  if (findings.length > 0) {
436
542
  throw new Error(`Fresh sandbox readiness check failed: ${describeFindings(findings)}`);
437
543
  }
@@ -441,6 +547,11 @@ export async function ensureSandboxReady(params) {
441
547
  const startFn = deps?.start ?? startSandboxContainer;
442
548
  const warnings = [];
443
549
  let failure = null;
550
+ ensureSandboxControlBroker({
551
+ config: params.config,
552
+ container: params.row.name,
553
+ workspace: params.workspace ?? { mode: 'branch-only' }
554
+ });
444
555
  try {
445
556
  if (!params.row.running) {
446
557
  startFn(params.engine, params.row.name);
@@ -448,6 +559,7 @@ export async function ensureSandboxReady(params) {
448
559
  config: params.config,
449
560
  engine: params.engine,
450
561
  branch: params.branch,
562
+ workspace: params.workspace,
451
563
  container: params.row.name,
452
564
  deps
453
565
  });
@@ -472,6 +584,7 @@ export async function ensureSandboxReady(params) {
472
584
  config: params.config,
473
585
  engine: params.engine,
474
586
  branch: params.branch,
587
+ workspace: params.workspace,
475
588
  container: params.row.name,
476
589
  deps
477
590
  });
@@ -486,6 +599,7 @@ export async function ensureSandboxReady(params) {
486
599
  config: params.config,
487
600
  engine: params.engine,
488
601
  branch: params.branch,
602
+ workspace: params.workspace,
489
603
  container: params.row.name,
490
604
  deps
491
605
  });
@@ -499,6 +613,7 @@ export async function ensureSandboxReady(params) {
499
613
  config: params.config,
500
614
  engine: params.engine,
501
615
  branch: params.branch,
616
+ workspace: params.workspace,
502
617
  container: params.row.name,
503
618
  deps
504
619
  });
@@ -521,6 +636,7 @@ export async function ensureSandboxReady(params) {
521
636
  config: params.config,
522
637
  engine: params.engine,
523
638
  branch: params.branch,
639
+ workspace: params.workspace,
524
640
  container: params.row.name,
525
641
  deps
526
642
  });
@@ -539,6 +655,7 @@ export async function ensureSandboxReady(params) {
539
655
  config: params.config,
540
656
  engine: params.engine,
541
657
  branch: params.branch,
658
+ workspace: params.workspace,
542
659
  container: params.row.name,
543
660
  deps
544
661
  });
@@ -557,6 +674,11 @@ export async function ensureSandboxReady(params) {
557
674
  const warning = `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
558
675
  warnings.push(warning);
559
676
  (params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
677
+ const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
678
+ if (params.row.running) {
679
+ runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
680
+ }
681
+ runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
560
682
  await params.recreate(params.branch);
561
683
  const rows = (deps?.fetchRows ?? fetchSandboxRows)(params.engine, sandboxLabel(params.config), sandboxBranchLabel(params.config));
562
684
  const replacement = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(params.config, params.branch));
@@ -567,6 +689,7 @@ export async function ensureSandboxReady(params) {
567
689
  config: params.config,
568
690
  engine: params.engine,
569
691
  branch: params.branch,
692
+ workspace: params.workspace,
570
693
  container: replacement.name,
571
694
  deps
572
695
  });
@@ -0,0 +1,12 @@
1
+ const SENSITIVE_OUTPUT_PATTERNS = [
2
+ { pattern: /\{[^{}]*"claudeAiOauth"[\s\S]*?\}\s*\}/g, replacement: '[REDACTED credentials blob]' },
3
+ { pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g, replacement: '[REDACTED claude token]' },
4
+ { pattern: /gh[psoru]_[A-Za-z0-9]{30,}/g, replacement: '[REDACTED github token]' },
5
+ { pattern: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, replacement: 'Bearer [REDACTED]' }
6
+ ];
7
+ export function redactCommandError(text) {
8
+ if (!text || typeof text !== 'string')
9
+ return '';
10
+ return SENSITIVE_OUTPUT_PATTERNS.reduce((result, { pattern, replacement }) => result.replace(pattern, replacement), text);
11
+ }
12
+ //# sourceMappingURL=redaction.js.map
@@ -64,69 +64,9 @@ RUN printf '%s\n' \
64
64
  'set -g mouse on' \
65
65
  'set -g status-interval 1' \
66
66
  'set -g status-right-length 80' \
67
- "set -g status-right '#(/usr/local/bin/cc-token-status) | %H:%M'" \
67
+ "set -g status-right '%H:%M'" \
68
68
  > /etc/tmux.conf
69
69
 
70
- RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/cc-token-status
71
- #!/bin/sh
72
- set -eu
73
-
74
- SETTINGS_FILE="/home/devuser/.claude/settings.json"
75
- if [ -r "$SETTINGS_FILE" ] && jq -e '
76
- def has_nonempty($v):
77
- if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
78
- (if type == "object" then . else {} end) as $settings
79
- | (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
80
- | has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
81
- or has_nonempty($env.ANTHROPIC_API_KEY)
82
- or has_nonempty($settings.apiKeyHelper)
83
- ' "$SETTINGS_FILE" >/dev/null 2>&1; then
84
- exit 0
85
- fi
86
-
87
- CRED_FILE="/home/devuser/.claude/.credentials.json"
88
- [ -r "$CRED_FILE" ] || exit 0
89
-
90
- EXPIRES_MS=$(jq -r '(.claudeAiOauth.expiresAt // .expiresAt) // empty' "$CRED_FILE" 2>/dev/null || true)
91
- case "$EXPIRES_MS" in
92
- ''|*[!0-9]*) exit 0 ;;
93
- esac
94
-
95
- NOW_MS=$(($(date +%s) * 1000))
96
- DIFF_MS=$((EXPIRES_MS - NOW_MS))
97
- DIFF_S=$((DIFF_MS / 1000))
98
-
99
- DIM='#[fg=colour245]'
100
- YELLOW='#[fg=yellow]'
101
- YELLOW_BOLD='#[fg=yellow,bold]'
102
- RED_BOLD='#[fg=red,bold]'
103
- RED_REV='#[fg=red,reverse]'
104
- RESET='#[default]'
105
-
106
- if [ "$DIFF_S" -le 0 ]; then
107
- ELAPSED=$(( -DIFF_S ))
108
- M=$((ELAPSED / 60))
109
- printf '%sClaude Code auth EXPIRED %dm ago%s' "$RED_REV" "$M" "$RESET"
110
- elif [ "$DIFF_S" -lt 60 ]; then
111
- printf '%sClaude Code auth expires in %ds%s' "$RED_BOLD" "$DIFF_S" "$RESET"
112
- elif [ "$DIFF_S" -lt 300 ]; then
113
- M=$((DIFF_S / 60))
114
- S=$((DIFF_S % 60))
115
- printf '%sClaude Code auth expires in %dm %ds%s' "$RED_BOLD" "$M" "$S" "$RESET"
116
- elif [ "$DIFF_S" -lt 1800 ]; then
117
- M=$((DIFF_S / 60))
118
- printf '%sClaude Code auth expires in %dm%s' "$YELLOW_BOLD" "$M" "$RESET"
119
- elif [ "$DIFF_S" -lt 3600 ]; then
120
- M=$((DIFF_S / 60))
121
- printf '%sClaude Code auth expires in %dm%s' "$YELLOW" "$M" "$RESET"
122
- else
123
- TOTAL_M=$((DIFF_S / 60))
124
- H=$((TOTAL_M / 60))
125
- M=$((TOTAL_M % 60))
126
- printf '%sClaude Code auth expires in %dh %dm%s' "$DIM" "$H" "$M" "$RESET"
127
- fi
128
- SCRIPT
129
-
130
70
  RUN cat > /usr/local/bin/sandbox-dotfiles-link <<'SCRIPT' && chmod +x /usr/local/bin/sandbox-dotfiles-link
131
71
  #!/bin/sh
132
72
  # Mirror /dotfiles/ tree as symlinks under $HOME/, overwriting any image-baked
@@ -136,23 +76,34 @@ set -eu
136
76
  DOTFILES_SRC=/dotfiles
137
77
  [ -d "$DOTFILES_SRC" ] || exit 0
138
78
 
139
- cd "$DOTFILES_SRC"
140
- find . -type f -print | while IFS= read -r rel; do
141
- rel=${rel#./}
142
- target="$HOME/$rel"
143
- case "$rel" in
79
+ is_dotfiles_excluded() {
80
+ candidate=$1
81
+ case "$candidate" in
144
82
  .ssh|.ssh/*|\
145
83
  .gnupg|.gnupg/*|\
146
- .claude|.claude/*|\
147
- .codex|.codex/*|\
148
- .gemini|.gemini/*|\
149
- .config/opencode|.config/opencode/*|\
150
- .local/share/opencode|.local/share/opencode/*|\
151
84
  .host-shell-config|.host-shell-config/*|\
152
85
  .gitconfig|.gitignore_global|.stCommitMsg|.bash_aliases|README.md)
153
- continue ;;
86
+ return 0 ;;
154
87
  esac
155
88
 
89
+ [ -r /etc/agent-infra/dotfiles-exclusions ] || return 1
90
+ while IFS= read -r prefix; do
91
+ [ -n "$prefix" ] || continue
92
+ case "$candidate" in
93
+ "$prefix"|"$prefix"/*) return 0 ;;
94
+ esac
95
+ done < /etc/agent-infra/dotfiles-exclusions
96
+ return 1
97
+ }
98
+
99
+ cd "$DOTFILES_SRC"
100
+ find . -type f -print | while IFS= read -r rel; do
101
+ rel=${rel#./}
102
+ target="$HOME/$rel"
103
+ if is_dotfiles_excluded "$rel"; then
104
+ continue
105
+ fi
106
+
156
107
  mkdir -p "$(dirname "$target")"
157
108
  if [ -d "$target" ] && [ ! -L "$target" ]; then
158
109
  printf 'sandbox-dotfiles-link: skipping %s (existing directory; use nested path like %s/<file> instead)\n' "$target" "$rel" >&2
@@ -198,8 +149,7 @@ fi
198
149
  exec tmux new-session -s "$SESSION"
199
150
  SCRIPT
200
151
 
201
- RUN test -s /usr/local/bin/cc-token-status && test -x /usr/local/bin/cc-token-status && \
202
- test -s /usr/local/bin/sandbox-dotfiles-link && test -x /usr/local/bin/sandbox-dotfiles-link && \
152
+ RUN test -s /usr/local/bin/sandbox-dotfiles-link && test -x /usr/local/bin/sandbox-dotfiles-link && \
203
153
  test -s /usr/local/bin/sandbox-tmux-entry && test -x /usr/local/bin/sandbox-tmux-entry
204
154
 
205
155
  ENV LANG=en_US.UTF-8