|
| Iterator[None] | human_confirmed_write (str target) |
| bool | _plan_envelope_covers_write (str target) |
| dict | _make_protected_write_block (str target, str reason) |
| dict | deny_protected_write (dict result, *, str note="") |
| "Path | dict" | _enforce_write_gates (str path) |
| Path | _safe_path (str path) |
| Path|None | _find_worktree_root (Path|None start=None) |
| tuple[Path, str] | _sandbox_root_info () |
| Path | _sandbox_root () |
| "tuple[Path, Path]" | _resolve_unclamped (str path) |
| "str | None" | _outside_sandbox_error (str path) |
| bool | _was_discovery_rescue (str requested, Path resolved) |
| Path | _discover_file_path (str path) |
| Path | _discover_dir_path (str path) |
| dict | list_dir (str path=".", int max_depth=2, bool exclude_hidden=True) |
| ReadFileResult|ToolError | read_file (str path, int max_bytes=50_000, int offset=0) |
| str | _normalize_newlines (str content) |
| WriteFileResult|ToolError | write_file (str path, str content, int chunk_size=0, int max_chunks=0) |
| list[int] | _match_start_lines (str text, str anchor) |
| str | _edit_near_miss_message (str text, str anchor, str path) |
| str | _edit_context_snippet (str edited_text, str replacement, int start_line, int radius=2) |
| None | _atomic_write_bytes (Path target, bytes data) |
| dict | edit_file (str path, str old_string, str new_string, int expected_count=1) |
| dict | file_info (str path) |
| dict | read_chunks (str path, int chunk_size=10_000, int max_chunks=10, int offset=0) |
| dict | search_files (str root_dir=".", str pattern="*") |
| str | read_prompt_file (str filepath, *, bool allow_absolute=False) |
Filesystem tools — all operations are clamped to the working directory.
Purpose
-------
Provides :func:`list_dir`, :func:`read_file`, :func:`write_file`,
:func:`edit_file`, :func:`read_chunks`, and :func:`search_files` as both
Python callables and OpenAI tool schemas. Every path is resolved relative
to the project root and validated to prevent escaping the sandbox.
Responsibilities
----------------
- :func:`_safe_path` — resolve and reject paths that escape ``ROOT``.
- :func:`_enforce_write_gates` — the single shared write-side M8 gate
(sandbox clamp, hard-deny protected-write, human-confirm tier) both
:func:`write_file` and :func:`edit_file` route through.
- :func:`list_dir` — recursive directory walk with depth limit and
hidden-file filter.
- :func:`read_file`, :func:`read_chunks` — UTF-8 reads with offset
pagination and natural-boundary lookahead.
- :func:`write_file` — whole-file overwrite/create with informational
chunk metadata.
- :func:`edit_file` — fail-closed anchored partial edit (exact
``old_string`` → ``new_string``); atomic, newline-preserving; the
primary modify path (plan 171 / F20).
- :func:`search_files` — recursive grep over text files.
- Module-level ``TOOLS`` (callables) and ``SCHEMAS`` (OpenAI shapes)
for auto-registration.
Diagnostics
-----------
Domain: OC_CHAT.TOOLS
Levels:
L2 — lifecycle events
L3 — semantic details
L4 — deep tracing
Contracts
---------
- All paths are validated by :func:`_safe_path`; escapes raise
:class:`PermissionError` rather than silently allowing access.
- Reads/writes use UTF-8 with ``errors="replace"``; binary files are
rejected.
- ``read_chunks`` clamps ``offset`` to ``[0, total_chars]`` and never
reads past EOF.
- ``write_file`` chunk slicing is character-based (not byte-based)
to handle multi-byte UTF-8 correctly.
Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_chat.core.tool_results (ReadFileResult, WriteFileResult and ToolError typed return shapes)
- oc_chat.tools._sandbox (sandbox clamp, protected read and write classes, human-confirm tier and secret redaction)
- oc_chat.core.cancellation (lazy; cooperative Ctrl+C check in list_dir and search_files)
- oc_chat.core.resource_caps (lazy; clamps the model-supplied depth, byte and chunk limits)
See Also
--------
oc_chat/docs/conventions/CONTRACTS.md §3 Tool registry contract; §4 Tool return-
value conventions (read_file → §4.2, write_file → §4.3).
| "Path | dict" oc_chat.tools.filesystem._enforce_write_gates |
( |
str | path | ) |
|
|
protected |
Resolve *path* through the shared write-side M8 gate; return the target or a block.
The SINGLE enforcement path every write-class tool routes through
(:func:`write_file`, :func:`edit_file`) — one policy, never a parallel path.
In order (M8.0 WU-5 / WU-8; §5.1.2):
1. Hard-deny the protected-write class BEFORE the sandbox clamp (``.git``
internals, the protected-read secret class, ``*.pth`` / ``sitecustomize`` /
``conftest`` import-exec hooks, the clean-exit marker).
2. Clamp to the effective sandbox root (a ``..`` / sibling-prefix / symlink
escape is refused).
3. Re-check the RESOLVED target (catches a ``..`` / symlink into a ``.git``
dir the literal string hid).
4. The human-confirm protected-write TIER (config-as-code + the importable
tree): the ``_human_approved_write`` ContextVar authorizes EXACTLY the
approved resolved target on a react-loop re-dispatch (a divergence fails
closed); otherwise a ``requires_confirmation`` block is returned (the
react-loop relays a y/N; headless fails closed, F70a).
Returns the resolved absolute :class:`~pathlib.Path` on success, or a
``{"error": ...}`` / ``{"requires_confirmation": ...}`` dict on denial / relay.
Definition at line 147 of file filesystem.py.
| "str | None" oc_chat.tools.filesystem._outside_sandbox_error |
( |
str | path | ) |
|
|
protected |
A boundary-truthful error for an out-of-sandbox *path* (F25), or ``None``.
When *path* resolves OUTSIDE the effective sandbox root, return a message that
NAMES the root + the resolved absolute path and says whether it exists on disk,
so the model can tell "exists where you cannot reach" from "does not exist"
instead of reading every boundary denial as nonexistence (the F25 confusion that
produced the factually-wrong "the worktree lacks oc_diagnostics" report claim).
M8 basis (design foundation V1.1): the sandbox clamps REACH — every filesystem op
resolves through ``require_inside_sandbox(path, effective_root)`` (§5.1.2) — not
knowledge; the T1 tier has "no determined in-process adversary" and a git-recoverable
local blast radius (§3.2). SECRET existence/paths are withheld by the F46 protected-read
class, which every caller checks FIRST; defensively, this returns ``None`` (disclose
nothing, fall through to not-found) when the resolved out-of-bounds target is itself in
the protected-read class (a symlink-hidden secret). No content is read; the existence
probe is a bare ``os.path.exists`` on the ALREADY-RESOLVED path — no further
out-of-bounds symlink following.
Returns ``None`` for an in-bounds path (not a boundary case).
Definition at line 265 of file filesystem.py.
| "tuple[Path, Path]" oc_chat.tools.filesystem._resolve_unclamped |
( |
str | path | ) |
|
|
protected |
Resolve *path* against the effective sandbox root WITHOUT clamping.
Returns ``(abs_root, abs_candidate)`` — the resolved sandbox root and the
resolved absolute path *path* names, whether or not it stays inside the root.
A relative *path* is joined to the root (matching :func:`require_inside_sandbox`);
symlinks / ``..`` are resolved via ``Path.resolve(strict=False)`` so the file
need not exist. The counterpart of the clamped :func:`_safe_path`, used only to
build a truthful boundary error (:func:`_outside_sandbox_error`).
Definition at line 248 of file filesystem.py.
| dict oc_chat.tools.filesystem.edit_file |
( |
str | path, |
|
|
str | old_string, |
|
|
str | new_string, |
|
|
int | expected_count = 1 ) |
Replace an exact ``old_string`` with ``new_string`` in an EXISTING file.
The anchored-edit primary modify path (plan 171 / F20): the tool-call payload
is the anchor + replacement, not the whole file, so it does not hit the
large-write provider-truncation class. Fail-closed always — ``old_string`` must
occur exactly *expected_count* times (default 1) or nothing is written:
* 0 matches → an error naming the closest near-miss line (first divergence) so
the model can re-anchor.
* more than *expected_count* → an error listing the occurrence line numbers; no
write. (Set ``expected_count`` to the reported count to replace them all.)
* ``old_string == new_string`` or an empty ``old_string`` → error (this tool
never creates files; ``write_file`` owns creation).
Routes through the SAME :func:`_enforce_write_gates` policy as ``write_file``
(sandbox clamp, hard-deny protected-write, human-confirm tier). The write is
atomic (temp + ``os.replace``) and preserves the file's existing newline style
and UTF-8 encoding; binary files are refused. The result echoes the RESOLVED
absolute path + the match line number(s) + a ±2-line context snippet of the
applied change.
Args:
path: File path (relative to the working directory) to edit in place.
old_string: The exact text to find (anchor). Must match the file byte-for-byte
after newline normalization; include enough surrounding context to be unique.
new_string: The replacement text.
expected_count: Required number of occurrences of ``old_string`` (default 1).
Definition at line 757 of file filesystem.py.
Read a text file within the working directory with pagination support.
Returns ``{"content": ..., "truncated": bool, "total_size": int, "offset": int,
"next_offset": int|None, "lines_count": int}`` or ``{"error": ...}``.
Args:
path: File path relative to working directory
max_bytes: Maximum bytes to read per request (default 50_000 to avoid API truncation)
offset: Byte offset to start reading from (default 0)
Definition at line 495 of file filesystem.py.
| str oc_chat.tools.filesystem.read_prompt_file |
( |
str | filepath, |
|
|
* | , |
|
|
bool | allow_absolute = False ) |
Read a file destined for the provider-bound prompt, with M8.0 WU-2b (F73)
enforcement.
Prompt-composition reads (``system_prompt_file``, ``include_files``) happen
before the tool gate and their content egresses to the model provider, so
they must honour the same floor as the read tools:
* **No absolute paths** -- there is no out-of-band allowlist yet, so
``allow_absolute`` is effectively always ``False`` (the seam for a future
OOB-allow; F73 "unless OOB-allowed").
* **Clamp to the effective sandbox root** (``_sandbox_root()``); a ``..`` /
sibling-prefix / symlink escape is refused.
* **Deny the protected-read (secret) class** on the final resolved path.
* **Redact secret patterns** from the returned content (defence in depth).
Returns the file's text, stripped + redacted. Raises :class:`PermissionError`
on a policy denial (absolute, sandbox escape, or protected-read) and
:class:`FileNotFoundError` when the file is missing or empty -- the callers
fall back to the built-in default / skip the include, so a denied secret is
never inserted into the prompt.
Definition at line 1053 of file filesystem.py.