fix: explain the launchctl registration restriction without inventing KeepAlive
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""Diagnostics must distinguish policy rejection from inspected job properties."""
|
||||
|
||||
import json
|
||||
import plistlib
|
||||
|
||||
from tools.terminal_tool_guards import gateway_lifecycle_block
|
||||
|
||||
|
||||
def test_bootstrap_rejection_does_not_invent_keepalive(tmp_path, monkeypatch):
|
||||
from tools import process_registry
|
||||
|
||||
monkeypatch.setattr(process_registry, "_is_supervised_gateway_process", lambda: True)
|
||||
plist = tmp_path / "com.example.schedule.plist"
|
||||
plist.write_bytes(plistlib.dumps({
|
||||
"Label": "com.example.schedule",
|
||||
"ProgramArguments": ["/bin/true"],
|
||||
"RunAtLoad": False,
|
||||
"StartCalendarInterval": {"Hour": 9, "Minute": 0},
|
||||
}))
|
||||
blocked = gateway_lifecycle_block(
|
||||
command=f"launchctl bootstrap gui/501 {plist}",
|
||||
env=None, env_type="local", cwd=str(tmp_path), workdir=None,
|
||||
session_key="diagnostic-test",
|
||||
)
|
||||
assert blocked is not None
|
||||
result = json.loads(blocked)
|
||||
assert result["exit_code"] == 1
|
||||
assert "regardless of the job label" in result["error"]
|
||||
assert "does not inspect" in result["error"]
|
||||
assert "KeepAlive settings" in result["error"]
|
||||
assert "separate shell outside the gateway" in result["error"]
|
||||
@@ -213,10 +213,12 @@ def gateway_lifecycle_block(
|
||||
# oversized roots never reach shlex here.
|
||||
if lifecycle_scan_root_within_budget(command) and contains_launchctl_submit_command(command):
|
||||
return _blocked_json(
|
||||
"Blocked: launchctl submit/bootstrap registers a persistent "
|
||||
"KeepAlive job and is unsafe from inside the gateway process. "
|
||||
"Use Hermes cron for one-shot delayed work, or install an "
|
||||
"explicit LaunchAgent from a separate shell.",
|
||||
"Blocked: launchctl submit/bootstrap is restricted inside a supervised "
|
||||
"gateway regardless of the job label, to prevent indirect gateway "
|
||||
"restart loops. This guard does not inspect the job's KeepAlive settings "
|
||||
"or determine whether it is independent of Hermes. Perform authorized "
|
||||
"LaunchAgent maintenance from a separate shell outside the gateway, "
|
||||
"not by switching launchctl verbs to bypass this rejection.",
|
||||
"error",
|
||||
)
|
||||
guard_cwd_base = get_session_cwd(session_key)
|
||||
|
||||
@@ -93,6 +93,31 @@ YOLO mode disables **all** dangerous command safety checks for the session — *
|
||||
|
||||
For destructive session slash commands (`/clear`, `/new` / `/reset`, `/undo`, `/quit --delete` — `/exit --delete` is an alias), the CLI also prompts for confirmation before running them. See [Slash Commands — Confirmation prompts for destructive commands](../reference/slash-commands.md#confirmation-prompts-for-destructive-commands).
|
||||
|
||||
### Supervised-gateway lifecycle restriction
|
||||
|
||||
The terminal tool has a separate, non-overridable guard against stopping or
|
||||
restarting the gateway from inside its own supervised process. A self-restart can
|
||||
terminate the tool before it finishes and cause a supervisor/auto-resume loop.
|
||||
User approval, YOLO mode, and `force=True` do not bypass this guard.
|
||||
|
||||
On macOS, executed `launchctl submit` and `launchctl bootstrap` commands are
|
||||
restricted **regardless of the job label**. This is a conservative registration
|
||||
restriction intended to catch indirect restart helpers with neutral labels, not
|
||||
an inspection of the target plist. It also rejects independent scheduled jobs
|
||||
with `RunAtLoad=false` and no `KeepAlive` key; rejection does **not** establish that
|
||||
the job uses KeepAlive or controls Hermes.
|
||||
|
||||
For authorized LaunchAgent maintenance, use a separate shell outside the running
|
||||
gateway. Some independent `load`/`unload` commands currently pass the label-based
|
||||
checks, but that is not a target-verified exemption or a supported way to evade a
|
||||
`bootstrap` rejection. Read-only `launchctl print` is not a lifecycle operation.
|
||||
After external maintenance, distinguish the on-disk plist from the loaded job:
|
||||
validate the plist and read back the loaded schedule before reporting activation.
|
||||
|
||||
A tool rejection means the command did not execute through that tool call. An
|
||||
assistant declining to issue a call is a separate model decision; changing models
|
||||
does not change the terminal guard's policy.
|
||||
|
||||
### Hardline Blocklist (Always-On Floor)
|
||||
|
||||
Some commands are so catastrophic — irreversible filesystem wipes, fork bombs, direct block-device writes — that Hermes refuses to run them **regardless** of:
|
||||
|
||||
Reference in New Issue
Block a user