ADORSYS-GIS / ADORSYS-GIS/ai-governance

[Ticket]: opencode reviewer 401s mid-run — 5-minute GH OIDC token vs 20-minute job

オープン
#35 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
ticket
主要言語
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 はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。