Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/reference/protocols/host-mode-plan-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ quota guard command, and required proofs.
- `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.
resolves the concrete host from its own explicit product default
(`loopx/control_plane/turn_driver/host_binding.py`) when `plan_command` runs,
and `LOOPX_TURN_HOST` or an explicit `--host` re-points that default. An
operator credential authenticates the selected host; it does not select one.
- `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`.
Expand Down
28 changes: 26 additions & 2 deletions examples/host-mode-plan-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from __future__ import annotations

import os
import re
import sys
from pathlib import Path
Expand Down Expand Up @@ -113,8 +114,8 @@ def test_headless_maps_to_loopx_turn_plan_not_parallel_runner() -> None:
assert selected["scheduler_owner"] == "outer_controller", selected
command = selected["plan_command"]
assert "loopx turn plan" in command, command
# The preview keeps the shipped host resolution, so a lane without an
# operator credential does not land on the compatibility adapter path.
# The preview keeps the shipped host resolution, so it does not freeze the
# compatibility adapter path as the product default.
assert "--host" not in command, command
assert "--execution-mode isolated-headless" in command, command
assert "--scheduler-owner outer_controller" in command, command
Expand All @@ -127,6 +128,28 @@ def test_headless_maps_to_loopx_turn_plan_not_parallel_runner() -> None:
assert plan["turn_contract"]["writeback_before_quota_spend"] is True, plan


def test_headless_preview_ignores_operator_credential() -> None:
credential_env = "DEEPSEEK_API_KEY"
previous = os.environ.pop(credential_env, None)
try:
without_credential = build_full_plan("continue_without_ui")["selected_turn_mapping"]
os.environ[credential_env] = "host-mode-plan-smoke-not-a-credential"
with_credential = build_full_plan("continue_without_ui")["selected_turn_mapping"]
finally:
if previous is None:
os.environ.pop(credential_env, None)
else:
os.environ[credential_env] = previous
# `loopx turn plan`/`run-once` ship one explicit product default that
# `LOOPX_TURN_HOST` or an explicit `--host` re-points, and an operator
# credential only authenticates the host that was already selected. A
# preview whose shape changed when the credential appeared would re-introduce
# a credential-selected Turn host, so the shape is pinned here instead.
assert with_credential == without_credential, (without_credential, with_credential)
assert without_credential["host_selection"] == "resolved_default", without_credential
assert "--host" not in without_credential["plan_command"], without_credential


def test_visible_mode_stays_visible_and_scoped() -> None:
plan = build_workflow_identity_plan("watch_each_turn", host_identity="codex-cli")
selected = plan["selected_turn_mapping"]
Expand Down Expand Up @@ -469,6 +492,7 @@ def test_markdown_and_docs_are_wired() -> None:
def main() -> int:
test_intent_selects_distinct_host_modes()
test_headless_maps_to_loopx_turn_plan_not_parallel_runner()
test_headless_preview_ignores_operator_credential()
test_visible_mode_stays_visible_and_scoped()
test_visible_mode_preserves_distinct_host_identities()
test_visible_mode_fails_closed_without_host_identity()
Expand Down
13 changes: 8 additions & 5 deletions loopx/host_mode_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,14 @@
_INTENT_PRIMARY_MODE = {meta["intent"]: mode for mode, meta in _MODE_METADATA.items()}

# The headless Turn modes preview the *shipped* host resolution instead of
# pinning one host. `loopx turn plan`/`run-once` resolve their default from the
# operator credential, so a preview that pinned `generic-cli` would quietly ask
# every operator for the compatibility adapter path. The declared host stays in
# the mapping and in the rollback command, because the mode's scheduler context
# and capability requirements are still stated for it.
# pinning one host. `loopx turn plan`/`run-once` ship one explicit product
# default, owned by `control_plane.turn_driver.host_binding.selected_turn_host`,
# which `LOOPX_TURN_HOST` or an explicit `--host` re-points; an operator
# credential authenticates that host and never selects it, so a preview that
# pinned `generic-cli` would quietly ask every operator for the compatibility
# adapter path. The declared host stays in the mapping and in the rollback
# command, because the mode's scheduler context and capability requirements are
# still stated for it.
RESOLVED_DEFAULT_TURN_HOST_MODES = frozenset(
{MODE_ISOLATED_HEADLESS_TURN, MODE_SHELL_SERVICE}
)
Expand Down