Skip to content

fix(cli): seed personal provider config so -p --model works on a fresh host - #7

Merged
robotlearning123 merged 3 commits into
masterfrom
fix/fresh-host-model-flag
Sep 30, 2026
Merged

robotlearning123 merged 3 commits into
masterfrom
fix/fresh-host-model-flag

Conversation

@robotlearning123

Copy link
Copy Markdown
Member

Summary

zagent -p "..." --model glm-5.3 failed with Provider Registry 中不存在 Model: zai/glm-5.3 (exit 1) on any host that has only ~/.zcode/cli/config.json (no desktop-written ~/.zcode/v2/provider_config.json), while zagent doctor reported the credential as healthy. Reproduced on ZCode runtimes 3.14.3 and 3.14.4 with a fresh HOME; not a 3.14.4 regression.

Root cause: the 3.14.x app-server (which the --model/--effort selection path, commit-msg and models test spawn) builds its Provider Registry without the standalone options, so it never imports the legacy cli config. Personal providers come only from ~/.zcode/v2/provider_config.json. The kernel's own -p path does run that import and persists the file, which is why a run without --model "fixed" later runs.

Change

  • packages/driver/personal-provider.mjs: converts the cli config exactly like the kernel's legacy importer and writes provider_config.json only when it does not exist (a desktop-written or kernel-migrated file always wins). Atomic 0600 write under the shared interprocess lock, best-effort, never throws.
  • Seed it before the app-server spawn in -p selection runs, commit-msg and models test.
  • doctor prints a model: line and exits 1 when the configured model cannot resolve in the runtime registry.
  • package.json files includes the new module; CHANGELOG Unreleased entry.

Verification

  • node packages/cli/test-personal-provider.mjs: 29 checks pass; the seed-before-spawn check fails on the pre-fix zagent-print.mjs (ENOENT on provider_config.json).
  • Live, fresh HOME with only cli/config.json, first run, -p --model glm-5.3:
    • before: 3.14.3 and 3.14.4 both Provider Registry 中不存在 Model: zai/glm-5.3, exit 1
    • after: 3.14.3 ok exit 0, 3.14.4 ok exit 0; provider_config.json created with mode 600
  • The written file is byte-identical to the file the kernel's own migration writes for the same cli config.
  • make check: PASS, and the new module is in the packed payload.
  • Touched test files (doctor, cli-ux, ux-audit, print-selection, commit-msg, packaging, account-provider, account-config, onboard, signin-card, update-command, mcp-server) all exit 0. scripts/test-all.mjs needs scripts/discover-tests.mjs, which lands with feat: port remaining zcode-cli source modules and test tooling #6.

The 3.14.x app-server builds its Provider Registry without the standalone
options (runZCodeProtocolAgent -> Ykt(env)): personal providers come only
from ZCODE_PERSONAL_PROVIDER_CONFIG_FILE (default
~/.zcode/v2/provider_config.json) and the legacy cli config is never
imported there. A host with only ~/.zcode/cli/config.json therefore
answered every model-bearing session/create from -p --model with
model-not-found, while the kernel's own -p path worked because IT runs
the legacy import and persists the migrated file.

Mirror that migration in zagent: convert the cli config exactly like the
kernel importer (NHa/MHa/FHa/LHa parity incl. the builtin:zai and
builtin:bigmodel family mapping and the apiKeyRequired:false refusal)
and write it only when the target file does not exist — a desktop-written
or kernel-migrated file always wins. Atomic 0600 write under the shared
interprocess lock, best-effort, before the app-server spawn on the -p
selection path, commit-msg, and models test.

doctor now resolves the configured model against the same effective
source (existing file wins, else the derivation) and exits 1 when it
cannot resolve, instead of reporting a healthy credential-only setup.
@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at a5b9fb9: REQUEST_CHANGES

review

我先读 PR 元数据、完整 diff 和提交说明,再按文件核对正确性、安全和测试。提交说明里没有 AI 署名。接下来按文件读 diff 和实现,并跑仓库自带的检查。Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
npm test(make check)在 a5b9fb9 上通过,打包结果包含 packages/driver/personal-provider.mjs。test-personal-provider.mjs 29 项和 test-doctor.mjs 也通过。提交说明和 PR 正文没有 AI 署名,diff 里没有新增链接。

Findings

  1. major packages/driver/personal-provider.mjs:112
    自定义 provider 把 options.apiKey 原样写入,只要值不是 undefined(包括 "" 和前后空白)。builtin:zai 会先 trim(),空 key 不写入(同文件第 96–98 行)。zagent.mjs:170-174 写明 OAuth 配置会先落成 apiKey:''。探测:这种配置会生成 provider_config.json 且 "apiKey": "";随后把 cli config 改成真实 key 再调用 provisionPersonalProviderConfig,结果是 provisioned: false、reason: personal provider config already present,文件里仍然是空 key。这次写入是永久的,后面的 kernel 迁移和 ensureConfig 回填都不会改它。

  2. major packages/driver/personal-provider.mjs:161
    目标路径按 ZCODE_DATA_BASE_DIR 放到数据根下(第 32–33 行),cli config 却始终读 $home/.zcode/cli/config.json。本仓库里 kernel 的 .zcode/cli 跟这个数据根走(packages/driver/rewind.mjs:91)。探测:HOME 的 key 是 HOME-SECRET-KEY,ZCODE_DATA_BASE_DIR 下的 cli config 是 DATA-SECRET-KEY,结果写入了数据根的 provider_config.json,内容是 HOME 的 key,数据根里的 cli config 没有被读。已存在文件不会被覆盖,所以错误根上的凭据会留下。

  3. minor packages/driver/personal-provider.mjs:71
    models 里值为 null 的条目在读 m.contextWindow 时抛 TypeError。deleted: true 会被跳过,null 不会。provisionPersonalProviderConfig 捕获后整份导入放弃,不写文件。modelResolutionCheck(第 213–214 行)把任何异常都说成当前 model.main 的 provider “not expressible”。探测:{ 'glm-5.3': null, 'glm-5.3-flash': {} } 抛 TypeError: Cannot read properties of null (reading 'contextWindow')。

  4. minor packages/driver/personal-provider.mjs:218
    已存在的 providerRules 若不是数组,rules.providers.find 抛 TypeError。personalModelIds 若是普通对象,第 221 行 ids.includes 同样会抛。zagent.mjs:631 没有接住。探测:providerRules 为对象时,modelResolutionCheck 抛 TypeError: rules.providers.find is not a function,doctor 会以未捕获异常退出,而不是打印 model: 行。

  5. nit packages/cli/test-personal-provider.mjs:10
    chmodSync 被导入但未使用。

VERDICT: REQUEST_CHANGES

Harden the provisioning module:

- Never seed a permanent empty-key config. NHa stores a custom
  provider's apiKey verbatim (xz ApiKeyAccessConfig, offset 547578,
  keeps "" through toJSON), and ensureConfig writes apiKey:'' during
  the OAuth window — a create-if-missing seed of that state could
  never be repaired (the kernel's import skips existing files and the
  OAuth backfill reads this very file). The converter keeps the kernel
  parity, and the provisioner now refuses to persist when the SELECTED
  provider's key is unusable (empty/whitespace/absent); the next run
  after the key lands seeds correctly.

- Make the source/target root rule explicit. The kernel's legacy
  import reads the cli config from the REAL home (U7, offset 4090234)
  even when ZCODE_DATA_BASE_DIR relocates the personal target (dQi,
  offset 1068307) — measured live on 3.14.4: HOME's key lands in the
  data-root file and a data-root cli config is never consulted. That
  asymmetry is now a documented, named helper (legacyCliConfigPath)
  with pinning tests instead of an implicit join.

- Skip null/primitive model-map entries like deleted ones instead of
  throwing, and make modelResolutionCheck's refusal reasons honest
  (expressibility vs malformed input).

- Treat a structurally wrong existing provider_config.json (non-array
  providerRules / provider rule config / personalModelIds) as a
  NOT-RESOLVABLE verdict — the kernel's strict parser does the same —
  so doctor prints a model: line instead of crashing.

- Drop an unused chmodSync import in the test.

Each class has a unit test that fails on the pre-fix module and
passes now.
@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at c860a4d: REQUEST_CHANGES

review

我先按独立评审来看这份 PR:读评审规范、PR 元数据和完整 diff,再对照仓库自身的检查命令。评审范围已钉在 PR #7 的 head。接下来读仓库检查命令、提交说明和完整 diff。Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
packages/cli/test-personal-provider.mjs 与 packages/driver/test-doctor.mjs 已通过。npm test(verify-public-package.mjs)结果为 PASS,打包清单含 packages/driver/personal-provider.mjs。提交说明里没有 Co-Authored-By 或 Generated with。

  • major packages/driver/personal-provider.mjs:278(打印处 packages/cli/zagent.mjs:633,退出处 packages/cli/zagent.mjs:658)
    modelResolutionCheck 在目标 provider_config.json 不存在时,只要 model.main 的模型 id 出现在 cli 配置的 models 里就返回 ok: true,source 为 cli config (provisioned on first -p run)。同一状态下 provisionPersonalProviderConfig(personal-provider.mjs:206-209)会拒绝落盘。实测:provider.zai.options.apiKey 为 ""、model.main 为 zai/glm-5.3、models 含 glm-5.3 时,resolution 为 {"ok":true,"source":"cli config (provisioned on first -p run)","detail":"zai/glm-5.3"},provision 为 provisioned:false 且文件不存在。zagent doctor 退出码 0,并打印 model: zai/glm-5.3。OAuth token 存在时凭证行是 credential: kernel OAuth store (zagent login),没有 warn;设置 ZAI_API_KEY 时凭证行是 credential: ZAI_API_KEY (env),同样退出 0。ensureConfig 只在文件已存在时从 provider_config.json 回填空 key,不会把环境变量写进已有的 cli 配置;-p 也不调用 syncAccountConfig。因此 doctor 显示模型可解析,随后的 -p --model 仍然没有 registry 文件。

  • major packages/driver/personal-provider.mjs:215
    空 key 会拒绝写入,但「选中的 model.main 不在将要写入的 personalModelIds 里」仍会 create-if-missing。实测:cli 配置有可用 apiKey 和 model.main: zai/glm-5.3、没有 models 时,provisioned: true,写入的 rule 只有 group/access/api,没有 personalModelIds。写入后再检查为 ok: false,detail 为 model zai/glm-5.3 is not in the provider's model list。已有文件在 personal-provider.mjs:188 直接返回,之后往 cli 配置补上模型也不会重写该文件。第一次失败的 -p --model 会把原本还可以等配置补全后再迁移的状态变成永久 miss。

  • minor packages/driver/personal-provider.mjs:131
    p?.name?.trim() 在 name 为对象时抛出 TypeError: p?.name?.trim is not a function,整个 importLegacyCliConfig 失败,同文件里带有效 key 的其他 provider 也不会被写入。builtin:zai 的 options.apiKey?.trim()(personal-provider.mjs:122)对非字符串 key 同样中断整次导入。

VERDICT: REQUEST_CHANGES

planPersonalProviderConfig is now the single answer to "would the -p
path make this host's selection resolvable, and what would it write";
provisioning and doctor's modelResolutionCheck both consume it, so
doctor can never report ok for a state the -p path refuses to seed
(the OAuth/ZAI_API_KEY empty-key states: credential lines look fine,
yet no registry file can be created).

The plan also refuses to seed a document that would not resolve the
selected model — a usable key with no models map used to leave a
rule without personalModelIds behind forever (nothing rewrites an
existing personal file), turning a wait-for-the-config-to-grow state
into a permanent miss on the first failed run. Dangling selections
are the same class. Family rules (templateId) resolve through the
builtin template's model list and stay seedable.

Wrong-typed fields (name:{}, apiKey:5, baseURL:7) now degrade to
absent for that entry alone instead of aborting the whole import.
The kernel's zod layer (QAn -> RHa) is strict — one bad field
rejects the entire config — but it also has a desktop to repair the
file; zagent's create-if-missing seed does not, so per-entry
leniency is the safer failure mode for the converter.

doctor's exit contract changes accordingly for keyless configs: a
selection the -p path cannot seed is NOT RESOLVABLE and unhealthy
(exit 1), matching what provisioning refuses — previously a
credential-less config kept exit 0 while -p --model failed.
@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at a982ef8: APPROVE

review

我先读 PR 元数据、完整 diff 和仓库检查约定,再按文件核对正确性、安全性和测试。PR 是个人 provider 配置的种子写入。我对照 diff、提交说明和仓库自检命令逐项核对。提交说明里没有 AI 署名。接下来核对种子写入、doctor 判定,以及相关测试是否真的覆盖这些路径。Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
检查命令和关键单测一起跑,并用小探针核对「空 key 也会被写成永久文件」这条路径。Memory flush started.
Memory flush completed.
make check 通过(verify-public-package PASS,打包清单含 packages/driver/personal-provider.mjs)。test-personal-provider.mjs、test-doctor.mjs、test-packaging.mjs、test-update-command.mjs 均退出 0。三个提交的说明里没有 Co-Authored-By 或 Generated with。本次 diff 没有新增链接,也没有内部主机名或路径。

Findings

  1. minor — packages/driver/personal-provider.mjs:287
    selectedModel() 对 account: / builtin: 或缺失的 model.main 返回 null 之后,planPersonalProviderConfig 在空 key 检查之前就 write: true。探针:model.main 为 account:zai-individual-coding-plan/glm-5.3,provider.zai.options.apiKey 为 "" 时,provisionPersonalProviderConfig 仍写入 apiKey:"",modelResolutionCheck 返回 null(doctor 不报 NOT RESOLVABLE)。该文件一旦存在,内核导入和 provisionedPlanKey() 都不会再补真 key。zagentMemory flush started. -p --effort、commit-msg、models test 都会走这条播种路径。默认的 ensureConfig 形状(model.main 为 zai/glm-5.3)会被后面的检查拦住。

  2. minor — packages/driver/personal-provider.mjs:126Memory flush completed. 与 :294
    导入方把 provider id trim() 成 zai,空 key 判断却用未 trim 的 cli.provider[sel.providerId]。探针:键名 "zai "、apiKey:""、model.main 为 zai/glm-5.3 时,write:true,resolves.ok:true,落盘 access.apiKey 为 ""。doctor 会把这次选择报成可解析。

  3. minor — packages/driver/personal-provider.mjs:322
    注释写明 Never throws,但 planPersonalProviderConfig() 在 try 之外。探针里 exists 抛错会直接冒泡。现有调用点有空 catch,默认的 existsSync 很少抛。

  4. minor — packages/driver/personal-provider.mjs:212
    带 templateId 的 family 规则不看 model id 就判可解析。探针:builtin:zai 加可用 key、model.main 为 zai-api/not-a-real-model 时,write:true 且 resolves.ok:true。模板里没有这个 id 时,create-if-missing 文件仍会留下。

  5. minor — packages/cli/zagent-commit-msg.mjs:160、packages/cli/zagent-models.mjs:145
    这两处播种没有测试。删掉这两行,test-personal-provider.mjs 仍会通过。-p 路径有 runPrintOnce 在打开 client 之前播种的断言。

  6. nit — packages/driver/personal-provider.mjs:241
    cliUnreadable 赋值后未再读取。同函数第 236 行的 path 遮住了 node:path。

  7. nit — packages/cli/test-personal-provider.mjs:223
    用例文案是 provider 不在已有文件里。同一次 home 上的文件已有 provider other,缺的是 model m。断言只检查 ok === false。

  8. nit — packages/cli/test-update-command.mjs:48
    doctor 夹具仍是有 key、无 models 的 zai/model。隔离 HOME 下 doctor 现为退出 1,并打印 model: ... NOT RESOLVABLE。该测试不检查退出码,所以仍然 PASS,不再覆盖「健康 doctor」路径。

VERDICT: APPROVE

@robotlearning123
robotlearning123 merged commit d555b40 into master Sep 30, 2026
4 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.

1 participant