CI Machine Authentication
A CI runner — a GitHub Actions job, an E2E harness, any unattended environment — can authenticate to the Spec Kitty SaaS with no browser, no TTY, and no human approval step, using a machine credential instead of a person's OAuth session. This page is the CLI-side runbook; the server-side contract (identity, blast radius, scope vocabulary, issuance, revocation) lives in the SaaS repo's machine-credential work.
The three authentication modes
| Mode | Command | Credential | Who it is for |
|---|---|---|---|
| Developer OAuth (browser) | spec-kitty auth login |
Your SaaS user session, minted via Authorization Code + PKCE in a browser | A person at a workstation — the default |
| Device flow (headless) | spec-kitty auth login --headless |
Same human session, minted via RFC 8628 — still needs a human to open a browser and approve the device code | A person on SSH / a no-browser machine |
| Machine / CI | spec-kitty auth login --machine |
A ServicePrincipal client_id + client_secret exchanged via the OAuth client_credentials grant — no human anywhere in the loop |
CI runners, E2E harnesses, unattended automation |
The device flow is headless, not unattended: it still requires a person to approve. That is precisely the gap the machine mode closes.
Configuring a CI runner
The machine credential arrives as environment variables (the usual CI secret mechanism), and the target server is selected by the same variable every hosted surface reads:
export SPEC_KITTY_SAAS_URL=https://team.spec-kitty.ai # select the target
export SPEC_KITTY_MACHINE_CLIENT_ID=<principal client id>
export SPEC_KITTY_MACHINE_CLIENT_SECRET=<secret> # or use the file form below
spec-kitty auth login --machine
For runners that materialize secrets as files (mounted credential files,
gh secret file mounts), point at the file instead — its contents are read
once and stripped; the path wins over a stale inline value:
export SPEC_KITTY_MACHINE_CLIENT_SECRET_FILE=/run/secrets/machine_client_secret
After login, the credential is an ordinary stored session tagged
auth_method="client_credentials": every hosted command, the
TokenManager refresh path, and the Zeitgeist capability mint all work
unchanged, and the SaaS attributes every write to the machine principal in
its audit log.
What a runner can verify without a TTY
spec-kitty auth status— prints the auth mode (Machine / CI (Client Credentials Grant)) and session lifetime; exit 0 whether or not authenticated.spec-kitty auth doctor --json— deterministic JSON (schema v3) that now includessession.auth_method; contains no token or secret material, ever.spec-kitty auth whoami— exits 0/1 on authentication state alone.
Fail-closed behavior
A misconfigured machine login never degrades into a device-flow or browser prompt. Missing or blank credentials exit 1 with the exact variables to set; a rejected credential exits 1 with the operator remediation (check/re-provision the principal) — the server deliberately does not distinguish an unknown client id, a revoked principal, and a wrong secret, and the CLI mirrors that indistinguishability. The secret value is never printed, logged, or embedded in an error.
Provisioning, rotation, and revocation (server side)
A team admin provisions a scoped principal per environment — develop and
production get separate principals, each scoped to its target team and
to the sync:ingest / sync:read scope vocabulary:
# in the SaaS repo's environment
make manage ARGS='provision_service_principal <name> --team <slug> --scope sync:ingest,sync:read'
make manage ARGS='revoke_service_principal <client-id>'
The provisioning command prints the client secret once; it is stored
only as a hash server-side. To rotate, revoke the old principal, provision
a new one, and re-run spec-kitty auth login --machine --force on the
runner with the new pair. Revocation takes effect on the next request (the
SaaS test suite proves the still-signature-valid token is refused
immediately).
Security constraints
- The client secret is supplied via environment or secret file only —
never committed to a repository, never in
.kitty.env, never in test YAML, never in generated artifacts. - A personal user's refresh token is never the CI credential.
- The env-file doctor and secret-redaction allowlist treat
SPEC_KITTY_MACHINE_CLIENT_SECRETas secret-shaped: provisioning emits at most a commented blank template, diagnostics report name/presence only. - Every authenticated write is attributable to the machine principal in
the SaaS audit log (
service_principal_token_issued), so E2E writes are attributable and revocable as a unit.