🤖 fix: report backup repository access denial instead of a login failure - #4226
Conversation
…ailure When the remote accepts a credential but refuses the repository (GitHub 403 "Write access to repository not granted", "Permission to X denied to Y", "Repository not found", or a read-only deploy key), the backup ladder now throws BackupAuthFailedError carrying the remote's own reason and the denied credential instead of telling the user to run `gh auth login`. The denying rung's diagnosis wins over a later rung that merely had no credential to offer, so the informative denial is no longer lost behind the ambient rung's failure. Credential labels move to src/constants/backup.ts so the backend message and the Settings UI share them.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Server progress also arrives as remote: lines, so pick the remote line that matched the access-denied classifier before falling back to the first one.
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1adb0ed5d9
ℹ️ 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".
…ccepted-credential claim The classifier can match only the fatal line while an earlier remote: line is progress, so search remote and fatal lines for the match before falling back. A bare 403 or "not found" does not prove the credential was accepted, so the message stops asserting that and keeps the URL and permission guidance.
Summary
When the backup repository refuses a credential the host already accepted, the Settings > Backup section now says so, naming the refused credential and quoting the remote's own reason, instead of telling the user to run
gh auth login.Background
With a wrapper-managed
gh(for example on a Coder workspace),gh auth statussucceeds, but pushing to a backup repository the token cannot write returnsremote: Write access to repository not granted.followed byThe requested URL returned error: 403.runGitWithCredentialLaddermatched that as an authentication failure and threw the fixed message "Could not authenticate to the backup repository. Check your SSH key orgh auth login", sending a logged-in user to log in again. The throwncausewas also only the ambient rung's failure, so the gh rung's informative denial disappeared whenever the ambient rung failed differently (for example "could not read Username ... terminal prompts disabled").Implementation
credentials.ts:ACCESS_DENIED_PATTERN(403, "Write access to repository not granted", "Permission to X denied", "Repository not found", read-only deploy key) selects, among all recorded rung failures in ladder order, the first the remote recognised and refused. The diagnostic line that matched the classifier (aremote:orERROR:line, else thefatal:line) and the credential label go into the message. Pure login failures keep the existing message. The error code staysAUTH_FAILED, so there is no IPC or schema change and theinstanceofchecks ingitRepo.tsare unchanged.src/constants/backup.ts: sharedBACKUP_CREDENTIAL_LABELS, used by the backend message and byBackupSection.tsx(replaces the localgetCredentialLabel).credentials.test.ts(a 403 denial names the gh rung and quotes the reason; the denying rung wins over a credential-less ambient rung; an sshERROR: Permission to ... deniednames the ssh rung) plus a login-guidance assertion on the existing exhausted-ladder case.Validation
Risks
Low. Classification only changes which message is thrown after every rung has already failed; ladder order and retry behaviour are unchanged. The one widening is that a read-only deploy key ("marked as read only") now counts as an auth failure, so the ambient rung gets a turn instead of the raw git error surfacing.
Follow-up (pre-existing, out of scope)
UAT also found that an ssh host-key verification failure (
Host key verification failed, here fromcoder gitsshwithout a known_hosts entry) still surfaces raw multi-line stderr in the error area. That path matched no classifier before this PR either and is untouched here; it deserves its own classifier and message.Delivery record (reviews, UAT, CI)
Reviews (process limit: 6 completed code, security, and advisory reviews per PR; 7 completed, see note)
42c9038c642c9038c642c9038c6remoteReason()quoted the firstremote:line, which could be server progress rather than the denial. P3: a bare HTTP 403 treated as proof that re-authenticating cannot help. Recommendation: specific blocker on the P2.1adb0ed5d(quote the remote line that matched the classifier; new test; red-green verified). P3 was first rejected with reasoning (401 vs 403), then fixed ina56ddd6a6after Codex raised the same point: the "signing in again will not help" sentence is gone.1adb0ed5dremote:line could be quoted over a matchingfatal:403 line. P2 thread: bare 403 / not-found treated as proof the credential was accepted.a56ddd6a6(search remote and fatal lines for the classifier match; sentence removed); replied on both threads and resolved them.1adb0ed5da56ddd6a6a56ddd6a6Process note: the six-review limit was exceeded by one. This repository's Codex integration starts a code and a security review automatically on every push ("New commits" trigger); rows 6 and 7 were not requested and cannot be suppressed without changing repository settings, which was out of bounds. No further review was requested after row 5.
Remote dogfood UAT (Coder Agents, vibe-coding template; evidence kept in the driving workspace under
.mux-uat/, not committed)42c9038c6(remote chatc742c3f5): the remote agent's own verdict was FAIL, twice. A separate critical UAT runner re-examined the evidence, rejected duplicate and cropped screenshots, forced re-proofs, and endorsed PASS for every claim in scope: 403 denial via "Back up now", 404 "Repository not found" via Validate, login message with no credential available, network error for an unreachable host, successful backup to a writable repository, rendering at 1280 px and 375 px, ssh-rung denial after populating known_hosts. The FAIL came from one pre-existing defect outside this change (below).1adb0ed5d: PASS, superseded by the next push.a56ddd6a6(same chat, remotegit rev-parse HEADand the app version badge both confirm the SHA): PASS. 403 via "Back up now" and 404 via Validate both render the final wording; DOM checks confirm the removed sentence andgh auth loginare absent. The remote checkout reported-dirtybecause its bun 1.2.15 rewrotebun.lock(repo pins bun 1.3.5): the diff is aconfigVersionheader plus re-hoisting of the type-only packages@types/nodeandundici-types; no runtime package changed and nothing undersrc/differed, so the tested build is dependency-equivalent to the committed lockfile.Deferred, pre-existing: an ssh host-key verification failure (
Host key verification failed, here fromcoder gitsshwithout a known_hosts entry) surfaces raw multi-line stderr in the Backup error area. It matched no classifier before this PR and is untouched by it. Owner: next backup follow-up PR. Trigger: add a host-key classifier and message incredentials.tsnext to the network and login classifiers.CI: on
42c9038c6, attempt 1 failed on "Codex Comments" (ran before Codex answered) and "Test / Unit" (Bun 1.3.5 segfault, zero failing tests). Ona56ddd6a6, attempt 1 failed only on "Codex Comments" for the same timing reason; the failed jobs were rerun once Codex had completed.Generated with
xum• Model:anthropic:claude-fable-5-1• Thinking:xhigh