Skip to content

fix(doctor): post-eeqi credential checks + correct user guidance#602

Merged
galexy merged 4 commits into
mainfrom
worktree-fix-stale-ledger-remote-url-check
May 12, 2026
Merged

fix(doctor): post-eeqi credential checks + correct user guidance#602
galexy merged 4 commits into
mainfrom
worktree-fix-stale-ledger-remote-url-check

Conversation

@galexy
Copy link
Copy Markdown
Contributor

@galexy galexy commented May 12, 2026

Summary

Fixes two user-visible breakages introduced by PR #597 ("security: credential redaction improvements"):

  1. ox doctor reports an unfixable "Ledger remote URL match" warning on every run. The check was written for the pre-ox-eeqi world (PAT embedded in origin URL) and never updated when the credential helper migration landed. On a correctly-migrated bare URL, it always reported "missing credentials" and the auto-fix did nothing.
  2. User-facing guidance points at a flag that doesn't exist. The push-refused error message and the redact-history long help tell users to run ox doctor --check=ledger-secrets — but --check is not a flag.

Forest view

There are two models for where the ledger's GitLab PAT lives:

Model PAT location git auth via
Pre-eeqi Embedded in origin URL The URL itself
Post-eeqi Credential helper ox git-credential-helper

The codebase had two doctor checks that looked at the same origin URL and disagreed about which model was correct:

  • Ledger embedded credentials (Check A) — speaks the new model: bare URL = good.
  • Ledger remote URL match (Check B) — still speaks the old model: bare URL = "missing credentials."

Check B was registered as FixLevelAuto, fired on every run, and its "fix" called MigrateLedgerCredentials (which strips the PAT — correctly, per the new model). Next run, Check B still saw a bare URL, complained again, and called the fix again. Forever.

What this PR does

Removes the stale check and its scaffolding.

  • checkLedgerRemoteURLMatch + CheckSlugLedgerRemoteURLMatch + registry entry
  • checkLedgerRemoteURL wrapper + CheckSlugLedgerRemote + registry entry
  • extractPATFromURL, fixLedgerStalePAT (helpers exclusive to Check B)
  • fixRemote parameter from checkLedgerGitHealth, plus its two inline call sites in doctor.go

Wires up two registered-but-dead checks so the doctor pipeline actually exercises them.

  • Ledger embedded credentials was registered (slug ledger-embedded-creds) but never invoked from runDoctorChecks — now runs in the Ledger Git Health category.
  • Ledger remote URL vs API (ledger-url-api-match) was registered but never invoked — same fix.

Extends Check A to catch a silent-success trap that the deleted Check B accidentally papered over: a bare HTTPS origin with no credential helper installed in .git/config would pass with the original no embedded PAT logic, then fetch/push would fail at auth. Check A now verifies that credential.https://<host>.helper is configured for the host and offers a fix that installs it. (Codex adversarial review round 1.)

Fixes a security regression in checkLedgerURLAPIMatch's fix path. It was rebuilding the corrected origin URL with parsed.User = url.UserPassword("oauth2", creds.Token) — re-embedding the PAT into origin, directly violating the post-eeqi invariant. The fix is now extracted into applyCorrectedLedgerURL, which writes a bare URL and then calls MigrateLedgerCredentials for the host. (Codex adversarial review round 2.)

Updates user-facing strings that promised the non-existent --check=ledger-secrets flag:

  • cmd/ox/prepush_scan.go:210 — push-refused message now points at ox session redact-history --dry-run (which exists, performs a thicker audit than the hypothetical --check would have, and classifies findings as pushed vs unpushed).
  • cmd/ox/session_redact_history.go — header comment and Long help corrected.
  • cmd/ox/doctor_ledger_secrets.go — doc comment for checkLedgerSecrets no longer claims to implement a flag that doesn't exist.
  • cmd/ox/prepush_scan_test.go:219 — assertion updated to match the new message.

Conceptual flow

graph TD
    A[ox doctor runs] --> B{origin URL state}
    B -->|embedded oauth2:TOKEN| C[Check A: FAIL — leak]
    B -->|bare URL + helper installed| D[Check A: PASS]
    B -->|bare URL + no helper| E[Check A: FAIL — auth will break]
    C -->|--fix| F[MigrateLedgerCredentials:<br/>strip PAT + install helper]
    E -->|--fix| F
    F --> D

    G[checkLedgerURLAPIMatch runs] --> H{local URL == API URL?}
    H -->|yes| I[PASS]
    H -->|no| J{--fix?}
    J -->|no| K[FAIL — URL mismatch]
    J -->|yes| L[applyCorrectedLedgerURL:<br/>set bare URL +<br/>install helper]
    L --> M[verify with ls-remote]
Loading

Tests added

  • TestLedgerHasCredentialHelper_{NotConfigured,Configured} — Codex round 1 regression.
  • TestLedgerOriginState_{BareURL,NonHTTPSReturnsNoHost} — host-resolution invariants.
  • TestApplyCorrectedLedgerURL_LeavesOriginBare — load-bearing security regression: the URL-API-match fix must never write an oauth2:TOKEN@ form to origin.
  • TestApplyCorrectedLedgerURL_InstallsHelper — Codex round 2 regression: helper must be installed after URL correction.

Test plan

  • make test — 14,382 pass, 0 fail
  • go vet ./cmd/ox/ clean
  • End-to-end on a real ledger: stale "Ledger remote URL match" warning is gone; Ledger embedded credentials passes with precise "ox credential helper installed" message; Ledger remote URL vs API runs and gracefully skips with "API unavailable" when offline
  • Two foreground Codex adversarial reviews (/codex:adversarial-review); both substantive findings addressed and covered by regression tests

Out of scope (filing as follow-ups)

  • docs/human/security/credential-redaction.md has 5 remaining references to --check=ledger-secrets / --check=ledger-embedded-creds. File is tagged <!-- doc-audience: human --> so per CLAUDE.md it's not for me to rewrite.
  • The session credential leakage causing Ledger branch status (reconcile failed) on the reporter's ledger — that's the pre-push gate doing its job; content needs to be redacted via ox session redact-history, not a doctor bug.

🤖 Generated with Claude Code

Co-Authored-By: SageOx

Summary by CodeRabbit

  • Bug Fixes
    • Improved ledger Git health checks to stop embedding tokens in origin URLs, ensure credentials are managed via the Git credential helper, and verify connectivity after fixes.
  • Tests
    • Added regression tests covering corrected ledger URL handling and installation/verification of credential helpers.
  • Documentation
    • Updated pre-push remediation guidance to recommend ox session audit + ox session redact; clarified ox session redact documentation.

Review Change Stack

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: SageOx <ox@sageox.ai>
SageOx-Session: https://sageox.ai/repo/repo_019c5812-01e9-7b7d-b5b1-321c471c9777/sessions/2026-05-11T22-36-galexy-OxE2aB/view
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b66e0df-fd8f-406e-8393-7975e231ddcf

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe6246 and af2ae06.

📒 Files selected for processing (1)
  • cmd/ox/session_redact_history.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/ox/session_redact_history.go

📝 Walkthrough

Walkthrough

The ox doctor command's ledger health diagnostics were restructured to eliminate in-URL PAT checks and instead rely on credential-helper-based detection and remediation; origin parsing and helper verification were added, URL corrections now write bare origins and install helpers, and guidance/tests were updated.

Changes

Ledger Credential Handling Refactor

Layer / File(s) Summary
Remove deprecated ledger-remote check infrastructure
cmd/ox/doctor_types.go, cmd/ox/doctor_check_registry.go, cmd/ox/doctor_git_repos.go, cmd/ox/doctor_ledger_git.go
Delete CheckSlugLedgerRemote and ledger-remote-url-match registrations and implementations; remove helper functions and recategorize embedded-creds into Ledger Git Health.
Expand credential detection with origin state and helper verification
cmd/ox/doctor_ledger_secrets.go, cmd/ox/doctor_ledger_secrets_test.go
Add ledgerOriginState to parse origin URL and return (hasEmbeddedPAT, host); add ledgerHasCredentialHelper to check git-config helper; rework checkLedgerEmbeddedCreds to validate both axes and run MigrateLedgerCredentials on fix with post-migration verification; add unit tests for HTTPS and non-HTTPS remotes and helper presence.
Refactor ledger URL correction to use bare origins and credential helpers
cmd/ox/doctor_ledger_git.go, cmd/ox/doctor_ledger_git_test.go
Introduce applyCorrectedLedgerURL that strips userinfo, sets origin to a bare API URL, calls gitserver.MigrateLedgerCredentials for the host, and performs a timed git ls-remote to verify connectivity; remove old token-rebuilding fix; add regression tests ensuring origin is bare and helper installed.
Update checkLedgerGitHealth wiring and remote URL checks
cmd/ox/doctor.go
Change checkLedgerGitHealth signature to accept fixEmbeddedCreds and fixURLAPIMatch, wire new per-check fix flags at call sites, and adjust the earlier remote-URL-checks block to focus only on team-context remote URL checks.
Update user-facing credential remediation guidance
cmd/ox/prepush_scan.go, cmd/ox/prepush_scan_test.go, cmd/ox/session_redact_history.go
Change remediation guidance to recommend ox session audit and ox session redact instead of ox doctor --check=ledger-secrets; update tests and reword session-redact header comment formatting/tool refs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • sageox/ox#462: Both PRs modify checkLedgerGitHealth wiring in cmd/ox/doctor.go to change parameter handling and gate additional ledger checks.

Poem

🐇 I scrubbed the ledger, stripped secrets from view,
No PATs in URLs—helpers do the glue,
Remotes now bare, helpers set just right,
A rabbit hops off, the repo sleeps tight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main changes: removing a stale doctor check and correcting user guidance for credential remediation.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-stale-ledger-remote-url-check

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@cmd/ox/doctor_ledger_git_test.go`:
- Line 40: The test contains a PAT-shaped literal
"https://oauth2:glpat-secret-token-do-not-leak@git.sageox.ai/team/ledger.git"
which triggers secret detectors; replace it with a non-PAT placeholder or
construct the URL at runtime (e.g., use an env var or join pieces like "glpat-"
+ "REDACTED") so the literal pattern is broken. Update the test in
doctor_ledger_git_test.go to use the placeholder constant or
os.Getenv("TEST_GIT_URL") and adjust any assertions accordingly.

In `@cmd/ox/doctor_ledger_git.go`:
- Around line 602-606: When url.Parse(apiURL) fails in the remediation path,
treat it as a failed remediation rather than a warning: replace the WarningCheck
creation with a FailureCheck (or the code path your checker uses for failed
fixes) and return that failure result (include the error text via
parseErr.Error()); ensure the created check uses a message like "failed to fix
(invalid API URL)" and still references checkName and parseErr so callers see
the concrete parse error when --fix is used (update the code that currently
creates r := WarningCheck(checkName, "cannot fix (invalid API URL)",
parseErr.Error()) to construct a failure result instead).
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dcb9836e-81bc-475f-8ca2-ab2c7d8176a8

📥 Commits

Reviewing files that changed from the base of the PR and between 8967a47 and 26ecfb4.

📒 Files selected for processing (11)
  • cmd/ox/doctor.go
  • cmd/ox/doctor_check_registry.go
  • cmd/ox/doctor_git_repos.go
  • cmd/ox/doctor_ledger_git.go
  • cmd/ox/doctor_ledger_git_test.go
  • cmd/ox/doctor_ledger_secrets.go
  • cmd/ox/doctor_ledger_secrets_test.go
  • cmd/ox/doctor_types.go
  • cmd/ox/prepush_scan.go
  • cmd/ox/prepush_scan_test.go
  • cmd/ox/session_redact_history.go
💤 Files with no reviewable changes (2)
  • cmd/ox/doctor_types.go
  • cmd/ox/doctor_git_repos.go

Comment thread cmd/ox/doctor_ledger_git_test.go Outdated
Comment thread cmd/ox/doctor_ledger_git.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
cmd/ox/doctor_ledger_secrets.go (3)

143-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make pre-scan hydration failures block a clean result.

hyd.Failed is only folded into SessionsSkipped, but the decision matrix below only looks at SessionsHydrationFailed. If the pre-pass misses sessions and the later scan finds no secrets, this path can still return a clean result with incomplete coverage. Either carry hyd.Failed into SessionsHydrationFailed as well, or gate the switch on a separate coverageIncomplete flag that includes both sources.

🤖 Prompt for 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.

In `@cmd/ox/doctor_ledger_secrets.go` around lines 143 - 145, The current logic
adds hyd.Failed to result.SessionsSkipped but the final decision only checks
result.SessionsHydrationFailed, allowing a false "clean" result when pre-scan
hydration missed sessions; update the handling so pre-scan failures affect the
coverage decision: either increment result.SessionsHydrationFailed by hyd.Failed
in the same place where you increment SessionsSkipped, or introduce a
coverageIncomplete boolean that is set if hyd.Failed>0 or
result.SessionsHydrationFailed>0 and use that in the switch/decision below
(refer to hyd.Failed, result.SessionsSkipped, result.SessionsHydrationFailed,
and the switch that determines the clean result).

313-317: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t count unreadable files as successfully scanned.

scanLedgerFileForSecrets currently returns nil on os.Open and scanner.Err() failures, and the caller increments FilesScanned / anyFileRead before it knows the scan succeeded. That lets unreadable or truncated session files still count as scanned, which can preserve a false “clean” audit despite incomplete coverage.

💡 Suggested fix
-			anyFileRead = true
-			result.FilesScanned++
 			relForReport := filepath.Join("sessions", sessionName, filename)
 			if err := scanLedgerFileForSecrets(redactor, contentPath, relForReport, info.ModTime(), result); err != nil {
 				anyFileFailed = true
 				continue
 			}
+			anyFileRead = true
+			result.FilesScanned++
 	f, err := os.Open(abs)
 	if err != nil {
-		return nil // skip unreadable files; audit is best-effort
+		return fmt.Errorf("open %s: %w", rel, err)
 	}
@@
 	if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) {
-		return nil
+		return fmt.Errorf("scan %s: %w", rel, err)
 	}

Also applies to: 373-389

🤖 Prompt for 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.

In `@cmd/ox/doctor_ledger_secrets.go` around lines 313 - 317,
scanLedgerFileForSecrets currently swallows I/O errors (os.Open and scanner.Err)
causing the caller to mark files as scanned even when unreadable; update
scanLedgerFileForSecrets to return a non-nil error on open/read/scan failures
and only set anyFileRead and increment result.FilesScanned in the caller after
scanLedgerFileForSecrets returns nil (look for the caller code around the
assignment to anyFileRead and result.FilesScanned in doctor_ledger_secrets.go).
Also apply the same change to the other caller block noted (around the 373-389
area) so unreadable/truncated session files are not counted as successfully
scanned.

209-217: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Reuse getLedgerPath() instead of adding another resolution path.

This helper duplicates ledger-path resolution inside cmd/ox, which makes it easy for doctor to diverge from the rest of the CLI if the canonical lookup chain changes again. Reusing getLedgerPath() here keeps these checks aligned with the established local-config/default fallback behavior.

Based on learnings, in the cmd/ox Go CLI commands, use getLedgerPath() as the canonical way to resolve the ledger path when repoID and endpointURL are not directly available.

🤖 Prompt for 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.

In `@cmd/ox/doctor_ledger_secrets.go` around lines 209 - 217, The helper
resolveLedgerPathForAudit duplicates ledger path resolution; replace its custom
logic with a call to the canonical getLedgerPath() used by other cmd/ox commands
so the doctor uses the same local-config/default fallback behavior. Locate
resolveLedgerPathForAudit and remove the manual localCfg/Ledger.Path and
ledger.DefaultPath() checks, instead invoking getLedgerPath() (using the same
arguments/signature used elsewhere in cmd/ox) and return its result (propagating
or handling any error per surrounding callers). This ensures consistency with
getLedgerPath() for ledger path resolution.
🧹 Nitpick comments (1)
cmd/ox/prepush_scan_test.go (1)

221-221: ⚡ Quick win

Assert the canonical remediation command explicitly

This now checks for ox session audit, but the PR objective calls out ox session redact-history --dry-run as the replacement guidance. Add an assertion for that exact command so this test guards the user-facing fix end-to-end.

Suggested diff
 	assert.Contains(t, msg, "aws_access_key")
 	assert.Contains(t, msg, "leak.jsonl:42")
 	assert.Contains(t, msg, "ox session audit")
+	assert.Contains(t, msg, "ox session redact-history --dry-run")
 	assert.Contains(t, msg, "OX_ALLOW_SECRETS=1")
🤖 Prompt for 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.

In `@cmd/ox/prepush_scan_test.go` at line 221, The test currently only asserts
assert.Contains(t, msg, "ox session audit") but needs to explicitly assert the
canonical remediation command; update the test to also assert that msg includes
the exact replacement "ox session redact-history --dry-run" (for example add
assert.Contains(t, msg, "ox session redact-history --dry-run")) so the
user-facing guidance is validated—ensure you modify the same test that
sets/reads msg and leave the existing audit assertion in place.
🤖 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.

Outside diff comments:
In `@cmd/ox/doctor_ledger_secrets.go`:
- Around line 143-145: The current logic adds hyd.Failed to
result.SessionsSkipped but the final decision only checks
result.SessionsHydrationFailed, allowing a false "clean" result when pre-scan
hydration missed sessions; update the handling so pre-scan failures affect the
coverage decision: either increment result.SessionsHydrationFailed by hyd.Failed
in the same place where you increment SessionsSkipped, or introduce a
coverageIncomplete boolean that is set if hyd.Failed>0 or
result.SessionsHydrationFailed>0 and use that in the switch/decision below
(refer to hyd.Failed, result.SessionsSkipped, result.SessionsHydrationFailed,
and the switch that determines the clean result).
- Around line 313-317: scanLedgerFileForSecrets currently swallows I/O errors
(os.Open and scanner.Err) causing the caller to mark files as scanned even when
unreadable; update scanLedgerFileForSecrets to return a non-nil error on
open/read/scan failures and only set anyFileRead and increment
result.FilesScanned in the caller after scanLedgerFileForSecrets returns nil
(look for the caller code around the assignment to anyFileRead and
result.FilesScanned in doctor_ledger_secrets.go). Also apply the same change to
the other caller block noted (around the 373-389 area) so unreadable/truncated
session files are not counted as successfully scanned.
- Around line 209-217: The helper resolveLedgerPathForAudit duplicates ledger
path resolution; replace its custom logic with a call to the canonical
getLedgerPath() used by other cmd/ox commands so the doctor uses the same
local-config/default fallback behavior. Locate resolveLedgerPathForAudit and
remove the manual localCfg/Ledger.Path and ledger.DefaultPath() checks, instead
invoking getLedgerPath() (using the same arguments/signature used elsewhere in
cmd/ox) and return its result (propagating or handling any error per surrounding
callers). This ensures consistency with getLedgerPath() for ledger path
resolution.

---

Nitpick comments:
In `@cmd/ox/prepush_scan_test.go`:
- Line 221: The test currently only asserts assert.Contains(t, msg, "ox session
audit") but needs to explicitly assert the canonical remediation command; update
the test to also assert that msg includes the exact replacement "ox session
redact-history --dry-run" (for example add assert.Contains(t, msg, "ox session
redact-history --dry-run")) so the user-facing guidance is validated—ensure you
modify the same test that sets/reads msg and leave the existing audit assertion
in place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 659b290d-f1d7-4aac-86b0-74be62bc9099

📥 Commits

Reviewing files that changed from the base of the PR and between e1d9c8e and 4fe6246.

📒 Files selected for processing (4)
  • cmd/ox/doctor_ledger_secrets.go
  • cmd/ox/doctor_ledger_secrets_test.go
  • cmd/ox/prepush_scan.go
  • cmd/ox/prepush_scan_test.go
✅ Files skipped from review due to trivial changes (2)
  • cmd/ox/prepush_scan.go
  • cmd/ox/doctor_ledger_secrets_test.go

@galexy galexy merged commit 3cef613 into main May 12, 2026
3 checks passed
@galexy galexy deleted the worktree-fix-stale-ledger-remote-url-check branch May 12, 2026 04:00
@coderabbitai coderabbitai Bot mentioned this pull request May 12, 2026
6 tasks
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.

1 participant