fix(guard): split on & and unwrap subshells so real daemon kills classify (Closes #787)

`runtime_recovery_guard._SEGMENT_SPLIT_RE` claimed to split a compound command
line "on shell separators", but omitted the background-job separator `&`, and no
caller stripped a subshell wrapper before tokenising. `_analyse_kill_segment`
only inspects the first command token of a segment, so in both forms the kill
verb never reached the classifier.

Measured against the previous implementation at 35e94e10:

    sleep 1 & pkill -f mcp_server.py   ->  process_kill False, contamination False
    (pkill -f mcp_server.py)           ->  process_kill False, contamination False

Changes:

- Add `&` to `_SEGMENT_SPLIT_RE` as `(?:\|\||&&|[|;&\n])`; the two-character
  logical forms stay first so `&&` and `||` are consumed whole.
- Add `_strip_subshell()` and apply it in `_split_segments()`, removing leading
  `(` and trailing `)` (including nested wrappers) before tokenising.

Both forms now classify as contamination with reason class `manual_daemon_kill`,
and the record tool writes a durable marker for each. Direct-command behaviour is
unchanged. The contamination model, gated-task set, marker lifecycle, and
reconciler-only clearing path are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-21 19:56:00 -05:00
co-authored by Claude Opus 4.8
parent 35e94e107c
commit 6b58f04d39
2 changed files with 103 additions and 3 deletions
+23 -3
View File
@@ -86,8 +86,12 @@ REMEDIATION = (
# Split a compound command line into simple commands on shell separators so
# ``ps aux | grep mcp_server`` is analysed segment by segment and its harmless
# inspection half never reaches the kill classifier.
_SEGMENT_SPLIT_RE = re.compile(r"(?:\|\||&&|\||;|\n)")
# inspection half never reaches the kill classifier. The background separator
# ``&`` is a separator too: without it ``sleep 1 & pkill -f mcp_server.py`` was
# a single segment whose command position held ``sleep``, so the kill was never
# classified (#787). The two-character logical forms are listed first so ``&&``
# and ``||`` are consumed whole rather than split into single characters.
_SEGMENT_SPLIT_RE = re.compile(r"(?:\|\||&&|[|;&\n])")
_KILL_VERBS = frozenset({"kill", "pkill", "killall"})
@@ -133,8 +137,24 @@ def _clean(value: str | None) -> str:
return (value or "").strip()
def _strip_subshell(segment: str) -> str:
"""Remove subshell wrappers so ``(pkill -f mcp_server.py)`` is classified.
The parentheses are shell syntax, not part of the simple command, so a
wrapped kill otherwise put ``(pkill`` in command position and never
reached the kill classifier (#787). Nested wrappers are unwrapped too.
"""
stripped = segment.strip()
while stripped.startswith("("):
stripped = stripped[1:].strip()
while stripped.endswith(")"):
stripped = stripped[:-1].strip()
return stripped
def _split_segments(command: str) -> list[str]:
return [seg for seg in _SEGMENT_SPLIT_RE.split(command) if seg.strip()]
segments = (_strip_subshell(seg) for seg in _SEGMENT_SPLIT_RE.split(command))
return [seg for seg in segments if seg]
def is_sanctioned_recovery(text: str | None) -> bool: