8c4f0f8a
feat(agent): a Mac host installs the way a Mac does
a73x 2026-08-06 09:12
Commit message
docs/quickstart.md
| Old | New | ||
|---|---|---|---|
| @@ -57,20 +57,33 @@ here: vfkit only works carrying Apple's virtualization entitlement, and an | |||
| 57 | entitlement lives in a code signature. Homebrew's copy is signed. Without it, | 57 | entitlement lives in a code signature. Homebrew's copy is signed. Without it, |
| 58 | VMs placed on this host fail at once, saying so. | 58 | VMs placed on this host fail at once, saying so. |
| 59 | 59 | ||
| 60 | Take the darwin bundle instead of the linux one, run `eitri-agent join` with | 60 | Take the darwin bundle instead of the linux one. Nothing here needs `sudo`, and |
| 61 | the blob from **+ Add host**, then install the launchd job that rides with it: | 61 | nothing goes in a system directory: |
| 62 | 62 | ||
| 63 | ```sh | 63 | ```sh |
| 64 | sudo cp sh.eitri.agent.plist /Library/LaunchDaemons/ | 64 | mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/ |
| 65 | sudo chown root:wheel /Library/LaunchDaemons/sh.eitri.agent.plist | 65 | ~/.local/bin/eitri-agent join eitri_join_<blob-from-console> |
| 66 | sudo launchctl load -w /Library/LaunchDaemons/sh.eitri.agent.plist | 66 | ./eitri-agent-launchagent.sh install ~/.local/bin/eitri-agent |
| 67 | ``` | 67 | ``` |
| 68 | 68 | ||
| 69 | Set `UserName` in the plist to the account that owns the state directory before | 69 | State lands in `~/.eitri/agent`, logs in `~/Library/Logs/eitri-agent.log`, and |
| 70 | you load it. Unlike the Linux agent, this one does not run as root: on Linux | 70 | the agent starts with your session. |
| 71 | the agent owns the host's network—the bridge, the taps, `/dev/kvm`—and on macOS | 71 | |
| 72 | none of that is eitri's, because Virtualization.framework owns the guest | 72 | Keep the binary somewhere this account owns. The agent replaces its own binary |
| 73 | network and vfkit carries the entitlement for it. | 73 | when it upgrades, so a root-owned prefix like `/usr/local/bin` would look tidy |
| 74 | and quietly disable every future upgrade—the installer refuses rather than let | ||
| 75 | that happen. | ||
| 76 | |||
| 77 | Unprivileged throughout, because on macOS eitri owns none of the host's | ||
| 78 | networking: Virtualization.framework owns the guest network and vfkit carries | ||
| 79 | the entitlement for it. Contrast the Linux agent, which is root because it owns | ||
| 80 | the bridge, the taps, `/dev/kvm` and the nftables rules. | ||
| 81 | |||
| 82 | A **LaunchAgent**, not a LaunchDaemon, because a Mac host is nearly always | ||
| 83 | somebody's Mac and an agent runs where that person already is. For a dedicated | ||
| 84 | machine that must run guests with nobody logged in, move the generated plist to | ||
| 85 | `/Library/LaunchDaemons` and add a `UserName` key naming this account; leave the | ||
| 86 | binary and state where they are. | ||
| 74 | 87 | ||
| 75 | Apple Silicon runs **arm64 guests only**: the framework cannot emulate another | 88 | Apple Silicon runs **arm64 guests only**: the framework cannot emulate another |
| 76 | architecture. You do not have to remember that—`default_images` is keyed by | 89 | architecture. You do not have to remember that—`default_images` is keyed by |
| @@ -79,8 +92,9 @@ on a Mac. If you *do* name an image, name an arm64 image; nothing downstream | |||
| 79 | checks, and one built for the wrong architecture boots into nothing. | 92 | checks, and one built for the wrong architecture boots into nothing. |
| 80 | 93 | ||
| 81 | Guests get their addresses from macOS's own NAT rather than from the agent, so | 94 | Guests get their addresses from macOS's own NAT rather than from the agent, so |
| 82 | a Mac's guests sit on vmnet's subnet, not on the bridge CIDR the console shows. | 95 | a Mac's guests sit on vmnet's subnet. The host reports that subnet to the fleet, |
| 83 | Everything above that—`eitri ssh`, the console, reconcile—is the same. | 96 | so it is what the console shows—no allocation is made for a Mac and none is |
| 97 | used. Everything above that—`eitri ssh`, the console, reconcile—is the same. | ||
| 84 | 98 | ||
| 85 | ### Boot a VM | 99 | ### Boot a VM |
| 86 | 100 | ||
| @@ -302,8 +316,8 @@ The host goes **online** in the console. Logs: | |||
| 302 | `journalctl -u eitri-agent -f`. | 316 | `journalctl -u eitri-agent -f`. |
| 303 | 317 | ||
| 304 | A Mac joins the same fleet from the `darwin_arm64` bundle—`brew install vfkit` | 318 | A Mac joins the same fleet from the `darwin_arm64` bundle—`brew install vfkit` |
| 305 | first, then `eitri-agent join`, then the launchd job in place of the systemd | 319 | first, then `eitri-agent join`, then `eitri-agent-launchagent.sh install` in |
| 306 | unit. The differences are the same ones the hosted section covers above, and | 320 | place of the systemd unit. The differences are the same ones the hosted section covers above, and |
| 307 | the one extra thing self-hosting asks of you is an `arm64` entry in the | 321 | the one extra thing self-hosting asks of you is an `arm64` entry in the |
| 308 | server's `default_images`. | 322 | server's `default_images`. |
| 309 | 323 | ||
internal/agent/run/cli.go
| Old | New | ||
|---|---|---|---|
| @@ -92,7 +92,15 @@ func RunCLI(args []string) error { | |||
| 92 | // touched. | 92 | // touched. |
| 93 | func parseConfig(args []string) (Config, []string, error) { | 93 | func parseConfig(args []string) (Config, []string, error) { |
| 94 | fs := flag.NewFlagSet("eitri-agent", flag.ContinueOnError) | 94 | fs := flag.NewFlagSet("eitri-agent", flag.ContinueOnError) |
| 95 | stateDir := fs.String("state-dir", "/var/lib/eitri-agent", "agent state directory") | 95 | // The default is per-platform: a Linux host keeps state where its |
| 96 | // root-run unit can create it, a Mac in a dotdir the running account owns. | ||
| 97 | // See each wire_*.go — importing one platform's filesystem layout into the | ||
| 98 | // other is how a Mac ended up being told to create /var/lib by hand. | ||
| 99 | stateDirDefault, err := defaultStateDir() | ||
| 100 | if err != nil { | ||
| 101 | return Config{}, nil, err | ||
| 102 | } | ||
| 103 | stateDir := fs.String("state-dir", stateDirDefault, "agent state directory") | ||
| 96 | // --ch-bin/--firmware/--bootstrap-url configure the cloud-hypervisor | 104 | // --ch-bin/--firmware/--bootstrap-url configure the cloud-hypervisor |
| 97 | // backend and --vfkit-bin the macOS one. They are accepted everywhere and | 105 | // backend and --vfkit-bin the macOS one. They are accepted everywhere and |
| 98 | // ignored by platforms that don't run them, so the agent has one flag | 106 | // ignored by platforms that don't run them, so the agent has one flag |
internal/agent/run/wire_darwin.go
| Old | New | ||
|---|---|---|---|
| @@ -4,6 +4,9 @@ package run | |||
| 4 | 4 | ||
| 5 | import ( | 5 | import ( |
| 6 | "context" | 6 | "context" |
| 7 | "fmt" | ||
| 8 | "os" | ||
| 9 | "path/filepath" | ||
| 7 | 10 | ||
| 8 | "github.com/a73x/eitri/internal/agent/reconcile" | 11 | "github.com/a73x/eitri/internal/agent/reconcile" |
| 9 | "github.com/a73x/eitri/internal/agent/serialpump" | 12 | "github.com/a73x/eitri/internal/agent/serialpump" |
| @@ -15,6 +18,31 @@ import ( | |||
| 15 | // time — an opaque label the server stores and never interprets. | 18 | // time — an opaque label the server stores and never interprets. |
| 16 | const platformProvisioner = "vfkit" | 19 | const platformProvisioner = "vfkit" |
| 17 | 20 | ||
| 21 | // defaultStateDir is where a Mac agent keeps its state when nothing says | ||
| 22 | // otherwise: a dotdir in the running account's home, which is what the tools a | ||
| 23 | // Mac user already runs do (colima keeps ~/.colima, lima ~/.lima). Not | ||
| 24 | // /var/lib/eitri-agent, which is a Linux path an unprivileged account cannot | ||
| 25 | // create; and not ~/Library/Application Support, Apple's convention for | ||
| 26 | // application data, for a measured reason: vfkit's control socket lives under | ||
| 27 | // the state directory, macOS caps a unix socket path at 103 usable bytes, and | ||
| 28 | // | ||
| 29 | // /Users/<user>/Library/Application Support/eitri-agent/vms/<32-hex>/vfkit.sock | ||
| 30 | // | ||
| 31 | // is exactly 103 for an eight-character username. It would work for the author | ||
| 32 | // of this line and fail for the next person. The dotdir leaves 27 bytes of | ||
| 33 | // headroom, and Preflight still refuses outright rather than failing later if a | ||
| 34 | // deeper --state-dir spends them. | ||
| 35 | // | ||
| 36 | // An unresolvable home is not worth guessing at: the caller sees the error and | ||
| 37 | // the operator passes --state-dir. | ||
| 38 | func defaultStateDir() (string, error) { | ||
| 39 | home, err := os.UserHomeDir() | ||
| 40 | if err != nil { | ||
| 41 | return "", fmt.Errorf("locate home directory for the default --state-dir: %w", err) | ||
| 42 | } | ||
| 43 | return filepath.Join(home, ".eitri", "agent"), nil | ||
| 44 | } | ||
| 45 | |||
| 18 | // platform is this host's backend pair, mirroring wire_linux.go's contract: | 46 | // platform is this host's backend pair, mirroring wire_linux.go's contract: |
| 19 | // serve() is the sole consumer, wiring Prov and Pumps into the reconcile engine | 47 | // serve() is the sole consumer, wiring Prov and Pumps into the reconcile engine |
| 20 | // and the sync client. | 48 | // and the sync client. |
internal/agent/run/wire_linux.go
| Old | New | ||
|---|---|---|---|
| @@ -47,6 +47,11 @@ type platform struct { | |||
| 47 | GuestCIDR func() string | 47 | GuestCIDR func() string |
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | // defaultStateDir is where a Linux agent keeps its state when nothing says | ||
| 51 | // otherwise. The unit runs as root and the agent creates the directory itself, | ||
| 52 | // so there is nothing for an operator to pre-create. | ||
| 53 | func defaultStateDir() (string, error) { return "/var/lib/eitri-agent", nil } | ||
| 54 | |||
| 50 | // lastResortGuestCIDR is used only when nothing else produced one: no persisted | 55 | // lastResortGuestCIDR is used only when nothing else produced one: no persisted |
| 51 | // identity, no flag, and no suggestion from the control plane. It is a last | 56 | // identity, no flag, and no suggestion from the control plane. It is a last |
| 52 | // resort and not "the default" — a host that reaches it is a host the fleet | 57 | // resort and not "the default" — a host that reaches it is a host the fleet |
scripts/eitri-agent-launchagent.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,117 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # | ||
| 3 | # Install this Mac's eitri-agent as a LaunchAgent, so it starts with the | ||
| 4 | # session and is restarted if it crashes. The macOS counterpart of | ||
| 5 | # eitri-agent.service. | ||
| 6 | # | ||
| 7 | # ./eitri-agent-launchagent.sh install [/path/to/eitri-agent] | ||
| 8 | # ./eitri-agent-launchagent.sh uninstall | ||
| 9 | # ./eitri-agent-launchagent.sh status | ||
| 10 | # | ||
| 11 | # No privileges anywhere. A Mac runs guests through vfkit, which needs none, so | ||
| 12 | # the agent needs none either — and the two things every agent does need, a | ||
| 13 | # state directory it can create and a binary it can replace when it upgrades, | ||
| 14 | # are both satisfied by keeping them in the running account's own space. | ||
| 15 | # | ||
| 16 | # A LaunchAgent and not a LaunchDaemon, because a Mac host is almost always | ||
| 17 | # somebody's Mac. An agent runs in the login session, which is where the person | ||
| 18 | # using that machine already is. A dedicated always-on host that must run guests | ||
| 19 | # with nobody logged in wants a LaunchDaemon instead — same plist, moved to | ||
| 20 | # /Library/LaunchDaemons with a UserName key naming this account, and the | ||
| 21 | # binary and state left exactly where they are here. | ||
| 22 | # | ||
| 23 | # A generated plist rather than a shipped one: launchd does not expand ~ or any | ||
| 24 | # variable in ProgramArguments, so every path has to be absolute and resolved on | ||
| 25 | # the machine it will run on. | ||
| 26 | set -euo pipefail | ||
| 27 | |||
| 28 | LABEL="sh.eitri.agent" | ||
| 29 | PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" | ||
| 30 | LOG_DIR="$HOME/Library/Logs" | ||
| 31 | DOMAIN="gui/$(id -u)" | ||
| 32 | |||
| 33 | die() { printf 'error: %s\n' "$*" >&2; exit 1; } | ||
| 34 | |||
| 35 | cmd_install() { | ||
| 36 | local bin="${1:-}" | ||
| 37 | if [[ -z $bin ]]; then | ||
| 38 | bin="$(command -v eitri-agent || true)" | ||
| 39 | [[ -n $bin ]] || die "eitri-agent is not on PATH — pass its path: $0 install /path/to/eitri-agent" | ||
| 40 | fi | ||
| 41 | bin="$(cd "$(dirname "$bin")" && pwd)/$(basename "$bin")" | ||
| 42 | [[ -x $bin ]] || die "not executable: $bin" | ||
| 43 | |||
| 44 | # The agent replaces this file when it upgrades itself, so the account | ||
| 45 | # running it has to own it. A root-owned prefix like /usr/local/bin looks | ||
| 46 | # tidy and quietly disables every future upgrade. | ||
| 47 | [[ -w $bin ]] || die "$bin is not writable by $(id -un) — self-upgrade would fail. Keep the binary somewhere this account owns." | ||
| 48 | |||
| 49 | command -v vfkit >/dev/null || printf 'warning: vfkit is not on PATH; guests will fail Preflight until `brew install vfkit`\n' >&2 | ||
| 50 | |||
| 51 | mkdir -p "$(dirname "$PLIST")" "$LOG_DIR" | ||
| 52 | cat > "$PLIST" <<-PLIST | ||
| 53 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 54 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 55 | <plist version="1.0"> | ||
| 56 | <dict> | ||
| 57 | <key>Label</key> | ||
| 58 | <string>$LABEL</string> | ||
| 59 | <key>ProgramArguments</key> | ||
| 60 | <array> | ||
| 61 | <string>$bin</string> | ||
| 62 | </array> | ||
| 63 | <key>RunAtLoad</key> | ||
| 64 | <true/> | ||
| 65 | <!-- Restart a crash, but let a clean stop be a stop: the agent exits 0 | ||
| 66 | on SIGTERM, so bootout stays a stop rather than a fight. --> | ||
| 67 | <key>KeepAlive</key> | ||
| 68 | <dict> | ||
| 69 | <key>SuccessfulExit</key> | ||
| 70 | <false/> | ||
| 71 | </dict> | ||
| 72 | <key>ThrottleInterval</key> | ||
| 73 | <integer>5</integer> | ||
| 74 | <!-- Homebrew's directory is absent from a launchd job's default PATH, | ||
| 75 | and vfkit lives there. Without this the agent starts and then | ||
| 76 | refuses every create, correctly but avoidably. --> | ||
| 77 | <key>EnvironmentVariables</key> | ||
| 78 | <dict> | ||
| 79 | <key>PATH</key> | ||
| 80 | <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> | ||
| 81 | </dict> | ||
| 82 | <key>StandardOutPath</key> | ||
| 83 | <string>$LOG_DIR/eitri-agent.log</string> | ||
| 84 | <key>StandardErrorPath</key> | ||
| 85 | <string>$LOG_DIR/eitri-agent.log</string> | ||
| 86 | </dict> | ||
| 87 | </plist> | ||
| 88 | PLIST | ||
| 89 | plutil -lint "$PLIST" >/dev/null || die "generated plist is malformed: $PLIST" | ||
| 90 | |||
| 91 | # bootout first so install is idempotent; ignore "not loaded". | ||
| 92 | launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true | ||
| 93 | launchctl bootstrap "$DOMAIN" "$PLIST" | ||
| 94 | printf 'installed %s\n binary %s\n log %s/eitri-agent.log\n' "$PLIST" "$bin" "$LOG_DIR" | ||
| 95 | cmd_status | ||
| 96 | } | ||
| 97 | |||
| 98 | cmd_uninstall() { | ||
| 99 | # bootout, NOT `unload -w`: the -w flag writes to the persistent override | ||
| 100 | # database and leaves the label DISABLED, so every later load fails with an | ||
| 101 | # error that says nothing about why. | ||
| 102 | launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true | ||
| 103 | rm -f "$PLIST" | ||
| 104 | printf 'removed %s (state and binary left alone)\n' "$PLIST" | ||
| 105 | } | ||
| 106 | |||
| 107 | cmd_status() { | ||
| 108 | launchctl print "$DOMAIN/$LABEL" 2>/dev/null | grep -E '^\s+(state|pid|last exit code) ' || | ||
| 109 | printf 'not loaded\n' | ||
| 110 | } | ||
| 111 | |||
| 112 | case "${1:-}" in | ||
| 113 | install) shift; cmd_install "$@" ;; | ||
| 114 | uninstall) cmd_uninstall ;; | ||
| 115 | status) cmd_status ;; | ||
| 116 | *) sed -n '3,10p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;; | ||
| 117 | esac | ||
scripts/release.sh
| Old | New | ||
|---|---|---|---|
| @@ -1,7 +1,7 @@ | |||
| 1 | #!/usr/bin/env bash | 1 | #!/usr/bin/env bash |
| 2 | # Cross-compiled release artifacts for eitri.sh, into dist/<version>/: | 2 | # Cross-compiled release artifacts for eitri.sh, into dist/<version>/: |
| 3 | # eitri_<v>_linux_{amd64,arm64}.tar.gz host bundle: server+agent+systemd unit | 3 | # eitri_<v>_linux_{amd64,arm64}.tar.gz host bundle: server+agent+systemd unit |
| 4 | # eitri_<v>_darwin_arm64.tar.gz Mac host bundle: agent + launchd job | 4 | # eitri_<v>_darwin_arm64.tar.gz Mac host bundle: agent + LaunchAgent installer |
| 5 | # eitri-cli_<v>_<os>_<arch>.tar.gz client CLI (eitri) for linux+darwin | 5 | # eitri-cli_<v>_<os>_<arch>.tar.gz client CLI (eitri) for linux+darwin |
| 6 | # eitri-oidc_<v>_linux_{amd64,arm64}.tar.gz bundled OIDC issuer + its unit | 6 | # eitri-oidc_<v>_linux_{amd64,arm64}.tar.gz bundled OIDC issuer + its unit |
| 7 | # (optional sidecar; not in manifest.json) | 7 | # (optional sidecar; not in manifest.json) |
| @@ -62,7 +62,10 @@ for arch in amd64 arm64; do | |||
| 62 | cp "$stage/$bundle/eitri-agent" "$OUT/eitri-agent_linux_${arch}" | 62 | cp "$stage/$bundle/eitri-agent" "$OUT/eitri-agent_linux_${arch}" |
| 63 | done | 63 | done |
| 64 | 64 | ||
| 65 | # macOS host bundle: the agent and its launchd job. arm64 ONLY, and that is a | 65 | # macOS host bundle: the agent and the script that installs it as a LaunchAgent. |
| 66 | # A generated plist rather than a shipped one, because launchd expands nothing in | ||
| 67 | # ProgramArguments — every path must be absolute and resolved on the machine that | ||
| 68 | # will run it. arm64 ONLY, and that is a | ||
| 66 | # claim about what has booted a guest, not about what compiles — a darwin/amd64 | 69 | # claim about what has booted a guest, not about what compiles — a darwin/amd64 |
| 67 | # binary would put an upgrade button in front of an Intel Mac we have never run. | 70 | # binary would put an upgrade button in front of an Intel Mac we have never run. |
| 68 | # Without one, such a host gets "no eitri-agent artifact for darwin/amd64", | 71 | # Without one, such a host gets "no eitri-agent artifact for darwin/amd64", |
| @@ -79,7 +82,7 @@ mkdir -p "$mac_stage/$mac_bundle" | |||
| 79 | echo "==> building darwin/arm64" | 82 | echo "==> building darwin/arm64" |
| 80 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags "$LDFLAGS" \ | 83 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags "$LDFLAGS" \ |
| 81 | -o "$mac_stage/$mac_bundle/eitri-agent" ./cmd/eitri-agent | 84 | -o "$mac_stage/$mac_bundle/eitri-agent" ./cmd/eitri-agent |
| 82 | cp scripts/sh.eitri.agent.plist "$mac_stage/$mac_bundle/" | 85 | cp scripts/eitri-agent-launchagent.sh "$mac_stage/$mac_bundle/" |
| 83 | tar -C "$mac_stage" -czf "$OUT/$mac_bundle.tar.gz" "$mac_bundle" | 86 | tar -C "$mac_stage" -czf "$OUT/$mac_bundle.tar.gz" "$mac_bundle" |
| 84 | cp "$mac_stage/$mac_bundle/eitri-agent" "$OUT/eitri-agent_darwin_arm64" | 87 | cp "$mac_stage/$mac_bundle/eitri-agent" "$OUT/eitri-agent_darwin_arm64" |
| 85 | 88 | ||
scripts/sh.eitri.agent.plist
| Old | New | ||
|---|---|---|---|
| @@ -1,72 +0,0 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- | ||
| 3 | eitri-agent launchd job — the macOS counterpart of eitri-agent.service. | ||
| 4 | Install on each Mac host: | ||
| 5 | |||
| 6 | sudo cp sh.eitri.agent.plist /Library/LaunchDaemons/ | ||
| 7 | sudo chown root:wheel /Library/LaunchDaemons/sh.eitri.agent.plist | ||
| 8 | sudo launchctl load -w /Library/LaunchDaemons/sh.eitri.agent.plist | ||
| 9 | |||
| 10 | A LaunchDaemon, not a LaunchAgent: a fleet host has to run its guests without | ||
| 11 | anyone logged in, and a LaunchAgent only exists inside a login session. | ||
| 12 | |||
| 13 | UserName is deliberate, and the opposite of the Linux unit's User=root. The | ||
| 14 | Linux agent needs root because it owns the host's network — /dev/kvm and | ||
| 15 | /dev/net/tun, the bridge, the taps, nftables, DHCP on :67. On macOS none of | ||
| 16 | that is eitri's: Virtualization.framework owns the guest network through | ||
| 17 | vmnet, and vfkit carries the entitlement that grants it. Set UserName to the | ||
| 18 | account that owns the state directory and has vfkit on its PATH. | ||
| 19 | |||
| 20 | KeepAlive replaces Restart=on-failure. Agent self-upgrades never need it — | ||
| 21 | the agent re-execs in place and keeps its PID, so launchd sees one | ||
| 22 | uninterrupted job. SuccessfulExit=false restarts a crash but not a clean | ||
| 23 | stop, so `launchctl unload` stays a stop rather than a fight. | ||
| 24 | |||
| 25 | There is no macOS equivalent of KillMode=process, and none is needed: launchd | ||
| 26 | does not place children in a job-wide kill group the way a systemd cgroup | ||
| 27 | does, so stopping the agent leaves running guests alone. Guests survive an | ||
| 28 | agent restart on macOS for the same reason they do on Linux — the agent | ||
| 29 | re-adopts them by boot-scoped pidfile. | ||
| 30 | --> | ||
| 31 | <plist version="1.0"> | ||
| 32 | <dict> | ||
| 33 | <key>Label</key> | ||
| 34 | <string>sh.eitri.agent</string> | ||
| 35 | |||
| 36 | <key>ProgramArguments</key> | ||
| 37 | <array> | ||
| 38 | <string>/usr/local/bin/eitri-agent</string> | ||
| 39 | <string>--state-dir</string> | ||
| 40 | <string>/usr/local/var/lib/eitri-agent</string> | ||
| 41 | </array> | ||
| 42 | |||
| 43 | <!-- The account that owns the state directory. Guests run as this user. --> | ||
| 44 | <key>UserName</key> | ||
| 45 | <string>CHANGEME</string> | ||
| 46 | |||
| 47 | <key>RunAtLoad</key> | ||
| 48 | <true/> | ||
| 49 | <key>KeepAlive</key> | ||
| 50 | <dict> | ||
| 51 | <key>SuccessfulExit</key> | ||
| 52 | <false/> | ||
| 53 | </dict> | ||
| 54 | <key>ThrottleInterval</key> | ||
| 55 | <integer>5</integer> | ||
| 56 | |||
| 57 | <!-- Homebrew's bin directory is not on a LaunchDaemon's default PATH, and | ||
| 58 | vfkit lives there. Without this the agent starts and then refuses every | ||
| 59 | create in Preflight, which is correct but avoidable. Apple silicon | ||
| 60 | Homebrew is /opt/homebrew; an Intel install is /usr/local. --> | ||
| 61 | <key>EnvironmentVariables</key> | ||
| 62 | <dict> | ||
| 63 | <key>PATH</key> | ||
| 64 | <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> | ||
| 65 | </dict> | ||
| 66 | |||
| 67 | <key>StandardOutPath</key> | ||
| 68 | <string>/usr/local/var/log/eitri-agent.log</string> | ||
| 69 | <key>StandardErrorPath</key> | ||
| 70 | <string>/usr/local/var/log/eitri-agent.log</string> | ||
| 71 | </dict> | ||
| 72 | </plist> | ||