From 8871adaa38dffcf524ff547c2eb71ef0802df647 Mon Sep 17 00:00:00 2001 From: Tartar Date: Thu, 12 Nov 2026 15:24:25 +0800 Subject: [PATCH 1/5] fix(quota): render pinned Windows settlement commands for PowerShell Signed-off-by: Tartar --- docs/reference/protocols/turn-envelope-v0.md | 10 ++++ loopx/cli_commands/refresh_settlement.py | 12 ++++- loopx/control_plane/quota/settlement.py | 19 +++++-- .../control_plane/test_refresh_settle_turn.py | 49 ++++++++++++++++++- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 9474a88164..f05e51c931 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -943,6 +943,16 @@ identity, step conditions, authority and one-spend semantics are unchanged. 补结算命令保持一致。填充占位项后直接执行,检查退出码和 JSON `ok`;不再需要 手工在子命令后追加格式参数。直接 CLI 的默认格式、历史回执与结算权限均不变。 +On Windows, when a pinned launcher needs quoting, `settlement_owed.command` +uses PowerShell's call operator. Run it in PowerShell from the original +workspace. The same response carries `settlement_owed.command_argv` for direct +process invocation 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. diff --git a/loopx/cli_commands/refresh_settlement.py b/loopx/cli_commands/refresh_settlement.py index ec52549a7e..b5f1de7eb2 100644 --- a/loopx/cli_commands/refresh_settlement.py +++ b/loopx/cli_commands/refresh_settlement.py @@ -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, ) @@ -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 diff --git a/loopx/control_plane/quota/settlement.py b/loopx/control_plane/quota/settlement.py index 4ea55d529d..e717595c57 100644 --- a/loopx/control_plane/quota/settlement.py +++ b/loopx/control_plane/quota/settlement.py @@ -7,6 +7,7 @@ from typing import Any from ...command_invocation import loopx_cli_argv +from ...project_prompt import host_command, shell_arg_for_host from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result from ..settlement_driver import decode_settlement_result from .accounting_admission import quota_accounting_admission @@ -179,6 +180,10 @@ class QuotaSettlementReadback: successor_replan_pending: bool = False +def render_owed_host_command(argv: list[str]) -> str: + return " ".join([host_command(argv[0]), *(shell_arg_for_host(arg) for arg in argv[1:])]) + + def attach_settlement_progress( payload: dict[str, Any], readback: QuotaSettlementReadback, @@ -196,7 +201,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: @@ -213,7 +219,8 @@ 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": ( @@ -221,8 +228,14 @@ def attach_settlement_progress( 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, } + # A quoted PowerShell executable needs the call operator. Keep the argv + # separately in this case so the composed CLI can execute it without a shell. + if host_command(route[0]).startswith("& "): + argv = shlex.split(command) + owed.update(command=render_owed_host_command(argv), command_argv=argv) + payload["settlement_owed"] = owed def render_settlement_progress_markdown(payload: dict[str, Any]) -> list[str]: diff --git a/tests/control_plane/test_refresh_settle_turn.py b/tests/control_plane/test_refresh_settle_turn.py index bacacce249..803208fd8b 100644 --- a/tests/control_plane/test_refresh_settle_turn.py +++ b/tests/control_plane/test_refresh_settle_turn.py @@ -3,8 +3,10 @@ import json import copy +import os from pathlib import Path import shlex +import shutil import subprocess import sys @@ -12,7 +14,7 @@ from canonical_authority_fixture import isolate_sqlite_runtime from test_quota_settlement_cli import ( - AGENT_ID, GOAL_ID, TODO_ID, TURN_ID, _heartbeat_receipt_count, _initialize_fixture_authority, + AGENT_ID, GOAL_ID, REPO_ROOT, TODO_ID, TURN_ID, _heartbeat_receipt_count, _initialize_fixture_authority, _run_cli, _run_generated_cli, _spend_run_count, _write_fixture, ) @@ -421,6 +423,51 @@ def test_wrong_or_missing_child_workspace_retains_owed_command_for_exact_recover assert _spend_run_count(runtime) == 1 +@pytest.mark.skipif(os.name != "nt", reason="Exercises the native Windows PowerShell launcher") +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +@pytest.mark.parametrize("composed", [False, True]) +def test_pinned_powershell_launcher_settles_original_turn_once(tmp_path, monkeypatch, provider, composed): + powershell = shutil.which("pwsh") or shutil.which("powershell") + assert powershell is not None + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + _initialize_fixture_authority(project, runtime, provider=provider) + launcher = tmp_path / "candidate's launcher" / "loopx.ps1" + launcher.parent.mkdir() + python = sys.executable.replace("'", "''") + launcher.write_text(f"& '{python}' -m loopx.entrypoint @args\nexit $LASTEXITCODE\n", encoding="utf-8") + binding = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID] + rc, guard = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", *binding) + assert rc == 0, guard + identity = guard["heartbeat_receipt"]["settlement_identity"] + prefix = ["-File", str(launcher), "--cli-bin", str(launcher), "--format", "json", + "--registry", str(registry), "--runtime-root", str(runtime)] + refresh = ["refresh-state", *binding, "--classification", "verified", "--delivery-batch-scale", "implementation", + "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--no-global-sync", "--suppress-external-sinks"] + + def run(args): + process = subprocess.run([powershell, "-NoProfile", "-NonInteractive", *args], cwd=project, + env={**os.environ, "PYTHONPATH": str(REPO_ROOT)}, capture_output=True, timeout=60) + assert process.returncode == 0, (process.stdout, process.stderr) + result = json.loads(process.stdout) + assert result["ok"] and result["settlement_identity"] == identity + return result + + if composed: + action = [*prefix, *refresh, "--settle-turn"] + else: + owed = run([*prefix, *refresh])["settlement_owed"] + assert _spend_run_count(runtime) == 0 + action = ["-Command", owed["command"]] + first, replay = run(action), run(action) + if not composed: + assert owed["command_argv"][:3] == [str(launcher), "--cli-bin", str(launcher)] + assert first["settlement_progress"]["state"] == "settled" + assert replay["idempotent_replay"] + assert _spend_run_count(runtime) == 1 + + @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("finish_todo", [False, True]) def test_same_turn_progress_or_completion_and_replay_without_extra_guard( From 05cfe10e623a388081690d25193d8d3c51772e46 Mon Sep 17 00:00:00 2001 From: Tartar Date: Thu, 12 Nov 2026 17:07:55 +0800 Subject: [PATCH 2/5] test(ci): qualify native Windows settlement recovery Signed-off-by: Tartar --- .github/workflows/python-tests.yml | 19 ++++++ scripts/ci/test_windows_lane_coverage.py | 32 ++++++++++ .../control_plane/test_refresh_settle_turn.py | 64 +++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index c69cf05c75..2e9f6beac0 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -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 diff --git a/scripts/ci/test_windows_lane_coverage.py b/scripts/ci/test_windows_lane_coverage.py index cb456d0208..898558e0c7 100644 --- a/scripts/ci/test_windows_lane_coverage.py +++ b/scripts/ci/test_windows_lane_coverage.py @@ -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) @@ -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() diff --git a/tests/control_plane/test_refresh_settle_turn.py b/tests/control_plane/test_refresh_settle_turn.py index 803208fd8b..0d41b878b0 100644 --- a/tests/control_plane/test_refresh_settle_turn.py +++ b/tests/control_plane/test_refresh_settle_turn.py @@ -423,6 +423,70 @@ def test_wrong_or_missing_child_workspace_retains_owed_command_for_exact_recover assert _spend_run_count(runtime) == 1 +@pytest.mark.skipif(os.name != "nt", reason="Exercises the native Windows PowerShell launcher") +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_legacy_posix_quoted_powershell_launcher_fails_before_settlement( + tmp_path, monkeypatch, provider, +): + powershell = shutil.which("pwsh") or shutil.which("powershell") + assert powershell is not None + isolate_sqlite_runtime(tmp_path, monkeypatch) + project, runtime, registry = _write_fixture(tmp_path) + _initialize_fixture_authority(project, runtime, provider=provider) + marker = tmp_path / "launcher-started.txt" + launcher = tmp_path / "candidate's launcher" / "loopx.ps1" + launcher.parent.mkdir() + python = sys.executable.replace("'", "''") + marker_arg = str(marker).replace("'", "''") + launcher.write_text( + f"Set-Content -LiteralPath '{marker_arg}' -Value started\n" + f"& '{python}' -m loopx.entrypoint @args\n" + "exit $LASTEXITCODE\n", + encoding="utf-8", + ) + binding = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID] + rc, guard = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", *binding) + assert rc == 0, guard + identity = guard["heartbeat_receipt"]["settlement_identity"] + prefix = ["-File", str(launcher), "--cli-bin", str(launcher), "--format", "json", + "--registry", str(registry), "--runtime-root", str(runtime)] + refresh = ["refresh-state", *binding, "--classification", "verified", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--delivery-boundary", "in_flight_continuation", "--no-global-sync", + "--suppress-external-sinks"] + environment = {**os.environ, "PYTHONPATH": str(REPO_ROOT)} + prepared = subprocess.run( + [powershell, "-NoProfile", "-NonInteractive", *prefix, *refresh], cwd=project, + env=environment, capture_output=True, timeout=60, + ) + assert prepared.returncode == 0, (prepared.stdout, prepared.stderr) + owed = json.loads(prepared.stdout)["settlement_owed"] + assert marker.exists() and _spend_run_count(runtime) == 0 + marker.unlink() + + # Immutable baseline oracle: this is the exact POSIX rendering used by the + # parent commit before host-aware PowerShell rendering was introduced. + legacy_command = shlex.join(owed["command_argv"]) + legacy = subprocess.run( + [powershell, "-NoProfile", "-NonInteractive", "-Command", legacy_command], + cwd=project, env=environment, capture_output=True, timeout=60, + ) + assert legacy.returncode != 0, (legacy.stdout, legacy.stderr) + assert not marker.exists() and _spend_run_count(runtime) == 0 + + repaired = subprocess.run( + [powershell, "-NoProfile", "-NonInteractive", "-Command", owed["command"]], + cwd=project, env=environment, capture_output=True, timeout=60, + ) + assert repaired.returncode == 0, (repaired.stdout, repaired.stderr) + result = json.loads(repaired.stdout) + assert marker.exists() + assert result["settlement_progress"]["state"] == "settled" + assert result["settlement_identity"] == identity + assert _spend_run_count(runtime) == 1 + + @pytest.mark.skipif(os.name != "nt", reason="Exercises the native Windows PowerShell launcher") @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("composed", [False, True]) From 356e4e11f175f9858f9c75d3ad8677972cfb4e45 Mon Sep 17 00:00:00 2001 From: Tartar Date: Thu, 12 Nov 2026 23:40:48 +0800 Subject: [PATCH 3/5] fix(quota): render all owed Windows arguments for PowerShell Signed-off-by: Tartar --- docs/reference/protocols/turn-envelope-v0.md | 19 +++++++------- loopx/control_plane/quota/settlement.py | 7 ++--- .../control_plane/test_refresh_settle_turn.py | 26 ++++++++++++++----- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 1d6a57fed6..a325346986 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -956,15 +956,16 @@ identity, step conditions, authority and one-spend semantics are unchanged. 补结算命令保持一致。填充占位项后直接执行,检查退出码和 JSON `ok`;不再需要 手工在子命令后追加格式参数。直接 CLI 的默认格式、历史回执与结算权限均不变。 -On Windows, when a pinned launcher needs quoting, `settlement_owed.command` -uses PowerShell's call operator. Run it in PowerShell from the original -workspace. The same response carries `settlement_owed.command_argv` for direct -process invocation without parsing shell text. Both forms retain the launcher -pin and original settlement identity. - -中文:Windows 上指定的启动器需要加引号时,补结算命令会使用 PowerShell 的 -`&` 调用运算符。请在原工作目录执行。程序可用同一响应里的 `command_argv` -直接启动进程。两种形式均保留启动器和原结算身份。 +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. diff --git a/loopx/control_plane/quota/settlement.py b/loopx/control_plane/quota/settlement.py index e717595c57..98ec71b299 100644 --- a/loopx/control_plane/quota/settlement.py +++ b/loopx/control_plane/quota/settlement.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from dataclasses import dataclass +import os from pathlib import Path import shlex from typing import Any @@ -230,9 +231,9 @@ def attach_settlement_progress( ), "command": command, } - # A quoted PowerShell executable needs the call operator. Keep the argv - # separately in this case so the composed CLI can execute it without a shell. - if host_command(route[0]).startswith("& "): + # 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) payload["settlement_owed"] = owed diff --git a/tests/control_plane/test_refresh_settle_turn.py b/tests/control_plane/test_refresh_settle_turn.py index 0d41b878b0..bf224d483f 100644 --- a/tests/control_plane/test_refresh_settle_turn.py +++ b/tests/control_plane/test_refresh_settle_turn.py @@ -490,13 +490,23 @@ def test_legacy_posix_quoted_powershell_launcher_fails_before_settlement( @pytest.mark.skipif(os.name != "nt", reason="Exercises the native Windows PowerShell launcher") @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("composed", [False, True]) -def test_pinned_powershell_launcher_settles_original_turn_once(tmp_path, monkeypatch, provider, composed): +@pytest.mark.parametrize("relative_launcher", [False, True], ids=["quoted-launcher", "relative-launcher"]) +def test_pinned_powershell_launcher_settles_original_turn_once( + tmp_path, monkeypatch, provider, composed, relative_launcher, +): powershell = shutil.which("pwsh") or shutil.which("powershell") assert powershell is not None isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) + if relative_launcher: + # The executable needs no quoting, but a later argument still does. + registry = registry.rename(registry.with_name("operator's registry.json")) + launcher_arg = "./bin/loopx.ps1" + launcher = project / launcher_arg + else: + launcher = tmp_path / "candidate's launcher" / "loopx.ps1" + launcher_arg = str(launcher) _initialize_fixture_authority(project, runtime, provider=provider) - launcher = tmp_path / "candidate's launcher" / "loopx.ps1" launcher.parent.mkdir() python = sys.executable.replace("'", "''") launcher.write_text(f"& '{python}' -m loopx.entrypoint @args\nexit $LASTEXITCODE\n", encoding="utf-8") @@ -504,7 +514,7 @@ def test_pinned_powershell_launcher_settles_original_turn_once(tmp_path, monkeyp rc, guard = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", *binding) assert rc == 0, guard identity = guard["heartbeat_receipt"]["settlement_identity"] - prefix = ["-File", str(launcher), "--cli-bin", str(launcher), "--format", "json", + prefix = ["-File", launcher_arg, "--cli-bin", launcher_arg, "--format", "json", "--registry", str(registry), "--runtime-root", str(runtime)] refresh = ["refresh-state", *binding, "--classification", "verified", "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", @@ -524,10 +534,14 @@ def run(args): owed = run([*prefix, *refresh])["settlement_owed"] assert _spend_run_count(runtime) == 0 action = ["-Command", owed["command"]] - first, replay = run(action), run(action) - if not composed: - assert owed["command_argv"][:3] == [str(launcher), "--cli-bin", str(launcher)] + first = run(action) assert first["settlement_progress"]["state"] == "settled" + assert _spend_run_count(runtime) == 1 + if not composed: + argv = owed["command_argv"] + assert argv[:3] == [launcher_arg, "--cli-bin", launcher_arg] + assert argv[argv.index("--registry") + 1] == str(registry) + replay = run(action) assert replay["idempotent_replay"] assert _spend_run_count(runtime) == 1 From 6927981a2f29b7305dbda2772752acf77b34f60d Mon Sep 17 00:00:00 2001 From: Tartar Date: Fri, 13 Nov 2026 00:32:56 +0800 Subject: [PATCH 4/5] fix(windows): preserve typographic apostrophes in PowerShell commands Signed-off-by: Tartar --- loopx/doctor.py | 4 +-- loopx/install_contract.py | 25 +++++++++++-------- loopx/project_prompt.py | 2 +- .../control_plane/test_refresh_settle_turn.py | 6 ++--- tests/test_bootstrap_command_pack_platform.py | 11 +++++--- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/loopx/doctor.py b/loopx/doctor.py index 2b67287ab8..c187b57265 100644 --- a/loopx/doctor.py +++ b/loopx/doctor.py @@ -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 ( @@ -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)) ) diff --git a/loopx/install_contract.py b/loopx/install_contract.py index 66c734dbbe..3c85d53e19 100644 --- a/loopx/install_contract.py +++ b/loopx/install_contract.py @@ -34,10 +34,15 @@ _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: @@ -45,7 +50,7 @@ def _powershell_command(value: str) -> str: 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: @@ -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}") @@ -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")) @@ -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 "" ) @@ -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( diff --git a/loopx/project_prompt.py b/loopx/project_prompt.py index a96edde69b..de407238af 100644 --- a/loopx/project_prompt.py +++ b/loopx/project_prompt.py @@ -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) diff --git a/tests/control_plane/test_refresh_settle_turn.py b/tests/control_plane/test_refresh_settle_turn.py index bf224d483f..5e2a87393a 100644 --- a/tests/control_plane/test_refresh_settle_turn.py +++ b/tests/control_plane/test_refresh_settle_turn.py @@ -499,12 +499,12 @@ def test_pinned_powershell_launcher_settles_original_turn_once( isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) if relative_launcher: - # The executable needs no quoting, but a later argument still does. - registry = registry.rename(registry.with_name("operator's registry.json")) + # A later argument needs quoting even though the executable does not. + registry = registry.rename(registry.with_name("operator's O\u2019Neil registry.json")) launcher_arg = "./bin/loopx.ps1" launcher = project / launcher_arg else: - launcher = tmp_path / "candidate's launcher" / "loopx.ps1" + launcher = tmp_path / "candidate's O\u2019Neil launcher" / "loopx.ps1" launcher_arg = str(launcher) _initialize_fixture_authority(project, runtime, provider=provider) launcher.parent.mkdir() diff --git a/tests/test_bootstrap_command_pack_platform.py b/tests/test_bootstrap_command_pack_platform.py index 4acbf503ad..d717a6834b 100644 --- a/tests/test_bootstrap_command_pack_platform.py +++ b/tests/test_bootstrap_command_pack_platform.py @@ -98,17 +98,20 @@ def test_guided_bootstrap_commands_do_not_require_posix_continuations( ] +@pytest.mark.parametrize("quote", ["'", "\u2018", "\u2019", "\u201a", "\u201b"]) def test_host_shell_arg_escapes_powershell_apostrophes( - monkeypatch: pytest.MonkeyPatch, + monkeypatch: pytest.MonkeyPatch, quote: str, ) -> None: monkeypatch.setattr(project_prompt, "_is_windows_host", lambda: True) + monkeypatch.setattr(project_prompt.install_contract, "_is_windows_host", lambda: True) assert project_prompt.shell_arg_for_host("read_only_project_map_v0") == ( "read_only_project_map_v0" ) - assert project_prompt.shell_arg_for_host("Fix user's login") == ( - "'Fix user''s login'" - ) + path = f"C:\\O{quote}Neil\\loopx.ps1" + literal = f"'C:\\O{quote}{quote}Neil\\loopx.ps1'" + assert project_prompt.shell_arg_for_host(path) == literal + assert project_prompt.host_command(path) == f"& {literal}" def test_windows_guided_packet_quotes_every_executable_command( From 3a43c89e9008d32c897e30744a2b99a81800fd42 Mon Sep 17 00:00:00 2001 From: Tartar Date: Fri, 13 Nov 2026 06:35:11 +0800 Subject: [PATCH 5/5] fix(quota): pass recovery arguments as PowerShell strings Signed-off-by: Tartar --- loopx/control_plane/quota/settlement.py | 7 +++++-- tests/control_plane/test_refresh_settle_turn.py | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/loopx/control_plane/quota/settlement.py b/loopx/control_plane/quota/settlement.py index 98ec71b299..d8e1710326 100644 --- a/loopx/control_plane/quota/settlement.py +++ b/loopx/control_plane/quota/settlement.py @@ -8,7 +8,8 @@ from typing import Any from ...command_invocation import loopx_cli_argv -from ...project_prompt import host_command, shell_arg_for_host +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 @@ -182,7 +183,9 @@ class QuotaSettlementReadback: def render_owed_host_command(argv: list[str]) -> str: - return " ".join([host_command(argv[0]), *(shell_arg_for_host(arg) for arg in argv[1:])]) + # 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( diff --git a/tests/control_plane/test_refresh_settle_turn.py b/tests/control_plane/test_refresh_settle_turn.py index 5e2a87393a..25b76be1fa 100644 --- a/tests/control_plane/test_refresh_settle_turn.py +++ b/tests/control_plane/test_refresh_settle_turn.py @@ -494,7 +494,7 @@ def test_legacy_posix_quoted_powershell_launcher_fails_before_settlement( def test_pinned_powershell_launcher_settles_original_turn_once( tmp_path, monkeypatch, provider, composed, relative_launcher, ): - powershell = shutil.which("pwsh") or shutil.which("powershell") + powershell = shutil.which("pwsh") assert powershell is not None isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) @@ -508,8 +508,11 @@ def test_pinned_powershell_launcher_settles_original_turn_once( launcher_arg = str(launcher) _initialize_fixture_authority(project, runtime, provider=provider) launcher.parent.mkdir() - python = sys.executable.replace("'", "''") - launcher.write_text(f"& '{python}' -m loopx.entrypoint @args\nexit $LASTEXITCODE\n", encoding="utf-8") + shutil.copyfile(REPO_ROOT / "scripts/loopx.ps1", launcher) + pointer = tmp_path / "current-release.json" + pointer.write_text( + json.dumps({"release_root": str(REPO_ROOT), "python": sys.executable}), encoding="utf-8", + ) binding = ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", TURN_ID] rc, guard = _run_cli(registry, runtime, "quota", "should-run", "--codex-app", *binding) assert rc == 0, guard @@ -522,7 +525,7 @@ def test_pinned_powershell_launcher_settles_original_turn_once( def run(args): process = subprocess.run([powershell, "-NoProfile", "-NonInteractive", *args], cwd=project, - env={**os.environ, "PYTHONPATH": str(REPO_ROOT)}, capture_output=True, timeout=60) + env={**os.environ, "LOOPX_CURRENT_RELEASE_FILE": str(pointer)}, capture_output=True, timeout=60) assert process.returncode == 0, (process.stdout, process.stderr) result = json.loads(process.stdout) assert result["ok"] and result["settlement_identity"] == identity