Skip to content

feat(turn): select the managed Turn host explicitly - #4443

Merged
huangruiteng merged 2 commits into
mainfrom
codex/managed-execution-explicit-selection-20260915
Sep 15, 2026
Merged

huangruiteng merged 2 commits into
mainfrom
codex/managed-execution-explicit-selection-20260915

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Summary

The Turn host is now selected, never inferred, and the operator credential
only authenticates the host that was already selected. This replaces the
credential-resolved default rule, which let a configured DEEPSEEK_API_KEY
silently re-point loopx turn plan and loopx turn run-once from the
individual Codex CLI host to the managed dsh host.

Discovering a credential is not a decision to change where a Turn runs. Under
the old rule an operator could not tell a chosen configuration from an
incidental one, and the affected surfaces reported a billing boundary (managed
vs individual) that the environment had picked for them.

What changed

surface value
shipped default host dsh (managed executor)
explicit default selector LOOPX_TURN_HOST
per-command override --host codex-cli|claude-code|dsh|generic-cli (plan), codex-cli|dsh|generic-cli (run-once)
authenticating credential DEEPSEEK_API_KEY, optional endpoint DEEPSEEK_BASE_URL
  • resolve_default_turn_host / selected_turn_host in
    loopx/control_plane/turn_driver/host_binding.py are now pure functions of
    configuration: the product default applies, LOOPX_TURN_HOST re-points it,
    and no credential value or presence participates in selection.
  • loopx/control_plane/operator_credential.py reports credential facts only
    and documents that selection is owned by each surface, not by this module.
  • The planned execution mode follows the selected host: the managed default
    plans isolated-headless rather than an execution mode the outer controller
    rejects.

The managed host authenticates or it refuses

managed_executor_binding projects the executor, its kind (managed,
individual, generic), the credential env var name (never its value), the
endpoint env var name, whether the executor is operator-credential-bound, and
whether it can launch here. When it cannot, available is false and
unavailable_reason names the missing fact:

unavailable_reason meaning remediation
dsh_runtime_unavailable the DeepSeek Harness runtime is not importable and no explicit runner hook was supplied install the released runtime, pass its runner hook, or select --host codex-cli
operator_credential_unconfigured the managed host is selected but nothing would authenticate it set DEEPSEEK_API_KEY, or select --host codex-cli explicitly

run-once --execute fails closed on that verdict: status unavailable, no host
invocation, no Journal write, and no quota slot spend. plan reports the same
verdict without refusing. An explicitly selected individual host
(--host codex-cli, --host claude-code) is billed to that individual login and
makes no launchability claim (available: null).

Default behavior change

Affected lanes: codex-cli, claude-code, dsh, generic-cli.

  • loopx turn run-once: generic-cli → dsh
  • loopx turn plan: codex-cli → dsh

Disclosed in docs/reference/protocols/loopx-turn-v0.md under a new Host
Selection
section, with the typed refusal reasons and their remediations.
Tests and smokes that name a specific host now pass --host explicitly so they
keep testing the host they named rather than the ambient default.
--host generic-cli remains the compatibility and rollback path, and
LOOPX_TURN_HOST re-points the default once for a machine that wants the former
behavior.

CLI output budget

The Turn plan readback grows once, from two reviewed causes: the new
managed_executor block, and the host-selection consequences on the plan
(host projection, scheduler execution context, and controller-owned
next_cli_actions rendering). The allowance in cli_output_differential.py is
surface-scoped to the Turn surfaces, bound to the none-to-v0 transition, and
one-time: once v0 is the baseline the same growth is a regression.

Validation

check result
313 tests across the turn driver/executor, default-host, managed-executor, dsh goal mode, scan-root, and CLI output budget/differential suites passed
examples/loopx-turn-managed-executor-binding-smoke.py passed
examples/loopx-turn-managed-default-flow-smoke.py passed
examples/dsh-turn-host-adapter-smoke.py, loopx-turn-dsh-e2e-smoke.py, loopx-turn-dsh-builtin-host-e2e-smoke.py, loopx-turn-dsh-real-e2e-smoke.py --host dsh|generic-cli passed
examples/loopx-turn-codex-cli-e2e-smoke.py, loopx-turn-codex-cli-quickstart-smoke.py, loopx-turn-fake-host-walkthrough-smoke.py, loopx-turn-path-delta-acceptance-smoke.py passed
examples/control_plane/cli-output-budget-regression-smoke.py, control-plane-maintainability-ratchet-smoke.py, docs-governance-smoke.py passed
loopx canary premerge --from-git-diff gate passed

The default-flow smoke runs the real dsh runtime against a local mock
OpenAI-compatible SSE endpoint, so it consumes no operator key and no individual
CLI subscription. Every credential in the tests and smokes is a fixture string
whose only role is to authenticate the already-selected host.

Boundaries

  • No credential value is read or projected; only env var names appear.
  • No provider calls are made by the tests or the smokes.
  • No new runtime dependency, no new configuration file, no state migration.

Supersedes

This single main-based change replaces the stacked
#4409 (credential-resolved default host), #4416 (managed executor readback
and fail-closed start), and #4427 (credential-resolved default flow
qualification). Those PRs are closed in favor of this one so the delivery chain
does not need layer-by-layer refreshes and can be merged one slice at a time.

The Turn host must be selected, never inferred. The previous default rule
read the environment: a configured `DEEPSEEK_API_KEY` silently re-pointed
`loopx turn plan` and `loopx turn run-once` from the individual Codex CLI
host to the managed dsh host. Discovering a credential is not a decision to
change where a Turn runs, and it left the operator unable to tell a chosen
configuration from an incidental one.

Selection is now explicit and the credential only authenticates it:

- `MANAGED_DEFAULT_TURN_HOST` (`dsh`) is the shipped product default, chosen
  once and independent of the environment;
- `LOOPX_TURN_HOST` re-points that default without repeating `--host`;
- an explicit `--host` still wins over both;
- `managed_executor_binding` reports the executor, its kind, the credential
  env var *name*, the endpoint env var name, whether it is
  operator-credential-bound, and whether it can launch here.

The managed host is billed to the operator endpoint, so it is unavailable
with a typed reason when it cannot be authenticated or launched:
`operator_credential_unconfigured` or `dsh_runtime_unavailable`. `run-once
--execute` fails closed on that verdict with no host invocation, no Journal
write, and no quota spend; `plan` reports it without refusing. An explicitly
selected individual host makes no launchability claim (`available: null`).

Default behavior change on the `codex-cli`/`dsh`/`generic-cli` Turn lanes:
`run-once` moved from `generic-cli` to `dsh`, and `plan` from `codex-cli` to
`dsh`, with the planned execution mode following the selected host.
`--host generic-cli` and `--host codex-cli` remain the explicit compatibility
and rollback paths, and `LOOPX_TURN_HOST` re-points the default once.

Validation:
- 313 tests across the turn driver/executor, default-host, managed-executor,
  dsh goal mode, scan-root, and CLI output budget/differential suites;
- `examples/loopx-turn-managed-executor-binding-smoke.py` (explicit selection,
  both typed refusals, fail-closed execution, explicit individual host);
- `examples/loopx-turn-managed-default-flow-smoke.py` (the real dsh runtime on
  a local mock endpoint: credential-bound default commits one validated Turn,
  no-credential default refuses, hidden runtime refuses);
- `loopx canary premerge --from-git-diff`, including the one-time bounded
  CLI output growth allowance for the Turn surfaces.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
The connector doc still described the host as resolved from the environment.
It now documents the explicit selection surfaces (shipped default, LOOPX_TURN_HOST,
--host), the managed_executor readback block and what each field claims, and the
fail-closed verdict run-once applies before it invokes a host.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng added a commit that referenced this pull request Sep 15, 2026
…n gates

The RFC still described the managed host as resolved from the operator
credential. It now records what the repository ships and what the managed
stack will ship: the Turn host is selected, never inferred (shipped default
`dsh`, `LOOPX_TURN_HOST` re-points it, an explicit `--host` wins), the steward
channel selects its own executor (shipped default `codex`, because it is the
only interactive Chat transport today), and a configured credential only
authenticates the selection instead of changing it.

- The role table gains the steward channel executor row and states each
  selection source, including the `individual` versus `managed` executor kind.
- The steward-channel section replaces the credential-resolved binding rule and
  records the promotion gate for the managed host (an interactive Chat
  transport), so the credential is explicitly not the promotion signal.
- The milestone table records the steward channel's M1 contract as selected --
  endpoint, model, source and executor kind -- and states the session-identity
  gap that still blocks a channel answer from proving which session served it.
- Evidence and dsh-pin rows are re-pointed at the replacement PRs (#4443,
  #4446) and the dsh pin PR #4420.

Docs only: no runtime is promoted, no default behavior changes here, and the
English and Chinese mirrors are updated together.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

详细中文评审

审查对象:464262e6abc552b28e202bc90eeeaf0dd5c28f0e(feat(turn): select the managed Turn host explicitly,base main,19 文件 +2099/-200)。执行契约 policy_revision=3;结果 JSON 已通过 pr-review --check-result。

动机

改之前,托管 Turn 的默认宿主由环境碰巧决定:loopx turn plan 默认 codex-cli、run-once 默认 generic-cli,两个命令的口径都不一样,而且没有单一 owner。前一轮堆叠版本更进一步按凭据解析默认值——装了 DEEPSEEK_API_KEY 的机器悄悄换成 dsh,没装的机器留在 generic-cli。结果是同一份计划在不同机器上语义不同,运营方无法区分「我选的」和「环境给的」,而计费边界恰恰挂在这个默认值上。本 PR 把选择收敛为一处显式解析,凭据退回认证事实。

改动思路

把三件事拆成三个 owner:选择归 loopx/control_plane/turn_driver/host_binding.py::selected_turn_host(显式 --host > LOOPX_TURN_HOST > 产品默认 MANAGED_DEFAULT_TURN_HOST),可用性归同模块的 managed_executor_binding(只在能证明不可启动时给 available=false,非托管宿主保持 None,不做无证据断言),执行前拒绝归 turn_driver/executor.run_loopx_turn_once。凭据事实抽到共享叶子模块 loopx/control_plane/operator_credential.py,只报变量名与是否配置,不读取值、不参与选择。CLI 侧只做接线,并复用仓库既有的 CLI 输出差分/预算机制来表达一次性增长,而不是放宽断言。

具体改动

  • selected_turn_host(host_binding.py:66):唯一的宿主选择点,返回 (host, source),来源用 product_default / explicit_config 区分。
  • managed_executor_binding(host_binding.py:105):投影 executor、executor_kind、credential_env、endpoint_env、operator_credential_bound、available、unavailable_reason;typed 原因只有 dsh_runtime_unavailable 与 operator_credential_unconfigured,remediation 刻意留在文档而不是 payload。
  • run_loopx_turn_once(executor.py:81):不可用时以 status unavailable 终止,且发生在宿主调用、Journal 写入、配额消耗之前。
  • operator_credential.py:configured_operator_credential / operator_credential_configured,被 host_binding 与 chat_manager 共同引用,消除读取漂移。
  • CLI 输出预算:cli_output_differential.py 新增按 surface 限定的 _TURN_HOST_AND_MANAGED_EXECUTOR_BINDING_V0_GROWTH_ALLOWANCE(chars 832 / lines 14),绑定 none→v0 过渡且一次性。
  • 测试与 smoke:tests/test_turn_default_host_binding.py、tests/test_turn_managed_executor_binding.py、tests/test_turn_executor.py,以及 examples/loopx-turn-managed-executor-binding-smoke.py(365 行)与 examples/loopx-turn-managed-default-flow-smoke.py(642 行)。
  • 文档:docs/reference/protocols/loopx-turn-v0.md 新增 Host Selection;docs/integrations/deepseek-harness-connector.md 补宿主选择与读取章节。

对主干的风险

  • 默认路径行为收窄:未安装 loopx[deepseek-harness] 的机器上,默认 run-once --execute 从「能跑」变成「拒绝」。这是有意的 fail-closed,但对既有用户是一次真实变化,需要发布说明显眼披露并给出三种补救(装运行时 / 注入 runner / 显式 --host)。回滚成本低(一个环境变量或一个参数),无持久状态迁移。
  • 调度器产生的 unattended 计划仍映射到 isolated_headless_turn 的 generic-cli,与新默认不一致;PR 在文档里把它记为待决缺口而不是声称已统一,这个处理是诚实的,但缺口本身仍在。
  • operator_credential.py 与同系列 #4446 互相新增,后合并方会有一次 add/add 冲突,需要一次 rebase(内容相同,取任一即可)。
  • 未验证维度:managed default flow smoke 用真实 dsh runtime 与真实 executor,但 LLM 端点是本地 mock SSE,因此真实 provider 往返不在本 head 的验证范围;Pi 与 L1 事件源仍是 opt-in,未被本 PR 晋级。

我的整体评价

APPROVE。这是对默认路径语义错误的正确修复:change_proportionality=proportionate、repository_reuse=separation_justified、authority_semantics=aligned、default_off_isolation=isolated、observable_semantics=intentional_change_validated。相对被它替代的旧堆叠链(#4409+#4416+#4427)范围是缩小的:合并为单一 main-based 切片,删掉了凭据解析分支。非阻塞建议两条:把 unattended 计划映射对齐或显式标注为回滚路径(P2);managed default flow smoke 偏厚,若 dsh 层已有等价覆盖,可只保留默认选择那一段。

验证:test_turn_default_host_binding.py + test_turn_managed_executor_binding.py + test_turn_executor.py 通过;313 条 turn/executor/CLI 输出预算测试通过;loopx-turn-managed-default-flow-smoke.py(真实 dsh runtime + 本地 mock SSE)与既有 codex-cli/dsh e2e smokes 通过;docs-governance-smoke.py、cli-output-budget-regression-smoke.py 通过;loopx canary premerge --from-git-diff gate passed;远端 CI 12 pass / 4 skipping / 7 pending,无失败。


English verdict: APPROVE at 464262e6abc552b28e202bc90eeeaf0dd5c28f0e. The managed Turn host is now selected explicitly instead of being resolved from the environment: --host > LOOPX_TURN_HOST > product default (dsh), with host_binding.selected_turn_host as the single decision owner; an operator credential is reported as a fact only and no longer re-points the executor. An unavailable managed host now fails closed before host invocation, Journal write, or quota consumption, with typed reasons dsh_runtime_unavailable / operator_credential_unconfigured. Key non-blocking notes: the unattended scheduler plan still emits generic-cli without being labeled the rollback path (P2), and loopx/control_plane/operator_credential.py is added by both this PR and #4446, so the second one to merge needs an add/add rebase. Validation: 313 turn/executor/CLI-output tests, the managed-executor binding smoke, the managed default flow smoke (real dsh runtime, local mock SSE endpoint), the docs-governance and CLI-output-budget smokes, and loopx canary premerge --from-git-diff all pass; remote CI has no failures.

@huangruiteng
huangruiteng merged commit 3f916ff into main Sep 15, 2026
29 checks passed
@huangruiteng
huangruiteng deleted the codex/managed-execution-explicit-selection-20260915 branch September 15, 2026 10:53
huangruiteng added a commit that referenced this pull request Sep 15, 2026
2e96a57 (#4443) removed both halves of the `loopx turn managed-step`
surface while rewiring host selection: the `managed-step` subparser in
turn_registration.py and the `handle_turn_managed_step` dispatch in turn.py.
loopx/cli_commands/turn_managed_step.py and its unit test survived, so the
module still passed its own tests while the subcommand no longer existed.

Evidence:

- `turn managed-step` now exits 2 with `invalid choice: 'managed-step'`.
- examples/loopx-turn-managed-step-self-heal-smoke.py fails on main and passes
  at 503991d, the commit before that change.

Restore the subparser with the same flags and choices as before, and dispatch
it beside the journal-inspection owner. The managed step selects from the
shipped run-once hosts and always plans isolated-headless, but its default host
follows resolve_default_turn_host() so an explicit LOOPX_TURN_HOST selection is
honored instead of being pinned back to one host.

To keep loopx/cli_commands/turn.py inside its frozen 1114-line baseline while
adding the dispatch back, move the resume-binding resolution into
turn_selection.py. The helper returns `(requested, binding)` because those are
two different facts: a run-once Codex CLI Turn derives a binding from its own
envelope, and that derived binding must not be read back as an explicit resume
request or `--resume-turn-key` would refuse a legitimate resume.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Co-authored-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng added a commit that referenced this pull request Sep 16, 2026
`run-once` rebuilt the fresh Turn decision inline after #4443, so the status
read, scheduler context, route source, capability hooks and decision arguments
were maintained twice and the later fix only recovered part of them. Both
commands now build through `build_fresh_turn_decision_owner`, which also carries
the inputs a later spend or scheduler re-check must settle against, and the
decision builder is private.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Co-authored-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng added a commit that referenced this pull request Sep 16, 2026
`run-once` and `managed-step` must resolve the same governing decision. #4443
(2e96a57) replaced the shared call in `loopx/cli_commands/turn.py` with a
private copy of the whole chain - live status, scheduler context, capability
hook projection, decision parameters, advisory-primary rebinding and the signed
envelope - and a1213ed (#4451) restored only the advisory-primary half. The
copy stayed, so a decision input added to the shared owner would have moved one
Turn subcommand and not the other.

Route `run-once` through the shared owner. `build_fresh_turn_decision` owns the
whole chain and returns the decision together with the envelope signed from that
same decision, so an owner that also needs the raw decision (reward recall)
cannot re-derive a second copy. `build_fresh_envelope_for_managed_step` stays
the read-only projection: it exposes no hook parameter, so the managed step
cannot start publishing Go/No-Go hooks by accident.

Evidence (`/private/tmp/loopx-turn-dedup`, origin/main 292b85e):

- `turn plan` on the same fixture emits a byte-identical `turn_envelope` before
  and after (`diff` of the JSON dumps: no difference).
- `examples/loopx-turn-managed-step-self-heal-smoke.py` prints identical output
  before and after.
- `tests/test_loopx_turn_{managed_step,driver,executor,codex_cli}.py`,
  `tests/test_turn_{envelope,managed_executor_binding,default_host_binding,
  loop_disposition}.py`, `tests/test_loop_{turn_loop_controller}.py`,
  `tests/test_loopx_turn_{settlement_parity,host_failure,journal_inspection,
  transaction}.py`: 386 passed.
- `tests/architecture` plus the control-plane portfolio/CLI-budget/capability
  memory/shadow-e2e suites: 140 passed.

The test seam for the adaptive orchestration contract moves with the code: the
envelope is signed by the shared owner, so
`tests/test_loopx_turn_driver.py` injects `build_turn_envelope` where that owner
resolves it, and
`tests/test_loopx_turn_journal_inspection.py` guards the live reads of the
shared owner instead of the removed `turn.py` re-import. A new plan-mode test
fails if `turn.py` ever reads its live status outside the shared owner again.

The post-settlement `loopx_turn_run_once` scheduler re-evaluation still builds
its own decision: it deliberately re-reads status after the commit, has no
advisory primary and carries a different route source, so folding it into the
plan owner would change behavior. Boundary considered, left as is.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
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.

1 participant