@fitlab-ai/agent-infra 0.5.9 → 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 (100) hide show
  1. package/README.md +200 -8
  2. package/README.zh-CN.md +176 -8
  3. package/bin/{cli.js → cli.ts} +23 -19
  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} +64 -20
  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.ts +1507 -0
  54. package/lib/sandbox/commands/enter.ts +115 -0
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
  56. package/lib/sandbox/commands/rebuild.ts +135 -0
  57. package/lib/sandbox/commands/refresh.ts +128 -0
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
  60. package/lib/sandbox/config.ts +133 -0
  61. package/lib/sandbox/{constants.js → constants.ts} +41 -17
  62. package/lib/sandbox/credentials.ts +634 -0
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/dotfiles.ts +236 -0
  65. package/lib/sandbox/engine.ts +231 -0
  66. package/lib/sandbox/engines/colima.ts +81 -0
  67. package/lib/sandbox/engines/docker-desktop.ts +36 -0
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/native.ts +131 -0
  70. package/lib/sandbox/engines/orbstack.ts +78 -0
  71. package/lib/sandbox/engines/selinux.ts +66 -0
  72. package/lib/sandbox/engines/wsl2-paths.ts +65 -0
  73. package/lib/sandbox/engines/wsl2.ts +74 -0
  74. package/lib/sandbox/{index.js → index.ts} +17 -8
  75. package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
  76. package/lib/sandbox/runtimes/base.dockerfile +116 -1
  77. package/lib/sandbox/shell.ts +186 -0
  78. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  79. package/lib/sandbox/{tools.js → tools.ts} +33 -29
  80. package/lib/{update.js → update.ts} +33 -10
  81. package/package.json +22 -12
  82. package/templates/.agents/rules/create-issue.github.en.md +2 -4
  83. package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
  84. package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
  85. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
  86. package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
  87. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
  88. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  89. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  90. package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
  91. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
  92. package/lib/paths.js +0 -9
  93. package/lib/sandbox/commands/create.js +0 -1174
  94. package/lib/sandbox/commands/enter.js +0 -79
  95. package/lib/sandbox/commands/rebuild.js +0 -102
  96. package/lib/sandbox/config.js +0 -84
  97. package/lib/sandbox/engine.js +0 -256
  98. package/lib/sandbox/shell.js +0 -122
  99. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
  100. /package/lib/{version.js → version.ts} +0 -0
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
17
17
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
18
18
  <a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
19
- <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen?logo=node.js" alt="Node.js >= 18"></a>
19
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
22
22
  </p>
@@ -201,6 +201,101 @@ The sandbox image also preinstalls `gh`. When `gh auth token` succeeds on the ho
201
201
 
202
202
  `ai sandbox exec` also forwards a small terminal-detection whitelist (`TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `LC_TERMINAL`, `LC_TERMINAL_VERSION`) into the container. This keeps interactive TUIs aligned with the host terminal for behaviors such as Claude Code's Shift+Enter newline support, without passing through the full host environment.
203
203
 
204
+ `ai sandbox exec` and `ai sandbox refresh` reconcile Claude Code credentials in both directions across the host credential store and every sandbox project copy under `~/.agent-infra/credentials/*`. When a long-running sandbox refreshes OAuth tokens first, the next entry or refresh command writes the freshest valid copy back to the host Keychain or `~/.claude/.credentials.json`; when the host is fresher, it updates the project copies. If every copy is stale, `ai sandbox refresh` probes `claude /status` and asks you to log in only when the probe cannot recover credentials.
205
+
206
+ ### Host-sandbox file exchange
207
+
208
+ `ai sandbox create` mounts two writable directories for dropping files between
209
+ the host and the sandbox without polluting the git worktree:
210
+
211
+ - `/share/common` <- `~/.agent-infra/share/<project>/common/` - visible to every
212
+ sandbox of the same project, regardless of branch.
213
+ - `/share/branch` <- `~/.agent-infra/share/<project>/branches/<branch>/` -
214
+ exclusive to the current branch sandbox.
215
+
216
+ These paths are intentionally hardcoded; there is no `.airc.json` knob. Both
217
+ host directories are created automatically on first `create`. When you
218
+ `ai sandbox rm <branch>` or `ai sandbox rm --all`, you will be prompted (default
219
+ yes) to clean up the corresponding share dirs alongside the worktrees.
220
+ Existing sandboxes pick up these mounts after `ai sandbox rm <branch>` and
221
+ `ai sandbox create <branch>`.
222
+
223
+ ### User-level dotfiles channel
224
+
225
+ `ai sandbox create` also mounts an optional read-only channel for host user preferences:
226
+
227
+ - `/dotfiles` <- `~/.agent-infra/dotfiles/` - read-only, host-owned source.
228
+
229
+ The host tree mirrors the expected paths under the container `$HOME`, in the
230
+ same style as GNU stow or chezmoi:
231
+
232
+ ```text
233
+ ~/.agent-infra/dotfiles/
234
+ ├── .tmux.conf
235
+ └── .config/
236
+ ├── lazygit/config.yml
237
+ └── yazi/yazi.toml
238
+ ```
239
+
240
+ On each sandbox entry, `sandbox-dotfiles-link` links every file to
241
+ `$HOME/<relative-path>` with `ln -sfn`, overriding image defaults. If the host
242
+ directory does not exist, the mount and link step are skipped.
243
+
244
+ To add future preferences such as `starship.toml` or `.gitconfig.local`, put
245
+ files in `~/.agent-infra/dotfiles/`; no Dockerfile or `ai sandbox create`
246
+ changes are needed.
247
+
248
+ #### Symlinks as pointers to host files
249
+
250
+ You can place symlinks inside `~/.agent-infra/dotfiles/` to point at real files
251
+ on your host:
252
+
253
+ ```bash
254
+ ln -s ~/.tmux.conf ~/.agent-infra/dotfiles/.tmux.conf
255
+ ln -s ~/.config/lazygit ~/.agent-infra/dotfiles/.config/lazygit
256
+ ```
257
+
258
+ Before each `ai sandbox create` and `ai sandbox enter`, agent-infra
259
+ dereferences the dotfiles tree into
260
+ `~/.agent-infra/.cache/dotfiles-resolved/<project>/` and mounts that snapshot
261
+ into the container. Editing the host source file, then re-entering the sandbox,
262
+ is enough to pick up the latest content.
263
+
264
+ Dangling symlinks are skipped with a stderr warning. Symlink cycles and deeply
265
+ nested directories beyond 32 levels are also skipped with a warning. Symlinks
266
+ pointing outside `$HOME` are accepted as long as the host user can read the
267
+ target.
268
+
269
+ > **Do not put secrets in `~/.agent-infra/dotfiles/`.** The mount is read-only
270
+ > inside the container, but the full preference tree is linked into every
271
+ > project sandbox. Do not place `.ssh/`, `.aws/credentials`, `.netrc`,
272
+ > `.gnupg/`, `.npmrc` files containing `_authToken`, AI tool OAuth/access token
273
+ > files, or `.gitconfig` there. Use the dedicated SSH and credential channels,
274
+ > and prefer `.gitconfig.local` with `[include]` for local Git preferences.
275
+
276
+ **Protected paths** are ignored by the hook even if they appear under
277
+ `~/.agent-infra/dotfiles/`:
278
+
279
+ | Path pattern | Reason |
280
+ |---|---|
281
+ | `.ssh/*` | Host SSH credentials are managed by the read-only SSH mount. |
282
+ | `.gnupg/*` | GPG private material is managed by `gpg-agent`. |
283
+ | `.claude/*`, `.codex/*`, `.gemini/*` | AI tool credentials use dedicated bind mounts. |
284
+ | `.config/opencode/*`, `.local/share/opencode/*` | OpenCode credentials and data use dedicated bind mounts. |
285
+ | `.host-shell-config/*` | agent-infra managed shell and Git configuration. |
286
+ | `.gitconfig`, `.gitignore_global`, `.stCommitMsg`, `.bash_aliases` | agent-infra symlinks these to `.host-shell-config/`, including `safe.directory` and GPG sync state. |
287
+
288
+ Other existing real directories, such as `~/.config/` or `~/.cache/`, are not
289
+ replaced by top-level dotfiles. If a file conflicts with one of those
290
+ directories, the hook prints a warning and skips it:
291
+
292
+ ```text
293
+ sandbox-dotfiles-link: skipping /home/devuser/.config (existing directory; use nested path like .config/<file> instead)
294
+ ```
295
+
296
+ Use nested paths such as `~/.agent-infra/dotfiles/.config/lazygit/config.yml`
297
+ instead of treating `.config` as a top-level file.
298
+
204
299
  <a id="architecture-overview"></a>
205
300
 
206
301
  ## Architecture Overview
@@ -240,13 +335,65 @@ agent-infra is intentionally simple: a bootstrap CLI creates the seed configurat
240
335
 
241
336
  ## Platform Support
242
337
 
243
- agent-infra runs on macOS and Linux. The CLI itself only needs Node.js (>=18); container-related features (`ai sandbox *`) additionally need Docker.
338
+ agent-infra runs on macOS, Linux, and Windows. The CLI itself only needs Node.js (>=22); container-related features (`ai sandbox *`) additionally need Docker.
339
+
340
+ ### Sandbox engine selection
341
+
342
+ `sandbox.engine` in `.agents/.airc.json` selects the container engine. When it is `null` or omitted, agent-infra uses the platform default:
343
+
344
+ - Linux: `native`
345
+ - macOS: `colima`
346
+ - Windows: `wsl2`
347
+
348
+ You can override the engine in `.agents/.airc.json`. Valid engines are platform-specific:
349
+
350
+ - Linux: `native`, `docker-desktop`
351
+ - macOS: `colima`, `orbstack`, `docker-desktop`
352
+ - Windows: `wsl2`, `native`, `docker-desktop`
244
353
 
245
354
  ### macOS
246
355
 
247
356
  - `ai init`, `ai sync`, etc.: works out of the box after `npm install -g @fitlab-ai/agent-infra` (or Homebrew).
248
357
  - `ai sandbox *`: requires Colima, OrbStack, or Docker Desktop. Colima is the default engine on macOS — when it is selected and the `colima` command is missing, agent-infra auto-installs and starts Colima via Homebrew on first run. To use OrbStack or Docker Desktop instead, set `sandbox.engine` in `.agents/.airc.json`.
249
358
 
359
+ #### Engine resource configuration
360
+
361
+ | Engine | `vm.cpu` | `vm.memory` | `vm.disk` | Apply mode | Notes |
362
+ |--------|----------|-------------|-----------|------------|-------|
363
+ | Colima | applied | applied | applied | on-start | VM must be restarted (`ai sandbox vm stop && ai sandbox vm start`) for changes to take effect. |
364
+ | OrbStack | applied | applied | warned | hot | Applied via `orb config set` on every invocation. OrbStack manages disk via thin provisioning. |
365
+ | Docker Desktop | warned | warned | warned | manual | Resources must be set in Docker Desktop GUI (Settings -> Resources). |
366
+
367
+ `vm.memory` and `--memory` values are expressed in GiB.
368
+
369
+ #### SSH / locked keychain
370
+
371
+ On macOS over SSH, the login keychain may be locked and reject non-interactive reads or writes with `errSecInteractionNotAllowed`. You can unlock it on the host and re-run `ai sandbox refresh`:
372
+
373
+ ```bash
374
+ security unlock-keychain ~/Library/Keychains/login.keychain-db
375
+ ai sandbox refresh
376
+ ```
377
+
378
+ For long-lived SSH sessions or CI, bypass the keychain with `AGENT_INFRA_CLAUDE_CREDENTIALS_FILE`. macOS stores Claude Code credentials in the keychain by default, so seed the override file once from a session where the keychain is unlocked:
379
+
380
+ ```bash
381
+ security unlock-keychain ~/Library/Keychains/login.keychain-db
382
+ umask 077 && mkdir -p "$HOME/.agent-infra" && \
383
+ security find-generic-password -s "Claude Code-credentials" -w \
384
+ > "$HOME/.agent-infra/claude-credentials.json"
385
+ chmod 600 "$HOME/.agent-infra/claude-credentials.json"
386
+ ```
387
+
388
+ Then on the SSH / CI side:
389
+
390
+ ```bash
391
+ export AGENT_INFRA_CLAUDE_CREDENTIALS_FILE="$HOME/.agent-infra/claude-credentials.json"
392
+ ai sandbox refresh
393
+ ```
394
+
395
+ After that, sandbox create, exec, and refresh use the file instead of the keychain for Claude Code credential reads and writes.
396
+
250
397
  ### Linux
251
398
 
252
399
  - `ai init`, `ai sync`, etc.: works out of the box after `npm install -g @fitlab-ai/agent-infra`.
@@ -264,18 +411,63 @@ agent-infra runs on macOS and Linux. The CLI itself only needs Node.js (>=18); c
264
411
 
265
412
  GPG signing works when the host `gpg-agent` and signing key are available; if key sync fails, `ai sandbox create` falls back to a sanitized Git config so commits still work without host signing state.
266
413
 
414
+ #### Engine resource configuration
415
+
416
+ Linux uses native Docker on the host kernel, so there is no managed VM. `sandbox.vm.*` and the `--cpu / --memory` flags do not apply. To cap container resources, use `docker run --cpus / --memory` per container or configure host cgroups.
417
+
418
+ #### Rootless Docker (optional)
419
+
420
+ **Skip this section if you followed the Quick setup above.** The Quick setup installs the default rootful Docker, which works out of the box with `ai sandbox` — no extra configuration is required.
421
+
422
+ Rootless Docker is a separate Docker installation where the daemon runs as your normal user instead of `root`. It is typically chosen on shared hosts, multi-tenant servers, or when a security policy forbids a root-owned daemon. If you have intentionally installed rootless Docker (or plan to), follow the steps below; otherwise stay with rootful.
423
+
424
+ To install and verify rootless Docker:
425
+
426
+ ```bash
427
+ sudo apt install -y uidmap slirp4netns dbus-user-session
428
+ dockerd-rootless-setuptool.sh install
429
+ systemctl --user enable --now docker
430
+ export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
431
+ docker info
432
+ ```
433
+
434
+ Add the `DOCKER_HOST` export to your shell startup file after validation.
435
+
436
+ When rootless Docker is detected, agent-infra builds the sandbox image with `HOST_UID=0` and `HOST_GID=0`. Inside the container the sandbox user can read bind mounts such as `~/.ssh` without relaxing host file permissions. On the host, the daemon and container processes still run under the current user, so this does not grant host root privileges.
437
+
438
+ Known rootless differences:
439
+
440
+ - Networking uses slirp4netns by default and can be slower than rootful bridge networking.
441
+ - Processes run as UID 0 inside the container, unlike rootful Docker where agent-infra mirrors the host UID.
442
+ - The CI rootless matrix is initially allowed to fail while runner stability is observed.
443
+
444
+ Troubleshooting:
445
+
446
+ - If `docker info` fails, check `systemctl --user status docker` and confirm `DOCKER_HOST` points at `$XDG_RUNTIME_DIR/docker.sock`.
447
+ - If SSH files are still unreadable inside the sandbox, confirm the shell has not overridden `DOCKER_HOST` or Docker build arguments.
448
+
267
449
  #### Known limitations on Linux
268
450
 
269
451
  These configurations are not actively tested in this release:
270
452
 
271
- - **Rootless Docker**: Track [#256](https://github.com/fitlab-ai/agent-infra/issues/256).
272
- - **Podman** instead of Docker: Track [#257](https://github.com/fitlab-ai/agent-infra/issues/257).
273
- - **SELinux-enforcing** hosts (Fedora / RHEL) may need manual mount labels: Track [#258](https://github.com/fitlab-ai/agent-infra/issues/258).
274
- - `ai sandbox vm` is a no-op on Linux. Linux uses native Docker directly with no VM to manage; use `ai sandbox create`, `ai sandbox exec`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm` directly.
453
+ - **Podman** instead of Docker: Works on Fedora 40+ and other `dnf`-based RHEL family distros (RHEL, CentOS Stream, Rocky, Alma) via the `podman-docker` shim (`sudo dnf install podman podman-docker`; optionally `sudo touch /etc/containers/nodocker` to silence its per-command notice).
454
+ - **SELinux-enforcing** hosts (Fedora / RHEL): `ai sandbox create` automatically labels bind mounts with Docker's shared `:z` flag — no setup required. Set `AGENT_INFRA_SELINUX_DISABLE=1` to opt out for debugging.
455
+ - `ai sandbox vm` is a no-op on Linux. Linux uses native Docker directly with no VM to manage; use `ai sandbox create`, `ai sandbox exec`, `ai sandbox refresh`, `ai sandbox ls`, `ai sandbox rebuild`, `ai sandbox rm` directly.
275
456
 
276
457
  ### Windows
277
458
 
278
- WSL2 support is tracked in [#184](https://github.com/fitlab-ai/agent-infra/issues/184).
459
+ - `ai init`, `ai sync`, etc.: should work after `npm install -g @fitlab-ai/agent-infra` (Node.js >= 22). Not actively tested in this release.
460
+ - `ai sandbox *`: supported on Windows via WSL2 + Docker Desktop.
461
+
462
+ Before running `ai sandbox create`, install Windows 11 with WSL2, configure a default Linux distribution, install Docker Desktop, and enable Docker Desktop's WSL integration for that distribution.
463
+
464
+ You can run the CLI from PowerShell or Git Bash, but the project path must be visible from WSL, such as `C:\Users\you\project` or another drive mounted under `/mnt/<drive>`. UNC paths are not supported for sandbox mounts. If the Windows entrypoint cannot reach Docker through WSL2, run the same command from inside the WSL distribution as a fallback.
465
+
466
+ `ai sandbox vm` manages only the macOS Colima VM. On Windows, manage Docker Desktop and WSL2 with their native tools.
467
+
468
+ #### Engine resource configuration
469
+
470
+ WSL2 is the default sandbox engine on Windows. `sandbox.vm.cpu`, `sandbox.vm.memory`, and `--cpu / --memory` flags are not applied automatically when using WSL2 — configure CPU and memory limits in Docker Desktop (Settings → Resources) instead. `sandbox.vm.disk` is not applicable to WSL2. `vm.memory` and `--memory` values are expressed in GiB.
279
471
 
280
472
  <a id="what-you-get"></a>
281
473
 
@@ -585,7 +777,7 @@ The generated `.agents/.airc.json` file is the central contract between the boot
585
777
  "project": "my-project",
586
778
  "org": "my-org",
587
779
  "language": "en",
588
- "templateVersion": "v0.5.9",
780
+ "templateVersion": "v0.6.0",
589
781
  "templates": {
590
782
  "sources": [
591
783
  { "type": "local", "path": "~/private-templates" }
package/README.zh-CN.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
17
17
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
18
18
  <a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
19
- <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen?logo=node.js" alt="Node.js >= 18"></a>
19
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
22
22
  </p>
@@ -201,6 +201,77 @@ CLI 会收集项目元数据,向所有支持的 AI TUI 安装 `update-agent-in
201
201
 
202
202
  `ai sandbox exec` 也会向容器透传一小组终端检测白名单变量(`TERM_PROGRAM`、`TERM_PROGRAM_VERSION`、`LC_TERMINAL`、`LC_TERMINAL_VERSION`)。这样可以让交互式 TUI 保持与宿主终端一致的行为,例如 Claude Code 的 `Shift+Enter` 换行支持,同时避免把整个宿主环境灌入容器。
203
203
 
204
+ `ai sandbox exec` 和 `ai sandbox refresh` 会在宿主机凭证存储与 `~/.agent-infra/credentials/*` 下的所有沙箱项目副本之间做双向 reconcile。长时间运行的沙箱如果先刷新了 OAuth token,下一次进入或刷新命令会把最新有效副本回写到宿主 Keychain 或 `~/.claude/.credentials.json`;宿主机更新时也会继续覆盖项目副本。如果所有副本都已失效,`ai sandbox refresh` 会尝试 `claude /status` 探活,只有探活无法恢复时才提示重新登录。
205
+
206
+ ### 宿主-沙箱文件交换
207
+
208
+ `ai sandbox create` 会自动挂载两个可读写目录,方便宿主与容器之间互相 drop 文件,不污染 git 工作树:
209
+
210
+ - `/share/common` <- `~/.agent-infra/share/<project>/common/`:项目级共享,跨分支可见。
211
+ - `/share/branch` <- `~/.agent-infra/share/<project>/branches/<branch>/`:分支独占。
212
+
213
+ 这两条路径硬编码,不暴露 `.airc.json` 配置项。首次 `create` 时会自动创建宿主目录;`ai sandbox rm <branch>` 与 `ai sandbox rm --all` 删除时会附带询问是否清理(默认 yes)。
214
+ 已有沙箱需要执行 `ai sandbox rm <branch>` 后再执行 `ai sandbox create <branch>`,才能加载新的挂载点。
215
+
216
+ #### 用户级 dotfiles 通道
217
+
218
+ `ai sandbox create` 还会自动挂载一条可选的只读通道,用于把宿主机用户级偏好带进沙箱:
219
+
220
+ - `/dotfiles` <- `~/.agent-infra/dotfiles/`:只读,host 作为单向源。
221
+
222
+ host 端目录树镜像容器 `$HOME` 下的预期路径,风格类似 GNU stow 或 chezmoi:
223
+
224
+ ```text
225
+ ~/.agent-infra/dotfiles/
226
+ ├── .tmux.conf
227
+ └── .config/
228
+ ├── lazygit/config.yml
229
+ └── yazi/yazi.toml
230
+ ```
231
+
232
+ 每次进入沙箱时,`sandbox-dotfiles-link` 会用 `ln -sfn` 把每个文件链接到
233
+ `$HOME/<相对路径>`,覆盖镜像默认。host 端目录不存在时,会跳过挂载和链接步骤。
234
+
235
+ 未来要加 `starship.toml`、`.gitconfig.local` 等偏好,只需把文件放进
236
+ `~/.agent-infra/dotfiles/`,无需修改 Dockerfile 或 `ai sandbox create`。
237
+
238
+ ##### 符号链接作为指向 host 文件的指针
239
+
240
+ 你可以在 `~/.agent-infra/dotfiles/` 里放符号链接,让它们指向 host 上的真实文件:
241
+
242
+ ```bash
243
+ ln -s ~/.tmux.conf ~/.agent-infra/dotfiles/.tmux.conf
244
+ ln -s ~/.config/lazygit ~/.agent-infra/dotfiles/.config/lazygit
245
+ ```
246
+
247
+ 每次执行 `ai sandbox create` 和 `ai sandbox enter` 前,agent-infra 会先把
248
+ dotfiles 树解引用到
249
+ `~/.agent-infra/.cache/dotfiles-resolved/<project>/`,再把这份快照挂载进容器。
250
+ 因此修改 host 源文件后,重新进入沙箱即可看到最新内容。
251
+
252
+ 悬空符号链接会被跳过并在 stderr 输出警告。符号链接循环以及超过 32 层的深层目录也会被跳过并输出警告。指向 `$HOME` 之外的符号链接可以使用,只要 host 用户能读取目标。
253
+
254
+ > **不要往 `~/.agent-infra/dotfiles/` 放任何凭证。** 容器内是只读挂载,但整棵偏好树会链入所有项目沙箱。不要放 `.ssh/`、`.aws/credentials`、`.netrc`、`.gnupg/`、包含 `_authToken` 的 `.npmrc`、任何 AI 工具 OAuth/access token 文件,也不要放 `.gitconfig`。SSH 和工具凭证请使用专用通道;本地 Git 偏好建议用 `.gitconfig.local` 配合 `[include]`。
255
+
256
+ **受保护路径**即使出现在 `~/.agent-infra/dotfiles/` 下,也会被钩子忽略:
257
+
258
+ | 路径模式 | 原因 |
259
+ |---|---|
260
+ | `.ssh/*` | host SSH 凭证由只读 SSH 挂载管理。 |
261
+ | `.gnupg/*` | GPG 私钥由 `gpg-agent` 管理。 |
262
+ | `.claude/*`, `.codex/*`, `.gemini/*` | AI 工具凭证使用专用 bind mount。 |
263
+ | `.config/opencode/*`, `.local/share/opencode/*` | OpenCode 凭证和数据使用专用 bind mount。 |
264
+ | `.host-shell-config/*` | agent-infra 管理的 shell 和 Git 配置。 |
265
+ | `.gitconfig`, `.gitignore_global`, `.stCommitMsg`, `.bash_aliases` | agent-infra 将这些路径软链到 `.host-shell-config/`,包含 `safe.directory` 和 GPG 同步状态。 |
266
+
267
+ 其他已经存在的真实目录(如 `~/.config/`、`~/.cache/`)不会被顶层 dotfile 替换。如果某个文件与这类目录冲突,钩子会打印警告并跳过:
268
+
269
+ ```text
270
+ sandbox-dotfiles-link: skipping /home/devuser/.config (existing directory; use nested path like .config/<file> instead)
271
+ ```
272
+
273
+ 正确用法是嵌套路径,例如 `~/.agent-infra/dotfiles/.config/lazygit/config.yml`,不要把 `.config` 当成顶层文件。
274
+
204
275
  <a id="architecture-overview"></a>
205
276
 
206
277
  ## 架构概览
@@ -240,13 +311,65 @@ agent-infra 的结构刻意保持简单:引导 CLI 负责生成种子配置,
240
311
 
241
312
  ## 平台支持
242
313
 
243
- agent-infra 支持 macOS 和 Linux。CLI 本身只需要 Node.js (>=18);容器相关功能(`ai sandbox *`)额外需要 Docker。
314
+ agent-infra 支持 macOS、LinuxWindows。CLI 本身只需要 Node.js (>=22);容器相关功能(`ai sandbox *`)额外需要 Docker。
315
+
316
+ ### 沙箱引擎选择
317
+
318
+ `.agents/.airc.json` 中的 `sandbox.engine` 用来选择容器引擎。该字段为 `null` 或省略时,agent-infra 使用平台默认值:
319
+
320
+ - Linux:`native`
321
+ - macOS:`colima`
322
+ - Windows:`wsl2`
323
+
324
+ 你可以在 `.agents/.airc.json` 中覆盖该引擎。合法值按平台区分:
325
+
326
+ - Linux:`native`、`docker-desktop`
327
+ - macOS:`colima`、`orbstack`、`docker-desktop`
328
+ - Windows:`wsl2`、`native`、`docker-desktop`
244
329
 
245
330
  ### macOS
246
331
 
247
332
  - `ai init`、`ai sync` 等:执行 `npm install -g @fitlab-ai/agent-infra`(或 Homebrew 安装)后开箱即用。
248
333
  - `ai sandbox *`:需要 Colima、OrbStack 或 Docker Desktop。macOS 默认引擎是 Colima —— 当选用 Colima 且宿主机没有 `colima` 命令时,agent-infra 会在首次运行时通过 Homebrew 自动安装并启动。如需使用 OrbStack 或 Docker Desktop,请在 `.agents/.airc.json` 中设置 `sandbox.engine`。
249
334
 
335
+ #### 引擎资源配置
336
+
337
+ | 引擎 | `vm.cpu` | `vm.memory` | `vm.disk` | 应用方式 | 说明 |
338
+ |------|----------|-------------|-----------|----------|------|
339
+ | Colima | 生效 | 生效 | 生效 | 启动时 | 变更需重启 VM(`ai sandbox vm stop && ai sandbox vm start`)后生效。 |
340
+ | OrbStack | 生效 | 生效 | 警告 | 热应用 | 每次调用都会通过 `orb config set` 应用。OrbStack 通过 thin provisioning 管理磁盘。 |
341
+ | Docker Desktop | 警告 | 警告 | 警告 | 手动 | 资源必须在 Docker Desktop GUI(Settings -> Resources)中设置。 |
342
+
343
+ `vm.memory` 和 `--memory` 的单位是 GiB。
344
+
345
+ #### SSH / 锁定的 keychain
346
+
347
+ 在 macOS 上通过 SSH 使用时,login keychain 可能处于锁定状态,并以 `errSecInteractionNotAllowed` 拒绝非交互式读写。你可以在宿主机上解锁后重新运行 `ai sandbox refresh`:
348
+
349
+ ```bash
350
+ security unlock-keychain ~/Library/Keychains/login.keychain-db
351
+ ai sandbox refresh
352
+ ```
353
+
354
+ 对于长期 SSH 会话或 CI,可以通过 `AGENT_INFRA_CLAUDE_CREDENTIALS_FILE` 绕过 keychain。macOS 默认把 Claude Code 凭据存进 keychain,所以需要先在 keychain 已解锁的会话中 seed 一次 override 文件:
355
+
356
+ ```bash
357
+ security unlock-keychain ~/Library/Keychains/login.keychain-db
358
+ umask 077 && mkdir -p "$HOME/.agent-infra" && \
359
+ security find-generic-password -s "Claude Code-credentials" -w \
360
+ > "$HOME/.agent-infra/claude-credentials.json"
361
+ chmod 600 "$HOME/.agent-infra/claude-credentials.json"
362
+ ```
363
+
364
+ 之后在 SSH / CI 侧设置:
365
+
366
+ ```bash
367
+ export AGENT_INFRA_CLAUDE_CREDENTIALS_FILE="$HOME/.agent-infra/claude-credentials.json"
368
+ ai sandbox refresh
369
+ ```
370
+
371
+ 此后 sandbox create、exec、refresh 读取和写入 Claude Code 凭据时都会使用该文件,而不是 keychain。
372
+
250
373
  ### Linux
251
374
 
252
375
  - `ai init`、`ai sync` 等:执行 `npm install -g @fitlab-ai/agent-infra` 后开箱即用。
@@ -264,18 +387,63 @@ agent-infra 支持 macOS 和 Linux。CLI 本身只需要 Node.js (>=18);容器
264
387
 
265
388
  当宿主机 `gpg-agent` 和签名 key 可用时,GPG signing 可正常工作;如果 key 同步失败,`ai sandbox create` 会回退到清理后的 Git config,让提交仍可在没有宿主签名状态的情况下继续。
266
389
 
390
+ #### 引擎资源配置
391
+
392
+ Linux 直接使用宿主内核上的原生 Docker,没有受管 VM。`sandbox.vm.*` 与 `--cpu / --memory` 标志均不生效。如需限制容器资源,请用 `docker run --cpus / --memory` 设置单容器限制,或配置宿主 cgroups。
393
+
394
+ #### Rootless Docker(可选)
395
+
396
+ **如果你已按上面的 Quick setup 装好 rootful Docker,跳过本节即可。** Quick setup 装的就是默认的 rootful Docker,`ai sandbox` 开箱可用,不需要任何额外配置。
397
+
398
+ Rootless Docker 是一种另起一套的 Docker 安装方式:daemon 以你的普通用户身份运行,而不是 root。它通常用在共享主机、多租户服务器,或安全策略禁止 root 守护进程的场景。如果你**主动选择**安装了 rootless Docker(或打算这么做),按下面的步骤配置;否则继续用 rootful 就好。
399
+
400
+ 安装并验证 rootless Docker:
401
+
402
+ ```bash
403
+ sudo apt install -y uidmap slirp4netns dbus-user-session
404
+ dockerd-rootless-setuptool.sh install
405
+ systemctl --user enable --now docker
406
+ export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
407
+ docker info
408
+ ```
409
+
410
+ 验证通过后,请把 `DOCKER_HOST` export 写入 shell 启动文件。
411
+
412
+ agent-infra 检测到 rootless Docker 后,会用 `HOST_UID=0` 和 `HOST_GID=0` 构建 sandbox 镜像。这样容器内 sandbox 用户可以读取 `~/.ssh` 等 bind mount,无需放宽宿主文件权限。在宿主侧,daemon 和容器进程仍以当前用户身份运行,不会获得宿主 root 权限。
413
+
414
+ Rootless 模式的已知差异:
415
+
416
+ - 网络默认使用 slirp4netns,可能比 rootful bridge 网络慢。
417
+ - 容器内进程以 UID 0 运行;rootful Docker 下 agent-infra 仍会镜像宿主 UID。
418
+ - CI rootless matrix 初期允许失败,用于观察 GitHub runner 稳定性。
419
+
420
+ 排障:
421
+
422
+ - 如果 `docker info` 失败,请检查 `systemctl --user status docker`,并确认 `DOCKER_HOST` 指向 `$XDG_RUNTIME_DIR/docker.sock`。
423
+ - 如果 sandbox 内仍无法读取 SSH 文件,请确认 shell 没有覆盖 `DOCKER_HOST` 或 Docker build args。
424
+
267
425
  #### Linux 已知限制
268
426
 
269
427
  下列场景在本期未做主动验证:
270
428
 
271
- - **Rootless Docker**:后续跟踪 [#256](https://github.com/fitlab-ai/agent-infra/issues/256)。
272
- - **Podman** 替代 Docker:后续跟踪 [#257](https://github.com/fitlab-ai/agent-infra/issues/257)。
273
- - **SELinux enforcing** 宿主机(Fedora / RHEL)可能需要手动加挂载标签:后续跟踪 [#258](https://github.com/fitlab-ai/agent-infra/issues/258)。
274
- - `ai sandbox vm` 在 Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`。
429
+ - **Podman** 替代 Docker:Fedora 40+ 及其他 `dnf` 系 RHEL 发行版(RHEL、CentOS Stream、Rocky、Alma)上通过 `podman-docker` shim 已可使用(`sudo dnf install podman podman-docker`;可选 `sudo touch /etc/containers/nodocker` 抑制 podman 在每条命令前打印的提示)。
430
+ - **SELinux enforcing** 宿主机(Fedora / RHEL):`ai sandbox create` 会自动给 bind mount 加 Docker 共享 `:z` 标签,无需手动准备。如需排障可设 `AGENT_INFRA_SELINUX_DISABLE=1` 关闭。
431
+ - `ai sandbox vm` Linux 上是空操作。Linux 直接使用 native Docker,没有 VM 需要管理;请直接使用 `ai sandbox create`、`ai sandbox exec`、`ai sandbox refresh`、`ai sandbox ls`、`ai sandbox rebuild`、`ai sandbox rm`。
275
432
 
276
433
  ### Windows
277
434
 
278
- WSL2 支持在 [#184](https://github.com/fitlab-ai/agent-infra/issues/184) 跟踪。
435
+ - `ai init`、`ai sync` 等:执行 `npm install -g @fitlab-ai/agent-infra` 后理论上可用(需 Node.js >= 22)。本期未做主动验证。
436
+ - `ai sandbox *`:Windows 通过 WSL2 + Docker Desktop 支持。
437
+
438
+ 运行 `ai sandbox create` 前,请先准备 Windows 11、WSL2、默认 Linux distribution、Docker Desktop,并在 Docker Desktop 中为该 distribution 启用 WSL integration。
439
+
440
+ 你可以从 PowerShell 或 Git Bash 运行 CLI,但项目路径必须能被 WSL 访问,例如 `C:\Users\you\project`,或其他会挂载到 `/mnt/<drive>` 的磁盘路径。UNC 路径不支持作为沙箱挂载路径。如果 Windows 入口无法通过 WSL2 访问 Docker,可以进入对应 WSL distribution 后运行同一命令作为回退方案。
441
+
442
+ `ai sandbox vm` 只管理 macOS 的 Colima VM。在 Windows 上,请使用 Docker Desktop 和 WSL2 自带工具管理后端。
443
+
444
+ #### 引擎资源配置
445
+
446
+ WSL2 是 Windows 上的默认 sandbox 引擎。使用 WSL2 时,`sandbox.vm.cpu`、`sandbox.vm.memory` 以及 `--cpu / --memory` 标志不会自动生效——请在 Docker Desktop(Settings → Resources)中配置 CPU 和内存限制。`sandbox.vm.disk` 不适用于 WSL2。`vm.memory` 和 `--memory` 的单位是 GiB。
279
447
 
280
448
  <a id="what-you-get"></a>
281
449
 
@@ -585,7 +753,7 @@ import-issue #42 从 GitHub Issue 导入任务
585
753
  "project": "my-project",
586
754
  "org": "my-org",
587
755
  "language": "en",
588
- "templateVersion": "v0.5.9",
756
+ "templateVersion": "v0.6.0",
589
757
  "templates": {
590
758
  "sources": [
591
759
  { "type": "local", "path": "~/private-templates" }
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from '../lib/version.js';
2
+ import { VERSION } from '../lib/version.ts';
3
3
 
4
4
  // Node.js version check
5
- const major = parseInt(process.versions.node.split('.')[0], 10);
6
- if (major < 18) {
5
+ const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
6
+ if (major < 22) {
7
7
  process.stderr.write(
8
- `agent-infra requires Node.js >= 18 (current: ${process.version})\n`
8
+ `agent-infra requires Node.js >= 22 (current: ${process.version})\n`
9
9
  );
10
10
  process.exit(1);
11
11
  }
@@ -35,15 +35,19 @@ Examples:
35
35
 
36
36
  const command = process.argv[2] || '';
37
37
 
38
- async function importCommand(importPath) {
38
+ function errorMessage(error: unknown): string {
39
+ return error instanceof Error ? error.message : String(error);
40
+ }
41
+
42
+ async function importCommand(importPath: string) {
39
43
  try {
40
44
  return await import(importPath);
41
45
  } catch (error) {
42
- if (error?.code === 'ERR_MODULE_NOT_FOUND') {
46
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') {
43
47
  process.stderr.write(
44
48
  'Error: Missing npm dependency. Run npm install before using agent-infra from a development checkout.\n'
45
49
  );
46
- process.stderr.write(`${error.message}\n`);
50
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
47
51
  process.exitCode = 1;
48
52
  return null;
49
53
  }
@@ -53,41 +57,41 @@ async function importCommand(importPath) {
53
57
 
54
58
  switch (command) {
55
59
  case 'init': {
56
- const imported = await importCommand('../lib/init.js');
60
+ const imported = await importCommand('../lib/init.ts');
57
61
  if (!imported) break;
58
62
  const { cmdInit } = imported;
59
- await cmdInit().catch((e) => {
60
- process.stderr.write(`Error: ${e.message}\n`);
63
+ await cmdInit().catch((e: unknown) => {
64
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
61
65
  process.exitCode = 1;
62
66
  });
63
67
  break;
64
68
  }
65
69
  case 'update': {
66
- const imported = await importCommand('../lib/update.js');
70
+ const imported = await importCommand('../lib/update.ts');
67
71
  if (!imported) break;
68
72
  const { cmdUpdate } = imported;
69
- await cmdUpdate().catch((e) => {
70
- process.stderr.write(`Error: ${e.message}\n`);
73
+ await cmdUpdate().catch((e: unknown) => {
74
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
71
75
  process.exitCode = 1;
72
76
  });
73
77
  break;
74
78
  }
75
79
  case 'merge': {
76
- const imported = await importCommand('../lib/merge.js');
80
+ const imported = await importCommand('../lib/merge.ts');
77
81
  if (!imported) break;
78
82
  const { cmdMerge } = imported;
79
- await cmdMerge(process.argv.slice(3)).catch((e) => {
80
- process.stderr.write(`Error: ${e.message}\n`);
83
+ await cmdMerge(process.argv.slice(3)).catch((e: unknown) => {
84
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
81
85
  process.exitCode = 1;
82
86
  });
83
87
  break;
84
88
  }
85
89
  case 'sandbox': {
86
- const imported = await importCommand('../lib/sandbox/index.js');
90
+ const imported = await importCommand('../lib/sandbox/index.ts');
87
91
  if (!imported) break;
88
92
  const { runSandbox } = imported;
89
- await runSandbox(process.argv.slice(3)).catch((e) => {
90
- process.stderr.write(`Error: ${e.message}\n`);
93
+ await runSandbox(process.argv.slice(3)).catch((e: unknown) => {
94
+ process.stderr.write(`Error: ${errorMessage(e)}\n`);
91
95
  process.exitCode = 1;
92
96
  });
93
97
  break;