Skip to content

2026-05-13-host-installation-auto-discovery


title: Host auto-discovers GitHub App installations date: 2026-05-13 status: ratified supersedes: [“docs/decisions/2026-05-13-host-multi-installation-app-auth.md”] mr_compliance: [MR-7, MR-13]

Section titled “title: Host auto-discovers GitHub App installations date: 2026-05-13 status: ratified supersedes: [“docs/decisions/2026-05-13-host-multi-installation-app-auth.md”] mr_compliance: [MR-7, MR-13]”

Earlier on 2026-05-13 we shipped a manual PETROVA_APP_INSTALLATIONS JSON env var to let the host federate App auth across orgs (docs/decisions/2026-05-13-host-multi-installation-app-auth.md). Within hours that approach hit its predicted ceiling: aligning a second org (stratt-hq) meant another env edit on Vercel and another redeploy. The original ADR explicitly flagged “Reserved as a follow-up if env-var sprawl becomes painful.” We hit the condition.

Alternative paths we considered and rejected before landing this:

  • Transferring ownership of consumer repos (skyflow-me, stratt-hq, kahn-hq…) to the petrova-codes org would unify auth under a single installation, but at the cost of: URL breakage for every external consumer, brand/identity collapse against the deliberate *-hq separation, detachment of org-scoped secrets/runners/branch-protection, and absurd scaling (skyflow-me alone owns ~25 repos). The orgs are kept separate by design — they are products governed by petrova, not parts of it.
  • Continuing with the env-var map would force a config edit and redeploy for every new org. That is the friction we are deleting.

host/src/github-auth.ts now performs runtime installation auto-discovery for the Petrova-act GitHub App. On first auth resolution after process start, the host mints an App JWT (no installation id), calls GET /app/installations, and caches account.login → installation_id for 1 hour. resolveAuthForOwner(owner) consults the cache first; if the owner is unmapped, it falls back to the existing PETROVA_APP_INSTALLATIONS env var, then to PETROVA_APP_INSTALLATION_ID. Cache eviction is TTL-based; an in-flight discovery promise is deduplicated so concurrent first-touches share one API call.

Operational consequence: onboarding a new org is now “install the Petrova-act App on it.” No env edit, no code change, no redeploy. The two fallback env vars remain in the resolver as a safety net for the cases auto-discovery can’t cover — App without metadata:read on a given installation, or air-gapped local dev.

resolveAuthForOwner and makeOctokitForOwner are now async (the discovery call must be awaited). The three federating sources — acts.ts, audit.ts, eva.ts — await the per-owner Octokit at the point they parse the registry URL. The synchronous resolveAuth() / makeOctokit() API is retained for tests and any caller that can’t await; it skips discovery and reads only env.

The PETROVA_APP_INSTALLATIONS env var I set on Vercel in the previous ADR has been removed — auto-discovery covers it.

  • Static env-var map (the prior ADR’s choice) — superseded. Required a config edit and redeploy per new org; the maintenance burden compounds as the fleet grows.
  • Transfer repos to petrova-codes org — rejected for the four reasons listed in the context (URL breakage, identity collapse, secret detachment, no scaling story).
  • Discover once at module-load with a top-level await — module-load can’t await in CommonJS bundles and complicates serverless cold-start telemetry. Lazy first-call discovery with promise-deduplication achieves the same effect with a simpler shape.

For code: host/src/github-auth.ts adds discoverInstallations() (cached, deduplicated). resolveAuthForOwner / makeOctokitForOwner are now async; acts.ts, audit.ts, eva.ts await at the call site. No new package deps — @octokit/auth-app already supports App-JWT mode.

For ops: PETROVA_APP_INSTALLATIONS and PETROVA_APP_INSTALLATION_ID become optional fallbacks. Removing them from Vercel is allowed once auto-discovery is verified live. The Petrova-act App needs metadata:read on each installation (it already does — that’s the same permission it uses for the read tools today).

For phases: Unblocks any new org joining the fleet (stratt-hq, future onboards) without per-org plumbing.

For invariants: act = signed verb PR unchanged. Acts still surface only when a petrova:metadata block is present on a PR; auto-discovery only affects whether the host can see those PRs.

  • npm --prefix host run typecheck — pass
  • npm --prefix host test — 74/74 pass
  • Production: petrova-host deployment after this commit will exercise discovery on the first /console/acts request; expected behavior is acts appearing for any org with the App installed, with no env-var dependency.