diff --git a/tools/approval.py b/tools/approval.py index 0b28fb772a..87d233a0cb 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -258,22 +258,22 @@ def _release_permission_mode_dependents(session_key: str) -> None: logger.debug("Failed to release permission-mode dependent resources for %s", session_key, exc_info=True) -def enable_session_yolo(session_key: str) -> None: - """Enable YOLO bypass for a single session key.""" +def _set_session_yolo(session_key: str, enabled: bool) -> None: if not session_key: return with _lock: - _session_yolo.add(session_key) + (_session_yolo.add if enabled else _session_yolo.discard)(session_key) _release_permission_mode_dependents(session_key) +def enable_session_yolo(session_key: str) -> None: + """Enable YOLO bypass for a single session key.""" + _set_session_yolo(session_key, True) + + def disable_session_yolo(session_key: str) -> None: """Disable YOLO bypass for a single session key.""" - if not session_key: - return - with _lock: - _session_yolo.discard(session_key) - _release_permission_mode_dependents(session_key) + _set_session_yolo(session_key, False) def clear_session(session_key: str) -> None: @@ -326,10 +326,8 @@ def is_approved(session_key: str, pattern_key: str) -> bool: regex-derived key so existing command_allowlist entries survive key migrations.""" aliases = _approval_key_aliases(pattern_key) with _lock: - if any(alias in _permanent_approved for alias in aliases): - return True - session_approvals = _session_approved.get(session_key, set()) - return any(alias in session_approvals for alias in aliases) + approved = _permanent_approved | _session_approved.get(session_key, set()) + return any(alias in approved for alias in aliases) def approve_permanent(pattern_key: str): @@ -349,10 +347,10 @@ def _persist_choice(session_key: str, choice: str, warnings: list[tuple]) -> Non findings are session-max by design (no broad permanent allowlisting of content-level findings), so ``always`` downgrades them to session. ``once`` persists nothing.""" for key, _, is_tirith in warnings: - if choice == "session" or (choice == "always" and is_tirith): - approve_session(session_key, key) - elif choice == "always": - approve_session(session_key, key) + if choice not in ("session", "always"): + continue + approve_session(session_key, key) + if choice == "always" and not is_tirith: approve_permanent(key) save_permanent_allowlist(_permanent_approved) diff --git a/tools/approval_detection.py b/tools/approval_detection.py index 9b98baf379..8ed08a8d8b 100644 --- a/tools/approval_detection.py +++ b/tools/approval_detection.py @@ -590,6 +590,8 @@ def _shell_tokens_with_spans(segment: str, start: int): if token_start is not None: flush(len(segment)) return tokens + + def _quoted_grep_pattern_spans(command: str) -> tuple[list[tuple[int, int]], bool]: """Structurally locate quoted grep PCRE operands -> (spans, malformed). On an ambiguous or malformed grep parse callers fail closed and use the original command: no text is hidden on @@ -667,6 +669,8 @@ def _grep_safe_detection_variant(command: str) -> tuple[str, bool]: if malformed or not spans: return command, malformed return _splice(command, [(start, end, " " * (end - start)) for start, end in spans]), False + + def _interpreter_family(executable: str) -> str | None: name = os.path.basename(executable).lower() return next((family for family, name_re in _INTERPRETER_NAME_RES if name_re.fullmatch(name)), None) @@ -724,6 +728,8 @@ def _interpreter_exec_flag(family: str, args: list[str]) -> str | None: return bundled skip_value = comparable in with_arg and not equals return None + + def _bash_exec_payload(args: list[str]) -> tuple[bool, str | None]: """Return whether Bash ``-c`` occurs and the command string it owns. Bash's O/o options consume the following argument even when they precede a later ``-c`` or @@ -821,7 +827,6 @@ def _skip_shell_whitespace(command: str, pos: int) -> int: return pos - def _scan_shell(text: str, start: int = 0, end: int | None = None, *, subst: str = "", brace: bool = False, stop_unterminated: bool = False, naive_backtick: bool = False): """Yield ``(kind, i, j, quote)`` lexical steps over ``text[start:end]`` without expanding. @@ -951,7 +956,6 @@ def _deobfuscate_shell_word_for_detection(word: str) -> str: return word - def _iter_shell_command_starts(command: str): starts = [0] @@ -1101,6 +1105,8 @@ def _is_verification_artifact_cleanup(command: str) -> bool: and os.path.dirname(os.path.realpath(operand)) == temp_dir and re.fullmatch(r"hermes-(?:verify|ad-hoc)-[A-Za-z0-9_.-]+", basename) is not None ) + + def _is_shell_token_spliced_gateway_lifecycle(command: str) -> bool: """Catch gateway-lifecycle verbs spelled with quote splicing. Backslash splicing (``kick\\start``) is undone by normalization, but quote splicing is not: