Skip to content

fix(oauth): attest credential-free live provider reloads - #1541

Merged
lidge-jun merged 3 commits into
lidge-jun:devfrom
luvs01:agent/verify-live-provider-update
Aug 12, 2026
Merged

fix(oauth): attest credential-free live provider reloads#1541
lidge-jun merged 3 commits into
lidge-jun:devfrom
luvs01:agent/verify-live-provider-update

Conversation

@luvs01

@luvs01 luvs01 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace credential-bearing live provider updates with an attested, bodyless POST /api/providers/reload operation
  • bind a single-use 10-second capability to the provider name, method, exact path, nonce, PID, port, and expiry
  • reload the named schema-valid provider from the protected config.json snapshot without sending provider credentials or a reusable management token
  • validate the destination, recheck the config-byte digest under the config mutation lock, adopt only the named live row, and invalidate affected runtime caches without rewriting disk
  • use direct local HTTP for both the challenge and reload request so environment proxies cannot observe or fabricate the exchange

Test plan

  • Bun 1.3.14 focused changed-file run: 152 passed, 0 failed; exact post-rebase capability and reload contracts: 12 passed
  • Bun 1.4.0-canary.1 focused new and changed regressions: 14 passed, 0 failed; exact post-rebase capability and reload contracts: 12 passed
  • exact /healthz capability contract test passed on Bun 1.3.14 and Bun 1.4.0-canary.1
  • TypeScript 7 tsc --noEmit passed on Bun 1.3.14 and Bun 1.4.0-canary.1
  • bun run privacy:scan passed on both runtimes after rebasing to current dev
  • git diff --check passed
  • final security review found no P0-P2 issue

Focused dual-runtime validation covers the capability contract, replay and TTL bounds, direct bodyless POST framing, attestation failure, runtime-record replacement, named-row-only live adoption, disk-change rejection during destination validation, OAuth/key-login live convergence, unchanged disk bytes, and config-baseline preservation. The repository-wide suite is left to GitHub CI rather than duplicated locally.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for credential forwarding, capability scope/replay, listener attestation, and config authority.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • New Features

    • OAuth and API-key logins can now refresh a running proxy’s provider credentials without transmitting secrets.
    • Reloads are securely verified and limited to supported providers.
    • The CLI reports whether the running proxy adopted the new credentials.
  • Bug Fixes

    • If live refresh is unavailable or rejected, credentials are still saved and the CLI advises restarting the proxy.
    • Provider updates preserve unrelated live settings and disk configuration.
  • Documentation

    • Added guidance describing credential reload behavior and restart requirements.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/oauth/login-cli.ts.

@github-actions github-actions Bot added the bug Something isn't working label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • hygiene: unsponsored_surface.

What to do

  • Fix unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/oauth/login-cli.ts, src/server/management-auth.ts.
  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

Automatic draft conversion failed (token cannot change draft status). Please convert this pull request to a draft manually. The required enforce-target check will keep failing until every issue above is resolved.

@github-actions
github-actions Bot marked this pull request as draft August 12, 2026 14:46
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 02370fe6-4212-43c9-a069-65e5c49dce47

📥 Commits

Reviewing files that changed from the base of the PR and between 4be2c6d and 17e4b6a.

📒 Files selected for processing (19)
  • docs-site/src/content/docs/reference/cli/providers-accounts.md
  • src/config.ts
  • src/lib/local-provider-reload-contract.ts
  • src/oauth/login-cli.ts
  • src/server/direct-local-http.ts
  • src/server/index.ts
  • src/server/local-provider-reload-client.ts
  • src/server/management-auth.ts
  • src/server/management/provider-routes.ts
  • src/server/proxy-liveness.ts
  • structure/05_gui-and-management-api.md
  • tests/config-user-edits.test.ts
  • tests/key-login-live-update.test.ts
  • tests/local-management-direct-transport.test.ts
  • tests/local-provider-reload-client.test.ts
  • tests/management-provider-validation.test.ts
  • tests/oauth-login-cli-live-update.test.ts
  • tests/server-auth.test.ts
  • tests/server-management-auth.test.ts

📝 Walkthrough

Walkthrough

OAuth and API-key login now save credentials to disk, then attempt an attested, process-bound reload in a running proxy. The proxy validates the request, rereads the provider, updates live state, refreshes related state, and reports failures to the CLI.

Changes

Local provider reload

Layer / File(s) Summary
Capability contract and runtime attestation
src/lib/local-provider-reload-contract.ts, src/server/local-provider-reload-client.ts, src/server/direct-local-http.ts, src/server/index.ts, src/server/proxy-liveness.ts
The client attests the runtime through /healthz, validates PID and port identity, creates a short-lived HMAC capability, and sends a bodyless POST reload request.
Capability admission and protected route
src/server/management-auth.ts, src/server/management/provider-routes.ts
The server accepts one exact process-bound capability request. The reload route validates the provider and disk snapshot before applying the change.
Persisted provider adoption and state reconciliation
src/config.ts, tests/config-user-edits.test.ts, tests/management-provider-validation.test.ts
The server adopts only the requested persisted provider, updates its baseline, preserves unrelated live state, and refreshes dependent caches and overlays.
Login integration and validation
src/oauth/login-cli.ts, tests/oauth-login-cli-live-update.test.ts, tests/key-login-live-update.test.ts, tests/local-provider-reload-client.test.ts, tests/local-management-direct-transport.test.ts, tests/server-management-auth.test.ts, tests/server-auth.test.ts, docs-site/src/content/docs/reference/cli/providers-accounts.md, structure/05_gui-and-management-api.md
Login flows report structured reload outcomes and warn when the proxy keeps the previous credential. Tests cover attestation, replay prevention, unsupported providers, runtime changes, bodyless transport, and unchanged disk configuration.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

Sequence Diagram(s)

sequenceDiagram
  participant LoginCLI
  participant ProxyHealth
  participant ManagementAuth
  participant ProviderRoute
  participant LiveConfig
  LoginCLI->>ProxyHealth: Attest PID, port, nonce, and capability version
  ProxyHealth-->>LoginCLI: Return runtime proof
  LoginCLI->>ManagementAuth: Send bodyless provider reload capability
  ManagementAuth->>ProviderRoute: Admit single-use request
  ProviderRoute->>LiveConfig: Adopt validated persisted provider
  LiveConfig-->>ProviderRoute: Refresh live state
  ProviderRoute-->>LoginCLI: Return reload outcome
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the OAuth fix that prevents credential-bearing live provider reloads without verified proxy identity.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4be2c6dced

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/oauth/login-cli.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/oauth-login-cli-live-update.test.ts`:
- Around line 61-75: Update the test handler around notifyRunningProxy to
increment a dedicated counter when url.pathname is "/healthz". After awaiting
notifyRunningProxy, assert that the health-probe counter is greater than zero,
then retain the existing receivedPaths empty assertion to verify the management
POST remains blocked.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 899560d5-1817-4349-82b6-ceb4ad0bca10

📥 Commits

Reviewing files that changed from the base of the PR and between d03755e and 4be2c6d.

📒 Files selected for processing (2)
  • src/oauth/login-cli.ts
  • tests/oauth-login-cli-live-update.test.ts

Comment thread tests/oauth-login-cli-live-update.test.ts
@luvs01
luvs01 force-pushed the agent/verify-live-provider-update branch from 4be2c6d to a11d672 Compare August 12, 2026 16:54
@luvs01 luvs01 changed the title fix(oauth): require verified proxy for live provider updates fix(oauth): attest credential-free live provider reloads Aug 12, 2026
@luvs01

luvs01 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Exact-head handoff for a11d67257 on current dev (cce631179): both review threads are resolved, focused dual-runtime verification and TypeScript checks pass, privacy scan and diff check pass, and the final security review found no P0-P2 issue. The live update now sends no provider credential or reusable management token; it uses an exact one-shot capability and reloads the named provider from the protected disk snapshot. The remaining unsponsored_surface result is the expected authentication-surface gate. Please review this exact head for maintainer-sponsored and approve the exact-head GitHub Actions when appropriate.

@luvs01
luvs01 force-pushed the agent/verify-live-provider-update branch from a11d672 to de921ff Compare August 12, 2026 17:49
@luvs01

luvs01 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Exact-head update for de921ff on current dev c125b61: the rebase is conflict-free, the exact post-rebase capability and reload contracts pass on Bun 1.3.14 and Bun 1.4, TypeScript checks and privacy scans pass on both runtimes, diff-check is clean, and all review threads remain resolved. The full local suite was not duplicated because GitHub CI owns that workload. The remaining unsponsored_surface result is the expected authentication-surface gate; please review this exact head for maintainer-sponsored and approve exact-head GitHub Actions when appropriate.

@Wibias Wibias left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep security + normal review completed on de921ff8213e5e69797b0eaa9861cc8e6f1e733f.

I found no actionable correctness or security finding in the current diff.

Security paths reviewed end to end:

  • CLI -> protected runtime record -> fresh /healthz HMAC attestation -> bodyless reload request
  • domain-separated HMAC capability binding to provider name, method, exact path, nonce, PID, port, and 10 s expiry
  • one-shot replay protection and exact body/path/query checks in management auth
  • listener replacement/runtime-record recheck between proof and POST
  • direct TCP transport, including proxy-header stripping, no environment proxy, bounded response framing, and no redirect path
  • management principal propagation into the exact reload route
  • disk snapshot authority, same-buffer SHA-256, post-DNS exact-byte recheck under the config mutation lock, and named-row-only adoption
  • provider destination validation, namespace collision checks, live-baseline preservation, and cache/state invalidation
  • credential exposure: provider/API/OAuth values and the reusable management token no longer cross this reload socket

I also checked the previous P1 listener-impersonation finding. The current design removes that credential-bearing attack path rather than merely adding another listener identity heuristic.

Residual assumptions I did not treat as new findings: compromise by another process running with the same protected config/runtime-state access, and the repository's existing provider-DNS rebinding/config-time resolution semantics.

I am not approving yet for process reasons only. This touches shared OAuth/config/auth/server code, and the repository guidance requires the full test suite before approval. The PR explicitly leaves the repository-wide suite to GitHub CI, while the current Cross-platform CI and React Doctor runs are still action_required. Once the required full CI has actually run green on this exact head, I have no code/security blocker.

The reload result was discarded and login printed unconditional success. A CLI
talking to a proxy that predates attested reload would persist the credential
and report success while the running process kept routing with the old one,
with nothing telling the operator a restart was needed.

Return the outcome from notifyRunningProxy and distinguish the two null cases
(nothing listening, or a provider outside the live-reload allowlist) from a
real failure, warn on failure after the success line, and document the restart
requirement.
@lidge-jun

Copy link
Copy Markdown
Owner

Review: LAND (one Medium fixed on top)

Threat-model reviewed against the current tree with an independent reviewer, then verified on a Linux CI host. The contributor commit is preserved; my fix is a separate commit on top.

What holds up

The capability is minted over nonce + method + path + provider + PID + port + expiry and verified with timingSafeEqual after an equal-length check (src/lib/local-provider-reload-contract.ts:68-99), which matches the repo convention. Provider, method, path, and expiry are genuinely load-bearing: change any of them and the MAC no longer verifies, so a captured capability cannot be steered at a different provider or endpoint. Exact replay is rejected by the consumed-capability map, and a process restart rotates the attestation secret anyway.

The 10-second window uses Date.now(). That is a wall clock, but the failure direction is safe — a jump forward expires the capability and a jump backward pushes expiry past now + TTL, so both are rejected. Ordinary clock movement fails closed.

The config-digest recheck is a real bounded TOCTOU fix, not a decorative one: the digest is computed over the same buffer that is subsequently parsed and adopted under the mutation lock, so there is no re-read gap. A non-cooperating filesystem editor can still write immediately after the final read, but no in-process lock can prevent that and the codebase already acknowledges the limit.

The consumed-capability map is bounded at 256 entries with expired entries pruned on valid admission, so it is not a memory-growth vector.

One thing the description overstates

PID and port binding sounds stronger than it is. Both are observable or guessable; neither adds entropy. The security root is possession of the per-process attestation secret, and the nonce supplies uniqueness rather than secrecy. PID/port are useful defense-in-depth against stale runtime records and endpoint replacement, not against a leaked secret. Worth stating plainly so nobody later relies on them as authentication.

The Medium I fixed

notifyRunningProxy() discarded the LocalProviderReloadResult, and OAuth login printed unconditional success. Update the CLI while an older proxy is still running and the outcome was: credential correctly written to disk, reload silently refused, running proxy keeps serving the previous credential, operator told "Logged in". Disk state was never at risk — but a login that appears to work and does not is the kind of thing that gets diagnosed an hour later.

Now the outcome is returned and surfaced, with the two harmless cases (nothing listening, provider outside the live-reload allowlist) kept distinct from a real failure so they do not produce noise:

⚠️  A proxy is running but could not reload this provider (unattested-target).
   The credential is saved to disk; the running proxy keeps using the previous one.
   Restart it to pick this up: ocx restart

The restart requirement is now documented in docs-site/src/content/docs/reference/cli/providers-accounts.md.

A note on my own fix, because it is a good trap. My first attempt wrote onLiveReload?.(await notifyRunningProxy(name)). That reads as "reload, then hand the result to an optional callback", but optional-call short-circuiting skips the entire argument list when the callback is absent — so the reload stopped happening for every caller that did not pass one. tests/key-login-live-update.test.ts caught it immediately by asserting the live config actually carried the overlay. I split the await out and added a test that pins the callback-free shape specifically.

Verification

Linux CI host, Bun 1.3.14, at the pushed head 17e4b6a22.

Check Result
bun x tsc --noEmit exit 0
Login/live-update trio 12 pass / 0 fail
Wider suite (8 files: management auth, provider validation, proxy liveness, server auth, config) 389 pass / 0 fail
bun run privacy:scan passed

The regression above was confirmed red before the fix (1 fail) and green after.

@lidge-jun
lidge-jun marked this pull request as ready for review August 12, 2026 18:17
@lidge-jun
lidge-jun merged commit fb4f2fe into lidge-jun:dev Aug 12, 2026
3 of 6 checks passed
lidge-jun added a commit that referenced this pull request Aug 12, 2026
Also records the #1302 reproduction seen while running campaign CI: three
spinning bun test processes, State R with wchan 0, growing epoll/eventfd
counts, and an open SQLite journal — diagnosis only, the issue stays open.
Wibias pushed a commit that referenced this pull request Aug 12, 2026
…reload call

#1541 (fb4f2fe) added a seventh 'await convergeCodexCatalog()' to
provider-routes.ts in the new attested credential-free provider-reload
route, but the inventory contract still expected 6, leaving dev CI red
for every open PR. Update the expected count and test title to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wibias added a commit that referenced this pull request Aug 12, 2026
…ovider-reload

test(codex-convergence): update route inventory for #1541's attested reload call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants