fix(cron): attribute cloud-path refusals to the cloud-synced script, not a lifecycle command
When check_gateway_lifecycle refuses a cron script that lives on a FileProvider path, the generic error implied the job contained a dangerous gateway lifecycle command. Surface the real reason instead — the script lives on a cloud-synced path whose evicted placeholder could hang the preflight scan — while staying fail-closed. Regression test asserts the cron-script scan path blocks without opening the file and that the message names the cloud-synced path rather than a lifecycle command.
This commit is contained in:
@@ -746,6 +746,29 @@ def check_gateway_lifecycle(
|
||||
python_script = False
|
||||
if script:
|
||||
resolved_script = _resolve_script_path(script)
|
||||
if resolved_script is not None:
|
||||
try:
|
||||
real_script = resolved_script.resolve(strict=False)
|
||||
except (OSError, ValueError):
|
||||
real_script = resolved_script
|
||||
if _is_cloud_placeholder_path(resolved_script) or _is_cloud_placeholder_path(
|
||||
real_script
|
||||
):
|
||||
# Attribute the refusal correctly: the script is not known to
|
||||
# contain a lifecycle command — it lives on a cloud-synced
|
||||
# FileProvider path (iCloud Drive / ~/Library/CloudStorage)
|
||||
# that the guard refuses to open because an evicted
|
||||
# placeholder can hang preflight indefinitely (#88052).
|
||||
# Fail closed with the real reason instead of implying a
|
||||
# dangerous lifecycle command.
|
||||
raise GatewayLifecycleBlocked(
|
||||
"Blocked: the cron script lives on a cloud-synced path "
|
||||
"(iCloud Drive / ~/Library/CloudStorage). Opening an "
|
||||
"evicted FileProvider placeholder can hang the guard's "
|
||||
"preflight scan indefinitely, so it is refused without "
|
||||
"being read. Move the script to a local, non-cloud path "
|
||||
"(e.g. ~/.hermes/scripts/) and recreate the job."
|
||||
)
|
||||
python_script = resolved_script is not None and resolved_script.suffix == ".py"
|
||||
script_text = _read_script_for_scanning(script)
|
||||
if script_text:
|
||||
|
||||
@@ -858,6 +858,45 @@ class TestLifecycleGuardModule:
|
||||
assert text is None
|
||||
assert unsafe is True
|
||||
|
||||
def test_cron_script_scan_blocks_cloud_script_without_opening(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""The cron-script scan path (_read_script_for_scanning via
|
||||
check_gateway_lifecycle) must also refuse a cloud-resident script
|
||||
without opening it, and the surfaced reason must attribute the
|
||||
refusal to the cloud-synced path — not to a lifecycle command."""
|
||||
import cron.lifecycle_guard as lifecycle_guard
|
||||
from cron.lifecycle_guard import (
|
||||
GatewayLifecycleBlocked,
|
||||
check_gateway_lifecycle,
|
||||
)
|
||||
|
||||
cloud_dir = (
|
||||
tmp_path
|
||||
/ "Library"
|
||||
/ "Mobile Documents"
|
||||
/ "com~apple~CloudDocs"
|
||||
/ "scripts"
|
||||
)
|
||||
cloud_dir.mkdir(parents=True)
|
||||
script = cloud_dir / "nightly.sh"
|
||||
script.write_text("#!/bin/sh\necho safe\n", encoding="utf-8")
|
||||
|
||||
real_open = lifecycle_guard.os.open
|
||||
|
||||
def reject_cloud_open(path, flags, *args, **kwargs):
|
||||
if str(path) == str(script):
|
||||
pytest.fail("cron-script scan opened a cloud-resident script")
|
||||
return real_open(path, flags, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(lifecycle_guard.os, "open", reject_cloud_open)
|
||||
|
||||
with pytest.raises(GatewayLifecycleBlocked) as excinfo:
|
||||
check_gateway_lifecycle("nightly job", str(script))
|
||||
message = str(excinfo.value)
|
||||
assert "cloud-synced" in message
|
||||
assert "lifecycle command" not in message
|
||||
|
||||
# -- Whole-class regression tests (tilllt's T1-T4 on PR #79454) --------
|
||||
|
||||
def test_tilde_nul_candidate_does_not_crash_terminal_walk(self):
|
||||
|
||||
Reference in New Issue
Block a user