|
oc_chat
|
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 |
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
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
| str oc_chat.tools._local_fetch.LOCAL_FETCH_DIRNAME = ".oc_chat" |
Definition at line 65 of file _local_fetch.py.
| str oc_chat.tools._local_fetch.LOCAL_FETCH_FILENAME = "local_fetch.json" |
Definition at line 68 of file _local_fetch.py.
| str oc_chat.tools._local_fetch.LOCAL_FETCH_PROFILE = "permissive" |
Definition at line 81 of file _local_fetch.py.
| int oc_chat.tools._local_fetch.LOCAL_FETCH_SCHEMA_VERSION = 1 |
Definition at line 71 of file _local_fetch.py.
| frozenset oc_chat.tools._local_fetch.LOCAL_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"}) |
Definition at line 75 of file _local_fetch.py.
| tuple oc_chat.tools._local_fetch.LOCAL_RESPONSE_HEADERS |
Definition at line 86 of file _local_fetch.py.
| frozenset oc_chat.tools._local_fetch.LOCAL_SCHEMES = frozenset({"http", "https"}) |
Definition at line 78 of file _local_fetch.py.