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:
Teknium
2026-08-16 20:55:45 -07:00
parent 42a96e7543
commit b2cc4ceef0
2 changed files with 62 additions and 0 deletions
+23
View File
@@ -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):