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

Classes

class  LocalFetchRefused

Functions

Path allowlist_path ("Path | None" root=None)
frozenset[int] read_allowed_ports ("Path | None" root=None)
str normalize_local_host ("str | None" host)
bool is_local_target (str url)
int admit_local_url (str url, "Path | None" root=None)
dict bounded_response_headers (object headers)

Variables

str LOCAL_FETCH_DIRNAME = ".oc_chat"
str LOCAL_FETCH_FILENAME = "local_fetch.json"
int LOCAL_FETCH_SCHEMA_VERSION = 1
frozenset LOCAL_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})
frozenset LOCAL_SCHEMES = frozenset({"http", "https"})
str LOCAL_FETCH_PROFILE = "permissive"
tuple LOCAL_RESPONSE_HEADERS

Detailed Description

Port-allowlisted loopback mode for ``fetch_url`` (plan 232 / WEB-01).

Purpose
-------
``fetch_url`` is ``https``-only and refuses every non-public address (M8.0 WU-4
§5.1.3, F2/F2a), so a developer dogfooding OC Chat inside a project that serves a
local site cannot check the SERVED page — only the file on disk. This module is
the narrow, operator-ruled exception: a URL on ``localhost`` / ``127.0.0.1`` /
``::1`` may be fetched when — and only when — the active risk profile is
``permissive`` AND its explicit port is listed in a visible, per-project
allowlist file. Everything else about the egress policy is untouched.

Responsibilities
----------------
- :func:`allowlist_path` — where the allowlist lives for a sandbox root.
- :func:`read_allowed_ports` — the fail-closed reader (missing / unreadable /
  malformed / wrong-schema / bad-port file admits NOTHING).
- :func:`is_local_target` — is this URL a candidate for local mode at all?
- :func:`admit_local_url` — the full admission rule; returns the admitted port
  or raises :class:`LocalFetchRefused` with a NAMED reason.
- :func:`bounded_response_headers` — the delivery-check header allowlist.

Contracts
---------
- **Fail closed.** A missing, unreadable, or malformed allowlist file admits no
  loopback address at all. There is no environment-variable source: the operator
  ruling requires the allowlist to be a visible file.
- **No caching.** The file is read on every call, so an edit takes effect
  immediately and a deletion closes the door immediately.
- **Named refusals.** Every refusal names the profile, the port, or the file —
  a refused local URL never falls through to the public-internet path.
- **The three hosts only.** ``127.0.0.2``, RFC1918, link-local, cloud-metadata
  and every other non-public address stay refused by the unchanged egress guard,
  whatever the allowlist says.

Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_chat.core.risk_profile (lazy; READ-ONLY — the profile is resolved, never set)
- oc_chat.tools.filesystem (lazy; READ-ONLY — the one sandbox-root resolution)

Diagnostics
-----------
Domain: OC_CHAT.TOOLS
Levels:
    L2 — admissions and refusals
    L3 — per-decision detail (the ports a file admitted)
    L4 — deep tracing

Function Documentation

◆ admit_local_url()

int oc_chat.tools._local_fetch.admit_local_url ( str url,
"Path | None" root = None )
Admit *url* to local mode and return its port, or raise :class:`LocalFetchRefused`.

The full admission rule (plan 232 P2), in order: the resolved risk profile is
``permissive``; the scheme is ``http``/``https``; the host is one of the three
named local hosts; there are no URL credentials; the port is explicit; and the
port is listed in the allowlist file. Call once per redirect hop.

Definition at line 217 of file _local_fetch.py.

Here is the call graph for this function:

◆ allowlist_path()

Path oc_chat.tools._local_fetch.allowlist_path ( "Path | None" root = None)
Return the allowlist path for *root* (default: the active sandbox root).

The sandbox root is resolved by the ONE existing resolver
(:func:`oc_chat.tools.filesystem._sandbox_root`) so local mode can never
disagree with the filesystem sandbox about which project is active. The
import is lazy and read-only.

Definition at line 95 of file _local_fetch.py.

Here is the caller graph for this function:

◆ bounded_response_headers()

dict oc_chat.tools._local_fetch.bounded_response_headers ( object headers)
Return the :data:`LOCAL_RESPONSE_HEADERS` subset of *headers*, lowercased.

Allowlist, never denylist: a header the delivery check does not need is a
header that cannot leak. Absent headers are simply omitted.

Definition at line 262 of file _local_fetch.py.

◆ is_local_target()

bool oc_chat.tools._local_fetch.is_local_target ( str url)
True if *url* is a candidate for local mode (an http/https URL on the
three named local hosts).

A candidate is never "maybe public": :func:`admit_local_url` either admits it
or refuses it by name, so a refused candidate cannot fall through to the
public path and be re-refused with a misleading message.

Definition at line 203 of file _local_fetch.py.

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

◆ normalize_local_host()

str oc_chat.tools._local_fetch.normalize_local_host ( "str | None" host)
Return *host* lowercased, trailing-dot-stripped, and IPv6-compressed.

``[0:0:0:0:0:0:0:1]`` and ``[::1]`` are the same host; ``LOCALHOST.`` and
``localhost`` are the same host. An IPv4-mapped form such as
``::ffff:127.0.0.1`` compresses to itself and therefore does NOT match
``127.0.0.1`` — that is deliberate, it is not one of the three named hosts.

Definition at line 186 of file _local_fetch.py.

Here is the caller graph for this function:

◆ read_allowed_ports()

frozenset[int] oc_chat.tools._local_fetch.read_allowed_ports ( "Path | None" root = None)
Return the allowlisted ports, or raise :class:`LocalFetchRefused`.

Fail-closed at every step: a missing file, an unreadable file, undecodable
bytes, invalid JSON, a non-object payload, an unexpected ``schema_version``,
a non-list ``ports``, or ANY malformed entry inside ``ports`` refuses the
whole file rather than silently admitting the entries that happened to parse.
``True``/``False`` are rejected explicitly — ``bool`` is a subclass of ``int``
and ``True`` would otherwise read as port 1.

Every failure raises with the FILE NAMED (plan 232 P4 / P8). Two exception
types are caught for that reason alone, because both fail closed already but
would otherwise surface unnamed: ``UnicodeDecodeError`` (a ``ValueError``, so
it escapes the ``OSError`` arm) and ``RecursionError`` (a ``RuntimeError``, so
it escapes the JSON ``ValueError`` arm).

Definition at line 109 of file _local_fetch.py.

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

Variable Documentation

◆ LOCAL_FETCH_DIRNAME

str oc_chat.tools._local_fetch.LOCAL_FETCH_DIRNAME = ".oc_chat"

Definition at line 65 of file _local_fetch.py.

◆ LOCAL_FETCH_FILENAME

str oc_chat.tools._local_fetch.LOCAL_FETCH_FILENAME = "local_fetch.json"

Definition at line 68 of file _local_fetch.py.

◆ LOCAL_FETCH_PROFILE

str oc_chat.tools._local_fetch.LOCAL_FETCH_PROFILE = "permissive"

Definition at line 81 of file _local_fetch.py.

◆ LOCAL_FETCH_SCHEMA_VERSION

int oc_chat.tools._local_fetch.LOCAL_FETCH_SCHEMA_VERSION = 1

Definition at line 71 of file _local_fetch.py.

◆ LOCAL_HOSTS

frozenset oc_chat.tools._local_fetch.LOCAL_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})

Definition at line 75 of file _local_fetch.py.

◆ LOCAL_RESPONSE_HEADERS

tuple oc_chat.tools._local_fetch.LOCAL_RESPONSE_HEADERS
Initial value:
1= (
2 "content-type", "content-length", "location", "last-modified", "etag", "server",
3)

Definition at line 86 of file _local_fetch.py.

◆ LOCAL_SCHEMES

frozenset oc_chat.tools._local_fetch.LOCAL_SCHEMES = frozenset({"http", "https"})

Definition at line 78 of file _local_fetch.py.