Skip to content

feat(selfhost): rotate subscription CLI credentials at runtime without a restart - #9548

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
feat/runtime-credential-rotation
Jul 28, 2026
Merged

feat(selfhost): rotate subscription CLI credentials at runtime without a restart#9548
loopover-orb[bot] merged 1 commit into
mainfrom
feat/runtime-credential-rotation

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Rotating a subscription-CLI credential on an ORB host was a manual, multi-step process with two failure modes that are both silent — the container stays healthy and the status stays green while every review quietly degrades to the fallback provider. Both were hit for real on the hosted box:

  1. Shape. loadFileSecrets only .trim()s the file — it does not strip comments or select a line. A human-added label line above the value became part of the credential.
  2. Inode. A Compose secrets: entry is a bind mount pinned to the inode. Writing in place propagates instantly; write-new-then-rename leaves the container serving the old bytes, and docker compose up -d does not repair it (it prints Container Running and changes nothing when the config is unchanged).

On top of that, rotating required a container recreate at all — dropping in-flight reviews for a credential swap that should be a no-op.

Call-time resolution

createClaudeCodeAi now resolves the token per call instead of reading a value frozen into process.env at boot, mirroring how codex's auth.json is already re-resolved per call. Precedence, highest first:

  1. A DB-backed fleet credential, when one is stored.
  2. A fresh read of CLAUDE_CODE_OAUTH_TOKEN_FILE — but only when the boot loader actually sourced the live value from that file. docker-compose.yml sets <NAME>_FILE unconditionally, so its mere presence proves nothing; an operator using an inline .env value has both set, and re-reading the file for them would silently swap their credential. loadFileSecrets now returns which vars it materialised, which is the only signal that separates the two cases — so the documented "an inline .env value always wins" precedence is preserved exactly.
  3. The boot-time env value.

Every failure degrades to the next rung rather than throwing: a half-written file caught mid-rotation, or a DB blip, must not fail a review the previous credential could still have served.

Write surfaces

Both deployment shapes are covered, because both are in use:

  • Single box, localscripts/rotate-secret.sh: validates shape, backs up, writes in place, and verifies the running container's own view matches before reporting success.
  • Single box, remote — a rotate-secret verb on the redeploy companion, plus a loopover_admin_rotate_secret MCP admin tool. This lives host-side for a hard reason: docker inspect reports the secrets mount as rw=false, so a container-side rotation endpoint is impossible, not merely undesirable. An allowlist of rotatable names keeps it from becoming a general "write any file as me" primitive.
  • FleetPOST/GET/DELETE /v1/internal/provider-credentials/:provider, backed by a new provider_credentials table reusing the existing BYOK AES-256-GCM envelope. GET never returns the credential, only configured/last4/timestamps. A fleet has no shared filesystem for the file path to use.

For claude_code_oauth_token no restart is needed at all now. Codex was already hot (its auth.json is re-resolved per call) — the script and companion cover it, and its docs are corrected.

Scoping note

The fleet (DB) path is wired for claude-code. Codex's fleet story stays host-side (per-host companion) rather than DB-backed, because materialising a DB credential for codex would mean writing auth.json per call and interacting with assertCodexCredentialIsolation, a deliberate fail-closed invariant. Weakening that to save a config step is a bad trade; the table and registry are provider-generic so it remains a small follow-up if wanted.

Validation

  • npm run typecheck — clean (0 errors)
  • npm run test:changed476 files, 11,486 tests pass, no regressions
  • npm run db:migrations:check — contiguous 0001..0196
  • npm run db:schema-drift:check — schema matches migrations
  • npm run ui:openapi:check / npm run selfhost:env-reference:check — no drift
  • bash -n + shellcheck clean on the new script
  • Patch coverage 430/431 = 99.77% on changed src/** lines (the one line is repositoryLinearKeys' pre-existing updatedAt default, pulled into the diff by hunk alignment, not new code)

Coverage was verified by cross-referencing the diff's changed line numbers against a JSON coverage report, not by reading a summary percentage. Both sides of every new ??/ternary/&& are exercised, including: the resolver's five fallback rungs, an empty file mid-rotation, an unreadable file, a throwing resolver, a rotation refusal carrying no error message, and a non-Error rejection.

Two things the tests pin explicitly:

  • Regression: a value with a comment/label line above it is refused at every layer (script, companion, MCP schema, HTTP schema).
  • Backward compat: a companion request with no action still redeploys — the app container and the host companion upgrade independently, so a new companion must keep serving an old client.

One piece of dead code was found and removed rather than tested around: the route's empty_credential catch was unreachable, since the zod schema already rejects an empty credential with a 400. The repository-level guard stays for direct callers.

Safety

  • No credential material in any audit row, tool result, summary, log line, or error path — asserted by a test that greps the serialized tool result for the value.
  • GET returns configured/last4 only; a test asserts the response body does not contain the credential.
  • Negative-path tests: the ordinary mcp actor and a session identity are both rejected from the admin tool; the internal routes 401 without INTERNAL_JOB_TOKEN; an unexpected storage failure propagates as a 500 rather than being mislabelled a credential error.
  • POST returns 503 rather than storing anything in the clear when TOKEN_ENCRYPTION_SECRET is unset.
  • The new MCP tool is gated behind both LOOPOVER_MCP_ADMIN_ENABLED (registration) and mcp-admin actor (call time), unchanged from the existing admin tools.

Note

claude_code_error_429 is a quota signal, not an auth signal — rotating a credential does not clear it. Keeping the two visibly distinct is filed in the issue as follow-up, not done here.

Closes #9543

@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-28 08:21:15 UTC

25 files · 1 AI reviewer · no blockers · readiness 86/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR wires a per-instance subscription-CLI credential rotation path: a new `provider_credentials` DB table + repository/API surface for fleet-mode rotation, a host-side `rotate-secret` verb on the redeploy companion for single-box rotation, and call-time (not boot-time) resolution of the Claude OAuth token in `src/selfhost/ai.ts` via a nullable registry, with a precedence chain (DB > freshly-read file (only if boot-sourced from file) > boot env). The design is coherent — the `wasLoadedFromFile` signal correctly preserves the documented inline-.env-wins precedence, and every failure path degrades rather than throws, matching the stated goal of never silently blocking a review. Validation of the credential shape (single line, no comment, no leading '#') is duplicated across the shell script, the companion, and the MCP/API schema, but each copy is consistent.

Nits — 7 non-blocking
  • scripts/redeploy-companion.ts and src/api/routes.ts / src/mcp/server.ts triplicate the single-line/no-comment/no-whitespace credential validation logic (isValidSecretValue vs the zod refinements) — consider extracting one shared validator to avoid the three copies drifting.
  • src/db/repositories.ts's upsertProviderCredential comment calls out the `empty_credential` throw as unreachable through the API route, but it's still worth a short inline test note since dead-branch claims tend to rot silently.
  • Several new magic numbers (4096 byte limits, 0o644/0o600 modes) are repeated across files without a shared named constant — minor but easy to consolidate.
  • scripts/rotate-secret.sh and scripts/redeploy-companion.ts's ROTATABLE_SECRETS lists must be kept in sync by hand (and don't include the same secrets, e.g. internal_job_token appears in the CLI script but not the companion) — worth a comment flagging that these two allowlists are intentionally different or unifying them.
  • Extract the shared secret-value validation (single line, no comment, no surrounding whitespace) into one function imported by scripts/redeploy-companion.ts, src/api/routes.ts, and src/mcp/server.ts.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Possible secret-shaped assignment in the diff (generic_secret_assignment) — Verify the value is not a real credential.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9543
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 326 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 326 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The diff implements call-time credential resolution with the specified precedence (fleet DB, file-sourced-only re-read via wasLoadedFromFile, boot env fallback), adds rotate-secret.sh with shape validation and in-place writes, a rotate-secret verb on the redeploy companion plus a matching MCP admin tool, a provider_credentials table with encrypted storage and internal routes, and updates load-file

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 326 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: success
  • config: 58650b16a8815d26da3a95e22fcb8b2bb90fb25b1f23adedc9b7bf381b550c9c · pack: oss-anti-slop · ci: passed
  • record: 4b715e92a60714efb3ab0cba128321b3f10c8a88af5a712b576464daa1ef1122 (schema v5, head 31500b4)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@JSONbored JSONbored self-assigned this Jul 28, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 755ee9e Commit Preview URL

Branch Preview URL
Jul 28 2026, 08:04 AM

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 8.26kB (0.11%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.66MB 8.26kB (0.11%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-CcdRgudh.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-C9X7qYKM.js (New) 865.23kB 865.23kB 100.0% 🚀
openapi.json 5.84kB 628.9kB 0.94%
assets/docs.fumadocs-spike-api-reference-CCmRBMGq.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-BxkAihiB.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-BKGhkZG8.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-CgtUtbry.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-ckQ7IKCX.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-DC3HqA1d.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-pZDGOmak.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-BYOOYAq4.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-jA89uwBJ.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-C0sy_ekS.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-a2PTbrUu.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-CCnNU90n.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-D8Mxincs.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-CFcDNz1B.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-rcBhgKQH.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-CIGtcKqw.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-7gzyiKMu.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-oePlNEr1.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-BngQ2aE9.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-ygVxCQNJ.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-C43qLtWB.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-9pAL0QXq.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-DiPPLaf7.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-JMkKegPd.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BkyHml8W.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-DUGkrrLf.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-CNL5BrRe.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-BNyTSLmx.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-CUex09D1.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-JKZIETXP.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-CdmlSHDl.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-BAqEJ8ZY.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-O3w9wXo8.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-Be3tpkdM.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-C0Mmwd8L.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-CI--PbRD.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-BZ4nnQWn.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-DtWd-Xm3.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-BCFerBfT.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-C3TGCiOl.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BVwk85HS.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-2_IzBbBx.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-CyLRHnzG.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-aBtSMNNs.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-DaBeELSQ.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-C-2KtIe5.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-CgVH7XeT.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-Ck3rqIkx.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-BUscPdty.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-C8p8QttD.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-BLLnvP40.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-8iFEB41c.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-CNb7H_vv.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-BP0yuXzI.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-D_phafsV.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-DjwlDTf3.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-BLUvVF_D.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-DRQekxke.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-CoB4l0nE.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-BiJ2EYZC.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-Dpm0HCBI.js (Deleted) -862.81kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-DUTRBWoc.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-D_KUTMH_.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-D8ojZG0Z.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-CdVzAeF0.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-CcVXeWvS.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-Cb5UhhF2.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-H4JtG5ux.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-kkE4Uzym.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-DTGFF7e-.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-98QT1fcP.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-DeKF1Hkv.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-DHqdj6kA.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-C-rttmRE.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-CUKmfdSY.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-BEoutzN5.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-Bt0_E8CI.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-7v4ddyoe.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-jHnv_uSI.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-CB5I-QrG.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-CESH99Ak.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-i9uTPDSA.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-DI88r7z8.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BfrONh82.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-jWy_lPgK.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-DjcAmw8J.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-WgNF_aYq.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-BGs3wGLm.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-DBIfA7F8.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-DQi29QIB.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-D9aBn0vT.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-DLtkrv5r.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-6OpAQym5.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-Cqn-hZiK.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-otYfnD8m.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-RShTrhm0.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-CDawofeV.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-EKi7iFBk.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-P5iPo5Zm.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-BBN0xN5d.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-Brwnzxt7.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-CXrsBKTZ.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-Bg-MmAyw.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CjqCiMUR.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-BRRrLyaa.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-CqtdDevR.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-BdgzGKiy.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CaJHUw_j.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-3h8Vdfud.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-Cbo9i_w6.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-DPCly8mS.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-BdWIzW7A.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-uUbixOap.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-fwVrLVrW.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BcjN1cpj.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-CGZYIjFk.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-DVHv5LOW.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-Cl5Typ5W.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-CzAbGLel.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-BIkmAtvM.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

…t a restart

Resolve the Claude OAuth token at AI-call time instead of freezing it into
process.env at boot, and add three write surfaces for rotating it safely.

Resolution order (highest first): a DB-backed fleet credential, a fresh read of
CLAUDE_CODE_OAUTH_TOKEN_FILE, then the boot env value. The file is only re-read
when the boot loader actually sourced the value from it, so the documented
'an inline .env value always wins' precedence is preserved; every failure
degrades to the next rung rather than failing a review.

Write surfaces:
- scripts/rotate-secret.sh for a single box, validating shape and writing in
  place so the container's inode-pinned bind mount sees the change immediately
- a rotate-secret verb on the redeploy companion, plus a
  loopover_admin_rotate_secret MCP admin tool, since the app container cannot
  write its own secrets (the mount is read-only)
- INTERNAL_JOB_TOKEN-gated /v1/internal/provider-credentials/* backed by a new
  provider_credentials table, encrypted with the existing BYOK AES-256-GCM
  envelope, for fleets with no shared filesystem

This closes two silent failure modes seen in production: a label line above the
value became part of the credential (the loader only trims), and a
write-new-then-rename left the running container serving the old value while
still reporting healthy.

Closes #9543
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.59%. Comparing base (8751b9a) to head (755ee9e).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9548      +/-   ##
==========================================
+ Coverage   89.58%   89.59%   +0.01%     
==========================================
  Files         846      848       +2     
  Lines      110384   110501     +117     
  Branches    26295    26325      +30     
==========================================
+ Hits        98889    99006     +117     
  Misses      10231    10231              
  Partials     1264     1264              
Flag Coverage Δ
backend 95.30% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/api/routes.ts 95.60% <100.00%> (+0.04%) ⬆️
src/db/repositories.ts 96.85% <100.00%> (+0.05%) ⬆️
src/db/schema.ts 74.87% <100.00%> (+0.39%) ⬆️
src/mcp/redeploy-companion-registry.ts 100.00% <100.00%> (ø)
src/mcp/server.ts 97.19% <100.00%> (+0.05%) ⬆️
src/openapi/spec.ts 99.64% <100.00%> (+<0.01%) ⬆️
src/selfhost/ai.ts 98.35% <100.00%> (+0.03%) ⬆️
src/selfhost/file-sourced-secrets.ts 100.00% <100.00%> (ø)
src/selfhost/load-file-secrets.ts 100.00% <100.00%> (ø)
src/selfhost/provider-credential-registry.ts 100.00% <100.00%> (ø)
... and 1 more

@loopover-orb loopover-orb 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.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit c2d6102 into main Jul 28, 2026
11 checks passed
@loopover-orb
loopover-orb Bot deleted the feat/runtime-credential-rotation branch July 28, 2026 08:21
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.

Runtime credential rotation for subscription CLI providers (no restart, single-box + fleet)

1 participant