Repository navigation
feat: own the platform model configuration in the plugin - #10
Conversation
SDK 0.11.0 lets agent/start return the AgentModelApplied confirmation a platform-model start requires. deno.json and deno.lock pin the published 0.11.0.
A start that carries a frozen model context gets the OpenCode provider
configuration the Node used to generate: one ora-model provider for the
frozen model, the token referenced through {env:...}, a private config
directory and project configuration disabled. agent/start confirms the
exact configuration once the CLI started with it, an Effect restart
keeps it, and a start without a private config root is refused.
4f2e82b to
fd54585
Compare
deno task test needs no CLI, so CI now runs it after check and lint; the model route tests guard the platform start contract on every PR.
cc7168a to
a81f517
Compare
wanglongan587
left a comment
There was a problem hiding this comment.
审查版本:a81f51790e9f9d053e5d67116e225eb4300807c4(base 8de5860)。结论:Request changes。
路由生成本身是对的(一个 ora-model provider、token 只以 {env:...} 引用、私有 XDG_CONFIG_HOME/OPENCODE_CONFIG_DIR、关闭 project config、返回精确 AgentModelApplied),但这次替代路线同样丢了几项 ADR 要求的保护。
我核对过的
- 本地
deno task check/lint/test全部通过(11 passed)。 - 逐行读了
model-route.ts、opencode-client.ts、command.ts、lifecycle.ts、models.ts,并与 #9 对照。
阻塞
1. 绑定执行的模型发现是未授权的影子运行,且缓存只按 cwd(ADR D5)。
src/main.ts:119-127 无条件调用 listOpenCodeModels;src/handlers/models.ts:32,60-127 用 spawnOpenCode 起第二个 opencode acp,不携带 modelContext 的 route env,并按 cwd 缓存结果。D5 明确:“模型发现和别名不能扩大冻结输入……额外发现不能制造未经准入的影子运行……缓存身份至少区分执行/用户隔离范围、Plugin ID/发行、声明/配置 revision 和模型绑定,不能只有 cwd。”
对一个带 modelContext 的执行,这个探测既没有平台 token,也不在本次授权里;它返回的是宿主/用户配置下的模型集合,和冻结模型无关。最小修法:route 存在时直接返回 ora-model/<model.id>(和 #9 的 frozenModel 一样),不 spawn;若仍要探测,必须带同一 route env 并把缓存键扩到执行/配置 revision。
2. 平台启动可以运行另一个 OpenCode CLI(ADR D2)。
src/services/command.ts:53-88 的 spawnOpenCode 接受 ORA_OPENCODE_BIN,包内没有 bundled CLI 时回退 PATH;src/services/opencode-client.ts:118-120,290 在有 route 时仍用它。D2:“身份/声明/契约不匹配必须在首次模型调用前拒绝,不能启动另一个版本、直连上游或用部署 Key 重试。” 不同版本的 CLI 拿到 ORA_MODEL_ACCESS_TOKEN 和配置后,宿主侧的 verifyModelApplied 仍会通过(它只看回传的三元组)。有 modelContext 时只允许包内 bundled CLI,其他情况以 AGENT_UNUSABLE 失败。
3. CLI 的 stderr / 非 JSON stdout / 错误帧原样进入日志(ADR D2)。
src/services/opencode-client.ts:225-227(非 JSON stdout 原文)、:238-241(错误帧原样转发)、:261-266(stderr 逐行 info)都没有平台模式的脱敏。D2:“临时令牌、私钥内容及带认证头的 provider 初始化材料不能进入……错误链或用户日志……原始供应商/CLI 错误不直接公开。” 恢复 #9 的 sensitive 模式:route 存在时错误帧只保留数值 code + 通用 message,stderr/非协议行不记录原文。
4. 绑定执行仍转发认证/供应商/模型变更帧(ADR D4/D5)。
src/handlers/acp.ts:31-39 与 src/services/opencode-client.ts:142-150 把所有 host→CLI 帧直接转发,包括 authenticate、logout、providers/*、session/set_model、session/set_config_option。D4:“在途会话只能使用创建时的冻结输入”;D5:“会话配置选项不能扩大允许集合。” 即使 OpenCode 的 provider 策略会限制其他 provider(需要在真实 CLI 上确认),adapter 仍应把非冻结模型的 set_model/set_config_option 以 -32602 拒绝,并拒绝 provider/认证变更,而不是交给 CLI 再失败。
should-fix
OPENCODE_CONFIG没有清空:model-route.ts未设置OPENCODE_CONFIG: ""(#9 设过)。在OPENCODE_CONFIG_CONTENT之外,继承的OPENCODE_CONFIG仍可能加载额外的 plugin/MCP/instructions;配合 desktop#708 的 env 清理一起解决更彻底。- “私有配置根”只检查了非空:
src/main.ts:104-108读取XDG_CONFIG_HOME,model-route.ts:48-55只拒绝空串;若宿主把用户的.config传进来也会接受。建议用专用子目录(如join(configDirectory, "opencode-platform"))并确认在 checkout 之外;AGENTS.md 的表述也要相应收紧。 - 代理与身份校验弱于 #9:
model-route.ts:98-107只要求 HTTPS、无凭据/query/fragment,不校验/runtime/openai/v1或/runtime/anthropic/v1,也不检查target/pluginId。Node 会校验,adapter 侧是廉价防御。 - 被拒绝的后续平台启动会保留旧 route/token:
src/handlers/lifecycle.ts:36-40先算 route 再useRoute;如果之前成功过一次、这次modelRoute抛错,#route仍是旧绑定,Effectreactivate会用旧 route 重启。建议先useRoute(undefined)再计算。 - 确认早于 CLI 实际应用:
lifecycle.ts:40-44在client.start()返回后立即返回 applied;spawn+attach 并不证明OPENCODE_CONFIG_CONTENT已被解析应用。D2 要求“CLI/ACP 的实际就绪和绑定应用成功之后才允许首条 prompt”。至少把 host 的 initialize 作为就绪门并补一条配置应用的断言。 - 测试:
tests/model_route_test.ts覆盖了生产modelRoute/startOpenCode/OpenCodeClient和注入 spawn,但没有走main.ts的onStart、SDKverifyModelApplied、effects.reactivate、bundled-only 选择,也没有继承环境泄漏、空 token、目标/包不匹配、拒绝后旧 route 等用例。
未验证
没有运行真实 opencode acp 或真实 gateway;上述 CLI 侧行为来自代码与 ADR 对照,真实验收仍由本运行后续承接。此结论只针对 a81f517。
wanglongan587
left a comment
There was a problem hiding this comment.
审查版本:cfea22cb2a1cf0f0fe2dbe8bf1d400670a4ab241(base 8de5860,8 commits,12 files,+1164/−32)。结论:Approve(无阻塞)。
上一轮四个阻塞在本 head 均已修复,有对应测试和 pinned 版本源码证据。
四个阻塞
- 绑定执行的模型发现(D5)——已修复。 route 存在时
onListModels直接返回冻结模型、不 spawn(src/main.ts:119-138),OpenCodeClient.frozenModel由useRoute设置(src/services/opencode-client.ts:79-81,104-109);选择器在model-guard中收窄。残留(非阻塞):start 前frozenModel为 undefined 时仍走 cwd 缓存探测,测试是读main.ts源码文本而非 behavior(见 should-fix)。 - 平台启动只运行 bundled CLI(D2)——已修复。
spawnBundledOpenCode只processes.spawn({ packageCommand, ...invocation }),不读ORA_OPENCODE_BIN、不回退 PATH,失败统一AGENT_UNUSABLE(src/services/command.ts:100-120;opencode-client.ts:332-335);测试在设置ORA_OPENCODE_BIN的进程里确认只 spawnpackageCommand。 - stderr/非 JSON stdout/错误帧脱敏(D2)——已修复。 stdout/stderr 每行先 scrub 再 parse/转发/记录(
opencode-client.ts:255-316),错误帧只保留整数code+ 通用 message,丢弃 data(model-guard.ts:115-147);secret 清单含 token、proxy URL、origin、host、hostname(model-route.ts:131-140)。有负向测试(token/网关 host 不在转发帧和日志里)。 - 拒绝认证/供应商/模型变更帧(D4/D5)——已修复。
authenticate、logout、auth/*、_auth/*、providers/*一律拒绝;非冻结session/set_model/set_config_option(model)以-32602拒绝;冻结值及 effort variant 放行;无 id 通知 drop(model-guard.ts:36-103,177-180)。我对照 pinned OpenCode v2.0.32 源码确认实际 ACP 方法面(initialize、authenticate、session/*),模型切换实际入口是session/set_config_option的model选项,guard 已覆盖;restart 保留 route(lifecycle.ts:39,测试tests/model_route_test.ts:131-151)。
非阻塞事项(建议本轮至少更正描述,其余承接)
- PR 描述数字:正文写
tests/model_route_test.ts (new, 6 tests)、deno task test (11 tests);实际 15 + 5 = 20 passed(我本地deno task test已复核)。请更正。 - PR 描述「#9 的保护已全部迁移」不准确:私有配置根仍只检查非空(
model-route.ts:56-63、main.ts:104-108),代理 URL 仍只要求 HTTPS 无凭据/query/fragment(model-route.ts:119-129),未像 #9 那样校验/runtime/.../v1路径与 target/pluginId。当前 host(desktop#708)会给私有 HOME/XDG 且 Node 侧校验,但描述应改成「除代理路径/身份校验外的 #9 保护」或补上校验。 - 被拒绝的后续平台启动会保留旧 route/token:
lifecycle.ts:36-39先算 route 再useRoute;modelRoute抛错时旧 route 留在 client,Effectreactivate会用旧 route 重启。当前插件进程只服务一个执行,风险有限,但建议失败时useRoute(undefined)或client.stop()清 route,并补用例。 - applied 确认早于 CLI 实际应用:
lifecycle.ts:40-44在client.start()(spawn+attach)后立即返回;SDKverifyModelApplied只校验回传三元组。host 的 initialize 握手与 gateway 每次请求的绑定校验是 backstop,但建议后续补一个可见的就绪门(至少确认 initialize/session/new 的模型选择为冻结模型)或在描述中承接。 - 测试:
onListModels冻结分支是源码文本断言,建议抽成可测函数补 behavior 测试;未走main.ts onStart/reactivate;没有「拒绝后旧 route」「session/load 选择」用例。 - session/load/resume 的选择未校验:pinned 2.0.32 会从持久化 session 恢复
session.model,guard 不检查;当前 Node 侧XDG_DATA_HOME每执行私有,不可达,但建议在描述中承接(依赖 host 隔离的假设)。 - 其它:managed/system 配置来源可越过插件配置(backstop 是 gateway 每次请求校验);
main.ts:95-108在 start 前设置#send/#cwd,失败不回滚;agent/stop后 route/token 仍保留(restart 语义),建议文档说明。
已核对
- 本地
deno task check/lint/test(20 passed)通过;改动文件在 LF 归一后deno fmt --check通过(仓库其它三个既有文件在 base 上就不通过,与本 PR 无关;CI 未跑 fmt)。CIcheck在cfea22c上为 success。 - pinned
opencodev2.0.32 源码:OPENCODE_CONFIG/DIR/PROJECT_DISABLE/MODELS_URL/PATH/CONTENT的读取顺序与空值语义、project config 关闭、v1 配置迁移、{env:...}替换、@ai-sdk/*重写均成立;OPENCODE_CONFIG等置空有效。 - 与 desktop#708 的约定:配置会话的插件进程从空环境 + 私有 HOME/XDG 启动,token 只在
agent/start的 modelContext,host 侧有NODE_EXTRA_CA_CERTS。
交付物 / ADR
12 个文件全是交付物,8 个 commit 拆分合理;Refs ora-space/circus#354、合并顺序 cloud#106 → desktop#708 → 本 PR 正确。ADR 保持 approved 的判断正确:本 PR 是 OpenCode 迁移的一半,真实 CLI/gateway 验收与 implemented specs PR 由后续运行承接。
以上非阻塞项不影响 Approve,建议 merge 前更正 PR 描述的两处事实。
Summary
The OpenCode provider configuration that the Ora Node used to generate (
apps/ora-node/src/model_proxy/opencode.rs) moves into this plugin, as the plugin-owned adapter ADR requires:{env:...}; only the CLI's own environment holds it.openai-completionsmaps to@ai-sdk/openai-compatible,anthropic-messagesto@ai-sdk/anthropic, as before.opencode.json.reactivaterestart keeps the route. A start without a model context is unchanged.Not in this PR: the Agent declaration (
assets/config.json) and the--agent-configurationpackaging gate, which ship together with the release step once compatible hosts are deployed. Until then no Node sends this plugin a model context and the Node keeps its legacy OpenCode route.Depends on: plugin SDK 0.11.0 (ora-space/desktop#703), now merged and published on JSR;
deno.jsonanddeno.lockpin it. Rebased onto main8de5860; thetesttask merged with #11's packaging test, so it runs bothtests/package_test.tsandtests/model_route_test.ts, and CI now runsdeno task test(the workflow step the handover left for someone with theworkflowscope).Covers the same scope as the Draft #9 by a different route. Most protections from #9 are carried over onto this route:
OPENCODE_CONFIG,OPENCODE_MODELS_URLandOPENCODE_MODELS_PATHare overridden with empty strings and bothOPENCODE_CONFIG_PROJECT_DISABLEandOPENCODE_DISABLE_PROJECT_CONFIGaretrue(2.0.32 reads the former first); frames to the CLI that authenticate, log out, touch providers or select a model other than the frozenora-model/<id>are refused; a routed execution spawns only the bundled CLI (noORA_OPENCODE_BIN/PATH);agent/list_modelsand the model selector report only the frozen model; CLI error messages are replaced and route secrets are scrubbed from every output line.Not carried over from #9, and relied on the Node instead: the private configuration root is only checked to be non-empty, the proxy URL only to be HTTPS without credentials, query or fragment (not its
/runtime/.../v1path), and the frozentargetandpluginIdare not compared with this plugin's own. ora-space/desktop#708 starts a configured session from an empty environment with a private HOME/XDG and checks the package identity before granting access.Known limits, left for follow-up: a later platform start that is refused keeps the previous route for an Effect restart (one plugin process serves one execution today);
AgentModelAppliedis returned once the CLI is spawned, before itsinitialize/session/newreported the frozen model (the gateway checks the binding on every request);session/load/resumerestore a persisted model that the guard does not check, which depends on the Node's per-executionXDG_DATA_HOME; the route and token stay with the client afteragent/stop, for a restart.Merge order: ora-space/cloud#106 → ora-space/desktop#708 → this PR (no code dependency on them; nothing sends a model context until the Node half is deployed).
ADR: plugin-owned-model-adapters remains approved; this is the OpenCode migration step of its rollout section.
Refs ora-space/circus#354
Evidence
tests/model_route_test.ts(new, 15 tests;deno task test): the exact configuration the Node wrote, without the token in its text; Anthropic provider selection; refusal of Settings, non-object Settings, non-HTTPS or credentialed proxies and a missing configuration root without echoing the token; confirmation plus a restart that keeps the environment; a local start unchanged; refusal before spawning.deno task check,lintandtest(20 tests: 15 here, 5 intests/package_test.ts) pass against the published SDK 0.11.0; the changed files passdeno fmt --check.Merge Danger
Door: two-way
Blast Radius: agent-start