fix(onboarding): load pasted custom-provider credentials - #3741
Conversation
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Verdict: REQUEST_CHANGES
Exact-head review completed against the current PR head and current dev base.
- PR head:
0581f2c0009fb05fbd2c714a6cc2032e83f9feb1 - Current
devbase:2b806cc7809aa828323b04e4e86cbe242decdff7 - Full 10-file diff reviewed, including the credential marker, persistence, reload, selector flow, error handling, documentation, generated docs index, and tests.
Blocking finding: stale stored credential survives switching the same provider to env auth
addApiCompatibleProvider() now persists pasted literal keys in the canonical AuthStorage database, but the apiKeyEnv branch does not remove an existing stored key. On a force overwrite of an already-configured provider, changing from a pasted literal key to an environment-backed key therefore leaves the old database credential active:
- Configure provider
pwith a pasted key. The new code stores it withAuthStorage.set()and writesapiKeyStored: true. - Run the wizard again for
pwithapiKeyEnv: P_KEYandforce: true. - The replacement
models.ymlcontainsapiKeyEnvand noapiKeyStored, but the oldpAPI-key row remains in AuthStorage. AuthStorage.getApiKey()resolves stored API keys before environment variables, so requests continue using the old pasted secret rather thanP_KEY.
This is a credential-source/config-persistence mismatch and can silently send requests with a revoked or unintended secret. The reverse transition (env to literal) uses replaceAuthCredentialsForProvider() and replaces the stored rows, so the asymmetry is specifically in the env branch. Please clear/remove the provider's stored credentials when replacing a literal setup with an env-backed setup, and add a force-overwrite regression test asserting the selected key is the environment key and the old stored row is gone.
#3738 overlap
The original #3738 reproducer remains correctly rejected: auth: apiKey without apiKey, apiKeyEnv, or apiKeyStored: true still fails validation. The new marker fixes the wizard-generated literal-key path, but it does not address the stale-credential overwrite defect above.
Verification
Current-head GitHub CI is green: the status query reports no non-passing checks; completed checks are SUCCESS and only inapplicable jobs are SKIPPED. No source files were modified and no merge was performed during this review.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
0581f2c to
f55cd7f
Compare
|
Addressed the blocking literal-to-env credential transition finding on the latest
Please rerun the exact-head review against |
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Signed exact-head verdict — REQUEST_CHANGES
Reviewed against the live GitHub state on 2026-08-05:
- PR head:
f55cd7fb4779f31748ef5bb293ef6407650f5655 - recorded PR base:
be3940aa72f08bfce43b4e5a72b5d6f3260e3ba5(devat the last rebase) - current
dev:732856b3ccb3fade6e9fbc17908a4fbca5a7682f - integration state: draft,
dirty, 6 commits ahead / 56 behind, not rebaseable - exact-head CI: Dev CI and Public site sync completed successfully: 18 successful jobs, 6 intentional skips, 0 failures, 0 pending
That CI is valid evidence for this exact head on its old base. It is not current-dev integration evidence.
Contributor ledger
The contributor record is substantial: GitHub search returns 51 authored PRs (25 merged, 5 open, 21 closed without merge) plus 11 issues; the contributors endpoint credits 24 repository contributions. On this PR, the contributor responded to the prior stale-credential blocker within five minutes, added normal-path persistence/reload coverage, kept pasted secrets out of models.yml, updated the public schema/docs/generated index, and supplied green exact-head CI. Those strengths deserve explicit credit. They do not close the following auth/data-integrity blockers.
Blocking findings
-
P1 — credential/config replacement is not failure-atomic. In the exact-head
provider-onboarding.tshunk,AuthStorage.remove()orset()completes beforewriteModelsConfig(). If the YAML replacement fails, stored→env deletes the working credential while the oldapiKeyStored: trueconfig remains; env→stored can replace the credential while the old config remains. The YAML write is atomic by itself, but the SQLite/YAML transition has no compensation or durable recovery. Add injected write-failure and retry/interruption coverage for both directions. -
P1 — the wizard writes through the wrong credential authority in broker mode.
addApiCompatibleProvider()hardcodesAuthStorage.create(getAgentDbPath()). Broker-backed sessions useRemoteAuthCredentialStore; reloading that live store fetches the broker snapshot, not the local database the helper just changed. A pasted-key wizard run can therefore persistapiKeyStored: truewhile the active/broker credential store has no key. Inject and mutate the session's active, broker-aware credential backend; add broker-backed setup and restart coverage. -
P1 — env replacement deletes the whole provider credential set.
AuthStorage.remove(providerId)disables every stored credential for the resolved provider, not only the wizard-owned stale API key. Onboarding accepts arbitrary normalized provider IDs and does not reserve bundled IDs or aliases, so a custom-provider collision can erase unrelated OAuth or multi-account credentials. Namespace/reject collisions or track a specific onboarding-owned credential and replace only that record; test bundled IDs, aliases, OAuth rows, and multiple credentials. -
P1 — cache activation is incomplete across call sites. The new
authStorage.reload()exists only in the TUI wizard. Both/provider addhandlers call the same mutation helper and then onlyModelRegistry.refresh(), which does not reload AuthStorage. A force switch to env auth can therefore leave the active session using the stale stored key until restart. Centralize mutation + reload/activation on the active store and cover every caller. -
P1 — current-
devintegration is unresolved. The exact patch no longer applies cleanly tomodel-registry.ts,docs/models.md,CHANGELOG.md, ordocs-index.generated.ts. Currentdevalready merged #3757's actionable credential-source diagnostic; the rebase must preserve it while adding the stored-key form, then regenerate the embedded docs index. Fresh current-head CI is required after the rebase.
P2 follow-up: after durable writes succeed, reload/refresh/notification failures are caught and reported as Provider setup failed. Separate durable commit from activation failure and provide a non-mutating activation retry so users are not told to repeat an operation that already committed.
Required re-review boundary
Rebase onto current dev, repair the credential ownership/atomicity design, route all mutations through the active broker-aware store, cover every call site and failure phase, retain the current #3757 diagnostic, regenerate public artifacts, and obtain fresh exact-head CI. A new exact-head hostile review is required; the old green runs cannot be carried forward as approval.
—
Signed: Yeachan-Heo · repository owner · hostile exact-head review · 2026-08-05
Yeachan-Heo
left a comment
There was a problem hiding this comment.
VERDICT: REQUEST_CHANGES — one specific blocker.
Blocker: generated JSON schema not regenerated
ModelsConfigSchema gains apiKeyStored: z.literal(true).optional() in this PR, but the generated artifact schemas/models.schema.json (produced by bun run generate-schemas via scripts/generate-json-schemas.ts from ModelsConfigSchema) was not regenerated. Verified at this exact head f55cd7fb:
$ bun scripts/generate-json-schemas.ts --check
Generated JSON Schemas are out of date: schemas/models.schema.json
Run `bun run generate-schemas` and commit the updated files.
The only expected delta is the provider property (inserted after apiKeyEnv):
"apiKeyStored": {
"type": "boolean",
"const": true
}Why this blocks:
- Root
bun run checkincludescheck:schemasand fails at this head, contradicting the PR body's "bun check passes" claim. The greencheck:@gajae-code/coding-agentworkspace run does not includecheck:schemas. - The dev-branch CI (24 runs at this head: 17 success / 7 skipped / 0 failures) does not exercise this gate:
ci.yml'scheckjob (ci:check:full) only triggers on PRs to and pushes ofmain, so the drift silently passes dev CI and will fail the main merge path. - Repo contract: AGENTS.md — "Generated artifacts — change the generator, then regenerate;
checkenforces sync" — and existing tests (models-config-send-session-headers.test.ts,models-config-tool-choice-support.test.ts) assert newly added schema fields are exposed inschemas/models.schema.json.
Fix: run bun run generate-schemas and commit the regenerated schemas/models.schema.json in this PR.
Verified clean (no changes needed)
- Exact head matches; runtime logic is correct: pasted keys are stored in
agent.dbwith onlyapiKeyStored: truewritten tomodels.yml; env replacement removes the stale stored key (stored credentials outrank env values per resolution order);authStorage.reload()beforerefresh("offline")makes the provider immediately usable without restart; strict validation message updated for the models-config path;z.literal(true)rejects non-truevalues. - Security: the secret never lands in
models.yml(asserted by tests); docs (docs/models.md) and the embedded docs index (docs-index.generated.ts, in sync perdocs-index-lazy.test.ts) are updated. - Affected test files pass at this head (247/252; the 5 failures reproduce identically at merge-base
be3940aa— pre-existing/environmental, unrelated to this PR).
Non-blocking notes
authStorage.remove(validated.providerId)on the env path deletes ALL credentials for the provider id, including OAuth credentials (e.g. force-replacing a first-class provider id with env auth silently drops its OAuth login). Consider scoping removal toapi_keycredentials or documenting the behavior.remove()/set()run beforewriteModelsConfig(); a failed config write leaves the credential store already mutated. Pre-existing ordering forset(), newly destructive forremove()— low severity.
f55cd7f to
0df28bb
Compare
6efd547 to
72ff7ab
Compare
72ff7ab to
8f8ad9e
Compare
yazzang-homelab
left a comment
There was a problem hiding this comment.
Independent architect review of head 8f8ad9e3b. @Yeachan-Heo's blocker is resolved; I verified it rather than taking the diff's word.
The blocker is closed
The objection was that ModelsConfigSchema gained apiKeyStored: z.literal(true).optional() without regenerating schemas/models.schema.json. On this head the property is present in both the source schema and the generated artifact, and — the part that matters — the artifact is generator-consistent, not hand-edited:
$ bun scripts/generate-json-schemas.ts --check
$ echo $?
0I ran that on a clean checkout of this exact head. Hand-patching the JSON to satisfy a reviewer is the common way this gets "fixed"; it did not happen here.
Tests pass — but only in a hermetic environment, and that is a repo bug, not yours
First run on your head:
packages/coding-agent/test/model-registry.test.ts
(fail) generic local OpenAI-compatible provider config > uses stored credentials for OpenAI-compatible providers…
(fail) active provider resolution > keeps credentialless discovery active with an irrelevant dangling…
I did not attribute those to this PR, because current dev fails the same two. Root cause: model-registry.test.ts reads provider API-key environment variables from the host shell, so the assertion picks up the developer's real credentials:
- Expected - 0
+ Received + 16
+ { "connectionKind": "credential", "provider": "openai" },
+ { "connectionKind": "credential", "provider": "openrouter" },
With every *API_KEY* variable unset and an isolated HOME, dev passes 225/225 and your head passes 266/266 across all four suites you touched:
model-registry.test.ts
provider-onboarding.test.ts
provider-onboarding-wizard.test.ts
provider-onboarding-wizard-redteam.test.ts
-> 266 pass, 0 fail
So this PR is clean. The hermeticity gap is pre-existing and I am reporting it separately — it matters because it makes local verification unreliable for exactly the area this PR changes, and any contributor with credentials configured will see red and assume they broke something.
Other state
- CHANGELOG intact at 312,501 bytes — this PR is not among the twelve damaged by #3942.
- Merges cleanly into current
dev. - Note this PR's workflow runs are in
action_requiredand have never executed (#3940). Only a maintainer can release those, so the CI evidence the template asks for is unobtainable from your side right now.
The remaining question is whether apiKeyStored: z.literal(true) is the right shape — a literal-true marker means "a key exists in storage" can only ever be asserted, never explicitly negated, so false and "absent" are indistinguishable on the wire. That is fine if the only consumer treats absence as false, which appears to be the case. Worth one sentence in the PR body confirming that was deliberate rather than incidental.
gajae.pr-review-verdict.v1 merge-approved sha256:8f8ad9e3b76499d21250aedb31d15356db612d8d reviewer:architect evidence:bun scripts/generate-json-schemas.ts --check exit 0 on this head; 266 pass / 0 fail across the four touched suites with *API_KEY* unset and isolated HOME; merge-tree vs origin/dev clean
yazzang-homelab
left a comment
There was a problem hiding this comment.
앞선 내 승인을 정정한다. 승인 근거에 빠진 게 있었다.
CHANGELOG가 "손상되지 않았다"(312,501 bytes)는 것만 확인하고 항목이 어느 섹션에 들어갔는지는 보지 않았다. 다시 보니 잘못된 자리에 있다.
28: ## [0.12.12] - 2026-08-05
...
61: - Pasted API keys from the custom-provider wizard now leave an explicit stored-credential ...
이 PR의 새 항목이 이미 2026-08-05에 배포된 0.12.12 섹션 안에 있다. ## [Unreleased]가 아니다.
네 실수가 아닐 가능성이 높다. .gitattributes의 merge=union이 오늘 11:25:32Z까지 살아 있었고, union은 충돌을 내지 않고 양쪽을 이어붙인다. 릴리스 커밋이 ## [X.Y.Z]를 살아남은 ## [Unreleased] 바로 아래에 삽입하기 때문에, Unreleased 아래 추가한 항목이 리베이스에서 새 버전 헤딩 밑으로 조용히 옮겨진다. dev에서 같은 상태인 기존 항목을 35건 찾았다(#3929). 열린 PR 중에도 8건이 같다.
드라이버는 제거됐으니(#3932) 한 번 옮겨두면 다시 움직이지 않는다. 61행을 ## [Unreleased] 아래로 옮기면 된다. 리베이스로는 안 풀린다 — 이미 커밋된 위치라서 직접 옮겨야 한다.
코드에 대한 판단은 그대로다: 스키마 재생성은 bun scripts/generate-json-schemas.ts --check exit 0으로 검증했고, 격리 환경에서 266 pass / 0 fail이다. 이 한 줄만 옮기면 승인으로 되돌린다.
내가 승인하면서 놓친 부분이라 미안하다. 리뷰 체크리스트에 "항목이 Unreleased 아래에 있는지"를 넣었어야 했다.
gajae.pr-review-verdict.v1 merge-blocked sha256:8f8ad9e3b76499d21250aedb31d15356db612d8d reviewer:architect evidence:CHANGELOG entry at line 61 resolves under "## [0.12.12] - 2026-08-05" (heading at line 28) on this head
|
Closing during the emergency maintenance freeze. This PR is not in the retained critical or maintainer-owned set. Do not open a replacement PR unless a maintainer explicitly directs it. — |
What
apiKeyStored: truecredential-source marker for custom models whose key lives in GJC credential storage.Why
Fixes #3738.
The wizard already stored pasted API keys outside
models.yml, but the generated provider entry did not declare that credential source. Static config validation therefore rejected the wizard output.A force replacement from a pasted key to
apiKeyEnvalso left the stored key active. Because stored credentials outrank environment values, the old secret continued to win. Env setup now removes that stored credential, and the running session reloads the canonical store before refreshing models.Hand-authored custom models with no
apiKey,apiKeyEnv, or stored-key marker remain invalid.Testing
bun test packages/coding-agent/test/provider-onboarding.test.ts packages/coding-agent/test/provider-onboarding-wizard.test.ts(255 focused tests passed / 2841 assertions)bun test packages/coding-agent/test/provider-onboarding.test.ts packages/coding-agent/test/provider-onboarding-wizard.test.ts packages/coding-agent/test/model-registry.test.ts --test-name-pattern "stored API key|declared credential source|provider onboarding"(32 passed)bun test packages/coding-agent/test/docs-index-lazy.test.ts(5 passed)bun --cwd=packages/coding-agent run checkGJC verdict
devbun checkpasses