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
11 changes: 8 additions & 3 deletions docs/reference/protocols/host-mode-plan-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,11 @@ as an executor.
"selected_connector_id": "loopx_turn",
"selected_turn_mapping": {
"host": "generic-cli",
"host_selection": "resolved_default",
"execution_mode": "isolated-headless",
"scheduler_owner": "outer_controller",
"plan_command": "loopx turn plan --goal-id loopx-meta --agent-id codex-main-control --host generic-cli --execution-mode isolated-headless --scheduler-owner outer_controller"
"plan_command": "loopx turn plan --goal-id loopx-meta --agent-id codex-main-control --execution-mode isolated-headless --scheduler-owner outer_controller",
"plan_command_rollback": "loopx turn plan --goal-id loopx-meta --agent-id codex-main-control --host generic-cli --execution-mode isolated-headless --scheduler-owner outer_controller"
},
"next_preview_command": "loopx turn plan ...",
"mode_options": [],
Expand Down Expand Up @@ -161,8 +163,11 @@ A fixture or implementation is acceptable when:

1. `schema_version=host_mode_plan_v0` and `mode=dry_run_host_mode_selector`;
2. the five canonical modes are present and intent selects the expected mode;
3. `isolated_headless_turn` maps to `loopx turn plan --host generic-cli
--execution-mode isolated-headless --scheduler-owner outer_controller`;
3. `isolated_headless_turn` maps to the shipped host resolution: the preview
command is `loopx turn plan --execution-mode isolated-headless
--scheduler-owner outer_controller` with no pinned `--host`, and the pinned
compatibility variant is reported as `plan_command_rollback`
(`--host generic-cli`);
4. scoped identity flows into Turn and quota preview commands as `--agent-id`;
5. the no-spend policy covers selector previews, Turn plan previews, quiet
monitors, cadence-only changes, and final/readiness checks;
Expand Down
7 changes: 6 additions & 1 deletion examples/host-mode-plan-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,20 @@ def test_headless_maps_to_loopx_turn_plan_not_parallel_runner() -> None:
plan = build_full_plan("continue_without_ui")
selected = plan["selected_turn_mapping"]
assert selected["host"] == "generic-cli", selected
assert selected["host_selection"] == "resolved_default", selected
assert selected["execution_mode"] == "isolated-headless", selected
assert selected["scheduler_owner"] == "outer_controller", selected
command = selected["plan_command"]
assert "loopx turn plan" in command, command
assert "--host generic-cli" 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.
assert "--host" not in command, command
assert "--execution-mode isolated-headless" in command, command
assert "--scheduler-owner outer_controller" in command, command
assert "--agent-id codex-main-control" in command, command
assert "--available-capability shell" in command, command
rollback = selected["plan_command_rollback"]
assert "--host generic-cli" in rollback, rollback
assert plan["turn_contract"]["schema_version"] == "loopx_turn_v0", plan
assert plan["turn_contract"]["independent_validation_required"] is True, plan
assert plan["turn_contract"]["writeback_before_quota_spend"] is True, plan
Expand Down
7 changes: 6 additions & 1 deletion examples/project/host-mode-plan-cli-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,16 @@ def test_cli_selects_headless_turn_and_scopes_agent_id() -> None:
assert payload["selected_capability_ready"] is True, payload
command = payload["next_preview_command"]
assert "loopx turn plan" in command, command
assert "--host generic-cli" in command, command
# The preview keeps the shipped host resolution instead of pinning the
# compatibility adapter host; the pinned variant stays available as the
# mode's rollback command.
assert "--host" not in command, command
assert "--execution-mode isolated-headless" in command, command
assert "--scheduler-owner outer_controller" in command, command
assert "--agent-id codex-main-control" in command, command
assert "--available-capability shell" in command, command
rollback = payload["selected_turn_mapping"]["plan_command_rollback"]
assert "--host generic-cli" in rollback, rollback
assert payload["selected_missing_host_capabilities"] == [], payload
assert payload["selected_blocking_reasons"] == [], payload
assert payload["operator_next_steps"][0]["kind"] == "state_preview", payload
Expand Down
43 changes: 41 additions & 2 deletions loopx/host_mode_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@
]
_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.
RESOLVED_DEFAULT_TURN_HOST_MODES = frozenset(
{MODE_ISOLATED_HEADLESS_TURN, MODE_SHELL_SERVICE}
)
TURN_HOST_SELECTION_PINNED = "pinned"
TURN_HOST_SELECTION_RESOLVED_DEFAULT = "resolved_default"

# Typed host identity -> runtime connector catalog id. Only identities with a
# registered catalog connector may emit a host-specific visible mapping; any
# other identity fails closed instead of fabricating a connector id.
Expand Down Expand Up @@ -260,6 +272,7 @@ def _turn_plan_command(
cli_bin: str,
available_capabilities: list[str] | None,
host_identity: str | None,
pin_host: bool = True,
) -> str | None:
meta = _MODE_METADATA[mode]
turn_host = meta.get("turn_host")
Expand Down Expand Up @@ -303,9 +316,10 @@ def _turn_plan_command(
)
scheduler_owner = meta.get("scheduler_owner")
scheduler_arg = f" --scheduler-owner {shell_arg(scheduler_owner)}" if scheduler_owner else ""
host_arg = f" --host {shell_arg(turn_host)}" if pin_host else ""
return (
f"{shell_arg(cli_bin)} turn plan --goal-id {shell_arg(goal_id)}{agent_arg} "
f"--host {shell_arg(turn_host)} --execution-mode {shell_arg(execution_mode)}"
f"{shell_arg(cli_bin)} turn plan --goal-id {shell_arg(goal_id)}"
f"{agent_arg}{host_arg} --execution-mode {shell_arg(execution_mode)}"
f"{scheduler_arg}{capability_args}"
)

Expand Down Expand Up @@ -526,6 +540,7 @@ def _build_mode_option(
visible_unresolved = (
mode == MODE_VISIBLE_TUI and host_identity not in VISIBLE_HOST_CONNECTOR_IDS
)
resolves_default_host = mode in RESOLVED_DEFAULT_TURN_HOST_MODES
turn_plan_command = (
None
if visible_unresolved
Expand All @@ -536,8 +551,25 @@ def _build_mode_option(
cli_bin=cli_bin,
available_capabilities=available_capabilities,
host_identity=host_identity,
pin_host=not resolves_default_host,
)
)
# A mode that previews the shipped resolution still names the pinned host it
# would use instead, so an operator who wants the compatibility adapter path
# reads one command rather than re-deriving the flags.
turn_plan_command_rollback = (
_turn_plan_command(
goal_id=goal_id,
agent_id=agent_id,
mode=mode,
cli_bin=cli_bin,
available_capabilities=available_capabilities,
host_identity=host_identity,
pin_host=True,
)
if resolves_default_host
else None
)
quota_guard_command = _mode_quota_guard(
mode=mode,
goal_id=goal_id,
Expand Down Expand Up @@ -614,9 +646,15 @@ def _build_mode_option(
"recommended_next_steps": recommended_next_steps,
"turn_mapping": {
"host": effective_turn_host,
"host_selection": (
TURN_HOST_SELECTION_RESOLVED_DEFAULT
if resolves_default_host
else TURN_HOST_SELECTION_PINNED
),
"execution_mode": meta.get("turn_execution_mode"),
"scheduler_owner": meta.get("scheduler_owner"),
"plan_command": turn_plan_command,
"plan_command_rollback": turn_plan_command_rollback,
},
"scheduler_execution_context": _scheduler_context(mode, host_identity),
"quota_guard_command": quota_guard_command,
Expand All @@ -641,6 +679,7 @@ def target_turn_command(to_mode: str) -> str | None:
cli_bin=cli_bin,
available_capabilities=available_capabilities,
host_identity=host_identity,
pin_host=to_mode not in RESOLVED_DEFAULT_TURN_HOST_MODES,
)
except HostModePlanError:
# A visible target without a catalog-registered identity has no
Expand Down
2 changes: 1 addition & 1 deletion loopx/semantics/inventory_v0.json
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@
"python_closed_sets": 495,
"python_literal_aliases": 8,
"typescript_const_arrays": 40,
"named_string_constants": 2031,
"named_string_constants": 2033,
"schema_version_names": 756,
"schema_version_same_runtime_forks": 7,
"cross_runtime_twins": 166,
Expand Down