feat(turn): resolve the default host from the operator credential - #4479
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>
An operator-credential-resolved default made the `MANAGED_DEFAULT_TURN_HOST` and `INDIVIDUAL_DEFAULT_TURN_HOST` aliases conditional claims stated as unconditional names: "default" is only true for the credential branch each alias belongs to, so a later caller reading them would re-import the assumption that one host is always the default. The alias names are removed and the selection now returns the host constants directly; "default" stays in `selected_turn_host` and its source value, which is where it is true. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…d default Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
动机
有界 Turn 的默认宿主此前是环境无关的 dsh:selected_turn_host() 从不读环境,
直接返回 (dsh, product_default)。在一台没有 DEEPSEEK_API_KEY 的机器上,默认的
loopx turn plan / run-once 因此解析出一个无法认证的托管执行器,绑定读回
operator_credential_unconfigured,Turn 在 journal/host/quota 之前失败关闭。
operator 必须先知道内部退出 --host codex-cli,才能让第一个默认 Turn 跑起来;这个
成本在每台无凭据机器、每次默认调用上重复。受影响的是默认路径上的所有 operator,
以及通过同一 owner 驱动有界 Turn 的管家通道。
修复方向不需要新机制:管家通道(chat_manager.manager_channel_binding)已经用同一
个凭据事实解析执行器——配了凭据走 dsh,否则回退 codex。本 PR 把 Turn 面拉齐到
同一条规则,而不是让两个面继续各自声称"凭据能不能解析默认"。非目标(明确写在
PR 正文):不改显式 --host / LOOPX_TURN_HOST 语义,不取消显式托管选择在无凭据
时的 fail-closed 拒绝,不引入新 provider、新凭据变量或新模型默认值。
改动思路
决策 owner 不变,仍是
loopx/control_plane/turn_driver/host_binding.selected_turn_host();改动只是给这个
既有 owner 增加一条凭据分支,并把"来源"从一个 product_default 拆成三个可判别
的来源码。
入口是 loopx turn plan|run-once:turn_registration.py 把
resolve_default_turn_host() 当作 --host 默认值并据此配对执行模式,
turn.py:221 把 managed_executor_binding(...) 挂到 plan payload。权威输入是
operator 环境事实(DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL,由
operator_credential.py 只报告变量名、从不读取值)加上显式选择。分支顺序把"显式
优先"写成不变量:先看显式配置,再看凭据事实。
复用判断:仓库里已经有同一规则形状的实现(管家通道按
operator_credential_configured() 条件绑定),而 Turn 面唯一的 owner 就是
selected_turn_host(),所以这里既没有新增"第二个选择器",也没有新增持久化状态——
host 与 source 都是每次调用从环境加显式配置派生出来的,没有需要迁移或对齐的
存储。失败/重试 owner 仍在 managed_executor_binding():显式选择托管宿主而又没有
凭据时,依然给出 typed reason 与 remediation。被改的只有"没有主人时的出厂默认",
operator 已经做出的选择一律不动。
具体改动
关键代码讲解
selected_turn_host()(loopx/control_plane/turn_driver/host_binding.py:91)
是默认宿主决策的唯一 owner。改后返回(host, source):显式LOOPX_TURN_HOST
→explicit_config;configured_operator_credential()为真 →dsh/
operator_credential;否则 →codex-cli/no_operator_credential。不变量是
分支顺序——显式永远先判定,因此凭据只能解析出厂默认,不能改写已选宿主;走
env_text()还保证"设了但为空"的变量不会被当成凭据。managed_executor_binding()(同文件:157)的读回形状不变:executor、
executor_kind、credential_env、endpoint_env、execution_profile、
available、unavailable_reason、unavailable_remediation。变化在于默认路径
不再落到需要凭据的托管分支;available is False只在 LoopX 能证明该执行器在此
无法启动时给出。configured_operator_credential()(loopx/control_plane/operator_credential.py:36)
仍只报告"哪个变量被配置",不读值;本 PR 把它的 docstring 收敛为"只解析那些没有
自己选择的面的出厂默认",这样这个模块的语义与它现在参与的决定一致。register_turn_commands()(loopx/cli_commands/turn_registration.py:22)
行为不变,只是注释改为"执行模式跟随被解析出的宿主",因为宿主本身现在可以来自
凭据而不是纯产品常量。
其余改动是契约与验证面:loopx-turn-v0.md、host-mode-plan-v0.md、DSH/Pi RFC 的
英文与 zh-CN 双镜像、connector 文档同步了新规则,并把被这次变更推翻的三句话(托管
替代宿主的升格门禁、凭据只认证不选择、管家通道"没有默认路径走到个体订阅")就地
改写、写明改写理由,而不是留下自相矛盾的两段话;host_mode_planner.py 的依据注释、
两个 Turn smoke 的断言与 named_string_constants 2049 → 2050 的语义清单随之更新。
对主干的风险
最强回归场景:依赖"默认即托管宿主"的调用方,在无凭据机器上会静默改用 codex-cli
这个个体宿主执行,于是原本假定托管端点(provider、模型、计费)的工作跑到个人 CLI
登录上。触发状态是 DEEPSEEK_API_KEY 缺失或为空,且没有 --host/LOOPX_TURN_HOST。
阻止与该状态可见性:分支顺序决定只有"什么都没选"时才应用新默认;该状态下
managed_executor_binding() 报 executor_kind: individual、credential_env: null,
因此依赖关系在产生效果之前就可读,而不是运行后才暴露。爆炸半径是默认
loopx turn 调用与引用 resolved host 的展示面;显式选择调用方不受影响。回滚成本低:
LOOPX_TURN_HOST=dsh(或 --host dsh)即可恢复托管宿主,revert 提交则恢复原默认。
反例检查:是否有绕过 owner 的第二条默认路径?rg 'MANAGED_TURN_HOST|INDIVIDUAL_TURN_HOST'
显示生产侧唯一消费者是 turn_registration.py 经 resolve_default_turn_host(),常量也
由 host_binding 自己持有,因此不存在第二个默认。真实边界:凭据两个方向都通过真实
CLI 入口跑过(凭据用合成非敏感值,未读取、未打印、未提交任何真实凭据);未验证项是
托管端点的实际计费行为——本次验证的是宿主解析、绑定读回与拒绝路径,不是一次真实
托管 Turn。显式 --host dsh + 空凭据的拒绝仍然成立(available: false、
operator_credential_unconfigured、remediation configure_operator_credential +
select_individual_host),已在 reviewed head 实测。
我的整体评价
方向与仓库既有规则一致,机制是"既有 owner 加一个分支 + 可判别来源码",没有新增抽象、
标志、schema 或持久化状态;被推翻的 RFC 语句就地改写而不是叠加第二份说法;验证覆盖
默认两个方向、显式覆盖、拒绝路径与仓库原生门禁(262 个聚焦测试、4 个公开 smoke、
canary premerge 17 项 0 失败、语义清单/文档治理/ruff 干净、5 个 commit DCO)。
这是 #4454 的修复版:其三条 review finding(P1 语义清单过期、P3 pin 建议、P3
*_DEFAULT_TURN_HOST 别名)在本 head 已逐条修掉;因修复需要 rebase + force-push,
GitHub 拒绝重开 #4454("was force-pushed or recreated"),故以本 PR 交付。
English verdict: APPROVE — the repaired change is rebased onto current main and
green. One decision owner (selected_turn_host()) now resolves the shipped
default from the operator credential facts while an explicit
--host/LOOPX_TURN_HOST still wins and the typed
operator_credential_unconfigured refusal is retained for an explicit managed
selection; the credential is still never read as a value. Verified on the real
CLI entrypoint in both credential states plus the refusal path, with 262 focused
tests, 4 public smokes, and loopx canary premerge (17 checks, 0 failures) at
the reviewed head. No blocking finding remains; the only unverified dimension is
live managed-endpoint billing behavior.
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head: b9c0fc440f29f51f24c3b53a6f030c4d18c1c2f0 (base main @
b132d0bfb); every claim below was re-verified at this head.
动机
有界 Turn 的默认宿主此前是环境无关的 dsh:selected_turn_host() 从不读环境,
直接返回 (dsh, product_default)。在一台没有 DEEPSEEK_API_KEY 的机器上,默认的
loopx turn plan / run-once 因此解析出一个无法认证的托管执行器,绑定读回
operator_credential_unconfigured,Turn 在 journal/host/quota 之前失败关闭。
operator 必须先知道内部退出 --host codex-cli,才能让第一个默认 Turn 跑起来;这个
成本在每台无凭据机器、每次默认调用上重复。受影响的是默认路径上的所有 operator,
以及通过同一 owner 驱动有界 Turn 的管家通道。
修复方向不需要新机制:管家通道(chat_manager.manager_channel_binding)已经用同一
个凭据事实解析执行器——配了凭据走 dsh,否则回退 codex。本 PR 把 Turn 面拉齐到
同一条规则,而不是让两个面继续各自声称"凭据能不能解析默认"。非目标(明确写在
PR 正文):不改显式 --host / LOOPX_TURN_HOST 语义,不取消显式托管选择在无凭据
时的 fail-closed 拒绝,不引入新 provider、新凭据变量或新模型默认值。
改动思路
决策 owner 不变,仍是
loopx/control_plane/turn_driver/host_binding.selected_turn_host();改动只是给这个
既有 owner 增加一条凭据分支,并把"来源"从一个 product_default 拆成三个可判别
的来源码。
入口是 loopx turn plan|run-once:turn_registration.py 把
resolve_default_turn_host() 当作 --host 默认值并据此配对执行模式,
turn.py:221 把 managed_executor_binding(...) 挂到 plan payload。权威输入是
operator 环境事实(DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL,由
operator_credential.py 只报告变量名、从不读取值)加上显式选择。分支顺序把"显式
优先"写成不变量:先看显式配置,再看凭据事实。
复用判断:仓库里已经有同一规则形状的实现(管家通道按
operator_credential_configured() 条件绑定),而 Turn 面唯一的 owner 就是
selected_turn_host(),所以这里既没有新增"第二个选择器",也没有新增持久化状态——
host 与 source 都是每次调用从环境加显式配置派生出来的,没有需要迁移或对齐的
存储。失败/重试 owner 仍在 managed_executor_binding():显式选择托管宿主而又没有
凭据时,依然给出 typed reason 与 remediation。被改的只有"没有主人时的出厂默认",
operator 已经做出的选择一律不动。
具体改动
关键代码讲解
selected_turn_host()(loopx/control_plane/turn_driver/host_binding.py:91)
是默认宿主决策的唯一 owner。改后返回(host, source):显式LOOPX_TURN_HOST
→explicit_config;configured_operator_credential()为真 →dsh/
operator_credential;否则 →codex-cli/no_operator_credential。不变量是
分支顺序——显式永远先判定,因此凭据只能解析出厂默认,不能改写已选宿主;走
env_text()还保证"设了但为空"的变量不会被当成凭据。managed_executor_binding()(同文件:157)的读回形状不变:executor、
executor_kind、credential_env、endpoint_env、execution_profile、
available、unavailable_reason、unavailable_remediation。变化在于默认路径
不再落到需要凭据的托管分支;available is False只在 LoopX 能证明该执行器在此
无法启动时给出。configured_operator_credential()(loopx/control_plane/operator_credential.py:36)
仍只报告"哪个变量被配置",不读值;本 PR 把它的 docstring 收敛为"只解析那些没有
自己选择的面的出厂默认",这样这个模块的语义与它现在参与的决定一致。register_turn_commands()(loopx/cli_commands/turn_registration.py:22)
行为不变,只是注释改为"执行模式跟随被解析出的宿主",因为宿主本身现在可以来自
凭据而不是纯产品常量。
其余改动是契约与验证面:loopx-turn-v0.md、host-mode-plan-v0.md、DSH/Pi RFC 的
英文与 zh-CN 双镜像、connector 文档同步了新规则,并把被这次变更推翻的三句话(托管
替代宿主的升格门禁、凭据只认证不选择、管家通道"没有默认路径走到个体订阅")就地
改写、写明改写理由,而不是留下自相矛盾的两段话;host_mode_planner.py 的依据注释、
两个 Turn smoke 的断言与 named_string_constants 2049 → 2050 的语义清单随之更新。
对主干的风险
最强回归场景:依赖"默认即托管宿主"的调用方,在无凭据机器上会静默改用 codex-cli
这个个体宿主执行,于是原本假定托管端点(provider、模型、计费)的工作跑到个人 CLI
登录上。触发状态是 DEEPSEEK_API_KEY 缺失或为空,且没有 --host/LOOPX_TURN_HOST。
阻止与该状态可见性:分支顺序决定只有"什么都没选"时才应用新默认;该状态下
managed_executor_binding() 报 executor_kind: individual、credential_env: null,
因此依赖关系在产生效果之前就可读,而不是运行后才暴露。爆炸半径是默认
loopx turn 调用与引用 resolved host 的展示面;显式选择调用方不受影响。回滚成本低:
LOOPX_TURN_HOST=dsh(或 --host dsh)即可恢复托管宿主,revert 提交则恢复原默认。
反例检查:是否有绕过 owner 的第二条默认路径?rg 'MANAGED_TURN_HOST|INDIVIDUAL_TURN_HOST'
显示生产侧唯一消费者是 turn_registration.py 经 resolve_default_turn_host(),常量也
由 host_binding 自己持有,因此不存在第二个默认。真实边界:凭据两个方向都通过真实
CLI 入口跑过(凭据用合成非敏感值,未读取、未打印、未提交任何真实凭据);未验证项是
托管端点的实际计费行为——本次验证的是宿主解析、绑定读回与拒绝路径,不是一次真实
托管 Turn。显式 --host dsh + 空凭据的拒绝仍然成立(available: false、
operator_credential_unconfigured、remediation configure_operator_credential +
select_individual_host),已在 reviewed head 实测。
我的整体评价
方向与仓库既有规则一致,机制是"既有 owner 加一个分支 + 可判别来源码",没有新增抽象、
标志、schema 或持久化状态;被推翻的 RFC 语句就地改写而不是叠加第二份说法;验证覆盖
默认两个方向、显式覆盖、拒绝路径与仓库原生门禁(262 个聚焦测试、4 个公开 smoke、
canary premerge 17 项 0 失败、语义清单/文档治理/ruff 干净、5 个 commit DCO)。
这是 #4454 的修复版:其三条 review finding(P1 语义清单过期、P3 pin 建议、P3
*_DEFAULT_TURN_HOST 别名)在本 head 已逐条修掉;因修复需要 rebase + force-push,
GitHub 拒绝重开 #4454("was force-pushed or recreated"),故以本 PR 交付。
English verdict: APPROVE — the repaired change is rebased onto current main and
green. One decision owner (selected_turn_host()) now resolves the shipped
default from the operator credential facts while an explicit
--host/LOOPX_TURN_HOST still wins and the typed
operator_credential_unconfigured refusal is retained for an explicit managed
selection; the credential is still never read as a value. Verified on the real
CLI entrypoint in both credential states plus the refusal path, with 262 focused
tests, 4 public smokes, and loopx canary premerge (17 checks, 0 failures) at
the reviewed head. No blocking finding remains; the only unverified dimension is
live managed-endpoint billing behavior.
动机
loopx turn的默认宿主此前是环境无关的dsh:在一台没有DEEPSEEK_API_KEY的机器上,默认的loopx turn plan/run-once会以operator_credential_unconfigured直接失败关闭,operator 必须先知道要补--host codex-cli才能把 Turn 跑起来。本 PR 把默认改成 owner 指定的那一条:配了厂商凭据就走托管
dsh,没配就走本机本来就能用的
codex-cli。显式的--host/LOOPX_TURN_HOST永远优先,凭据只解析"本来没有主人"的出厂默认,绝不改写已经做出的选择。
改动
turn_driver/host_binding.selected_turn_host()增加凭据分支:显式选择 →explicit_config;有凭据 →dsh/operator_credential;无凭据 →codex-cli/no_operator_credential。两个方向都带来源读回,operator 能区分"产品默认"和"我选过"。
managed_executor_binding保持既有读回(executor / executor_kind /execution_profile / available / unavailable_reason / remediation);typed
operator_credential_unconfigured拒绝现在只在 operator 显式选择托管宿主而没有凭据时触发,不再由默认路径触发。
loopx-turn-v0.md、host-mode-plan-v0.md、docs/integrations/deepseek-harness-connector.md,以及 DSH/Pi RFC 的英文与zh-CN双镜像。RFC 里被这次变更推翻的语句(托管替代宿主的升格门禁、凭据只认证不选择、管家通道"没有默认路径走到个体订阅")就地改写并把改写理由写进
正文,而不是留下自相矛盾的两段话。
host_mode_planner.py的依据说明、两个 Turn smoke 的断言、cli_commands/turn_registration.py与operator_credential.py的措辞对齐。loopx/semantics/inventory_v0.json重生成(named_string_constants2049 → 2050)。
与 #4454 的关系
本 PR 是 #4454 的修复版,不是新方向。#4454 上的三条 review finding 在这里
逐条修掉:
main后重生成清单,--check已最新。条件默认"是两个独立读回这一点,写进了两份镜像。
*_DEFAULT_TURN_HOST别名:删除,选择直接返回宿主常量。修完 finding 后分支必须 rebase + force-push,GitHub 因此拒绝重开 #4454
(
state cannot be changed. The ... branch was force-pushed or recreated.),所以修复结果以本 PR 交付,#4454 的讨论保留为变更记录。差异:本 PR 的 head 是
rebase 后的
b9c0fc440,相对当时记录的1a1ab47e2只多了对当前main的rebase 与随之重生成的语义清单。
验证
真实 CLI 读回(
PYTHONPATH=<worktree> python -m loopx.entrypoint turn plan --goal-id loopx-meta --agent-id codex-managed-steward-product):DEEPSEEK_API_KEY→executor: codex-cli、executor_kind: individual;executor: dsh、executor_kind: managed、execution_profile: deepseek-v4-flash@high、available: true。自动化检查:
test_turn_default_host_binding、test_turn_managed_executor_binding、turn driver / executor / host-failure /managed-step / codex-cli / envelope);
host-mode-plan-smoke.py、project/host-mode-plan-cli-smoke.py、loopx-turn-managed-executor-binding-smoke.py、loopx-turn-managed-default-flow-smoke.py;scripts/generate_semantic_inventory.py --check:最新;examples/docs-governance-smoke.py:ok;ruff check:ok;loopx canary premerge --from-git-diff --timeout-seconds 420:passed,tier=standard,changed_files=15,surfaces=control_plane、docs_project_content、
public_boundary、python,selected=17,failures=0,advisory_failures=0;
Signed-off-by。风险
解析"。仅在没有任何显式选择时生效,
--host/LOOPX_TURN_HOST语义不变。managed_executor.executor_kind/available读回表达。显式选择dsh仍然fail closed。
dsh,否则codex)现在一致,两处读回不再互相矛盾,这是本 PR 的正面效果之一。
English verdict: the repaired branch is rebased onto current
mainand green.The default Turn host now resolves from the operator credential (
dshwhen it isconfigured,
codex-cliotherwise) while an explicit--host/LOOPX_TURN_HOSTstill wins, the typed
operator_credential_unconfiguredrefusal is retained foran explicit managed selection, and the RFC/gate wording the change falsifies is
rewritten in place rather than left contradictory. Supersedes #4454, which
GitHub cannot reopen after the rebase. No open review finding remains.