@appforge-ci/agent

中文文档
0.3.29发布于 2026/10/24MITES Module近 30 天下载 2519

The build-runner daemon that turns a Mac mini into a fleet member. It:

原始使用文档
文档目录5 节
  1. Setup on a Mac mini
  2. Will this Mac come back by itself? appforge-agent doctor
  3. Sandbox self-test (regulated builds)
  4. How a build clones
  5. Remote management, and why it's an allowlist

@appforge-ci/agent

The build-runner daemon that turns a Mac mini into a fleet member. It:

  • Registers itself against the control plane with a one-time enrollment token generated from the dashboard (Fleet → Add Mac mini).
  • Sends a heartbeat every 15s (CPU/mem/disk, current build, Tailscale hostname if installed) and picks up any pending remote management command (drain, resume, reboot, restart, run a named maintenance script) in the same response.
  • Long-polls POST /agents/lease-job for work — no inbound port is ever opened on the Mac mini.
  • Runs one or more builds at a time (resolveMaxConcurrentBuilds() in src/lib/config.ts — APPFORGE_MAX_CONCURRENT_BUILDS/ AgentConfig.maxConcurrentBuilds, or an auto-detected default from CPU core count, capped at 4), each in its own job-scoped directory: clones the repo, reads appforge.yaml, runs the workflow's scripts in order via bash -lc, streams logs back line-by-line, uploads matched artifacts straight to Cloudflare R2 via a presigned URL, then reports success/failure. See docs/fleet-management.md's "Concurrent builds" section for the full picture. A regulated VPN build (a lease that carries sandbox) is the exception: it runs alone, and the agent itself refuses to share the Mac with it (src/lib/exclusive-builds.ts, docs/vpn.md, "Regulated mode").

Setup on a Mac mini

If this repo is already cloned on the machine (true for your own laptop, or any mini you git cloned directly), there's a one-command path — see docs/self-hosting.md's "Adding a Mac mini" section:

appforge-admin login --api-url <url> --admin-api-url <url>   # once
appforge-admin agents add-mac --name "mac-mini-1"

That requests the enrollment token, reads this machine's own info, enrolls, and installs a launchd service (no sudo) in one step. It then ends the login it needed (revokes the token on the server and removes the file; --keep-login keeps it): the login is a long-lived token that tenants' builds on this Mac can read (the doctor's cli-credentials-off-build-mac check fails while it is there; docs/security.md, "A CLI login never stays on a build Mac"). The from-scratch path below needs no login on the Mac at all.

For a from-scratch box that doesn't have this repo cloned at all — no git clone, no pnpm install — install just the two CLIs actually needed from npm instead:

# 1. Prerequisites (once per machine) — see infra/mac-mini-setup/bootstrap.sh
#    for the full list (Homebrew, Xcode CLT, Node, CocoaPods, fastlane,
#    Android SDK/adb if you also build Android/Flutter/RN apps), or run
#    `appforge-admin agents bootstrap` once it's installed below (skips
#    the sudo/system-setting parts unless you pass --full).

npm install -g @appforge-ci/admin-cli @appforge-ci/agent
# ^ needs a version actually published first (tag-triggered
#   .github/workflows/publish.yml, gated on an NPM_TOKEN secret that
#   isn't set up on this instance yet) — 404s until then. See
#   packages/admin-cli/README.md's "Install" section for the
#   `npm link`-from-a-checkout path that works today.

# 2. Enroll — get a token with `appforge-admin agents enroll`:
appforge-agent enroll \
  --api-url https://your-admin-api-instance.example \
  --token afe_xxx \
  --name "mac-mini-1"
# Already enrolled? `enroll` refuses unless you pass --force. The control
# plane dedupes on this Mac's hardware UUID, so a re-enroll rotates the
# agent token and keeps the same fleet record — it can't make a duplicate.

# 3. Run it directly to sanity-check...
appforge-agent run

# 4. ...then Ctrl-C and install as a launchd service so it survives
#    reboots/logouts — no sudo, no copying files, points straight at
#    wherever npm just installed this binary:
appforge-agent install-service

# If another account is this Mac's auto-login account (a Mac that also hosts a
# GitHub Actions runner, say), a LaunchAgent would wait for a manual login after
# every reboot. Install the agent as a boot-time LaunchDaemon instead (asks for
# sudo; runs as your account; `--dry-run` previews every step):
appforge-agent install-service --daemon

install-service flags: --daemon (boot-time LaunchDaemon, see above), --dry-run (with --daemon: print the steps and the plist, change nothing), --force (install even though the running agent reports active builds; the restart cancels them), --drop-env (let the new plist drop operator knobs such as APPFORGE_SANDBOX_MONITOR that the plist being replaced sets and this shell does not export; without it a daemon install refuses). A re-run without --daemon keeps whichever form is installed. Details, rollback and limits: docs/fleet-management.md, "Agent service forms".

Will this Mac come back by itself? appforge-agent doctor

Read-only checks, run on the Mac itself as the user that runs the agent. It answers what docs/datacenter-readiness.md ends on (pull the power and the agent is back with nobody at the keyboard) without pulling the power:

appforge-agent doctor            # human-readable; exit 1 if anything fails
appforge-agent doctor --json     # the same, machine-readable, with a `rebootRecovery` roll-up
appforge-agent doctor --strict   # exit 1 on warnings too
GroupChecks
Reboot recoveryFileVault is off; the agent's service starts without a login (a LaunchDaemon, or a LaunchAgent of the auto-login account); a co-hosted GitHub runner's service does too; autorestart is on
Servicesevery installed com.appforge.* launchd job is loaded and not crash-looping; every built app (packages/agent, apps/*) can resolve its dependencies; the service does not pin a version-pinned node; the agent is installed as one launchd job (not a LaunchDaemon and a LaunchAgent at once); no control-plane secret is readable in a service configuration (#902); no CLI login token is stored in ~/.appforge-admin or ~/.appforge (cli-credentials-off-build-mac, #904: tenants' builds run as this user and can read it; skipped on a machine with no agent, such as an operator's laptop); the running agent's heartbeat is fresh
Hostnever sleeps on AC power; a stable host name (macOS HostName set); Tailscale as a daemon, if used; Remote Login; passwordless shutdown -r now for REBOOT_HOST; free disk
Networkthe default route is not Wi-Fi; Wi-Fi is not joined next to a wired link (two LAN addresses on one Mac); when Wi-Fi is the link, it loses at most 2 % of 20 pings to the gateway (a tart pull does not resume). A colo mini has no Wi-Fi, so a mini that quietly depends on it will not survive the move; skipped on a laptop and when a VPN is the default route

A laptop (battery present) skips the checks that only matter for an unattended Mac. install-service and appforge-admin agents add-mac print the reboot-recovery verdict when they finish.

Sandbox self-test (regulated builds)

The agent can run a build's scripts under a macOS Seatbelt sandbox when a job asks for it (nothing asks yet). To check this Mac:

appforge-agent sandbox-selftest                 # the matrix, against this Mac's real home; exit 0 pass / 1 fail / 2 inconclusive
appforge-agent sandbox-run --proxy-port 18888 -- /bin/bash -lc 'flutter pub get'   # one command under the same profile

The agent runs the same matrix before each regulated build, and, only when the operator opted the monitor in with APPFORGE_SANDBOX_MONITOR=1 (off by default; canary Mac first), at start and every 10 minutes too. It reports enforcementLevel: sandbox only while it passes. It is a regression suite for the bypass classes it lists, not a proof of confinement. APPFORGE_SANDBOX_DISABLED=1 is the operator kill switch: set it in the shell you run appforge-agent install-service from (it is baked into the launchd plist like APPFORGE_MAX_CONCURRENT_BUILDS; a hand edit of the plist is dropped by the next install). The matrix's unsandboxed controls briefly write to /opt/homebrew/bin and ~/Library/LaunchAgents, a throwaway preferences domain, and submit a launchd job, on every run. Procedure and caveats: docs/manual-sandbox-validation.md.

How a build clones

Every https build clones (and fetches its commitSha) with a hermetic git: no system or global git config, an empty $HOME, no askpass, no prompt, none of the agent's own GIT_*/SSH_* variables. A private GitHub repository gets the GitHub App's one-hour token in that environment only; every other build clones anonymously, so the Mac's own login (gh, the keychain, ~/.netrc) is never what clones a tenant's repository (appforge-ci#893). An ssh remote is unchanged. A self-hoster whose Mac serves one trusted org and who clones private repositories with that login can opt back in with APPFORGE_AMBIENT_GIT_CREDENTIALS=1 in the shell that runs install-service (baked into the plist like the knobs above); never on a Mac that runs other orgs' builds. docs/github-app.md.

Remote management, and why it's an allowlist

The dashboard can only ever send one of eight command types (DRAIN, RESUME, REBOOT_HOST, RESTART_AGENT, RUN_MAINTENANCE_SCRIPT, UPDATE_AGENT, CANCEL_BUILD, TEST_TUNNEL) — there is deliberately no "run arbitrary shell command" endpoint. UPDATE_AGENT drains the agent, waits out every in-flight build, then self-updates and re-execs; CANCEL_BUILD kills exactly one named build's process group among this agent's concurrent slots. See docs/fleet-management.md's command table for the full list. RUN_MAINTENANCE_SCRIPT further requires the script name to already exist in this machine's local ~/.appforge-agent/config.json under maintenanceScripts, e.g.:

{
  "...": "... (written by `enroll`)",
  "maintenanceScripts": {
    "clear-derived-data": "rm -rf ~/Library/Developer/Xcode/DerivedData/*",
    "update-cocoapods": "sudo gem install cocoapods",
    "prune-simulators": "xcrun simctl delete unavailable"
  }
}

REBOOT_HOST shells out to sudo /sbin/shutdown -r now; give the agent's user a sudoers entry scoped to exactly that command (see infra/mac-mini-setup/bootstrap.sh), not blanket sudo.

For anything beyond this allowlist — interactive debugging, installing a new Xcode version, watching a stuck build's screen — use a real remote desktop tool (Tailscale SSH, Apple Remote Desktop, or tailscale funnel/VNC). AppForge deliberately doesn't reimplement a remote shell or screen-share for the host machine; see docs/fleet-management.md. (The in-browser "Test in browser" feature is different: that streams the emulator/simulator screen for the built app, not the Mac mini's own desktop — see apps/device-stream/README.md.)

版本与发布记录

@appforge-ci/agent 共发布 36 个版本,首次发布于 2026/09/23;过去 12 个月发布了 36 个版本。

版本发布日期距今
0.3.29latest2026/10/2414 天前
0.3.282026/10/2315 天前
0.3.272026/10/2315 天前
0.3.262026/10/2315 天前
0.3.252026/10/1721 天前
0.3.242026/10/1722 天前

关于 @appforge-ci/agent 的常见问题

@appforge-ci/agent 是什么?

The build-runner daemon that turns a Mac mini into a fleet member. It:

如何安装 @appforge-ci/agent?

在项目目录运行 npm install @appforge-ci/agent,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 appforge-agent,可以用 npx appforge-agent 直接运行。

@appforge-ci/agent 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/appforge-ci__agent 是否存在。

@appforge-ci/agent 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@appforge-ci/agent 可以商用吗?

@appforge-ci/agent 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@appforge-ci/agent 的下载量有多少?

根据 npm 官方下载统计,@appforge-ci/agent 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,519 次,日均约 84 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@appforge-ci/agent 还在维护吗?

最新版本 0.3.29 发布于 2026/10/24(14 天前);至今共发布 36 个版本,其中过去 12 个月发布了 36 个;首次发布于 2026/09/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。