Skip to content

feat(cmd): get a record key from a CI OIDC token when none is set [ENG-2225] - #511

Open
agoldis wants to merge 4 commits into
mainfrom
agoldis/oidc
Open

agoldis wants to merge 4 commits into
mainfrom
agoldis/oidc

Conversation

@agoldis

@agoldis agoldis commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Owner: @agoldis
Linear: project P-ENG-132 (OIDC record keys for CI)

When no record key is set, the currents CLI exchanges an OIDC token for a record key that expires after 6 hours. Customers no longer need to store CURRENTS_RECORD_KEY as a secret. This also covers Jest and node:test results, because currents run upload sends them.

Do not merge or release until the server routes POST /oidc/token and POST /oidc/revoke are deployed and turned on (currents-dev/currents#4948). Until then, nothing changes for anyone with a key. Without the revoke route the revoke fails quietly and the key lives until it expires.

Token sources

Used only when no key is set. The first one that applies wins:

  1. --oidc-token <token>, then CURRENTS_OIDC_TOKEN: a token the job gets itself, on any CI provider. For GitLab CI: id_tokens: CURRENTS_OIDC_TOKEN: aud: <Currents URL>. Prefer the variable: a flag is visible in the process list and in logs that echo commands.
  2. GitHub Actions, when the job has permissions: id-token: write: the CLI requests a token with ACTIONS_ID_TOKEN_REQUEST_URL.

The audience of the token is the origin of the Currents URL (CURRENTS_API_URL, default https://cy.currents.dev), so a token for one deployment is not accepted by another. --key or CURRENTS_RECORD_KEY always wins.

Exchange and revoke

  • Exchange: POST /oidc/token, OAuth 2.0 Token Exchange (RFC 8693). Form body grant_type, subject_token, subject_token_type and project_id; the response has access_token (the record key) and expires_in. Errors show error_description, then error, then the HTTP status text.
  • Revoke: when the command ends, POST /oidc/revoke (RFC 7009) with the key. One attempt, 5 seconds, no retries. A failure only writes a debug line and never changes the exit code. A key from --key or CURRENTS_RECORD_KEY is not revoked.
  • The exchange is scoped to one project. project_id comes from --project-id or CURRENTS_PROJECT_ID. Without it the CLI does not call the exchange and says which flag and variable to set. cache set, cache get, session attach and session share have --project-id only for this; it is not required when a record key or an API key is set. The project ID is not masked in --debug output.
  • The exchange runs once per process in the program's preAction hook and fills the key option before validation, so no command changed. Commands with --key get it: run upload, run cancel, run attach, cache get, cache set, and session start, attach and share.
  • run attach and session * skip the exchange when an API key is set. Commands with only --api-key (api, run get, run share) are unchanged.
  • No key and no token source: the existing "record key is required" error, plus a hint.
  • Exchange fails: stop with the server's message, exit code 1, no fallback.
  • One line on stderr says where the key came from and when it expires. With --json, all logs go to stderr before the exchange.
  • On GitHub Actions the CLI prints ::add-mask::<key> on stderr, so a later step that prints the key shows ***.
  • The tokens and the key are never printed. --debug masks subject_token, token, oidcToken and access_token, and --oidc-token is cleared from the options after it is read, so it does not reach the config dump.
  • Both requests to GitHub and Currents retry network errors and 408/429/502/503/504, after 3s, 15s and 30s.

@currents/jest and @currents/node-test-reporter don't send a record key and are unchanged.

Tests

  • cmd: types, lint and Prettier pass. vitest: 1131 passed, plus 1 expected failure.
  • bin/__tests__/oidc.test.ts runs the CLI against a local server that plays both GitHub and Currents: precedence of the sources, the request shapes, retries, the audience for the default and a custom URL, revoke, masking with --debug, --json, and the missing-key hint.
  • A fresh checkout needs npm run build -w @currents/rules before the tests that run the built CLI pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_011baD3jW7wFPFbKF1o7FCeC

@baz-reviewer

baz-reviewer Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Review this PR on Baz

Baz Summary

Enable the currents CLI to exchange CI OIDC tokens for project-scoped, short-lived record keys when no configured key exists, supporting uploads, cache operations, run commands, and sessions while preserving existing credentials. Add audience validation, retries, secure logging and masking, best-effort revocation, improved guidance, and comprehensive integration coverage for the new authentication flow.

Topics

TopicDetails
Security and reliability Protect exchanged tokens and record keys across request bodies, response debugging, configuration dumps, GitHub Actions logs, and error paths while adding configurable retries, audience normalization, and end-to-end tests for precedence, failures, masking, JSON output, and revocation.
Modified files (5)
  • packages/cmd/src/bin/__tests__/oidc.test.ts
  • packages/cmd/src/http/client.ts
  • packages/cmd/src/http/http.ts
  • packages/cmd/src/http/httpConfig.ts
  • packages/cmd/src/lib/credentials.ts
Latest Contributors(2)
UserCommitDate
agoldis@gmail.comfeat(cmd): send projec...November 14, 2026
agoldis@users.noreply....fix(cmd): replace axio...November 04, 2026
OIDC authentication flow Implement OIDC token discovery, project-scoped RFC 8693 exchange, command integration, precedence rules, and RFC 7009 cleanup for upload, cache, run, cancel, and session workflows.
Modified files (10)
  • packages/cmd/src/bin/program.ts
  • packages/cmd/src/commands/cache/index.ts
  • packages/cmd/src/commands/cancel/index.ts
  • packages/cmd/src/commands/options.ts
  • packages/cmd/src/commands/run/index.ts
  • packages/cmd/src/commands/session/index.ts
  • packages/cmd/src/commands/upload/index.ts
  • packages/cmd/src/commands/utils.ts
  • packages/cmd/src/lib/exitHooks.ts
  • packages/cmd/src/lib/oidc.ts
Latest Contributors(2)
UserCommitDate
agoldis@gmail.comfeat(cmd): send projec...November 14, 2026
agoldis@users.noreply....feat(cmd): add run sha...November 03, 2026
CLI configuration UX Extend configuration validation and user-facing command help with OIDC options, project requirements, deployment-specific audience guidance, and compatibility behavior for configured record or API keys.
Modified files (5)
  • packages/cmd/src/bin/__tests__/__snapshots__/help.test.ts.snap
  • packages/cmd/src/commands/session/options.ts
  • packages/cmd/src/config/keys.ts
  • packages/cmd/src/config/session/config.ts
  • packages/cmd/src/config/utils.ts
Latest Contributors(2)
UserCommitDate
agoldis@gmail.comfeat(cmd): send projec...November 14, 2026
agoldis@users.noreply....feat(cmd): add run sha...November 03, 2026

Merger   ✅ Ready to Merge

MERGE: the OIDC record-key flow is implemented, CI ran successfully, and all review discussions are settled.



Skills  Activate Skill Maintainer to keep your skills up to date

Planner  This PR would have been improved with Baz Planner - Try it now

Comment thread packages/cmd/src/bin/program.ts Resolved
@agoldis agoldis changed the title feat(cmd): get a record key from GitHub Actions OIDC when none is set feat(cmd): get a record key from a CI OIDC token when none is set Nov 13, 2026
Comment thread packages/cmd/src/lib/credentials.ts Resolved
@agoldis agoldis added the OIDC OIDC record keys for CI (P-ENG-132) label Nov 13, 2026
Comment thread packages/cmd/src/http/httpConfig.ts Resolved
@agoldis agoldis changed the title feat(cmd): get a record key from a CI OIDC token when none is set feat(cmd): get a record key from a CI OIDC token when none is set [ENG-2225] Nov 13, 2026
When --key and CURRENTS_RECORD_KEY are not set, commands that take a record
key exchange an OIDC token for a short-lived record key with
POST /oidc/token (OAuth 2.0 Token Exchange, RFC 8693).

Token sources, in order:
- CURRENTS_OIDC_TOKEN: a token the job gets itself, on any CI provider. Its
  audience must be the Currents URL.
- GitHub Actions: requested with the Currents URL as the audience, with
  retries, when the job has `id-token: write`.

The exchange runs once per process in the preAction hook, and is skipped when
an API key is set on a command that accepts one. The key is revoked with
POST /oidc/revoke (RFC 7009) when the command ends; a failed revoke only
writes a debug line. --debug never prints the tokens or the key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011baD3jW7wFPFbKF1o7FCeC
Comment thread packages/cmd/src/lib/oidc.ts Resolved
Every command that takes --key also takes --oidc-token <token>, with
CURRENTS_OIDC_TOKEN as its environment variable. Order: --key or
CURRENTS_RECORD_KEY, then --oidc-token, then CURRENTS_OIDC_TOKEN, then GitHub
Actions. An empty value counts as unset.

The preAction hook clears the value from the options after reading it, so it
is not copied into the config of a command or printed with --debug. oidcToken
is also in the masked field names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011baD3jW7wFPFbKF1o7FCeC

agoldis commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Security review at 5bb328ad. From reading the diff; no tests were run.

The issued record key is not masked in CI logs

A key from a CI secret is masked by the provider. A key fetched at runtime is not. The CLI masks its own debug output, but a later step or tool that prints the command's options or environment shows the key. On GitHub Actions, printing ::add-mask::<key> right after the exchange covers it.

--oidc-token puts the token on the command line

A value passed as a flag is visible in the process list and in CI logs that echo commands. The option also reads CURRENTS_OIDC_TOKEN; the help text and docs should point users to the variable and keep the flag for local use.

oidc/revoke has no server route yet

The exit hook posts the key to oidc/revoke. currents-dev/currents#4948 has no such route, so the call fails and the key lives until it expires. The two should ship together.

Checked and fine: only GitHub Actions is used automatically, with the Currents URL as the requested audience; an API key or a record key wins over OIDC; subject_token, token, oidcToken and access_token are masked in debug output.


Generated by Claude Code

agoldis commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

Code review follow-up, checked at 5bb328ad. From reading the code; no tests were run.

Tokens from --oidc-token or CURRENTS_OIDC_TOKEN are refused once the job is older than 5 minutes

The exchange in currents-dev/currents#4948 refuses a token whose iat is more than 5 minutes old. A GitLab id_tokens token is issued when the job starts and cannot be refreshed. The CLI exchanges in every process, so the first command may succeed and a currents upload or cache set later in the same job fails with invalid_grant and exit code 1.

The fix belongs on the server (details on #4948). GitHub Actions is not affected, because the CLI requests a fresh token each time.


Generated by Claude Code

- Commands with --json print logs to stderr before the exchange, so the
  retry warnings of the exchange do not come before the JSON on stdout.
- On GitHub Actions, print ::add-mask::<key> on stderr after the exchange. A
  key fetched at run time is not a secret the runner knows, so a later step
  that prints it would show it.
- The --oidc-token help says to prefer CURRENTS_OIDC_TOKEN: a flag is visible
  in the process list and in logs that echo commands.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011baD3jW7wFPFbKF1o7FCeC
baz-app[bot]
baz-app Bot previously approved these changes Nov 14, 2026
The exchange is scoped to one project, so POST /oidc/token carries a
project_id form field from --project-id or CURRENTS_PROJECT_ID. Without a
project ID the CLI does not call the exchange and says which flag and
variable to set.

cache set, cache get, session attach and session share get --project-id,
read only for the exchange. It is not required when a record key or an API
key is set. session attach and share do not copy it into their config.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011baD3jW7wFPFbKF1o7FCeC
@baz-app
baz-app Bot dismissed their stale review November 14, 2026 01:14

Baz withdrew its approval because this pull request is no longer ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

baz: ready to merge OIDC OIDC record keys for CI (P-ENG-132)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant