Skip to content

feat(ci): mint the gateway token from a GitHub identity token - #1242

Draft
brandonleung wants to merge 6 commits into
mainfrom
brandon/wizard-ci-oidc
Draft

feat(ci): mint the gateway token from a GitHub identity token#1242
brandonleung wants to merge 6 commits into
mainfrom
brandon/wizard-ci-oidc

Conversation

@brandonleung

@brandonleung brandonleung commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Wizard CI in wizard-workbench and this repo's smoke test authenticate the gateway mint with a personal API key, which the mint refuses. They pass only because --ci falls back to the legacy gateway, and posthog#96861 turns that off.

Changes

Adds an opt-in GitHub Actions identity for the mint. Nothing changes until a workflow sets WIZARD_CI_IDENTITY=github-actions.

  • src/lib/ci-identity.ts asks GitHub for a fresh identity token before every mint, renewals included, because each token is single-use and lives five minutes.
  • The runner's request pair leaves process.env when the module loads and is only sent over https to *.actions.githubusercontent.com, with redirects refused.
  • A CI identity never falls back to the legacy gateway, and an unknown opt-in value fails the run.
  • A renewal that fails or is throttled keeps the live capped token and retries three times with a doubling wait. gateway mint refused gains a renewal flag.

The name avoids the POSTHOG_WIZARD_ prefix, which yargs' strict env parsing rejects for unknown names. No workflow opts in here.

Test plan

Why it is safe: the CLI only ever holds a capped phe_ token, and the request pair goes only to GitHub's token service.

  • Vitest on the touched files pins:
    • the host guard and the request timeout
    • capture at import
    • renewals: joiners, every refusal status, and the retry schedule through expiry
  • eslint is clean, and typecheck matches origin/main (29 pre-existing errors).
  • The request code, run on GitHub-hosted ubuntu and macOS runners, received a 300-second token that a local Django verified and minted from.

LLM context

Authored by Claude Code (Opus 5); requires human review.

The smoke test authenticates the mint with a personal API key, which the mint
refuses; the run only passes because it falls back to the legacy gateway, and
that fallback is going away.

The workflow now asks GitHub for an identity token and passes it as
POSTHOG_WIZARD_GATEWAY_TOKEN. The mint reads that bearer when it is set, so CI
receives the same capped, program-pinned token a user's run gets. The personal
key stays for API calls and for the legacy fallback, which must never see the
identity token.

The smoke-test step blanks the identity-token request variables, so the
model-written code it runs in the sandbox cannot ask for tokens of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a Context Mill branch:

  • /wizard-ci all context-mill:my-branch

Add context-mill:<branch> to any command above to pin the Context Mill branch. It defaults to main.

Results will be posted here when complete.

brandonleung and others added 3 commits September 10, 2026 00:23
A step-level env: block does not remove ACTIONS_ID_TOKEN_REQUEST_URL or
ACTIONS_ID_TOKEN_REQUEST_TOKEN; the runner sets them back afterwards. The smoke
test runs model-written code, and those two are permission to ask GitHub for a
token naming any audience, so the shell drops them itself instead.

The agent subprocess never needed them either, nor the minted identity token:
all three join the host-only denylist that already strips the orchestration
values.

The minted log line now names which identity minted, so a CI run is separable
from a user run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ut it

The token was requested in its own workflow step, then aged through the build,
pack and install before the mint read it. GitHub does not document how long one
lives. The script now asks for it immediately before the wizard runs and drops
the request variables before starting it, so the workflow step no longer needs
to blank anything.

A CI run whose mint refuses no longer falls back to the legacy gateway. The
refusal a broken identity path produces is a 401, which the fallback admits, so
a green smoke test could have hidden it while spending on the gateway this work
exists to stop using. A run without id-token: write fails with that reason for
the same purpose.

The subprocess denylist takes the whole ACTIONS_ID_TOKEN_REQUEST namespace
rather than the two names that exist today, and the minted log line names which
identity minted rather than re-deriving it by comparing two secrets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The missing-permission branch said the run would quietly spend on the legacy
path. A CI run that cannot mint now fails instead, so the comment names what
failing there buys rather than what it prevents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brandonleung and others added 2 commits September 10, 2026 16:03
Opt in with WIZARD_CI_IDENTITY=github-actions. The request pair leaves the
environment at import, and a renewal that fails for availability or is
throttled keeps the live token with bounded retries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants