diff --git a/ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.md b/ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.md new file mode 100644 index 0000000..4ec6f70 --- /dev/null +++ b/ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.md @@ -0,0 +1,48 @@ +--- +model: claude-opus-4-6 +service: claude +session: py-multiaddr-wg-protocol +timestamp: 2026-04-15T00:24:53Z +git_ref: 0b6493a +scope: code +substantive: true +raw_file: 20260415T002453Z_0b6493a_prompt_io.raw.md +--- + +## Prompt + +The user requested adding full support for a new "wireguard" protocol +denoted as `wg` into the py-multiaddr codebase. They referenced the +upstream spec repo links: + +- https://github.com/multiformats/multiaddr/blob/master/protocols.csv +- https://multiformats.io/multiaddr/ + +And noted the multicodec addition process: + +- https://github.com/multiformats/multicodec +- https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table +- https://github.com/multiformats/multicodec/blob/master/table.csv#L130 + +## Response summary + +Add WireGuard (`wg`) as a new multiaddr protocol with draft code +`0x01C7` (an unassigned slot between `noise` 0x01C6 and `shs` 0x01C8 +in the multicodec table). The codec handles 32-byte Curve25519 public +keys encoded as standard base64, matching `wg(8)` tooling conventions. +Example multiaddr: `/ip4/1.2.3.4/udp/51820/wg/`. + +## Files changed + +- `multiaddr/protocols.py` — Add `P_WG = 0x01C7` constant and + `Protocol(P_WG, "wg", "wg")` entry in `PROTOCOLS` list +- `multiaddr/codecs/wg.py` — New codec for 32-byte Curve25519 + public keys (SIZE=256, standard base64 encoding) +- `tests/test_protocols.py` — Add `wg` codec unit tests (roundtrip, + validation, error paths, registry lookup, integration) +- `tests/test_multiaddr.py` — Add valid and invalid WireGuard + multiaddr string test cases + +## Human edits + +None — committed as generated. diff --git a/ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.raw.md b/ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.raw.md new file mode 100644 index 0000000..f5b1db9 --- /dev/null +++ b/ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.raw.md @@ -0,0 +1,154 @@ +--- +model: claude-opus-4-6 +service: claude +timestamp: 2026-04-15T00:24:53Z +git_ref: 0b6493a +--- + +Let me explore the codebase and check the upstream spec simultaneously. + +[Launched Explore agent to thoroughly explore py-multiaddr codebase structure, protocol definitions, codec patterns, and tests] + +[Fetched https://github.com/multiformats/multiaddr/blob/master/protocols.csv — no WireGuard/wg entry found in upstream spec] + +The upstream `protocols.csv` doesn't yet include a `wg` entry, so we'll need to pick a code. Let me read the key files to understand the exact patterns before implementing. + +[Read multiaddr/protocols.py, multiaddr/codecs/__init__.py, multiaddr/codecs/uint16be.py] + +Now let me also check the upstream multicodec table for any existing WireGuard code assignment. + +[Fetched https://raw.githubusercontent.com/multiformats/multicodec/master/table.csv — no wireguard/wg entry found. Available slots in 0x01C0-0x01D0 range: 0x01c2, 0x01c3, 0x01c4, 0x01c5, 0x01c7, 0x01c9, 0x01ca, 0x01cb, 0x01ce, 0x01cf, 0x01d0] + +[Read tests/test_protocols.py, tests/test_multiaddr.py] + +I have a clear picture. Here's the plan: + +- **Protocol code**: `0x01C7` (available slot between `noise` 0x01C6 and `shs` 0x01C8 in the multicodec table) +- **Codec**: 32-byte (256-bit) Curve25519 public key, base64 encoded (standard WireGuard key format) +- **Multiaddr example**: `/ip4/1.2.3.4/udp/51820/wg/` + +Let me implement all three pieces. + +### Edit 1: multiaddr/protocols.py — Add P_WG constant + +Added `P_WG = 0x01C7` before `P_MEMORY`. + +### Edit 2: multiaddr/protocols.py — Add Protocol entry + +Added `Protocol(P_WG, "wg", "wg"),` after the `noise` entry in the PROTOCOLS list. + +### New file: multiaddr/codecs/wg.py + +```python +""" +WireGuard protocol codec. + +Encode/decode a 32-byte Curve25519 public key as standard +base64 (the canonical format used by `wg(8)` tooling). + +The protocol code `0x01C7` is a draft allocation not yet +present in the upstream multicodec table: +- https://github.com/multiformats/multicodec/blob/master/table.csv +- https://github.com/multiformats/multiaddr/blob/master/protocols.csv +- https://multiformats.io/multiaddr/ + +See also the upstream multicodec addition process: +- https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table +""" + +import base64 +from typing import Any + +from ..codecs import CodecBase +from ..exceptions import BinaryParseError + +SIZE = 256 # 32 bytes * 8 bits +IS_PATH = False + +WG_KEY_LENGTH = 32 # Curve25519 public key + + +class Codec(CodecBase): + SIZE = SIZE + IS_PATH = IS_PATH + + def to_bytes(self, proto: Any, string: str) -> bytes: + try: + raw = base64.b64decode(string, validate=True) + except Exception as exc: + raise ValueError( + f"invalid base64 WireGuard public key: {exc}" + ) from exc + + if len(raw) != WG_KEY_LENGTH: + raise ValueError( + f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " + f"got {len(raw)}" + ) + return raw + + def to_string(self, proto: Any, buf: bytes) -> str: + if len(buf) != WG_KEY_LENGTH: + raise BinaryParseError( + f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " + f"got {len(buf)}", + buf, + "wg", + ) + return base64.b64encode(buf).decode("ascii") + + def validate(self, b: bytes) -> None: + if len(b) != WG_KEY_LENGTH: + raise ValueError( + f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " + f"got {len(b)}" + ) +``` + +### Edit 3: tests/test_protocols.py — Add wg import and tests + +Added `wg` to codec imports. + +Added test functions: +- `test_wg_valid_roundtrip` — base64 encode/decode of random 32-byte key +- `test_wg_zero_key_roundtrip` — all-zeros key roundtrip +- `test_wg_invalid_base64_raises` — rejects invalid base64 +- `test_wg_wrong_length_string_raises` — rejects 16-byte and 64-byte keys +- `test_wg_wrong_length_bytes_raises` — rejects wrong-length binary buffers +- `test_wg_validate` — direct validate() method testing +- `test_wg_protocol_lookup` — registry lookup by name +- `test_wg_integration` — full Multiaddr roundtrip with `/ip4/1.2.3.4/udp/51820/wg/` + +### Edit 4: tests/test_multiaddr.py — Add invalid cases + +Added to invalid test params: +- `/wg` — missing key +- `/wg/not-valid-base64` — invalid base64 +- `/ip4/1.2.3.4/udp/51820/wg` — missing key after full address + +### Edit 5: tests/test_multiaddr.py — Add valid cases + +Added to valid test params: +- `/ip4/1.2.3.4/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` +- `/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` + +### Test run + +All 269 tests pass. + +### Summary + +**3 files touched:** + +1. **`multiaddr/protocols.py`** — Added `P_WG = 0x01C7` constant and `Protocol(P_WG, "wg", "wg")` entry in the `PROTOCOLS` list (placed after `noise`). + +2. **`multiaddr/codecs/wg.py`** (new) — Codec for 32-byte Curve25519 public keys using standard base64 encoding (the canonical format from `wg(8)` tooling). Fixed size of 256 bits. Includes docstring referencing the upstream multicodec/multiaddr spec repos and the addition process. + +3. **`tests/test_protocols.py`** + **`tests/test_multiaddr.py`** — Added codec roundtrip, validation, error-path tests, protocol registry lookup, integration tests, and valid/invalid multiaddr string cases. + +**Protocol details:** +- Code: `0x01C7` (draft, between `noise` 0x01C6 and `shs` 0x01C8 — not yet allocated upstream) +- Codec: fixed 256-bit (32-byte) Curve25519 public key, standard base64 +- Example multiaddr: `/ip4/1.2.3.4/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` + +Regarding the upstream multicodec table — `0x01C7` is currently unassigned. To make this official, a PR to multiformats/multicodec adding `wg,multiaddr,0x01c7,draft` to `table.csv` would be needed per their adding new multicodecs process (similar to how `shs`/Secure Scuttlebutt got `0x01c8`). diff --git a/ai/prompt-io/claude/README.md b/ai/prompt-io/claude/README.md new file mode 100644 index 0000000..b93e8ad --- /dev/null +++ b/ai/prompt-io/claude/README.md @@ -0,0 +1,27 @@ +# AI Prompt I/O Log — claude + +This directory tracks prompt inputs and model +outputs for AI-assisted development using +`claude` (Claude Code). + +## Policy + +Prompt logging follows the +[NLNet generative AI policy][nlnet-ai]. +All substantive AI contributions are logged +with: +- Model name and version +- Timestamps +- The prompts that produced the output +- Unedited model output (`.raw.md` files) + +[nlnet-ai]: https://nlnet.nl/foundation/policies/generativeAI/ + +## Usage + +Entries are created by the `/prompt-io` skill +or automatically via `/commit-msg` integration. + +Human contributors remain accountable for all +code decisions. AI-generated content is never +presented as human-authored work. diff --git a/multiaddr/codecs/wg.py b/multiaddr/codecs/wg.py new file mode 100644 index 0000000..d8df787 --- /dev/null +++ b/multiaddr/codecs/wg.py @@ -0,0 +1,63 @@ +""" +WireGuard protocol codec. + +Encode/decode a 32-byte Curve25519 public key as standard +base64 (the canonical format used by `wg(8)` tooling). + +The protocol code `0x01C7` is a draft allocation not yet +present in the upstream multicodec table: +- https://github.com/multiformats/multicodec/blob/master/table.csv +- https://github.com/multiformats/multiaddr/blob/master/protocols.csv +- https://multiformats.io/multiaddr/ + +See also the upstream multicodec addition process: +- https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table +""" + +import base64 +from typing import Any + +from ..codecs import CodecBase +from ..exceptions import BinaryParseError + +SIZE = 256 # 32 bytes * 8 bits +IS_PATH = False + +WG_KEY_LENGTH = 32 # Curve25519 public key + + +class Codec(CodecBase): + SIZE = SIZE + IS_PATH = IS_PATH + + def to_bytes(self, proto: Any, string: str) -> bytes: + try: + raw = base64.b64decode(string, validate=True) + except Exception as exc: + raise ValueError( + f"invalid base64 WireGuard public key: {exc}" + ) from exc + + if len(raw) != WG_KEY_LENGTH: + raise ValueError( + f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " + f"got {len(raw)}" + ) + return raw + + def to_string(self, proto: Any, buf: bytes) -> str: + if len(buf) != WG_KEY_LENGTH: + raise BinaryParseError( + f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " + f"got {len(buf)}", + buf, + "wg", + ) + return base64.b64encode(buf).decode("ascii") + + def validate(self, b: bytes) -> None: + if len(b) != WG_KEY_LENGTH: + raise ValueError( + f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " + f"got {len(b)}" + ) diff --git a/multiaddr/protocols.py b/multiaddr/protocols.py index 43c9ffa..37d7bf9 100644 --- a/multiaddr/protocols.py +++ b/multiaddr/protocols.py @@ -83,6 +83,7 @@ P_WEBTRANSPORT = 0x01D1 P_WEBRTC_DIRECT = 0x118 P_WEBRTC = 0x119 +P_WG = 0x01C7 P_MEMORY = 0x309 P_CERTHASH = 0x1D2 @@ -157,6 +158,7 @@ def __repr__(self) -> str: Protocol(P_DNSADDR, "dnsaddr", "domain"), Protocol(P_SNI, "sni", "domain"), Protocol(P_NOISE, "noise", None), + Protocol(P_WG, "wg", "wg"), Protocol(P_SCTP, "sctp", "uint16be"), Protocol(P_UDT, "udt", None), Protocol(P_UTP, "utp", None), diff --git a/tests/test_multiaddr.py b/tests/test_multiaddr.py index da402e5..af8023e 100644 --- a/tests/test_multiaddr.py +++ b/tests/test_multiaddr.py @@ -83,6 +83,9 @@ "/dns4", "/dns6", "/cancer", + "/wg", + "/wg/not-valid-base64", + "/ip4/1.2.3.4/udp/51820/wg", ], ) def test_invalid(addr_str): @@ -139,6 +142,8 @@ def test_invalid(addr_str): "/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct", "/ip4/127.0.0.1/tcp/127/webrtc-direct", "/ip4/127.0.0.1/tcp/127/webrtc", + "/ip4/1.2.3.4/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "/certhash/uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g" "/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g", "/ip4/127.0.0.1/udp/1234/quic-v1/webtransport/certhash/u1QEQOFj2IjCsPJFfMAxmQxLGPw", diff --git a/tests/test_protocols.py b/tests/test_protocols.py index 3d9c515..615ce2b 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -7,7 +7,7 @@ import varint from multiaddr import Multiaddr, exceptions, protocols -from multiaddr.codecs import certhash, garlic32, garlic64, http_path, ipcidr, memory +from multiaddr.codecs import certhash, garlic32, garlic64, http_path, ipcidr, memory, wg from multiaddr.exceptions import BinaryParseError, StringParseError @@ -625,3 +625,84 @@ def test_certhash_validate_function(): # Invalid bytes should raise a ValueError with pytest.raises(ValueError): codec.validate(INVALID_BYTES) + + +# --- WireGuard (wg) Tests --- +# A valid 32-byte Curve25519 public key (random) +VALID_WG_KEY_BYTES = os.urandom(32) +VALID_WG_KEY_STRING = base64.b64encode(VALID_WG_KEY_BYTES).decode("ascii") + +# A well-known test key (all zeros) +ZERO_WG_KEY_BYTES = b"\x00" * 32 +ZERO_WG_KEY_STRING = base64.b64encode(ZERO_WG_KEY_BYTES).decode("ascii") + + +def test_wg_valid_roundtrip(): + codec = wg.Codec() + + b = codec.to_bytes(None, VALID_WG_KEY_STRING) + assert isinstance(b, bytes) + assert len(b) == 32 + assert b == VALID_WG_KEY_BYTES + + s_out = codec.to_string(None, b) + assert s_out == VALID_WG_KEY_STRING + + +def test_wg_zero_key_roundtrip(): + codec = wg.Codec() + + b = codec.to_bytes(None, ZERO_WG_KEY_STRING) + assert b == ZERO_WG_KEY_BYTES + assert codec.to_string(None, b) == ZERO_WG_KEY_STRING + + +def test_wg_invalid_base64_raises(): + codec = wg.Codec() + with pytest.raises(ValueError): + codec.to_bytes(None, "not-valid-base64!!!") + + +def test_wg_wrong_length_string_raises(): + codec = wg.Codec() + # 16 bytes encoded as base64 (too short) + short_key = base64.b64encode(os.urandom(16)).decode("ascii") + with pytest.raises(ValueError): + codec.to_bytes(None, short_key) + + # 64 bytes encoded as base64 (too long) + long_key = base64.b64encode(os.urandom(64)).decode("ascii") + with pytest.raises(ValueError): + codec.to_bytes(None, long_key) + + +def test_wg_wrong_length_bytes_raises(): + codec = wg.Codec() + with pytest.raises(BinaryParseError): + codec.to_string(None, os.urandom(16)) + with pytest.raises(BinaryParseError): + codec.to_string(None, os.urandom(64)) + + +def test_wg_validate(): + codec = wg.Codec() + codec.validate(VALID_WG_KEY_BYTES) + + with pytest.raises(ValueError): + codec.validate(os.urandom(31)) + with pytest.raises(ValueError): + codec.validate(os.urandom(33)) + + +def test_wg_protocol_lookup(): + proto = protocols.protocol_with_name("wg") + assert proto.name == "wg" + assert proto.code == protocols.P_WG + assert proto.codec == "wg" + assert proto.size == 256 + + +def test_wg_integration(): + ma = Multiaddr(f"/ip4/1.2.3.4/udp/51820/wg/{VALID_WG_KEY_STRING}") + assert str(ma) == f"/ip4/1.2.3.4/udp/51820/wg/{VALID_WG_KEY_STRING}" + assert ma.value_for_protocol(protocols.P_WG) == VALID_WG_KEY_STRING