feat(usage): link GLM Coding Plan accounts in AI Provider Accounts - #23804
parkavenue9639 wants to merge 3 commits into
Conversation
Add a GLM Coding Plan section to Settings > AI Provider Accounts with a site selector (Z.AI / Zhipu BigModel) and an encrypted in-app API key store, extending the merged zcode provider so the saved key takes priority over ~/.zcode/cli/config.json and usage refreshes hit the selected site. A saved plan key also exempts the zcode status-bar meter from ZCode CLI PATH detection, so subscribers without the CLI still see their quota.
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change adds Z.AI and BigModel site settings, API-key storage with encrypted and plaintext envelope handling, and IPC and preload methods for credential management. Zcode quota fetching can use the saved key ahead of CLI credentials. Fetch generations prevent results from older credential configurations from replacing current state. The settings UI adds site and key controls, usage windows, and status-bar visibility based on saved-key status or CLI detection. Priority: ➖ Normal Merge Risk: 🔵 Low · up to Under an uncommon file-permission failure, an unsuccessful key replacement may take effect after restart. The change is otherwise mergeable with this known risk tracked for correction. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to The new account-linking flow keeps keys in the main process and restricts quota requests to approved sites, but credential-file permission failures and failed credential changes can leave security-sensitive state inconsistent. Access to the new credential controls from less-trusted windows also needs confirmation. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: ce99b535-f780-43d2-b135-3a469ec392f0
📒 Files selected for processing (41)
src/main/ipc/register-core-handlers/register-core-handlers.test.tssrc/main/ipc/register-core-handlers/register-core-handlers.tssrc/main/ipc/zcode-plan-credentials.tssrc/main/rate-limits/service-zcode-usage.test.tssrc/main/rate-limits/service/service-account-refresh.tssrc/main/rate-limits/service/service-configuration.tssrc/main/rate-limits/service/service-fetch-policy.tssrc/main/rate-limits/service/service-fetch-targets.tssrc/main/rate-limits/service/service-full-cycle-application.tssrc/main/rate-limits/service/service-full-cycle-preparation.tssrc/main/rate-limits/service/service-state.tssrc/main/rate-limits/service/service-types.tssrc/main/rate-limits/zcode-usage-fetcher.test.tssrc/main/rate-limits/zcode-usage-fetcher.tssrc/main/runtime/runtime-client-settings.tssrc/main/runtime/runtime-store-contract.tssrc/main/startup/main-process-account-services.tssrc/main/zcode/zcode-plan-api-key-store.test.tssrc/main/zcode/zcode-plan-api-key-store.tssrc/preload/api-types.tssrc/preload/api/agent-account-api.tssrc/preload/api/zcode-plan-credentials-bridge.tssrc/preload/index.tssrc/renderer/src/components/settings/AccountsPane.tsxsrc/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsxsrc/renderer/src/components/settings/ZcodePlanAccountsSection.tsxsrc/renderer/src/components/settings/accounts-search.tssrc/renderer/src/components/settings/zcode-plan-usage-windows.tsxsrc/renderer/src/components/status-bar/status-bar-provider-visibility.test.tssrc/renderer/src/components/status-bar/status-bar-provider-visibility.tssrc/renderer/src/components/status-bar/usage-provider-settings-target.tssrc/renderer/src/components/status-bar/use-status-bar-controller.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/shared/default-global-settings.tssrc/shared/global-settings-types.tssrc/shared/rate-limit-state-factory.tssrc/shared/rate-limit-types.test.tssrc/shared/rate-limit-types.tssrc/shared/rpc-contract/client-settings-params.tssrc/shared/zcode-plan-sites.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.
There was a problem hiding this comment.
ℹ️ No critical issues — one clear copy bug and a couple of minor suggestions.
Reviewed changes
Read the full 41-file diff for the new GLM Coding Plan linking flow in Settings → AI Provider Accounts.
- Credential store
zcode-plan-api-key-store.ts— safeStorage envelope mirror of the MiniMax store (~/.orca/zcode-plan-api-key.enc); main-only, IPC returns booleans, hardened file, plaintext fallback. Correctly matches the landed MiniMax behavior. - Fetcher priority
zcode-usage-fetcher.ts— an Orca-savedplanCredentialtakes priority over the ZCode CLI config, reusing the HTTPS/port/host allowlist;credentialSourcebecomes'orca-plan'. Host-rejection and malformed-key fallback are tested. - Service wiring —
setZcodePlanConfigResolver, config-hash +zcodeFetchGenerationinvalidation, generation-guarded apply, resolver errors surfaced as zcode-onlykeychain-unavailablestate, andRateLimitState.zcodePlanApiKeyConfiguredas the durable meter signal. The generation/invalidation shape mirrors MiniMax and the in-flight-discard case is covered. - Settings section — site picker, save/replace/forget with success/error toasts, credential-state card, live quota windows; search entries,
accounts-zcodetarget, and en/zh catalogs added. - Status-bar gating — a saved plan key exempts the zcode meter from ZCode CLI PATH detection, same exemption as MiniMax/Cursor.
ℹ️ The GLM section is not usable from the browser web client
PreloadApi.zcodePlanCredentials was added to the desktop preload, but no matching shim was added to src/renderer/src/web/preload-api/web-agent-accounts-api.ts / web-preload-api.ts, unlike minimaxCredentials. In the browser web client the section still renders, but window.api.zcodePlanCredentials resolves through the fallback proxy: getStatus() yields [] and saveApiKey() resolves undefined, so next.apiKeyConfigured in saveApiKey throws and the user gets an opaque "credential update failed" toast. MiniMax's shim returns an explicit "only available in the desktop app" message instead.
Technical details
# Web client shim for zcodePlanCredentials
## Affected sites
- `src/renderer/src/web/preload-api/web-agent-accounts-api.ts` — add a `createZcodePlanCredentialsApi()` alongside `createMiniMaxCredentialsApi`, returning `{ apiKeyConfigured: false, zcodeCliConfigured: false }` for `getStatus` and rejecting `saveApiKey` with the desktop-only explanation.
- `src/renderer/src/web/web-preload-api.ts` — register `zcodePlanCredentials: createZcodePlanCredentialsApi()`.
## Required outcome
- On the browser web client, the GLM Coding Plan section degrades like the MiniMax section: it reads as unlinked and any save attempt reports that key storage is desktop-only, rather than throwing.
- `web-preload-api-composition.test.ts` enumerates the concrete surface and will need `zcodePlanCredentials` added to its expected key list.
## Open questions for the human
- If the browser web client is intentionally out of scope for this feature (the PR notes remote/mobile), hiding the section there instead of shimming it is also fine — but the current partial wiring leaves a Save button that cannot work.ℹ️ Nitpicks
ZCODE_PLAN_SITESandisZcodePlanSiteinsrc/shared/zcode-plan-sites.tsare exported but unused; drop them or consumeisZcodePlanSitein the site-select validation instead of the inlinevalue !== 'zai' && value !== 'bigmodel'check.src/main/ipc/zcode-plan-credentials.tshas no dedicated handler test, while the parallelminimax-credentials.tshasminimax-credentials.test.tscovering the same getStatus/save/clear and argument-validation paths.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
- Reject keys with interior newlines at save time and surface an unusable saved key as its own error instead of silently switching to the ZCode CLI config's account (greptile P1). - Refuse to keep an unrestricted plaintext key when file hardening fails (greptile P1, security). - Include the resolver error in the zcode config hash, mirroring MiniMax (CodeRabbit). - Render the reset countdown from the bare duration so the copy reads 'resets in 47m' once, not 'resets in Resets in 47m' (Pullfrog); covered by a new component test.
| if (!wroteRestricted) { | ||
| rmSync(getZcodePlanApiKeyPath(), { force: true }) | ||
| throw new Error('GLM Coding Plan API key could not be stored securely on this device') | ||
| } |
There was a problem hiding this comment.
Failed replacement deletes saved key
If a user replaces an existing key while OS encryption is unavailable and file hardening fails, the write has already replaced the old file before this cleanup deletes the new one. The replacement reports failure, but the old key remains in memory, so the current session can keep showing its usage while the saved key is gone after restart. Preserve the existing credential on a failed replacement, or clear the stale cache and make the loss explicit.
There was a problem hiding this comment.
Fixed in d2f13d2: the previous envelope bytes are captured before the plaintext write, and a failed replacement now restores them (writeSecureFile with the old bytes) instead of deleting the file — only a first-time key with nothing to preserve is removed. The in-memory cache was never updated on failure, so memory and disk stay consistent (old key both places). Same fix covers the CodeRabbit major; new test: restores the previous envelope when a plaintext replacement cannot be restricted.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟠 Major · Reject insecure plaintext credentials during reads. · zcode-plan-api-key-store.ts:106-130
src/main/zcode/zcode-plan-api-key-store.ts:106-130
🔒 Security & Privacy | 🟠 Major | ⚡ Quick winReject insecure plaintext credentials during reads.
hardenExistingSecureFile()discards failed and pending hardening results. Therefore,hasZcodePlanApiKey()can report an unrestricted file as configured, andreadZcodePlanApiKey()can return and cache its plaintext payload. The startup resolver then uses that credential.The save path already deletes the file when restriction fails. Apply the same fail-closed policy to existing plaintext files. Remove the file and do not cache the key unless synchronous hardening succeeds.
Suggested fix
-import { hardenExistingSecureFile, writeSecureFile } from '../../shared/secure-file' +import { + hardenExistingSecureFile, + hardenSecurePath, + writeSecureFile +} from '../../shared/secure-file' ... -export function hasZcodePlanApiKey(): boolean { - const keyPath = getZcodePlanApiKeyPath() - if (!existsSync(keyPath)) { - return false - } - try { - hardenExistingSecureFile(keyPath) - } catch (error) { - if (!warnedZcodePlanApiKeyStatusHardenFailure) { - warnedZcodePlanApiKeyStatusHardenFailure = true - console.warn( - '[zcode] Failed to harden GLM Coding Plan API key file while checking status', - error - ) - } - } - return true +export function hasZcodePlanApiKey(): boolean { + try { + return readZcodePlanApiKey() !== null + } catch { + return false + } } ... const raw = readFileSync(keyPath) const envelope = decodeApiKeyEnvelope(raw) + if (envelope.kind === 'plaintext') { + const restricted = hardenSecurePath(keyPath, { + isDirectory: false, + platform: process.platform, + sync: true + }) + if (!restricted) { + rmSync(keyPath, { force: true }) + throw new Error('GLM Coding Plan API key could not be stored securely') + } + } cachedZcodePlanApiKey = readEnvelope(envelope)-): void { - applySecurePathRestriction( +): boolean { + return applySecurePathRestriction( targetPath, options.isDirectory, options.platform, options.sync ?? false - ) + ) === 'applied' }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 1ee3837b-d6c9-4049-9d28-8b3ed9cf581c
📒 Files selected for processing (7)
src/main/rate-limits/service/service-full-cycle-preparation.tssrc/main/rate-limits/zcode-usage-fetcher.test.tssrc/main/rate-limits/zcode-usage-fetcher.tssrc/main/zcode/zcode-plan-api-key-store.test.tssrc/main/zcode/zcode-plan-api-key-store.tssrc/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsxsrc/renderer/src/components/settings/zcode-plan-usage-windows.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/rate-limits/service/service-full-cycle-preparation.ts
- src/renderer/src/components/settings/zcode-plan-usage-windows.tsx
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
There was a problem hiding this comment.
ℹ️ No new issues from this delta itself — the follow-up commit resolves the prior Pullfrog copy bug, hardens the credential store, and adds real coverage. Holding approval only because an open thread from another reviewer on the store's failed-replacement path is still unresolved; please resolve or dismiss it before merge.
Reviewed changes
Read the fix(usage): address review on GLM Coding Plan linking commit (e3b970b) since the prior Pullfrog review, against the full PR diff for context.
- Hardened the credential store —
saveZcodePlanApiKeyrejects keys with interior CR/LF, and the plaintext fallback now checkswriteSecureFile's hardening result: on failure it removes the file and throws, so an unrestricted credential is neither reported as saved nor left on disk. - Stopped cross-account fallback —
fetchZcodeRateLimitsreturns a dedicatedThe saved GLM Coding Plan API key is unusableerror when a supplied plan credential can't be parsed, instead of silently reading a different account's quota from the ZCode CLI config. - Detected resolver errors as config changes — the zcode config hash now falls back to
zcodePlanConfigResult.error ?? '', mirroring MiniMax, so an undecryptable saved key invalidates the snapshot and resets the visible meter. - Fixed the doubled reset copy —
ZcodeUsageWindowViewformats the reset from the bareformatResetDurationrather thanformatResetCountdown, so it renders5 hours — resets in 47monce. - Coverage — new store tests (interior newline, hardening-failure refusal), rewritten fetcher tests for the no-fallback semantics, and a reset-countdown component test; 40 tests pass locally across the four touched suites.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
writeSecureFile has already replaced the credential file when it reports that restriction failed, so deleting the result also destroyed the user's previous working key (greptile P1, CodeRabbit major). Capture the previous envelope before the write and restore it on failure; only a first-time key with nothing to preserve is removed.
| rmSync(keyPath, { force: true }) | ||
| } else { | ||
| try { | ||
| writeSecureFile(keyPath, previousEnvelope.toString('utf8')) |
There was a problem hiding this comment.
Failed restoration exposes API key
If OS encryption is unavailable and file-permission hardening fails during a key replacement, this restoration ignores whether the old key was written securely. An old plaintext key can remain readable under inherited file permissions. If the restoration write throws, the newly entered plaintext key can remain instead, even though the save reports failure. Remove the published file when secure restoration cannot be confirmed.
How this was verified: The credential writer publishes the file before reporting a hardening failure, and this path neither checks the restoration result nor removes the file when restoration throws.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Reviewed the fix(usage): restore the previous key when a plaintext replacement fails commit (d2f13d2) since the prior Pullfrog review, against the full PR diff for context.
- Restored the previous credential on a failed plaintext replacement —
saveZcodePlanApiKeynow snapshots the existing envelope before the fallback write and re-publishes it whenwriteSecureFilereports the new file could not be restricted, instead of deleting the file and leavingcachedZcodePlanApiKeypointing at a credential that no longer exists on disk.rmSyncis now reached only when there was no previous envelope. - Added a regression test — the store suite asserts the restore write targets the captured previous bytes and that the file is not removed, so it fails against the old delete-and-throw behavior.
This resolves the prior failed-replacement concern that was holding approval. The remaining third-party note on the same path (the restore write's hardening result is not re-checked) is the availability-over-restriction tradeoff the commit documents in-line, and the unrestricted-publish window originates in the first fallback write rather than in this delta — I don't consider it a merge blocker.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

ELI5
Orca can already show a GLM Coding Plan quota meter, but only if you installed the ZCode CLI and configured it. This adds a GLM Coding Plan section to Settings → AI Provider Accounts: pick which site your plan belongs to (Z.AI international or Zhipu BigModel mainland), paste the plan API key, and the status-bar meter works — no ZCode CLI needed.
What Changed
#accounts-zcode): site selector + API key input with Save/Replace/Forget, a "Get API key" link to the selected site's console, credential-state card, and the live 5-hour / weekly / MCP quota windows.src/main/zcode/zcode-plan-api-key-store.ts: same safeStorage envelope pattern as the MiniMax API-key store; the key never crosses IPC back to the renderer.fetchZcodeRateLimitsaccepts an Orca-savedplanCredential(site → base URL via the sharedsrc/shared/zcode-plan-sites.tstable, host allowlist reused) that takes priority over~/.zcode/cli/config.json; the CLI config path is unchanged as fallback.setZcodePlanConfigResolver, config-hash +zcodeFetchGenerationinvalidation and generation-guarded result application, mirroring MiniMax;RateLimitState.zcodePlanApiKeyConfiguredkeeps the meter durable across reloads.zcodePlanSitein GlobalSettings + RPC contract + runtime client settings projection; localization catalogs with Chinese translations; settings search entries;usage-provider-settings-targetnow points zcode at the new section.Why
The merged #23520 meter reads only the ZCode CLI's config file, and the meter is PATH-gated on that CLI — a GLM Coding Plan subscriber running GLM through Claude Code, OpenCode, or Cline had no way to link their plan inside Orca. Both sites serve the same quota endpoint (
/api/monitor/usage/quota/limit, rawAuthorizationkey), so one credential flow with a site picker covers both. Extending the mergedzcodeprovider (rather than adding a new provider id like earlier community PRs did) keeps a single status-bar slot and one snapshot pipeline.Linked Issue
Fixes #23803
Visual Proof
Before: Settings → AI Provider Accounts ended at Cursor — no GLM section, and the status bar showed no zcode meter without the ZCode CLI on PATH.
After (live Coding Plan data on the mainland BigModel site, key linked through the real IPC path in a headless Electron run):
The strip reads
66% used 15m · 33% used 3d— the 5-hour and weekly windows from the live quota endpoint, with reset countdowns.Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Live smoke against
https://open.bigmodel.cn/api/monitor/usage/quota/limitwith a real Coding Plan key through the production fetcher: mapped 5-hour (credit-based percentage) and weekly windows, plan level, reset timestamps; a mangled key surfaces the server's auth error with no credential leakage.The screenshots above come from an Electron run that linked the key through
zcodePlanCredentials:saveApiKey(real safeStorage store → invalidate → refresh) and asserted the section's live rows before capturing.New/extended suites:
src/main/zcode/zcode-plan-api-key-store.test.ts,src/main/rate-limits/zcode-usage-fetcher.test.ts(plan-credential priority, host rejection, malformed fallback),src/main/rate-limits/service-zcode-usage.test.ts(resolver wiring, config-change discard, in-flight invalidation),src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx, plus updated registration/visibility/search fixtures.Local checks:
pnpm tc, oxlint on all changed files,pnpm run check:code-quality:changed, localization catalog/extraction/coverage verifies, targeted vitest (rate-limits, status-bar, settings, startup, IPC: 1685 tests),electron-vite build --mode e2e. Full matrix left to CI.AI Disclosure
Claude (Anthropic) implemented this change end-to-end under my direction, including the live smoke test and the headless screenshot run; I reviewed and validated the result.
Review
Self-reviewed for:
net.fetchpath, no shortcuts/platform assumptions, plaintext-envelope fallback matches the MiniMax store's landed behavior;zcodePlanSitedefaults tozaifor existing profiles; allRateLimitStateadditions are additive.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
redirect: 'error', 15s timeout, unread-body cancellation).-ExecutionPolicy, interpreter spawning, or shell paths added.getState()only adds one sync existence check like the MiniMax flags.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)