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)