@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
@@ -0,0 +1,144 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ const PROBE_TIMEOUT_MS = 2_000;
6
+ const READ_IMAGE_TIMEOUT_MS = 5_000;
7
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
+ const POWERSHELL_ARGS = ['-NoProfile', '-NonInteractive', '-STA', '-Command'];
9
+ export function createWin32ClipboardAdapter({ execFn = execFileSync, mkdtempFn = fs.mkdtempSync, readFileFn = fs.readFileSync, rmFn = fs.rmSync, existsFn = fs.existsSync } = {}) {
10
+ return {
11
+ available() {
12
+ try {
13
+ execFn('powershell.exe', [...POWERSHELL_ARGS, clipboardProbeScript()], {
14
+ encoding: 'utf8',
15
+ timeout: PROBE_TIMEOUT_MS
16
+ });
17
+ return { ok: true };
18
+ }
19
+ catch {
20
+ return {
21
+ ok: false,
22
+ reason: 'Windows PowerShell Get-Clipboard is unavailable; install or enable powershell.exe to use image paste'
23
+ };
24
+ }
25
+ },
26
+ readImagePng() {
27
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
28
+ const outputPath = path.join(tmpDir, 'clipboard.png');
29
+ try {
30
+ execFn('powershell.exe', [...POWERSHELL_ARGS, pngWriteScript(outputPath)], {
31
+ encoding: 'utf8',
32
+ timeout: READ_IMAGE_TIMEOUT_MS
33
+ });
34
+ const png = Buffer.from(readFileFn(outputPath));
35
+ return isPng(png) ? png : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ finally {
41
+ rmFn(tmpDir, { recursive: true, force: true });
42
+ }
43
+ },
44
+ readImageFromPath(imagePath) {
45
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
46
+ const outputPath = path.join(tmpDir, 'clipboard.png');
47
+ try {
48
+ execFn('powershell.exe', [...POWERSHELL_ARGS, pngFromPathScript(imagePath, outputPath)], {
49
+ encoding: 'utf8',
50
+ timeout: READ_IMAGE_TIMEOUT_MS
51
+ });
52
+ const png = Buffer.from(readFileFn(outputPath));
53
+ return isPng(png) ? png : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ finally {
59
+ rmFn(tmpDir, { recursive: true, force: true });
60
+ }
61
+ },
62
+ readImageFromText(text) {
63
+ const trimmed = text.trim();
64
+ if (!isWindowsImagePath(trimmed)) {
65
+ return null;
66
+ }
67
+ if (!existsFn(trimmed)) {
68
+ return null;
69
+ }
70
+ return this.readImageFromPath(trimmed);
71
+ }
72
+ };
73
+ }
74
+ function clipboardProbeScript() {
75
+ return 'if ($null -eq (Get-Command Get-Clipboard -ErrorAction SilentlyContinue)) { exit 1 }';
76
+ }
77
+ // Embed a filesystem path in a PowerShell single-quoted string literal.
78
+ // Single-quoted strings in PowerShell are verbatim (no variable expansion,
79
+ // no escape sequences). The only special character is a single quote itself,
80
+ // which is escaped by doubling it.
81
+ function psLiteral(value) {
82
+ return `'${value.replace(/'/g, "''")}'`;
83
+ }
84
+ function pngWriteScript(outputPath) {
85
+ const psOutputPath = psLiteral(outputPath);
86
+ return [
87
+ '$ErrorActionPreference = "Stop"',
88
+ `$outputPath = ${psOutputPath}`,
89
+ '$image = $null',
90
+ 'try {',
91
+ ' Add-Type -AssemblyName System.Drawing',
92
+ ' $image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue',
93
+ ' if ($null -ne $image) {',
94
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
95
+ ' return',
96
+ ' }',
97
+ ' $files = Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue',
98
+ ' if ($null -ne $files) {',
99
+ ' $allowed = @(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".ico")',
100
+ ' foreach ($file in $files) {',
101
+ ' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }',
102
+ ' $ext = [System.IO.Path]::GetExtension($file)',
103
+ ' if ($allowed -notcontains $ext) { continue }',
104
+ ' try {',
105
+ ' $image = [System.Drawing.Image]::FromFile($file)',
106
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
107
+ ' return',
108
+ ' } catch {}',
109
+ ' }',
110
+ ' }',
111
+ ' throw "clipboard has no image"',
112
+ '} finally {',
113
+ ' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
114
+ '}'
115
+ ].join('\n');
116
+ }
117
+ function pngFromPathScript(imagePath, outputPath) {
118
+ const psImagePath = psLiteral(imagePath);
119
+ const psOutputPath = psLiteral(outputPath);
120
+ return [
121
+ '$ErrorActionPreference = "Stop"',
122
+ `$imagePath = ${psImagePath}`,
123
+ `$outputPath = ${psOutputPath}`,
124
+ '$image = $null',
125
+ 'try {',
126
+ ' Add-Type -AssemblyName System.Drawing',
127
+ ' if (-not (Test-Path -LiteralPath $imagePath -PathType Leaf)) { throw "file not found: $imagePath" }',
128
+ ' $image = [System.Drawing.Image]::FromFile($imagePath)',
129
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
130
+ '} finally {',
131
+ ' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
132
+ '}'
133
+ ].join('\n');
134
+ }
135
+ const IMAGE_EXTENSIONS_RE = /\.(png|jpg|jpeg|gif|bmp|webp|tiff?|ico)$/i;
136
+ function isWindowsImagePath(text) {
137
+ // Must be a Windows absolute path: drive letter + colon + backslash/slash
138
+ // followed by a path ending with a known image extension.
139
+ return /^[A-Za-z]:[\\/].+$/.test(text) && IMAGE_EXTENSIONS_RE.test(text);
140
+ }
141
+ function isPng(buffer) {
142
+ return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
143
+ }
144
+ //# sourceMappingURL=win32.js.map
@@ -7,21 +7,21 @@ import { parseArgs } from 'node:util';
7
7
  import * as p from '@clack/prompts';
8
8
  import pc from 'picocolors';
9
9
  import * as toml from 'smol-toml';
10
- import { loadConfig } from "../config.js";
11
- import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from "../constants.js";
12
- import { prepareDockerfile } from "../dockerfile.js";
13
- import { detectEngine, ensureDocker } from "../engine.js";
14
- import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from "../shell.js";
15
- import { resolveTaskBranch } from "../task-resolver.js";
16
- import { imageSignatureFields, resolveTools, toolConfigDirCandidates, toolNpmPackagesArg, toolShellInstallScriptBase64 } from "../tools.js";
17
- import { hostJoin, toEnginePath, volumeArg } from "../engines/wsl2-paths.js";
18
- import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from "../clipboard/paths.js";
19
- import { validateSelinuxDisableEnv } from "../engines/selinux.js";
20
- import { resolveBuildUid } from "../engines/native.js";
21
- import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
22
- import { ensureSandboxDiscoveryReadmes } from "../readme-scaffold.js";
23
- import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
24
- import { detectHostTimezone } from "../host-timezone.js";
10
+ import { loadConfig } from '../config.js';
11
+ import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
12
+ import { prepareDockerfile } from '../dockerfile.js';
13
+ import { detectEngine, ensureDocker } from '../engine.js';
14
+ import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from '../shell.js';
15
+ import { resolveTaskBranch } from '../task-resolver.js';
16
+ import { resolveTools, toolConfigDirCandidates } from '../tools.js';
17
+ import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
18
+ import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
19
+ import { validateSelinuxDisableEnv } from '../engines/selinux.js';
20
+ import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
21
+ import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.js';
22
+ import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
23
+ import { detectHostTimezone } from '../host-timezone.js';
24
+ import { buildImageSignature, buildSandboxImageArgs, isRefreshDisabled, isRefreshDue, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
25
25
  const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
26
26
  const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
27
27
  const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
@@ -47,22 +47,13 @@ alias gy='gemini --yolo; tput ed'
47
47
  `;
48
48
  const CONTAINER_HOME = '/home/devuser';
49
49
  const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
50
- const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>]
50
+ const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh]
51
51
 
52
52
  Host aliases:
53
53
  ${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
54
54
  as ${CONTAINER_HOME}/.bash_aliases inside the sandbox container (the host
55
55
  shell-config directory is bind-mounted at ${CONTAINER_SHELL_CONFIG_MOUNT} and
56
56
  symlinked into $HOME).`;
57
- function buildSignature(preparedDockerfile, tools) {
58
- return createHash('sha256')
59
- .update(JSON.stringify({
60
- dockerfile: preparedDockerfile.signature,
61
- tools: imageSignatureFields(tools)
62
- }))
63
- .digest('hex')
64
- .slice(0, 12);
65
- }
66
57
  function resolveToolDirs(config, tools, branch) {
67
58
  return tools.map((tool) => {
68
59
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -218,12 +209,14 @@ export function writeSanitizedGitconfig({ home, hostConfigDir, stripGpg, repoRoo
218
209
  // Keep in sync with the symlink block in lib/sandbox/runtimes/ai-tools.dockerfile.
219
210
  const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
220
211
  export function ensureShellConfigSymlinks(engine, container, execFn = execEngine) {
221
- // Idempotent symlink setup. Runs against a started container so it also
222
- // covers custom Dockerfiles that don't bake the symlinks into the image.
223
- const script = SHELL_CONFIG_SYMLINKS
224
- .map((file) => `ln -sf .host-shell-config/${file} ${CONTAINER_HOME}/${file}`)
225
- .join(' && ');
226
- execFn(engine, 'docker', ['exec', container, 'bash', '-lc', script], { stdio: 'ignore' });
212
+ // Idempotent symlink setup. Avoid a shell command here because Windows .cmd
213
+ // engine shims would interpret metacharacters in a `bash -lc` script before
214
+ // forwarding it to Docker.
215
+ for (const file of SHELL_CONFIG_SYMLINKS) {
216
+ execFn(engine, 'docker', [
217
+ 'exec', container, 'ln', '-sf', `.host-shell-config/${file}`, `${CONTAINER_HOME}/${file}`
218
+ ], { stdio: 'ignore' });
219
+ }
227
220
  }
228
221
  export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
229
222
  const hostDir = hostShellConfigDir(home, project, branch);
@@ -433,9 +426,12 @@ function formatEnvFileEntry(key, value) {
433
426
  return `${key}=${value}`;
434
427
  }
435
428
  export function buildContainerEnvFile(resolvedTools, engine, runSafeEngineFn = runSafeEngine, options = {}) {
436
- const { mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir() } = options;
429
+ const { mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir(), runSafeFn = runSafe } = options;
437
430
  const entries = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
438
- const ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
431
+ let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
432
+ if (!ghToken && engine === 'wsl2') {
433
+ ghToken = runSafeFn('gh', ['auth', 'token']);
434
+ }
439
435
  if (ghToken) {
440
436
  entries.push(['GH_TOKEN', ghToken]);
441
437
  }
@@ -908,34 +904,25 @@ export function buildTmpfsRunArgs(containerMount, tmpfs) {
908
904
  const size = tmpfs.size ?? '512m';
909
905
  return ['--tmpfs', `${containerMount}:rw,size=${size}`];
910
906
  }
911
- export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env } = {}) {
907
+ export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh } = {}) {
912
908
  const selectedEngine = engine ?? detectEngine({ engine: config.engine });
913
- const { uid: hostUid, gid: hostGid } = resolveBuildUid({
909
+ runVerboseFn(selectedEngine, 'docker', buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
914
910
  engine: selectedEngine,
915
911
  runFn,
916
912
  runSafeFn,
917
- env
918
- });
919
- runVerboseFn(selectedEngine, 'docker', [
920
- 'build',
921
- '-t',
922
- config.imageName,
923
- '--build-arg',
924
- `HOST_UID=${hostUid}`,
925
- '--build-arg',
926
- `HOST_GID=${hostGid}`,
927
- '--build-arg',
928
- `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
929
- '--build-arg',
930
- `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
931
- '--label',
932
- sandboxLabel(config),
933
- '--label',
934
- `${sandboxImageConfigLabel(config)}=${imageSignature}`,
935
- '-f',
936
- toEnginePath(selectedEngine, dockerfilePath),
937
- toEnginePath(selectedEngine, config.repoRoot)
938
- ], { cwd: config.repoRoot });
913
+ env,
914
+ refresh,
915
+ lastRefresh
916
+ }), { cwd: config.repoRoot });
917
+ }
918
+ function readImageLabels(config, engine) {
919
+ return parseImageLabels(runSafeEngine(engine, 'docker', [
920
+ 'image',
921
+ 'inspect',
922
+ '--format',
923
+ '{{ json .Config.Labels }}',
924
+ config.imageName
925
+ ]));
939
926
  }
940
927
  export async function create(args) {
941
928
  const { values, positionals } = parseArgs({
@@ -945,6 +932,7 @@ export async function create(args) {
945
932
  options: {
946
933
  cpu: { type: 'string' },
947
934
  memory: { type: 'string' },
935
+ 'no-refresh': { type: 'boolean' },
948
936
  help: { type: 'boolean', short: 'h' }
949
937
  }
950
938
  });
@@ -986,7 +974,7 @@ export async function create(args) {
986
974
  const shareBranch = shareBranchDir(effectiveConfig, branch);
987
975
  const preparedDockerfile = prepareDockerfile(effectiveConfig);
988
976
  const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
989
- const expectedImageSignature = buildSignature(preparedDockerfile, tools);
977
+ const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
990
978
  const engine = detectEngine(effectiveConfig);
991
979
  p.intro(pc.cyan('AI Sandbox'));
992
980
  p.log.info(`Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`);
@@ -1002,20 +990,41 @@ export async function create(args) {
1002
990
  });
1003
991
  p.log.success('Docker is ready');
1004
992
  const imageExists = runOkEngine(engine, 'docker', ['image', 'inspect', effectiveConfig.imageName]);
1005
- const currentImageSignature = imageExists
1006
- ? runSafeEngine(engine, 'docker', [
1007
- 'image',
1008
- 'inspect',
1009
- '--format',
1010
- `{{ index .Config.Labels "${sandboxImageConfigLabel(effectiveConfig)}" }}`,
1011
- effectiveConfig.imageName
1012
- ])
1013
- : '';
1014
- const needsImageBuild = !imageExists || currentImageSignature !== expectedImageSignature;
993
+ const imageLabels = imageExists ? readImageLabels(effectiveConfig, engine) : {};
994
+ const currentImageSignature = imageLabels[sandboxImageConfigLabel(effectiveConfig)] ?? '';
995
+ const currentLastRefresh = parseRefreshTimestamp(imageLabels[sandboxImageRefreshLabel(effectiveConfig)] ?? '');
996
+ const signatureStale = !imageExists || currentImageSignature !== expectedImageSignature;
997
+ const now = Date.now();
998
+ const refreshDue = imageExists
999
+ && !signatureStale
1000
+ && !isRefreshDisabled(process.env, values['no-refresh'] ?? false)
1001
+ && isRefreshDue(currentLastRefresh, now, effectiveConfig.refreshIntervalDays);
1002
+ const needsImageBuild = signatureStale || refreshDue;
1015
1003
  if (needsImageBuild) {
1016
- p.log.step(imageExists ? 'Rebuilding stale image...' : 'Building image for first use...');
1017
- buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, { engine });
1018
- p.log.success(imageExists ? 'Image rebuilt' : 'Image built');
1004
+ const buildRefresh = !imageExists || refreshDue;
1005
+ const buildLastRefresh = buildRefresh ? now : currentLastRefresh || 0;
1006
+ p.log.step(refreshDue
1007
+ ? 'Refreshing stale image...'
1008
+ : imageExists
1009
+ ? 'Rebuilding stale image...'
1010
+ : 'Building image for first use...');
1011
+ try {
1012
+ buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, { engine, refresh: buildRefresh, lastRefresh: buildLastRefresh });
1013
+ p.log.success(refreshDue
1014
+ ? 'Image refreshed'
1015
+ : imageExists
1016
+ ? 'Image rebuilt'
1017
+ : 'Image built');
1018
+ }
1019
+ catch (error) {
1020
+ if (refreshDue && !signatureStale && imageExists) {
1021
+ p.log.warn('Scheduled sandbox image refresh failed; continuing with the existing image. ' +
1022
+ commandErrorMessage(error));
1023
+ }
1024
+ else {
1025
+ throw error;
1026
+ }
1027
+ }
1019
1028
  }
1020
1029
  else {
1021
1030
  p.log.step(`Using existing image ${effectiveConfig.imageName}`);
@@ -1141,6 +1150,7 @@ export async function create(args) {
1141
1150
  : null;
1142
1151
  const envFile = buildContainerEnvFile(effectiveResolvedTools, engine);
1143
1152
  let hostShellConfig;
1153
+ let tmpfsSeedPlan = [];
1144
1154
  try {
1145
1155
  const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
1146
1156
  if (claudeCodeEntry) {
@@ -1177,21 +1187,23 @@ export async function create(args) {
1177
1187
  '-v',
1178
1188
  volumeArg(engine, hostPath, containerPath, ':ro')
1179
1189
  ]);
1180
- // A tmpfs containerMount starts empty, so the config seeded into the
1181
- // host dir before launch would be invisible in-container. Bind only
1182
- // the explicitly declared seed entries (config.toml, model-catalogs)
1183
- // back over the tmpfs as nested mounts the same proven mechanism as
1184
- // hostLiveMounts/auth.json, established at `docker run` time (no
1185
- // post-start `docker cp`, which can land under a freshly-mounted
1186
- // tmpfs instead of inside it). The allowlist is deliberate: any
1187
- // runtime files left in the host dir (e.g. a stale logs_2.sqlite or
1188
- // sessions/ from a previous bind-mount era) must NOT be re-mounted,
1189
- // or the high-churn writes would land on the host SSD again.
1190
- const tmpfsSeedVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry) => {
1190
+ // Mount each declared seed read-only at an isolated staging path.
1191
+ // After docker run mounts the empty tmpfs, the container's default
1192
+ // user copies these entries into it so the runtime targets are normal,
1193
+ // writable files rather than nested mount points. The allowlist keeps
1194
+ // stale runtime files (logs_2.sqlite, sessions, etc.) on the host from
1195
+ // being exposed or written through.
1196
+ tmpfsSeedPlan = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.tmpfs?.seed ?? []).flatMap((entry, index) => {
1191
1197
  const hostPath = path.join(dir, entry);
1192
- return fs.existsSync(hostPath)
1193
- ? ['-v', volumeArg(engine, hostPath, path.posix.join(tool.containerMount, entry))]
1194
- : [];
1198
+ if (!fs.existsSync(hostPath)) {
1199
+ return [];
1200
+ }
1201
+ const stagingPath = path.posix.join('/run/agent-infra/tmpfs-seeds', tool.id, String(index));
1202
+ return [{
1203
+ stagingPath,
1204
+ targetPath: path.posix.join(tool.containerMount, entry),
1205
+ volumeArgs: ['-v', volumeArg(engine, hostPath, stagingPath, ':ro')]
1206
+ }];
1195
1207
  }));
1196
1208
  const liveMountVolumes = effectiveResolvedTools.flatMap(({ tool }) => (tool.hostLiveMounts ?? [])
1197
1209
  .filter(({ hostPath }) => fs.existsSync(hostPath))
@@ -1231,12 +1243,10 @@ export async function create(args) {
1231
1243
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1232
1244
  '-v',
1233
1245
  volumeArg(engine, path.join(effectiveConfig.repoRoot, '.git'), `${toEnginePath(engine, effectiveConfig.repoRoot)}/.git`),
1234
- '-v',
1235
- volumeArg(engine, hostJoin(effectiveConfig.home, '.ssh'), '/home/devuser/.ssh', ':ro'),
1236
1246
  ...dotfilesMount,
1237
1247
  ...toolVolumes,
1238
1248
  ...tmpfsArgs,
1239
- ...tmpfsSeedVolumes,
1249
+ ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1240
1250
  ...liveMountVolumes,
1241
1251
  ...shellConfigVolumes,
1242
1252
  ...envFile.dockerArgs,
@@ -1249,6 +1259,14 @@ export async function create(args) {
1249
1259
  finally {
1250
1260
  envFile.cleanup();
1251
1261
  }
1262
+ for (const { stagingPath, targetPath } of tmpfsSeedPlan) {
1263
+ runEngineTaskCommand(engine, 'docker', [
1264
+ 'exec', container, 'mkdir', '-p', path.posix.dirname(targetPath)
1265
+ ]);
1266
+ runEngineTaskCommand(engine, 'docker', [
1267
+ 'exec', container, 'cp', '-R', '--', stagingPath, targetPath
1268
+ ]);
1269
+ }
1252
1270
  // Belt-and-suspenders: re-create the four shell-config symlinks at
1253
1271
  // runtime so users with a custom `sandbox.dockerfile` (which won't
1254
1272
  // include the ai-tools.dockerfile symlink fragment) still get
@@ -1,12 +1,12 @@
1
- import { loadConfig } from "../config.js";
2
- import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
3
- import { detectEngine } from "../engine.js";
4
- import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from "../credentials.js";
5
- import { runInteractiveEngine } from "../shell.js";
6
- import { dotfilesCacheDir, materializeDotfiles } from "../dotfiles.js";
7
- import { runInteractiveWithClipboardBridge } from "../clipboard/bridge.js";
8
- import { detectHostTimezone } from "../host-timezone.js";
9
- 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 { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
5
+ import { runInteractiveEngine } from '../shell.js';
6
+ import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
7
+ import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.js';
8
+ import { detectHostTimezone } from '../host-timezone.js';
9
+ import { fetchSandboxRows, resolveBranchArg, selectSandboxContainer, startSandboxContainer } from './list-running.js';
10
10
  const USAGE = `Usage: ai sandbox exec <branch | TASK-id | N | '#N'> [cmd...]
11
11
 
12
12
  N (bare) and '#N' both reference the same active task short id from
@@ -1,8 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { runSafeEngine, runVerboseEngine } from "../shell.js";
5
- import { resolveTaskBranch } from "../task-resolver.js";
4
+ import { runSafeEngine, runVerboseEngine } from '../shell.js';
5
+ import { resolveTaskBranch } from '../task-resolver.js';
6
6
  export function containerListFormat() {
7
7
  return '{{.Names}}\t{{.Status}}\t{{.Labels}}';
8
8
  }
@@ -1,12 +1,12 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
- import { loadConfig } from "../config.js";
4
- import { sandboxBranchLabel, sandboxLabel } from "../constants.js";
5
- import { detectEngine } from "../engine.js";
6
- import { formatTable } from "../../table.js";
7
- import { lookupShortIdByBranch } from "../../task/short-id.js";
8
- import { fetchSandboxRows } from "./list-running.js";
9
- export { containerListFormat, parseLabels } from "./list-running.js";
3
+ import { loadConfig } from '../config.js';
4
+ import { sandboxBranchLabel, sandboxLabel } from '../constants.js';
5
+ import { detectEngine } from '../engine.js';
6
+ import { formatTable } from '../../table.js';
7
+ import { lookupShortIdByBranch } from '../../task/short-id.js';
8
+ import { fetchSandboxRows } from './list-running.js';
9
+ export { containerListFormat, parseLabels } from './list-running.js';
10
10
  const USAGE = `Usage: ai sandbox ls
11
11
 
12
12
  Lists all containers for the current project. The '#' column is a
@@ -3,14 +3,14 @@ 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 { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from "../constants.js";
8
- import { detectEngine } from "../engine.js";
9
- import { hostJoin } from "../engines/wsl2-paths.js";
10
- import { removeManagedDir, removeWorktreeDir } from "../managed-fs.js";
11
- import { parseLabels } from "./ls.js";
12
- import { runEngine, runSafe } from "../shell.js";
13
- import { resolveTools } from "../tools.js";
6
+ import { loadConfig } from '../config.js';
7
+ import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
8
+ import { detectEngine } from '../engine.js';
9
+ import { hostJoin } from '../engines/wsl2-paths.js';
10
+ import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
+ import { parseLabels } from './ls.js';
12
+ import { runEngine, runSafe } from '../shell.js';
13
+ import { resolveTools } from '../tools.js';
14
14
  const USAGE = `Usage: ai sandbox prune [--dry-run]`;
15
15
  function listChildDirs(base) {
16
16
  if (!fs.existsSync(base)) {