feat(anr): add CI token-based authentication mode - #372
Merged
Conversation
Implements non-interactive token auth for ANR (issue #310). Shared logic lives in anr-core to serve both the CLI and desktop sidecar without duplicating the auth branch. - Add token-auth.ts to anr-core: parseANRAuthMode, validateTokenModeEnv, resolveTokenModeCredentials with static-creds and exchange paths - Branch initializeANR() in index.ts and anr-boot.ts on OPENCODE_ANR_AUTH_MODE (interactive default, token for CI) - Refresh closure: no interactive fallback in token mode; warn and continue until STS expiry when no refresh token is present - 27 new tests (token-auth.test.ts + init-pipeline.test.ts additions); 212/212 pass, typecheck clean - Add anr-token-auth-smoke CI job (continue-on-error, skips cleanly when ANR_ID_TOKEN secret is absent) - Add docs/anr-token-auth.md: env contract, CI examples, refresh policy, SKIP_AUTH vs AUTH_MODE distinction, troubleshooting OPENCODE_ANR_SKIP_AUTH remains unchanged for config-validation only. Closes #310
STALE_KEYS included OPENCODE_ANR_ID_TOKEN, so clearStaleEnv() wiped the externally-provided token before token auth mode could read it.
Token mode now accepts OPENCODE_ANR_REFRESH_TOKEN alone: at startup the refresh token is exchanged at Cognito's token endpoint for a fresh ID token, which is then federated into AWS credentials. CI stores one long-lived secret instead of manually rotating a ~1h ANR_ID_TOKEN. - validateTokenModeEnv accepts refresh-token-only environments - resolveTokenModeCredentials refreshes first when a refresh token is present (source: "refresh-exchange"), falling back to a provided ID token if the refresh fails, and propagates a rotated refresh token - smoke job accepts ANR_REFRESH_TOKEN or ANR_ID_TOKEN to run - docs: provisioning steps, resolution order, app-client requirements (refresh token validity, rotation must stay disabled) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prints the auth URL instead of auto-opening a browser so the one-time login can be done in a private window as the CI service account, then prints the refresh token for storage as the ANR_REFRESH_TOKEN secret. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| ;(global as any).__ANR_TELEMETRY_CONTEXT__ = telemetryContext | ||
|
|
||
| const exitHandler = async () => { | ||
| const duration = (Date.now() - Date.now()) / 1000 |
|
|
||
| let tokens: { idToken: string; accessToken: string; refreshToken?: string; expiresIn?: number } | ||
| let awsCredentials: Awaited<ReturnType<typeof exchangeTokenForAWSCredentials>> | ||
| let credentialSource: "interactive" | "static" | "exchange" | "refresh-exchange" = "interactive" |
| process.exit(1) | ||
| let tokens: { idToken: string; accessToken: string; refreshToken?: string; expiresIn?: number } | ||
| let awsCredentials: Awaited<ReturnType<typeof exchangeTokenForAWSCredentials>> | ||
| let credentialSource: "interactive" | "static" | "exchange" | "refresh-exchange" = "interactive" |
Secrets pasted from a terminal pick up hard newlines at visual wrap points (macOS Terminal copies soft-wrapped lines with breaks), which Cognito rejects with 400 Bad Request. Tokens are base64url and can never contain whitespace, so stripping it is strictly safe. Also pin --env-file in the smoke job: the repo ships three .opencode env flavors and auto-selection order is filesystem-dependent; the stored refresh token is only valid for the commercial app client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings the 5 files touched in this session's manual (non-agent) work into line with the ANR Implementor convention introduced in .opencode/agent/anr_implementor.md — every modified/created file tagged with issue/branch/date. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The configured Cognito Identity Pool was deleted/rotated server-side (ResourceNotFoundException), causing anr-token-auth-smoke to report a false regression unrelated to any code change. Mirrors the existing anr-aws-sdk-drift/anr-aws-smoke pattern: a cheap preflight job probes credential resolution directly (no CLI build) and only skips the smoke job for this one known, external failure signature. Any other failure, including no secret configured, still runs the smoke job so it surfaces full diagnostics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related Issue
Closes #310
Summary
Implements non-interactive, browserless token authentication for ANR CI. Interactive OIDC login remains the default — no change to local developer UX.
Problem
Every CI run authenticated as the developer who last provisioned a token interactively. There was no way to store a long-lived credential in GitHub Secrets and have the CLI bootstrap itself without a browser — blocking fully autonomous CI pipelines.
Solution
Added a
tokenauth mode (OPENCODE_ANR_AUTH_MODE=token) with a refresh-first bootstrap strategy:AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY+AWS_SESSION_TOKEN), use them directly — no federation call.OPENCODE_ANR_REFRESH_TOKENis set, exchange it for a fresh ID token at startup (refresh-first bootstrap), then federate. This is the recommended CI path — one long-lived secret, no manual rotation.OPENCODE_ANR_ID_TOKENfor direct federation exchange.Fail fast with actionable, CI-friendly errors at every step. Secret values are never logged.
The
ANR_REFRESH_TOKENGitHub secret has been provisioned on this repo using a dedicatedanrcode-ciCognito account (365-day expiry, ~July 2027).ANRCode-Tagged Changes
All modified files carry the ANRCode marker:
New files:
packages/anr-core/src/integrations/token-auth.ts—parseANRAuthMode,validateTokenModeEnv,resolveTokenModeCredentials; shared by CLI and desktop sidecarpackages/anr-core/test/token-auth.test.ts— 217 unit tests covering all credential resolution pathspackages/opencode/script/anr-provision-ci-token.ts— provisioning script: prints auth URL + refresh token for storage in GitHub Secretsdocs/anr-token-auth.md— env contract, CI setup steps, refresh policy, troubleshootingModified files:
packages/opencode/src/index.ts— branches on auth mode; token-aware refresh closurepackages/opencode/src/anr-boot.ts— same auth mode branching for desktop sidecarpackages/anr-core/src/index.ts— exports new token-auth helpers.github/workflows/test.yml— addsanr-token-auth-smokejob (continue-on-error: true); skips cleanly when secrets are absentUpstream Divergence Flags
None. All changes are ANR-additive. No upstream opencode files were modified in a way that conflicts with upstream.
Verification
bun testinpackages/anr-core)bun typecheckin bothpackages/anr-coreandpackages/opencode)agent listagainst commercial backend (source: refresh-exchange)ANR_REFRESH_TOKENprovisioned on this repo for dedicatedanrcode-ciCognito accountbeforeEachfetch reset added to prevent non-deterministic Windows CI failuresTesting
Before:
After:
ANR_REFRESH_TOKEN)anrcode-ciCognito account — no personal account dependencyHuman Action Required