"""Write-side safety guards for write_file / patch. Every guard returns ``None`` when the write may proceed, else an error string the tool returns verbatim. Guards, in the order the tools apply them: ``_check_sensitive_path`` (hard deny), ``_check_binary_document_write``, ``_check_protected_instruction_write`` (ALWAYS ask), ``_check_approval_required_write`` (normal gate), ``_check_cross_profile_path`` (sandbox-mirror lost-work), ``_is_internal_file_tool_content``. ``_stale_overwrite_blocker`` (write_file only, under the per-path lock) refuses a whole-file overwrite of content this task never saw or that changed since. """ import fnmatch import os from pathlib import Path from agent.file_safety import get_nt_namespace_error from tools import file_state from tools.binary_extensions import ( has_binary_extension, has_opaque_document_extension, is_pdf_path, is_sqlite_sidecar, ) from tools.file_tools_paths import ( _expand_tilde, _resolve_path_for_task, _ssh_path_escapes_home, _terminal_env_type_for_task) from tools.file_tools_read_tracking import _has_full_write_baseline, _read_mtime_drifted # Prefixes matched after realpath. macOS: /private/var mirrors /var — block the # sensitive subtrees only; a blanket "/private/var/" refuses every temp-file # write because $TMPDIR, /tmp and /var/folders all realpath there. _SENSITIVE_PATH_PREFIXES = ( "/etc/", "/boot/", "/usr/lib/systemd/", "/private/etc/", "/private/var/db/", "/private/var/root/") _SENSITIVE_EXACT_PATHS = {"/var/run/docker.sock", "/run/docker.sock"} # NOTE: these four are a TEST-OVERRIDE surface, not a process cache. Both getters # resolve per call in production (below) because ``get_hermes_home()`` / # ``get_config_path()`` are per-turn contextvar-scoped: a multiplexed gateway # (``gateway.multiplex_profiles: true``) serves many profiles in one process, and # a process-wide memo would freeze whichever profile's home/config ran first — # making the protected-instruction gate and the ``config.yaml`` hard-block # order-dependent, up to letting a later profile rewrite its own ``config.yaml`` # the block exists to protect (#107327). A test can still pin a value by setting # the slot and its ``_loaded`` flag. _hermes_config_resolved: str | None = None _hermes_config_resolved_loaded = False _real_hermes_home_cached: str | None = None _real_hermes_home_loaded = False def _config_path_resolved() -> str: from hermes_cli.config import get_config_path return str(get_config_path().resolve()) def _hermes_home_real() -> str: from hermes_constants import get_hermes_home return os.path.realpath(str(get_hermes_home())) def _get_hermes_config_resolved() -> str | None: """Resolved absolute path of the Hermes config file for the ACTIVE profile. Resolved per call so it tracks the per-turn ``HERMES_HOME`` scope (#107327); a test may pin it via ``_hermes_config_resolved`` + ``_hermes_config_resolved_loaded``.""" if _hermes_config_resolved_loaded: return _hermes_config_resolved try: return _config_path_resolved() except Exception: # Resolver failure must stay bound to the ACTIVE profile's home, not the # subprocess HOME. ``_expand_tilde("~/...")`` follows the subprocess-HOME # contract, which under host ``auto`` mode can be the real/default user # home rather than the active multiplex ``HERMES_HOME`` — comparing # beta's ``config.yaml`` against the default/root config would let the # hard-block fail open on the exception path. Re-derive from the same # ``get_hermes_home()`` key the happy path uses, and substitute no # unrelated home if even that is gone (#107327 follow-up; PR #107335). try: from hermes_constants import get_hermes_home return str((Path(str(get_hermes_home())) / "config.yaml").resolve()) except Exception: return None def _get_real_hermes_home() -> str | None: """Realpath of the authoritative Hermes home for the ACTIVE profile. Resolved per call so it tracks the per-turn ``HERMES_HOME`` scope (#107327); a test may pin it via ``_real_hermes_home_cached`` + ``_real_hermes_home_loaded``. Consumers exempting a whole TREE want ``_hermes_exempt_homes()``: under a named profile this home is ``/profiles/`` and the root is exempt too.""" if _real_hermes_home_loaded: return _real_hermes_home_cached try: return _hermes_home_real() except Exception: # Same active-profile binding on the exception path (see # ``_get_hermes_config_resolved``): re-derive from ``get_hermes_home()`` # rather than ``_expand_tilde("~/.hermes")`` so the protected-instruction # exemption resolves against the active profile — not the subprocess / # default home — and substitute no unrelated home if the active security # path cannot be established (PR #107335). A ``None`` here fails closed at # the consumer: the ``~/.hermes`` exemption is skipped, so the gate runs. try: from hermes_constants import get_hermes_home return os.path.realpath(str(get_hermes_home())) except Exception: return None def _hermes_exempt_homes() -> tuple[str, ...]: """Realpaths of the Hermes home tree(s) the protected-instruction gate must stay out of: the ACTIVE profile's home, plus the Hermes ROOT when that home is a named profile (``/profiles/``). Exempting only the profile dir left the root's DIRECT files (LEDGER.md / MEMORY.md / SOUL.md / AGENTS.md ...) to the ``.hermes`` component rule, which gated them like a project-local ``/.hermes/config.yaml`` — fail-closed headless (#110630). They are the agent's own store, governed by their own guards, exactly like ``~/.hermes`` under the default profile. The root is added only when the shape really is a named profile (``named_profile_home``), so a coincidental ``profiles/`` dir elsewhere never exempts its parent; the home comes from the ACTIVE scope, never ``HERMES_HOME`` alone.""" home = _get_real_hermes_home() if not home: return () try: from hermes_constants import named_profile_home profile_home = named_profile_home(home) except Exception: profile_home = None if profile_home is None: return (home,) root = os.path.realpath(str(Path(str(profile_home)).parent.parent)) return (home, root) if root and root != home else (home,) def _resolved_or_raw(filepath: str, task_id: str) -> str: """Task-resolved path string, falling back to the raw input on resolution failure.""" try: return str(_resolve_path_for_task(filepath, task_id)) except (OSError, ValueError): return filepath def _check_sensitive_path(filepath: str, task_id: str = "default") -> str | None: """Return an error message if the path targets a sensitive system location.""" # NT/device-namespace guard on the RAW string, BEFORE the task-base join: # on POSIX a leading "\??\" reads as a relative segment and gets anchored # under the base dir, hiding the prefix from the resolved-path checks, # while the same string relayed to a Windows host (remote backend, desktop # bridge) triggers the NTLM-leak vector. See agent/file_safety.py. nt_err = get_nt_namespace_error(filepath, verb="Write") if nt_err: return nt_err candidates = (_resolved_or_raw(filepath, task_id), os.path.normpath(_expand_tilde(filepath))) if _ssh_path_escapes_home(candidates[0]) and _terminal_env_type_for_task(task_id) == "ssh": return ( f"Refusing to write to {filepath}: it climbs above the SSH home and the remote " "home could not be detected, so its target cannot be checked. Pass an absolute path.") if any(c.startswith(_SENSITIVE_PATH_PREFIXES) or c in _SENSITIVE_EXACT_PATHS for c in candidates): return ( f"Refusing to write to sensitive system path: {filepath}\n" "Use the terminal tool with sudo if you need to modify system files.") # approvals.mode and other security settings live in config.yaml; a # prompt-injected agent could silently disable exec approval by editing it. hermes_config = _get_hermes_config_resolved() if hermes_config and hermes_config in candidates: return ( f"Refusing to write to Hermes config file: {filepath}\n" "Agent cannot modify security-sensitive configuration. " "Edit ~/.hermes/config.yaml directly or use 'hermes config' instead.") return None # ── Protected agent-instruction files (always-ask approval gate) ───────── # Files that steer FUTURE agent behavior are a prompt-injection persistence # vector (AGENTS.md / CLAUDE.md / SOUL.md / .cursorrules / project .hermes tree). # Writes ALWAYS require human approval — even under --yolo — and fail closed # without a human channel. Basenames match in ANY directory, case-insensitively. # Ported from: RooCodeInc/Roo-Code RooProtectedController (Apache-2.0). Companion: the terminal-tool vector # is covered separately (#58631); this gate covers the write_file/patch vector. Symlink lesson from #41351: # always realpath before matching. Scope decision (documented): basenames match in ANY directory, because # project-context instruction files are loaded from cwd trees — an AGENTS.md anywhere the agent might later # run from is a live target. Basenames match case-insensitively so case-variant spellings on # case-insensitive filesystems (macOS/Windows) cannot slip past; on case-sensitive filesystems most loaders # probe common case variants too, so the stricter behavior is kept uniform. _PROTECTED_INSTRUCTION_BASENAMES = frozenset({ "agents.md", "claude.md", "soul.md", ".cursorrules"}) def _protected_instruction_config() -> tuple[bool, list[str]]: """Return ``(enabled, extra_patterns)`` from ``security.protected_instruction_files`` / ``security.protected_instruction_extra_patterns`` (fnmatch on basename). Config read failures keep the gate ON — fail-safe for a security boundary.""" try: from hermes_cli.config import load_config, cfg_get cfg = load_config() enabled = cfg_get(cfg, "security", "protected_instruction_files", default=True) extra = cfg_get(cfg, "security", "protected_instruction_extra_patterns", default=[]) except Exception: return True, [] if not isinstance(enabled, bool): enabled = True if not isinstance(extra, list): extra = [] return enabled, [str(p) for p in extra if p] def _protected_instruction_reason(filepath: str, task_id: str = "default", *, enabled: bool | None = None, extra_patterns: list[str] | None = None) -> str | None: """Return a short label when ``filepath`` targets a protected instruction file, else ``None``. Matches BOTH the normalized input and its realpath so no symlink direction escapes. Matching runs on BOTH the normalized input path and its realpath so neither a symlink pointing AT a protected file (#41351) nor a protected name that is itself a symlink escapes the gate. ``..`` traversal is neutralized by normpath/realpath before the basename compare. """ if enabled is None or extra_patterns is None: enabled, extra_patterns = _protected_instruction_config() if not enabled: return None normalized = os.path.normpath(_expand_tilde(filepath)) try: resolved = os.path.realpath(str(_resolve_path_for_task(filepath, task_id))) except (OSError, ValueError, RuntimeError): resolved = os.path.realpath(normalized) # ~/.hermes itself is governed by its own guards (config.yaml hard-block, # mirror guard, write_approval); this gate targets PROJECT-LOCAL files only. # Must run before the ``.hermes`` component rule, which would match the home. # ``_hermes_exempt_homes`` also covers the ROOT when the active home is a named # profile, so ~/.hermes/ cannot read as project-local ``.hermes`` config. for real_home in _hermes_exempt_homes(): if resolved == real_home or resolved.startswith(real_home + os.sep): return None for candidate in (normalized, resolved): base = os.path.basename(candidate) base_lower = base.lower() if base_lower in _PROTECTED_INSTRUCTION_BASENAMES or any( fnmatch.fnmatch(base_lower, pattern.lower()) for pattern in extra_patterns): return base # Project-local .hermes config dirs (/.hermes/config.yaml) steer # behavior too. Only the IMMEDIATE parent counts — matching any ancestor # would gate every write inside a checkout living under ~/.hermes. parts = candidate.replace("\\", "/").rstrip("/").split("/") if len(parts) >= 2 and parts[-2] == ".hermes": return candidate return None _APPROVAL_UNAVAILABLE = "requires approval but the approval subsystem is unavailable." _NO_HUMAN = "requires approval but no interactive user or gateway is present to approve it." def _request_protected_instruction_approval(reasons: list[str], task_id: str = "default") -> str | None: """Ask the human to approve a write to protected instruction file(s); ``None`` when approved. Deliberately NOT routed through ``_run_approval_gate`` (honors --yolo and allowlists): this gate is one-operation approval EVERY time, no persisted scope, fail-closed without a human channel. """ targets = ", ".join(dict.fromkeys(reasons)) description = ( f"Write to protected agent-instruction file(s): {targets}. " "These files steer future agent behavior; approval is always " "required (not bypassed by auto-approve).") display = f"" blocked = ( f"BLOCKED: write to protected agent-instruction file(s) ({targets}) " "{why} The user has NOT consented to this write. Do NOT retry it or " "attempt the same edit via another path (terminal, execute_code, " "etc.).") timed_out = blocked.format(why="approval prompt timed out without a user response. Silence is not consent.") denied = blocked.format(why="was denied by the user.") try: import tools.approval as _approval from tools.approval_context import get_current_session_key from tools.approval_gateway_wait import _await_gateway_decision from tools.approval_prompt import prompt_dangerous_approval except Exception: return blocked.format(why=_APPROVAL_UNAVAILABLE) # Gateway surface: block on the button round-trip when a notify callback # is registered for this session. One-operation only — no scope buttons. session_key = get_current_session_key() try: with _approval._lock: notify_cb = _approval._gateway_notify_cbs.get(session_key) except Exception: notify_cb = None if notify_cb is not None: approval_data = { "command": display, "pattern_key": "protected_instruction_file", "pattern_keys": ["protected_instruction_file"], "description": description, "allow_permanent": False, "allow_session": False} decision = _await_gateway_decision(session_key, notify_cb, approval_data, surface="gateway") if decision.get("notify_failed"): return blocked.format(why="requires approval but the approval request could not be delivered.") if decision.get("cancelled"): return blocked.format(why=f"approval was withdrawn before the user answered ({decision['cancelled']}).") choice, timed = decision.get("choice"), not decision.get("resolved") else: # CLI surface: per-thread approval callback (prompt_toolkit panel). try: from tools.terminal_tool import _get_approval_callback callback = _get_approval_callback() except Exception: callback = None if callback is None: # No human channel (script, cron, background thread): fail closed — # auto-approving here would recreate the persistence vector. return blocked.format(why=_NO_HUMAN) choice = prompt_dangerous_approval( display, description, allow_permanent=False, allow_session=False, approval_callback=callback) if choice == "cancelled": return blocked.format(why="approval prompt could not be delivered or was not answered " f"({getattr(choice, 'cause', 'no answer')}).") timed = choice == "timeout" # Any tapped scope is a one-operation grant; nothing is persisted. if not timed and choice in {"once", "session", "always"}: return None return timed_out if timed else denied def _check_protected_instruction_write(paths: list[str], task_id: str = "default") -> str | None: """Gate a write/patch touching protected instruction files. ONE protected file gates the ENTIRE multi-file patch (one prompt, all-or-nothing).""" enabled, extra = _protected_instruction_config() if not enabled: return None reasons = [r for r in (_protected_instruction_reason(p, task_id, enabled=enabled, extra_patterns=extra) for p in paths) if r] if not reasons: return None return _request_protected_instruction_approval(reasons, task_id) def _check_approval_required_write(paths: list[str], task_id: str = "default") -> str | None: """Gate a write/patch touching an approval-required path (``~/.ssh/config`` can steer execution via ``ProxyCommand``). Routine gate: once/session/always, honors --yolo, fail-closed without an interactive/gateway channel.""" try: from agent.file_safety import is_write_approval_required except Exception: return None targets = [p for p in paths if is_write_approval_required(p)] if not targets: return None display_targets = ", ".join(dict.fromkeys(targets)) description = ( f"Write to SSH client config file(s): {display_targets}. " "The SSH config can carry ProxyCommand / Match exec directives that " "run commands, so writes require your approval.") blocked = ( f"BLOCKED: write to SSH config file(s) ({display_targets}) " "{why} Do NOT retry it via another path (terminal, execute_code) " "without the user's explicit consent.") try: import tools.approval as _approval except Exception: return blocked.format(why=_APPROVAL_UNAVAILABLE) result = _approval._run_approval_gate( pattern_key="ssh_config_write", description=description, display_target=f"", cron_deny_message=blocked.format(why="requires approval but this cron session denies it."), single_query_deny_message=blocked.format( why="requires approval but single-query (-q) sessions run " "without a user present to approve it. To allow flagged " "actions in single-query mode, set approvals.single_query_mode: " "approve in config.yaml."), autoapprove_log_prefix="ssh_config_write", fail_closed_when_no_human=True, no_human_block_message=blocked.format(why=_NO_HUMAN)) if result.get("approved"): return None return result.get("message") or blocked.format(why="was denied.") def _get_container_mirror_prefix_for_task(task_id: str = "default") -> str | None: """Return the container-side Hermes mirror prefix for persistent Docker file tools.""" try: from tools.terminal_tool import ( _active_environments, _env_lock, _get_env_config, _resolve_container_task_id) container_key = _resolve_container_task_id(task_id) with _env_lock: env = _active_environments.get(container_key) or _active_environments.get(task_id) if env is not None: persistent_docker = (env.__class__.__name__ == "DockerEnvironment" and bool(getattr(env, "_persistent", False))) return "/root/.hermes" if persistent_docker else None config = _get_env_config() except Exception: return None if config.get("env_type") == "docker" and config.get("container_persistent", True): return "/root/.hermes" return None def _check_cross_profile_path(filepath: str, task_id: str = "default") -> str | None: """Soft-guard: warn when ``filepath`` lands on a host-side or Docker sandbox MIRROR of Hermes state (a write the host never reads). Not profile isolation — that guard was removed; ``cross_profile=True`` keeps bypassing this one for replay compat. Fails open.""" try: from agent.file_safety import get_container_mirror_warning, get_sandbox_mirror_warning except Exception: return None resolved = _resolved_or_raw(filepath, task_id) warning = get_sandbox_mirror_warning(resolved) if warning is not None: return warning return get_container_mirror_warning(resolved, mirror_prefix=_get_container_mirror_prefix_for_task(task_id)) def _target_regular_file_state(filepath: str, task_id: str = "default") -> str: """Is a REGULAR file at *filepath* present where the write will execute (the task's backend, not the controller's disk — #122662)? Returns ``"exists"``, ``"absent"`` or ``"unavailable"``. Host-backed envs keep ``Path.is_file`` semantics; anything not proven absent is ``"unavailable"`` and callers must fail closed. """ from tools.file_tools import _file_ops_uses_host_paths, _get_file_ops try: resolved = str(_resolve_path_for_task(filepath, task_id)) except Exception: resolved = None try: file_ops = _get_file_ops(task_id) except Exception: return "unavailable" if _file_ops_uses_host_paths(file_ops): # Host writes land on the resolved path, else today's HOST # ``_expand_tilde`` fallback — probe exactly that string. probe = _expand_tilde(filepath) if resolved is None else resolved try: return "exists" if Path(probe).is_file() else "absent" except OSError: return "absent" try: # Backend writes land on ``_expand_path(_resolved or path)``: the # backend's own home for a tilde fallback, never the host's. _size, status = file_ops._probe_regular_file(file_ops._expand_path(resolved or filepath)) except Exception: return "unavailable" if status in ("ok", "bad_size"): # bad_size: ``[ -f ]`` succeeded, only ``wc`` was unparseable. return "exists" if status in ("missing", "not_regular"): # not_regular: no REGULAR file at the path (dir/FIFO/dangling link) — # the same answer Path.is_file gives on the host. return "absent" return "unavailable" def _check_binary_document_write(filepath: str, task_id: str = "default") -> str | None: """Reject text-tool writes that would corrupt a binary document (read_file showed EXTRACTED text, so the model may write it back). Opaque document formats and SQLite sidecars (-wal/-shm/-journal) are always rejected; .pdf and every other BINARY_EXTENSIONS suffix only when OVERWRITING an existing file (raw PDF syntax is text-authorable and text fixtures named ``*.db`` exist). "Existing" is asked of the filesystem the write will hit — the task's backend, via ``_target_regular_file_state`` — never the controller's disk alone (#122662). ``read_file`` auto-extracts .docx/.xlsx/.pptx (and PDF, via anydoc) to readable text, so the model plausibly believes it holds the file's contents and tries to write the edited text back with write_file/patch. A plain-text write can never produce a valid OOXML/OLE/ODF container, so that write silently destroys the document (port of nearai/ironclaw#7109). """ ext = os.path.splitext(filepath)[1].lower() if has_opaque_document_extension(filepath): return ( f"Refusing to write plain text to binary document '{filepath}' ({ext}). " "A text write cannot produce a valid document container and would " "corrupt the file (read_file showed you EXTRACTED text, not the real " "bytes). Use the docx/xlsx/powerpoint skills or a library like " "python-docx/openpyxl/python-pptx via the terminal to create or edit " "this document.") # A -wal/-shm/-journal path is never a legitimate text target, even when # no sidecar exists yet: a checkpointed db has none on disk, and a garbage # WAL dropped next to a live database is picked up on the next open. if is_sqlite_sidecar(filepath): return ( f"Refusing to write plain text to binary SQLite sidecar '{filepath}' ({ext}). " "A -wal/-shm/-journal file holds raw database pages that SQLite " "reads on the next open; text there corrupts the database. Use the " "sqlite3 CLI or a SQLite library via the terminal to modify the " "database instead.") # Overwriting an existing binary (PDF, image, archive, SQLite db, ...) # with text destroys it — the model only ever saw extracted or mojibake # text. Creating a NEW file with such an extension stays allowed: raw PDF # syntax is text-authorable and text fixtures named ``*.db`` exist. pdf = is_pdf_path(filepath) if pdf or has_binary_extension(filepath): state = _target_regular_file_state(filepath, task_id) if state == "exists": if pdf: return ( f"Refusing to overwrite existing PDF '{filepath}' with plain text. " "read_file showed you EXTRACTED text, not the real bytes — writing " "text back would destroy the document. Use the pdf skill or a PDF " "library via the terminal to modify it. (Creating a NEW .pdf file " "is allowed.)") return ( f"Refusing to overwrite existing binary file '{filepath}' ({ext}) " "with plain text — read_file showed you extracted or mojibake " "text, not the real bytes, and writing text back would destroy " "the file. Use a binary-aware tool via the terminal to modify it " "(for SQLite databases, the sqlite3 CLI or a SQLite library). " "(Creating a NEW file with this extension is allowed.)") if state == "unavailable": # Fail closed: absence not proven on the filesystem the write would # hit, so proceeding could destroy a binary the guard never saw. return ( f"Refusing to write to '{filepath}': could not establish whether " "the target file already exists where this write would execute " "(the terminal environment may be starting, unreachable, or was " "removed). The file was NOT modified — retry once the environment " "is reachable.") return None # ── Internal display text must never be persisted as file content ──────── _READ_DEDUP_STATUS_MESSAGE = ( "File unchanged since last read. The content from " "the earlier read_file result in this conversation is " "still current — refer to that instead of re-reading.") def _stale_overwrite_blocker(filepath: str, resolved: str | None, task_id: str) -> str | None: """Reason write_file must NOT replace the existing file, else ``None``. Refuses BEFORE any disk mutation (the pre-#65604 warning arrived after the clobber): a sibling/external/partial-read staleness finding, or an existing file with no full-content baseline for this task (never read in full, read redacted, only patched). Net-new files, files this task fully read (in one page or by paging contiguously to the last line) or wrote, unresolvable paths and the file-state kill switch all let the write proceed. """ if file_state.guard_disabled(): return None stale = file_state.check_stale(task_id, resolved) if resolved else None if stale: return stale if _read_mtime_drifted(filepath, task_id): return ( f"{filepath} was modified since you last read it (external edit or " "concurrent agent). Re-read the file before writing.") if not resolved or _has_full_write_baseline(resolved, task_id): return None try: exists = Path(resolved).exists() except OSError: return None if not exists: return None return ( f"{resolved} exists but this task has not seen its full current content " "(never read, only patched, or only a redacted/partial view). Read the " "file — every page of it, if it needs offset/limit — or use patch for a " "targeted edit; a stale conversation copy must not overwrite the current " "disk content.") def _stale_write_refusal(filepath: str, reason: str, resolved: str | None = None) -> dict: """Model-facing refusal payload for write_file; ``stale_write_blocked`` lets callers tell it apart from I/O errors.""" result = { "error": ( f"Refusing to overwrite {filepath}: {reason} " "The file was NOT modified. Reload the current contents with read_file " "(every page, for a file that needs offset/limit), merge the requested " "change, then call write_file again. For small edits, prefer patch so " "existing unrelated changes are preserved."), "stale_write_blocked": True, "path": filepath, } if resolved: result["resolved_path"] = resolved return result def _is_internal_file_status_text(content: str) -> bool: """True when content is the read_file dedup status message, verbatim or lightly framed (contains the full message and is <=2x its length — a real file quoting it would be longer).""" if not isinstance(content, str): return False stripped = content.strip() return bool(stripped) and _READ_DEDUP_STATUS_MESSAGE in stripped and ( len(stripped) <= 2 * len(_READ_DEDUP_STATUS_MESSAGE)) def _looks_like_read_file_line_numbered_content(content: str) -> bool: """True for content dominated by read_file's ``LINE_NUM|CONTENT`` display (>=60% of non-empty lines are consecutive numbered lines; a lone ``1|value`` is allowed).""" if not isinstance(content, str): return False lines = [line for line in content.splitlines() if line.strip()] if len(lines) < 2: return False numbered: list[int] = [] for line in lines: prefix, sep, _rest = line.lstrip().partition("|") if sep and prefix.isdigit(): numbered.append(int(prefix)) if len(numbered) < 2 or len(numbered) / len(lines) < 0.6: return False consecutive_pairs = sum(1 for prev, current in zip(numbered, numbered[1:]) if current == prev + 1) return consecutive_pairs >= len(numbered) - 1 def _is_internal_file_tool_content(content: str) -> bool: """Return True when content is file-tool display text, not intended file bytes.""" return _is_internal_file_status_text(content) or _looks_like_read_file_line_numbered_content(content)