ADORSYS-GIS / ADORSYS-GIS/ai-governance
[Ticket]: opencode reviewer 401s mid-run — 5-minute GH OIDC token vs 20-minute job
- 主要言語
- Shell
- スター
- 0
- フォーク
- 0
- PR マージ指標
- 30日以内にマージされた PR はありません
説明
### Type
Bug
### Summary
`opencode-review.yml` mints a **GitHub Actions OIDC token** (5-minute lifetime) once at job start and uses it as a static bearer for a review job allowed **20 minutes**. Any review lasting longer than ~5 minutes authenticates, runs, then dies mid-session with `401 Unauthorized`.
Because review duration scales with diff size, **the reviewer fails selectively on large PRs and passes on small ones** — it silently skips exactly the changes that most need review, and reports it as a flaky infra error rather than "unreviewed".
### Intent
Give the reviewer a credential that outlives the job, so review coverage stops being a function of diff size.
### Source of truth (links)
- Workflow: https://github.com/ADORSYS-GIS/ai-governance/blob/main/.github/workflows/opencode-review.yml
- Failing run: https://github.com/ADORSYS-GIS/converse-frontends/actions/runs/32613488993/job/97130839617
- ADR-0047 (lightbridge-repo-auth GitHub-OIDC binding) in `ai-helm`
### Current Behavior
The workflow mints the token once:
```js
const token = await core.getIDToken(process.env.AUDIENCE);
core.setSecret(token);
core.exportVariable('CAMER_DIGITAL_API_KEY', token);
```
and opencode consumes it as a static `apiKey` for the whole session. The job sets `timeout-minutes: 20`.
Measured on run `32613488993`, job `97130839617`:
| | |
|---|---|
| Job started | `2026-08-23T02:47:57Z` |
| Token `exp` | `2026-08-23T02:53:10Z` |
| **Effective TTL** | **~5 minutes** |
| First 401 | `2026-08-23T02:53:11Z` |
| Job died | `2026-08-23T02:53:15Z` |
The gateway behaved correctly and said so precisely — Authorino rejected the expired JWT across all three configured identity sources:
```
www-authenticate: Bearer realm="lightbridge-apikey"
x-ext-auth-reason: {"github-actions":"oidc: token is expired (Token Expiry: 2026-08-23 02:53:10 +0000 UTC)",
"keycloak":"oidc: token is expired (Token Expiry: 2026-08-23 02:53:10 +0000 UTC)",
"lightbridge-apikey":"oidc: token is expired (Token Expiry: 2026-08-23 02:53:10 +0000 UTC)"}
```
Observed pattern across recent PRs, consistent with the duration hypothesis:
| PR | Diff size | `review / opencode` |
|---|---|---|
| converse-frontends#197 | 1 file | pass (finished < 5 min) |
| converse-frontends#198 | 14 files, +305/−130 | **fail at 5m18s** |
| converse-frontends#196 | large | **fail** |
Nothing on the lightbridge side is misconfigured. This is purely a credential-lifetime-vs-workload-duration mismatch in the template.
### Expected Behavior
A review runs to completion regardless of duration, up to the job's own `timeout-minutes`. A credential expiry never truncates a review, and a truncated review never reports as a passing check.
### Acceptance Criteria
- [ ] A review job exceeding 5 minutes of wall-clock completes without a `401`.
- [ ] Verified on a deliberately large diff (the converse-frontends#198 diff, 14 files, is a good fixture — it reproduces today).
- [ ] The keyless posture of ADR-0047 is preserved: no long-lived shared secret is added to the calling repo.
- [ ] If the credential cannot be renewed for some reason, the job **fails loudly as unreviewed** rather than reporting a green check.
### Out of Scope
- Changing `timeout-minutes`. Reviews legitimately take this long; shortening the budget hides the problem rather than fixing it.
- Retrying the job. Confirmed ineffective — three consecutive runs (including a deliberate rerun) failed identically.
### Technical Context
**A straight RFC 8693 token-exchange step does not work today.** `lightbridge-authz`'s exchange validates the presented `subject_token` with the Keycloak bearer/JWKS validator:
```rust
let token_info = match self.bearer.validate_bearer_token(subject_token).await { ... }
```
(`crates/lightbridge-authz-rest/src/oauth2_op/store.rs`, and that module's own doc comment states "Our `subject_token` is a Keycloak access token signed by a completely different key".)
A GitHub Actions OIDC token is issued by `https://token.actions.githubusercontent.com` and would be rejected as `invalid_token`. Note the asymmetry: Authorino at the **gateway** already trusts a `github-actions` identity source, but `authz-idp`'s **exchange** does not.
Three viable directions, in increasing order of cost:
**A. Re-mint the OIDC token during the run (no backend change).**
GitHub exposes `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` for the whole job, and the ID-token endpoint may be called repeatedly. A small localhost proxy that mints a fresh token per upstream request and forwards to `https://api.ai.camer.digital/v1` would fully decouple credential lifetime from session length; opencode's `baseURL` points at the proxy. Entirely within this template — no lightbridge change, ADR-0047 posture preserved.
**B. Teach `authz-idp`'s exchange to accept GitHub Actions as a subject issuer.**
Then the workflow exchanges its 5-minute assertion for a lightbridge token whose TTL we control (`oauth2.token_exchange.access_ttl_seconds`, currently defaulting to 900s — note this would need raising above the job timeout, or pairing with `offline_access` refresh). Requires multi-issuer subject validation plus a mapping from a GH OIDC `sub` (`repo:ORG/REPO:ref:refs/heads/main`) onto an account/project — ADR-0006 makes `accounts.id` the JWT `sub`, so this is really "machine identity federation" and deserves its own ADR. Strategically the most interesting, since other CI consumers will want it.
**C. Issue a long-lived lightbridge API key for CI.**
Works today with no code change, but abandons ADR-0047's keyless design and reintroduces a shared secret per repo. Recorded for completeness; not recommended.
Recommendation: **A** now (small, self-contained, fixes every repo using the template), and open **B** separately as the strategic path if machine identity federation is wanted for other CI consumers.
### Risks
- A localhost proxy (A) becomes a new component in the review path; if it fails, reviews fail — but they fail *loudly*, which is strictly better than the current silent truncation.
- (B) widens the set of issuers `authz-idp` will mint tokens for. That is an authentication-boundary change and must fail closed on any subject-validation error, per this estate's standing rule that an unavailable dependency never becomes the permissive branch.
- Whichever path is taken, the "truncated review reports green" behaviour should be fixed independently — it is the reason this went unnoticed for days.
### Test Plan
- Reproduce first: re-run the reviewer against the converse-frontends#198 diff and observe the `401` at ~5 minutes with `x-ext-auth-reason` showing an expiry inside the run window.
- Apply the fix, re-run the same fixture, confirm completion past the 5-minute mark.
- Negative test: make the credential path fail deliberately and confirm the job reports failure rather than a passing check.
- Confirm a small PR still reviews normally (no regression on the path that works today).
コントリビューションガイド
評価
この issue はまだ評価されていません。