@fitlab-ai/agent-infra 0.8.2 → 0.8.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 (198) hide show
  1. package/README.zh-CN.md +10 -0
  2. package/bin/cli.ts +1 -1
  3. package/dist/bin/cli.js +2 -2
  4. package/dist/lib/cp.js +57 -16
  5. package/dist/lib/decide.js +2 -2
  6. package/dist/lib/defaults.json +2 -0
  7. package/dist/lib/init.js +7 -7
  8. package/dist/lib/merge.js +1 -1
  9. package/dist/lib/prompt.js +1 -1
  10. package/dist/lib/run/index.js +7 -7
  11. package/dist/lib/run/prompt.js +1 -1
  12. package/dist/lib/sandbox/capture.js +10 -8
  13. package/dist/lib/sandbox/clipboard/bridge.js +59 -21
  14. package/dist/lib/sandbox/clipboard/inbox.js +82 -0
  15. package/dist/lib/sandbox/clipboard/index.js +5 -6
  16. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  17. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  18. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  19. package/dist/lib/sandbox/commands/create.js +106 -88
  20. package/dist/lib/sandbox/commands/enter.js +9 -9
  21. package/dist/lib/sandbox/commands/list-running.js +2 -2
  22. package/dist/lib/sandbox/commands/ls.js +7 -7
  23. package/dist/lib/sandbox/commands/prune.js +8 -8
  24. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  25. package/dist/lib/sandbox/commands/refresh.js +2 -2
  26. package/dist/lib/sandbox/commands/rm.js +11 -11
  27. package/dist/lib/sandbox/commands/show.js +4 -4
  28. package/dist/lib/sandbox/commands/start.js +4 -4
  29. package/dist/lib/sandbox/commands/vm.js +4 -4
  30. package/dist/lib/sandbox/config.js +10 -4
  31. package/dist/lib/sandbox/constants.js +4 -1
  32. package/dist/lib/sandbox/credentials.js +1 -1
  33. package/dist/lib/sandbox/dotfiles.js +1 -1
  34. package/dist/lib/sandbox/engine.js +3 -3
  35. package/dist/lib/sandbox/engines/index.js +5 -5
  36. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  37. package/dist/lib/sandbox/image-build.js +80 -0
  38. package/dist/lib/sandbox/image-prune.js +2 -2
  39. package/dist/lib/sandbox/index.js +10 -10
  40. package/dist/lib/sandbox/managed-fs.js +1 -1
  41. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  42. package/dist/lib/sandbox/runtimes/base.dockerfile +3 -0
  43. package/dist/lib/sandbox/shell.js +9 -1
  44. package/dist/lib/sandbox/tools.js +4 -4
  45. package/dist/lib/server/adapters/feishu/index.js +10 -3
  46. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  47. package/dist/lib/server/adapters/feishu/transport.js +2 -13
  48. package/dist/lib/server/daemon.js +64 -43
  49. package/dist/lib/server/display.js +83 -0
  50. package/dist/lib/server/index.js +2 -2
  51. package/dist/lib/server/process-control.js +2 -2
  52. package/dist/lib/server/protocol.js +2 -2
  53. package/dist/lib/server/streamer.js +5 -4
  54. package/dist/lib/task/commands/cat.js +2 -2
  55. package/dist/lib/task/commands/decisions.js +4 -4
  56. package/dist/lib/task/commands/files.js +3 -3
  57. package/dist/lib/task/commands/grep.js +3 -3
  58. package/dist/lib/task/commands/issue-body.js +4 -4
  59. package/dist/lib/task/commands/log.js +5 -5
  60. package/dist/lib/task/commands/ls.js +3 -3
  61. package/dist/lib/task/commands/show.js +1 -1
  62. package/dist/lib/task/commands/status.js +71 -25
  63. package/dist/lib/task/index.js +9 -9
  64. package/dist/lib/task/resolve-ref.js +1 -1
  65. package/dist/lib/task/workflow-warnings.js +94 -0
  66. package/dist/lib/update.js +4 -4
  67. package/lib/cp.ts +56 -15
  68. package/lib/defaults.json +2 -0
  69. package/lib/sandbox/capture.ts +5 -3
  70. package/lib/sandbox/clipboard/bridge.ts +62 -18
  71. package/lib/sandbox/clipboard/inbox.ts +83 -0
  72. package/lib/sandbox/clipboard/index.ts +5 -6
  73. package/lib/sandbox/clipboard/linux.ts +124 -0
  74. package/lib/sandbox/clipboard/win32.ts +173 -0
  75. package/lib/sandbox/commands/create.ts +132 -88
  76. package/lib/sandbox/commands/rebuild.ts +42 -54
  77. package/lib/sandbox/config.ts +13 -1
  78. package/lib/sandbox/constants.ts +4 -0
  79. package/lib/sandbox/image-build.ts +134 -0
  80. package/lib/sandbox/runtimes/base.dockerfile +3 -0
  81. package/lib/sandbox/shell.ts +11 -2
  82. package/lib/sandbox/tools.ts +5 -5
  83. package/lib/server/adapters/_contract.ts +3 -0
  84. package/lib/server/adapters/feishu/index.ts +11 -3
  85. package/lib/server/adapters/feishu/renderer.ts +99 -0
  86. package/lib/server/adapters/feishu/transport.ts +5 -18
  87. package/lib/server/daemon.ts +74 -38
  88. package/lib/server/display.ts +136 -0
  89. package/lib/server/streamer.ts +5 -4
  90. package/lib/task/commands/log.ts +3 -3
  91. package/lib/task/commands/status.ts +102 -21
  92. package/lib/task/workflow-warnings.ts +121 -0
  93. package/package.json +2 -2
  94. package/templates/.agents/README.en.md +3 -0
  95. package/templates/.agents/README.zh-CN.md +3 -0
  96. package/templates/.agents/rules/create-issue.github.en.md +4 -4
  97. package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -4
  98. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  99. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  100. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  101. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  102. package/templates/.agents/rules/next-step-output.en.md +15 -1
  103. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  104. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  105. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  106. package/templates/.agents/rules/review-handshake.en.md +2 -2
  107. package/templates/.agents/rules/review-handshake.zh-CN.md +2 -2
  108. package/templates/.agents/rules/task-management.en.md +1 -1
  109. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  110. package/templates/.agents/rules/testing-discipline.en.md +3 -41
  111. package/templates/.agents/rules/testing-discipline.zh-CN.md +3 -41
  112. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  113. package/templates/.agents/scripts/validate-artifact.js +213 -0
  114. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  115. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  116. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +1 -1
  118. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  119. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  120. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  121. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  122. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +9 -9
  123. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  124. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  125. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  127. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +2 -2
  130. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +2 -2
  131. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +2 -2
  132. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  133. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  134. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  135. package/templates/.agents/skills/commit/reference/task-status-update.en.md +6 -6
  136. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +6 -6
  137. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +142 -0
  138. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +142 -0
  139. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  140. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  141. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  142. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  143. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  144. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  145. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  148. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  149. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  150. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  151. package/templates/.agents/skills/create-task/SKILL.en.md +6 -3
  152. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +9 -6
  153. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +3 -3
  155. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  156. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  157. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
  158. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  159. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
  160. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -2
  161. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -3
  162. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +165 -41
  163. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  164. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  165. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  166. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  167. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  168. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  169. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  170. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  171. package/templates/.agents/skills/review-code/SKILL.en.md +8 -4
  172. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +6 -3
  173. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  174. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  175. package/templates/.agents/skills/review-code/reference/output-templates.en.md +5 -5
  176. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +5 -5
  177. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -2
  178. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -2
  179. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  180. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  181. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  182. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  183. package/templates/.agents/skills/test/SKILL.zh-CN.md +26 -8
  184. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  185. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +36 -6
  186. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  187. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  188. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +8 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +8 -1
  190. package/templates/.agents/templates/task.en.md +7 -0
  191. package/templates/.agents/templates/task.zh-CN.md +7 -0
  192. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  193. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  194. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  195. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  196. package/templates/.github/workflows/metadata-sync.yml +5 -0
  197. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  198. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
@@ -1,58 +1,37 @@
1
1
  import { parseArgs } from 'node:util';
2
- import { createHash } from 'node:crypto';
3
2
  import * as p from '@clack/prompts';
4
3
  import pc from 'picocolors';
5
- import { loadConfig } from "../config.js";
6
- import { prepareDockerfile } from "../dockerfile.js";
7
- import { sandboxImageConfigLabel, sandboxLabel } from "../constants.js";
8
- import { detectEngine, ensureDocker } from "../engine.js";
9
- import { runEngine, runSafeEngine, runVerboseEngine } from "../shell.js";
10
- import { pruneSandboxDanglingImages } from "../image-prune.js";
11
- import { imageSignatureFields, resolveTools, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
12
- import { toEnginePath } from "../engines/wsl2-paths.js";
13
- import { resolveBuildUid } from "../engines/native.js";
4
+ import { loadConfig } from '../config.js';
5
+ import { prepareDockerfile } from '../dockerfile.js';
6
+ import { sandboxImageRefreshLabel } from '../constants.js';
7
+ import { detectEngine, ensureDocker } from '../engine.js';
8
+ import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.js';
9
+ import { pruneSandboxDanglingImages } from '../image-prune.js';
10
+ import { resolveTools } from '../tools.js';
11
+ import { buildImageSignature, buildSandboxImageArgs, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
14
12
  const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
15
- function buildSignature(preparedDockerfile, tools) {
16
- return createHash('sha256')
17
- .update(JSON.stringify({
18
- dockerfile: preparedDockerfile.signature,
19
- tools: imageSignatureFields(tools)
20
- }))
21
- .digest('hex')
22
- .slice(0, 12);
23
- }
24
- export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false } = {}) {
25
- const selectedEngine = engine ?? detectEngine(config);
26
- const { uid: hostUid, gid: hostGid } = resolveBuildUid({
27
- engine: selectedEngine,
13
+ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
14
+ return buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
15
+ engine: engine ?? detectEngine(config),
28
16
  runFn,
29
17
  runSafeFn,
30
- env
18
+ env,
19
+ refresh,
20
+ lastRefresh
31
21
  });
32
- const args = [
33
- 'build',
34
- '-t',
35
- config.imageName,
36
- '--build-arg',
37
- `HOST_UID=${hostUid}`,
38
- '--build-arg',
39
- `HOST_GID=${hostGid}`,
40
- '--build-arg',
41
- `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
42
- '--build-arg',
43
- `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
44
- '--label',
45
- sandboxLabel(config),
46
- '--label',
47
- `${sandboxImageConfigLabel(config)}=${imageSignature}`,
48
- '-f',
49
- toEnginePath(selectedEngine, dockerfilePath),
50
- toEnginePath(selectedEngine, config.repoRoot)
51
- ];
52
- if (refresh) {
53
- args.splice(1, 0, '--no-cache', '--pull');
22
+ }
23
+ function readExistingLastRefresh(config, engine) {
24
+ if (!runOkEngine(engine, 'docker', ['image', 'inspect', config.imageName])) {
25
+ return 0;
54
26
  }
55
- return args;
27
+ const labels = parseImageLabels(runSafeEngine(engine, 'docker', [
28
+ 'image',
29
+ 'inspect',
30
+ '--format',
31
+ '{{ json .Config.Labels }}',
32
+ config.imageName
33
+ ]));
34
+ return parseRefreshTimestamp(labels[sandboxImageRefreshLabel(config)] ?? '');
56
35
  }
57
36
  export async function rebuild(args) {
58
37
  const { values } = parseArgs({
@@ -72,24 +51,33 @@ export async function rebuild(args) {
72
51
  const config = loadConfig();
73
52
  const tools = resolveTools(config);
74
53
  const preparedDockerfile = prepareDockerfile(config);
75
- const imageSignature = buildSignature(preparedDockerfile, tools);
54
+ const imageSignature = buildImageSignature(preparedDockerfile, tools);
76
55
  const quiet = values.quiet ?? false;
77
56
  const refresh = values.refresh ?? false;
78
57
  const engine = detectEngine(config);
79
58
  await ensureDocker(config, undefined);
59
+ const lastRefresh = refresh ? Date.now() : readExistingLastRefresh(config, engine);
80
60
  p.intro(pc.cyan('Rebuilding sandbox image'));
81
61
  try {
82
62
  if (quiet) {
83
63
  const spinner = p.spinner();
84
64
  spinner.start('Building image...');
85
- runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
65
+ runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
66
+ engine,
67
+ refresh,
68
+ lastRefresh
69
+ }), {
86
70
  cwd: config.repoRoot
87
71
  });
88
72
  spinner.stop(pc.green('Sandbox image rebuilt'));
89
73
  }
90
74
  else {
91
75
  p.log.step('Building image');
92
- runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), { cwd: config.repoRoot });
76
+ runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
77
+ engine,
78
+ refresh,
79
+ lastRefresh
80
+ }), { cwd: config.repoRoot });
93
81
  p.log.success(pc.green('Sandbox image rebuilt'));
94
82
  }
95
83
  pruneSandboxDanglingImages(config, engine);
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { parseArgs } from 'node:util';
3
- import { buildLockedGuidance, discoverProjects, formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
4
- import { runProbe } from "../shell.js";
3
+ import { buildLockedGuidance, discoverProjects, formatCredentialWarnings, formatRemaining, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
4
+ import { runProbe } from '../shell.js';
5
5
  const USAGE = 'Usage: ai sandbox refresh';
6
6
  export function probeClaudeStatus(spawnFn = runProbe) {
7
7
  const result = spawnFn('claude', ['/status'], {
@@ -3,21 +3,21 @@ import path from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
4
  import * as p from '@clack/prompts';
5
5
  import pc from 'picocolors';
6
- import { loadConfig } from "../config.js";
7
- import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from "../constants.js";
8
- import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from "../engine.js";
9
- import { pruneSandboxDanglingImages } from "../image-prune.js";
10
- import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
11
- import { runOk, runSafe, runSafeEngine } from "../shell.js";
12
- import { resolveTaskBranch } from "../task-resolver.js";
13
- import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from "../tools.js";
14
- import { fetchSandboxRows } from "./list-running.js";
15
- import { lookupShortIdByBranch } from "../../task/short-id.js";
6
+ import { loadConfig } from '../config.js';
7
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel, shareBranchDir, shellConfigDirCandidates, worktreeDirCandidates } from '../constants.js';
8
+ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
9
+ import { pruneSandboxDanglingImages } from '../image-prune.js';
10
+ import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
+ import { runOk, runSafe, runSafeEngine } from '../shell.js';
12
+ import { resolveTaskBranch } from '../task-resolver.js';
13
+ import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
14
+ import { fetchSandboxRows } from './list-running.js';
15
+ import { lookupShortIdByBranch } from '../../task/short-id.js';
16
16
  const USAGE = `Usage:
17
17
  ai sandbox rm <branch> Remove one sandbox (branch | TASK-id | short id)
18
18
  ai sandbox rm --all [--dry-run] [--yes] Remove every sandbox not bound to an active task
19
19
  ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
20
- export { assertManagedPath } from "../managed-fs.js";
20
+ export { assertManagedPath } from '../managed-fs.js';
21
21
  function projectToolDirs(config, tools) {
22
22
  return tools.flatMap((tool) => toolProjectDirCandidates(tool, config.project));
23
23
  }
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
- import { loadConfig } from "../config.js";
5
- import { assertValidBranchName, worktreeDirCandidates } from "../constants.js";
6
- import { resolveBranchArg } from "./list-running.js";
7
- import { resolveTools, toolConfigDirCandidates } from "../tools.js";
4
+ import { loadConfig } from '../config.js';
5
+ import { assertValidBranchName, worktreeDirCandidates } from '../constants.js';
6
+ import { resolveBranchArg } from './list-running.js';
7
+ import { resolveTools, toolConfigDirCandidates } from '../tools.js';
8
8
  const USAGE = `Usage: ai sandbox show <branch | TASK-id | N | '#N'>
9
9
 
10
10
  Shows one sandbox's worktree path and per-tool state paths (Claude Code,
@@ -1,7 +1,7 @@
1
- import { loadConfig } from "../config.js";
2
- import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
3
- import { detectEngine } from "../engine.js";
4
- import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from "./list-running.js";
1
+ import { loadConfig } from '../config.js';
2
+ import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
3
+ import { detectEngine } from '../engine.js';
4
+ import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from './list-running.js';
5
5
  const USAGE = `Usage: ai sandbox start <branch | TASK-id | N | '#N'>
6
6
 
7
7
  Start an existing sandbox container that has stopped (for example after the
@@ -1,10 +1,10 @@
1
1
  import { parseArgs } from 'node:util';
2
2
  import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
- import { loadConfig } from "../config.js";
5
- import { parsePositiveIntegerOption } from "../constants.js";
6
- import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, startManagedVm, stopManagedVm } from "../engine.js";
7
- import { runOk, runSafe } from "../shell.js";
4
+ import { loadConfig } from '../config.js';
5
+ import { parsePositiveIntegerOption } from '../constants.js';
6
+ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, startManagedVm, stopManagedVm } from '../engine.js';
7
+ import { runOk, runSafe } from '../shell.js';
8
8
  const USAGE = `Usage: ai sandbox vm <status|start|stop> [--cpu <n>] [--memory <n>]`;
9
9
  export function ensureManagedVm(engine) {
10
10
  if (engine === ENGINES.NATIVE) {
@@ -3,14 +3,15 @@ import path from 'node:path';
3
3
  import { homedir, platform } from 'node:os';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import pc from 'picocolors';
6
- import { validateSandboxEngine } from "./engine.js";
7
- import { hostJoin } from "./engines/wsl2-paths.js";
8
- import { findRuntimeEngineMismatches } from "./runtime-engines.js";
9
- import { parseCustomTools } from "./tools.js";
6
+ import { validateSandboxEngine } from './engine.js';
7
+ import { hostJoin } from './engines/wsl2-paths.js';
8
+ import { findRuntimeEngineMismatches } from './runtime-engines.js';
9
+ import { parseCustomTools } from './tools.js';
10
10
  const DEFAULTS = Object.freeze({
11
11
  engine: null,
12
12
  runtimes: ['node22'],
13
13
  tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
14
+ refreshIntervalDays: 7,
14
15
  dockerfile: null,
15
16
  vm: {
16
17
  cpu: null,
@@ -32,11 +33,15 @@ function detectRepoRoot() {
32
33
  function asPositiveNumberOrNull(value) {
33
34
  return typeof value === 'number' ? value : null;
34
35
  }
36
+ function asNonNegativeIntegerOrDefault(value, fallback) {
37
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
38
+ }
35
39
  function cloneDefaults() {
36
40
  return {
37
41
  engine: DEFAULTS.engine,
38
42
  runtimes: [...DEFAULTS.runtimes],
39
43
  tools: [...DEFAULTS.tools],
44
+ refreshIntervalDays: DEFAULTS.refreshIntervalDays,
40
45
  dockerfile: DEFAULTS.dockerfile,
41
46
  vm: { ...DEFAULTS.vm }
42
47
  };
@@ -96,6 +101,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
96
101
  ? [...sandbox.tools]
97
102
  : defaults.tools,
98
103
  customTools,
104
+ refreshIntervalDays: asNonNegativeIntegerOrDefault(sandbox.refreshIntervalDays, defaults.refreshIntervalDays),
99
105
  dockerfile,
100
106
  vm: {
101
107
  cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
@@ -1,6 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import { execFileSync } from 'node:child_process';
3
- import { hostJoin } from "./engines/wsl2-paths.js";
3
+ import { hostJoin } from './engines/wsl2-paths.js';
4
4
  const validatedBranches = new Set();
5
5
  function dedupe(items) {
6
6
  return [...new Set(items)];
@@ -72,6 +72,9 @@ export function sandboxBranchLabel(config) {
72
72
  export function sandboxImageConfigLabel(config) {
73
73
  return `${sandboxLabel(config)}.image-config`;
74
74
  }
75
+ export function sandboxImageRefreshLabel(config) {
76
+ return `${sandboxLabel(config)}.last-refresh`;
77
+ }
75
78
  export function parsePositiveIntegerOption(value, optionName) {
76
79
  if (value === undefined || value === null) {
77
80
  return undefined;
@@ -2,7 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { hostJoin } from "./engines/wsl2-paths.js";
5
+ import { hostJoin } from './engines/wsl2-paths.js';
6
6
  const LOCKED_PATTERN = /errSecInteractionNotAllowed|User interaction is not allowed/i;
7
7
  const NOT_FOUND_PATTERN = /errSecItemNotFound|specified item could not be found/i;
8
8
  const REDACTION_PATTERNS = [
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { hostJoin } from "./engines/wsl2-paths.js";
3
+ import { hostJoin } from './engines/wsl2-paths.js';
4
4
  export function dotfilesCacheDir(home, project) {
5
5
  return hostJoin(home, '.agent-infra', '.cache', 'dotfiles-resolved', project);
6
6
  }
@@ -1,7 +1,7 @@
1
1
  import { platform } from 'node:os';
2
- import { detectHostResources } from "./constants.js";
3
- import { ADAPTERS, enginesForPlatform, getAdapter } from "./engines/index.js";
4
- import { run, runOk, runSafe, runVerbose } from "./shell.js";
2
+ import { detectHostResources } from './constants.js';
3
+ import { ADAPTERS, enginesForPlatform, getAdapter } from './engines/index.js';
4
+ import { run, runOk, runSafe, runVerbose } from './shell.js';
5
5
  export const ENGINES = Object.freeze({
6
6
  COLIMA: 'colima',
7
7
  ORBSTACK: 'orbstack',
@@ -1,8 +1,8 @@
1
- import { colimaAdapter } from "./colima.js";
2
- import { dockerDesktopAdapter } from "./docker-desktop.js";
3
- import { nativeAdapter } from "./native.js";
4
- import { orbstackAdapter } from "./orbstack.js";
5
- import { wsl2Adapter } from "./wsl2.js";
1
+ import { colimaAdapter } from './colima.js';
2
+ import { dockerDesktopAdapter } from './docker-desktop.js';
3
+ import { nativeAdapter } from './native.js';
4
+ import { orbstackAdapter } from './orbstack.js';
5
+ import { wsl2Adapter } from './wsl2.js';
6
6
  export const ADAPTERS = Object.freeze({
7
7
  colima: colimaAdapter,
8
8
  orbstack: orbstackAdapter,
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { selinuxLabelForMount } from "./selinux.js";
2
+ import { selinuxLabelForMount } from './selinux.js';
3
3
  const WINDOWS_DRIVE_PATH_PATTERN = /^([A-Za-z]):[\\/](.*)$/;
4
4
  const UNC_PATH_PATTERN = /^(?:\\\\|\/\/)[^\\/]+[\\/][^\\/]+/;
5
5
  export function hostJoin(basePath, ...segments) {
@@ -0,0 +1,80 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { detectEngine } from './engine.js';
3
+ import { resolveBuildUid } from './engines/native.js';
4
+ import { toEnginePath } from './engines/wsl2-paths.js';
5
+ import { sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel } from './constants.js';
6
+ import { runEngine, runSafeEngine } from './shell.js';
7
+ import { imageSignatureFields, toolNpmPackagesArg, toolShellInstallScriptBase64 } from './tools.js';
8
+ export function buildImageSignature(preparedDockerfile, tools) {
9
+ return createHash('sha256')
10
+ .update(JSON.stringify({
11
+ dockerfile: preparedDockerfile.signature,
12
+ tools: imageSignatureFields(tools)
13
+ }))
14
+ .digest('hex')
15
+ .slice(0, 12);
16
+ }
17
+ export function buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
18
+ const selectedEngine = engine ?? detectEngine({ engine: config.engine });
19
+ const { uid: hostUid, gid: hostGid } = resolveBuildUid({
20
+ engine: selectedEngine,
21
+ runFn,
22
+ runSafeFn,
23
+ env
24
+ });
25
+ const args = [
26
+ 'build',
27
+ '-t',
28
+ config.imageName,
29
+ '--build-arg',
30
+ `HOST_UID=${hostUid}`,
31
+ '--build-arg',
32
+ `HOST_GID=${hostGid}`,
33
+ '--build-arg',
34
+ `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
35
+ '--build-arg',
36
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
37
+ '--label',
38
+ sandboxLabel(config),
39
+ '--label',
40
+ `${sandboxImageConfigLabel(config)}=${imageSignature}`
41
+ ];
42
+ if (lastRefresh !== undefined) {
43
+ args.push('--label', `${sandboxImageRefreshLabel(config)}=${lastRefresh}`);
44
+ }
45
+ args.push('-f', toEnginePath(selectedEngine, dockerfilePath), toEnginePath(selectedEngine, config.repoRoot));
46
+ if (refresh) {
47
+ args.splice(1, 0, '--no-cache', '--pull');
48
+ }
49
+ return args;
50
+ }
51
+ export function parseRefreshTimestamp(value) {
52
+ const parsed = Number(value);
53
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
54
+ }
55
+ export function parseImageLabels(raw) {
56
+ try {
57
+ const parsed = JSON.parse(raw);
58
+ if (!parsed || typeof parsed !== 'object') {
59
+ return {};
60
+ }
61
+ return Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === 'string'));
62
+ }
63
+ catch {
64
+ return {};
65
+ }
66
+ }
67
+ export function isRefreshDisabled(env, noRefreshFlag) {
68
+ if (noRefreshFlag) {
69
+ return true;
70
+ }
71
+ const value = env.AI_SANDBOX_NO_REFRESH?.trim().toLowerCase();
72
+ return value === '1' || value === 'true' || value === 'yes';
73
+ }
74
+ export function isRefreshDue(lastRefresh, now, intervalDays) {
75
+ if (intervalDays <= 0 || lastRefresh > now) {
76
+ return false;
77
+ }
78
+ return now - lastRefresh >= intervalDays * 24 * 60 * 60 * 1000;
79
+ }
80
+ //# sourceMappingURL=image-build.js.map
@@ -1,6 +1,6 @@
1
1
  import * as p from '@clack/prompts';
2
- import { sandboxLabel } from "./constants.js";
3
- import { runEngine } from "./shell.js";
2
+ import { sandboxLabel } from './constants.js';
3
+ import { runEngine } from './shell.js';
4
4
  export function pruneSandboxDanglingImages(config, engine) {
5
5
  try {
6
6
  runEngine(engine, 'docker', [
@@ -37,12 +37,12 @@ export async function runSandbox(args) {
37
37
  }
38
38
  switch (subcommand) {
39
39
  case 'create': {
40
- const { create } = await import("./commands/create.js");
40
+ const { create } = await import('./commands/create.js');
41
41
  await create(rest);
42
42
  break;
43
43
  }
44
44
  case 'exec': {
45
- const { enter } = await import("./commands/enter.js");
45
+ const { enter } = await import('./commands/enter.js');
46
46
  const exitCode = await enter(rest);
47
47
  if (typeof exitCode === 'number' && exitCode !== 0) {
48
48
  process.exitCode = exitCode;
@@ -50,27 +50,27 @@ export async function runSandbox(args) {
50
50
  break;
51
51
  }
52
52
  case 'ls': {
53
- const { ls } = await import("./commands/ls.js");
53
+ const { ls } = await import('./commands/ls.js');
54
54
  ls(rest);
55
55
  break;
56
56
  }
57
57
  case 'show': {
58
- const { show } = await import("./commands/show.js");
58
+ const { show } = await import('./commands/show.js');
59
59
  show(rest);
60
60
  break;
61
61
  }
62
62
  case 'prune': {
63
- const { prune } = await import("./commands/prune.js");
63
+ const { prune } = await import('./commands/prune.js');
64
64
  await prune(rest);
65
65
  break;
66
66
  }
67
67
  case 'rebuild': {
68
- const { rebuild } = await import("./commands/rebuild.js");
68
+ const { rebuild } = await import('./commands/rebuild.js');
69
69
  await rebuild(rest);
70
70
  break;
71
71
  }
72
72
  case 'refresh': {
73
- const { refresh } = await import("./commands/refresh.js");
73
+ const { refresh } = await import('./commands/refresh.js');
74
74
  const exitCode = await refresh(rest);
75
75
  if (typeof exitCode === 'number' && exitCode !== 0) {
76
76
  process.exitCode = exitCode;
@@ -78,17 +78,17 @@ export async function runSandbox(args) {
78
78
  break;
79
79
  }
80
80
  case 'rm': {
81
- const { rm } = await import("./commands/rm.js");
81
+ const { rm } = await import('./commands/rm.js');
82
82
  await rm(rest);
83
83
  break;
84
84
  }
85
85
  case 'start': {
86
- const { start } = await import("./commands/start.js");
86
+ const { start } = await import('./commands/start.js');
87
87
  await start(rest);
88
88
  break;
89
89
  }
90
90
  case 'vm': {
91
- const { vm } = await import("./commands/vm.js");
91
+ const { vm } = await import('./commands/vm.js');
92
92
  await vm(rest);
93
93
  break;
94
94
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { run } from "./shell.js";
3
+ import { run } from './shell.js';
4
4
  export function assertManagedPath(root, target) {
5
5
  const resolvedRoot = path.resolve(root);
6
6
  const resolvedTarget = path.resolve(target);
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { shareBranchDir, shareCommonDir } from "./constants.js";
3
+ import { shareBranchDir, shareCommonDir } from './constants.js';
4
4
  const DOTFILES_README = `# User-level dotfiles channel
5
5
 
6
6
  This directory is mounted **read-only** into every sandbox container at
@@ -12,6 +12,9 @@ RUN if [ "${HOST_UID}" = "0" ]; then \
12
12
  (groupadd -o -g ${HOST_GID} devuser || true) && \
13
13
  useradd -o -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
14
14
  else \
15
+ if ubuntu_uid="$(id -u ubuntu 2>/dev/null)" && [ "${ubuntu_uid}" = "${HOST_UID}" ]; then \
16
+ userdel -r ubuntu || exit 1; \
17
+ fi; \
15
18
  (groupadd -g ${HOST_GID} devuser || true) && \
16
19
  useradd -u ${HOST_UID} -g ${HOST_GID} -m -s /bin/bash devuser; \
17
20
  fi
@@ -1,6 +1,7 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { getAdapter } from './engines/index.js';
4
5
  const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
5
6
  function normalizeOptions(opts = {}, stdio) {
6
7
  return {
@@ -114,6 +115,12 @@ export function commandForEngine(engine, cmd, args = []) {
114
115
  const resolvedWrapper = resolveCommand('wsl.exe');
115
116
  return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
116
117
  }
118
+ if (cmd === 'docker') {
119
+ const context = getAdapter(engine).dockerContext;
120
+ if (context) {
121
+ return { cmd, args: ['--context', context, ...args] };
122
+ }
123
+ }
117
124
  return { cmd, args };
118
125
  }
119
126
  export function runEngine(engine, cmd, args, opts = {}) {
@@ -122,7 +129,8 @@ export function runEngine(engine, cmd, args, opts = {}) {
122
129
  }
123
130
  export function execEngine(engine, cmd, args, opts = {}) {
124
131
  const command = commandForEngine(engine, cmd, args);
125
- return execFileSync(command.cmd, command.args, opts);
132
+ const resolved = resolveCommand(command.cmd);
133
+ return execFileSync(resolved, command.args, commandOptions(resolved, opts));
126
134
  }
127
135
  export function runOkEngine(engine, cmd, args, opts = {}) {
128
136
  const command = commandForEngine(engine, cmd, args);
@@ -1,5 +1,5 @@
1
- import { safeNameCandidates, sanitizeBranchName } from "./constants.js";
2
- import { hostJoin } from "./engines/wsl2-paths.js";
1
+ import { safeNameCandidates, sanitizeBranchName } from './constants.js';
2
+ import { hostJoin } from './engines/wsl2-paths.js';
3
3
  const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
4
4
  function createBuiltinTools(home, project) {
5
5
  return {
@@ -44,8 +44,8 @@ function createBuiltinTools(home, project) {
44
44
  // codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
45
45
  // a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
46
46
  // codex home as tmpfs so those logs stay in RAM and die with the container.
47
- // Only the seeded config (config.toml, model-catalogs) is bound back over
48
- // the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
47
+ // Seeded config (config.toml, model-catalogs) is copied into the tmpfs at
48
+ // startup; runtime files like logs_2.sqlite stay in RAM.
49
49
  tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
50
50
  hostLiveMounts: [
51
51
  { hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
@@ -1,4 +1,5 @@
1
- import { cardMessage, createFeishuTransport, normalizeMessage } from "./transport.js";
1
+ import { renderFeishuMessage } from './renderer.js';
2
+ import { createFeishuTransport, normalizeMessage } from './transport.js';
2
3
  // Assemble the feishu adapter. The transport is injectable so unit tests can
3
4
  // drive start/dispatch/reply/sendMessage against a fake transport without the
4
5
  // real SDK; production uses the default SDK-backed transport.
@@ -19,7 +20,10 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
19
20
  messageId: normalized.messageId,
20
21
  raw: normalized.raw,
21
22
  reply: async (text) => {
22
- await transport.send(normalized.chatId, cardMessage(text));
23
+ await transport.send(normalized.chatId, renderFeishuMessage(text));
24
+ },
25
+ replyDisplay: async (message) => {
26
+ await transport.send(normalized.chatId, renderFeishuMessage(message));
23
27
  }
24
28
  };
25
29
  await adapterCtx.dispatch(message);
@@ -33,7 +37,10 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
33
37
  await transport.stop();
34
38
  },
35
39
  async sendMessage(target, text) {
36
- await transport.send(target.chatId, cardMessage(text));
40
+ await transport.send(target.chatId, renderFeishuMessage(text));
41
+ },
42
+ async sendDisplayMessage(target, message) {
43
+ await transport.send(target.chatId, renderFeishuMessage(message));
37
44
  }
38
45
  };
39
46
  }