Skip to content

Prove the browser claim under a real Pyodide runtime (#99) - #140

Merged
EONRaider merged 1 commit into
masterfrom
claude/decoder-depth-polish-8m54to
Sep 4, 2026
Merged

Prove the browser claim under a real Pyodide runtime (#99)#140
EONRaider merged 1 commit into
masterfrom
claude/decoder-depth-polish-8m54to

Conversation

@EONRaider

Copy link
Copy Markdown
Owner

Summary

Tier 4 (#106), item 1 of 3: proves the README's browser/WASM claim under a real Pyodide runtime rather than a simulation of it. Closes #99.

What's included

  • New blocking CI job (pyodide in .github/workflows/ci.yml): builds the wheel, boots actual Pyodide (WebAssembly CPython) under Node — no browser needed, same WASM build either way — installs the wheel, and decodes the entire real-capture fixture corpus with it.
  • scripts/pyodide/run_in_pyodide.mjs: generic Node driver that mounts the checked-out repo into Pyodide's virtual filesystem and runs a Python script inside it.
  • scripts/pyodide/check_in_pyodide.py: the actual check — re-asserts fcntl/termios/resource/grp/pwd are genuinely unavailable under Pyodide (not just blocklisted), then decodes every corpus frame, failing on anything but ProtocolError.
  • A real bug found and fixed along the way: IPv6.decode()/bytes(IPv6(...)) went through socket.inet_ntop/inet_pton(AF_INET6, ...). Pyodide's CPython build has AF_INET6 sockets disabled, so every IPv6 frame failed to decode there before this fix. _base.py's ipv6_to_bytes/bytes_to_ipv6 are now pure Python: ipaddress.IPv6Address for parsing, and a hand-rolled RFC 5952 canonicalizer for formatting — str(ipaddress.IPv6Address(...)) was tried first and rejected because it disagrees with glibc's inet_ntop on IPv4-mapped addresses (::ffff:a.b.c.d), and disagrees with itself between Python 3.11 and 3.12. The replacement is differentially verified against socket.inet_ntop across 500,000+ random addresses plus an exhaustive sweep of both dotted-quad special cases (see tests/test_ip.py's new Hypothesis properties) — zero divergence. IPv4 addressing is untouched (its AF_INET socket calls work fine under Pyodide). The invalid-input exception type (OSError) and message are preserved, so this is a pure implementation swap on every existing platform.
  • docs/CLAIMS.md 3.1's status flipped from GATED ON #99 to reflect the capability is now CI-verified — the COMPARATIVE — HELD embargo on the claim itself (naming scapy) is untouched, per the roadmap's standing embargo through the rest of Roadmap: post-1.3.0 — performance, platform, typing, proof #107.
  • Drive-by: docs/CLAIMS.md 5.3's stale "gate pending Enforce the coverage level already achieved #79" language corrected — Enforce the coverage level already achieved #79 already shipped (fail_under = 98), and the statement/miss counts were out of date.
  • CHANGELOG.md entry under ## [Unreleased].

Verification

  • uv run ruff check and uv run ruff format --check are clean
  • uv run mypy is clean (strict)
  • uv run pytest passes locally — 1036 passed
  • CHANGELOG.md has an entry under ## [Unreleased]
  • uv run --group bench python scripts/benchmark.py --check --threshold 15 — within threshold (-10.5% normalized vs. baseline)
  • The new pyodide job itself, run locally the same way CI will: netprotocols 2.0.0 imported and decoded the real 97-frame corpus under a real Pyodide runtime (97 decoded cleanly, 0 raised ProtocolError, 0 raised anything else)

Notes

Scope note for reviewers: the issue offered a cheaper alternative (just sys.meta_path-block the POSIX-only modules and run the existing suite under CPython) that would have missed the AF_INET6 bug entirely — the real-runtime approach was chosen deliberately for the stronger proof, at the cost of the extra Node/Pyodide CI plumbing.

Closes #99.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MGDTcK51CWcy6PrNetN213


Generated by Claude Code

Adds a blocking CI job that boots actual Pyodide (WebAssembly CPython)
under Node, installs the wheel this job just built, and decodes the
whole real-capture fixture corpus with it — real proof the library
runs where scapy/dpkt-with-fcntl cannot, not a simulation of it via a
sys.meta_path blocklist.

That run surfaced a genuine bug: IPv6 decode/encode went through
socket.inet_ntop/inet_pton(AF_INET6, ...), and Pyodide's CPython build
has AF_INET6 sockets disabled, so every IPv6 frame failed under it.
Fixed by reimplementing _base.py's ipv6_to_bytes/bytes_to_ipv6 in pure
Python (ipaddress for parsing; a hand-rolled RFC 5952 canonicalizer,
differentially verified against glibc's inet_ntop across 500,000+
random addresses plus both of its dotted-quad special cases, for
formatting) — str(ipaddress.IPv6Address) was tried first and rejected
because it disagrees with glibc on IPv4-mapped addresses and disagrees
with itself between Python 3.11 and 3.12. Exception type and message
are preserved, so this is a pure implementation swap, not a behavior
change on any platform that already worked.

Also flips docs/CLAIMS.md 3.1's status now that the capability is
CI-verified (the COMPARATIVE — HELD embargo on the claim itself is
unchanged), and 5.3's stale "gate pending #79" language — #79 already
shipped fail_under=98.

Closes #99.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGDTcK51CWcy6PrNetN213
@EONRaider
EONRaider merged commit 625f884 into master Sep 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prove the browser claim with a Pyodide/WASM CI job

2 participants