Three Macs and six accounts were done by hand, and the fourth would have repeated every mistake the first three taught. This script carries them. Each guard is something a hand-run got wrong first: - an account may not own its own home. A `sudo mkdir` before sysadminctl leaves /Users/<account> root-owned; the account then authenticates, gets a shell, has a correct $HOME and cannot write to it. Surfaced on the Studio as a bare "Permission denied" hours after the account looked fine. - `sudo -u` keeps the CALLER's $HOME. Without -H the install's rm -rf aims at the wrong account — it did, at a working install, and only permissions stopped it. The remote half refuses to run unless $HOME matches the target. - the provider ships a hard-coded model catalog that the web GUI reads INDEPENDENTLY of agent-default-model, so a correct default still showed DeepSeek models in the picker. `models:` replaces it. - reasoningEffort / maxTokens / defaultContextWindow are all measured against the seat; the harness defaults fail on every one. - the key is scoped per machine and the scope is VERIFIED (200 on gen-reasoning, 403 on gen), not trusted from the mint. The first run found two more: it named the vault item after the IP (`mac-10-0-10-10/`, unreadable beside esh-mac-studio) and its config check used grep -A3 where the block needs -A4, so it printed an empty model and passed anyway. Both fixed, and verification now asserts the model rather than only the answer token — a check that cannot fail is not a check. Run twice against the same account to confirm idempotence, then against vhpfi. docs/runbooks/mac-provisioning.md carries the operator-run stage and the traps that are not the script's to solve.
4.4 KiB
Provisioning a Mac for the fleet
Three Macs are provisioned this way as of 2026-09-02: vuongs-mac-mini
(10.100.79.2), esh-macbook-air (10.0.10.83), esh-mac-studio (10.0.10.10).
None is in servers/ or dns/internal.yaml — they are the operator's personal
machines, not PFI-managed fleet hosts, and registering them there would imply
otherwise.
Two stages
Stage 1 — an account I can reach. Operator-run, because it needs a password I do not have. See "Operator steps" below.
Stage 2 — the harness. scripts/provision-mac-dsh.sh <host> <account> [name],
idempotent, run once per account.
scripts/provision-mac-dsh.sh 10.0.10.10 vhpfi esh-mac-studio
scripts/provision-mac-dsh.sh --check 10.0.10.10 vhpfi
Operator steps (stage 1)
sudo sysadminctl -addUser infra-ops -fullName "PFI infra-ops" \
-shell /bin/zsh -home /Users/infra-ops -password -
sudo dseditgroup -o edit -a infra-ops -t user admin
sudo mkdir -p /Users/infra-ops/.ssh
sudo tee /Users/infra-ops/.ssh/authorized_keys >/dev/null <<'KEY'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN+1HBwfXrkfTYWdcnWCjLJ6VLAGC87gxH5h5vKaaA3c infra-ops@pfi-fleet
KEY
sudo chown -R infra-ops:staff /Users/infra-ops/.ssh
sudo chmod 700 /Users/infra-ops/.ssh; sudo chmod 600 /Users/infra-ops/.ssh/authorized_keys
Then hand over a throwaway password; infra-ops rotates it, vaults it as
<name>/infra-ops-password, and installs a visudo-validated NOPASSWD drop-in.
⚠ Install the key and prove key-auth on a FRESH connection BEFORE touching the password. These machines have no out-of-band access — a failed rotation means the operator walks to the machine.
⚠ Traps, all of them paid for
An account may not own its own home. If /Users/<account> was created by a
sudo mkdir before sysadminctl ran, sysadminctl adopts the existing directory
and leaves it root-owned. The account then authenticates, gets a shell, has a
correct $HOME, and cannot write to it — surfacing as a bare Permission denied
from mkdir long after the account looked healthy. The script detects and fixes
this; a hand-run will not.
sudo -u <user> keeps the CALLER's $HOME. Without -H and an explicit
HOME=, "$HOME/.local" resolves to the caller's home. On 2026-09-02 that
pointed an rm -rf at a working install in another account; only filesystem
permissions stopped it. The remote half of the script refuses to run unless
$HOME matches the target account.
A wrong username looks exactly like a wrong password. sshd answers
Permission denied (publickey,password,keyboard-interactive) for a bad user, a
bad password, AND a user outside com.apple.access_ssh. Two of the three Macs
produced a false diagnosis this way. Check dscl . -list /Users first — the
Studio's operator account is vhpfi, not lkraven.
com.apple.access_ssh gates SSH when it exists, but admins usually pass
anyway through a nested group. Do not create the group if absent: doing so
flips SSH from open-to-all to members-only and can lock out the operator.
Password rotation: use dscl . -passwd, not sysadminctl. With FileVault on
and no Secure Token on the account, sysadminctl -resetPasswordFor refuses with
"Operation is not permitted without secure token unlock". dscl works
precisely because there is no token to desync. True on all three Macs.
⚠ FileVault kills remote access across reboots. The machine sits at the pre-boot unlock screen with no network until someone unlocks it physically. Nothing unattended should depend on a Mac being reachable after a restart.
macOS has no adduser, useradd, or timeout. Use sysadminctl, and wrap
the ssh call locally rather than reaching for a remote timeout.
Harness specifics
Node is installed private to the account under ~/.local/node,
checksum-verified — deliberately not Homebrew, which owns /opt/homebrew and
edits PATH. One device-scoped gateway key per machine (<name>-dsh, scoped
to gen-reasoning), shared by that machine's accounts and vaulted at
<name>/litellm-dsh-key; the script verifies the scope (200 on gen-reasoning,
403 on gen) rather than trusting the mint.
See stacks/litellm/README.md for why reasoningEffort: high works at all, and
scripts/provision-mac-dsh.sh for the three measured limits (reasoningEffort,
maxTokens, defaultContextWindow) and the hard-coded model catalog the web
GUI reads independently of the default model.