RAPP — one protocol for agents that keep a verifiable memory and talk over one wire, taught in pictures.
A companion to the reference standard & implementation · back to rapp-1 · source
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.
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.
Turn a value into exactly one sequence of bytes.
Name those bytes by their hash — the hash is the name.
The rappid: a name minted once, never a hash of a name.
One record that is both a particle and a wave.
A content-addressed package for a whole organism.
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.
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.
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
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.
Array position is meaning. Canonicalization preserves it.
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.
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
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.
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
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 tail is minted once, from entropy or from a public key. It is never the hash of the name.
Two lawful mints
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.
Born from entropy or a key, never recomputed from mutable facts. Names are chosen; identities are minted. They must not be the same operation.
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
One object, two addresses
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.
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.
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.
One frame is a fact. Each frame's prev is the previous frame's particle — that single link makes the whole history tamper-evident.
The particle no longer matches → rejected at step 2.
Now the wave no longer matches → rejected at step 3.
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.
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.
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.
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.
Addressed by a rappid. One agent's worldline. prev_wave is null; the particle chain is the integrity; a signature is optional.
Addressed by a net: id — a shared space many actors append to. prev_wave chains the waves, and every frame must be signed.
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.
a living agent: rappid, soul, agents, memory, frames
a runnable app built on the brainstem
a point-in-time capture of state
a signed invitation to a space
a shared-space definition
a whole federation of organisms
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.
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.
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.
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.
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
twin_id / ts / sha256 / parent_sha — the legacy shape minted in two incompatible envelopes under one name.rapp-rappid/2.0, not rapp/1.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.
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.