@fitlab-ai/agent-infra 0.5.10 → 0.6.0

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 (82) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/{cli.js → cli.ts} +21 -17
  4. package/dist/bin/cli.js +116 -0
  5. package/dist/lib/defaults.json +61 -0
  6. package/dist/lib/init.js +238 -0
  7. package/dist/lib/log.js +18 -0
  8. package/dist/lib/merge.js +747 -0
  9. package/dist/lib/paths.js +18 -0
  10. package/dist/lib/prompt.js +85 -0
  11. package/dist/lib/render.js +139 -0
  12. package/dist/lib/sandbox/commands/create.js +1173 -0
  13. package/dist/lib/sandbox/commands/enter.js +98 -0
  14. package/dist/lib/sandbox/commands/ls.js +93 -0
  15. package/dist/lib/sandbox/commands/rebuild.js +101 -0
  16. package/dist/lib/sandbox/commands/refresh.js +85 -0
  17. package/dist/lib/sandbox/commands/rm.js +226 -0
  18. package/dist/lib/sandbox/commands/vm.js +144 -0
  19. package/dist/lib/sandbox/config.js +85 -0
  20. package/dist/lib/sandbox/constants.js +104 -0
  21. package/dist/lib/sandbox/credentials.js +437 -0
  22. package/dist/lib/sandbox/dockerfile.js +76 -0
  23. package/dist/lib/sandbox/dotfiles.js +170 -0
  24. package/dist/lib/sandbox/engine.js +155 -0
  25. package/dist/lib/sandbox/engines/colima.js +64 -0
  26. package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
  27. package/dist/lib/sandbox/engines/index.js +25 -0
  28. package/dist/lib/sandbox/engines/native.js +96 -0
  29. package/dist/lib/sandbox/engines/orbstack.js +63 -0
  30. package/dist/lib/sandbox/engines/selinux.js +48 -0
  31. package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
  32. package/dist/lib/sandbox/engines/wsl2.js +57 -0
  33. package/dist/lib/sandbox/index.js +70 -0
  34. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
  35. package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
  36. package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
  37. package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
  38. package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
  39. package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
  40. package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
  41. package/dist/lib/sandbox/shell.js +148 -0
  42. package/dist/lib/sandbox/task-resolver.js +35 -0
  43. package/dist/lib/sandbox/tools.js +115 -0
  44. package/dist/lib/update.js +186 -0
  45. package/dist/lib/version.js +5 -0
  46. package/dist/package.json +5 -0
  47. package/lib/{init.js → init.ts} +48 -18
  48. package/lib/{log.js → log.ts} +4 -4
  49. package/lib/{merge.js → merge.ts} +129 -63
  50. package/lib/paths.ts +18 -0
  51. package/lib/{prompt.js → prompt.ts} +12 -12
  52. package/lib/{render.js → render.ts} +30 -17
  53. package/lib/sandbox/commands/{create.js → create.ts} +224 -118
  54. package/lib/sandbox/commands/{enter.js → enter.ts} +17 -14
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +10 -10
  56. package/lib/sandbox/commands/{rebuild.js → rebuild.ts} +38 -21
  57. package/lib/sandbox/commands/{refresh.js → refresh.ts} +16 -7
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +15 -13
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +14 -11
  60. package/lib/sandbox/{config.js → config.ts} +55 -10
  61. package/lib/sandbox/{constants.js → constants.ts} +30 -18
  62. package/lib/sandbox/{credentials.js → credentials.ts} +160 -46
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/{dotfiles.js → dotfiles.ts} +66 -19
  65. package/lib/sandbox/{engine.js → engine.ts} +57 -25
  66. package/lib/sandbox/engines/{colima.js → colima.ts} +9 -7
  67. package/lib/sandbox/engines/{docker-desktop.js → docker-desktop.ts} +5 -3
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/{native.js → native.ts} +25 -6
  70. package/lib/sandbox/engines/{orbstack.js → orbstack.ts} +7 -5
  71. package/lib/sandbox/engines/{selinux.js → selinux.ts} +11 -5
  72. package/lib/sandbox/engines/{wsl2-paths.js → wsl2-paths.ts} +15 -9
  73. package/lib/sandbox/engines/{wsl2.js → wsl2.ts} +9 -7
  74. package/lib/sandbox/{index.js → index.ts} +8 -8
  75. package/lib/sandbox/{shell.js → shell.ts} +30 -17
  76. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  77. package/lib/sandbox/{tools.js → tools.ts} +30 -26
  78. package/lib/{update.js → update.ts} +33 -10
  79. package/package.json +17 -9
  80. package/lib/paths.js +0 -9
  81. package/lib/sandbox/engines/index.js +0 -27
  82. /package/lib/{version.js → version.ts} +0 -0
@@ -3,11 +3,12 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { execFileSync } from 'node:child_process';
6
+ import type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
6
7
  import { parseArgs } from 'node:util';
7
8
  import * as p from '@clack/prompts';
8
9
  import pc from 'picocolors';
9
10
  import * as toml from 'smol-toml';
10
- import { loadConfig } from '../config.js';
11
+ import { loadConfig } from '../config.ts';
11
12
  import {
12
13
  assertValidBranchName,
13
14
  containerName,
@@ -20,9 +21,9 @@ import {
20
21
  shareBranchDir,
21
22
  shareCommonDir,
22
23
  worktreeDirCandidates
23
- } from '../constants.js';
24
- import { prepareDockerfile } from '../dockerfile.js';
25
- import { detectEngine, ensureDocker } from '../engine.js';
24
+ } from '../constants.ts';
25
+ import { prepareDockerfile } from '../dockerfile.ts';
26
+ import { detectEngine, ensureDocker } from '../engine.ts';
26
27
  import {
27
28
  commandForEngine,
28
29
  execEngine,
@@ -33,18 +34,19 @@ import {
33
34
  runSafe,
34
35
  runSafeEngine,
35
36
  runVerboseEngine
36
- } from '../shell.js';
37
- import { resolveTaskBranch } from '../task-resolver.js';
38
- import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from '../tools.js';
39
- import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
40
- import { validateSelinuxDisableEnv } from '../engines/selinux.js';
41
- import { resolveBuildUid } from '../engines/native.js';
42
- import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
37
+ } from '../shell.ts';
38
+ import { resolveTaskBranch } from '../task-resolver.ts';
39
+ import { resolveTools, toolConfigDirCandidates, toolNpmPackagesArg } from '../tools.ts';
40
+ import type { SandboxTool } from '../tools.ts';
41
+ import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
42
+ import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
43
+ import { resolveBuildUid } from '../engines/native.ts';
44
+ import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
43
45
  import {
44
46
  assertClaudeCredentialsAvailable,
45
47
  redactCommandError,
46
48
  validateClaudeCredentialsEnvOverride
47
- } from '../credentials.js';
49
+ } from '../credentials.ts';
48
50
 
49
51
  const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
50
52
  const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
@@ -79,7 +81,32 @@ Host aliases:
79
81
  shell-config directory is bind-mounted at ${CONTAINER_SHELL_CONFIG_MOUNT} and
80
82
  symlinked into $HOME).`;
81
83
 
82
- function buildSignature(preparedDockerfile, tools) {
84
+ type SandboxCreateConfig = ReturnType<typeof loadConfig>;
85
+ type PreparedDockerfile = ReturnType<typeof prepareDockerfile>;
86
+ type ResolvedTool = { tool: SandboxTool; dir: string };
87
+ type RuntimeCheck = { name: string; cmd: string[] };
88
+ type JsonObject = Record<string, unknown>;
89
+ type GpgCache = { pub: Buffer; sec: Buffer } | null;
90
+ type ExecSyncOptions = ExecFileSyncOptions & {
91
+ input?: Buffer | string;
92
+ env?: NodeJS.ProcessEnv;
93
+ stdio?: StdioOptions;
94
+ encoding?: BufferEncoding;
95
+ };
96
+ type ExecSyncFn = (cmd: string, args: string[], options?: ExecSyncOptions) => Buffer | string;
97
+ type EngineExecFn = (engine: string, cmd: string, args: string[], opts?: ExecFileSyncOptions) => Buffer | string;
98
+ type EngineRunFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => string;
99
+ type EngineRunSafeFn = EngineRunFn;
100
+ type EngineRunVerboseFn = (engine: string, cmd: string, args: string[], opts?: { cwd?: string }) => void;
101
+ type DirectRunFn = (cmd: string, args: string[], opts?: { cwd?: string }) => string;
102
+ type DirectRunSafeFn = DirectRunFn;
103
+ type DirectRunVerboseFn = (cmd: string, args: string[], opts?: { cwd?: string }) => void;
104
+ type HostShellConfig = {
105
+ hostDir: string;
106
+ mounts: Array<{ hostPath: string; containerPath: string }>;
107
+ };
108
+
109
+ function buildSignature(preparedDockerfile: PreparedDockerfile, tools: SandboxTool[]): string {
83
110
  return createHash('sha256')
84
111
  .update(JSON.stringify({
85
112
  dockerfile: preparedDockerfile.signature,
@@ -89,22 +116,22 @@ function buildSignature(preparedDockerfile, tools) {
89
116
  .slice(0, 12);
90
117
  }
91
118
 
92
- function resolveToolDirs(config, tools, branch) {
119
+ function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
93
120
  return tools.map((tool) => {
94
121
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
95
122
  return {
96
123
  tool,
97
- dir: candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0]
124
+ dir: candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0] ?? ''
98
125
  };
99
126
  });
100
127
  }
101
128
 
102
- export function hostShellConfigDir(home, project, branch) {
129
+ export function hostShellConfigDir(home: string, project: string, branch: string): string {
103
130
  return hostJoin(home, '.agent-infra', 'config', project, sanitizeBranchName(branch));
104
131
  }
105
132
 
106
- function runtimeChecks(runtimes) {
107
- const checks = [];
133
+ function runtimeChecks(runtimes: string[]): RuntimeCheck[] {
134
+ const checks: RuntimeCheck[] = [];
108
135
  if (runtimes.some((runtime) => runtime.startsWith('node'))) {
109
136
  checks.push({ name: 'Node.js', cmd: ['node', '--version'] });
110
137
  }
@@ -118,25 +145,25 @@ function runtimeChecks(runtimes) {
118
145
  return checks;
119
146
  }
120
147
 
121
- export function detectGpgConfig(gitconfig) {
148
+ export function detectGpgConfig(gitconfig: string): boolean {
122
149
  return /\bgpgsign\s*=\s*true\b/i.test(gitconfig) || /^\s*\[gpg(?:\s|"|\])/im.test(gitconfig);
123
150
  }
124
151
 
125
- function appendSafeDirectories(lines, repoRoot) {
152
+ function appendSafeDirectories(lines: string[], repoRoot: string): string[] {
126
153
  if (!repoRoot) {
127
154
  return lines;
128
155
  }
129
156
 
130
157
  const requiredDirectories = ['/workspace', repoRoot];
131
- const existingDirectories = new Set();
158
+ const existingDirectories = new Set<string>();
132
159
  let firstSafeSectionIndex = -1;
133
160
  let inSafeSection = false;
134
161
 
135
162
  for (let index = 0; index < lines.length; index += 1) {
136
- const line = lines[index];
163
+ const line = lines[index] ?? '';
137
164
  const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
138
165
  if (sectionMatch) {
139
- inSafeSection = sectionMatch[1].trim().toLowerCase() === 'safe';
166
+ inSafeSection = (sectionMatch[1] ?? '').trim().toLowerCase() === 'safe';
140
167
  if (inSafeSection && firstSafeSectionIndex === -1) {
141
168
  firstSafeSectionIndex = index;
142
169
  }
@@ -149,7 +176,7 @@ function appendSafeDirectories(lines, repoRoot) {
149
176
 
150
177
  const directoryMatch = line.match(/^\s*directory\s*=\s*(.+?)\s*$/i);
151
178
  if (directoryMatch) {
152
- existingDirectories.add(directoryMatch[1].trim());
179
+ existingDirectories.add((directoryMatch[1] ?? '').trim());
153
180
  }
154
181
  }
155
182
 
@@ -170,7 +197,7 @@ function appendSafeDirectories(lines, repoRoot) {
170
197
  const updatedLines = [...lines];
171
198
  let insertIndex = updatedLines.length;
172
199
  for (let index = firstSafeSectionIndex + 1; index < updatedLines.length; index += 1) {
173
- if (/^\s*\[([^\]]+)\]\s*$/.test(updatedLines[index])) {
200
+ if (/^\s*\[([^\]]+)\]\s*$/.test(updatedLines[index] ?? '')) {
174
201
  insertIndex = index;
175
202
  break;
176
203
  }
@@ -184,12 +211,16 @@ function appendSafeDirectories(lines, repoRoot) {
184
211
  return updatedLines;
185
212
  }
186
213
 
187
- function normalizeContainerHomeSeparators(content) {
214
+ function normalizeContainerHomeSeparators(content: string): string {
188
215
  const containerHomePattern = new RegExp(`${escapeRegExp(CONTAINER_HOME)}\\S*`, 'g');
189
216
  return content.replace(containerHomePattern, (value) => value.replaceAll('\\', '/'));
190
217
  }
191
218
 
192
- export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot = '' } = {}) {
219
+ export function sanitizeGitConfig(
220
+ gitconfig: string,
221
+ home: string,
222
+ { stripGpg = false, repoRoot = '' }: { stripGpg?: boolean; repoRoot?: string } = {}
223
+ ): string {
193
224
  const posixHome = home.replaceAll('\\', '/');
194
225
  const normalizedGitconfig = gitconfig
195
226
  .replaceAll(home, CONTAINER_HOME)
@@ -206,8 +237,8 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
206
237
  for (const line of lines) {
207
238
  const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
208
239
  if (sectionMatch) {
209
- const sectionName = sectionMatch[1].trim();
210
- currentSection = (sectionName.match(/^([^\s"]+)/)?.[1] ?? '').toLowerCase();
240
+ const sectionName = (sectionMatch[1] ?? '').trim();
241
+ currentSection = ((sectionName.match(/^([^\s"]+)/)?.[1]) ?? '').toLowerCase();
211
242
  inGpgSection = /^gpg(?:\s+"[^"]+")?$/i.test(sectionName);
212
243
  if (stripGpg && inGpgSection) {
213
244
  continue;
@@ -241,11 +272,21 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
241
272
  return appendSafeDirectories(sanitized, repoRoot).join('\n');
242
273
  }
243
274
 
244
- export function hostHasGpgKeys(home, execFn = execFileSync) {
275
+ export function hostHasGpgKeys(home: string, execFn: ExecSyncFn = execFileSync): boolean {
245
276
  return currentKeyringFingerprint(home, execFn) !== null;
246
277
  }
247
278
 
248
- export function writeSanitizedGitconfig({ home, hostConfigDir, stripGpg, repoRoot }) {
279
+ export function writeSanitizedGitconfig({
280
+ home,
281
+ hostConfigDir,
282
+ stripGpg,
283
+ repoRoot
284
+ }: {
285
+ home: string;
286
+ hostConfigDir: string;
287
+ stripGpg: boolean;
288
+ repoRoot: string;
289
+ }): string {
249
290
  const gitconfigPath = hostJoin(home, '.gitconfig');
250
291
  // Always emit a sanitized .gitconfig, even when the host has none. The
251
292
  // container ~/.gitconfig is a symlink into the bound shell-config directory;
@@ -266,7 +307,7 @@ export function writeSanitizedGitconfig({ home, hostConfigDir, stripGpg, repoRoo
266
307
  // Keep in sync with the symlink block in lib/sandbox/runtimes/ai-tools.dockerfile.
267
308
  const SHELL_CONFIG_SYMLINKS = ['.gitconfig', '.gitignore_global', '.stCommitMsg', '.bash_aliases'];
268
309
 
269
- export function ensureShellConfigSymlinks(engine, container, execFn = execEngine) {
310
+ export function ensureShellConfigSymlinks(engine: string, container: string, execFn: EngineExecFn = execEngine): void {
270
311
  // Idempotent symlink setup. Runs against a started container so it also
271
312
  // covers custom Dockerfiles that don't bake the symlinks into the image.
272
313
  const script = SHELL_CONFIG_SYMLINKS
@@ -275,7 +316,17 @@ export function ensureShellConfigSymlinks(engine, container, execFn = execEngine
275
316
  execFn(engine, 'docker', ['exec', container, 'bash', '-lc', script], { stdio: 'ignore' });
276
317
  }
277
318
 
278
- export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
319
+ export function prepareHostShellConfig({
320
+ home,
321
+ project,
322
+ branch,
323
+ repoRoot
324
+ }: {
325
+ home: string;
326
+ project: string;
327
+ branch: string;
328
+ repoRoot: string;
329
+ }): HostShellConfig {
279
330
  const hostDir = hostShellConfigDir(home, project, branch);
280
331
  fs.rmSync(hostDir, { recursive: true, force: true });
281
332
  fs.mkdirSync(hostDir, { recursive: true });
@@ -308,11 +359,11 @@ export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
308
359
  return { hostDir, mounts };
309
360
  }
310
361
 
311
- function gpgCacheDir(home, project) {
362
+ function gpgCacheDir(home: string, project: string): string {
312
363
  return hostJoin(home, '.agent-infra', 'gpg-cache', project);
313
364
  }
314
365
 
315
- function normalizeSigningKey(signingKey) {
366
+ function normalizeSigningKey(signingKey: unknown): string | null {
316
367
  if (typeof signingKey !== 'string') {
317
368
  return null;
318
369
  }
@@ -321,7 +372,7 @@ function normalizeSigningKey(signingKey) {
321
372
  return trimmed.length > 0 ? trimmed : null;
322
373
  }
323
374
 
324
- function normalizeWorktreePath(worktreePath) {
375
+ function normalizeWorktreePath(worktreePath: string): string {
325
376
  if (!worktreePath) {
326
377
  return '';
327
378
  }
@@ -333,7 +384,15 @@ function normalizeWorktreePath(worktreePath) {
333
384
  }
334
385
  }
335
386
 
336
- export function getGitSigningKey({ home, repoPath = null, execFn = execFileSync } = {}) {
387
+ export function getGitSigningKey({
388
+ home,
389
+ repoPath = null,
390
+ execFn = execFileSync
391
+ }: {
392
+ home?: string;
393
+ repoPath?: string | null;
394
+ execFn?: ExecSyncFn;
395
+ } = {}): string | null {
337
396
  if (!home) {
338
397
  return null;
339
398
  }
@@ -348,13 +407,13 @@ export function getGitSigningKey({ home, repoPath = null, execFn = execFileSync
348
407
  env: { ...process.env, HOME: home },
349
408
  stdio: ['ignore', 'pipe', 'pipe']
350
409
  });
351
- return normalizeSigningKey(output);
410
+ return normalizeSigningKey(output.toString());
352
411
  } catch {
353
412
  return null;
354
413
  }
355
414
  }
356
415
 
357
- export function currentKeyringFingerprint(home, execFn = execFileSync) {
416
+ export function currentKeyringFingerprint(home: string, execFn: ExecSyncFn = execFileSync): string | null {
358
417
  const hostEnv = { ...process.env, HOME: home };
359
418
  try {
360
419
  const keyring = execFn('gpg', ['--list-secret-keys', '--with-colons'], {
@@ -362,23 +421,29 @@ export function currentKeyringFingerprint(home, execFn = execFileSync) {
362
421
  env: hostEnv,
363
422
  stdio: ['ignore', 'pipe', 'pipe']
364
423
  });
365
- if (!keyring || keyring.trim().length === 0) {
424
+ const keyringText = keyring.toString();
425
+ if (!keyringText || keyringText.trim().length === 0) {
366
426
  return null;
367
427
  }
368
- return createHash('sha256').update(keyring).digest('hex');
428
+ return createHash('sha256').update(keyringText).digest('hex');
369
429
  } catch {
370
430
  return null;
371
431
  }
372
432
  }
373
433
 
374
- export function readGpgCache(home, project, execFn = execFileSync, signingKey = null) {
434
+ export function readGpgCache(
435
+ home: string,
436
+ project: string,
437
+ execFn: ExecSyncFn = execFileSync,
438
+ signingKey: string | null = null
439
+ ): GpgCache {
375
440
  const cacheDir = gpgCacheDir(home, project);
376
441
  const pubPath = path.join(cacheDir, 'public.asc');
377
442
  const secPath = path.join(cacheDir, 'secret.asc');
378
443
  const statePath = path.join(cacheDir, 'state.json');
379
444
 
380
445
  try {
381
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
446
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8')) as { fingerprint?: unknown; signingKey?: unknown };
382
447
  if (typeof state?.fingerprint !== 'string' || state.fingerprint.length === 0) {
383
448
  return null;
384
449
  }
@@ -403,7 +468,14 @@ export function readGpgCache(home, project, execFn = execFileSync, signingKey =
403
468
  }
404
469
  }
405
470
 
406
- export function writeGpgCache(home, project, pub, sec, fingerprint, signingKey = null) {
471
+ export function writeGpgCache(
472
+ home: string,
473
+ project: string,
474
+ pub: Buffer | string,
475
+ sec: Buffer | string,
476
+ fingerprint: string | null,
477
+ signingKey: string | null = null
478
+ ): boolean {
407
479
  if (!fingerprint) {
408
480
  return false;
409
481
  }
@@ -414,7 +486,7 @@ export function writeGpgCache(home, project, pub, sec, fingerprint, signingKey =
414
486
  const statePath = path.join(cacheDir, 'state.json');
415
487
 
416
488
  try {
417
- const state = { fingerprint };
489
+ const state: { fingerprint: string; signingKey?: string } = { fingerprint };
418
490
  const normalizedSigningKey = normalizeSigningKey(signingKey);
419
491
  if (normalizedSigningKey) {
420
492
  state.signingKey = normalizedSigningKey;
@@ -439,13 +511,19 @@ export function writeGpgCache(home, project, pub, sec, fingerprint, signingKey =
439
511
  }
440
512
 
441
513
  export function syncGpgKeys(
442
- container,
443
- home,
444
- project,
445
- execFn = execFileSync,
446
- runSafeFn = runSafe,
447
- options = {}
448
- ) {
514
+ container: string,
515
+ home: string,
516
+ project: string,
517
+ execFn: ExecSyncFn = execFileSync,
518
+ runSafeFn: DirectRunSafeFn = runSafe,
519
+ options: {
520
+ cachedOverride?: GpgCache;
521
+ repoPath?: string | null;
522
+ signingKey?: string | null;
523
+ dockerExecFn?: ExecSyncFn;
524
+ dockerRunSafeFn?: DirectRunSafeFn;
525
+ } = {}
526
+ ): boolean {
449
527
  const {
450
528
  cachedOverride = null,
451
529
  repoPath = null,
@@ -477,18 +555,18 @@ export function syncGpgKeys(
477
555
  ? ['--export-secret-keys', signingKey]
478
556
  : ['--export-secret-keys'];
479
557
 
480
- pubKeys = execFn('gpg', exportArgs, {
558
+ pubKeys = Buffer.from(execFn('gpg', exportArgs, {
481
559
  env: hostEnv,
482
560
  stdio: ['ignore', 'pipe', 'pipe']
483
- });
561
+ }));
484
562
  if (!pubKeys || pubKeys.length === 0) {
485
563
  return false;
486
564
  }
487
565
 
488
- secKeys = execFn('gpg', exportSecretArgs, {
566
+ secKeys = Buffer.from(execFn('gpg', exportSecretArgs, {
489
567
  env: hostEnv,
490
568
  stdio: ['ignore', 'pipe', 'pipe']
491
- });
569
+ }));
492
570
  if (!secKeys || secKeys.length === 0) {
493
571
  return false;
494
572
  }
@@ -505,11 +583,11 @@ export function syncGpgKeys(
505
583
  }
506
584
 
507
585
  dockerExecFn('docker', ['exec', '-i', container, 'gpg', '--import'], {
508
- input: pubKeys,
586
+ input: pubKeys ?? undefined,
509
587
  stdio: ['pipe', 'pipe', 'pipe']
510
588
  });
511
589
  dockerExecFn('docker', ['exec', '-i', container, 'gpg', '--batch', '--import'], {
512
- input: secKeys,
590
+ input: secKeys ?? undefined,
513
591
  stdio: ['pipe', 'pipe', 'pipe']
514
592
  });
515
593
 
@@ -520,7 +598,7 @@ export function syncGpgKeys(
520
598
  // Docker `--env-file` parsing has no quoting/escaping support and treats
521
599
  // leading '#' as a comment. Newlines split entries, so reject them outright.
522
600
  // Other shell metacharacters are safe because the values are not expanded.
523
- function formatEnvFileEntry(key, value) {
601
+ function formatEnvFileEntry(key: string, value: string): string {
524
602
  if (String(key).includes('\n') || String(value).includes('\n')) {
525
603
  throw new Error(`Container environment variable ${key} must not contain newlines`);
526
604
  }
@@ -528,11 +606,17 @@ function formatEnvFileEntry(key, value) {
528
606
  }
529
607
 
530
608
  export function buildContainerEnvFile(
531
- resolvedTools,
532
- engine,
533
- runSafeEngineFn = runSafeEngine,
534
- options = {}
535
- ) {
609
+ resolvedTools: ResolvedTool[],
610
+ engine: string,
611
+ runSafeEngineFn: EngineRunSafeFn = runSafeEngine,
612
+ options: {
613
+ mkdtempFn?: typeof fs.mkdtempSync;
614
+ writeFileFn?: typeof fs.writeFileSync;
615
+ chmodFn?: typeof fs.chmodSync;
616
+ rmFn?: typeof fs.rmSync;
617
+ tmpDir?: string;
618
+ } = {}
619
+ ): { dockerArgs: string[]; cleanup: () => void } {
536
620
  const {
537
621
  mkdtempFn = fs.mkdtempSync,
538
622
  writeFileFn = fs.writeFileSync,
@@ -541,7 +625,7 @@ export function buildContainerEnvFile(
541
625
  tmpDir = os.tmpdir()
542
626
  } = options;
543
627
 
544
- const entries = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
628
+ const entries: Array<[string, string]> = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
545
629
  const ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
546
630
  if (ghToken) {
547
631
  entries.push(['GH_TOKEN', ghToken]);
@@ -579,7 +663,11 @@ export function buildContainerEnvFile(
579
663
  }
580
664
  }
581
665
 
582
- export function buildDotfilesVolumeArgs(engine, snapshotDir, existsFn = fs.existsSync) {
666
+ export function buildDotfilesVolumeArgs(
667
+ engine: string,
668
+ snapshotDir: string | null | undefined,
669
+ existsFn: typeof fs.existsSync = fs.existsSync
670
+ ): string[] {
583
671
  if (!snapshotDir || !existsFn(snapshotDir)) {
584
672
  return [];
585
673
  }
@@ -587,10 +675,10 @@ export function buildDotfilesVolumeArgs(engine, snapshotDir, existsFn = fs.exist
587
675
  }
588
676
 
589
677
  export function assertBranchAvailable(
590
- repoRoot,
591
- branch,
592
- { allowedWorktrees = [], runFn = runSafe } = {}
593
- ) {
678
+ repoRoot: string,
679
+ branch: string,
680
+ { allowedWorktrees = [], runFn = runSafe }: { allowedWorktrees?: string[]; runFn?: DirectRunSafeFn } = {}
681
+ ): void {
594
682
  const normalizedAllowedWorktrees = new Set(allowedWorktrees.map((worktree) => normalizeWorktreePath(worktree)));
595
683
  const output = runFn('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain']);
596
684
  if (!output) {
@@ -620,25 +708,29 @@ export function assertBranchAvailable(
620
708
  }
621
709
  }
622
710
 
623
- function readHostJsonSafe(filePath) {
711
+ function readHostJsonSafe(filePath: string): JsonObject | null {
624
712
  if (!filePath || !fs.existsSync(filePath)) {
625
713
  return null;
626
714
  }
627
715
 
628
716
  try {
629
- const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
630
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
717
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
718
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as JsonObject : null;
631
719
  } catch {
632
720
  return null;
633
721
  }
634
722
  }
635
723
 
636
- export function ensureClaudeOnboarding(toolDir, hostHomeDir) {
724
+ export function ensureClaudeOnboarding(toolDir: string, hostHomeDir?: string): void {
637
725
  const claudeJsonPath = path.join(toolDir, '.claude.json');
638
- let data = {};
726
+ let data: JsonObject & {
727
+ hasCompletedOnboarding?: boolean;
728
+ projects?: Record<string, { hasTrustDialogAccepted?: boolean }>;
729
+ model?: string;
730
+ } = {};
639
731
  if (fs.existsSync(claudeJsonPath)) {
640
732
  try {
641
- data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
733
+ data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')) as typeof data;
642
734
  } catch {
643
735
  // malformed JSON, start fresh
644
736
  }
@@ -697,12 +789,12 @@ export function ensureClaudeOnboarding(toolDir, hostHomeDir) {
697
789
  }
698
790
  }
699
791
 
700
- export function ensureClaudeSettings(toolDir, hostHomeDir) {
792
+ export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): void {
701
793
  const settingsPath = path.join(toolDir, 'settings.json');
702
- let data = {};
794
+ let data: JsonObject & { skipDangerousModePermissionPrompt?: boolean; effortLevel?: string } = {};
703
795
  if (fs.existsSync(settingsPath)) {
704
796
  try {
705
- data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
797
+ data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as typeof data;
706
798
  } catch {
707
799
  // malformed JSON, start fresh
708
800
  }
@@ -729,7 +821,7 @@ export function ensureClaudeSettings(toolDir, hostHomeDir) {
729
821
  }
730
822
  }
731
823
 
732
- export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
824
+ export function ensureCodexModelInheritance(toolDir: string, hostHomeDir?: string): void {
733
825
  if (!hostHomeDir) {
734
826
  return;
735
827
  }
@@ -739,19 +831,19 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
739
831
  return;
740
832
  }
741
833
 
742
- let hostParsed;
834
+ let hostParsed: JsonObject;
743
835
  try {
744
- hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8'));
836
+ hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8')) as JsonObject;
745
837
  } catch {
746
838
  return;
747
839
  }
748
840
 
749
841
  const sandboxConfigPath = path.join(toolDir, 'config.toml');
750
842
  // This rewrites sandbox-side TOML and drops comments; the host config stays untouched.
751
- let sandboxParsed = {};
843
+ let sandboxParsed: JsonObject = {};
752
844
  if (fs.existsSync(sandboxConfigPath)) {
753
845
  try {
754
- sandboxParsed = toml.parse(fs.readFileSync(sandboxConfigPath, 'utf8'));
846
+ sandboxParsed = toml.parse(fs.readFileSync(sandboxConfigPath, 'utf8')) as JsonObject;
755
847
  } catch {
756
848
  return;
757
849
  }
@@ -775,7 +867,7 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir) {
775
867
  }
776
868
  }
777
869
 
778
- export function ensureCodexWorkspaceTrust(toolDir) {
870
+ export function ensureCodexWorkspaceTrust(toolDir: string): void {
779
871
  const configPath = path.join(toolDir, 'config.toml');
780
872
  let content = '';
781
873
  if (fs.existsSync(configPath)) {
@@ -787,7 +879,7 @@ export function ensureCodexWorkspaceTrust(toolDir) {
787
879
  }
788
880
  }
789
881
 
790
- export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
882
+ export function ensureOpenCodeModelInheritance(toolDir: string, hostHomeDir?: string): void {
791
883
  if (!hostHomeDir) {
792
884
  return;
793
885
  }
@@ -799,7 +891,7 @@ export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
799
891
  }
800
892
 
801
893
  const sandboxConfigPath = path.join(toolDir, 'opencode.json');
802
- let sandboxJson = {};
894
+ let sandboxJson: JsonObject = {};
803
895
  if (fs.existsSync(sandboxConfigPath)) {
804
896
  const existing = readHostJsonSafe(sandboxConfigPath);
805
897
  if (!existing) {
@@ -825,12 +917,12 @@ export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
825
917
  }
826
918
  }
827
919
 
828
- export function ensureGeminiWorkspaceTrust(toolDir) {
920
+ export function ensureGeminiWorkspaceTrust(toolDir: string): void {
829
921
  const trustPath = path.join(toolDir, 'trustedFolders.json');
830
- let data = {};
922
+ let data: Record<string, string> = {};
831
923
  if (fs.existsSync(trustPath)) {
832
924
  try {
833
- data = JSON.parse(fs.readFileSync(trustPath, 'utf8'));
925
+ data = JSON.parse(fs.readFileSync(trustPath, 'utf8')) as Record<string, string>;
834
926
  } catch {
835
927
  // malformed JSON, start fresh
836
928
  }
@@ -841,15 +933,15 @@ export function ensureGeminiWorkspaceTrust(toolDir) {
841
933
  }
842
934
  }
843
935
 
844
- export function sandboxAliasesPath(home) {
936
+ export function sandboxAliasesPath(home: string): string {
845
937
  return hostJoin(home, '.agent-infra', 'aliases', 'sandbox.sh');
846
938
  }
847
939
 
848
- function escapeRegExp(value) {
940
+ function escapeRegExp(value: string): string {
849
941
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
850
942
  }
851
943
 
852
- function stripManagedSandboxAliasBlocks(content) {
944
+ function stripManagedSandboxAliasBlocks(content: string): string {
853
945
  const blockPattern = new RegExp(
854
946
  `${escapeRegExp(SANDBOX_ALIAS_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(SANDBOX_ALIAS_BLOCK_END)}\\n?`,
855
947
  'g'
@@ -857,7 +949,7 @@ function stripManagedSandboxAliasBlocks(content) {
857
949
  return content.replace(blockPattern, '').trimEnd();
858
950
  }
859
951
 
860
- function isLegacyManagedSandboxAliasFile(content) {
952
+ function isLegacyManagedSandboxAliasFile(content: string): boolean {
861
953
  const lines = content
862
954
  .split(/\r?\n/)
863
955
  .map((line) => line.trim())
@@ -871,7 +963,7 @@ function isLegacyManagedSandboxAliasFile(content) {
871
963
  return lines.every((line) => aliasPattern.test(line));
872
964
  }
873
965
 
874
- export function ensureSandboxAliasesFile(home) {
966
+ export function ensureSandboxAliasesFile(home: string): { created: boolean; path: string } {
875
967
  const aliasesPath = sandboxAliasesPath(home);
876
968
  const managedBlock = `${SANDBOX_ALIAS_BLOCK_BEGIN}\n${DEFAULT_SANDBOX_ALIASES}${SANDBOX_ALIAS_BLOCK_END}\n`;
877
969
  fs.mkdirSync(path.dirname(aliasesPath), { recursive: true });
@@ -896,12 +988,19 @@ export function ensureSandboxAliasesFile(home) {
896
988
  return { created, path: aliasesPath };
897
989
  }
898
990
 
899
- export function commandErrorMessage(error) {
900
- const stderr = error?.stderr?.toString().trim();
901
- return redactCommandError(stderr || error?.message || 'Command failed');
991
+ export function commandErrorMessage(error: unknown): string {
992
+ const stderr = typeof error === 'object' && error !== null && 'stderr' in error
993
+ ? String(error.stderr).trim()
994
+ : '';
995
+ const message = error instanceof Error
996
+ ? error.message
997
+ : typeof error === 'object' && error !== null && 'message' in error
998
+ ? String(error.message)
999
+ : 'Command failed';
1000
+ return redactCommandError(stderr || message);
902
1001
  }
903
1002
 
904
- function runTaskCommand(cmd, args, opts = {}) {
1003
+ function runTaskCommand(cmd: string, args: string[], opts: { cwd?: string } = {}): string {
905
1004
  try {
906
1005
  return run(cmd, args, opts);
907
1006
  } catch (error) {
@@ -909,32 +1008,39 @@ function runTaskCommand(cmd, args, opts = {}) {
909
1008
  }
910
1009
  }
911
1010
 
912
- function runEngineTaskCommand(engine, cmd, args, opts = {}) {
1011
+ function runEngineTaskCommand(engine: string, cmd: string, args: string[], opts: { cwd?: string } = {}): string {
913
1012
  const command = commandForEngine(engine, cmd, args);
914
1013
  return runTaskCommand(command.cmd, command.args, opts);
915
1014
  }
916
1015
 
917
1016
  export function buildImage(
918
- config,
919
- tools,
920
- dockerfilePath,
921
- imageSignature,
1017
+ config: SandboxCreateConfig,
1018
+ tools: SandboxTool[],
1019
+ dockerfilePath: string,
1020
+ imageSignature: string,
922
1021
  {
923
1022
  engine,
924
1023
  runFn = runEngine,
925
1024
  runSafeFn = runSafeEngine,
926
1025
  runVerboseFn = runVerboseEngine,
927
1026
  env = process.env
1027
+ }: {
1028
+ engine?: string;
1029
+ runFn?: EngineRunFn;
1030
+ runSafeFn?: EngineRunSafeFn;
1031
+ runVerboseFn?: EngineRunVerboseFn;
1032
+ env?: NodeJS.ProcessEnv;
928
1033
  } = {}
929
- ) {
1034
+ ): void {
1035
+ const selectedEngine = engine ?? detectEngine(config);
930
1036
  const { uid: hostUid, gid: hostGid } = resolveBuildUid({
931
- engine,
1037
+ engine: selectedEngine,
932
1038
  runFn,
933
1039
  runSafeFn,
934
1040
  env
935
1041
  });
936
1042
 
937
- runVerboseFn(engine, 'docker', [
1043
+ runVerboseFn(selectedEngine, 'docker', [
938
1044
  'build',
939
1045
  '-t',
940
1046
  config.imageName,
@@ -949,12 +1055,12 @@ export function buildImage(
949
1055
  '--label',
950
1056
  `${sandboxImageConfigLabel(config)}=${imageSignature}`,
951
1057
  '-f',
952
- toEnginePath(engine, dockerfilePath),
953
- toEnginePath(engine, config.repoRoot)
1058
+ toEnginePath(selectedEngine, dockerfilePath),
1059
+ toEnginePath(selectedEngine, config.repoRoot)
954
1060
  ], { cwd: config.repoRoot });
955
1061
  }
956
1062
 
957
- export async function create(args) {
1063
+ export async function create(args: string[]): Promise<void> {
958
1064
  const { values, positionals } = parseArgs({
959
1065
  args,
960
1066
  allowPositionals: true,
@@ -979,7 +1085,7 @@ export async function create(args) {
979
1085
  validateClaudeCredentialsEnvOverride();
980
1086
 
981
1087
  const config = loadConfig();
982
- const [branchOrTaskId, base] = positionals;
1088
+ const [branchOrTaskId = '', base] = positionals;
983
1089
  const branch = resolveTaskBranch(branchOrTaskId, config.repoRoot);
984
1090
  assertValidBranchName(branch);
985
1091
  const effectiveConfig = {
@@ -1004,7 +1110,7 @@ export async function create(args) {
1004
1110
  resolvedTools
1005
1111
  );
1006
1112
  const container = containerName(effectiveConfig, branch);
1007
- const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0];
1113
+ const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
1008
1114
  const shareCommon = shareCommonDir(effectiveConfig);
1009
1115
  const shareBranch = shareBranchDir(effectiveConfig, branch);
1010
1116
  const preparedDockerfile = prepareDockerfile(effectiveConfig);
@@ -1019,7 +1125,7 @@ export async function create(args) {
1019
1125
 
1020
1126
  try {
1021
1127
  p.log.step('Checking container engine...');
1022
- await ensureDocker(effectiveConfig, (detail) => {
1128
+ await ensureDocker(effectiveConfig, (detail: string) => {
1023
1129
  p.log.info(` ${detail}`);
1024
1130
  });
1025
1131
  p.log.success('Docker is ready');
@@ -1053,7 +1159,7 @@ export async function create(args) {
1053
1159
  await p.tasks([
1054
1160
  {
1055
1161
  title: 'Setting up git worktree',
1056
- task: async (message) => {
1162
+ task: async (message: (text: string) => void) => {
1057
1163
  if (fs.existsSync(worktree)) {
1058
1164
  if (fs.readdirSync(worktree).length > 0) {
1059
1165
  return `Worktree exists at ${worktree}`;
@@ -1142,7 +1248,7 @@ export async function create(args) {
1142
1248
  },
1143
1249
  {
1144
1250
  title: `Starting container '${container}'`,
1145
- task: async (message) => {
1251
+ task: async (message: (text: string) => void) => {
1146
1252
  const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
1147
1253
  const matchedContainers = containerNameCandidates(effectiveConfig, branch)
1148
1254
  .filter((name) => existing.includes(name));
@@ -1178,7 +1284,7 @@ export async function create(args) {
1178
1284
  )
1179
1285
  : null;
1180
1286
  const envFile = buildContainerEnvFile(resolvedTools, engine);
1181
- let hostShellConfig;
1287
+ let hostShellConfig: HostShellConfig;
1182
1288
  try {
1183
1289
  const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
1184
1290
  if (claudeCodeEntry) {
@@ -1303,8 +1409,8 @@ export async function create(args) {
1303
1409
  cachedOverride: cachedGpg,
1304
1410
  repoPath: worktree,
1305
1411
  signingKey,
1306
- dockerExecFn: (cmd, args, opts) => execEngine(engine, cmd, args, opts),
1307
- dockerRunSafeFn: (cmd, args, opts) => runSafeEngine(engine, cmd, args, opts)
1412
+ dockerExecFn: (cmd: string, args: string[], opts?: ExecSyncOptions) => execEngine(engine, cmd, args, opts),
1413
+ dockerRunSafeFn: (cmd: string, args: string[], opts?: { cwd?: string }) => runSafeEngine(engine, cmd, args, opts)
1308
1414
  }
1309
1415
  )) {
1310
1416
  writeSanitizedGitconfig({