oc_chat
Loading...
Searching...
No Matches
oc_chat.tools._sandbox Namespace Reference

Classes

class  SandboxViolation
class  LineRedactor

Functions

Path _resolve_default_root ()
None set_session_sandbox_root_override (str|Path|None path)
None install_resolved (Path|None resolved)
Path|None get_session_sandbox_root_override ()
bool is_path_inside_sandbox (str|Path path, Path|None root=None)
Path require_inside_sandbox (str|Path path, Path|None root=None)
str _norm_component (str name)
bool is_protected_read_path (str|Path path)
bool is_protected_write_path ("str | Path" path)
bool is_human_confirm_write_path ("str | Path" path)
bool is_importable_write_path ("str | Path" path)
str redact_secrets (str text)
"dict[str, str]" scrubbed_env ("dict[str, str] | None" extra=None)

Variables

Path DEFAULT_SANDBOX_ROOT = _resolve_default_root()
Path _SESSION_SANDBOX_ROOT_OVERRIDE = None
frozenset _PROTECTED_READ_NAMES
tuple _PROTECTED_READ_SUFFIXES = (".pem", ".key", ".pfx", ".p12")
frozenset _PROTECTED_READ_DIRS = frozenset({".ssh", ".gnupg", ".aws"})
tuple _SECRETY_STEMS
frozenset _PROTECTED_WRITE_NAMES
frozenset _LOCKFILE_NAMES
frozenset _STARTUP_SCRIPT_NAMES
str _SECRET_REDACTION_PLACEHOLDER = "[REDACTED-SECRET]"
str _PEM_REDACTION_PLACEHOLDER = "[REDACTED-PRIVATE-KEY]"
str _PEM_BEGIN = r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"
str _PEM_END = r"-----END [A-Z0-9 ]*PRIVATE KEY-----"
list _SECRET_PATTERNS
 _PEM_BEGIN_RE = re.compile(_PEM_BEGIN)
 _PEM_END_RE = re.compile(_PEM_END)

Detailed Description

Sandbox policy — path clamping (H.2).

Purpose
-------
Single source of truth for one H.2 policy:

1. **Path sandbox** (H.2a) — which filesystem locations may be written
   to by tool callers (LLM agents). Paths are clamped to a configurable
   ``OC_CHAT_SANDBOX_ROOT`` (or the current working directory by
   default). Symlinks and ``..`` escapes are resolved before the check
   via ``Path.resolve(strict=False)``, so a path that *resolves*
   outside the root is rejected even if the literal string looks safe.

Responsibilities
----------------
- :class:`SandboxViolation` — exception raised when a path resolves
  outside the configured sandbox root.
- :func:`is_path_inside_sandbox` — predicate; ``True`` if the resolved
  path is inside *root*.
- :func:`require_inside_sandbox` — resolve the path and return it;
  raise :class:`SandboxViolation` otherwise.
- :func:`redact_secrets` — whole-text secret redaction (M8 F46/F51).
- :class:`LineRedactor` — the same redaction applied LINE BY LINE, with the
  multi-line private-key block tracked across calls; one instance per output
  stream (plan 287).

Diagnostics
-----------
Domain: OC_CHAT.TOOLS
Levels:
    L2 — lifecycle events
    L3 — semantic details (sandbox violation)
    L4 — deep tracing

Contracts
---------
- :func:`is_path_inside_sandbox` and :func:`require_inside_sandbox`
  call ``Path.resolve(strict=False)`` so the path does not need to
  exist; this is required because write operations target paths that
  are about to be created.
- A relative input path is interpreted relative to *root*, not the
  process's current working directory. This matches how the historic
  :func:`oc_chat.tools.filesystem._safe_path` joins paths to
  ``ROOT``.
- The sandbox-root env-var default is re-read at call time, not import
  time, so ``monkeypatch.setenv`` works in tests.
- :class:`LineRedactor` adds state, never patterns: its BEGIN and END forms
  are the two halves the table's PEM entry is itself composed from, so the
  per-line filter and the whole-text one cannot drift. A caller holds ONE
  instance per stream; an instance is single-threaded by design.

Dependencies
------------
- oc_diagnostics (structured debug logging via ``_dbg``)
- oc_chat.core.deployment_shape (lazy; enumerate_importable_roots - the importable roots a .py write target is checked against)

Function Documentation

◆ _norm_component()

str oc_chat.tools._sandbox._norm_component ( str name)
protected
Normalize a single path component for protected-class matching.

Strips a Windows alternate-data-stream suffix (``.env::$DATA``) AND the
trailing dots/spaces that the Win32 layer removes at open time (so
``read_file(".env ")`` / ``write_file("conftest.py ")`` open the real file),
then casefolds. Over-stripping on POSIX only ever over-denies (safe).

Definition at line 209 of file _sandbox.py.

Here is the caller graph for this function:

◆ _resolve_default_root()

Path oc_chat.tools._sandbox._resolve_default_root ( )
protected
Return the default sandbox root, re-reading the env var each call.

Order of precedence:
1. ``OC_CHAT_SANDBOX_ROOT`` environment variable.
2. Process current working directory (``os.getcwd()``).

Definition at line 79 of file _sandbox.py.

Here is the caller graph for this function:

◆ get_session_sandbox_root_override()

Path | None oc_chat.tools._sandbox.get_session_sandbox_root_override ( )
Return the process-local session sandbox override, if set.

Definition at line 128 of file _sandbox.py.

◆ install_resolved()

None oc_chat.tools._sandbox.install_resolved ( Path | None resolved)
Install an ALREADY-RESOLVED session sandbox override — no-fail (plan 152 D4).

The COMMIT-phase counterpart to :func:`set_session_sandbox_root_override`.
That function re-runs ``Path().expanduser().resolve(strict=False)``, which is
fallible and therefore cannot sit inside a transition's no-fail critical
section. A transition resolves the path once during PREPARE and hands the
resulting :class:`~pathlib.Path` here, so this is a bare assignment that
cannot raise.

*resolved* must already be resolved; ``None`` clears the override.
:func:`set_session_sandbox_root_override` stays for legacy callers.

Definition at line 111 of file _sandbox.py.

◆ is_human_confirm_write_path()

bool oc_chat.tools._sandbox.is_human_confirm_write_path ( "str | Path" path)
Return ``True`` if *path* is a config-as-code human-confirm-tier write target
(``*settings*.json`` incl. ``.oc_chat``/``.claude`` settings, ``pyproject.toml``,
a dependency lockfile, a shell-init startup script, the provider-endpoint config
``endpoints.toml``, the local-fetch port allowlist ``local_fetch.json`` —
F72/§5.1.2). Hard-deny (the protected-write class) takes
precedence and is checked first by the caller; this is the softer tier (the
react-loop relays a y/N showing the write content so a human can inspect e.g. the
``endpoints.toml`` base_url; headless fails closed).

Definition at line 324 of file _sandbox.py.

Here is the call graph for this function:

◆ is_importable_write_path()

bool oc_chat.tools._sandbox.is_importable_write_path ( "str | Path" path)
Return ``True`` if *path* is a ``.py`` under a harness/contract importable root
(a write-then-import in-process ACE vector, F57). Profile-tracking by topology:
under ``guarded``/``isolated`` the importable roots are outside the sandbox (G-1),
so an in-sandbox write resolves under no root and returns ``False``; under
``permissive`` monorepo dogfooding it does overlap and returns ``True``.

Definition at line 359 of file _sandbox.py.

◆ is_path_inside_sandbox()

bool oc_chat.tools._sandbox.is_path_inside_sandbox ( str | Path path,
Path | None root = None )
Return ``True`` if *path* resolves to a location inside *root*.

If *root* is ``None`` (the default), the sandbox root is resolved
at call time via :func:`_resolve_default_root`.

Relative paths are interpreted relative to *root*. The path is
resolved with ``strict=False`` so missing files do not raise.

Definition at line 133 of file _sandbox.py.

Here is the call graph for this function:

◆ is_protected_read_path()

bool oc_chat.tools._sandbox.is_protected_read_path ( str | Path path)
Return ``True`` if *path* is in the protected-read (secret) class.

The deterministic, name/location-based secret class the read tools must
refuse to read or list (M8 F46; M8.0 WU-2): ``.env`` / ``.env.*``,
secret-bearing ``*.local.json`` (``*settings*`` / ``*secret*`` /
``*credential*`` / ...), ``agent_secrets.json``, the ``.netrc`` / ``.npmrc``
/ ``.pypirc`` / ``.git-credentials`` credential stores, private-key material
(``*.pem`` / ``*.key`` / ``*.pfx`` / ``*.p12`` and ``id_rsa`` / ``id_dsa`` /
``id_ecdsa`` / ``id_ed25519``), and anything under an ``.ssh`` / ``.gnupg`` /
``.aws`` credential directory. Matching is case-insensitive (Windows) and
strips a Windows alternate-data-stream suffix AND trailing dots/spaces
(``.env::$DATA`` / ``.env `` / ``.env.``) before classification.

Content-tagged ``redaction_class=secret`` is the M8.3 metadata layer (F46),
not this deterministic floor. The committed ``*settings*.json`` (no
``.local``) is a protected-WRITE concern, not protected-read.

Definition at line 220 of file _sandbox.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_protected_write_path()

bool oc_chat.tools._sandbox.is_protected_write_path ( "str | Path" path)
Return ``True`` if *path* is in the hard-deny protected-write class.

The integrity / config-as-code / secret targets ``write_file`` (and other
path sinks) must refuse BEFORE the sandbox clamp, regardless of profile
(M8 §5.1.2 hard-deny + Appendix C G-4; M8.0 WU-5): anything under a ``.git``
directory or a ``.git*`` pointer / submodule-config file; the protected-read
secret class (a secret must not be written either); ``*.pth`` site-path
files; ``sitecustomize.py`` / ``usercustomize.py`` / ``conftest.py`` import/
collection hooks; ``.pre-commit-config.yaml``; the ``.oc_chat_clean_exit``
marker. Matching is case-insensitive (Windows) and strips a Windows
alternate-data-stream suffix and trailing dots/spaces before classification.

The softer human-confirm protected-write TIER (config-as-code incl.
``endpoints.toml``, + the importable tree) is :func:`is_human_confirm_write_path`
/ :func:`is_importable_write_path` (WU-8 / F70, §5.1.2). The F59 custody anchors
and the contract ``canonical_json`` MAC preimage land with M8.3.

Definition at line 268 of file _sandbox.py.

Here is the call graph for this function:

◆ redact_secrets()

str oc_chat.tools._sandbox.redact_secrets ( str text)
Redact unambiguous secret tokens / keys from *text* (M8 F46/F51; WU-2).

A deterministic, low-false-positive scrub applied to all secret-bearing tool
and command output (``read_file`` / ``read_chunks`` content, ``run_command``
stdout/stderr) so a secret embedded in otherwise-readable content, or echoed
by a command, does not exfiltrate. Matches well-known credential formats
(PEM private keys, URL ``user:pass``, AWS / GitHub / Slack / Google keys,
``sk-`` / ``sk-ant-`` provider keys, JWTs, Bearer tokens); each match is
replaced with a visible ``[REDACTED-SECRET]`` marker so the redaction is
apparent rather than silently corrupting.

Username / home-path / private-index *path* disclosure (the non-secret
Tier-1 disclosure-safety, F51) is WU-3; content-class
``redaction_class=secret`` tagging is the M8.3 metadata layer.

Definition at line 463 of file _sandbox.py.

Here is the caller graph for this function:

◆ require_inside_sandbox()

Path oc_chat.tools._sandbox.require_inside_sandbox ( str | Path path,
Path | None root = None )
Resolve *path* and return it if inside *root*; raise otherwise.

Returns the resolved absolute :class:`~pathlib.Path`. Raises
:class:`SandboxViolation` when the resolved path is outside the
sandbox root.

Symlinks and ``..`` components are resolved before the check via
``Path.resolve(strict=False)``.

Definition at line 154 of file _sandbox.py.

Here is the call graph for this function:

◆ scrubbed_env()

"dict[str, str]" oc_chat.tools._sandbox.scrubbed_env ( "dict[str, str] | None" extra = None)
Return a minimal subprocess environment (M8 F25/F72/F78/F79; WU-3c).

``os.environ`` filtered to :data:`_ENV_ALLOWLIST`, plus *extra* (caller-set
vars such as ``PYTHONIOENCODING``/``TMPDIR``, applied last). Drops the
attacker-poisonable families that would redirect a subprocess to a malicious
index / proxy / cert or inject code, regardless of how they entered
``os.environ``. Matching is case-insensitive (Windows).

Definition at line 593 of file _sandbox.py.

◆ set_session_sandbox_root_override()

None oc_chat.tools._sandbox.set_session_sandbox_root_override ( str | Path | None path)
Set or clear the process-local session sandbox override.

Definition at line 102 of file _sandbox.py.

Variable Documentation

◆ _LOCKFILE_NAMES

frozenset oc_chat.tools._sandbox._LOCKFILE_NAMES
protected
Initial value:
1= frozenset({
2 "poetry.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "uv.lock",
3 "pipfile.lock", "cargo.lock", "composer.lock", "gemfile.lock", "requirements.lock",
4})

Definition at line 310 of file _sandbox.py.

◆ _PEM_BEGIN

str oc_chat.tools._sandbox._PEM_BEGIN = r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"
protected

Definition at line 394 of file _sandbox.py.

◆ _PEM_BEGIN_RE

oc_chat.tools._sandbox._PEM_BEGIN_RE = re.compile(_PEM_BEGIN)
protected

Definition at line 505 of file _sandbox.py.

◆ _PEM_END

str oc_chat.tools._sandbox._PEM_END = r"-----END [A-Z0-9 ]*PRIVATE KEY-----"
protected

Definition at line 395 of file _sandbox.py.

◆ _PEM_END_RE

oc_chat.tools._sandbox._PEM_END_RE = re.compile(_PEM_END)
protected

Definition at line 506 of file _sandbox.py.

◆ _PEM_REDACTION_PLACEHOLDER

str oc_chat.tools._sandbox._PEM_REDACTION_PLACEHOLDER = "[REDACTED-PRIVATE-KEY]"
protected

Definition at line 386 of file _sandbox.py.

◆ _PROTECTED_READ_DIRS

frozenset oc_chat.tools._sandbox._PROTECTED_READ_DIRS = frozenset({".ssh", ".gnupg", ".aws"})
protected

Definition at line 200 of file _sandbox.py.

◆ _PROTECTED_READ_NAMES

frozenset oc_chat.tools._sandbox._PROTECTED_READ_NAMES
protected
Initial value:
1= frozenset({
2 "agent_secrets.json",
3 "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", # private SSH keys
4 ".netrc", ".npmrc", ".pypirc", ".git-credentials", # credential stores
5})

Definition at line 190 of file _sandbox.py.

◆ _PROTECTED_READ_SUFFIXES

tuple oc_chat.tools._sandbox._PROTECTED_READ_SUFFIXES = (".pem", ".key", ".pfx", ".p12")
protected

Definition at line 197 of file _sandbox.py.

◆ _PROTECTED_WRITE_NAMES

frozenset oc_chat.tools._sandbox._PROTECTED_WRITE_NAMES
protected
Initial value:
1= frozenset({
2 "sitecustomize.py", "usercustomize.py", "conftest.py",
3 ".pre-commit-config.yaml", ".oc_chat_clean_exit",
4 ".gitmodules",
5 # CONFIG-01 (plan 238): the shipped, read-only defaults file carrying
6 # every default and ceiling. Protection BY NAME is what makes it a bound.
7 "oc_chat.defaults.toml",
8})

Definition at line 258 of file _sandbox.py.

◆ _SECRET_PATTERNS

list oc_chat.tools._sandbox._SECRET_PATTERNS
protected

Definition at line 403 of file _sandbox.py.

◆ _SECRET_REDACTION_PLACEHOLDER

str oc_chat.tools._sandbox._SECRET_REDACTION_PLACEHOLDER = "[REDACTED-SECRET]"
protected

Definition at line 383 of file _sandbox.py.

◆ _SECRETY_STEMS

tuple oc_chat.tools._sandbox._SECRETY_STEMS
protected
Initial value:
1= (
2 "settings", "secret", "credential", "token", "auth", "password", "apikey", "api_key",
3)

Definition at line 204 of file _sandbox.py.

◆ _SESSION_SANDBOX_ROOT_OVERRIDE

Path oc_chat.tools._sandbox._SESSION_SANDBOX_ROOT_OVERRIDE = None
protected

Definition at line 99 of file _sandbox.py.

◆ _STARTUP_SCRIPT_NAMES

frozenset oc_chat.tools._sandbox._STARTUP_SCRIPT_NAMES
protected
Initial value:
1= frozenset({
2 ".bashrc", ".bash_profile", ".bash_login", ".bash_logout", ".profile",
3 ".zshrc", ".zprofile", ".zlogin", ".zshenv", ".cshrc", ".tcshrc", ".kshrc", ".login",
4})

Definition at line 318 of file _sandbox.py.

◆ DEFAULT_SANDBOX_ROOT

Path oc_chat.tools._sandbox.DEFAULT_SANDBOX_ROOT = _resolve_default_root()

Definition at line 97 of file _sandbox.py.