From 2206618e76c73980dabec2363d8ad8d0a6d45e59 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 00:49:12 +0800 Subject: [PATCH] docs(host-mode): define the declared host and assert the transition policy Three follow-ups from the review of #4456. `turn_mapping.host` (`generic-cli`) sat next to `host_resolution: resolved` and the new `host_selection: resolved_default` while `plan_command` pinned no host, so a consumer that read only `host`/`host_resolution` could still conclude that the concrete host had resolved to the compatibility adapter. The protocol doc now defines the field as the mode's *declared* host and scheduler context and states that the concrete host is resolved when `plan_command` runs; it also documents `host_selection` and `plan_command_rollback`. The RFC recorded the unattended mapping as an open decision between three options. The shipped behavior is one of them, so the paragraph now records the decision (write out the resolved default, label the pinned compatibility variant as the rollback path) instead of contradicting the code; the Chinese mirror is updated with it. The transition policy at `host_mode_planner.py` target commands had no assertion: `examples/host-mode-plan-smoke.py` only checked `preserves_agent_id`, `spends_quota` and the guard command. It now asserts that targets which resolve their host pin no `--host`, and that targets needing a visible identity still pin `--host codex-cli` with `--execution-mode interactive-visible`. This also supersedes the merged PR description, which described the affected transitions as hybrid-handoff although no transition targets that mode. Validation: `examples/host-mode-plan-smoke.py` ok, `examples/project/host-mode-plan-cli-smoke.py` ok, `pytest tests/test_host_mode_planner.py -q` 2 passed, `ruff check` clean, and `loopx canary premerge --from-git-diff --timeout-seconds 300` gate=passed with 11 executed checks and 0 failures. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../rfcs/harness-selection-dsh-pi-v0.md | 25 +++++++++++-------- .../rfcs/harness-selection-dsh-pi-v0.zh-CN.md | 14 +++++++---- docs/reference/protocols/host-mode-plan-v0.md | 20 ++++++++++++++- examples/host-mode-plan-smoke.py | 17 +++++++++++++ 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md index 765a656dcf..91177506fd 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md @@ -118,16 +118,21 @@ Open gaps before this binding is a promoted production default: (`@deepseek-ai/dsh-tool-fs`, `@deepseek-ai/dsh-tool-bash`). Without them a live model can answer but cannot act, and the Turn ends in a validation failure rather than in work. -- the host-mode plan still maps the unattended intent to the compatibility path: - `isolated_headless_turn` carries `turn_host: generic-cli` - (`loopx/host_mode_planner.py`), so the `loopx turn plan` command it prints - names `--host generic-cli` instead of the selected `dsh` default recorded above, - which is correct as a labelled rollback path but is not labelled as one. The - plan's `--host-identity` list deliberately covers visible - hosts only, because a headless-only host such as `dsh` cannot own a visible - session; the unattended mapping itself still needs a decision between naming - the resolved default, offering a `dsh` variant, or labelling the emitted - command as the rollback path. +- the host-mode plan used to map the unattended intent to the compatibility + path: `isolated_headless_turn` carried `turn_host: generic-cli` + (`loopx/host_mode_planner.py`), so the `loopx turn plan` command it printed + named `--host generic-cli` instead of the selected `dsh` default recorded + above, which is correct as a labelled rollback path but was not labelled as + one. **Decided and shipped:** the plan takes the "write out the resolved + default" option. The preview command pins no host, the pinned compatibility + variant is reported as `plan_command_rollback`, and the typed + `turn_mapping.host_selection` states which of the two a command is; targets + that genuinely need a visible identity (transitions into `visible_tui`) still + pin their host. `docs/reference/protocols/host-mode-plan-v0.md` defines + `turn_mapping.host` as the mode's declared host and scheduler context rather + than as an already-resolved concrete host. The plan's `--host-identity` list + still covers visible hosts only, because a headless-only host such as `dsh` + cannot own a visible session. ## Evidence Baseline diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md index b1fabf1715..0a6417d837 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md @@ -97,13 +97,17 @@ CLI 的情况下把管家悄悄换成 operator 模型。 - LoopX 的 DSH Turn 组合必须显式列出托管动作所需的工具行 (`@deepseek-ai/dsh-tool-fs`、`@deepseek-ai/dsh-tool-bash`)。缺少它们时,真实模型 只能作答而无法动手,Turn 会以验证失败而不是产出工作结束。 -- 宿主模式计划仍把无人值守意图映射到兼容路径:`isolated_headless_turn` 的 +- 宿主模式计划此前把无人值守意图映射到兼容路径:`isolated_headless_turn` 的 `turn_host` 取 `generic-cli`(`loopx/host_mode_planner.py`),因此它打印的 `loopx turn plan` 命令写的是 `--host generic-cli`,而不是上文记录的已选 `dsh` - 默认值;作为回滚路径本身没错,但没有被标注为回滚路径。该计划的 - `--host-identity` 列表只覆盖可见宿主是有意为之——像 - `dsh` 这种仅 headless 的宿主无法拥有可见会话;但无人值守映射本身仍需在"写出解析 - 后的默认值 / 提供 `dsh` 变体 / 把该命令标注为回滚路径"之间做出决定。 + 默认值;作为回滚路径本身没错,但没有被标注为回滚路径。**已决并已落地:**计划采用 + "写出解析后的默认值"这一支——预览命令不再 pin 任何 host,pin 死的兼容变体报为 + `plan_command_rollback`,类型化的 `turn_mapping.host_selection` 说明该命令属于 + 哪一种;真正需要可见身份的路径(转入 `visible_tui` 的 transition)仍然 pin 具体 + 宿主。`docs/reference/protocols/host-mode-plan-v0.md` 已把 `turn_mapping.host` + 定义为该模式的声明宿主与调度上下文,而不是"具体宿主已经解析完成"。该计划的 + `--host-identity` 列表仍只覆盖可见宿主,因为像 `dsh` 这种仅 headless 的宿主无法 + 拥有可见会话。 ## 证据基线 diff --git a/docs/reference/protocols/host-mode-plan-v0.md b/docs/reference/protocols/host-mode-plan-v0.md index 6651139075..b1cb25e3b7 100644 --- a/docs/reference/protocols/host-mode-plan-v0.md +++ b/docs/reference/protocols/host-mode-plan-v0.md @@ -142,6 +142,23 @@ Each `mode_options[]` entry includes the connector id, readiness, required host capabilities, Turn mapping when one exists, scheduler execution context, quota guard command, and required proofs. +`selected_turn_mapping` fields mean: + +- `host` is the mode's **declared** host: the scheduler context the readiness + statement and capability requirements are written against. It is not a claim + that this concrete host has already been resolved for the run; the runtime + resolves the concrete host (including a credential- or configuration-resolved + default) when `plan_command` runs. +- `host_selection` is `resolved_default` when the command deliberately leaves + host resolution to `loopx turn plan`/`run-once`, and `pinned` when the command + carries an explicit `--host`. +- `plan_command` is the command to run for this mapping. When `host_selection` + is `resolved_default` it pins no host, so it cannot freeze the compatibility + adapter path as the product default. +- `plan_command_rollback` is the pinned compatibility variant for an operator + who deliberately wants that path instead of the resolved default. It is + present only when the mapping resolves its host. + ## Functional Points The selector provides four concrete functions: @@ -150,7 +167,8 @@ The selector provides four concrete functions: users and agents to infer visible/headless/gateway/timer behavior manually. 2. **Turn mapping:** for unattended execution, print the exact `loopx turn plan` preview that preserves host, execution mode, scheduler owner, agent id, and - available capabilities. + available capabilities, while leaving the concrete host to the runtime's + resolved default and reporting the pinned variant as the rollback command. 3. **Readiness surface:** report which advertised capabilities are missing before a mode can be trusted. 4. **Safe handoff plan:** name transitions such as visible bootstrap to diff --git a/examples/host-mode-plan-smoke.py b/examples/host-mode-plan-smoke.py index a31a9276d8..8c3f4b2239 100755 --- a/examples/host-mode-plan-smoke.py +++ b/examples/host-mode-plan-smoke.py @@ -348,6 +348,23 @@ def test_hybrid_requires_two_ready_modes_and_names_handoffs() -> None: assert transition["preserves_agent_id"] is True, transition assert transition["spends_quota"] is False, transition assert "--agent-id codex-main-control" in transition["guard_command"], transition + # The transition policy is the same rule as the mode preview: a target that + # resolves its host must not pin the compatibility adapter path, while a + # target that needs a visible identity still pins it. + by_id = {item["transition"]: item for item in two_ready["transitions"]} + for transition_id in ( + "visible_bootstrap_to_isolated_headless_turn", + "im_gateway_to_isolated_headless_turn", + ): + command = by_id[transition_id]["target_turn_plan_command"] + assert "--host" not in command, (transition_id, command) + for transition_id in ( + "isolated_headless_turn_to_visible_tui_escalation", + "shell_service_to_visible_tui_escalation", + ): + command = by_id[transition_id]["target_turn_plan_command"] + assert "--host codex-cli" in command, (transition_id, command) + assert "--execution-mode interactive-visible" in command, (transition_id, command) def test_identity_gate_and_no_spend_boundary() -> None: