feat(scripts): provision-mac-dsh.sh — one script for a Mac, end to end
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.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# 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)
|
||||
|
||||
```zsh
|
||||
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.
|
||||
Reference in New Issue
Block a user