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
19 changes: 19 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,25 @@ jobs:
python -m pytest -q
tests/control_plane/test_quota_settlement.py::test_codex_app_windows_settlement_command_substitutes_powershell_turn

- name: Set up the qualified SQLite runtime for settlement recovery
uses: actions/setup-node@v6
with:
node-version: "22.22.3"

- name: Qualify native PowerShell settlement recovery and its immutable baseline
run: >-
python -m pytest -q --basetemp "${{ runner.temp }}/lxs-${{ github.run_id }}-${{ github.run_attempt }}"
tests/control_plane/test_refresh_settle_turn.py::test_wrong_or_missing_child_workspace_retains_owed_command_for_exact_recovery
tests/control_plane/test_refresh_settle_turn.py::test_legacy_posix_quoted_powershell_launcher_fails_before_settlement
tests/control_plane/test_refresh_settle_turn.py::test_pinned_powershell_launcher_settles_original_turn_once

- name: Restore the primary Windows Node runtime
uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
cache-dependency-path: apps/presentation/dashboard/package-lock.json

- name: Qualify TypeScript shard paths on native Windows
run: python -m pytest -q tests/test_python_ci_workflow.py::test_typescript_shards_select_every_test_file_exactly_once

Expand Down
11 changes: 11 additions & 0 deletions docs/reference/protocols/turn-envelope-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,17 @@ identity, step conditions, authority and one-spend semantics are unchanged.
补结算命令保持一致。填充占位项后直接执行,检查退出码和 JSON `ok`;不再需要
手工在子命令后追加格式参数。直接 CLI 的默认格式、历史回执与结算权限均不变。

On Windows, `settlement_owed.command` uses PowerShell syntax for the launcher
and every argument, adding the call operator when the launcher needs quoting.
Run it in PowerShell from the original workspace. The same response carries
`settlement_owed.command_argv` for programmatic callers that need the original
arguments without parsing shell text. Both forms retain the launcher pin and
original settlement identity.

中文:Windows 补结算命令按 PowerShell 语法呈现启动器和每个参数,启动器需要
加引号时会加上 `&`。请在原工作目录执行。程序可从同一响应的 `command_argv`
读取原始参数,无需解析命令文本。两种形式均保留启动器和原结算身份。

When an admitted Turn's verified durable writeback is followed by a Todo
dependency wait, its replay now projects only the original `quota_spend` step.
The later wait cannot replace the committed outcome with a blocked writeback.
Expand Down
12 changes: 10 additions & 2 deletions loopx/cli_commands/refresh_settlement.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
SETTLEMENT_RECEIPT_SCHEMA_VERSION, SettlementStepKind, decode_settlement_receipt_payload,
)
from ..control_plane.quota.settlement import (
SettlementIdentity, attach_settlement_progress, read_heartbeat_settlement, settlement_result_payload,
SettlementIdentity, attach_settlement_progress, read_heartbeat_settlement, render_owed_host_command,
settlement_result_payload,
)


Expand Down Expand Up @@ -130,7 +131,14 @@ def settle_refreshed_turn(
if not settled_replay:
try:
workspace = delivery_workspace_path.expanduser().resolve() if delivery_workspace_path else None
command = shlex.split(owed["command"])
if "command_argv" in owed:
command = owed["command_argv"]
if (not isinstance(command, list) or not command or
not all(isinstance(arg, str) for arg in command) or
render_owed_host_command(command) != owed["command"]):
raise ValueError("owed quota command does not match its argument vector")
else:
command = shlex.split(owed["command"])
# Validate the owner-rendered argv through the actual CLI parser too:
# a projector bug must not debit a different valid Turn before readback.
from ..cli import build_parser
Expand Down
23 changes: 20 additions & 3 deletions loopx/control_plane/quota/settlement.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

from collections.abc import Mapping
from dataclasses import dataclass
import os
from pathlib import Path
import shlex
from typing import Any

from ...command_invocation import loopx_cli_argv
from ...install_contract import powershell_literal
from ...project_prompt import host_command
from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result
from ..settlement_driver import decode_settlement_result
from .accounting_admission import quota_accounting_admission
Expand Down Expand Up @@ -179,6 +182,12 @@ class QuotaSettlementReadback:
successor_replan_pending: bool = False


def render_owed_host_command(argv: list[str]) -> str:
# The PowerShell launcher scans its arguments as strings, including numbers.
quote = powershell_literal if os.name == "nt" else shlex.quote
return " ".join([host_command(argv[0]), *(quote(arg) for arg in argv[1:])])


def attach_settlement_progress(
payload: dict[str, Any],
readback: QuotaSettlementReadback,
Expand All @@ -196,7 +205,8 @@ def attach_settlement_progress(
identity = readback.identity.value
if payload.get("ok") is not True or progress.get("next_step") != "quota_spend" or identity is None:
return
prefix = shlex.join(loopx_cli_argv())
route = loopx_cli_argv()
prefix = shlex.join(route)
if registry_path is not None:
prefix += f" --registry {shlex.quote(str(registry_path))}"
if runtime_root is not None:
Expand All @@ -213,16 +223,23 @@ def attach_settlement_progress(
quota_spend_source=progress["quota_spend_source"],
goal_ref=goal_ref,
)
payload["settlement_owed"] = {
command = settlement_step_command(plan.as_dict(), SettlementStepKind.QUOTA_SPEND)
owed = {
**identity.as_dict(), "schema_version": "turn_settlement_owed_v0",
"kind": "quota_spend", "recovery_does_not_spend": True,
"reason": (
"quota spend is committed but its receipt is missing; retry the same identity to repair it without another debit"
if progress["state"] == "spend_receipt_required" else
"writeback is verified; execute quota spend once for the same settlement identity"
),
"command": settlement_step_command(plan.as_dict(), SettlementStepKind.QUOTA_SPEND),
"command": command,
}
# PowerShell quoting applies to every argument, even with an unquoted launcher.
# Keep the argv for the composed CLI so it does not have to parse shell text.
if os.name == "nt":
argv = shlex.split(command)
owed.update(command=render_owed_host_command(argv), command_argv=argv)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 实际 PowerShell 7.7.0 对照:相对 ./bin/loopx.ps1 与已加引号的 launcher 都无法执行含 C:\Users\O’Neil\registry.json 的 emitted command(exit 1,启动标记未出现);将同一 argv 的弯单引号正确转义后,参数原样到达且现有 CLI parser 接受。 在既有 PowerShell 参数及可执行文件引号工具里把弯单引号与 ASCII 单引号统一安全转义;在真实 PowerShell 下验证相对 launcher 和带弯单引号的路径,保留原 identity 与重放对照。

payload["settlement_owed"] = owed


def render_settlement_progress_markdown(payload: dict[str, Any]) -> list[str]:
Expand Down
4 changes: 2 additions & 2 deletions loopx/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
)
from .control_plane.runtime.time import chronology_key
from .install_contract import (
_powershell_literal,
NO_CLONE_INSTALL_URL,
local_install_command,
no_clone_upgrade_command,
powershell_literal,
)
from .paths import configured_runtime_route, default_runtime_route, global_registry_path
from .python_install_owner import (
Expand Down Expand Up @@ -774,7 +774,7 @@ def collect_doctor(
skills = inspect_skill_facades(skill_roots[0])
project_skill = skills["loopx"]
home_arg = (
_powershell_literal(selected_zcode_home)
powershell_literal(selected_zcode_home)
if os.name == "nt"
else shlex.quote(str(selected_zcode_home))
)
Expand Down
25 changes: 15 additions & 10 deletions loopx/install_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,23 @@


_POWERSHELL_SAFE_COMMAND = re.compile(r"^[A-Za-z0-9_./-]+$")
# PowerShell also treats U+2018 through U+201B as single-quote delimiters.
_POWERSHELL_SINGLE_QUOTE_ESCAPES = str.maketrans(
{quote: quote * 2 for quote in "'\u2018\u2019\u201a\u201b"}
)


def _powershell_literal(value: str | Path) -> str:
return "'" + str(value).replace("'", "''") + "'"
def powershell_literal(value: str | Path) -> str:
"""Render a verbatim PowerShell string while preserving the original characters."""
return "'" + str(value).translate(_POWERSHELL_SINGLE_QUOTE_ESCAPES) + "'"


def _powershell_command(value: str) -> str:
"""A command name or path PowerShell can invoke from the start of a pipeline."""

if _POWERSHELL_SAFE_COMMAND.fullmatch(value):
return value
return f"& {_powershell_literal(value)}"
return f"& {powershell_literal(value)}"


def host_command_name(value: str) -> str:
Expand Down Expand Up @@ -80,7 +85,7 @@ def _checkout_snapshot_installer(repo_root: Path) -> Path | None:
def _windows_pypi_upgrade_command(*, doctor_agent_arg: str, skip_skills: bool) -> str:
"""No-clone upgrade through the interpreter that owns this installation."""

lines = [f"& {_powershell_literal(sys.executable)} -m pip install --upgrade loopx"]
lines = [f"& {powershell_literal(sys.executable)} -m pip install --upgrade loopx"]
if not skip_skills:
lines.append(DEFAULT_WORKFLOW_SKILL_INSTALL_COMMAND)
lines.append(f"loopx doctor{doctor_agent_arg}")
Expand All @@ -91,8 +96,8 @@ def local_install_command(repo_root: Path, *, skip_skills: bool = False) -> str:
if _is_windows_host():
return (
"pwsh -NoLogo -NoProfile -File "
f"{_powershell_literal(repo_root / 'scripts' / 'install-windows.ps1')} "
f"-Python {_powershell_literal(sys.executable)}"
f"{powershell_literal(repo_root / 'scripts' / 'install-windows.ps1')} "
f"-Python {powershell_literal(sys.executable)}"
+ (" -SkipSkills" if skip_skills else "")
)
command = shlex.quote(str(repo_root / "scripts" / "install-local.sh"))
Expand All @@ -108,7 +113,7 @@ def no_clone_upgrade_command(
if _is_windows_host():
repo_root = Path(__file__).resolve().parents[1]
doctor_agent_arg = (
f" --agent-type {_powershell_literal(doctor_agent_type)}"
f" --agent-type {powershell_literal(doctor_agent_type)}"
if doctor_agent_type
else ""
)
Expand Down Expand Up @@ -169,13 +174,13 @@ def doctor_preflight_command(

if _is_windows_host():
doctor_arg = (
f" --agent-type {_powershell_literal(doctor_agent_type)}"
f" --agent-type {powershell_literal(doctor_agent_type)}"
if doctor_agent_type
else ""
)
install_lines = [
f"if (-not (Get-Command {_powershell_literal(cli_bin)} -ErrorAction SilentlyContinue)) {{",
f" & {_powershell_literal(sys.executable)} -m pip install --upgrade loopx",
f"if (-not (Get-Command {powershell_literal(cli_bin)} -ErrorAction SilentlyContinue)) {{",
f" & {powershell_literal(sys.executable)} -m pip install --upgrade loopx",
]
if install_skills:
install_lines.append(
Expand Down
2 changes: 1 addition & 1 deletion loopx/project_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def shell_arg_for_host(value: str) -> str:
"""Quote one argument for the shell that will run the generated command."""

if _is_windows_host() and not _SHELL_SAFE_ARGUMENT.fullmatch(value):
return "'" + value.replace("'", "''") + "'"
return install_contract.powershell_literal(value)
return shell_arg(value)


Expand Down
32 changes: 32 additions & 0 deletions scripts/ci/test_windows_lane_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
"tests/control_plane_ts/task_lease_workspace.test.ts", # #7805
)

SETTLEMENT_RECOVERY_TESTS = (
"tests/control_plane/test_refresh_settle_turn.py::test_wrong_or_missing_child_workspace_retains_owed_command_for_exact_recovery",
"tests/control_plane/test_refresh_settle_turn.py::test_legacy_posix_quoted_powershell_launcher_fails_before_settlement",
"tests/control_plane/test_refresh_settle_turn.py::test_pinned_powershell_launcher_settles_original_turn_once",
)


def workflow() -> dict:
return yaml.load(WORKFLOW.read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
Expand Down Expand Up @@ -111,6 +117,32 @@ def test_windows_qualified_suites_are_unconditionally_executed_in_the_lane(self)
self.assertNotIn("<<", step, "the host-qualified suites step must not use YAML merge keys")
self.assertNotIn("NODE_OPTIONS", step.get("env") or {}, NODE_OPTION_DENIAL)

def test_native_settlement_recovery_runs_on_qualified_sqlite_then_restores_node(self):
steps = windows_lane()["steps"]
by_name = {step.get("name"): (index, step) for index, step in enumerate(steps)}
setup_index, setup = by_name["Set up the qualified SQLite runtime for settlement recovery"]
test_index, recovery = by_name[
"Qualify native PowerShell settlement recovery and its immutable baseline"
]
restore_index, restore = by_name["Restore the primary Windows Node runtime"]
self.assertLess(setup_index, test_index)
self.assertLess(test_index, restore_index)
self.assertEqual(setup["uses"], "actions/setup-node@v6")
self.assertEqual(setup["with"]["node-version"], "22.22.3")
self.assertEqual(restore["uses"], "actions/setup-node@v6")
self.assertEqual(restore["with"]["node-version"], "24")
self.assertIn("--basetemp", recovery["run"])
for test in SETTLEMENT_RECOVERY_TESTS:
with self.subTest(test=test):
self.assertIn(test, recovery["run"])
for step in (setup, recovery, restore):
with self.subTest(step=step.get("name")):
self.assertNotIn("if", step, "settlement recovery qualification must not be skipped")
self.assertNotIn(
"continue-on-error", step,
"settlement recovery qualification must block the Windows lane",
)


if __name__ == "__main__":
unittest.main()
Loading
Loading