Skip to content

feat(node): hand model grants to plugin-owned adapters - #708

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

EricWvi merged 18 commits into
ora-space:mainfrom
ObsisMc:feat/354-node-model-adapter-grants

Conversation

@ObsisMc

@ObsisMc ObsisMc commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Cloud frozen configuration ──► Controller ──► Node StartAgentSession.agent_configuration
Node handshake ── model_adapter {contract, target, protocols} ──► Controller ──► RegisterNode
Node session:  grant (mTLS) ─► grant serves exactly the frozen protocol + model?
               ├─ no  → revoke, end before any plugin process
               └─ yes → private home + public CA in env, token only in agent/start modelContext
                        → adapter must confirm exact configuration → ACP → first prompt
  • node-protocol: a configured session names the non-secret FrozenAgentConfiguration beside its model binding; it must describe the session's own plugin release, contract version 1 and one JSON object of Settings. HelloAccepted.model_adapter advertises the Node's target, contract and protocols, only beside model_proxy.
  • Controller: maps Cloud's frozen configuration into the Node start, reports the handshake's adapter facts at registration, and claims or retransmits a configured session only to an incarnation whose target, contract and protocol admit it. Mismatched work stays queued in Cloud.
  • Node: a configured session gets no Node-written OpenCode configuration. Before any grant, under the package lease, the Node proves the installed package against the frozen configuration: an install receipt (kept outside the package, cleared before republishing) must match the frozen digest and target, the installed declaration's fingerprint, schema and adapter contract must match, the Settings must validate against it, and it must offer the frozen protocol (agent_model_configuration_invalid / agent_model_protocol_mismatch, no process started). The plugin and every process the host spawns for it start from an empty environment plus PATH, LANG, LC_ALL, TMPDIR and the session's own variables. The grant must serve exactly the frozen protocol and model (model_grant_mismatch otherwise, revoked). The token travels only in the agent/start model context; the runtime refuses ACP until the adapter confirms the configuration, and every failure path revokes the grant. Only official/ora-space.opencode may still take the legacy route without a frozen configuration; any other plugin with a binding ends agent_model_configuration_required before a grant. A configured start keeps AGENT_NOT_INSTALLED/AGENT_UNUSABLE apart while still dropping the plugin's message.
  • The echo fixture acts as a minimal adapter, which is also the extension proof: no Cloud page, Controller or Node code names a plugin ID on this path.

Stack: rebased onto main 42f224a. third_party/cloud pins Cloud main 4d35aad, which contains the merged ora-space/cloud#106 (and cloud#103); proto/ is identical to the earlier pin, and proto:check regenerates the committed client without a diff.

Covers the same scope as the Draft #705 by a different route. The protections from #705 that the first review asked for are now carried over: the install receipt and start-time package/declaration/Settings check (from #705's receipt approach), the empty inherited environment (#705's ProcessEnvironment::Empty, cherry-picked with its test), the legacy route fence and the distinct start codes. Also aligned with Cloud: frozen Settings up to 256 KiB (as Cloud stores and the SDK accepts) and the adapter capability only on x86_64/aarch64-unknown-linux-gnu.

Merge order: ora-space/cloud#106 (merged) → this PR → ora-space/claude-code-agent#4 → ora-space/opencode-agent#10.

ADR: plugin-owned-model-adapters remains approved. This PR implements the Node/Controller half of D1–D4 and D6. Still open: the concrete plugin adapters (ora-space/claude-code-agent and ora-space/opencode-agent PRs from the same run), publishing declarations, and real Claude acceptance; tracked in the run below.

Refs ora-space/circus#354

Evidence

  • Before: a frozen configuration stopped at Cloud; the Node only knew a binding and always wrote OpenCode configuration.
    After: a_configured_adapter_receives_its_grant_only_in_the_startup_context runs a real plugin process: the adapter answers with the frozen model and the platform proxy, token_in_environment=false, host_environment=false (none of the test runner's CARGO* variables reach it), the token is in neither history nor Thread, and ending revokes the grant. an_unconfirmed_adapter_never_receives_a_prompt ends agent_failed/agent_unavailable with no Thread event, the grant revoked and the plugin process gone.
  • a_configuration_the_installed_package_cannot_prove_never_gets_a_grant and an_undeclared_plugin_never_takes_the_legacy_model_route: the session ends before any grant request or plugin process. installed_configuration_requires_receipt_declaration_and_settings covers a missing receipt, another package digest, another fingerprint, undeclared/missing Settings, another protocol and a retired receipt; empty_environment_does_not_inherit_host_authority spawns a real process.
  • Unit tests: adapter route keeps the token out of every environment variable; grants for another protocol, model id or limit are revoked before any CLI state exists (model_grant_mismatch).
  • Protocol tests: wire shape and round trip of the configuration and capability, including revision 0 (an Agent on the personal model default that was never saved, such as a declared OpenCode); rejection of another plugin/version, empty target, unknown contract, non-object or malformed Settings, missing binding, uppercase digest, adapter without model_proxy, duplicate or unknown protocols.
  • Controller tests: configured work stays queued with no adapter, another target or another protocol; with a match the configuration reaches the Node unchanged (a 20-digit Settings number included) and registration carries the adapter facts.
  • cargo test for ora-node-protocol, ora-node-db, ora-agent-runtime, ora-plugin-protocol, ora-controller and ora-node plus ora-process, ora-plugin-config and ora-plugin-manager pass on head c91935c (including the standalone suite, 52 passed). The CI clippy command (--workspace --exclude ora-desktop --exclude ora-desktop-tests -- -D warnings), cargo fmt --check and check-rust-size pass.

Merge Danger

Door: one-way

New fields are optional on the wire; legacy starts and handshakes are unchanged, and an older Controller ignores model_adapter. An older Node does not ignore agent_configuration: AgentSessionSpec denies unknown fields, so it would reject such a start. That is safe only because nothing sends one to it: the Controller dispatches a configured session only to a Node whose model_adapter admits it (an older Node advertises none), and Cloud's personal_agent_target stays empty until compatible Nodes are deployed. One-way because, once that target is set, rolling a Node back to a build without this PR makes it reject configured starts; a rollback has to clear the Cloud target first.

Blast Radius: agent-session-startup

Cloud keeps plugins.personal_agent_target empty until compatible Nodes and plugins are deployed; until then no configured session is admitted.

@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.

审查版本:fdcb67973c23371147ea3486f16ff0bc849602a8(base 9774456,对照 upstream/main 42f224a)。结论:Request changes。

协议、Controller 派发门禁、grant 与 adapter 上下文的交接本身是对的,CI 8/8 通过。但这次替代路线把 #705 里与已批准 ADR 直接相关的三项保护丢了:两项是 ADR 明文要求,第三项会把 live token 交给任意未声明的插件。补齐后才能算实现 D1–D4/D6。

阻塞 1:启动时没有核对安装包与声明(ADR D2 第 1 步)

证据:

  • apps/ora-node/src/session/driver.rs:107-116 只取安装租约和 installed(plugin, version);runs_package()(:238-243)只比较包根和 contributes,没有读安装包的声明,也没有比较冻结的包摘要/指纹/Settings。
  • crates/node-protocol/src/domain/model_adapter.rs:77-113 只做格式检查:两个 digest 是 64 位小写 hex、Settings 是 ≤64 KiB 的 JSON 对象、plugin id/version 等于会话里的值、adapter_version == 1。package_sha256 与 declaration_fingerprint 只被转发(apps/ora-node/src/model_proxy/adapter.rs:49,51);adapter 的 AgentModelApplied 只是把 fingerprint 原样回显(crates/agent-runtime/src/plugin_agent/control.rs:119-125),证明它“收到过这个字符串”,不证明安装的声明哈希等于它。
  • settings_json 在 Node 上从未按声明校验。

ADR D2 第 1 步是“Node 核对执行指定的安装包及声明关联,取得安装使用租约…”,D2 还有“身份/声明/契约不匹配必须在首次模型调用前拒绝”;personal-agent-configurations D3 也写明“能力检查并不替代实际 Node 安装和启动时的精确身份检查”。#705 的 plugins/model_configuration.rs 用 receipt、读取 assets/config.json、指纹/协议/Settings 校验实现了这一条,这次没有替代实现。

后果:Cloud 在准入时校验过声明,但从准入到会话启动之间,同版本目录被重新发布或替换时,冻结摘要和实际运行的字节不再对应;冻结的声明指纹/Settings 也可能属于另一份声明。这类情况现在没有任何一层会拒绝,而 Node 会把 live grant 和 token 交给这个包。测试里也没有“替换包后必须拒绝”的用例(#705 有)。

最小修法:恢复 #705 的 receipt 方案。在 prepare_and_converse 拿到租约后、ModelAccess::open 之前,读取安装的声明并与冻结配置比较 target/schema/fingerprint/adapter contract,要求冻结 protocol ∈ 声明 protocols,并按声明校验 Settings;失败用有界错误码(如 agent_model_configuration_invalid / agent_model_protocol_mismatch)在首次请求前结束。

阻塞 2:插件进程继承 Node 的环境(ADR D4)

证据:插件和它经宿主拉起的 CLI 都用 TokioProcessSpawner(crates/plugin-lifecycle/src/runtime.rs:70,79,93;apps/ora-node/src/session/workload.rs:97-124),该 spawner 只在显式 spec.env 上追加,不清空继承(crates/process/src/tokio_process.rs:64-83)。apps/ora-node/src/session/host.rs:59-65 的注释还明确写着 “The rest of the host environment (model credentials, proxy settings) still reaches the agent”。这次只加了 HOME/XDG/CA(apps/ora-node/src/model_proxy.rs:379-389)。

ADR D4 明文:“不得读取宿主登录文件或从继承环境取得原生 provider Key”;“仅换 HOME 不足以证明路由固定。adapter 必须处理该 CLI 的专用配置目录变量、项目设置、环境和其他高优先级设置……不支持可靠覆盖控制的固定版本拒绝进入此能力。”插件进程持有 --allow-env(crates/plugin-lifecycle/src/permissions.rs),Node 环境里的 ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL/OPENAI_*/CLAUDE_CONFIG_DIR/OPENCODE_CONFIG 以及部署凭据会一并到达第三方插件及其 CLI。CLI 用环境变量覆盖 provider 配置时,请求会绕过平台网关直连上游,网关的逐次校验根本看不到它——这正是 D4 要防的“改向另一个上游/登录态”。

最小修法:把 #705 的 ProcessEnvironment::Empty + ModelProcessIsolation 拿回来,agent_configuration.is_some() 时使用 Empty spawner,只显式补 PATH/LANG/LC_ALL/TMPDIR 和本次会话变量;加一个真实子进程测试,断言继承变量不存在(#705 的 crates/process/tests/environment.rs 就是这种证据)。

阻塞 3:未声明插件的模型绑定无条件落到 OpenCode 旧路线(ADR 兼容与落地)

证据:apps/ora-node/src/session/driver.rs:156-161 用 map_or(ModelRoute::LegacyOpenCode, ModelRoute::Adapter);任何没有冻结配置的插件(只要 Cloud 给了 binding)都会走 model_proxy/opencode.rs,被注入 OPENCODE_CONFIG_CONTENT、ORA_MODEL_ACCESS_TOKEN(本次 grant 的 live token)和 OPENCODE_CONFIG_DIR。

ADR 兼容与落地:“旧 OpenCode 数据/默认选择的兼容过渡只能留在明确的旧路线,不能成为新插件缺能力时的回退。” 现在 Cloud 确实只为 OpenCode 建无配置 binding(internal/core/model_run_binding.go),暂时不可达;但 Node 协议本身允许,Cloud 一侧的遗漏或未来接入就会把一个与 OpenCode 无关的插件接上这条路线,并把 token 交给它。这种兜底不该建立在“Cloud 不会这么发”的假设上。

最小修法:把旧路线限定为明确的插件(official/ora-space.opencode,或由 Cloud 显式标记 legacy);其它插件有 binding 没配置就拒绝。补一条测试断言不发起 grant、不启动进程。

should-fix

  • 失败分类:crates/agent-runtime/src/plugin_agent/control.rs 的 adapter start 错误仍全部收敛为 Failed("agent_model_adapter_failed");#705 区分 AGENT_NOT_INSTALLED/AGENT_UNUSABLE(-32001/-32002)的 10 行改动没带过来。第一次在没装 CLI 的机器上跑 Claude,会从“Unavailable、安静重试”变成累计 3 次失败并进入 Failing/重启电路。建议补回;若留作后续,请在 PR 描述和运行 issue 写明承接 issue。
  • target 集合:apps/ora-node/src/session.rs:170-184 对任何 *-unknown-linux-gnu 宣告 adapter 能力,而 cloud#106 的 internal/core/agent_adapter_capability.go 只接受 x86_64/aarch64。其它 GNU target 的 Node 会在登记时被 Cloud 以 invalid_node_report 拒绝整条登记,而不是不宣告能力。两边取齐。
  • Settings 上限:Node 64 KiB(crates/node-protocol/src/domain/model_adapter.rs:20)与 cloud#106 回执的 256 KiB 不一致;请确认声明编译器的上限小于 64 KiB,否则同一份冻结配置会在 Node 被拒。
  • third_party/cloud:现在钉的是 cloud#106 的 head 1be0610,而 cloud#106 需要 rebase 并顺延迁移号;合并前按 cloud main 重新钉。

已核对、未验证

已核对:HelloAccepted.model_adapter 只在 model_proxy 时出现、旧 Node 省略;FrozenAgentConfiguration::validate 要求会话有 binding、Settings 是对象;Controller 在 claim 和 retransmit 两处都按 target/contract/protocol 匹配,不匹配保持排队;model_grant_mismatch 在生成任何 CLI 状态前撤销;结束/失败/进程退出都收敛到同一个 close();echo fixture 的真实进程测试覆盖 token 不在 env/history/Thread、未确认不收到 prompt、结束后撤销授权。

未验证:本地没有重跑 cargo(Windows 环境受 rsproxy 代理影响),门禁以 fdcb679 的 8/8 CI 为准;真实 Claude/OpenCode 验收仍由本运行后续承接。此结论只针对 fdcb679。

ObsisMc and others added 15 commits October 21, 2026 09:27
…figuration

Moves third_party/cloud to the cloud#106 head (1be0610) that adds the
frozen AgentModelConfiguration to AgentSessionSpec and the Node model
adapter capabilities to node registration, and regenerates the client.
…ability

A configured session names the exact non-secret configuration Cloud
froze at admission next to its model binding; it must describe the
session's own plugin release, a supported adapter contract and one
JSON object of Settings. HelloAccepted advertises the Node's adapter
target, contract and protocols, only beside model_proxy.
The Controller maps Cloud's frozen configuration into the Node start,
reports the handshake's adapter facts at registration, and claims or
retransmits a configured session only to an incarnation whose target,
contract and protocols admit it; other work stays queued in Cloud.
A configured session no longer gets Node-written OpenCode configuration.
The Node checks that the grant serves exactly the frozen protocol and
model, gives the plugin a private home and the public CA, and passes the
token only in the agent/start model context; the runtime refuses ACP
until the adapter confirms the configuration, and every failure path
revokes the grant. The echo fixture acts as a minimal adapter, and the
gateway fixture is shared with the session integration tests.
An Agent that keeps using the personal model default and declares no
Settings, such as a declared OpenCode, is frozen with configuration
revision 0. Refusing it left the session queued in Cloud until its
startup budget failed it with agent_start_failed.
…claration

Expose the Settings validation the editor uses for a compiled Agent
declaration, and let a host compile the declaration an installed package
actually carries, so a configured session can re-check both at start.
The installer records a receipt of the archive digest and target it
verified, outside the package, and clears it before republishing. A
configured session then re-checks under its package lease, before
requesting a grant, that the receipt matches the frozen package, that the
installed declaration's fingerprint, schema and adapter contract match,
that the frozen Settings validate against it and that it offers the
frozen protocol (ADR D2 step 1). A failure ends the session with
agent_model_configuration_invalid or agent_model_protocol_mismatch and
starts no plugin process.
A configured session's plugin and every process the host spawns for it
now clear inheritance and receive only PATH, LANG, LC_ALL, TMPDIR and the
session's own variables, so provider keys, base URLs and CLI
configuration in the Node's environment never reach the adapter or its
CLI (ADR D4). Sessions without a frozen configuration keep the existing
inherited environment. The echo fixture reports whether it still sees
the test runner's own variables.
A model binding without a frozen configuration used to fall back to the
legacy route for any plugin, injecting the OpenCode configuration and the
live grant token. Only official/ora-space.opencode may take that route
now; any other plugin ends with agent_model_configuration_required before
a grant is requested or a process starts.
A configured agent/start failure still drops the plugin's message, but
-32001 and -32002 again map to AgentNotInstalled and AgentUnusable, so a
host without the CLI keeps retrying quietly instead of tripping the
restart circuit.
Accept frozen Settings up to the 256 KiB Cloud stores and the plugin SDK
accepts, and advertise the adapter capability only on the two Linux
targets Cloud admits, so another target never gets its whole
registration refused.
cloud#106 was rebased onto Cloud main without changing its proto; the
pin follows the new head so the commit stays fetchable.
@ObsisMc
ObsisMc force-pushed the feat/354-node-model-adapter-grants branch from fdcb679 to c91935c Compare October 21, 2026 09:49
@ObsisMc

ObsisMc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All three blockers and the should-fix items are addressed on c91935c (rebased onto main 42f224a):

  1. Start-time package/declaration/Settings check (D2 step 1) — d3b7e93. Ported feat(node): verify frozen model bindings and plugin startup proof #705's receipt approach: the installer records the verified archive digest and target outside the package and clears it before republishing. Under the package lease and before ModelAccess::open, the session checks the receipt against the frozen digest/target, compiles the installed assets/config.json and compares fingerprint, schema and adapter contract, validates the Settings against it (validate_agent_settings, also from feat(node): verify frozen model bindings and plugin startup proof #705), and requires the frozen protocol in the declaration. Failures end with agent_model_configuration_invalid / agent_model_protocol_mismatch. Tests cover a replaced package, another fingerprint, bad Settings, another protocol and the session ending with no grant request and no process.
  2. Empty inherited environment (D4) — ffb18ce (your ProcessEnvironment::Empty commit with its real-process test) and e180ba8. A configured session's plugin and every host-spawned child clear inheritance and get only PATH, LANG, LC_ALL, TMPDIR plus the session's own variables. The echo-fixture test now also asserts the test runner's CARGO* variables do not reach the plugin.
  3. Legacy route fence — e235258. Only official/ora-space.opencode takes the legacy route without a frozen configuration; other plugins end agent_model_configuration_required before a grant or process.

Should-fix:

  • Start codes: ae84838. -32001/-32002 map to AgentNotInstalled/AgentUnusable again; the plugin message is still dropped.
  • Targets: f54d0ed. The capability is advertised only on x86_64-unknown-linux-gnu and aarch64-unknown-linux-gnu, matching cloud#106.
  • Settings bound: f54d0ed. Raised to 256 KiB, which is what Cloud stores (personal_agent_settings.go) and what the SDK accepts; declarations allow up to 128 settings × 64 KiB strings, so 64 KiB on the Node could refuse a configuration Cloud saved.
  • third_party/cloud: c91935c pins cloud#106's rebased head 132cc6a (proto unchanged); it will be re-pinned to Cloud main once cloud#106 merges.

Locally: cargo test for ora-process, ora-plugin-config, ora-plugin-manager, ora-node-protocol, ora-agent-runtime, ora-controller and ora-node (incl. standalone 52/52), the CI clippy command, cargo fmt --check and check-rust-size all pass. Please take another look at c91935c.

@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.

审查版本:c91935c47ebce6d332d5acea2d02753062673e59(base 42f224a,15 commits,69 files,+2219/−319)。结论:Approve(无阻塞)。

三个上一轮阻塞在代码和可验证测试上都已真正落地,四个 should-fix 也全部落实。

三个阻塞

  1. 启动时核对安装包与声明(D2 第 1 步)——已修复。 租约先于一切(apps/ora-node/src/session/driver.rs:122),check_configuration 在 restore、placement 和任何 grant 之前(driver.rs:134-138),失败在首次模型调用前以 agent_model_configuration_invalid / agent_model_protocol_mismatch 结束(apps/ora-node/src/session/ports.rs:135-150)。检查内容(apps/ora-node/src/plugins/model_configuration.rs:104-151):target 等于本机 target、receipt 为精确 {sha256,target}、声明指纹/schema/adapter contract 匹配、Settings 按已装声明校验、冻结 protocol ∈ 声明 protocols。receipt 在包内容之外、先清后写(apps/ora-node/src/plugins/catalog.rs:100-130),声明包缺 receipt 时强制重装(:87-91)。单测覆盖缺 receipt/错 digest/错指纹/错误 Settings/错协议/退役 receipt;会话级测试断言无 grant、无进程(apps/ora-node/tests/agent_session.rs:777-803);CI Ubuntu 日志已确认这些用例实际运行并通过。
  2. 配置会话从空继承环境启动(D4)——已修复。 ProcessEnvironment::Empty + 同一 spawner 覆盖插件与宿主拉起的子进程(apps/ora-node/src/session/process.rs、session/workload.rs:99-127、crates/plugin-lifecycle/src/runtime.rs:150-165),只显式补 PATH/LANG/LC_ALL/TMPDIR 和会话变量;真实 echo 插件进程断言 CARGO* 不传入、token 不在 env/history/Thread(agent_session.rs:689-741);重启复用同一 launcher。
  3. 旧路线只给 official/ora-space.opencode——已修复。 driver.rs:35-37,188-194;全仓 ModelAccess::open 只有一个调用点;测试断言 agent_model_configuration_required、无 grant、无进程(agent_session.rs:805-848)。

should-fix:AGENT_NOT_INSTALLED/AGENT_UNUSABLE 区分、target 收窄至两个 Linux gnu、Settings 256 KiB、third_party/cloud 钉 132cc6a 均已落实;生成的 controller-proto 与 cloud proto 的字段 1–9/8/10 一致。

非阻塞事项(建议合并前或合并后承接)

  • P1 跨 PR:新错误码在 Cloud 侧被折叠。 cloud#106 的 internal/core/agent_run_failure.go:15-23 allowlist 没有 agent_model_configuration_invalid、agent_model_protocol_mismatch、agent_model_configuration_required、model_grant_mismatch、agent_model_adapter_failed,公开 failureCode 会全部回落 agent_failed,ADR D2 的分类要求在 Cloud 产品面不成立(Node wire 本身是对的)。已同步请 cloud#106 本轮补 allowlist + 测试;若不做,本 PR 描述里应说明 Cloud 侧仍是通用码。
  • Node 文档与实现不一致。 docs/node/agent-sessions.md:49-53 仍写「Node 环境中的模型凭据和代理设置仍会到达 Agent」,对配置会话已相反;docs/node/protocol.md:79-83 的 AgentSessionSpec 字段清单未提新增的可选 agent_configuration,HelloAccepted 的 model_adapter 也未提。建议同 PR 或紧随的 follow-up 补两行。
  • Shared placement 与 D4 前提。 model_adapter()(apps/ora-node/src/session.rs:181-195)只按 model_proxy + target 宣告能力,不要求 Separate;Shared 下配置会话仍有 model_proxy 生成的私有 HOME/XDG,默认解析是隔离的,但 agent 与 Node 同 OS 身份,仍可按绝对路径读 Node 的 ~/.claude 等,不满足 D4「不得读取宿主登录文件」。现行保护在别处(apps/ora-node/src/main.rs:42-46 要求 mTLS 必须有 workload_uid,service/agents.rs:193-200 要求 workload_directory),因此不是现行 live violation;建议在 model_adapter() 增加 Separate 前提或把该部署不变量写进文档,并补一个 Separate 配置会话测试(现在 CI 的配置会话测试是 Shared)。
  • receipt 威胁模型与升级影响。 receipt 证明「安装时验证过该归档」,不证明「当前字节仍是该归档」;在 Separate(Node home 0700 属 root、包 view 硬链接只读)下足够,Shared 下可被伪造(与上一条同源)。remove() 不删 receipt(catalog.rs:133-151),旧 receipt 只有在「回滚后同版本重装不同字节且声明不变」的组合下才有低概率 fail-open;Node 升级后旧包无 receipt,配置会话会 fail-closed 直到下一次安装计划重装。建议 remove 时清 receipt,并在 PR 描述/文档写明升级与重装语义。
  • 测试缺口:没有「真实安装器写 receipt → 替换/重发布包 → 启动被拒」的端到端用例;agent_model_protocol_mismatch 与 target mismatch 未经过 driver;256 KiB 边界、Node 侧 model_adapter() 宣告值、Controller retransmit mismatch、重启重放 model_context 均无直接测试;crates/process/tests/environment.rs 仅 Unix。
  • PR 描述措辞:"Door: two-way / older Controller ignores model_adapter" 只说对一半:老 Node 的 AgentSessionSpec 也 deny_unknown_fields,收到带 agent_configuration 的 start 会拒绝而不是忽略;安全性靠新 Controller 的能力门和 Cloud target 保持空。建议改准。
  • 小项:apps/ora-node/src/model_proxy/test_gateway.rs:31-37 的 anthropic 开关在现有测试中无人设置;三层 target/contract 常量目前一致但靠人工同步,可考虑留 issue。

已核对

  • CI 8/8 在 c91935c4 上通过(Actions head_sha 核对);本地 cargo fmt --all --check、5 个相关 crate 的 cargo test、ora-node --lib、check-rust-size 通过;Linux 集成用例以同 head 的 Ubuntu CI 日志为证据(本地 Windows 未复跑)。
  • 69 个文件全是交付物,无过程记录;15 个 commit 拆分基本单一目的。
  • PR 描述 ADR 状态准确:specs 0a100a6 两份 ADR 仍 approved,本 PR 实现 Node/Controller 半边 D1–D4/D6,adapter、发布与真实验收仍缺。合并顺序 cloud#106 → 重钉 third_party/cloud → 本 PR → 两个 adapter,当前 pin 是 cloud#106 的 PR head,cloud 合并后需按当时 main 重钉(描述已写明)。

以上非阻塞事项不影响 Approve;owner 合并前请记得重钉 submodule,并与 cloud#106 的 allowlist 一起收尾。

…ields

agent-sessions still said the Node's model credentials and proxy settings reach every agent; a session with a frozen Agent configuration now starts from an empty environment. protocol now names agent_configuration on a session start and model_adapter in hello_accepted, and that an older Node rejects the field rather than ignoring it.

@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.

审查版本:d7b5b6b8bf3941bee794ca2ad0b25f63d875367f(增量 c91935c4..d7b5b6b8:单 commit、4 个文档文件、+27/−7;上一轮已在 c91935c4 Approve)。结论:Approve(无阻塞)。

本轮只改 docs/node/agent-sessions{,.zh}.md 与 docs/node/protocol{,.zh}.md,中英同改,无过程记录、无其它文件。逐句核对与代码一致:

  • 配置会话空环境:apps/ora-node/src/session/driver.rs:266-268(for_session(spec.agent_configuration.is_some()))→ apps/ora-node/src/session/process.rs:12,23-52(PROCESS_NECESSITIES + ProcessEnvironment::Empty)→ apps/ora-node/src/session/workload.rs:98-131(Shared/Separate 都用同一 isolation spawner)→ 插件进程与其宿主子进程共用该 spawner(crates/plugin-lifecycle 接线)→ crates/process/src/tokio_process.rs:93-95 的 env_clear()。文档只对带冻结配置的会话这么写、并明确其他会话仍是继承环境,范围正确。
  • protocol 新字段:crates/node-protocol/src/domain/model_adapter.rs:57-74(逐字段对上)、agent_session.rs:166-173(agent_configuration 必须与 model_binding_id 同时出现)、message/session.rs:44-54(hello_accepted.model_adapter 只随 model_proxy,Option+default);contract 1、Settings 单 JSON 对象上限 256 KiB。
  • 旧 Node 拒绝:upstream/main 的 AgentSessionSpec 同样 #[serde(deny_unknown_fields)](crates/node-protocol/src/domain/agent_session.rs:113),且 upstream/main 没有 agent_configuration。文档说「会拒绝未知字段而不是忽略」成立。
  • 旧 Controller 忽略:旧 HelloAccepted 没有 deny_unknown_fields,新字段是 Option+default,旧 Controller 会忽略 model_adapter。成立。
  • PR 描述更正:安全边界(Controller 的能力门 + Cloud plugins.personal_agent_target 默认空)、合并顺序、两份 ADR 仍 approved、Refs ora-space/circus#354 均属实。

合并时条件(由 owner 在合并步骤执行)

third_party/cloud 现钉 132cc6a88c518c267774a37cdb2d4570dfd0968e,它是 cloud#106 当前 head f454afe 的祖先、不在 cloud main 上;两处的 proto/ 子树与 cloud main 中将要合并的契约一致,所以 CI 契约仍匹配。cloud#106 合并后,需要把 third_party/cloud 重钉到包含它的 cloud main 提交、push 新 head 并重跑 crates (ubuntu-latest)/crates (macos-15)(含 proto:init/proto:check)后再合本 PR。

非阻塞

  1. docs/node/agent-sessions.md:52-53 / agent-sessions.zh.md:43 写「只保留 PATH、LANG、LC_ALL 和 TMPDIR 的空环境」,漏了「会话自带变量」(HOME/XDG/CA/Git 身份等);建议与 docs/plugins/agent-configuration.md 的口径对齐(PR 描述里的写法是对的)。
  2. docs/node/protocol.md:89 / protocol.zh.md:74 的「旧 Node 拒绝未知字段」可补半句:实际是协议违规、整条 Node↔Controller 连接关闭,「never sent one」是承重前提。
  3. PR 描述「pins ora-space/cloud#106 head 132cc6a」已过期(现 head 是 f454afe,132cc6a 是祖先);Merge Danger 的 Door: two-way 标签建议改成 one-way 或删掉。
  4. 可选:文档未写 configuration_revision 允许 0、adapter contract 当前必须为 1(PR 描述与测试已覆盖)。

验证与未覆盖

  • CI:本 head 8/8 pass(run 39599234425:changes、ci-ok、crates ubuntu/macos/windows、e2e、frontend、tauri)。
  • 本轮是文档改动,未本地跑 cargo;文档陈述以上面的代码阅读逐句核对,CI 只证明构建与测试未因该 commit 回退。
  • ADR:specs main 3c41a34 两份仍为 approved;本 PR 未改 ADR(implemented 由后续落地运行承接)。

…adapter contract

third_party/cloud moves from ora-space/cloud#106's branch head 132cc6a to Cloud main 4d35aad, which merged it. proto/ is identical between the two, and proto:check regenerates the committed client without a diff.
…l rejection

A configured session keeps its own variables beside PATH, LANG, LC_ALL and TMPDIR, and an older Node that cannot decode agent_configuration closes the whole control session as a protocol violation rather than refusing only that start.

@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.

审查版本:5fdfef5fb23d5bef871a7282fffdaa9aa4f77a2f(增量 d7b5b6b8..5fdfef5f:2 个 commit、5 个文件、+11/−6,即 third_party/cloud 钉子与中英 4 个文档文件;上一轮已在 d7b5b6b8 Approve)。结论:Approve(无阻塞)。

本轮增量

  1. 72afd052 重钉 third_party/cloud。 从 cloud#106 的 PR head 132cc6a 改为 cloud main 4d35aad。核对:

    • 4d35aad 是当前 cloud main 的 tip(合并 cloud#103 的 merge commit),第一父提交正是 cloud#106 的 merge commit e2b5a38,所以钉子既落在 main 上、又包含本 PR 依赖的契约;按 SHA 取子模块不会取不到。
    • 两个钉点之间 proto/ 完全一致:git diff --stat 132cc6a 4d35aad -- proto 为空,已提交的 crates/controller-proto/src/gen 因此不需要变化(增量里确实没有变化),生成的 AgentModelConfiguration 字段 9 与 PluginAgentConfiguration 字段 5 都在。
    • 该 head 的 CI 8/8 全绿(run 39613584763,head_sha 已核对为 5fdfef5f)。其中 crates (ubuntu-latest) 与 crates (macos-15) 跑 task test:crates → lint:crates → proto:check(Taskfile.yml:84-92:check-cloud-proto.sh 校验子模块处于钉住的提交且未被改动、重新生成客户端后 git diff --exit-code),所以钉子与已提交客户端的一致性由门禁证明,不只是描述里的说法。
  2. 5fdfef5f 补两条文档措辞(只动 docs/node/agent-sessions{,.zh}.md、docs/node/protocol{,.zh}.md,正是上一轮的两条非阻塞建议)。逐句核对实现:

    • 「会话自带的其他变量」:apps/ora-node/src/session/host.rs:59-63,73-131 的 SessionEnvironment 把自己的变量(Git 身份、HOME/XDG/DENO_DIR,以及插件连接的 runtime 变量)同时交给插件进程和宿主为它拉起的每个进程;apps/ora-node/src/session/process.rs:12,38-52 只从 Node 环境补 PATH/LANG/LC_ALL/TMPDIR(会话已设置的值优先),配合 ProcessEnvironment::Empty 的 env_clear()。文档现在与实现一致,范围仍限定在带冻结配置的会话。
    • 「旧 Node 解码失败是协议违规、关闭整个控制会话」:crates/node-protocol/src/domain/agent_session.rs:91-93 的 AgentSessionSpec 带 deny_unknown_fields(upstream/main 的同名结构同样如此,且 main 里没有 agent_configuration);apps/ora-node/src/service/session.rs:220-229 把解码失败映射为 CloseReason::ProtocolViolation,crates/node-transport/src/lib.rs:31-52 给出 1002 与「IPC 没有关闭码、只结束流」;apps/ora-node/src/service/session.rs:155-157 在会话结束后把 admission 置 false,apps/ora-node/src/service/worker.rs:143-170 据此丢弃尚未接纳的排队工作。control-session.md#closing / control-session.zh.md#关闭 两个锚点都存在。
  3. PR 描述同步更正:钉点改为 4d35aad,Door: one-way 并写明回滚前要先清空 Cloud target,与 deny_unknown_fields 的后果一致。

整体状态(沿用前两轮结论,本轮无代码变化)

  • head 5fdfef5f 为 MERGEABLE/CLEAN;18 个 commit、73 个文件全部是交付物(代码、测试、docs/ 正式说明、Cargo.lock 与子模块钉子),没有过程记录。
  • 两份 ADR 仍是 approved(specs main 83e9d01 核对)。描述写明本 PR 实现 Node/Controller 半边 D1–D4/D6,缺 adapter、发布声明与真实验收、由本运行承接,符合「部分实现」的写法。
  • 上一轮列出的非阻塞项(Shared placement 与 D4 前提、receipt 威胁模型与 remove() 语义、若干测试缺口、test_gateway 的 anthropic 开关)本轮没有新增,仍按 follow-up 处理。

验证范围

本轮是钉子 + 文档改动,未在本地重跑 cargo 与 proto:check(Windows 环境);契约一致性与 8 项门禁以 5fdfef5f 的 CI 为准,文档陈述按上面的代码逐句核对。真实 bundled CLI/gateway、双用户与浏览器验收仍未做,按交接留给本运行后续步骤。

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

3 participants