Skip to content

feat: own the platform model configuration in the plugin - #10

Merged
EricWvi merged 8 commits into
ora-space:mainfrom
ObsisMc:feat/354-opencode-model-adapter
Oct 21, 2026
Merged

EricWvi merged 8 commits into
ora-space:mainfrom
ObsisMc:feat/354-opencode-model-adapter

Conversation

@ObsisMc

@ObsisMc ObsisMc commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

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:

agent/start + modelContext
  → accept only empty Settings, an https proxy, a private XDG_CONFIG_HOME
  → spawn `opencode acp` with OPENCODE_CONFIG_CONTENT (one ora-model provider for the frozen
    model, apiKey "{env:ORA_MODEL_ACCESS_TOKEN}"), OPENCODE_CONFIG_DIR, project config disabled
  → return AgentModelApplied
  • The configuration text names the token through {env:...}; only the CLI's own environment holds it.
  • openai-completions maps to @ai-sdk/openai-compatible, anthropic-messages to @ai-sdk/anthropic, as before.
  • A start whose host provides no private configuration root is refused before spawning instead of reading the host's own opencode.json.
  • An Effect reactivate restart keeps the route. A start without a model context is unchanged.

Not in this PR: the Agent declaration (assets/config.json) and the --agent-configuration packaging 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.json and deno.lock pin it. Rebased onto main 8de5860; the test task merged with #11's packaging test, so it runs both tests/package_test.ts and tests/model_route_test.ts, and CI now runs deno task test (the workflow step the handover left for someone with the workflow scope).

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_URL and OPENCODE_MODELS_PATH are overridden with empty strings and both OPENCODE_CONFIG_PROJECT_DISABLE and OPENCODE_DISABLE_PROJECT_CONFIG are true (2.0.32 reads the former first); frames to the CLI that authenticate, log out, touch providers or select a model other than the frozen ora-model/<id> are refused; a routed execution spawns only the bundled CLI (no ORA_OPENCODE_BIN/PATH); agent/list_models and 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/.../v1 path), and the frozen target and pluginId are 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); AgentModelApplied is returned once the CLI is spawned, before its initialize/session/new reported the frozen model (the gateway checks the binding on every request); session/load/resume restore a persisted model that the guard does not check, which depends on the Node's per-execution XDG_DATA_HOME; the route and token stay with the client after agent/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, lint and test (20 tests: 15 here, 5 in tests/package_test.ts) pass against the published SDK 0.11.0; the changed files pass deno fmt --check.

Merge Danger

Door: two-way

Blast Radius: agent-start

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.
@ObsisMc
ObsisMc force-pushed the feat/354-opencode-model-adapter branch from 4f2e82b to fd54585 Compare October 21, 2026 07:07
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.

@wanglongan587 wanglongan587 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查版本: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 仍是旧绑定,Effect reactivate 会用旧 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、SDK verifyModelApplied、effects.reactivate、bundled-only 选择,也没有继承环境泄漏、空 token、目标/包不匹配、拒绝后旧 route 等用例。

未验证

没有运行真实 opencode acp 或真实 gateway;上述 CLI 侧行为来自代码与 ADR 对照,真实验收仍由本运行后续承接。此结论只针对 a81f517。

@wanglongan587 wanglongan587 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查版本:cfea22cb2a1cf0f0fe2dbe8bf1d400670a4ab241(base 8de5860,8 commits,12 files,+1164/−32)。结论:Approve(无阻塞)。

上一轮四个阻塞在本 head 均已修复,有对应测试和 pinned 版本源码证据。

四个阻塞

  1. 绑定执行的模型发现(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)。
  2. 平台启动只运行 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 的进程里确认只 spawn packageCommand。
  3. 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 不在转发帧和日志里)。
  4. 拒绝认证/供应商/模型变更帧(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,Effect reactivate 会用旧 route 重启。当前插件进程只服务一个执行,风险有限,但建议失败时 useRoute(undefined) 或 client.stop() 清 route,并补用例。
  • applied 确认早于 CLI 实际应用:lifecycle.ts:40-44 在 client.start()(spawn+attach)后立即返回;SDK verifyModelApplied 只校验回传三元组。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)。CI check 在 cfea22c 上为 success。
  • pinned opencode v2.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 描述的两处事实。

@EricWvi
EricWvi merged commit b0e72d5 into ora-space:main Oct 21, 2026
1 check 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.

3 participants