Writing your first extension
This guide walks through publishing a tiny plane codec end-to-end: scaffold the project, write the encode/decode functions, sign the bundle, install it, and verify PTWM picks it up at runtime.
Total time: ~15 minutes. You'll need a Rust toolchain with the
wasm32-wasip1 target, plus the extension-author extras:
rustup target add wasm32-wasip1
pip install 'ptwm[ext-dev]'
1. Scaffold
ptwm ext init xor-codec --lang rust --kind plane_codec
cd xor-codec
ls
You get:
Cargo.toml manifest.toml src/lib.rs
The Rust crate is configured as a cdylib targeting
wasm32-wasip1. manifest.toml declares one plane_codec
contribution with a placeholder canonical id (rewritten at sign
time).
src/lib.rs exports the byte-buffer ABI symbols
(see the extension system doc for the full plane_codec signature):
#[unsafe(no_mangle)]
pub extern "C" fn ptwm_plane_codec_v1_init() -> u32 { 0 }
#[unsafe(no_mangle)]
pub extern "C" fn ptwm_plane_codec_v1_cleanup(_state: u32) {}
#[unsafe(no_mangle)]
pub extern "C" fn ptwm_plane_codec_v1_encode(
_state: u32,
in_ptr: u32, in_len: u32,
out_ptr: u32, out_len: u32,
) -> i64 { /* copy input → output */ }
#[unsafe(no_mangle)]
pub extern "C" fn ptwm_plane_codec_v1_decode(
_state: u32,
in_ptr: u32, in_len: u32,
out_ptr: u32, out_len: u32,
) -> i64 { /* copy input → output */ }
The generated stub is a passthrough — input bytes flow out unchanged.
A real codec replaces the bodies with entropy coding; for the tutorial
we'll XOR every byte with 0x5A.
2. Edit src/lib.rs
Replace the encode and decode bodies:
const KEY: u8 = 0x5A;
#[unsafe(no_mangle)]
pub extern "C" fn ptwm_plane_codec_v1_encode(
_state: u32,
in_ptr: u32, in_len: u32,
out_ptr: u32, out_len: u32,
) -> i64 {
if out_len < in_len { return -2; }
let input = unsafe { std::slice::from_raw_parts(in_ptr as *const u8, in_len as usize) };
let output = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut u8, out_len as usize) };
for (i, b) in input.iter().enumerate() {
output[i] = b ^ KEY;
}
in_len as i64
}
Decode is identical (XOR is self-inverse). Keep init returning 0
and cleanup empty — this codec has no per-call state.
3. Build
ptwm ext build
Behind the scenes this runs cargo build --target wasm32-wasip1 --release and copies the resulting .wasm to the bundle root. The
output:
target/wasm32-wasip1/release/xor_codec.wasm [intermediate]
xor-codec.wasm [bundle artifact]
4. Sign
ptwm ext sign --key <path> takes a 32-byte raw Ed25519 seed file.
The v1 CLI doesn't ship a gen-key helper, so generate one with
openssl (or any source of 32 random bytes):
mkdir -p ~/.ptwm/keys
openssl rand 32 > ~/.ptwm/keys/dev.priv
chmod 600 ~/.ptwm/keys/dev.priv
Then sign the bundle:
ptwm ext sign --key ~/.ptwm/keys/dev.priv
ext sign rewrites manifest.toml so that:
[bundle].author_pubkeybecomes the realed25519:<hex>of the key.- Every
[[contributions]].idbecomes the realblake3:<hex>canonical id (derived from your pubkey + name + version). signature.binis written next to the manifest: a 64-byte Ed25519 signature overblake3(manifest_bytes ‖ xor-codec.wasm).
Tampering with either the manifest or the binary after this step invalidates the signature; the container reader will reject the bundle.
5. Pack & install
After signing, manifest.toml carries your ed25519:<hex> public key
under [bundle].author_pubkey. Read it out and add it to your
keyring before installing:
ptwm ext pack # produces xor-codec-0.1.0.tar.zst
# Pull the public key from the signed manifest and trust it locally.
PUBKEY=$(python -c \
'import tomllib, pathlib; \
print(tomllib.loads(pathlib.Path("manifest.toml").read_text())["bundle"]["author_pubkey"])')
ptwm trust add --key "$PUBKEY" --label dev
ptwm ext install ./xor-codec-0.1.0.tar.zst
ptwm ext list
The trust add --key step tells the user keyring to accept any
contribution signed by this author. Without it, ext install would
refuse the bundle as untrusted.
6. Confirm it loads
ptwm ext show xor-codec
Look for pubkey: ed25519:<your-hex> — that's the keyring matching
the bundle's author. The contribution count, canonical id, and
installed flavors are also reported.
To exercise the new codec end-to-end, run the ablation harness so a
policy variant can target it. Write a policy that allows just your
new codec, then run ptwm bench compress so the dispatcher attempts
it against the built-ins:
python -c 'print("Hello, " * 50, end="")' > sample.bin
cat > policy.toml <<EOF
[allow]
extra = ["blake3:<canonical-id-from-show>"]
EOF
ptwm bench compress sample.bin --variant policy.toml --out bench-out/
bench compress writes a CSV under bench-out/. The
resolved_policy_toml column records exactly which codecs were
eligible per row; the compressed_bytes and ratio columns reflect
the dispatcher's choice. Since XOR is roughly identity for cost
(ratio ≈ 1.0), Huffman typically wins on a real model — but the
attempt proves the bundle loaded, verified, and ran.
Where to go from here
- Extension system design — full design rationale and per-kind ABI signatures.
- Reference extensions — one passthrough per kind, in either Rust or Python.
- Trust and security — how to harden your keyring before shipping to others.
- Extensibility concept — the wider design.
Common pitfalls
- Forgot to add
wasm32-wasip1—ptwm ext buildwill reportlinker error: …unknown target. Fix:rustup target add wasm32-wasip1. - Tampered with binary after sign — install will fail with
ContributionUntrusted. Re-runptwm ext signto refresh the signature. - Author key not in keyring — same error. Run
ptwm trust add --key …first. out_len < in_len— return-2(BufferTooSmall). The dispatcher will then skip your codec and pick a different one.