Skip to content

fix(driver): normalise provider ids, never throw, verify family models - #8

Open
robotlearning123 wants to merge 6 commits into
masterfrom
fix/personal-provider-edges
Open

robotlearning123 wants to merge 6 commits into
masterfrom
fix/personal-provider-edges

Conversation

@robotlearning123

@robotlearning123 robotlearning123 commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up hardening of the personal provider seeding added in #7:

  • One normalised provider id everywhere: the importer trimmed ids ("zai " -> zai) while the key gate read the untrimmed id, so an empty key could still be seeded and reported resolvable.
  • The public entry points never throw (the plan step used to run outside the guard).
  • Family rules with a templateId now resolve only when the selected model id is in the builtin template's model list; the list is read from the same file the kernel rebuilds its registry from (managed active cache first, then the preset/bundled copy) and ids compare exactly like the kernel. If the list is unavailable the seed still happens (family rules never brick a later selection) and doctor reports the check as unverified.
  • A family file seeded earlier without access.type (which the kernel's strict parser rejects) is repaired in place on the next -p run; doctor flags any rule whose access block is missing or untyped.
  • Tests that fail if the commit-msg or models test seed call is removed.
  • Nits: an unused variable, a local path shadowing node:path, a mislabeled verdict test split into precise missing-model / missing-provider cases, and the test-update-command doctor fixture now has a models map and asserts the healthy exit code.

Verification

  • node packages/cli/test-personal-provider.mjs: 79 checks pass.
  • Affected suites pass: doctor, update-command, print-selection, commit-msg (under the hermetic runner), cli-ux, ux-audit.
  • make check: PASS.
  • Live on ZCode 3.14.3 and 3.14.4, fresh HOME with only cli/config.json: the first -p --model run still succeeds, and an empty-key config still seeds nothing.

The importer trims provider map keys ('zai ' -> rule id 'zai') but the
key gate looked the cli entry up untrimmed, so an entry keyed 'zai '
slipped past the unusable-key gate and seeded an empty-key file the
doctor called resolvable. All lookups now go through one normalised
id (cliProviderEntry), matching the importer's own normalisation.

planPersonalProviderConfig is documented to never throw but was called
outside any try in the provisioner; a throwing exists bubbled straight
out. The public plan is now a wrapper that degrades any internal
throw to a conservative refusal, so provisioning and doctor are safe
by construction.

Family rules (templateId) resolved any model id without checking it.
The verdict now verifies the id against the builtin template's
builtinModelIds when the template list is available (injected document
or ZCODE_BUILTIN_PROVIDER_CONFIG_FILE / _BUNDLED_); without the list
the seed is refused conservatively and doctor says the verdict could
not be verified — a create-if-missing seed must not guess.

commit-msg and models test now have seeding tests: both commands are
spawned against a runtime stub that dies on contact, so they must
fail, but only after the seeding line ran — the assertions fail if
either call is removed. models test's fixture carries the v2 config
mirror because the command exits before its client without one.

Also: drop the unused cliUnreadable flag, stop shadowing node:path
with the local target path, label the existing-file verdict tests
precisely (missing model vs missing provider, exact detail asserted),
and give test-update-command's doctor fixture a models map with a
healthy exit-0 assertion so it covers the healthy path again.
@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at d570f91: REQUEST_CHANGES

review

我先读 PR 元数据、完整 diff 和仓库检查约定,再按文件核对正确性、安全性和测试。这是独立审阅,不改文件。接下来读 diff、提交说明和仓库检查命令。Memory flush started.
Memory flush completed.
diff 不大,我按调用链把 provider id 归一化和 family-Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
独立审阅 agent-next/zagent#8(d570f91522de9631b8648376c02130a384ce2156)。make check 为 PASS(verify-public-package,不覆盖这条判定逻辑)。node packages/cli/test-personal-provider.mjs 与 test-update-command.mjs 通过。git log --format=%B origin/HEAD..HEAD 无 AI 署名;diff 无新链接、无密钥、无内部主机名。

  1. major packages/driver/personal-provider.mjs:300
    家族规则的模板列表只从 ZCODE_BUILTIN_PROVIDER_CONFIG_FILE 或 ZCODE_BUILTIN_PROVIDER_BUNDLED_CONFIG_FILE 读取。后者在仓库里仅此一处,没有任何赋值。kernelEnv()(packages/driver/runtime.mjs:133)会把捆绑的 zcode-builtin.json 写进子进程环境,但 doctor 与种子都在父进程里、且发生在这次注入之前:zagent.mjs:631 只传 { config: cfgJson },zagent-print.mjs:194、zagent-commit-msg.mjs:160、zagent-models.mjs:145 调用的是无 builtin 的 provisionPersonalProviderConfig()。列表因此一直是 null。selectionVerdict(:242)把可判定的 zai-api/... 判成不可验证;doctor 在 zagent.mjs:634 把 unhealthy 设上并在 :658 以 1 退出。Memory flush started.
    已有个人配置文件只要含 templateId,:325 走同一加载器。实测(builtin:zai + model.main = zai-api/glm-5.3,env: {}):provisioned: false,doctor ok: false,detail 为 builtin template model list is unavailable。新增测试在 :559 把这个拒绝写成了预期,并用注入的 builtin 或手设环境变量覆盖成功路径,所以套件是绿的。

  2. major packages/driver/personal-provider.mjs:247
    命中与否用 ids.includes(modelId),大小写敏感。仓库里作为真实 zcode-builtin.jsonMemory flush completed. 子集的夹具是大写 GLM-5.3 / GLM-5.3-Flash(packages/driver/test-providers.mjs:70),providers.mjs:71 写明 catalog id 为大写;CLI 的 model.main 是小写 glm-5.3(zagent.mjs:206)。commit-msg 会按配置里的规范大小写改写后再发给内核(zagent-commit-msg.mjs:94),种子比较的是原始 id。同一份大写列表下,注入 builtin 和设置 ZCODE_BUILTIN_PROVIDER_CONFIG_FILE 都拒绝种子,doctor detail 为 model zai-api/glm-5.3 is not in the builtin template's model list。新测试的 BUILTIN_FIXTURE(test-personal-provider.mjs:537)把列表写成小写,与上述真实形状不一致,所以盖不住这个拒绝。

  3. nit packages/cli/test-personal-provider.mjs:516
    planPersonalProviderConfig({ home: tempHome(), ... }) 创建的临时家目录没有 rmSync。同块的 threw 在三次调用之间不重置。

VERDICT: REQUEST_CHANGES

Resolve the builtin template model list the way the runtime spawn sees
it, reusing zagent's own resolvers instead of an env-only lookup: an
injected document, then ZCODE_BUILTIN_PROVIDER_CONFIG_FILE, then the
bundled copy beside the discovered runtime (builtinConfigPath — exactly
what kernelEnv injects for our spawns), then the kernel's managed
active cache (kernelActiveBuiltinPath under the data root). When none
is readable the plan falls back to seeding exactly what the kernel
migration would write and reports the model check as UNVERIFIED
(resolves null plus a reason; doctor prints the caveat instead of
failing) — a seed the kernel itself performs is never refused, so
family rules no longer refuse on hosts without a resolvable list.

Model id comparison now follows the kernel's own lookup semantics:
the registry indexes models in plain Maps and the explicit-selection
path reads them verbatim (getModel: this.#n.get(p)?.get(m), ~576830;
models.find(g => g.modelId === t.modelId), ~579813) — exact and
case-sensitive, which is why commit-msg canonicalises casing before
sending. Measured live on 3.14.4: zai-api/glm-5.3 against the real
upper-case template list answers model-not-found while zai-api/GLM-5.3
answers ok. Family verdicts compare exactly against the real shape
(['GLM-5.3','GLM-5.3-Flash']) and custom selections against their own
config's casing; a case mismatch is flagged, and family seeds still
happen (their model list lives in the template, so a seed cannot brick
a later selection).

Also emit the family rule's access.type:'api-key' like every kernel
xz serialization — without it the strict parser rejects the file and
even the kernel's own -p fails (caught live: builtin:zai hosts seeded
an unusable file). The family seed is now byte-identical to the
kernel's own migration output.

Tests: real upper-case builtin fixture, the unverified fallback, both
resolver paths (runtime-anchored bundled copy, managed active cache),
custom-casing semantics, and the family byte-parity oracle; the
never-throw block no longer leaks its temp home and resets its threw
flag between calls.
@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at 9661a9a: REQUEST_CHANGES

review

我先按这个 PR 的 head 做只读审查:读元数据、完整 diff、提交说明,并核对仓库自己的检查命令。审查范围是这个 PR 自身的三处改动。我先读 diff、提交说明和相关源码,再跑仓库自己的检查。Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
相关测试已通过。我再用一次只读探针确认:当安装包里的模板和内核托管缓存同时存在时,判定读的是哪一份。Reviewed agent-next/zagent#8 at 9661a9ad9ab4bb10cecaa5438c55df8160af5c08 (3 files, commits d570f91 and 9661a9a). Commit messages have no Co-Authored-By or Generated with trailer. The diff adds no URLs. node --check on the three files, node packages/cli/test-personal-provider.mjs (all checks passed), and node packages/cli/test-update-command.mjs (PASS) all succeeded. make check is the npm-pack smoke gate and does not run these suites.

  1. packages/driver/personal-provider.mjs:256 — major — The family-model verdict reads the install bundle and stops there, so it disagrees with the builtin file the kernel actually loads. resolveBuiltinConfig pushes builtinConfigPath(entry) and then kernelActiveBuiltinPath, and returns the first file that parses. account-config.mjs:45-50 and :91-97 say a normal spawn (no ZCODE_PERSONAL_PROVIDER_CONFIG_FILE) rewrites the builtin path to the managed active cache, and buildAccountConfigParams therefore reads that effective path first. A probe with both files present (builtinModelIds: ['BUNDLED-ONLY'] beside the runtime, ['ACTIVE-ONLY'] under home/.zcode/v2/runtime/provider/linux-x86_64/0.0.0-dev/...) recorded a single read of resources/config/provider/zcode-builtin.json and resolves.ok === false for zai-api/ACTIVE-ONLY. The same preset-first push at line 253 has the same shape when only ZCODE_BUILTIN_PROVIDER_CONFIG_FILE is set. The new tests cover each path alone, so this ordering never fails CI.

  2. packages/driver/personal-provider.mjs:441 — major — A family file already seeded without access.type stays on disk, and doctor reports that selection resolvable. This PR adds type: 'api-key' only in importLegacyCliConfig (line 143). The commit message says a missing type makes the kernel's strict parser reject the file so that -p fails. provisionPersonalProviderConfig still returns at line 441 when provider_config.json exists (reason: "personal provider config already present"). selectionVerdict (line 291) returns { ok: true } for a templateId rule once the model id is in the template list, without reading access.type. Probe: a file with access: { apiKey: 'sk-test' } and defaultModelSelection zai-api/GLM-5.3 was left byte-identical (provisioned: false, hasType: false) and modelResolutionCheck returned { ok: true, detail: "zai-api/GLM-5.3" }. zagent.mjs:633-636 prints that detail and leaves unhealthy unset, so doctor exits 0. The personal-provider seed is already on master (changelog "Unreleased"), which is the shape this follow-up says it caught live.

  3. packages/driver/personal-provider.mjs:437 — minor — provisionPersonalProviderConfig forwards builtin and drops runtimeEntry. planPersonalProviderConfig and modelResolutionCheck both honor runtimeEntry. packages/cli/test-personal-provider.mjs:571 passes runtimeEntry: '/nonexistent-runtime-entry' into provisionPersonalProviderConfig; the call never reaches resolveBuiltinConfig. The byte-parity assertion still passes because the written document does not depend on the template list.

  4. packages/driver/personal-provider.mjs:137 — nit — The new comment names the zod layer QAn -> RHA. The existing comment at line 78 names the same layer QAn -> RHa.

VERDICT: REQUEST_CHANGES

@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at 3f2a773: 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.

Findings

  1. major — packages/driver/personal-provider.mjs:341
    A family file already on disk is never rewritten (exists(target) returns write: false). The fix(cli): seed personal provider config so -p --model works on a fresh host #7 seeder wrote access: { apiKey } with no type. This PR's own commit says that shape makes the kernel's strict parser reject the file, so -p fails. Fresh imports now include type: 'api-key' (probed: {"type":"api-key","apiKey":"sk-test"}), but an existing typeless file is left byte-for-byte unchanged (provisioned: false, reason: personal provider config already present). doctor only reports malformed (access.type) and packages/cli/zagent.mjs:635 still tells the user to fix the model list. The live hosts this change names stay broken until someone deletes provider_config.json by hand.

  2. minor — packages/driver/personal-provider.mjs:358
    The new guard runs only when access is present and type is not a string. Probed against a canonical GLM-5.3 template: config: null, a missing access object, and access.type: '' all come back modelResolutionCheck ok: true. The tests cover { apiKey } and a non-object access, not these shapes.

  3. minor — packages/driver/personal-provider.mjs:403
    The PR body says that if the builtin model list is unavailable, nothing is written and doctor says why. The code seeds anyway and sets unverifiedMemory flush completed.
    (selectionVerdict at line 283; gate at lines 404–406). The unverified unit test matches the code; the summary does not.

  4. nit — packages/driver/personal-provider.mjs:307
    The @returns type for planPersonalProviderConfig still omits unverified, which every return path now sets.

No AI-attribution trailers in git log --format=%B origin/HEAD..HEAD or in the PR body. No new broken links, secrets, or internal hostnames in the diff. node packages/cli/test-personal-provider.mjs and node packages/cli/test-update-command.mjs passed. make check (npm test / verify-public-package.mjs) passed at 3f2a773.

VERDICT: REQUEST_CHANGES

@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at 7125946: REQUEST_CHANGES

review

我先读 PR 元数据、完整 diff 和仓库自检说明,再按文件核对这次提交。PR 元数据和仓库结构已经对齐。接下来核对提交说明、diff 和仓库自检命令。Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.
自检和关键单测一起跑,核对 doctor 对缺 access.type 的判定是否和启动路径一致。Memory flush started.
Memory flush completed.
Memory flush started.
Memory flush completed.

Findings

  1. major packages/driver/personal-provider.mjs:499 — 磁盘上仍缺 access.type 时,modelResolutionCheck 按内存里修好的规则返回 ok: true。plan.repair 被丢掉。zagent doctor(packages/cli/zagent.mjs:631-637)只在 res.ok === false 时把检查标成不健康,所以这种文件会走健康退出。内核严格解析会拒绝没有 type 的 access 块,文件在 doctor 之后保持原样。PR 说明写的是 doctor 会标出缺失或无类型的 access。新测试把这个假健康结果固定下来了:packages/cli/test-personal-provider.mjs:752-755 断言 ok === true,并断言 doctor 不改文件。

  2. major packages/driver/personal-provider.mjs:458 — 原地补 type: 'api-key' 只发生在 provisionPersonalProviderConfig 里。调用点只有带 --model / --effort 的 runPrintOnce(packages/cli/zagent-print.mjs:65-66、193-194)、commit-msg 和 models test。普通 zagent -p(没有这两个 flag)和交互启动(packages/cli/zagent.mjs:774-801)直接拉起内核,不会补写。提交说明里内核自己的 -p 会因这种文件失败;这条路径在补写落地之前仍然会失败。

  3. minor packages/driver/personal-provider.mjs:418-425 — 家族规则在模型不在模板列表里时仍然写入,包括错误的 defaultModelSelection。目标文件已存在后不会再按 cli 的 model.main 重写。之后把 cli 改成模板里的合法 id,个人配置里的默认选择仍是当初的错误 id。自定义 provider 对同一类永久 miss 会拒绝播种(同文件 410-422 行)。packages/cli/test-personal-provider.mjs:578-582 把“未知 id 也播种”锁成了预期。

  4. nit packages/cli/test-personal-provider.mjs:621 — “从 env 指定的 builtin 文件播种”只断言 provisioned。家族规则在模板列表不可读时也会播种,所以这条断言在文件根本没被读时同样通过。同文件 702-704 行的 preset 用例才检查了列表内容。

已核对

  • make check(npm test / scripts/verify-public-package.mjs)在 7125946 上 PASS,sourceDirty: false。
  • node packages/cli/test-personal-provider.mjs 与 node packages/cli/test-update-command.mjs 均退出 0。
  • git log --format=%B origin/HEAD..HEAD 的四条提交正文和 PR body 没有 Co-Authored-By、也没有 Generated with / Generated by。
  • diff 没有新的 URL 或断链。
  • 补写走 atomicWriteFileSync(mode 0600)。测试密钥是假的 sk-test-zagent-000。没有把内部主机名或凭据写进公开树。

VERDICT: REQUEST_CHANGES

@robotlearning123

Copy link
Copy Markdown
Member Author

Independent review, owner-directed Sonnet high-quality lane (workflow wf_a064b94f-ab9). Two independent Sonnet reviewers looked at each PR through different lenses (correctness/test-oracle, and safety/scope/ops) at high effort. An adversarial Sonnet verifier then tried to refute every merged finding by execution and did a completeness pass. This is a same-family stand-in while grok is hanging and devin reports model-not-found.

VERDICT: FIX-FIRST. Reviewed at pinned 7125946 and re-checked by the loop session at the current tip 08b350c.

major (regression): packages/driver/personal-provider.mjs:373-374 requires a typed access on every rule. A kernel-valid provider_config.json therefore makes doctor report NOT RESOLVABLE and exit 1.

The kernel schema allows absent or null access:

  • /opt/ZCode/resources/glm/zcode.cjs declares access:Kji.nullable().optional().
  • Its superRefine forbids access on account:* ids: e.providerId.startsWith("account:")&&e.config.access!==void 0&&t.addIssue(...).

Session probe of modelResolutionCheck, with model.main=zai/glm-5.3, a good typed zai rule, and one extra rule:

                 only_typed  +overlay(no access)  +access:null  +account:* rule
tip 08b350c      ok:true     ok:false             ok:false      ok:false   (malformed (access.type))
pinned 7125946   ok:true     ok:false             ok:false      ok:false
master driver    ok:true     ok:true              ok:true       ok:true

Suggested fix:

  • Flag access only when it is present, non-null and typeless or the wrong type.
  • Never require it on account:* ids.
  • Adjust the "missing access is flagged" test (test-personal-provider.mjs:736), whose comment over-states what the kernel rejects.

Minor findings:

  1. selectionVerdict judges any rule with a templateId only by the builtin template's model list and ignores that rule's own personalModelIds.
  2. The never-throw catch makes an internal error, such as exists throwing EACCES, invisible in doctor: modelResolutionCheck returns null, where master threw loudly.
  3. Doctor judges a typeless-access file as "the repair would leave it", but only -p, commit-msg and models-test runs repair it. The tip commit 08b350c addresses this one.
  4. The repair path has no oracle. Replacing fresh.repair with plan.repair ?? fresh.repair still passes.
  5. The comments at lines 343, 442 and 457 still say an existing file is "never overwritten".

Pre-existing:

  • trim-collided cli keys (zai / zai ) are both imported;
  • model.main is not normalised.

Session execution at 08b350c (env -i, temp HOME, ZCODE_RUNTIME=no-runtime): node packages/cli/test-personal-provider.mjs gives all checks passed. The existing tests do not catch the regression above.

@robotlearning123

Copy link
Copy Markdown
Member Author

agent-review (grok) at 08b350c: REQUEST_CHANGES

review

我先读 PR 元数据、完整 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.
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.

Findings

  1. major Memory flush started. packages/driver/personal-provider.mjs:373 — 每条 rule 都被要求带非空 access.type,但内核个人配置 schema 把 access 定为可缺省。已有文件会被医生判成不可解析,且不会被修复写回。

内核(本机 /opt/ZCode/resources/glm/zcode.cjs,parsePersonalProviderConfigMap / Xtr → Ktr.parse)里个人 rule 的 config 来自 kz:access: Kji.nullable().optional()。Kji 是按 type 的 discriminated union(api-key | zhipu-coding-plan-api-key | zhipu-account)。缺省或 null 能通过;只有 access 对象存在但没有 type 才会被拒绝。account:* 还被 e3i 的 superRefine 禁止携带 access。

本 PR 在已有文件分支里对每条 rule 执行:

      if (!c.access || typeof c.access !== 'object' || typeof c.access.type !== 'string' || !c.access.type)
        return malformed('the personal provider config is malformed (access.type)');

实测(modelResolutionCheck,临时 HOME,模型 zai/glm-5.3 已在 personalModelIds 里):

文件形状 结果
config: { personalModelIds: ['glm-5.3'] }(无 access) ok: false,malformed (access.type);provisioned: false,文件未改
access: null 同上
另有一条 account:zai-individual-coding-plan 且 config: {},旁边的 zai 规则类型正确 整文件仍是 malformed (access.type)

zagent doctor Memory flush completed.

Findings

  1. major — packages/driver/personal-provider.mjs:373
    An existing personal-provider file is treated as unresolvable unless every rule has a non-empty access.type. The kernel parser does not require that.

    In /opt/ZCode/resources/glm/zcode.cjs, parsePersonalProviderConfigMap (Xtr, ~553783) is Ktr.parse over e3i rules. access on that config is Kji.nullable().optional() (~535675). Kji is a discriminatedUnion on type (api-key | zhipu-coding-plan-api-key | zhipu-account), so a present access object must be typed, but an omitted or null access parses. account:* rules are valid only when access is absent (e3i superRefine: access on those ids is rejected). decodeProviderConfigFile loads rules through Xtr (~599717).

    On this commit, a file the kernel accepts is refused and never repaired:

    • { providerId: 'zai', config: { personalModelIds: ['glm-5.3'] } } → modelResolutionCheck returns ok: false, detail malformed (access.type). provisionPersonalProviderConfig leaves the bytes unchanged (repaired unset).
    • access: null → the same verdict.
    • { providerId: 'account:zai-individual-coding-plan', config: {} } next to a fully typed zai rule → the whole file is malformed, so the typed rule is reported unresolvable too.

    packages/cli/zagent.mjs:631-637 marks doctor unhealthy when res.ok is false, so doctor exits non-zero for a file the runtime would load. packages/cli/test-personal-provider.mjs:739 ('missing access', mk(undefined)) asserts that outcome. JSON.stringify drops access: undefined, so the fixture is the kernel-valid shape and the test locks in the false reject.

    A typeless object that already has a string apiKey is a separate, consistent case: Kji rejects it, and ApiKeyAccessConfig defaults type to api-key (this.type=t.type??"api-key", ~547615). The in-place repair for that shape is fine.

No AI-attribution in git log --format=%B origin/HEAD..HEAD or the PR body. No new broken links, secrets, or injection. make check → status: PASS. node packages/cli/test-personal-provider.mjs → 104 ok. node packages/cli/test-update-command.mjs → PASS.

VERDICT: REQUEST_CHANGES

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