Skip to content

feat(anr): add CI token-based authentication mode - #372

Merged
donta-dalpoas merged 9 commits into
devfrom
anr-token-based-auth
Jul 30, 2026
Merged

feat(anr): add CI token-based authentication mode#372
donta-dalpoas merged 9 commits into
devfrom
anr-token-based-auth

Conversation

@dstokes0523

@dstokes0523 dstokes0523 commented Jul 22, 2026

Copy link
Copy Markdown

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 token auth mode (OPENCODE_ANR_AUTH_MODE=token) with a refresh-first bootstrap strategy:

  1. If static AWS creds are present (AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_SESSION_TOKEN), use them directly — no federation call.
  2. If OPENCODE_ANR_REFRESH_TOKEN is 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.
  3. Otherwise fall back to OPENCODE_ANR_ID_TOKEN for direct federation exchange.

Fail fast with actionable, CI-friendly errors at every step. Secret values are never logged.

The ANR_REFRESH_TOKEN GitHub secret has been provisioned on this repo using a dedicated anrcode-ci Cognito account (365-day expiry, ~July 2027).

ANRCode-Tagged Changes

All modified files carry the ANRCode marker:

// ANRCODE_CHANGE {"issue":310,"branch":"anr-token-based-auth","date":"2026-07-22"}

New files:

  • packages/anr-core/src/integrations/token-auth.tsparseANRAuthMode, validateTokenModeEnv, resolveTokenModeCredentials; shared by CLI and desktop sidecar
  • packages/anr-core/test/token-auth.test.ts — 217 unit tests covering all credential resolution paths
  • packages/opencode/script/anr-provision-ci-token.ts — provisioning script: prints auth URL + refresh token for storage in GitHub Secrets
  • docs/anr-token-auth.md — env contract, CI setup steps, refresh policy, troubleshooting

Modified files:

  • packages/opencode/src/index.ts — branches on auth mode; token-aware refresh closure
  • packages/opencode/src/anr-boot.ts — same auth mode branching for desktop sidecar
  • packages/anr-core/src/index.ts — exports new token-auth helpers
  • .github/workflows/test.yml — adds anr-token-auth-smoke job (continue-on-error: true); skips cleanly when secrets are absent

Upstream Divergence Flags

None. All changes are ANR-additive. No upstream opencode files were modified in a way that conflicts with upstream.

Verification

  • ✅ Full anr-core suite: 217/217 pass (bun test in packages/anr-core)
  • ✅ TypeScript compilation clean (bun typecheck in both packages/anr-core and packages/opencode)
  • ✅ Live end-to-end: refresh token → fresh ID token → federation exchange → agent list against commercial backend (source: refresh-exchange)
  • ✅ CI secret ANR_REFRESH_TOKEN provisioned on this repo for dedicated anrcode-ci Cognito account
  • ✅ Test isolation fixed: beforeEach fetch reset added to prevent non-deterministic Windows CI failures
  • ✅ Dead code removed from static-creds path

Testing

Before:

  • CI had no way to authenticate without a browser
  • Every token was tied to a developer's personal account
  • Short-lived ID tokens required manual rotation

After:

  • CI stores one long-lived refresh token in GitHub Secrets (ANR_REFRESH_TOKEN)
  • Fresh ID token minted automatically at each run startup
  • Dedicated anrcode-ci Cognito account — no personal account dependency
  • Fork PRs skip the smoke job cleanly (secrets not available to forks)

Human Action Required

Dylan Stokes and others added 5 commits July 13, 2026 07:27
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"
Dylan Stokes and others added 4 commits July 23, 2026 09:36
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>
@donta-dalpoas
donta-dalpoas merged commit 09b9ffe into dev Jul 30, 2026
56 of 57 checks passed
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.

[Feature]: ANR CI token-based authentication mode (non-interactive, browserless)

2 participants