Skip to content

feat(auth): make claude-api-key optional so the action can run keyless (NEX-2513) - #4

Merged
waleedzafar68 merged 3 commits into
mainfrom
feat/keyless-wif-auth
Sep 18, 2026
Merged

waleedzafar68 merged 3 commits into
mainfrom
feat/keyless-wif-auth

Conversation

@waleedzafar68

Copy link
Copy Markdown
Collaborator

Why

Every repo in the org is moving off static Anthropic API keys to workload identity federation (NEX-2513 step 2). This action is the last consumer that hard-fails without claude-api-key, so it alone keeps a static key alive in repo secrets.

What changes

Cherry-picks upstream anthropics/claude-code-security-review#116 by @ralphpina — open against an upstream main that has not moved since 2026-02-11, so we carry it here.

  • action.yml — claude-api-key becomes required: false, and the scan step unsets an empty ANTHROPIC_API_KEY instead of exiting 1. An empty-but-set value outranks keyless auth, so the unset is what makes federation reachable at all. It sits in the same run: block as the scan, so it covers both the python process and the claude it spawns.
  • claudecode/claude_api_client.py — construct Anthropic() with no key when none is present and let the SDK resolve environment credentials, instead of raising ValueError.
  • claudecode/github_action_audit.py — validate_claude_available() accepts a static credential or federation env, with an error message naming both.

Passing claude-api-key behaves exactly as before — this only adds a fallback path when it is absent.

Two fork-local fixes on top of the upstream patch:

How the caller authenticates

The action deliberately does not mint the OIDC token itself. The calling workflow (vuln-synthesis-agent/ai-security-scan) sets up federation the way anthropics/claude-code-action does: the GitHub OIDC token is written to a file with ANTHROPIC_IDENTITY_TOKEN_FILE pointing at it, plus a minimal profile under ANTHROPIC_CONFIG_DIR.

The profile is not cosmetic. A GitHub assertion carries a jti and our federation issuer has check_jti: true, so one assertion buys exactly one token exchange. This action starts more than one Anthropic client per run — the claude CLI (up to 3 retry processes in _run_claude_code) plus the Python false-positive filter — and resolving federation through a profile turns on the SDK's on-disk credentials cache so they share a single exchanged token. Bare env-var federation would 401 with jti_reused on the second consumer.

Verified

  • py_compile on both changed modules, action.yml parses as valid YAML.
  • Key-present path unchanged (same Anthropic(api_key=...) construction).
  • The federated bearer itself is already proven against this org's rules: exchange 200, /v1/messages 200, /v1/messages/batches 200 — with no anthropic-beta: oauth-2025-04-20 header, confirming it is not required for a federated token.
  • The keyless path end-to-end is validated by a real PR scan with no key on the consumer side, not by this repo's CI.

🤖 Generated with Claude Code

waleedzafar68 and others added 3 commits September 19, 2026 00:25
## Why
Every repo in the org is moving off static Anthropic API keys to workload
identity federation (NEX-2513). This action is the last consumer that hard-fails
without `claude-api-key`, so it alone keeps a static key alive in repo secrets.

## What changes
Cherry-picks upstream PR anthropics#116 (@ralphpina),
which is open against an upstream `main` that has not moved since 2026-02-11:

- `action.yml` — `claude-api-key` becomes `required: false`, and the scan step
  unsets an empty `ANTHROPIC_API_KEY` rather than exiting 1. An empty-but-set
  value outranks keyless auth, so unsetting it is what makes federation reachable.
- `claude_api_client.py` — construct `Anthropic()` with no key when none is
  present and let the SDK resolve environment credentials, instead of raising.
- `github_action_audit.py` — `validate_claude_available()` accepts a static
  credential or federation env, with an error message that names both.

Passing `claude-api-key` behaves exactly as before; this only adds a fallback
path when it is absent.

Two fork-local fixes on top of the upstream patch:

- the conflict against our cost-metrics change (#3) is resolved by keeping the
  keyless client construction and our `usage` / `_record_usage` block;
- `logger.info("Claude API client initialized successfully")` had ended up as
  the last statement of `_record_usage` in #3, so it logged on every API
  response. Moved back into `__init__`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Why
Upstream anthropics#116 unblocked ClaudeAPIClient but missed create_findings_filter, which
gates Claude filtering on `use_claude_filtering and api_key`. Under federation
there is no api_key, so the false-positive filter silently switched off — a
keyless scan on nexus-status came back with `"justification": "Claude filtering
disabled"` while still reporting findings and exiting 0. That is the worst shape
of failure here: the scan looks clean and the PR comment just gets noisier.

## What changes
Gate on `use_claude_filtering` alone and let the client resolve the credential.
FindingsFilter already validates API access on construction and degrades to hard
rules if it fails, so the key check bought nothing that the fallback did not.

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

The fallback that re-runs the audit without the diff has been unreachable.
nexus-status hit it for real: a PR that swept in build artifacts produced a
~1.2M-token prompt against the 1M limit, and instead of retrying without the
diff the action reported `Claude Code execution failed with return code 1`
and the PR got no review at all.

Two independent faults, either one fatal:

- The detection sat inside the `returncode == 0` branch. The CLI writes its
  result envelope to stdout and *still* exits non-zero on an API error, so
  stdout was never parsed. Worse, the loop treated it as a transient failure
  and spent all three retries resending the same oversized prompt.
- The test was `result == 'Prompt is too long'`, exact equality. The CLI now
  appends the measured counts — `Prompt is too long · the request is ~1199699
  tokens (limit 1000000) …` — so equality could never hold again.

This is not the same gap the 406 work closed. That change made an oversized
diff *fetchable* by assembling it from per-file patches; it makes the prompt
bigger, not smaller. Nothing downstream bounded what that diff cost in tokens.

## What changes

- `github_action_audit.py`: parse stdout before branching on the return code,
  and match the message by prefix. Usage is still recorded exactly once per
  attempt, so a billed prompt-too-long attempt keeps being counted.
- `test_claude_runner.py`: a regression test built from the observed envelope —
  non-zero exit plus the counts-bearing message — asserting both that the
  fallback fires and that the oversized prompt is not retried. It fails against
  the previous code with the production error string.
- Same file: the `validate_claude_available` assertion still expected the
  key-only message that upstream anthropics#116 replaced. Updated to the message that
  names both credential routes; the suite was 180/1 and is now 181/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@waleedzafar68
waleedzafar68 merged commit e28be03 into main Sep 18, 2026
@waleedzafar68
waleedzafar68 deleted the feat/keyless-wif-auth branch September 18, 2026 21:06
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.

1 participant