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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user