Skip to content

fix(accounts): remove a rejected GLM key when restore fails - #24964

Merged
nwparker merged 9 commits into
stablyai:mainfrom
parkavenue9639:fix/glm-plan-secure-save
Oct 10, 2026
Merged

nwparker merged 9 commits into
stablyai:mainfrom
parkavenue9639:fix/glm-plan-secure-save

Conversation

@parkavenue9639

@parkavenue9639 parkavenue9639 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

A failed replacement of a GLM Coding Plan key now removes the rejected key when it is still on disk. It preserves a previous key that was restored and a newer key published by another writer.

What Changed

Keep @parkavenue9639’s original fix and authored history, merge current main, and strengthen the rollback ownership check. Compare the current file with the rejected envelope before restoration or removal. The existing secure-file writer accepts an optional check immediately after staging permissions, so restoring an old key does not overwrite a competing publication observed during staging.

Retain the author’s failure-edge assertions and add real disposable-file tests for failed restoration before and after publication, identical keys, missing files, and competing writes during permission staging. Other secure-file callers retain their existing behavior.

Why

The original report and fix are valid. Deleting unconditionally loses restored or newer credentials; restoring unconditionally can overwrite a newer credential. Reusing the existing writer permits a second ownership check without another credential store. The check and rename remain separate operations, so this does not claim an atomic filesystem transaction.

Linked Issue

Includes the separately reviewable lint prerequisite #27326. Merge that first; its identical cleanup is composed here so the final head can pass required checks.

Fixes #24963

Credential discovery (#24674) and sign-in setup (#27321) are now independent PRs without this storage change. Merge this focused safety repair first; the original author’s commits remain here.

Visual Proof

N/A — failure cleanup changes credential-file bytes, with no visible UI change.

Testing

  • Ran the production store and writer against disposable files with synthetic keys.
  • Added and ran automated regression tests.

84 tests pass across six store/secure-file suites, with one Windows-only fsync test skipped on macOS arm64. pnpm tc, the changed-code quality gate against current main, formatting, diff checks, and normal commit hooks pass.

Added an isolated macOS/Linux/Windows CI matrix and three native Windows cases. The Windows cases use the real production permission implementation and actual files; injected failures cover restoration before publication, restoration after publication, and a competing key arriving during permission staging. They assert real file and parent-directory hardening succeeded and no staged files remain. Local macOS runs pass 79 targeted tests; the three Windows-native cases correctly skip here. Typecheck passes. The native contract matrix passes on final head 77a7b5ec7552 across macOS, Linux, and Windows. Windows runs all three native ACL cases, with 80 tests passed and two POSIX-only skips. Native verification. The standard Windows CI selection list and Windows boundary-test command now also register this test, as required by the repository-wide registration contract. All 31 registration checks pass locally. Hosted checks are rerunning after this configuration fix; older green checks are not claimed as proof of the new head. Real OS denial and competing external processes remain unverified.

Review

The execution host still owns its credential file. No provider request, personal credential read, IPC secret, new setting, required remote field, or workspace assumption is added. Plaintext saves continue reporting failure when the file could not be restricted. Unreadable bytes do not establish ownership and are left untouched.

Agent skill upstream boundary

  • Not applicable; no skill-installer material is copied.

Notes

Risk reduction: storage is isolated from discovery and UI, real-filesystem failure cases cover ownership and cleanup, and native Windows permission verification now runs on the exact PR head. Low merge risk is conditional on the new matrix and broader hosted checks passing. The comparison and rename remain separate operations; no atomic transaction is claimed.

Credit: Lu Chong / @parkavenue9639 remains the primary author of the original contribution. The maintainer follow-up reuses the stronger rollback implementation previously prepared in #24674. The original author’s AI disclosure: Claude implemented the initial change under their direction and they reviewed it.

Checklist

  • Focused credential safety change.
  • User-facing before/after, mechanism, and approach explained.
  • Visual proof marked N/A with reason.
  • Correctness, security, and competing-write behavior reviewed.
  • Cross-platform, host ownership, folder, and mixed-version impact considered.
  • Focused tests, typecheck, quality, and normal hooks pass locally.
  • New hosted matrix and broader checks pass on this head.

A failed plaintext replacement can publish the new key before file
restriction is confirmed. If restoring the previous key fails before it
replaces that file, delete only the rejected envelope so a completed
restore is left intact.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

  • Delete a rejected plaintext envelope on failed restore — saveZcodePlanApiKey now re-reads the credential file from a new removeRejectedPlaintextEnvelope helper and removes it only when the bytes still match the rejected replacement, so a restore that already published the previous key is preserved.
  • Two tests pin both restore-failure edges — one asserts deletion when the restore throws before publishing, the other asserts the file is kept when the restore throws after publishing the previous envelope.

The discriminator is sound: writeSecureFile (src/shared/secure-file.ts:146) renames the temp file into place before returning, so a false return always means the attempted bytes were published, a pre-rename throw leaves those bytes, and a post-rename throw leaves the previous bytes. The byte comparison distinguishes exactly those two cases, and readFileSync failures fall through to a no-op. Test 1 fails without the fix; test 2 guards against an unconditional delete.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e541bf0f-d29f-4e06-a3df-51585241003e





📥 Commits

Reviewing files that changed from the base of the PR and between 10a3c9a and 77a7b5e.






📒 Files selected for processing (4)
  • src/renderer/src/components/automations/use-automations-page-setup-state.ts
  • src/renderer/src/components/sidebar/folder-workspace-composer-submit-fixtures.ts
  • src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts
  • src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts





Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.







📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough

Walkthrough

writeSecureFile now accepts an optional predicate that can cancel publication after staged-file hardening. When a plaintext save fails its security check, saveZcodePlanApiKey checks the current file before restoring the previous envelope or removing the attempted one. Tests cover restore failures, competing publications, cached-key reads, and staged-file cleanup. A workflow runs credential-store and secure-file tests across Ubuntu, macOS, and Windows.












Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 77a7b

The reviewed changes have no established actionable merge risk. The possible competing-publication races lack a demonstrated production trigger, and the current rollback guard preserves an identical previous key.

Architecture Summary

Architecture risk: 🔵 Low · up to 96d14

The change affects 1 system.

Changed systems: src

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (service) was modified; 2 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in src/main/zcode/zcode-plan-api-key-store.test.ts: Adds a test where the plaintext replacement is rejected and restoring the previous encrypted envelope throws before publishing it; the test expects the secure-storage error and removal of the file.
  • observed — Modified behavior in src/main/zcode/zcode-plan-api-key-store.test.ts: Adds a test where restoring the previous encrypted envelope throws after publishing it; the test expects the secure-storage error and verifies the file is not removed.
  • observed — Modified behavior in src/main/zcode/zcode-plan-api-key-store.test.ts: Adds a test for replacing a plaintext envelope with the same key when restoration throws after publishing; the test expects the secure-storage error and verifies the file is not removed.
  • observed — Modified behavior in src/main/zcode/zcode-plan-api-key-store.ts: Adds removeRejectedPlaintextEnvelope, which reads the current file and removes it only if its contents equal the rejected attempted envelope; read failures leave the file unchanged.












Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Issue #24963 requires removal of the rejected plaintext envelope only while it still owns the file. rollbackPlaintextEnvelope compares the current bytes before recovery and removes the file only whe…
Out of Scope Changes check Passed The store and secure-file changes, regression tests, native Windows tests, and credential contract workflow support the credential-safety objective. The renderer dependency and fixture changes are ide…
Title check Passed The title clearly and concisely describes the main change: removing a rejected GLM key when rollback restoration fails.
Description check Passed The description follows the required template and explains the behavior change, rationale, linked issue, testing, platform coverage, security review, and checklist status. It also documents known veri…

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR



  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4a1b637c-bf39-4488-b769-633b0b60c6c6
📥 Commits

Reviewing files that changed from the base of the PR and between 055871a and 00fc9f8.

📒 Files selected for processing (2)
  • src/main/zcode/zcode-plan-api-key-store.test.ts
  • src/main/zcode/zcode-plan-api-key-store.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread src/main/zcode/zcode-plan-api-key-store.ts Outdated
A restored plaintext envelope can match the rejected replacement byte for
byte. Do not delete that file, since it is the previous key rather than a
new secret.
@parkavenue9639

Copy link
Copy Markdown
Contributor Author

Could you review this when you have a moment, @nwparker? It is a small follow-up to the GLM Coding Plan credential save path from #24618: a failed plaintext replacement no longer leaves a different rejected key on disk, and an identical previous key is preserved. I cannot add reviewers directly from my fork.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

  • Guard the rejected-key cleanup against an identical previous key — the restore catch now skips removeRejectedPlaintextEnvelope when the captured previous envelope is byte-identical to the attempted replacement, so re-saving the same plaintext key while encryption is unavailable no longer deletes the only key. The same guard covers the post-publish-throw variant of that case, which is indistinguishable by bytes and would otherwise delete a just-restored key.
  • Regression test for the identical-key edge — the new test fails on the pre-guard code (the previous envelope equals the attempted bytes, so cleanup deleted the file) and passes now.

This closes the edge CodeRabbit raised without changing the shared writer: the helper still re-reads and deletes only when the published bytes match the rejected attempt, and the new guard handles the one case where that comparison would misidentify the previous key as the rejected one.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@nwparker

Copy link
Copy Markdown
Contributor

Final reviewed head: 1c242de7c3743d915c94bd326dde7a66db578d57.

Recommend merge first, before #24674. All hosted PR Checks now pass, including five test shards, relay integration, and both packaging jobs. Local store/writer verification passes 84 tests, plus typecheck and changed-code quality.

The original authored commits and failure assertions are retained. The maintainer follow-up adds ownership checks around rollback publication and real disposable-file race regressions. The remaining non-atomic comparison/rename gap and native validation limits are documented in the body.

@nwparker
nwparker merged commit 33a1ddf into stablyai:main Oct 10, 2026
39 checks passed
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.

Rejected GLM Coding Plan key can remain after a failed plaintext replacement

2 participants