@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,40 +1,27 @@
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
4
  import { loadConfig } from '../config.ts';
6
5
  import type { SandboxConfig } from '../config.ts';
7
6
  import { prepareDockerfile } from '../dockerfile.ts';
8
- import { sandboxImageConfigLabel, sandboxLabel } from '../constants.ts';
7
+ import { sandboxImageRefreshLabel } from '../constants.ts';
9
8
  import { detectEngine, ensureDocker } from '../engine.ts';
10
- import { runEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
9
+ import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shell.ts';
11
10
  import { pruneSandboxDanglingImages } from '../image-prune.ts';
12
- import {
13
- imageSignatureFields,
14
- resolveTools,
15
- toolNpmPackagesArg,
16
- toolShellInstallScriptBase64
17
- } from '../tools.ts';
11
+ import { resolveTools } from '../tools.ts';
18
12
  import type { SandboxTool } from '../tools.ts';
19
- import { toEnginePath } from '../engines/wsl2-paths.ts';
20
- import { resolveBuildUid } from '../engines/native.ts';
13
+ import {
14
+ buildImageSignature,
15
+ buildSandboxImageArgs,
16
+ parseImageLabels,
17
+ parseRefreshTimestamp
18
+ } from '../image-build.ts';
21
19
 
22
20
  const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
23
21
 
24
- type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
25
22
  type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
26
23
  type EngineRunSafeFn = EngineRunFn;
27
24
 
28
- function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
29
- return createHash('sha256')
30
- .update(JSON.stringify({
31
- dockerfile: preparedDockerfile.signature,
32
- tools: imageSignatureFields(tools)
33
- }))
34
- .digest('hex')
35
- .slice(0, 12);
36
- }
37
-
38
25
  export function buildArgs(
39
26
  config: SandboxConfig,
40
27
  tools: SandboxTool[],
@@ -45,49 +32,41 @@ export function buildArgs(
45
32
  runFn = runEngine,
46
33
  runSafeFn = runSafeEngine,
47
34
  env = process.env,
48
- refresh = false
35
+ refresh = false,
36
+ lastRefresh
49
37
  }: {
50
38
  engine?: string;
51
39
  runFn?: EngineRunFn;
52
40
  runSafeFn?: EngineRunSafeFn;
53
41
  env?: NodeJS.ProcessEnv;
54
42
  refresh?: boolean;
43
+ lastRefresh?: number;
55
44
  } = {}
56
45
  ): string[] {
57
- const selectedEngine = engine ?? detectEngine(config);
58
- const { uid: hostUid, gid: hostGid } = resolveBuildUid({
59
- engine: selectedEngine,
46
+ return buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
47
+ engine: engine ?? detectEngine(config),
60
48
  runFn,
61
49
  runSafeFn,
62
- env
50
+ env,
51
+ refresh,
52
+ lastRefresh
63
53
  });
54
+ }
64
55
 
65
- const args = [
66
- 'build',
67
- '-t',
68
- config.imageName,
69
- '--build-arg',
70
- `HOST_UID=${hostUid}`,
71
- '--build-arg',
72
- `HOST_GID=${hostGid}`,
73
- '--build-arg',
74
- `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
75
- '--build-arg',
76
- `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
77
- '--label',
78
- sandboxLabel(config),
79
- '--label',
80
- `${sandboxImageConfigLabel(config)}=${imageSignature}`,
81
- '-f',
82
- toEnginePath(selectedEngine, dockerfilePath),
83
- toEnginePath(selectedEngine, config.repoRoot)
84
- ];
85
-
86
- if (refresh) {
87
- args.splice(1, 0, '--no-cache', '--pull');
56
+ function readExistingLastRefresh(config: SandboxConfig, engine: string): number {
57
+ if (!runOkEngine(engine, 'docker', ['image', 'inspect', config.imageName])) {
58
+ return 0;
88
59
  }
89
60
 
90
- return args;
61
+ const labels = parseImageLabels(runSafeEngine(engine, 'docker', [
62
+ 'image',
63
+ 'inspect',
64
+ '--format',
65
+ '{{ json .Config.Labels }}',
66
+ config.imageName
67
+ ]));
68
+
69
+ return parseRefreshTimestamp(labels[sandboxImageRefreshLabel(config)] ?? '');
91
70
  }
92
71
 
93
72
  export async function rebuild(args: string[]): Promise<void> {
@@ -110,19 +89,24 @@ export async function rebuild(args: string[]): Promise<void> {
110
89
  const config = loadConfig();
111
90
  const tools = resolveTools(config);
112
91
  const preparedDockerfile = prepareDockerfile(config);
113
- const imageSignature = buildSignature(preparedDockerfile, tools);
92
+ const imageSignature = buildImageSignature(preparedDockerfile, tools);
114
93
  const quiet = values.quiet ?? false;
115
94
  const refresh = values.refresh ?? false;
116
95
  const engine = detectEngine(config);
117
96
 
118
97
  await ensureDocker(config, undefined);
98
+ const lastRefresh = refresh ? Date.now() : readExistingLastRefresh(config, engine);
119
99
  p.intro(pc.cyan('Rebuilding sandbox image'));
120
100
 
121
101
  try {
122
102
  if (quiet) {
123
103
  const spinner = p.spinner();
124
104
  spinner.start('Building image...');
125
- runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }), {
105
+ runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
106
+ engine,
107
+ refresh,
108
+ lastRefresh
109
+ }), {
126
110
  cwd: config.repoRoot
127
111
  });
128
112
  spinner.stop(pc.green('Sandbox image rebuilt'));
@@ -131,7 +115,11 @@ export async function rebuild(args: string[]): Promise<void> {
131
115
  runVerboseEngine(
132
116
  engine,
133
117
  'docker',
134
- buildArgs(config, tools, preparedDockerfile.path, imageSignature, { engine, refresh }),
118
+ buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
119
+ engine,
120
+ refresh,
121
+ lastRefresh
122
+ }),
135
123
  { cwd: config.repoRoot }
136
124
  );
137
125
  p.log.success(pc.green('Sandbox image rebuilt'));
@@ -13,6 +13,7 @@ const DEFAULTS = Object.freeze({
13
13
  engine: null,
14
14
  runtimes: ['node22'],
15
15
  tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
16
+ refreshIntervalDays: 7,
16
17
  dockerfile: null,
17
18
  vm: {
18
19
  cpu: null,
@@ -29,6 +30,7 @@ type SandboxConfigInput = {
29
30
  runtimes?: string[];
30
31
  tools?: string[];
31
32
  customTools?: unknown;
33
+ refreshIntervalDays?: unknown;
32
34
  dockerfile?: string | null;
33
35
  vm?: Record<string, unknown>;
34
36
  };
@@ -55,6 +57,7 @@ export type SandboxConfig = {
55
57
  runtimes: string[];
56
58
  tools: string[];
57
59
  customTools: SandboxTool[];
60
+ refreshIntervalDays: number;
58
61
  dockerfile: string | null;
59
62
  vm: SandboxVmConfig;
60
63
  };
@@ -80,11 +83,16 @@ function asPositiveNumberOrNull(value: unknown): number | null {
80
83
  return typeof value === 'number' ? value : null;
81
84
  }
82
85
 
83
- function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes: string[]; tools: string[] } {
86
+ function asNonNegativeIntegerOrDefault(value: unknown, fallback: number): number {
87
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
88
+ }
89
+
90
+ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes: string[]; tools: string[]; refreshIntervalDays: number } {
84
91
  return {
85
92
  engine: DEFAULTS.engine,
86
93
  runtimes: [...DEFAULTS.runtimes],
87
94
  tools: [...DEFAULTS.tools],
95
+ refreshIntervalDays: DEFAULTS.refreshIntervalDays,
88
96
  dockerfile: DEFAULTS.dockerfile,
89
97
  vm: { ...DEFAULTS.vm }
90
98
  };
@@ -160,6 +168,10 @@ export function loadConfig({
160
168
  ? [...sandbox.tools]
161
169
  : defaults.tools,
162
170
  customTools,
171
+ refreshIntervalDays: asNonNegativeIntegerOrDefault(
172
+ sandbox.refreshIntervalDays,
173
+ defaults.refreshIntervalDays
174
+ ),
163
175
  dockerfile,
164
176
  vm: {
165
177
  cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
@@ -107,6 +107,10 @@ export function sandboxImageConfigLabel(config: Pick<SandboxPathConfig, 'project
107
107
  return `${sandboxLabel(config)}.image-config`;
108
108
  }
109
109
 
110
+ export function sandboxImageRefreshLabel(config: Pick<SandboxPathConfig, 'project'>): string {
111
+ return `${sandboxLabel(config)}.last-refresh`;
112
+ }
113
+
110
114
  export function parsePositiveIntegerOption(value: unknown, optionName: string): number | undefined {
111
115
  if (value === undefined || value === null) {
112
116
  return undefined;
@@ -0,0 +1,134 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { detectEngine } from './engine.ts';
3
+ import { resolveBuildUid } from './engines/native.ts';
4
+ import { toEnginePath } from './engines/wsl2-paths.ts';
5
+ import { sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel } from './constants.ts';
6
+ import { runEngine, runSafeEngine } from './shell.ts';
7
+ import {
8
+ imageSignatureFields,
9
+ toolNpmPackagesArg,
10
+ toolShellInstallScriptBase64
11
+ } from './tools.ts';
12
+ import type { SandboxTool } from './tools.ts';
13
+
14
+ type PreparedDockerfileSignature = {
15
+ signature: unknown;
16
+ };
17
+ type BuildImageConfig = {
18
+ project: string;
19
+ imageName: string;
20
+ repoRoot: string;
21
+ engine?: string | null;
22
+ };
23
+ type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
24
+ type EngineRunSafeFn = EngineRunFn;
25
+
26
+ export function buildImageSignature(preparedDockerfile: PreparedDockerfileSignature, tools: SandboxTool[]): string {
27
+ return createHash('sha256')
28
+ .update(JSON.stringify({
29
+ dockerfile: preparedDockerfile.signature,
30
+ tools: imageSignatureFields(tools)
31
+ }))
32
+ .digest('hex')
33
+ .slice(0, 12);
34
+ }
35
+
36
+ export function buildSandboxImageArgs(
37
+ config: BuildImageConfig,
38
+ tools: SandboxTool[],
39
+ dockerfilePath: string,
40
+ imageSignature: string,
41
+ {
42
+ engine,
43
+ runFn = runEngine,
44
+ runSafeFn = runSafeEngine,
45
+ env = process.env,
46
+ refresh = false,
47
+ lastRefresh
48
+ }: {
49
+ engine?: string;
50
+ runFn?: EngineRunFn;
51
+ runSafeFn?: EngineRunSafeFn;
52
+ env?: NodeJS.ProcessEnv;
53
+ refresh?: boolean;
54
+ lastRefresh?: number;
55
+ } = {}
56
+ ): string[] {
57
+ const selectedEngine = engine ?? detectEngine({ engine: config.engine });
58
+ const { uid: hostUid, gid: hostGid } = resolveBuildUid({
59
+ engine: selectedEngine,
60
+ runFn,
61
+ runSafeFn,
62
+ env
63
+ });
64
+
65
+ const args = [
66
+ 'build',
67
+ '-t',
68
+ config.imageName,
69
+ '--build-arg',
70
+ `HOST_UID=${hostUid}`,
71
+ '--build-arg',
72
+ `HOST_GID=${hostGid}`,
73
+ '--build-arg',
74
+ `AI_TOOL_PACKAGES=${toolNpmPackagesArg(tools)}`,
75
+ '--build-arg',
76
+ `AI_TOOLS_SHELL_INSTALL_B64=${toolShellInstallScriptBase64(tools)}`,
77
+ '--label',
78
+ sandboxLabel(config),
79
+ '--label',
80
+ `${sandboxImageConfigLabel(config)}=${imageSignature}`
81
+ ];
82
+
83
+ if (lastRefresh !== undefined) {
84
+ args.push('--label', `${sandboxImageRefreshLabel(config)}=${lastRefresh}`);
85
+ }
86
+
87
+ args.push(
88
+ '-f',
89
+ toEnginePath(selectedEngine, dockerfilePath),
90
+ toEnginePath(selectedEngine, config.repoRoot)
91
+ );
92
+
93
+ if (refresh) {
94
+ args.splice(1, 0, '--no-cache', '--pull');
95
+ }
96
+
97
+ return args;
98
+ }
99
+
100
+ export function parseRefreshTimestamp(value: string): number {
101
+ const parsed = Number(value);
102
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
103
+ }
104
+
105
+ export function parseImageLabels(raw: string): Record<string, string> {
106
+ try {
107
+ const parsed = JSON.parse(raw) as unknown;
108
+ if (!parsed || typeof parsed !== 'object') {
109
+ return {};
110
+ }
111
+ return Object.fromEntries(
112
+ Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
113
+ );
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ export function isRefreshDisabled(env: NodeJS.ProcessEnv, noRefreshFlag: boolean): boolean {
120
+ if (noRefreshFlag) {
121
+ return true;
122
+ }
123
+
124
+ const value = env.AI_SANDBOX_NO_REFRESH?.trim().toLowerCase();
125
+ return value === '1' || value === 'true' || value === 'yes';
126
+ }
127
+
128
+ export function isRefreshDue(lastRefresh: number, now: number, intervalDays: number): boolean {
129
+ if (intervalDays <= 0 || lastRefresh > now) {
130
+ return false;
131
+ }
132
+
133
+ return now - lastRefresh >= intervalDays * 24 * 60 * 60 * 1000;
134
+ }
@@ -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
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import type { ExecFileSyncOptions, StdioOptions, SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
5
+ import { getAdapter } from './engines/index.ts';
5
6
 
6
7
  const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
7
8
 
@@ -52,7 +53,7 @@ function resolveCommand(cmd: string): string {
52
53
  return cmd;
53
54
  }
54
55
 
55
- function commandOptions<T extends CommandOptions>(cmd: string, opts: T): T | (T & { shell: true }) {
56
+ function commandOptions<T extends object>(cmd: string, opts: T): T | (T & { shell: true }) {
56
57
  if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
57
58
  return { ...opts, shell: true };
58
59
  }
@@ -143,6 +144,13 @@ export function commandForEngine(engine: string, cmd: string, args: string[] = [
143
144
  return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
144
145
  }
145
146
 
147
+ if (cmd === 'docker') {
148
+ const context = getAdapter(engine).dockerContext;
149
+ if (context) {
150
+ return { cmd, args: ['--context', context, ...args] };
151
+ }
152
+ }
153
+
146
154
  return { cmd, args };
147
155
  }
148
156
 
@@ -153,7 +161,8 @@ export function runEngine(engine: string, cmd: string, args: string[], opts: Com
153
161
 
154
162
  export function execEngine(engine: string, cmd: string, args: string[], opts: ExecFileSyncOptions = {}) {
155
163
  const command = commandForEngine(engine, cmd, args);
156
- return execFileSync(command.cmd, command.args, opts);
164
+ const resolved = resolveCommand(command.cmd);
165
+ return execFileSync(resolved, command.args, commandOptions(resolved, opts));
157
166
  }
158
167
 
159
168
  export function runOkEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): boolean {
@@ -22,9 +22,9 @@ export type SandboxTool = {
22
22
  // When set, containerMount is mounted as an in-container tmpfs (RAM) instead
23
23
  // of bind-mounting the host config dir, keeping high-churn tool logs off the
24
24
  // host disk. `seed` lists the host-dir entries (relative to the tool's config
25
- // dir) to bind back over the tmpfs so seeded config stays visible — it is an
26
- // explicit allowlist so runtime files (e.g. logs_2.sqlite, sessions) left in
27
- // the host dir are NOT re-mounted, which would defeat the tmpfs.
25
+ // dir) to copy into the tmpfs when the container starts. It is an explicit
26
+ // allowlist so runtime files (e.g. logs_2.sqlite, sessions) left in the host
27
+ // dir stay out of the container and seeded config cannot write back.
28
28
  tmpfs?: { size?: string; seed?: string[] };
29
29
  };
30
30
 
@@ -80,8 +80,8 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
80
80
  // codex churns ~/.codex/logs_2.sqlite heavily (upstream openai/codex#24275);
81
81
  // a bind-mount would write-amplify onto the host SSD via virtiofs. Mount the
82
82
  // codex home as tmpfs so those logs stay in RAM and die with the container.
83
- // Only the seeded config (config.toml, model-catalogs) is bound back over
84
- // the tmpfs; runtime files like logs_2.sqlite must stay in RAM.
83
+ // Seeded config (config.toml, model-catalogs) is copied into the tmpfs at
84
+ // startup; runtime files like logs_2.sqlite stay in RAM.
85
85
  tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'] },
86
86
  hostLiveMounts: [
87
87
  { hostPath: hostJoin(home, '.codex', 'auth.json'), containerSubpath: 'auth.json' }
@@ -1,4 +1,5 @@
1
1
  import type { ServerConfig } from '../config.ts';
2
+ import type { OutboundMessage } from '../display.ts';
2
3
  import type { Logger } from '../logger.ts';
3
4
 
4
5
  // Adapter contract for agent-infra-server.
@@ -19,6 +20,7 @@ export type InboundMessage = {
19
20
  messageId: string;
20
21
  raw: unknown;
21
22
  reply: (text: string) => Promise<void>;
23
+ replyDisplay?: (message: OutboundMessage) => Promise<void>;
22
24
  };
23
25
 
24
26
  // Runtime context passed to every adapter's start(). dispatch() is registered
@@ -36,6 +38,7 @@ export type Adapter = {
36
38
  start: (ctx: AdapterCtx) => Promise<void>;
37
39
  stop: () => Promise<void>;
38
40
  sendMessage: (target: { chatId: string }, text: string) => Promise<void>;
41
+ sendDisplayMessage?: (target: { chatId: string }, message: OutboundMessage) => Promise<void>;
39
42
  };
40
43
 
41
44
  // The shape of an adapter module's default export (a factory).
@@ -1,5 +1,7 @@
1
1
  import type { Adapter, AdapterCtx, AdapterFactory, InboundMessage } from '../_contract.ts';
2
- import { cardMessage, createFeishuTransport, normalizeMessage } from './transport.ts';
2
+ import type { OutboundMessage } from '../../display.ts';
3
+ import { renderFeishuMessage } from './renderer.ts';
4
+ import { createFeishuTransport, normalizeMessage } from './transport.ts';
3
5
  import type { FeishuTransport } from './transport.ts';
4
6
 
5
7
  // Assemble the feishu adapter. The transport is injectable so unit tests can
@@ -26,7 +28,10 @@ export function createFeishuAdapter(
26
28
  messageId: normalized.messageId,
27
29
  raw: normalized.raw,
28
30
  reply: async (text) => {
29
- await transport.send(normalized.chatId, cardMessage(text));
31
+ await transport.send(normalized.chatId, renderFeishuMessage(text));
32
+ },
33
+ replyDisplay: async (message: OutboundMessage) => {
34
+ await transport.send(normalized.chatId, renderFeishuMessage(message));
30
35
  }
31
36
  };
32
37
  await adapterCtx.dispatch(message);
@@ -39,7 +44,10 @@ export function createFeishuAdapter(
39
44
  await transport.stop();
40
45
  },
41
46
  async sendMessage(target, text) {
42
- await transport.send(target.chatId, cardMessage(text));
47
+ await transport.send(target.chatId, renderFeishuMessage(text));
48
+ },
49
+ async sendDisplayMessage(target, message) {
50
+ await transport.send(target.chatId, renderFeishuMessage(message));
43
51
  }
44
52
  };
45
53
  }
@@ -0,0 +1,99 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import { normalizeOutbound, outboundToText, type DisplayMessage, type OutboundMessage } from '../../display.ts';
3
+ import type { FeishuMessagePayload } from './transport.ts';
4
+
5
+ const FEISHU_LARK_MD_MAX_LENGTH = 20_000;
6
+
7
+ type FeishuElement =
8
+ | { tag: 'div'; text: { tag: 'lark_md'; content: string } }
9
+ | { tag: 'div'; fields: Array<{ is_short: boolean; text: { tag: 'lark_md'; content: string } }> };
10
+
11
+ function templateForTone(tone: 'info' | 'success' | 'warning' | 'danger' | 'running'): string {
12
+ if (tone === 'success') return 'green';
13
+ if (tone === 'warning') return 'yellow';
14
+ if (tone === 'danger') return 'red';
15
+ return 'blue';
16
+ }
17
+
18
+ function templateForResult(exitCode: number | null): string {
19
+ return exitCode === 0 ? 'green' : 'red';
20
+ }
21
+
22
+ export function cleanFeishuText(text: string): string {
23
+ return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
24
+ }
25
+
26
+ function clampLarkMarkdown(text: string): string {
27
+ const clean = cleanFeishuText(text);
28
+ if (clean.length <= FEISHU_LARK_MD_MAX_LENGTH) return clean;
29
+ return `${clean.slice(0, FEISHU_LARK_MD_MAX_LENGTH - 18)}\n\n...(truncated)`;
30
+ }
31
+
32
+ function div(content: string): FeishuElement {
33
+ return { tag: 'div', text: { tag: 'lark_md', content: clampLarkMarkdown(content) } };
34
+ }
35
+
36
+ function fields(rows: [string, string][]): FeishuElement {
37
+ return {
38
+ tag: 'div',
39
+ fields: rows.map(([label, value]) => ({
40
+ is_short: true,
41
+ text: { tag: 'lark_md', content: clampLarkMarkdown(`**${label}**\n${value}`) }
42
+ }))
43
+ };
44
+ }
45
+
46
+ function card(title: string, template: string, elements: FeishuElement[]): FeishuMessagePayload {
47
+ return {
48
+ msg_type: 'interactive',
49
+ content: JSON.stringify({
50
+ config: { wide_screen_mode: true },
51
+ header: { title: { tag: 'plain_text', content: cleanFeishuText(title) }, template },
52
+ elements
53
+ })
54
+ };
55
+ }
56
+
57
+ function tableMarkdown(message: Extract<DisplayMessage, { kind: 'table' }>): string {
58
+ return outboundToText(message);
59
+ }
60
+
61
+ function renderStatus(message: Extract<DisplayMessage, { kind: 'status-card' }>): FeishuMessagePayload {
62
+ const elements: FeishuElement[] = [];
63
+ if (message.fields && message.fields.length > 0) elements.push(fields(message.fields));
64
+ if (message.body) elements.push(div(message.body));
65
+ if (elements.length === 0) elements.push(div(message.title));
66
+ return card(message.title, templateForTone(message.tone), elements);
67
+ }
68
+
69
+ function renderStream(message: Extract<DisplayMessage, { kind: 'stream-event' }>): FeishuMessagePayload {
70
+ const template = message.phase === 'finished' ? templateForResult(message.exitCode ?? null) : 'blue';
71
+ return card(message.title, template, [div(outboundToText(message))]);
72
+ }
73
+
74
+ function renderCommandResult(message: Extract<DisplayMessage, { kind: 'command-result' }>): FeishuMessagePayload {
75
+ return card(message.title, templateForResult(message.exitCode), [div(outboundToText(message))]);
76
+ }
77
+
78
+ function renderUnknownDisplayKind(message: never): FeishuMessagePayload {
79
+ return card('agent-infra', 'blue', [div(`[unknown display kind: ${String((message as { kind?: unknown }).kind)}]`)]);
80
+ }
81
+
82
+ export function renderFeishuMessage(message: OutboundMessage): FeishuMessagePayload {
83
+ const normalized = normalizeOutbound(message);
84
+ switch (normalized.kind) {
85
+ case 'text':
86
+ return card('agent-infra', 'blue', [div(normalized.text)]);
87
+ case 'markdown':
88
+ return card('agent-infra', 'blue', [div(normalized.markdown)]);
89
+ case 'table':
90
+ return card(normalized.title ?? 'agent-infra', 'blue', [div(tableMarkdown(normalized))]);
91
+ case 'status-card':
92
+ return renderStatus(normalized);
93
+ case 'stream-event':
94
+ return renderStream(normalized);
95
+ case 'command-result':
96
+ return renderCommandResult(normalized);
97
+ }
98
+ return renderUnknownDisplayKind(normalized);
99
+ }
@@ -1,5 +1,4 @@
1
1
  import * as lark from '@larksuiteoapi/node-sdk';
2
- import { stripVTControlCharacters } from 'node:util';
3
2
 
4
3
  // Transport layer for the feishu adapter. All @larksuiteoapi/node-sdk surface is
5
4
  // confined here so the adapter body (index.ts) depends only on this narrow
@@ -11,10 +10,10 @@ export type FeishuTransport = {
11
10
  // each inbound im.message.receive_v1.
12
11
  start: (onMessage: (raw: unknown) => Promise<void>) => Promise<void>;
13
12
  stop: () => Promise<void>;
14
- send: (chatId: string, message: FeishuOutgoingMessage) => Promise<void>;
13
+ send: (chatId: string, message: FeishuMessagePayload) => Promise<void>;
15
14
  };
16
15
 
17
- export type FeishuOutgoingMessage = { kind: 'interactive'; title: string; text: string };
16
+ export type FeishuMessagePayload = { msg_type: 'interactive'; content: string };
18
17
 
19
18
  type FeishuCreateData = {
20
19
  receive_id: string;
@@ -78,23 +77,11 @@ function resolveDomain(value: unknown): number {
78
77
  return value === 'lark' || value === 'Lark' ? lark.Domain.Lark : lark.Domain.Feishu;
79
78
  }
80
79
 
81
- export function cleanFeishuText(text: string): string {
82
- return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
83
- }
84
-
85
- export function cardMessage(text: string): FeishuOutgoingMessage {
86
- return { kind: 'interactive', title: 'agent-infra', text: cleanFeishuText(text) };
87
- }
88
-
89
- export function toFeishuCreateData(chatId: string, message: FeishuOutgoingMessage): FeishuCreateData {
80
+ export function toFeishuCreateData(chatId: string, message: FeishuMessagePayload): FeishuCreateData {
90
81
  return {
91
82
  receive_id: chatId,
92
- msg_type: 'interactive',
93
- content: JSON.stringify({
94
- config: { wide_screen_mode: true },
95
- header: { title: { tag: 'plain_text', content: message.title }, template: 'blue' },
96
- elements: [{ tag: 'div', text: { tag: 'lark_md', content: message.text } }]
97
- })
83
+ msg_type: message.msg_type,
84
+ content: message.content
98
85
  };
99
86
  }
100
87