Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 63 additions & 9 deletions .github/workflows/cora-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,80 @@ permissions:
contents: read
pull-requests: write
security-events: write
id-token: write # workload identity federation with Anthropic (no stored API key)

jobs:
cora-review:
name: Cora Review
runs-on: ubuntu-latest
timeout-minutes: 30
env:
# Anthropic workload identity federation (organization secrets or variables).
# When all four are set, the job exchanges its GitHub OIDC token for a
# short-lived Anthropic access token instead of using CORA_API_KEY.
FEDERATION_RULE_ID: ${{ secrets.ANTHROPIC_FEDERATION_RULE_ID || vars.ANTHROPIC_FEDERATION_RULE_ID }}
ORGANIZATION_ID: ${{ secrets.ANTHROPIC_ORGANIZATION_ID || vars.ANTHROPIC_ORGANIZATION_ID }}
SERVICE_ACCOUNT_ID: ${{ secrets.ANTHROPIC_SERVICE_ACCOUNT_ID || vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
WORKSPACE_ID: ${{ secrets.ANTHROPIC_WORKSPACE_ID || vars.ANTHROPIC_WORKSPACE_ID }}
steps:
# SECURITY: this job checks out untrusted PR code while holding
# CORA_API_KEY (pull_request_target). A PR-supplied .cora.yaml could set
# provider.base_url and exfiltrate the key. CORA_BASE_URL (env) always
# takes precedence over project config, so require it to be pinned.
- name: Exchange the GitHub OIDC token for an Anthropic access token
id: wif
if: env.FEDERATION_RULE_ID != '' && env.ORGANIZATION_ID != '' && env.SERVICE_ACCOUNT_ID != '' && env.WORKSPACE_ID != ''
continue-on-error: true
run: |
# The OIDC token stays in this step's shell: it is never exported to the
# job environment, so later steps (checkout of the PR, the review action)
# cannot see it. Only the short-lived Anthropic access token is exported.
jwt=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" | jq -r '.value // empty')
if [ -z "$jwt" ]; then
echo "::warning::Could not get a GitHub OIDC token; falling back to the CORA_* secrets."
exit 1
fi
echo "::add-mask::$jwt"
body=$(jq -n \
--arg jwt "$jwt" \
--arg rule "$FEDERATION_RULE_ID" \
--arg org "$ORGANIZATION_ID" \
--arg sa "$SERVICE_ACCOUNT_ID" \
--arg ws "$WORKSPACE_ID" \
'{grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: $jwt, federation_rule_id: $rule, organization_id: $org,
service_account_id: $sa, workspace_id: $ws}')
code=$(curl -sS -o exchange.json -w '%{http_code}' \
https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" --data "$body")
if [ "$code" != "200" ]; then
echo "::warning::Anthropic token exchange failed (HTTP $code: $(jq -r '.error.type // "unknown"' exchange.json 2>/dev/null)); falling back to the CORA_* secrets."
rm -f exchange.json
exit 1
fi
token=$(jq -r '.access_token' exchange.json)
rm -f exchange.json
echo "::add-mask::$token"
echo "ACCESS_TOKEN=$token" >> "$GITHUB_ENV"
echo "Using a short-lived Anthropic access token (workload identity federation)."

# SECURITY: this job checks out untrusted PR code while holding a credential
# (pull_request_target). A PR-supplied .cora.yaml could set provider.base_url
# and exfiltrate it. CORA_BASE_URL (env) always takes precedence over
# project config, so the base URL is pinned here, and the Anthropic token
# is only ever paired with api.anthropic.com (never with CORA_BASE_URL).
- name: Require the review secrets
env:
CORA_API_KEY: ${{ secrets.CORA_API_KEY }}
CORA_BASE_URL: ${{ secrets.CORA_BASE_URL }}
CORA_MODEL: ${{ secrets.CORA_MODEL }}
run: |
# An empty secret must fail the job: a skipped or fallback review is not a pass.
# No federation token: the stored secrets are required. An empty secret
# must fail the job: a skipped or fallback review is not a pass.
if [ -n "${ACCESS_TOKEN:-}" ]; then
exit 0
fi
missing=0
for name in CORA_API_KEY CORA_BASE_URL CORA_MODEL; do
if [ -z "${!name}" ]; then
echo "::error::Secret $name is empty or missing. Cora Review cannot run, and that must not pass silently."
echo "::error::Secret $name is empty or missing, and workload identity federation is not available. Cora Review cannot run, and that must not pass silently."
missing=1
fi
done
Expand All @@ -51,6 +103,8 @@ jobs:
uses: codecoradev/cora-review-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cora-api-key: ${{ secrets.CORA_API_KEY }}
cora-base-url: ${{ secrets.CORA_BASE_URL }}
cora-model: ${{ secrets.CORA_MODEL }}
# With a federation token, key, base URL and model switch together so the
# token is only ever sent to api.anthropic.com.
cora-api-key: ${{ env.ACCESS_TOKEN || secrets.CORA_API_KEY }}
cora-base-url: ${{ env.ACCESS_TOKEN && 'https://api.anthropic.com/v1' || secrets.CORA_BASE_URL }}
cora-model: ${{ env.ACCESS_TOKEN && (secrets.ANTHROPIC_MODEL || vars.ANTHROPIC_MODEL || 'claude-opus-5-5') || secrets.CORA_MODEL }}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Cora Review can use Anthropic workload identity federation (no stored API key).** The review job exchanges its GitHub OIDC token for a short-lived Anthropic access token (when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` are set) and sends it only to `api.anthropic.com`; otherwise it falls back to `CORA_API_KEY`, `CORA_BASE_URL` and `CORA_MODEL`. A smoke test workflow verifies the exchange and a Bearer call to `/v1/chat/completions`. See `docs/examples.md`.
- **Agents validate before they commit (#674).** A review of the staged diff, by an agent (`cora.review_finalize`) or by `cora review --staged`, now leaves a marker bound to that exact change set. `cora hook verify` reports it (exit 0 validated, 10 validated but blocking, 11 not validated) and `cora hook claude-guard` is a Claude Code `PreToolUse` hook that denies a `git commit` that is unreviewed, blocking, uses `--no-verify`, or is not a plain commit of the staged change. The pre-commit hook (now v2, upgraded in place by `cora hook install`) accepts a validated diff and otherwise falls back to `cora review --staged --ci --severity major`; a tooling error is reported as "not a pass". The `/cora-review` skill tells agents to review before every commit. See `docs/usage.md`.

### Fixed
Expand Down
8 changes: 8 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ jobs:
cora-model: ${{ secrets.CORA_MODEL }}
```

### Anthropic with workload identity federation (no stored API key)

Instead of a long-lived `CORA_API_KEY`, the job can exchange its GitHub OIDC token for a short-lived Anthropic access token. Create a federation rule in the Anthropic Console (issuer `https://token.actions.githubusercontent.com`, audience `https://api.anthropic.com`, a **subject prefix** that matches your repository), then provide `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` (and optionally `ANTHROPIC_MODEL`) as organization or repository secrets or variables.

GitHub now issues subjects with numeric ids, for example `repo:my-org@289767358/my-repo@1254014703:pull_request`; a prefix written as `repo:my-org/my-repo:` never matches it. The Anthropic Console authentication log shows the reason for a denial (`match_subject_prefix`).

The job needs `permissions: id-token: write`. The exchanged token is sent only to `https://api.anthropic.com/v1` (never to `CORA_BASE_URL`), and when federation is unavailable the job falls back to the `CORA_*` secrets. This repository's `.github/workflows/cora-review.yml` and `anthropic-wif-smoke.yml` are a working reference.

### Options

| Input | Default | Description |
Expand Down
Loading