feat(turn): resolve the default host from the operator credential - #4454
huangruiteng wants to merge 3 commits into
Conversation
The shipped default was the managed dsh host regardless of the credential, so a lane without one failed closed on operator_credential_unconfigured. Resolve the default from the operator's own credential facts instead: a configured credential keeps the managed dsh default, and no credential resolves the individual codex-cli host that can actually run here. An explicit --host or LOOPX_TURN_HOST still wins over either default, so a credential never re-points a host the operator already selected; it only resolves the default that would otherwise have to be chosen without any evidence. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Rewrite the two host-binding test modules so they encode the resolved default instead of the previous fixed one, and update both public smokes to prove it end to end: without a credential the default plan resolves to codex-cli and claims no managed credential, and an explicitly selected dsh host still fails closed on the typed operator_credential_unconfigured reason. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Disclose the default behavior change in the Turn protocol reference, the connector guide, and the DSH/Pi harness RFC (English and Chinese): the default host is now resolved from the operator credential, an explicit selection still wins, a lane without a credential keeps running on the individual CLI host, and an explicitly selected managed host still fails closed with the typed reason when nothing can authenticate it. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
There was a problem hiding this comment.
更正(2026-09-16):本 review 的 REQUEST_CHANGES 结论已被取代,而且推翻它的形态已经上线。
下面三条 finding 都在 rebase 之后的等价改动里逐条修掉了:语义清单重新生成(
named_string_constants2031 → 2032)、协议文档 Host Selection 段补上「要固定 host 请显式设LOOPX_TURN_HOST」的说明、MANAGED_DEFAULT_TURN_HOST/INDIVIDUAL_DEFAULT_TURN_HOST两个别名删除。修好后的同一改动以 #4479(squash001e0c380)合入main,所以本 PR 不会再有新的 head,GitHub 也不允许重开。这份 review 从此只作为「当时受审 head
2aee6ac93的判断」留存:其中的 P1 是 rebase 造成的计数器少算,P3 是命名与文档建议,都不是对行为设计的否决。当前契约请看 #4479 与main上的selected_turn_host()。Correction (2026-09-16): this REQUEST_CHANGES verdict is superseded. All three findings were fixed in the rebased equivalent and landed as #4479 (squash
001e0c380); the review below records the state of the head reviewed at the time.
动机
有界 Turn 的默认 host 一直固定指向托管 dsh:这在一台没有 DEEPSEEK_API_KEY 的机器上意味着默认的 loopx turn plan / run-once 会以 operator_credential_unconfigured 失败关闭,operator 必须先想清楚再加 --host codex-cli 才能跑起来。把默认值改成"由 operator 凭据解析"能直接消掉这个摩擦:有凭据就跑托管 host,没有就跑这台机器本来就能用的个体 CLI。方向我认可,尤其是它保留了显式选择永远优先、并新增了可判别的来源读回。
改动思路
host_binding.selected_turn_host()增加一条凭据分支:显式LOOPX_TURN_HOST→explicit_config;有可用凭据 →dsh/operator_credential;否则 →codex-cli/no_operator_credential(原来的product_default取值被这两个新取值取代)。operator_credential.py只更新模块契约文字(凭据可以解析未显式配置的 shipped default,但绝不重指显式选择),没有让它获得选择权。- 默认执行模式继续跟随 host 派生;协议文档、connector 文档与 RFC(en+zh)同步改写,并在协议文档里显式声明这是一次默认行为变更。
- 两个公开冒烟与两个绑定测试模块改断言:无凭据默认流改成"真的跑起来",显式
--host dsh无凭据仍然失败关闭。
具体改动
loopx/control_plane/turn_driver/host_binding.py(+55/-14)::91的凭据分支、:51新增TURN_HOST_SOURCE_OPERATOR_CREDENTIAL/TURN_HOST_SOURCE_NO_OPERATOR_CREDENTIAL、:48新增INDIVIDUAL_DEFAULT_TURN_HOST。loopx/control_plane/operator_credential.py:模块 docstring 契约改写;loopx/cli_commands/turn_registration.py:注释 1 行;loopx/semantics/inventory_v0.json:计数器 1 行。tests/test_turn_default_host_binding.py(:22/:42/:61三个用例)、tests/test_turn_managed_executor_binding.py、examples/loopx-turn-managed-default-flow-smoke.py(:576断言组)、examples/loopx-turn-managed-executor-binding-smoke.py(第 5 步显式传host=dsh)。- 文档:
docs/reference/protocols/loopx-turn-v0.md、docs/integrations/deepseek-harness-connector.md、docs/architecture/rfcs/harness-selection-dsh-pi-v0.md(en + zh-CN)。
关键内容讲解
- 默认值解析与读回是同一次改动:
resolve_default_turn_host()仍只返回 host,但selected_turn_host()现在返回(host, source),于是读回能区分"这是显式选的"还是"这是凭据解析出来的"。我读了消费面:turn_registration.py:57/:169是唯一两处调用点,managed_executor读回 schema 未变,仓库内没有其它 Turn 侧消费者依赖消失的product_default。 - 失败关闭没有被削弱:显式
dsh且无凭据时仍然是available=false+ typedoperator_credential_unconfigured,不调用 host、不写 Journal、不花 quota slot——冒烟第 5 步(现显式传host=dsh)本地跑过,quota_slot_spend_count保持不变。 - 本地验证:
tests/test_turn_default_host_binding.py+tests/test_turn_managed_executor_binding.py→ 37 passed;examples/loopx-turn-managed-executor-binding-smoke.py→ passed;examples/loopx-turn-managed-default-flow-smoke.py→ passed(含无凭据默认真的跑通一次 bounded Turn)。
对主干的风险
P1(本 head 的必需检查是红的,阻塞合并)。CI 在 head 2aee6ac93 上 test-shard (2) 与 test-shard (3) 失败,两者都是同一句:loopx/semantics/inventory_v0.json is stale。我把 CI 实际测试的合并提交取下来复现了:
git fetch origin refs/pull/4454/merge:refs/remotes/pr/4454-merge
git worktree add --detach /private/tmp/loopx-pr4454-merge refs/remotes/pr/4454-merge
cd /private/tmp/loopx-pr4454-merge && python scripts/generate_semantic_inventory.py --check
# → stale semantic inventory: loopx/semantics/inventory_v0.json运行生成器会显示 named_string_constants 需要从 2031 改成 2032。这不是继承失败:main(4f33d5ac6)单独一棵树是 up to date,本 PR 单独一棵树(相对它自己的 base 53fed471e)也是 up to date——问题是本 PR 与另一处同样把该计数从 2030 改到 2031 的改动落在同一行,git 干净合并之后净增被少算了一次。最小修法:在最新 origin/main 上 rebase,跑 python scripts/generate_semantic_inventory.py 并提交结果(计数器应为 2032),再重跑两个冒烟与语义检查确认全绿。
P3(凭据存在与否现在是默认去向的隐式信号)。:91 的判定只看"凭据是否非空",所以一个为了别的用途被导出到环境里的 DEEPSEEK_API_KEY 会把默认 Turn 从 codex-cli 改到托管 dsh,连带改变计费端点与执行模式。文档已经披露这次默认变更,并给了 LOOPX_TURN_HOST 作为固定 host 的做法,所以这是可接受的取舍;但如果希望减少现场误判,建议在协议文档的 Host Selection 段落补一句"固定 host 的机器请显式设置 LOOPX_TURN_HOST"。
P3(常量命名现在名不副实)。MANAGED_DEFAULT_TURN_HOST(:47)与 INDIVIDUAL_DEFAULT_TURN_HOST(:48)是基础常量的别名,但"default"只在各自对应的凭据条件下成立;后续代码若直接引用它们,就会重新引入"默认固定是 dsh"的假设。最小修法:删掉这两个别名,直接用 MANAGED_TURN_HOST / INDIVIDUAL_TURN_HOST,把"默认"只留给 selected_turn_host 的来源语义。
其余残余风险:本次行为变化本身是"无凭据机器从失败关闭变成能跑个体 CLI",属于可用性改善且已披露;未验证面是真实 DeepSeek 端点(冒烟用 hermetic mock,只证明读回与失败关闭),以及 Windows/macOS 安装包下的同一解析。CI 侧我只读到 test-shard (2)/(3) 的失败日志,其它 job 当时仍在运行。
我的整体评价
REQUEST_CHANGES。改动的方向、读回设计与文档披露我都认可:默认值解析集中在既有 owner 里,显式选择优先级没有被削弱,托管 host 无凭据仍然失败关闭,两个冒烟把正反两条路径都跑成了真实执行。但受审 head 的必需检查是红的,而且是可复现地红在 CI 实际运行的合并树上——语义清单的 named_string_constants 停在 2031,合并后需要 2032。这是一个 rebase + 重生成计数器的修复,不涉及行为设计;修好并让 test-shard (2)/(3) 转绿之后,这条 PR 就可以按现在的形态合入。另外两条 P3(凭据隐式改默认的说明、两个别名的命名)属于顺手可改,不阻塞。
[Superseded 2026-09-16 — see the correction at the top; the finding is fixed on the delivery path] English verdict: REQUEST_CHANGES at 2aee6ac. The change itself is sound: selected_turn_host now resolves the shipped default from the operator credential (credential → dsh/operator_credential, none → codex-cli/no_operator_credential) while an explicit --host or LOOPX_TURN_HOST still wins, and the old fail-closed path is preserved for an explicitly selected managed host with no credential (no host invocation, no Journal, no quota spend). I reproduced the local positives (37 passed across the two binding test modules; both public smokes pass, including a real hermetic bounded Turn on the uncredentialed default). The blocking problem is CI: test-shard (2) and (3) fail at this head with loopx/semantics/inventory_v0.json is stale, and I reproduced it on the exact merge commit GitHub tests (refs/pull/4454/merge, head merged into main 4f33d5a): scripts/generate_semantic_inventory.py --check reports stale and regeneration wants named_string_constants 2031 → 2032. Neither main alone nor this head against its own base is stale, so the undercount comes from the PR and another 2030→2031 edit landing on the same counter line. Rebase onto current main, regenerate the inventory, and re-run the two smokes plus the semantic checks. Two non-blocking notes: credential presence now implicitly re-points the default host (billing/execution mode follow), which the docs disclose but could call out with the LOOPX_TURN_HOST advice, and MANAGED_DEFAULT_TURN_HOST/INDIVIDUAL_DEFAULT_TURN_HOST are aliases whose "default" reading is only conditionally true.
There was a problem hiding this comment.
更正(2026-09-16):本 review 的 REQUEST_CHANGES 结论已被取代,而且推翻它的形态已经上线。
下面三条 finding 都在 rebase 之后的等价改动里逐条修掉了:语义清单重新生成(
named_string_constants2031 → 2032)、协议文档 Host Selection 段补上「要固定 host 请显式设LOOPX_TURN_HOST」的说明、MANAGED_DEFAULT_TURN_HOST/INDIVIDUAL_DEFAULT_TURN_HOST两个别名删除。修好后的同一改动以 #4479(squash001e0c380)合入main,所以本 PR 不会再有新的 head,GitHub 也不允许重开。这份 review 从此只作为「当时受审 head
2aee6ac93的判断」留存:其中的 P1 是 rebase 造成的计数器少算,P3 是命名与文档建议,都不是对行为设计的否决。当前契约请看 #4479 与main上的selected_turn_host()。Correction (2026-09-16): this REQUEST_CHANGES verdict is superseded. All three findings were fixed in the rebased equivalent and landed as #4479 (squash
001e0c380); the review below records the state of the head reviewed at the time.
Request changes conclusion Superseded 2026-09-16 (author-owned PR; GitHub blocks formal self-review)
动机
有界 Turn 的默认 host 一直固定指向托管 dsh:这在一台没有 DEEPSEEK_API_KEY 的机器上意味着默认的 loopx turn plan / run-once 会以 operator_credential_unconfigured 失败关闭,operator 必须先想清楚再加 --host codex-cli 才能跑起来。把默认值改成"由 operator 凭据解析"能直接消掉这个摩擦:有凭据就跑托管 host,没有就跑这台机器本来就能用的个体 CLI。方向我认可,尤其是它保留了显式选择永远优先、并新增了可判别的来源读回。
改动思路
host_binding.selected_turn_host()增加一条凭据分支:显式LOOPX_TURN_HOST→explicit_config;有可用凭据 →dsh/operator_credential;否则 →codex-cli/no_operator_credential(原来的product_default取值被这两个新取值取代)。operator_credential.py只更新模块契约文字(凭据可以解析未显式配置的 shipped default,但绝不重指显式选择),没有让它获得选择权。- 默认执行模式继续跟随 host 派生;协议文档、connector 文档与 RFC(en+zh)同步改写,并在协议文档里显式声明这是一次默认行为变更。
- 两个公开冒烟与两个绑定测试模块改断言:无凭据默认流改成"真的跑起来",显式
--host dsh无凭据仍然失败关闭。
具体改动
loopx/control_plane/turn_driver/host_binding.py(+55/-14)::91的凭据分支、:51新增TURN_HOST_SOURCE_OPERATOR_CREDENTIAL/TURN_HOST_SOURCE_NO_OPERATOR_CREDENTIAL、:48新增INDIVIDUAL_DEFAULT_TURN_HOST。loopx/control_plane/operator_credential.py:模块 docstring 契约改写;loopx/cli_commands/turn_registration.py:注释 1 行;loopx/semantics/inventory_v0.json:计数器 1 行。tests/test_turn_default_host_binding.py(:22/:42/:61三个用例)、tests/test_turn_managed_executor_binding.py、examples/loopx-turn-managed-default-flow-smoke.py(:576断言组)、examples/loopx-turn-managed-executor-binding-smoke.py(第 5 步显式传host=dsh)。- 文档:
docs/reference/protocols/loopx-turn-v0.md、docs/integrations/deepseek-harness-connector.md、docs/architecture/rfcs/harness-selection-dsh-pi-v0.md(en + zh-CN)。
关键内容讲解
- 默认值解析与读回是同一次改动:
resolve_default_turn_host()仍只返回 host,但selected_turn_host()现在返回(host, source),于是读回能区分"这是显式选的"还是"这是凭据解析出来的"。我读了消费面:turn_registration.py:57/:169是唯一两处调用点,managed_executor读回 schema 未变,仓库内没有其它 Turn 侧消费者依赖消失的product_default。 - 失败关闭没有被削弱:显式
dsh且无凭据时仍然是available=false+ typedoperator_credential_unconfigured,不调用 host、不写 Journal、不花 quota slot——冒烟第 5 步(现显式传host=dsh)本地跑过,quota_slot_spend_count保持不变。 - 本地验证:
tests/test_turn_default_host_binding.py+tests/test_turn_managed_executor_binding.py→ 37 passed;examples/loopx-turn-managed-executor-binding-smoke.py→ passed;examples/loopx-turn-managed-default-flow-smoke.py→ passed(含无凭据默认真的跑通一次 bounded Turn)。
对主干的风险
P1(本 head 的必需检查是红的,阻塞合并)。CI 在 head 2aee6ac93 上 test-shard (2) 与 test-shard (3) 失败,两者都是同一句:loopx/semantics/inventory_v0.json is stale。我把 CI 实际测试的合并提交取下来复现了:
git fetch origin refs/pull/4454/merge:refs/remotes/pr/4454-merge
git worktree add --detach /private/tmp/loopx-pr4454-merge refs/remotes/pr/4454-merge
cd /private/tmp/loopx-pr4454-merge && python scripts/generate_semantic_inventory.py --check
# → stale semantic inventory: loopx/semantics/inventory_v0.json运行生成器会显示 named_string_constants 需要从 2031 改成 2032。这不是继承失败:main(4f33d5ac6)单独一棵树是 up to date,本 PR 单独一棵树(相对它自己的 base 53fed471e)也是 up to date——问题是本 PR 与另一处同样把该计数从 2030 改到 2031 的改动落在同一行,git 干净合并之后净增被少算了一次。最小修法:在最新 origin/main 上 rebase,跑 python scripts/generate_semantic_inventory.py 并提交结果(计数器应为 2032),再重跑两个冒烟与语义检查确认全绿。
P3(凭据存在与否现在是默认去向的隐式信号)。:91 的判定只看"凭据是否非空",所以一个为了别的用途被导出到环境里的 DEEPSEEK_API_KEY 会把默认 Turn 从 codex-cli 改到托管 dsh,连带改变计费端点与执行模式。文档已经披露这次默认变更,并给了 LOOPX_TURN_HOST 作为固定 host 的做法,所以这是可接受的取舍;但如果希望减少现场误判,建议在协议文档的 Host Selection 段落补一句"固定 host 的机器请显式设置 LOOPX_TURN_HOST"。
P3(常量命名现在名不副实)。MANAGED_DEFAULT_TURN_HOST(:47)与 INDIVIDUAL_DEFAULT_TURN_HOST(:48)是基础常量的别名,但"default"只在各自对应的凭据条件下成立;后续代码若直接引用它们,就会重新引入"默认固定是 dsh"的假设。最小修法:删掉这两个别名,直接用 MANAGED_TURN_HOST / INDIVIDUAL_TURN_HOST,把"默认"只留给 selected_turn_host 的来源语义。
其余残余风险:本次行为变化本身是"无凭据机器从失败关闭变成能跑个体 CLI",属于可用性改善且已披露;未验证面是真实 DeepSeek 端点(冒烟用 hermetic mock,只证明读回与失败关闭),以及 Windows/macOS 安装包下的同一解析。CI 侧我只读到 test-shard (2)/(3) 的失败日志,其它 job 当时仍在运行。
我的整体评价
REQUEST_CHANGES。改动的方向、读回设计与文档披露我都认可:默认值解析集中在既有 owner 里,显式选择优先级没有被削弱,托管 host 无凭据仍然失败关闭,两个冒烟把正反两条路径都跑成了真实执行。但受审 head 的必需检查是红的,而且是可复现地红在 CI 实际运行的合并树上——语义清单的 named_string_constants 停在 2031,合并后需要 2032。这是一个 rebase + 重生成计数器的修复,不涉及行为设计;修好并让 test-shard (2)/(3) 转绿之后,这条 PR 就可以按现在的形态合入。另外两条 P3(凭据隐式改默认的说明、两个别名的命名)属于顺手可改,不阻塞。
[Superseded 2026-09-16 — see the correction at the top; the finding is fixed on the delivery path] English verdict: REQUEST_CHANGES at 2aee6ac. The change itself is sound: selected_turn_host now resolves the shipped default from the operator credential (credential → dsh/operator_credential, none → codex-cli/no_operator_credential) while an explicit --host or LOOPX_TURN_HOST still wins, and the old fail-closed path is preserved for an explicitly selected managed host with no credential (no host invocation, no Journal, no quota spend). I reproduced the local positives (37 passed across the two binding test modules; both public smokes pass, including a real hermetic bounded Turn on the uncredentialed default). The blocking problem is CI: test-shard (2) and (3) fail at this head with loopx/semantics/inventory_v0.json is stale, and I reproduced it on the exact merge commit GitHub tests (refs/pull/4454/merge, head merged into main 4f33d5a): scripts/generate_semantic_inventory.py --check reports stale and regeneration wants named_string_constants 2031 → 2032. Neither main alone nor this head against its own base is stale, so the undercount comes from the PR and another 2030→2031 edit landing on the same counter line. Rebase onto current main, regenerate the inventory, and re-run the two smokes plus the semantic checks. Two non-blocking notes: credential presence now implicitly re-points the default host (billing/execution mode follow), which the docs disclose but could call out with the LOOPX_TURN_HOST advice, and MANAGED_DEFAULT_TURN_HOST/INDIVIDUAL_DEFAULT_TURN_HOST are aliases whose "default" reading is only conditionally true.
Closing as supersededBoth halves of this PR's premise have been settled elsewhere, so keeping it open The steward-channel half is done, and done differently. The "Open decision" The Turn-host half contradicts the contract now on So the reusable part of this work — an explicit, disclosed readback for which If you want a credential-conditional Turn host after all, it is a real product |
Review findings on this branch: dispositionThe three findings in the review above are settled, so they are not left as open P1 — stale P3 — credential presence as an implicit signal for the default Turn host: not P3 — No code change is pending from these findings. If a credential-conditional Turn |
Review findings: repaired on the branch headThe three findings from the review are no longer disposition-only. The branch is P1 — stale P3 — credential presence as an implicit signal: fixed, in both directions. P3 — Also fixed while rebasing (not in the review):
Verification at
|
| check | result |
|---|---|
pytest tests/test_turn_default_host_binding.py tests/test_turn_managed_executor_binding.py |
43 passed |
examples/host-mode-plan-smoke.py |
ok |
examples/project/host-mode-plan-cli-smoke.py |
ok |
examples/loopx-turn-managed-executor-binding-smoke.py |
passed |
examples/loopx-turn-managed-default-flow-smoke.py |
passed |
scripts/generate_semantic_inventory.py --check |
up to date |
ruff check on the changed Python files |
passed |
| DCO trailers | present on all 4 commits |
Diff against main: 15 files, +240/−162, four commits (feat / test / docs
/ refactor).
This PR stays closed
Repairing the branch does not settle the product question, so I have not
reopened it or merged it. main still ships the other contract —
selected_turn_host() is environment-independent and a lane without an operator
credential fails closed with the typed operator_credential_unconfigured
refusal — and this branch reverses that, which the DSH/Pi RFC's own promotion
gate rules out for a default path: an individual lane must be selected, not
reached by default, so a managed lane cannot land on one person's CLI
subscription because a credential was missing.
Note that a closed PR keeps its recorded head frozen, so the repaired commits
appear in the PR only if it is reopened. If the credential-resolved Turn default
is wanted after all, reopening is enough now — the branch is green and mergeable
— but that is a product decision to take explicitly, together with the
promotion-gate wording it changes.
English: the review findings on this branch are repaired, not just dispositioned.
The branch is rebased on ade21106b at 6f5a9f500: stale semantic inventory
regenerated, pin advice and the two-surface statement fixed, the
*_DEFAULT_TURN_HOST aliases removed, the branch's own EN/ZH mirror drift fixed,
and the main-side "a credential never selects" prose the change falsifies
updated. Verified: 43 focused tests, 4 public smokes, inventory check, ruff; DCO
clean. The PR is intentionally left closed: main ships an
environment-independent Turn host and fails closed without a credential, and
reversing that is a product decision against the RFC's promotion gate, not a
rebase. Reopening the PR now shows the green repaired head.
Fifth commit: the file I repaired no longer contradicts itselfClosing out the three findings meant re-reading the file I had just edited, and the
Fixed at Re-verified at The PR is still deliberately closed. Fixing the findings and making the branch English: a fifth docs-only commit ( |
The repair continues in #4479 — GitHub will not reopen this PRThe three review findings on this branch are fixed, but reopening is no longer So the repaired head is delivered as #4479, and this PR stays as the change
English: this PR cannot be reopened after the rebase (GitHub: "was force-pushed |
) The shipped default Turn host is now resolved from the operator credential: `dsh` when `DEEPSEEK_API_KEY` is configured, `codex-cli` otherwise. An explicit `--host` or `LOOPX_TURN_HOST` still wins, and an explicitly selected managed host without a credential still fails closed with the typed `operator_credential_unconfigured` refusal and its remediation codes. This is the repaired delivery of #4454, which GitHub will not reopen after the rebase: the three review findings there (stale semantic inventory, pin advice, `*_DEFAULT_TURN_HOST` aliases) are fixed at this head, the RFC statements the change falsifies are rewritten in place, and the steward channel's already shipped credential rule is now matched by the Turn surface. Verified at the reviewed head: real CLI readback in both credential states plus the explicit refusal path, 262 focused tests, four public smokes, `loopx canary premerge` (17 checks, 0 failures), semantic inventory check, docs governance smoke, ruff, and DCO on all five commits. Merge readiness returned `ready=true` for `4479@b9c0fc4`; the admin bypass covers only the formal-approval requirement, which GitHub blocks for an author-owned pull request.
|
Landed: the repaired change merged as #4479 (squash |
Comment 记录修正:凭据解析的默认值最终还是落地了上面的 closing / disposition 两条 comment 是在 实际落地的形态(
本 head 上那三条 review finding 的现状
这里记录的 review 判决( English: the two disposition comments above were written while 补充(2026-09-16):这次记录修正的范围比上面写的更大——本 PR 的两条 review 正文( English: the two review bodies and the PR body's "Open decision" section carry the same correction, so every public artifact on this record PR now matches |
What changed
The bounded Turn default host is now resolved from the operator's own credential
facts instead of being fixed to the managed host:
DEEPSEEK_API_KEY): the default staysdsh(managed executor), authenticated by that credential;
codex-cli(individual executor), because a managed host nothing can authenticate would
otherwise refuse to run.
An explicit
--hostorLOOPX_TURN_HOSTstill wins over either default, so acredential resolves the shipped default only -- it never re-points a host the
operator already selected. The readback now carries the source
(
explicit_config,operator_credential,no_operator_credential).Changed surfaces
Runtime/API:
loopx/control_plane/turn_driver/host_binding.py(defaultresolution and readback source),
loopx/control_plane/operator_credential.py(module contract),
loopx/cli_commands/turn_registration.py(comment only),loopx/semantics/inventory_v0.json(regenerated).CLI/plan/run-once:
loopx turn planandloopx turn run-oncewithout--host.No frontend change is needed: the resolved host and the
managed_executorreadback are already what the Chat/Dashboard surfaces project, and this change
does not alter that projection.
Validation: both public smokes and both host-binding test modules.
Docs: Turn protocol reference, DeepSeek Harness connector guide, and the
DSH/Pi harness RFC (English + Chinese).
Default behavior change disclosure
Both
planandrun-oncepreviously defaulted todshregardless of thecredential, which made a lane without a credential fail closed on
operator_credential_unconfigured. A lane without an operator credential nowruns the individual CLI host by default.
--host dshremains the explicit managed path and still fails closed with thesame typed reason when nothing can authenticate it.
--host generic-cliremainsthe compatibility path, and a machine that wants one fixed host should set
LOOPX_TURN_HOSTonce instead of relying on the ambient environment.Validation
python -m pytest tests/test_turn_default_host_binding.py tests/test_turn_managed_executor_binding.py tests/test_loopx_turn_executor.py tests/test_host_mode_planner.py tests/test_host_parity_smoke.py-> 134 passedpython -m pytest tests/test_loopx_turn_driver.py tests/test_loopx_turn_managed_step.py tests/test_dsh_goal_mode.py tests/test_default_public_scan_root.py tests/control_plane/test_cli_output_budget.py-> 182 passedpython examples/loopx-turn-managed-executor-binding-smoke.py-> passedpython examples/loopx-turn-managed-default-flow-smoke.py-> passed (hermetic mock model endpoint; no operator key and no individual CLI subscription consumed)python examples/semantic-vocabulary-drift-smoke.py-> ok (inventory regenerated)python examples/docs-governance-smoke.py-> okloopx canary premerge --from-git-diff-> 0 failures across contract, risk-profile, and public-boundary checksOpen decision (closed 2026-09-16 -- resolved the other way; see the record update below)
The steward Chat channel keeps its own default (
codex), becausedshstillhas no interactive Chat transport and pointing the channel at it produces the
typed
managed_host_chat_transport_unsupportedgate. Whether the stewardchannel should also become credential-resolved is a separate owner decision that
depends on giving
dsha chat transport; this PR does not change it.Record update (2026-09-16)
This PR is a change record, not the delivery path. The reviewed change landed
as #4479 (squash
001e0c380), with the three findings raised against the frozenhead
2aee6ac93fixed there: the semantic inventory was regenerated, the Turnprotocol reference now tells an operator who wants one fixed host to set
LOOPX_TURN_HOSTexplicitly, and the*_DEFAULT_TURN_HOSTaliases are gone(
selected_turn_host()returnsMANAGED_TURN_HOST/INDIVIDUAL_TURN_HOSTdirectly). GitHub cannot reopen this branch after the rebase that the fix
needed, so this head stays closed as the record; the two review bodies below
carry a correction banner and their verdicts describe only that frozen head.
The open decision recorded above has since been settled by the owner, the other
way: making the steward Chat channel credential-resolved is no longer pending.
On
main,loopx/chat_manager.py::_resolve_manager_endpoint()resolves itsendpoint under the same rule as the Turn surface -- a configured operator
credential runs the managed
dshexecutor(
operator_credential_configured), its absence keeps the individualcodexendpoint (
operator_credential_absent), and an explicitLOOPX_MANAGER_ENDPOINTstill wins -- with the endpoint source and defaultreason projected in the channel readback (#4470, #4481, #4483).