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.
|
# oversized roots never reach shlex here.
|
||||||
if lifecycle_scan_root_within_budget(command) and contains_launchctl_submit_command(command):
|
if lifecycle_scan_root_within_budget(command) and contains_launchctl_submit_command(command):
|
||||||
return _blocked_json(
|
return _blocked_json(
|
||||||
"Blocked: launchctl submit/bootstrap registers a persistent "
|
"Blocked: launchctl submit/bootstrap is restricted inside a supervised "
|
||||||
"KeepAlive job and is unsafe from inside the gateway process. "
|
"gateway regardless of the job label, to prevent indirect gateway "
|
||||||
"Use Hermes cron for one-shot delayed work, or install an "
|
"restart loops. This guard does not inspect the job's KeepAlive settings "
|
||||||
"explicit LaunchAgent from a separate shell.",
|
"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",
|
"error",
|
||||||
)
|
)
|
||||||
guard_cwd_base = get_session_cwd(session_key)
|
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).
|
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)
|
### 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:
|
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