Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.md
Original file line number Diff line number Diff line change
@@ -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/<base64-pubkey>`.

## 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.
154 changes: 154 additions & 0 deletions ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.raw.md
Original file line number Diff line number Diff line change
@@ -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/<base64-pubkey>`

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/<key>`

### 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`).
27 changes: 27 additions & 0 deletions ai/prompt-io/claude/README.md
Original file line number Diff line number Diff line change
@@ -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.
63 changes: 63 additions & 0 deletions multiaddr/codecs/wg.py
Original file line number Diff line number Diff line change
@@ -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")
Comment on lines +33 to +56
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wg uses standard base64, which can include the '/' character. Because multiaddr strings are '/'-delimited and _from_string splits on '/', any key containing '/' cannot be represented or parsed, and bytes_to_string() may emit an invalid/ambiguous multiaddr. Encode reserved characters in to_string (at least '/') and decode them in to_bytes before base64 decoding (or switch to a URL-safe encoding); add a roundtrip test that includes a key whose base64 contains '/'.

Copilot uses AI. Check for mistakes.

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)}"
)
2 changes: 2 additions & 0 deletions multiaddr/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
P_WEBTRANSPORT = 0x01D1
P_WEBRTC_DIRECT = 0x118
P_WEBRTC = 0x119
P_WG = 0x01C7
P_MEMORY = 0x309
P_CERTHASH = 0x1D2

Expand Down Expand Up @@ -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),
Expand Down
5 changes: 5 additions & 0 deletions tests/test_multiaddr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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=",
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added wg valid cases only cover base64 strings without reserved multiaddr delimiters. Once wg supports escaping (or uses URL-safe encoding), add a valid case that includes a key whose encoded form would contain '/' to ensure string parsing and str(ma) roundtrip are reliable.

Suggested change
"/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F=",

Copilot uses AI. Check for mistakes.
"/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",
Expand Down
Loading
Loading