From a36998f4cc8b2fbd83eaa2d88ad36836659c8e17 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Mon, 13 Jul 2026 15:09:54 -0600 Subject: [PATCH] fix(tests): close the Oura egress guard's module-scope escape hatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard allowlisted (oura_auth, , module_network_import) and then treated that single entry as permission for any module-scope capability except dynamic_import. Because the check fell back to that broad rule, a future module-level `import subprocess`, `import socket`, `import ssl`, `import smtplib`, or an `os.system(...)` call in oura_auth.py would have been reported as allowed — defeating the guard's stated contract of allowlisting by module, function, and capability, and defeating its requirement to fail on sockets, TLS primitives, mail clients, and subprocess escape hatches. _allowed is now an exact (module, owner, capability) membership test with no fallback, and the broad entry is replaced by exactly the three capabilities oura_auth.py genuinely needs at module scope, matching its imports: urllib.request (http_client), webbrowser (browser_open), and http.server (loopback_http_server). urllib.parse remains covered by SAFE_IMPORTS. oura.py's lazy-import discipline is unchanged. Detection also missed several transports the boundary is supposed to cover, so urllib3 and pycurl are now recognized as HTTP clients and imaplib and poplib as mail clients. The synthetic self-test — which exists to prove the guard can actually fail — now feeds module-scope subprocess imports and calls, os.system, socket, smtplib, and urllib3 through the harness and asserts each is reported as a violation. Also corrects two design-doc statements that no longer matched the code: the Oura sync backend is registered and implements save-mode sync rather than raising as an unregistered skeleton, and oura.py keeps a lazy-import discipline with egress confined to the allowlisted transport rather than having no network imports at all. --- docs/design/oura-import.md | 4 +-- tests/test_oura_egress_guard.py | 63 ++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/docs/design/oura-import.md b/docs/design/oura-import.md index d5cf1980f..3591f1779 100644 --- a/docs/design/oura-import.md +++ b/docs/design/oura-import.md @@ -172,7 +172,7 @@ Importer-owned files under `imports/` are private (`0600`) and importer-owned di ## 5. (d) Sync design -**Backend.** `OuraSyncBackend` (skeleton class exists; `sync()` raises with a pointer here). Registered in `SYNCABLE_REGISTRY` **only at phase O3** — the skeleton deliberately leaves it unregistered so no runtime flow (CLI `--sync`, export tooling) can reach a half-built path; a test pins that until O3 flips both together. +**Backend.** `OuraSyncBackend` is registered in `SYNCABLE_REGISTRY["oura"]` and implements save-mode sync. Save runs validate the pre-save gate before taking the per-journal import lock, recheck the gate inside the lock, then fetch and persist only after the gate and lock both hold. Cursor-only quiet runs intentionally advance `imports/oura.json` without creating an import bundle. **Cursor state** at `imports/oura.json` via `sync.load_sync_state`/`save_sync_state`: @@ -415,7 +415,7 @@ reauthorization needed (the scopes were granted 2026-07-07). ## 10. What landed with this doc (phase O0 inventory) -- `solstone/think/importers/oura.py` — parse layer (`parse_oura_bundle`, `parse_endpoint_document`, `parse_oura_day`), normalizer (`normalize_bundle` → rows + `HealthDedupeRecord`s via `health_schema`), §13 copy reference (`render_day_summary`), `OuraImporter` (detect/preview/dry-run live; save gated then seamed), `OuraSyncBackend` + OAuth seams (all raise, pointing here). Zero network imports, test-enforced. +- `solstone/think/importers/oura.py` — parse layer (`parse_oura_bundle`, `parse_endpoint_document`, `parse_oura_day`), normalizer (`normalize_bundle` → rows + `HealthDedupeRecord`s via `health_schema`), §13 copy reference (`render_day_summary`), `OuraImporter` (detect/preview/dry-run live; save gated then seamed), `OuraSyncBackend` + OAuth seams. Network egress follows a lazy-import discipline: no module-level network imports, with live egress confined to the allowlisted transport path enforced by tests. - `solstone/think/importers/health_schema.py` — `SOURCE_OURA_API`, `KNOWN_SOURCE_FAMILIES` entry, friendly names for the seven `oura.*` record types. - `solstone/think/importers/pre_save_gate.py` — `"oura"` joins `SENSITIVE_IMPORTERS`. - `solstone/think/importers/file_importer.py` — registry entry (preview/dry-run-only paths active). diff --git a/tests/test_oura_egress_guard.py b/tests/test_oura_egress_guard.py index e936da6f2..7fc9479a0 100644 --- a/tests/test_oura_egress_guard.py +++ b/tests/test_oura_egress_guard.py @@ -13,7 +13,9 @@ IMPORTER_ROOT = Path(__file__).resolve().parents[1] / "solstone" / "think" / "im ALLOWED_EGRESS: frozenset[tuple[str, str, str]] = frozenset( { ("oura", "_default_transport", "http_client"), - ("oura_auth", "", "module_network_import"), + ("oura_auth", "", "browser_open"), + ("oura_auth", "", "http_client"), + ("oura_auth", "", "loopback_http_server"), ("oura_auth", "_default_http_transport", "http_client"), ("oura_auth", "_post_token_request", "http_client"), ("oura_auth", "_authorization_url", "authorization_url"), @@ -32,7 +34,10 @@ IMPORT_CAPABILITIES: dict[str, str] = { "http.client": "http_client", "http.server": "loopback_http_server", "httpx": "http_client", + "imaplib": "mail_client", "importlib": "dynamic_import", + "poplib": "mail_client", + "pycurl": "http_client", "requests": "http_client", "smtplib": "mail_client", "socket": "socket", @@ -41,6 +46,7 @@ IMPORT_CAPABILITIES: dict[str, str] = { "urllib": "http_client", "urllib.error": "http_client", "urllib.request": "http_client", + "urllib3": "http_client", "webbrowser": "browser_open", } @@ -52,15 +58,19 @@ CALL_CAPABILITIES: dict[str, str] = { "http.server.BaseHTTPRequestHandler": "loopback_http_server", "http.server.HTTPServer": "loopback_http_server", "httpx": "http_client", + "imaplib": "mail_client", "importlib": "dynamic_import", "os.popen": "process_escape", "os.system": "process_escape", + "poplib": "mail_client", + "pycurl": "http_client", "requests": "http_client", "smtplib": "mail_client", "socket": "socket", "ssl": "tls", "subprocess": "process_escape", "urllib.request": "http_client", + "urllib3": "http_client", "webbrowser.open": "browser_open", } @@ -155,13 +165,7 @@ def _resolve_name(name: str, bindings: dict[str, str]) -> str: def _allowed(module: str, owner: str, capability: str) -> bool: - if (module, owner, capability) in ALLOWED_EGRESS: - return True - return ( - owner == "" - and capability != "dynamic_import" - and (module, owner, "module_network_import") in ALLOWED_EGRESS - ) + return (module, owner, capability) in ALLOWED_EGRESS def _violation( @@ -256,17 +260,44 @@ def test_oura_egress_guard_covers_all_oura_modules() -> None: def test_oura_egress_guard_reports_synthetic_violation() -> None: source = """ +import os +import smtplib import socket - -def added_escape_hatch(): - import subprocess - socket.socket() - __import__("ssl") - subprocess.run(["true"]) +import subprocess +import urllib3 + +socket.socket() +smtplib.SMTP("localhost") +subprocess.run(["true"]) +os.system("true") +urllib3.PoolManager() +__import__("ssl") """ violations = _egress_violations("oura_future", source) - assert any("socket" in violation for violation in violations) + assert any( + "oura_future.: process_escape: import subprocess" in violation + for violation in violations + ) + assert any( + "oura_future.: process_escape: call subprocess.run" in violation + for violation in violations + ) + assert any( + "oura_future.: process_escape: call os.system" in violation + for violation in violations + ) + assert any( + "oura_future.: socket: import socket" in violation + for violation in violations + ) + assert any( + "oura_future.: mail_client: import smtplib" in violation + for violation in violations + ) + assert any( + "oura_future.: http_client: import urllib3" in violation + for violation in violations + ) assert any("dynamic_import" in violation for violation in violations) - assert any("process_escape" in violation for violation in violations) -- 2.51.2