Install macOS helper from PKG for XPC enroll

Browser enrollment used osascript on every elevate. Ship a signed
privileged helper installed at PKG time so probo:// can enroll over
XPC with no second admin prompt. Add make install/uninstall/clean for
local PKG test loops, and show alerts only on failure.

Mirror the Go lint path for the macOS SPM package: Make
targets, root configs, and a Linux CI job. Keep checks
syntax-only so they do not need a macOS SDK. Format the
existing sources so the new gates start clean.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-07-20 18:18:04 +02:00
parent 754d12d583
commit 85864a580c
42 changed files with 1903 additions and 341 deletions

View File

@@ -14,8 +14,10 @@ The project uses a `GNUmakefile` at the root. Builds run with `--jobs=$(nproc)`
| `make test-short` | Short tests only |
| `make test-bench` | Run benchmarks |
| `make test-e2e` | Run console end-to-end tests (requires `bin/probod`) |
| `make lint` | Run all linters: `vet` + `go-fmt` + `go-fix` + `go-lint` + `lint-js` |
| `make fmt` | Format Go code (`go fmt ./...`) |
| `make lint` | Run Go + JS linters: `vet` + `go-fmt` + `go-fix` + `go-lint` + `lint-js` |
| `make lint-swift` | Opt-in: lint Swift enroll-ui (`swift-fmt` + `swift-lint`; needs Swift + SwiftLint; CI runs this on Linux) |
| `make fmt` | Format Go code |
| `make fmt-swift` | Opt-in: format Swift enroll-ui (`swift format` + SwiftLint `--fix`; needs Swift) |
| `make clean` | Remove all build artifacts, `node_modules`, generated files, and coverage |
| `make help` | List targets with `##` doc comments |
@@ -81,3 +83,6 @@ Individual codegen is driven by `go generate`:
| `GOOS` | (host) | Cross-compile target OS |
| `TEST_FLAGS` | `-race -cover -coverprofile=coverage.out` | Extra flags passed to `go test` |
| `DOCKER_BUILD_FLAGS` | (empty) | Extra flags for `docker build` |
| `SWIFTLINTCMD` | `swiftlint` | SwiftLint binary |
| `SWIFTCMD` | `swift` | Swift toolchain binary (`swift format`) |
| `SWIFT_ENROLL_UI` | `cmd/probo-agent/installer/macos/enroll-ui` | Path to the Swift SPM package |

View File

@@ -28,9 +28,9 @@ If empty or non-user-facing only, do not release this track.
make bin/probo-agent
```
On macOS and Windows hosts, `make` enables CGO automatically so the
menu bar / tray enrollment helper is included. Linux and FreeBSD builds
stay pure Go (no tray).
On macOS hosts, `make` enables CGO so the menu bar enrollment helper
is included. Windows tray support is pure Go (`CGO_ENABLED=0`). Linux
and FreeBSD builds stay pure Go (no tray).
## Notes
@@ -45,27 +45,34 @@ verifies the cosign bundle before installing.
The menu bar / tray enrollment flow is **macOS and Windows only**.
Linux and FreeBSD use `probo-agent install --server …
--enrollment-token …` from the shell, or the curl-to-sh installer
documented below. Windows release binaries are
cross-compiled from Linux with MinGW (CGO). macOS release binaries and
the `.pkg` are built on macOS with `CGO_ENABLED=1`.
documented below. Windows release binaries are cross-compiled from
Linux with `CGO_ENABLED=0` (tray is pure Go). macOS release binaries
and the `.pkg` are built on macOS with `CGO_ENABLED=1`.
### macOS `.pkg` (MDM / GUI install)
Release and local builds use
`cmd/probo-agent/installer/macos/build.sh` (requires macOS, a
pre-built binary — preferably universal via `lipo` — and the Swift
toolchain). The script compiles `Probo Agent.app` (the headless
`probo://` URL handler) from
`cmd/probo-agent/installer/macos/enroll-ui/`, signs the binary and app
when `CODESIGN_IDENTITY` is set, signs the product with
`INSTALLER_IDENTITY`, and notarizes/staples when
`NOTARYTOOL_KEYCHAIN_PROFILE` is set, or when `APPLE_ID`,
`APPLE_ID_PASSWORD`, and `APPLE_TEAM_ID` are set (password is stored
into a keychain profile; submits use `--keychain-profile` so the secret
is not on `notarytool submit` argv).
toolchain). **Signing is mandatory:** `CODESIGN_IDENTITY` and
`APPLE_TEAM_ID` must be set. The script compiles `Probo Agent.app`
(the headless `probo://` URL handler + privileged helper) from
`cmd/probo-agent/installer/macos/enroll-ui/`, signs the binary and
app, optionally signs the product with `INSTALLER_IDENTITY`, and
notarizes/staples when `APPLE_ID` and `APPLE_ID_PASSWORD` are set
(password is stored into a keychain profile; submits use
`--keychain-profile` so the secret is not on `notarytool submit` argv).
There is no unsigned PKG path and no osascript elevation fallback.
Local testing of browser enrollment requires a Developer ID–signed
build. CLI enrollment without the app uses `sudo probo-agent install`.
```shell
# Local unsigned universal pkg (example)
# Local signed pkg (example)
export CODESIGN_IDENTITY="Developer ID Application: Probo Inc (TEAMID)"
export INSTALLER_IDENTITY="Developer ID Installer: Probo Inc (TEAMID)"
export APPLE_TEAM_ID="TEAMID"
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -o dist/probo-agent_arm64 ./cmd/probo-agent
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -o dist/probo-agent_amd64 ./cmd/probo-agent
lipo -create dist/probo-agent_arm64 dist/probo-agent_amd64 -output dist/probo-agent_universal
@@ -75,11 +82,29 @@ cmd/probo-agent/installer/macos/build.sh \
--version "$(cat cmd/probo-agent/VERSION)"
```
PKG postinstall always installs the global tray LaunchAgent and
registers `probo://`. The LaunchDaemon for `probo-agent run` is created
only after enrollment (`probo-agent install`, deep link, or MDM
PKG postinstall always installs the global tray LaunchAgent, registers
`probo://`, and installs the privileged helper
(`com.probo.agent.helper`) under `/Library/PrivilegedHelperTools` as
root. The only admin authentication is the normal macOS Installer
prompt for the PKG itself. The LaunchDaemon for `probo-agent run` is
created only after enrollment (`probo-agent install`, deep link, or MDM
`/tmp/probo-agent.conf`).
Browser enrollment uses `Probo Agent.app` over XPC to the
PKG-installed helper — no SMJobBless and no admin prompt on the enroll
path. A missing or dead helper surfaces an error asking to reinstall
the package.
Manual QA checklist (macOS PKG):
1. Fresh signed PKG: Installer may ask for admin once; deep link enrolls with no further prompt.
2. Repeat deep link on an enrolled device: no prompt, immediate success.
3. After `sudo make -C cmd/probo-agent uninstall`, deep link fails with a clear “reinstall package” error (no osascript).
4. `sudo make -C cmd/probo-agent uninstall` removes daemon, tray, helper, app, and state.
5. MDM `/tmp/probo-agent.conf` postinstall still enrolls without a browser prompt.
6. Notarized PKG passes Gatekeeper; `codesign --verify --deep` succeeds on the app bundle.
7. Unsigned `build-app.sh` / `build.sh` exits with an error requiring `CODESIGN_IDENTITY`.
### Apple signing secrets (GitHub)
The `build-macos` job in `release-probo-agent.yaml` expects the same
@@ -97,24 +122,23 @@ the probo GitHub repository (or org) before tagging a release:
| `APPLE_ID_PASSWORD` | App-specific password (stored into a keychain profile; not passed to `submit`) |
| `APPLE_TEAM_ID` | 10-character Team ID |
Local notarization can reuse a pre-stored profile instead of putting the
password in the environment:
Local notarization uses the same env vars as CI:
```shell
xcrun notarytool store-credentials probo-agent-notary \
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID"
# prompts for the app-specific password once
export NOTARYTOOL_KEYCHAIN_PROFILE=probo-agent-notary
export APPLE_ID="you@example.com"
export APPLE_ID_PASSWORD="app-specific-password"
# optional: NOTARYTOOL_KEYCHAIN_PROFILE=probo-agent-notary (default)
```
Windows enrollment is browser-driven: the console issues a
`probo://enroll?server=...&token=...` deep link handled by
`probo-agent enroll-url`. After install, register the protocol for the
`Probo Agent.app` on macOS (PKG-installed helper + XPC) or
`probo-agent enroll-url` on Windows. After install, register the protocol for the
current user with
`cmd/probo-agent/installer/windows/register-protocol.ps1` (per-user
`HKCU` handler pointing at `probo-agent.exe`). The system tray helper
(`probo-agent tray`, CGO build on local Windows hosts) shows enrollment
status; enrollment itself happens in the browser.
(`probo-agent tray`) shows enrollment status; enrollment itself happens
in the browser.
Region labels and console URLs for the macOS installer HTML live in
`cmd/probo-agent/installer/regions.json`. A Go test keeps US/EU URLs in
@@ -165,34 +189,32 @@ Environment variables:
Never pass the enrollment token in the curl URL.
## Local dev install
## Local install (macOS PKG)
Build, package, and install a dev binary with `sudo`. Agent state is stored
under `~/.local/share/probo-agent-dev` (`--dir`); release archives are staged
under `~/.cache/probo-agent-dev` so the two trees do not overlap.
Primary loop for testing browser enrollment (app + privileged helper):
```shell
make -C cmd/probo-agent install \
export CODESIGN_IDENTITY="Developer ID Application: … (TEAMID)"
export INSTALLER_IDENTITY="Developer ID Installer: … (TEAMID)" # optional
export APPLE_TEAM_ID="TEAMID"
make -C cmd/probo-agent install # uninstall leftovers → build PKG → installer
make -C cmd/probo-agent uninstall # full system teardown (idempotent)
make -C cmd/probo-agent clean # uninstall + wipe dist/ caches / enroll-ui/.build
```
`install` always runs `uninstall` first so previous helpers, apps, and
Launch Services registrations cannot shadow the new build. Signing env
vars are required for `pkg` / `install` on Darwin.
### CLI-only install (no app / helper)
Binary-only path via `installer/install.sh` (any Unix). State under
`~/.local/share/probo-agent-dev`; staging under `~/.cache/probo-agent-dev`.
```shell
make -C cmd/probo-agent install-cli \
PROBO_SERVER_URL=https://us.probo.com \
PROBO_ENROLLMENT_TOKEN='…'
```
This does not compile Go in `cmd/probo-agent`; it reuses `bin/probo-agent` from
the root `GNUmakefile` (via `make bin/probo-agent`, invoked automatically when
needed), stages it into a local release archive, then runs `installer/install.sh`
against that `file://` archive with `PROBO_AGENT_RELEASE_TAG=probo-agent/dev`,
skips checksum verification, installs the binary to `/usr/local/bin/probo-agent`,
and passes `--skip-service` and `--dir ~/.local/share/probo-agent-dev` by default
(`INSTALL_ARGS` overrides).
Run the agent in the foreground against the same dev state directory:
```shell
make -C cmd/probo-agent run
```
Remove dev artifacts:
```shell
make -C cmd/probo-agent clean
```