RAPP · The Visual Guide · RAPP rev-5

Design & Build
Agents

RAPP — one protocol for agents that keep a verifiable memory and talk over one wire, taught in pictures.

Canonicalization Content addressing Identity The frame The egg

A companion to the reference standard & implementation · back to rapp-1 · source

why
1

What is RAPP?

A medium for agents — a way to write down what an agent did, address it by its content, chain it into a life, and hand it to anyone.

RAPP is a protocol, in the same sense that HTTP, JSON, and git's object model are protocols: a small number of exact rules that let programs written by strangers produce bytes each other can trust.

In plain English

It is not a framework or an app. It is a shared agreement about bytes on a wire. Follow the rules and your agent's memory can be verified by anyone, anywhere, forever — no shared database, no trust required.

Everything in RAPP is built from five primitives. The whole rest of this guide is these five, one chapter at a time.

One

Canonicalization

Turn a value into exactly one sequence of bytes.

Two

Content addressing

Name those bytes by their hash — the hash is the name.

Three

Identity

The rappid: a name minted once, never a hash of a name.

Four

The frame

One record that is both a particle and a wave.

Five

The egg

A content-addressed package for a whole organism.

Why a protocol, and why now

The RAPP ecosystem is real, and it drifted. The same idea — “a frame,” “a rappid” — got built more than once, in incompatible ways, each copy claiming the same name. A frame was minted twice under one version string with two different hash rules. An identity was computed three ways in production, one of them the cardinal sin: hashing a name into an address.

This is the oldest failure in distributed systems, and it was solved long ago — by Linux's one mainline, by the Web's single living standard, by git making the hash the name. RAPP is the convergence: one spec, one canonicalizer, one mint, one frame.

bytes
2

Canonicalization

Between a value and its hash sits one question that has sunk more systems than any other: which bytes?

{"a":1,"b":2} and {"b":2,"a":1} are the same value and different bytes. If two programs disagree about which bytes represent a value, they hash it differently — and every promise above breaks silently.

The fix

One rule, shared by everyone: RFC 8785 (JCS). Sort object keys. No spaces. Keep array order. Reject duplicate keys. Now every value has exactly one byte form.

The same value → one canonical form

# two ways to write the same thing {"b":1, "a":[3,2]} {"a":[3,2], "b":1} │ ▼ canonical() {"a":[3,2],"b":1} # identical bytes, always
Try it — copy this prompt

In my clone of https://github.com/kody-w/rapp-1, canonicalize {'b':1,'a':[3,2]} with rapp.py and show me why both key orders give identical bytes.

Two rules that surprise people

✓ order matters in arrays
[1,2] ≠ [2,1]

Array position is meaning. Canonicalization preserves it.

✗ order is erased in objects
{"a":1,"b":2} = {"b":2,"a":1}

Key order is not meaning. Canonicalization sorts it away.

There is no Unicode normalization and no schema coercion — both would make the same value hash differently on different machines. The bytes you put in are the bytes that are hashed.

hash
3

Content addressing

Name content by its hash and something magical follows: two things with the same address are the same thing.

git built its whole object store on this. So does RAPP. But there is a trap that quietly brings the collisions back — and the fix is domain separation.

The trap

Hash a payload, a public key, and an egg all with plain SHA-256, and a value that can appear in more than one role gets the same address in every role. You built a system where a payload address and an identity can collide — not from a hash weakness, but because raw hashes were used as addresses in several namespaces at once.

The fix — tag the space, then hash

H(space, v) = SHA256( space ‖ 0x0A ‖ canonical(v) ) # same value {"x":1}, three spaces: H("rapp/1:particle", v) → a1b2… H("rapp/1:wave", v) → c3d4… H("rapp/1:egg", v) → e5f6… # three different addresses, by design
Why it works

The tag is part of what gets hashed, so a payload address can never equal a frame address can never equal an identity — even when the underlying bytes are identical. Cross-space collision isn't unlikely; it is unrepresentable.

One hash, everywhere: SHA-256, lowercase, 64 hex characters. Not a menu. A protocol whose hash is negotiable is really several protocols, and an attacker picks the weakest.

name
4

Identity — the rappid

An agent needs a name that stays the same as its content changes. This is the one place where content addressing is wrong.

Anatomy of a rappid

rappid:@the scheme kody-wowner — who / rapp-bodyslug — what : 324197c1…a8ba71f6the minted 64-hex tail — the identity

The @owner/slug part is a convenience for humans and resolvers. The identity is the 64-hex tail, and where it comes from is the single most important rule in RAPP.

The one rule

The tail is minted once, from entropy or from a public key. It is never the hash of the name.

Two lawful mints

# keyless — anchored on random entropy tail = Hb("rapp/1:rappid", uuid4_bytes) # keyed — anchored on the public key tail = Hb("rapp/1:rappid", SPKI_DER) # verifiable: recompute from the key

Why a name-hash is fatal

✗ forbidden
tail = sha256("kody/twin")

Anyone who names something kody/twin computes the same tail. Identity is the name in hex — it collides by construction, and can't be bound to a key. This was live in production. §6.2 outlaws it.

✓ minted once
tail = Hb("rapp/1:rappid", uuid4)

Born from entropy or a key, never recomputed from mutable facts. Names are chosen; identities are minted. They must not be the same operation.

both
5

The frame

The record of one moment in an agent's life: tamper-evident, content-addressed, chained to the moment before it — and both a particle and a wave.

Eleven fields — always all eleven

a framerapp/1
spec"rapp/1"
kindnoun.verb — the event type
stream_idthe rappid this belongs to
seqposition in the chain (0, 1, 2…)
utcmillisecond timestamp, always Z
payloadthe content of the moment
payload_hashthe particle — hash of the payload
frame_hashthe wave — hash of the whole frame
prevprevious frame's particle
prev_waveprevious wave (swarm streams)
sigsignature, or null
particle — the worldline wave — the wire

One object, two addresses

Particle

the address of what happened

H("rapp/1:particle", payload)

Follows the worldline. Each frame's prev points to the previous particle. This is the chain.

Wave

the address of this exact record

H("rapp/1:wave", frame∖{frame_hash,sig})

Checks the wire — envelope and all. Byte-for-byte integrity in transit.

The double-slit idea

A frame is one object that is both. You never choose when you emit — you emit one frame and observe the particle when you follow the life, the wave when you check the wire. This resolves the real bug where two teams built “the frame” with two different hash rules.

A chain is a biography

One frame is a fact. Each frame's prev is the previous frame's particle — that single link makes the whole history tamper-evident.

seq 0 · genesis
particle 7ab0…
prev: null
prev
seq 1
particle 8c69…
prev: 7ab0…
prev
seq 2
particle 946e…
prev: 8c69…

Try to forge the past — you must beat three layers

Edit a past payload

The particle no longer matches → rejected at step 2.

Recompute the particle to cover it

Now the wave no longer matches → rejected at step 3.

Recompute the wave too

The new particle isn't what the next frame recorded as its prev → rejected at step 4. To fix that you must forge every frame to the head.

The chain turns “rewrite one moment” into “rewrite the entire history from that moment forward.” That is exactly the property we wanted.

one door
6

The wire

Frames are the record. The wire is how they move — and RAPP keeps it to exactly one door.

Everything an agent can do enters through one endpoint: POST /chat. New capability is a new agent file dropped in — never a new route. The wire does not grow.

One shape, every tier

Local laptop, cloud endpoint, managed studio — all speak the identical /chat. Moving tiers is a change of URL, not of protocol. The moment there are two doors, the tiers drift apart.

POST /chat → request { "user_input": "deploy the notes agent", "session_id": "…" } ← response { "response": "…", "agent_logs": "…", "session_id": "…" }

Only user_input is required. Errors are typed, not prose: 422 malformed, 401 needs a token, and a bad frame comes back with the exact verify step that failed.

◆ biography stream

Addressed by a rappid. One agent's worldline. prev_wave is null; the particle chain is the integrity; a signature is optional.

◆ swarm stream

Addressed by a net: id — a shared space many actors append to. prev_wave chains the waves, and every frame must be signed.

handoff
7

The egg

To give an agent to someone, you pack its identity, code, soul, and memory into one addressable thing.

An egg is a stored (uncompressed) ZIP — a manifest plus the files it references, each content-addressed. One spec, six named variants distinguished by a field, never a fork.

organism

a living agent: rappid, soul, agents, memory, frames

rapplication

a runnable app built on the brainstem

session

a point-in-time capture of state

invite

a signed invitation to a space

neighborhood

a shared-space definition

estate

a whole federation of organisms

Born from a frame

An organism egg can carry a constructor pin — the exact {stream_id, seq, particle} of the parent frame it hatched from. Anyone can refetch that frame, recompute, and prove the origin moment. Provenance becomes a computation, not a claim.

Same idea as the frame

The egg content-addresses “what it contains” (the manifest hash) and “these exact bytes” (the egg hash, excluding the signature so it can be signed later) — the frame's particle/wave duality, at organism scale.

proof
8

Meeting a real world

A specification you cannot test is a wish. So we pointed RAPP at a real, committed, drifted estate.

The reference implementation was run against every frame and rappid actually committed to the live public repos — written by other programs, months before the spec existed. Two numbers told the before-story.

32 / 32

real committed payload hashes the reference canonicalizer reproduced byte-for-byte, even mid-drift. The spec matches reality exactly where reality already content-addresses. Every chain link held, too.

8

live drifts the protocol refused — legacy envelopes and short-tail identities. These refusals are the drift the standard exists to end.

The drift ledger — the captured before-state, generated by running code, not asserted

envelope · C1all 32 frames use twin_id / ts / sha256 / parent_sha — the legacy shape minted in two incompatible envelopes under one name.
identity · C3twin & rapp-commons rappids carry a 32-hex tail — the short-form name-hash lineage RAPP outlaws.
schema labelall four records still say rapp-rappid/2.0, not rapp/1.
What this proves

Nothing in that output was a spec bug — the refusals were the spec working. The owner then ran the authorized re-genesis, and the same code that computed the drift now computes the green: the captured 2026-08-20 audit accepted 46/46 frames with zero drift, and the 2026-08-26 re-observation of the grown estate accepted 50/50. Before and after are the same program, pointed at the same public repos.

See it yourself — copy this prompt, you never type code

Paste it into your brainsurgeon — an agentic CLI like Claude Code or GitHub Copilot CLI — and watch:

Clone https://github.com/kody-w/rapp-1 and prove RAPP to me: run python3 conformance.py (I expect 16/16 PASS), then python3 realcheck.py — the spec against the live public estate — and explain the verdict in plain English. Show real output and stop on any red result.

Every task has a prompt like this in the prompt book.