# 08 — is this file safe to send anywhere?

Browser execution via Pyodide (DESIGN.md sec 8), built around a
concrete niche rather than a generic demo: **checking whether an export
contains sensitive-looking columns before you're allowed to send it
anywhere.** For anyone handling a file under an NDA, a legal hold, or a
compliance policy — an incident responder, an auditor, a consultant, a
support engineer forwarding a customer export — "upload it to a checker
to find out if it's sensitive" is exactly the chicken-and-egg problem a
purely client-side tool sidesteps: it never makes a network request for
the file, and you can prove that to yourself by watching the runtime
log while it runs.

```bash
uv run python prepare_bundle.py   # copies src/duckpipe/ + generates sample data, once
python3 -m http.server 8000       # any static file server works
# open http://localhost:8000/
```

(`fetch()`-ing local files needs an http(s) origin — opening `index.html`
directly via `file://` will hit CORS restrictions in most browsers.
Any static host works for the real thing too: GitHub Pages, Netlify, a
plain `nginx` — this is a static page, nothing server-side to deploy.)

**What actually happens**, in order: load the Pyodide runtime, load the
real `duckdb` Python package (a real SQL engine, not a stub — it ships
in Pyodide's own package repository), fetch DuckPipe's own
`task.py`/`dag.py`/`fingerprint.py`/`state.py`/`scheduler.py` (copied
verbatim by `prepare_bundle.py`, not rewritten a single line) into the
sandbox's virtual filesystem, `import duckpipe`, then call
`duckpipe.run('/pipeline.py', db_path='/state/duckpipe.db')` — the exact
same call the CLI makes. `asyncio.run()` inside the scheduler works
unmodified because Pyodide uses WASM JSPI (stack switching) to let it
block on the browser's own event loop without deadlocking; JSPI ships in
Chrome 137+ stable today with no flag.

`pipeline.py` (open it, it's a completely normal DuckPipe pipeline) is
schema-agnostic on purpose — `profile → scan_sensitive_columns →
triage_report` works over any CSV/Parquet, since the whole point is
"bring your own file." Per column, it checks (via real DuckDB queries
over the actual file, not a sample-and-hope): does a majority of its
values match an email/phone/SSN/credit-card/IP-address pattern, and does
the column's own name hint at something sensitive. Deliberately
conservative and honest about scope — this is pattern/naming-heuristic
triage, not a claim of exhaustive PII detection (a genuinely hard,
model-shaped problem); a clean verdict means "these specific checks
found nothing," not "this file is definitely safe."

**The bundled sample** is entirely fabricated on purpose (generated by
`prepare_bundle.py`, not committed): fake names, `@example.com`
addresses (IANA-reserved for documentation), phone numbers and SSNs in
ranges North American authorities never issue to a real person, and
`192.0.2.x` IPs (RFC 5737, reserved for documentation) — so the first
run already has something real to flag without asking anyone to hand
over an actual file just to see the tool work.

**The two things this is actually for, checked, not just claimed:**

- **Your data never leaves the tab.** Pick "your own file" and the
  bytes go straight from the file picker into Pyodide's virtual
  filesystem via `arrayBuffer()` — no `fetch`, no `XMLHttpRequest`, no
  network request at all. Verified directly (Playwright, real
  Chromium): an uploaded CSV gets scanned correctly with zero network
  activity for the file itself, for both a file with sensitive-looking
  columns and one without.
- **The same fingerprint-based skip-if-unchanged story survives a real
  reload**, not just one page's JS lifetime. State persists via
  IndexedDB (`FS.mount(IDBFS, ..., '/state')`); reload the page — a
  brand new Pyodide instance, no shared JS state — and run again with
  the same file: every task reports `skipped`. Verified directly, not
  assumed: `tests/test_browser_wasm_example` (needs `uv run playwright
  install chromium` once) drives an actual headless browser through
  exactly this reload-and-rerun sequence, plus the sensitive/clean
  verdict logic itself.

**Honest limitations** (DESIGN.md sec 8 is explicit about these, this
example doesn't paper over them): no `state_uri` remote sync and no
DuckLake backend here — Pyodide's DuckDB build has no runtime-loaded
extensions (`httpfs`/`ducklake` unavailable), and whether `fsspec` even
works against Pyodide's virtual filesystem/CORS-constrained fetch model
is genuinely unverified, tracked as the next spike, not claimed here.
Single-threaded (a Pyodide constraint on DuckDB's own internal
parallelism, not on DuckPipe's `asyncio`-based task concurrency — several
independent tasks still run concurrently the same way they do natively).
And, stated plainly rather than left implicit: the sensitive-column
checks are heuristics over patterns and column names, not a certified
PII scanner — treat a "clear" verdict as "nothing these specific checks
catch," not a compliance sign-off.
