| Internet-Draft | RAPP/1 | October 2026 |
| Wildfeuer | Expires 20 April 2027 | [Page] |
RAPP/1 is a profile over existing standards that lets independent programs exchange verifiable, content-addressed records about agents: one canonicalization (JCS), one domain-separated hash, one mint-once identity, one eleven-key event envelope, one wire, and one package format. Two independent implementations that follow this document produce byte-identical artifacts with no out-of-band agreement. The normative text of record is the append-only specification chain published by the author; this document is a stable, archival rendering of it: revision rev-18, chain frame 83bec7762a04959dc91e252055d79f67690c605383018d23b26b99982916dd6a, normative SHA-256 e81e20ba9402ee96422f73e9ef737eba100264155c8f77521600766be8fb21e9. Any later revision supersedes this rendering; the chain, not this document, says which is current.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 20 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
RAPP is a content-addressed distributed organism. Its integrity rests on one invariant: the same
concept has the same bytes everywhere. This document specifies, normatively and completely, five
load-bearing primitives so any two independent implementations interoperate byte-for-byte with no
out-of-band agreement: canonicalization (§4), content addressing (§5), identity (§6), the frame (§7),
the egg (§9) — all riding one wire (§8): POST /chat, or a signed append-only frame. Implementations add
agents, cartridges, and registered kinds — never new endpoints, never new envelopes.¶
Repository scope. kody-w/RAPP is the canonical public RAPP foundation,
product home, reference implementation, organism model, and philosophy.
kody-w/rapp-1 is the canonical interoperable protocol authority. This
specification governs wire-compatible bytes and conformance; it does not absorb
the RAPP product or any downstream Rappter/RapterBox LLC product.¶
Specification authority. The append-only DOGG chain at
anchor/chain.jsonl carries the normative revision content. Its hashes prove
integrity, not authority or ratification. Until an authenticated RAPP
registry/checkpoint exists, the chain accepted by the owner onto protected
canonical kody-w/rapp-1 main is authoritative; an immutable accepted commit
plus head frame hash is the portable checkpoint. SPEC.md is the byte-exact
materialized human view, and orient.json is only a discovery beacon. The
selected chain-frame hash, not a mutable path or rev-N label, is the durable
identity of a protocol revision (§12.2).¶
L5 EGG cartridge packaging (§9) — MIME-multipart analogue
L4 FRAME universal event envelope (§7) — the IP packet of RAPP
L3 WIRE transport: /chat + frames (§8) — HTTP-analogue single method
L2 IDENTITY rappid namespace + trust (§6,§10) — URI + PKI analogue
L1 ADDRESS canonicalization + hash (§4,§5) — the git object model
A higher layer MUST NOT redefine a lower one. Every layer names exactly one canonical form.¶
The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, MAY are as in [RFC2119]/ [RFC8174] when in bold caps. (The Federal Constitution adopts the same clause.)¶
organism — a running brainstem with persistent identity. rappid — the eternal, mint-once name of an
organism/door/object (§6): minted once from a UUIDv4 or a public key, never derived from content or name.
frame — one immutable content-addressed event (§7). stream — an append-only hash-chained frame
sequence sharing one stream_id. particle / wave — a frame's two domain-separated addresses:
payload-hash / whole-frame-hash (§7.3). canonical form — the one [RFC8785] byte string for a value
(§4). legacy form — any other historical encoding; legacy is drift and MUST be migrated out
(Fed. Const. Art. III), except sealed re-genesis history (§12.1).
Grail kernel — an estate-declared, byte-pinned runtime substrate. Its grail_id is
"grail:" || Hb("rapp/1:grail", kernel_bytes); repository, immutable ref, commit, path, raw SHA-256,
and byte length are provenance and verification data, not alternate identities. Once activated through
§13.3, those bytes are permanent (Protocol Constitution Art. 15); capabilities evolve outside them.
release capsule — the canonical rapp-cicd/1-release payload identifying one immutable candidate.
serving lineage — the sequence of qualified release identities that have received user traffic;
the currently served release is immutable even while a separate candidate lineage grows.
deployment cell — an independently observable and isolatable runtime failure domain governed by
rapp-deploy/1.¶
canonical(v) is the UTF-8 byte string produced by [RFC8785] JCS for the value v, defined only
over I-JSON [RFC7493]. JCS fixes member-name ordering (UTF-16 code-unit), string escaping, and number
serialization (ECMAScript Number::toString, ECMA-262); there is no insignificant whitespace and no
byte-order mark. Input JSON text MUST be well-formed UTF-8 [RFC7493] §2.1; an implementation MUST
refuse (not strip) a leading byte-order mark and MUST NOT transcode UTF-16 or UTF-32 input.
(Insignificant whitespace in input text is accepted; it is absent only from the canonical form.)¶
RAPP input-domain profile (parse-side interoperability — this is a RAPP rule, not a JCS mandate). An
implementation MUST refuse (never repair) any JSON value that, at any depth, contains: (a) duplicate
member names in one object; (b) an unpaired UTF-16 surrogate, or a Unicode noncharacter (U+FDD0-U+FDEF, or
any code point whose low 16 bits are FFFE or FFFF) [RFC7493] §2.1, in any string or member name; (c) a number token that does
not survive the binary64 round-trip — let d be the token's nearest binary64 value under roundTiesToEven
(the IEEE-754 default; ±∞ are admissible results); the token is refused iff d is not finite, or the
[RFC8785] serialization of d (ECMA-262 Number::toString) denotes a different mathematical value than
the token (so 0.1 is accepted — it round-trips — while 9007199254740993 and 1e999 (→ +∞) are refused;
field range and type rules, such as uint53, are not part of this domain: the rule that types the field
applies them); (d) canonical form exceeding 1 MiB or JSON nesting depth
exceeding 64 (the root value is depth 1; each nested object/array adds 1). Refusal is whole (§7.5-style),
never partial.¶
No normalization. RAPP applies no Unicode normalization when hashing, storing, or re-emitting an
existing value; strings are code-point sequences preserved verbatim and equality everywhere is code-point
equality (no canonical-equivalence matching). A producer creating a new human-or-identifier string
(slug, kind, label, payload object key) MUST emit it in Unicode NFC. A producer that builds a frame or
packs an egg from given fields treats every payload object member name, at any depth, as new: it
MUST refuse (never normalize) a name that is not in NFC. Consumers do not test frame contents for NFC
(egg paths excepted, §9.1). NFC is Unicode Normalization Form C [UAX #15] in the Unicode version the
producer implements. A producer MUST also refuse a name containing a code point that is unassigned
(General_Category Cn) in that version. By the Unicode normalization stability policy, a name that passes
this test for one producer then passes it for every producer on the same or a later Unicode version.¶
Migration note (drift C4): the twin/rapp-body _frame.mjs::canonicalize() (sorted-key
JSON.stringify) coincides with JCS only for string-only payloads; it MUST be replaced by the JCS
implementation of record, imported (not re-typed) by every repo that content-addresses.¶
Every hash is domain-separated (git's type\0, Nix's tagged store — collisions across address spaces
are made unconstructible):
H(space, v) = lowercase_hex( SHA-256( utf8(space) || 0x0A || canonical(v) ) ) ; v a value (§4)
Hb(space, b) = lowercase_hex( SHA-256( utf8(space) || 0x0A || b ) ) ; b raw octets
space is an exact ASCII tag, none containing 0x0A: "rapp/1:particle", "rapp/1:wave", "rapp/1:egg",
"rapp/1:egg-manifest", "rapp/1:rappid", "rapp/1:grail", "rapp/1:seal",
"rapp/1:sealed-aad", "rapp/1:sealed-key-request". A tag is used by either H or Hb, never
both: H uses rapp/1:particle, rapp/1:wave, rapp/1:egg-manifest, rapp/1:sealed-aad and
rapp/1:sealed-key-request; Hb uses rapp/1:egg, rapp/1:rappid, rapp/1:grail and rapp/1:seal. An
implementation MUST refuse to compute a hash under any other tag or with the other function. Output is always exactly 64 lowercase hex, never truncated or uppercased. Two values
are treated as the same object iff their same-space hashes are equal; SHA-256 collision resistance
FIPS 180-4 is a security assumption of this standard (§14). A name/X.Y label is never identity — only
a hash is. A bare 64-hex is meaningful only within its space; an implementation MUST NOT dereference
a hash from one space as an object of another, and content-addressed stores MUST key by (space, hash).¶
## Grammar (case-sensitive, [RFC7405])
abnf
rappid = %s"rappid:@" owner "/" slug ":" hash
owner = lclabel ; the lowercase GitHub login (1-39 chars)
slug = lclabel ; 1-100 chars
lclabel = lcalnum *( ["-"] lcalnum ) ; no leading/trailing/adjacent hyphen
lcalnum = LCALPHA / DIGIT
LCALPHA = %x61-7A ; a-z
hash = 64HEXDIGLC
HEXDIGLC = DIGIT / %x61-66 ; 0-9 a-f
owner MUST be the lowercase form of the GitHub login (logins are case-insensitive; display casing is
presentation, never identity). Lengths are normative: owner 1–39, slug 1–100; an implementation
MUST refuse longer. This self-locating form is the only conformant rappid; rappid:<slug>:<hash>,
rappid:v2:…, bare UUIDs, moment:/name-hash derivations are legacy and MUST be migrated out
(Art. III), not read forever.¶
abnf
stream_id = memory-stream / body-stream / swarm-stream
memory-stream = rappid ":" instance ; one organism instance's memory
body-stream = rappid ; an organism's biography
swarm-stream = %s"net:" lclabel ; a planetary-wire stream
instance = lclabel ; 1-64 chars
kind = lclabel "." lclabel ; each label 1-64 chars
A kind string carries no intrinsic family; the §13 registry binds each registered kind to exactly
one family. Membership is tested by exact-match against the registry — never prefix inference, never wildcards.¶
The 64-hex tail is minted exactly once per identity, then immutable:
- keyless: tail = Hb("rapp/1:rappid", uuid4_octets), where uuid4_octets is the 16-octet binary
UUIDv4 [RFC9562] §5.4 (field/byte order per §4 of that RFC).
- keyed: tail = Hb("rapp/1:rappid", SPKI_DER), the DER SubjectPublicKeyInfo [RFC5280] of the master key.
A minter MUST refuse octets that are not the DER SPKI of a §10 key (an id-Ed25519 key [RFC8410], or an
id-ecPublicKey prime256v1 key with an uncompressed point); likewise a keyless mint MUST refuse 16 octets
whose [RFC9562] version is not 4 or whose variant is not 0b10.¶
A producer MUST NOT derive the tail from owner/slug or any name (sha256("owner/slug") is prohibited —
drift ID-01/C3). On read of an existing rappid.json an implementation MUST reuse the stored tail
(canonicalize-on-read) and MUST NOT re-mint — with exactly one mechanism: the owner-authorized
re-anchor (§6.3), which mints a fresh 64-hex tail once per authorization and records the superseded id
in _migrated_from. Re-anchor is lawful in exactly three cases: (a) a 128→256-bit provisional upgrade
(§6.3); (b) §10 key rotation or compromise; (c) migrating a pre-rev-3 keyed tail minted with the un-tagged
sha256(SPKI) formula (which fails §10 discovery and MUST be re-anchored like a provisional identity).¶
canonicalize_rappid(s) restructures any legacy form into §6.1, preserving the existing hash (never
inventing one). A restructured identifier whose tail is not exactly 64 lowercase hex (e.g. a legacy 32-hex
tail) is provisional: it exists only inside the reading process and MUST NOT appear in any emitted
frame, stream_id, egg, or registry entry. The one-time owner-authorized 128→256-bit re-anchor mints a
fresh 64-hex, records the old id in _migrated_from, and is the only way a provisional identity becomes
usable. A provisional identifier found in a stored artifact is a drift finding (Art. III). Re-anchor is the
single re-mint mechanism (§6.2), lawful only in the enumerated cases: provisional 128→256-bit upgrade,
§10 key rotation/compromise, and pre-rev-3 un-tagged-sha256(SPKI) keyed-tail migration.¶
A re-anchor is valid only with a verifiable authorization — a self-asserted _migrated_from is
insufficient (it would let anyone hijack an identity). A re-anchor MUST be recorded as an owner-signed
§13.3 re-anchor record {old_rappid, new_rappid, case, utc, sig, old_key_sig?}; a consumer MUST
refuse a new_rappid (and treat _migrated_from as drift) unless that record is present and:
- case:"rotation" (uncompromised): old_key_sig (a §10 JWS by the old key) verifies — proof of
continuity;
- case:"compromise": old_key_sig is waived but a §10 tombstone for old_rappid is registered in the
same append;
- case:"tag-migrate" (pre-rev-3 keyed tail): the verifier checks lowercase_hex(SHA-256(SPKI_DER_old)) ==
the old tail;
- case:"upgrade" (provisional 128→256): the old provisional id resolved to this owner at read time.
Each mints one fresh tail. The estate_owner's own re-anchor record MUST be signed by the outgoing
estate_owner key (§13.2); root-key compromise is recovered only by out-of-band re-anchoring (§13.1).¶
## The envelope — exactly eleven keys
json
{
"spec": "rapp/1",
"kind": "<klabel.klabel>",
"stream_id": "<stream_id>",
"seq": <uint53>,
"utc": "YYYY-MM-DDTHH:MM:SS.mmmZ",
"payload": { },
"payload_hash": "<64hex>",
"frame_hash": "<64hex>",
"prev": "<64hex|null>",
"prev_wave": "<64hex|null>",
"sig": "<jws|null>"
}
- spec MUST be the exact string "rapp/1" in every frame. rapp-frame/2.0/2.1 are legacy tokens
and MUST NOT be emitted. Any revision changing the key set, any field's grammar, or either hash rule
MUST change this token and land an Art. III total migration; revisions adding only new registered
kinds/registry entries keep the token (Fed. Const. Art. II).
- Exactly these eleven keys, always present, none missing, none extra. A field that does not apply is
present with value null (prev/prev_wave at genesis and on non-swarm streams; sig when unsigned) — never omitted,
because [RFC8785] hashes null and an absent key differently. Extra or missing keys are refused (§7.5).
- payload MUST be a JSON object (possibly empty {}); never null, array, string, number, or bool.
- seq is uint53 (§7.4). A producer MUST NOT emit a frame whose canonical form exceeds 1 MiB or
nesting depth 64 (§4).¶
| family | example registered kinds | stream_id form | logs |
|---|---|---|---|
| memory | memory.chat-turn, memory.tool-call, memory.save, memory.reconstructed | memory-stream | one organism's life |
| swarm | swarm.guidance, swarm.echo, swarm.telemetry, swarm.reconstructed | swarm-stream | the planetary wire |
| body | body.pulse, body.twin-pulse, body.reconstructed, body.re-genesis | body-stream | an organism's biography |
Each family also has a *.re-genesis kind (memory.re-genesis, swarm.re-genesis, body.re-genesis)
used only by §12.1. The family is not the kind's prefix — it is the §13 registry binding (so
body.twin-pulse is family body). Adding a family or event is a new registered kind on the same envelope (Art. IV), never a
new frame type. A frame's kind family MUST be compatible with its stream_id form (table column 3).¶
A frame carries both of its domain-separated addresses; a reader collapses it to whichever it needs.
Computed in order:
- particle — payload_hash = H("rapp/1:particle", payload). The worldline identity and chain link.
- wave — frame_hash = H("rapp/1:wave", frame \ {frame_hash, sig}) — the frame with exactly the
frame_hash and sig keys removed, all nine remaining keys (including payload_hash) present.
Because payload_hash is in the wave pre-image, frame_hash attests the particle; because only
frame_hash (cannot hash itself) and sig (signs the result) are removed, the pre-image is unambiguous
and non-circular. Both hashes are always present (never null).¶
utc MUST be exactly the 24-byte form YYYY-MM-DDTHH:MM:SS.mmmZ — every Y, M, D, H,
S and m is an ASCII digit (%x30-39; Unicode digits such as U+0660-0669 or U+FF10-FF19 are refused),
YYYY is any year 0000-9999, uppercase T/Z, exactly three fractional digits, no numeric offset; the
seconds field MUST NOT be 60 (a leap second clamps
to 59.999). All utc comparisons are bytewise over this fixed form (identical to chronological order).¶
Worldline chain (particle): the genesis frame has seq=0 and prev=null; every later frame has
seq = predecessor's seq+1 (contiguous) and prev = predecessor's payload_hash. seq is uint53: a
number token matching 0 / %x31-39 *DIGIT (no sign, fraction or exponent, so -0 is refused) whose value
is ≤ 2^53−1; a stream nearing 2^53−1 converges by re-genesis.¶
Wire chain (wave): prev_wave MUST be non-null iff stream_id is a swarm-stream and
seq > 0, in which case it equals the predecessor's frame_hash; in every other frame (all memory/body
streams, every genesis) it MUST be null. (Presence is a function of stream family, not transport.)¶
A frame is immutable: a new state is a new frame at a new hash; the head pointer (§7.6) re-points.¶
Cross-stream merge order (Dream-Catcher) is the total order: ascending utc (bytewise), ties broken
by ascending frame_hash (bytewise); no further ties are possible (§5).¶
Before accepting a frame, a consumer MUST, in order, refuse (never repair/reparent) on any failure:
1. Shape & types: exactly the eleven §7.1 keys; spec=="rapp/1"; kind a string matching §6.1.1
ABNF, registered (§13), and bound to a family whose §7.2 column-3 form is the form of stream_id (a
family outside the §7.2 table fits no form); stream_id a string matching §6.1.1; seq a uint53
(§7.4); utc matching the
§7.4 fixed form and a calendar-valid [RFC3339] date-time (so 2026-13-45T25:61:61.999Z is
refused); payload a JSON object; payload_hash/frame_hash 64HEXDIGLC; prev/prev_wave each
null or 64HEXDIGLC; sig null or a §10 JWS string: the detached compact form with strict unpadded
base64url parts, a protected header whose octets are canonical(header) and whose members are exactly
the §10 alg, b64, crit and kid (a §6.1 rappid) values, and a signature of exactly 64 octets.
A number token that passes §4 (c) but is not a uint53 — 9007199254740992 (2^53), 0.0, 1e0, a
negative value — leaves the frame a §4 value, so it is refused here, at step 1, never before the
checklist; only a token that §4 (c) itself refuses (9007199254740993, 1e999) makes the octets fail
before step 1.
1a. Stream binding: frame.stream_id MUST byte-equal the identifier of the stream being extended
or read (its declared stream_id at the head pointer / repo path of record). A genesis frame is
accepted only for the stream it names. (Defeats cross-stream genesis/segment replay — §14.)
2. Particle: payload_hash == H("rapp/1:particle", payload).
3. Wave: frame_hash == H("rapp/1:wave", frame \ {frame_hash, sig}).
4. Chain: seq==head.seq+1 and prev==head.payload_hash (or seq==0 ∧ prev==null at genesis);
utc ≥ head.utc (bytewise). With no head, a consumer holding the §13 registry MUST refuse a
seq=0 frame whose frame_hash differs from the stream's registered genesis (its sole non-deprecated
genesis entry) when the registry has one; a registry without a genesis entry for the stream does not
refuse on this ground. The comparison opens a chain: a later re-genesis resets the head (§7.6) and does
not revoke frames already verified.
5. Wire: if stream_id is a swarm-stream and seq>0, prev_wave==head.frame_hash; else
prev_wave==null.
6. Signature: if sig≠null, discover the key and verify per §10 (registry key discovery, tail check,
key lifecycle, the signature equation; present-but-invalid is refusal); a swarm-stream frame with
sig==null is refused (§8/§10).
Steps 1–5 are time-independent (kind lookups only append, §13), so a frame passing them passes forever;
step 6 alone may flip pass→fail when a §10 tombstone with revoked_utc ≤ the frame's utc is later
registered — the one time-dependent check.¶
A stream's head is the frame with the greatest verified seq descending from its registered genesis
(§13). A consumer MUST persist, per stream, the highest (seq, frame_hash) it verified and MUST
refuse any presented head with lower seq, or a different frame_hash at an already-verified seq (no
rollback, no silent reorg). Two distinct valid frames with equal stream_id, seq, and prev are a
fork: a consumer MUST refuse both branches past the fork point and surface a drift finding; only
the owner resolves a fork (Art. X), by re-genesis (§12.1) if needed.¶
Exception (re-genesis head reset). The no-rollback rule above refuses a lower-seq head — but a
legitimate §12.1 re-genesis presents a new genesis at seq=0. When (and only when) the §13 registry maps a
stream_id to a new genesis frame_hash (§12.1 step 3), a consumer MUST verify that registered
genesis (§7.5) and then reset its persisted head for that stream_id to it. Only a registry-published
genesis authorizes a reset; any other lower-seq head remains a refused rollback.¶
All interaction rides one of exactly two forms:
1. Synchronous — POST /chat, application/json both ways. The Grail is the reference: an endpoint
conforms when it answers every scenario of the Grail conformance suite (conformance/grail/) the way
the Grail pinned in that suite does — same HTTP status and members, the same response and session_id
values, and for a refusal the same status and members (the error text is compared only against the
same Grail version).
- Request: a JSON object. user_input (string, REQUIRED; empty or whitespace-only counts as missing).
conversation_history (OPTIONAL array; each item an object with role one of "user", "assistant",
"tool" and content a string). session_id (string, OPTIONAL). Unrecognized members MUST be
ignored, never refused.
- Success: HTTP 200, a JSON object with at least response (string), agent_logs (string: one line per
agent call, newline-separated, empty when no agent ran), and session_id (string). An endpoint
MAY add other members (the Grail adds model, requested_model, voice_mode); a consumer
MUST ignore members it does not recognize.
- Refusal of a malformed request: HTTP 400 with exactly {"error": string}, a human-readable message.
The status and the shape are fixed; the message text is not part of the frozen form (a Grail bug-fix
release may reword it).
- New capability is a new agent behind /chat, never a sibling REST route. GET /health is the
liveness probe (HTTP 200 JSON object with status); it carries no RAPP semantics.
2. Asynchronous — an append-only frame (§7) published to a stream (a repo path, an events/ log). A
frame on a swarm-stream MUST carry sig≠null (§10); memory/body-stream frames MAY be unsigned.
Any history is safe to read given a trusted head (§14); the hash chain (§5) makes tampering
detectable.¶
An egg is a cartridge packing a unit of the estate. RAPP §9 is the one egg spec of record (it
subsumes and retires EGG_FAMILY.md, NEIGHBORHOOD_EGG_SPEC.md, ESTATE_SPEC.md, rappterbook/EGG_SPEC.md,
and the rest — drift C7). No other document may re-specify eggs; they cite this section.¶
An egg is either a JSON object (invite/session variants) or a ZIP whose root is manifest.json (tree
variants). The manifest is a §4 value with exactly these members:
json
{ "schema": "rapp/1-egg", "variant": "<variant>", "rappid": "<§6.1 rappid>",
"created_utc": "<§7.4 utc>", "contents": [ {"path":"<rel>","hash":"<64hex>"}, … ], "payload": { },
"sig": "<jws|null>" }
- contents MUST list every packed file except manifest.json itself, exactly once each, with
hash = Hb("rapp/1:egg", file_octets) (§5) over the raw stored octets. contents is always present;
for JSON (pointer/session) variants it MUST be exactly [].
- path MUST be a relative POSIX path: /-separated non-empty NFC UTF-8 segments, no ./..
segment, no leading /, no backslash, no Windows drive-qualified first segment (ALPHA ":"), not the
root path manifest.json, and no two path values in one manifest equal as code-point sequences. Paths
that differ only in case or normalization, or where one path is a directory prefix of another (docs,
docs/a.md), are distinct and valid; a consumer that extracts to a filesystem where they would collide
refuses the extraction, not the egg. A segment MUST NOT end in a period/space, contain :, contain a
C0 control (U+0000-U+001F), or have a basename (the segment up to its first .), compared ASCII
case-insensitively, equal to CON, PRN, AUX, NUL, COM1–COM9, or LPT1–LPT9. §9.3's 'no
..' means no .. segment.
contents MUST be sorted ascending
by the UTF-8 bytes of path.
- payload is a §4 object (variant-specific). sig is a §10 JWS over canonical(manifest \ {sig}), or
null. For the invite variant sig is REQUIRED and MUST verify with kid in the §13.2 estate-owner
succession (invites are estate-issued; a sig by any other key, even a validly registered one, is
refused — otherwise an attacker mints a fresh rappid and forges invites). For other variants a non-null
sig MUST verify per §10 with kid == a keyed rappid the consumer resolves via §13; a consumer
presented a signed egg MUST verify it.
- The egg's one §5 address is egg_hash = H("rapp/1:egg-manifest", manifest \ {sig}) — the manifest with
exactly the sig key removed (mirroring §7.3's wave rule: sig authenticates the egg, egg_hash names
it, so re-signing never changes identity). Stores key eggs by ("rapp/1:egg-manifest", egg_hash).
("rapp/1:egg" addresses file octets; "rapp/1:egg-manifest" addresses the egg as a whole.)
- Container determinism. A ZIP variant MUST use compression method stored (0) for every entry
— no deflate in any variant (deflate is library-dependent, so it cannot be byte-reproducible; transport
compression, if any, wraps the egg and is not the egg). Entries appear in contents order with
manifest.json first; the manifest.json entry's octets MUST be exactly canonical(manifest).
Every local and central header carries: version needed to extract 20 (0x0014); general-purpose flags
exactly 0x0800 (UTF-8 filename; no data descriptor, no encryption); DOS time 0x0000 and date 0x0021
(1980-01-01 00:00:00); no extra field. Every central header also carries version made by 0x0014, no
file comment, disk number start 0, internal and external attributes 0. Every field present in both the
local and the central header holds the same value in both. The archive is the local records in entry
order with no gap, then the central directory, then an end-of-central-directory record with disk numbers
0, equal entry counts and no comment; no ZIP64 record, and no octet precedes the first local header or
follows the end record. contents[].hash is over the file octets (identical to the archive octets under
method 0). A JSON-variant egg's serialized form MUST be exactly canonical(manifest). A producer
MUST write exactly these values, so two conformant packers of the same manifest value emit
byte-identical eggs. A consumer MUST refuse any other violation of this paragraph, but MUST accept
any value of version made by, version needed (when both headers agree) and the internal and external
attributes, and MUST NOT interpret them.¶
| variant | container | packs | required members |
|---|---|---|---|
| organism | ZIP | a full brainstem instance | contents (sorted) MUST include the root paths rappid.json and soul.md; MAY include agents/*, organs/*, memory files |
| rapplication | ZIP | one rapp | contents MUST include rappid.json and exactly one agent.py at the root (the agent of record); MAY include one ui.html, files under state/, and other files: this MAY list, like the organism row's, is not exhaustive |
| session | JSON | one runtime + transcript | payload = {runtime:<string>, transcript:[<object>]}; contents [] |
| invite | JSON | a QR-sized pointer (no packed files) | payload = {target_rappid:<rappid>, target_url:<string>, target_kind:("neighborhood" / "estate")}; contents []; sig REQUIRED |
| neighborhood | ZIP | several organisms meant to live together | payload = {members:[<rappid>,…]}; contents = one sub-egg per member, named <owner>--<slug>.egg at the root, matched by the sub-egg manifest's rappid == the payload.members[] entry |
| estate | ZIP | several neighborhoods | payload = {neighborhoods:[<rappid>,…]}; contents = one sub-egg per neighborhood, named <owner>--<slug>.egg at root, matched by sub-egg rappid |
| sealed | ZIP | publicly mirrorable ciphertext with scoped key release | contents MUST be exactly ciphertext.bin; payload is the closed rapp-sealed-artifact/1 profile in §9.2.1; sig REQUIRED |¶
For organism and rapplication eggs, rappid.json MUST be a §4 object whose rappid member equals
manifest.rappid (code points) and whose schema, when present, is "rapp/1"; a consumer refuses the egg
at §9.3 step (2) otherwise (this is the §9.3 no-reparent rule applied to the packed identity).
payload.members (neighborhood) and payload.neighborhoods (estate) are arrays of distinct §6.1 rappids,
possibly empty; an empty list packs no files. "Several" describes use, not a minimum.¶
The QR-sized invite that caused EGG-01 is the invite variant: a signed pointer object, not a
member-packing neighborhood egg. The banned legacy stamps (brainstem-egg/2.3-neighborhood,
neighborhood-egg/1.0) migrate to {schema:"rapp/1-egg", variant:"invite" / "neighborhood"} (Art. III).¶
The sealed variant lets any unauthenticated mirror, including a commit-pinned
raw.githubusercontent.com URL, distribute an encrypted artifact globally without distributing its
decryption key.¶
The public bytes are ciphertext, not access control. Bytecode or obfuscation alone provides no confidentiality. A conformant producer MUST use a random 256-bit data-encryption key (DEK) per artifact, MUST NOT embed that DEK or a shared master key in the egg/client/URL, and MUST NOT place a password in a URL. A password, passkey, device assertion, or account credential may authenticate to the key service; it is not the artifact encryption key.¶
The ZIP contents are exactly one file:¶
text
ciphertext.bin = AES-256-GCM ciphertext || 16-byte authentication tag
¶
The manifest payload has exactly:¶
json
{
"schema": "rapp-sealed-artifact/1",
"cipher": "A256GCM",
"nonce": "<12 bytes, unpadded base64url>",
"plaintext_commitment": "<64hex>",
"plaintext_bytes": 1234,
"media_type": "application/wasm",
"key_id": "<64hex>",
"key_service_rappid": "rappid:@owner/key-service:64hex",
"key_service_url": "https://keys.example.com/chat",
"access": "scoped-key-release",
"aad_hash": "<64hex>"
}
¶
In this template schema, cipher and access are literals that a consumer MUST match exactly
(access is always scoped-key-release); a <…> value is a placeholder of the stated form; the values
shown for plaintext_bytes, media_type, key_service_rappid and key_service_url are examples, and only
the member's type is fixed by the template (the rules below still bound each value).¶
plaintext_commitment is a keyed commitment that cannot be tested without the DEK:¶
text
prk = HMAC-SHA-256(32 zero octets, DEK)
commitment_key = HMAC-SHA-256(prk, utf8("rapp/1:sealed-commitment") || 0x01)
plaintext_commitment = lowercase_hex(HMAC-SHA-256(commitment_key, plaintext_octets))
¶
This is the [RFC5869] HKDF-Extract/HKDF-Expand construction with an empty salt, one 32-byte output block,
and the exact info string rapp/1:sealed-commitment. The commitment is public but does not permit an
offline dictionary attack without the random 256-bit DEK.¶
The exact authenticated-data descriptor is:¶
json
{
"schema": "rapp-sealed-artifact/1",
"artifact_rappid": "<manifest.rappid>",
"created_utc": "<manifest.created_utc>",
"key_id": "<payload.key_id>",
"plaintext_commitment": "<payload.plaintext_commitment>",
"plaintext_bytes": 1234,
"media_type": "<payload.media_type>"
}
¶
aad_hash = H("rapp/1:sealed-aad", descriptor), and canonical(descriptor) is supplied as AES-GCM
additional authenticated data. nonce decodes to exactly 12 bytes. plaintext_bytes is an integer from
zero through 2^30 (1 GiB), a RAPP safety ceiling below both the NIST SP 800-38D per-invocation limit
and the deterministic non-ZIP64 container boundary.
ciphertext.bin length is exactly plaintext_bytes + 16. The sealed egg's manifest signature is REQUIRED
and verifies per §10 with kid exactly equal to manifest.rappid. A sealed artifact therefore uses a
keyed §6.2 rappid controlled by its authorized publisher; another valid registry key cannot sign on its
behalf.¶
key_service_rappid identifies the RAPP organism authorizing key release. key_service_url is its absolute
HTTPS §8 POST /chat URL and MUST end in /chat. The URL is location, not authority: the service
response is accepted only when its signatures and identifiers bind to key_service_rappid.¶
The caller sends the following canonical JSON object as the user_input string:¶
json
{
"schema": "rapp-sealed-key-request/1",
"egg_hash": "<64hex>",
"key_id": "<64hex>",
"recipient_rappid": "rappid:@owner/device:64hex",
"recipient_spki_der_b64": "<standard base64>",
"request_nonce": "<64hex>",
"expires_utc": "YYYY-MM-DDTHH:MM:SS.mmmZ",
"sig": "<recipient detached JWS>"
}
¶
sig is over canonical(request \ {sig}) and verifies under recipient_rappid; egg_hash is the §9.1
sealed egg address. The key service verifies entitlement, exact (egg_hash,key_id), request expiry,
recipient key binding, and nonce non-reuse.¶
The §8 response string is canonical JSON:¶
json
{
"schema": "rapp-sealed-key-release/1",
"egg_hash": "<request.egg_hash>",
"key_id": "<request.key_id>",
"recipient_rappid": "<request.recipient_rappid>",
"request_hash": "<H('rapp/1:sealed-key-request', request without sig)>",
"wrap_alg": "ECDH-ES+A256KW",
"wrapped_key_jwe": "<JWE Compact Serialization>",
"issued_utc": "YYYY-MM-DDTHH:MM:SS.mmmZ",
"expires_utc": "YYYY-MM-DDTHH:MM:SS.mmmZ",
"service_sig": "<key-service detached JWS>"
}
¶
service_sig is over canonical(release \ {service_sig}) and verifies under key_service_rappid.
wrapped_key_jwe uses [RFC7516] with alg:"ECDH-ES+A256KW" and enc:"A256GCM" for the declared
recipient SPKI. The service MUST NOT return a global raw DEK. Authorization, expiry, device scope, and
key rotation are service policy recorded in signed frames, not fields that alter the sealed egg's content
address.¶
A mirror URL SHOULD be immutable (for GitHub Raw, pin a commit SHA rather than a moving branch). URL secrecy is never assumed: integrity comes from the egg address and signature, confidentiality from AES-GCM, and use authorization from scoped key release.¶
Revocation prevents future key release. It cannot erase ciphertext or plaintext already downloaded and decrypted by an authorized recipient. A conformant product MUST state this limit plainly. It also MUST NOT claim that encrypted bytecode cannot be inspected after decryption; legal/license controls and bounded server authority remain separate from cryptographic confidentiality.¶
Producer MUST emit only schema:"rapp/1-egg" with a variant from §9.2, a §6.1 rappid, and, for
ZIP variants, a contents list whose every hash verifies. Every egg it emits, and every sub-egg it packs
into one, MUST pass consumer steps (0)-(2) below; a REQUIRED sig MUST be present and be a §10
JWS (a producer without the signer's key context need not verify it). Given input that cannot meet these
rules, a producer MUST refuse rather than emit or repair. It MUST NOT emit any legacy egg schema.¶
Consumer MUST read every §9.2 variant, dispatch on variant, verify integrity then viability —
(0) the manifest is a §4 value satisfying every §9.1 rule — exact member set, path grammar (no ..,
no leading /, no backslash), no duplicate paths, sort order, and for ZIP variants the archive entry set
equals contents ∪ {manifest.json} in the §9.1 deterministic order (this is the zip-slip defense; an
unenforced path grammar defends nothing); (1) every contents[].hash recomputes per §5; (2) the variant's
§9.2 structural requirements hold — and refuse whole on any failure; it MUST NOT reparent on transport.¶
grown_from lineage (rev-6; closes the hatch seam)
An egg names an artifact; a hatch creates an instance. Rev-5 specified packing and reading but left instantiation identity unwritten, so every implementation had to invent its own — this section is that missing word.¶
The rappid in a packed egg's rappid.json (§9.2) names the artifact. Every hatch of the same
egg reads the same artifact identity (§6.2 canonicalize-on-read); re-minting it on hatch is
prohibited (§6.2).¶
A consumer that instantiates an egg into a live installation MUST mint a fresh §6.2 identity for the instance, exactly once, at first boot — from entropy, never derived from the artifact identity, host name, or path. Two instances of one egg share the artifact identity and MUST NOT share an instance identity.¶
The instance's identity document SHOULD record grown_from: the §9.1 egg address
(64 lowercase hex) of the egg it was instantiated from. grown_from is written at mint time and is
immutable thereafter; it is null when the source is unknown; a producer MUST NOT fabricate
it — lineage is a fact about a birth, never a claim to invent. An instance that cannot say where it
came from says null.¶
grown_from is lineage, not inheritance: it confers no authority, no trust, and no §10 key
material. A reader MUST treat it as an unverified assertion unless the named egg is available and
its address recomputes per §9.1/§5.¶
A newly authored artifact produced from one or more existing RAPP objects MAY record
crossed_from in its packaged rappid.json. The value is a sorted, duplicate-free array of typed
addresses {space, hash}, where space is "rapp/1:particle" or "rapp/1:egg-manifest" and hash
is 64 lowercase hex. One parent denotes an offspring; two or more denote a cross. The new
artifact MUST mint its own §6.2 rappid; parent artifacts and identities remain unchanged.
crossed_from is lineage only: it transfers no signature authority, ownership, entitlement, secret,
or trust. A verifier treats each parent as unverified until the addressed object is available and
recomputes in its declared space.¶
sig is OPTIONAL on memory/body streams and REQUIRED on swarm streams (§8). Chain integrity comes from the
hash links (§5), not signatures. When present, sig MUST be JWS Compact Serialization with detached,
unencoded payload ([RFC7515] App. F + [RFC7797]):
- protected header members are exactly alg ("EdDSA" or "ES256"), b64 (false), crit
(["b64"]), kid (signer's §6.1 rappid), no others; the header octets MUST be canonical(header)
(§4 — JCS orders them alg, b64, crit, kid, no whitespace);
- the sig string is the detached compact form BASE64URL(canonical(header)) || ".." || BASE64URL(signature);
- JWS signing input = BASE64URL(canonical(header)) || "." || canonical(frame \ {sig});
- alg: EdDSA/Ed25519 [RFC8037] or ES256 [RFC7518]; Ed25519 verification is [RFC8032] §5.1.7 with
S < L required and the cofactorless check encode([S]B - [k]A) == R, where k is SHA-512(R || A ||
message) read as a little-endian integer and reduced mod L (as in [RFC8032]'s §6 reference code); public
keys that decode (including small-order points) are not refused for their order; ES256 signers SHOULD sign deterministically
[RFC6979] (Ed25519 is deterministic by construction) so signed frame files stay byte-reproducible.¶
Key discovery. A keyed rappid's tail is one-way (Hb("rapp/1:rappid",SPKI)). A verifier resolves the
signer's SPKI (DER) from the §13 registry entry (rappid → spki_der_b64); the door-of-record
rappid.json is the publication venue the registry entry is generated from, not itself a verification
source. The verifier MUST check Hb("rapp/1:rappid", SPKI_DER) == the rappid's tail and refuse on
mismatch or registry absence.¶
Key lifecycle. Rotation is an identity re-anchor (§6.3, case rotation) with new tail =
Hb("rapp/1:rappid", newSPKI) and a §13.3 re-anchor record. A re-anchor deprecates the superseded
rappid's §13 spki entry: a verifier MUST refuse a sig whose kid is a superseded rappid on any
frame with utc ≥ the re-anchor record's utc (rotation gives forward security; earlier frames verify as
before). The deprecated flag of an spki entry does not by itself refuse a sig: the entry still
resolves the key (entries are append-only, §13.3), and a sig by it is refused only by the re-anchor rule
above or by a tombstone. To stop a key without rotating, the owner registers a tombstone. Compromise is declared by an owner-signed tombstone in the §13 registry {rappid, revoked_utc}; a verifier MUST refuse any sig by a
tombstoned key on a frame whose utc ≥ revoked_utc, checking tombstones at verification time (§7.5 step
6). "Owner-signed" means a sig verifying with kid == the registry's designated estate_owner rappid
(§13). A consumer MUST NOT infer authorship from an unsigned frame (keyless rappids assert location,
not authorship).¶
Producer: emits only §4 JCS/I-JSON bytes, §5 domain-separated full-SHA-256 addresses, §6 rappids
minted per §6.2, §7.1 eleven-key frames, §9 rapp/1-egg variants — and no legacy form.¶
Consumer: runs the full §7.5 checklist (incl. 1a binding), §9.3 egg verification, §10 signature + key-discovery + tombstone checks, canonicalizes legacy ids on read (§6.3), refuses on any failure, never repairs/reparents/rolls back (§7.6).¶
Router/Mirror: invents no endpoints (§8), declares subordination to
kody-w/rapp-1 for protocol semantics, and serves only
provenance-stamped hash-matching mirrors. Estate-specific product scope is
resolved separately through that estate's signed registry and master plan.¶
An estate declares a Grail kernel through the exact §13.3 grail-kernel entry. Its release_scope
selects the release family to which the pin applies; a registry MUST contain at most one
grail-kernel entry for any release scope. Its entry-level sig
MUST verify under declared_by, which MUST be the estate owner in effect at activated_utc
(§13.2); activated_utc MUST NOT be more than 300 seconds after the verifier's first-seen time for
that entry. The pin activates locally when a consumer first accepts an authenticated, non-rollback
registry containing that valid declaration. Until then it is structural evidence only. The Grail is
identified by grail_id, never by a mutable branch, tag, repository name, or product label. Once
activated, the pin is a permanent compatibility anchor, not a moving release channel:¶
Every conformance evaluation MUST receive release_scope from an authenticated owner-controlled
release policy, never from candidate bytes or candidate-controlled configuration, then resolve its
one grail_id exclusively from the activated, persisted §13.3 binding. If the policy repeats
grail_id, that value is only a consistency assertion and MUST byte-equal the registry result; it
cannot select or rebind the pin. Every ring and release/deployment stage, including development,
qualification, Preprod, installation, and production verification, MUST compare the candidate's
Grail path to that exact binding, SHA-256, and byte length.¶
A missing, changed, substituted, or unmeasured Grail byte is kernel-drift and MUST fail closed.
Approval, urgency, compatibility claims, and semantic equivalence cannot waive byte inequality.¶
A pipeline MUST NOT silently restore the pinned file after testing a different file. The exact release-shaped artifact containing the immutable Grail bytes MUST itself pass the release gates.¶
Every gate MUST resolve every archive, container, wrapper, environment override, symlink, and
filesystem indirection to one regular runtime entry-point file under the immutable release root;
refuse links and ambiguous alternatives; verify its bytes; and bind the release digest, resolved
entry-point path, release_scope, grail_id, raw SHA-256, and byte length into its evidence.¶
Every gate that executes the kernel MUST bind verification atomically to execution from a sealed
immutable object or snapshot. An already-open descriptor is sufficient only when its backing bytes
are under platform-enforced write exclusion for the entire verification-through-consumption interval
and the interpreter/loader consumes that exact descriptor without reopening a pathname. An immutable
content-addressed release root is sufficient only when no concurrent principal can modify it. A
hash-then-path-launch without one of those guarantees is nonconformant. At least one execution gate for
every supported production platform MUST perform this proof before release approval. Merely
carrying an unused matching file is insufficient. A stored transpiled, bundled, generated, or patched
derivative is different kernel bytes and requires a new grail_id; ordinary interpreter/JIT
compilation directly from the pinned bytes is permitted when no alternate stored kernel is selected.¶
New implementation behavior MUST live outside the Grail kernel and MUST NOT alter any RAPP-visible canonical form or wire semantic except through the registration and evolution rules of §§7, 9, 12, and 13.¶
If required behavior cannot be expressed outside the pin, the implementation MUST report an incompatibility. It MUST NOT edit the Grail and continue under the same identity.¶
Different kernel bytes produce a different grail_id and therefore are a different Grail. A new
grail-kernel entry may coexist and may name the old grail_id as predecessor, but it MUST NOT
overwrite, retag, alias, or redefine the prior Grail. A successor uses a new release_scope; an
existing scope is never rebound. A non-null predecessor MUST name an earlier accepted
grail-kernel entry and MUST NOT form a cycle. A registry MUST contain at most one
grail-kernel entry for each grail_id.¶
On first activation a consumer MUST persist the full canonical grail-kernel entry. Every later
accepted registry MUST retain that entry byte-for-byte. Removal, mutation, or a second entry for
the same grail_id is a permanent kernel-drift refusal even when registry_seq increased.¶
This rule governs release topology rather than the §8 wire shape, so it does not add a frame member,
endpoint, compatibility shim, or alternate rapp/1 encoding.¶
RAPP/1 defines the substrate. Two subordinate operational profiles define how a production AI changes without mutating that substrate or the serving system underneath users:¶
RAPP CI/CD (rapp-cicd/1) — protocols/rapp-cicd/1/SPEC.md; immutable release capsules,
ordered qualification evidence, exact-candidate promotion, Preprod, and rollback/restore proof.¶
RAPP Deploy (rapp-deploy/1) — protocols/rapp-deploy/1/SPEC.md; isolated serving/candidate
lineages, cellular rollout, progressive exposure, expiring AI-health evidence, quarantine, and exact
rollback.¶
The profiles are RAPP/1 applications, not alternate wire versions:¶
Their payloads MUST be §4 canonical I-JSON and are identified by
H("rapp/1:particle", payload).¶
Authoritative profile payloads MUST travel in signed RAPP/1 frames using a registered compatible kind. Local unsigned files are drafts, fixtures, or caches and cannot authorize promotion or traffic.¶
They MUST NOT add a transport endpoint beside §8 POST /chat, redefine any RAPP/1 primitive, or
weaken §11.1.¶
An estate activates a profile through an authenticated §13.3 protocol entry pinning its exact
repository, path, and SHA-256. A moving branch is discovery, never authority.¶
A claim of RAPP production conformance requires both profiles. Core Producer, Consumer, and Router/Mirror implementations remain conformant without implementing production operations.¶
Fixed envelope keys carry the safety invariants. Policy-defined check identifiers, component kinds, health objectives, and resilience controls are extension points; adding one does not change the profile token or the RAPP/1 wire.¶
A consumer MUST refuse unknown required policy semantics. It may preserve and relay unknown optional evidence, but may not treat it as satisfying a requirement it does not implement.¶
The executable reference validators are rapp_cicd.py and rapp_deploy.py; controlled profile vectors
are in operations_conformance.py. JSON Schemas define structural shape, while the reference validators
enforce ordering, identity, temporal, and cross-document rules that JSON Schema cannot express.¶
RAPP is a living standard (WHATWG): revised in place, never forked into parallel versions; a name/X.Y
label MUST NOT ever denote two shapes (Art. II) — a shape change moves the token (§7.1). Published
content-addressed artifacts are immutable (SemVer/crates). Within an estate there is no perpetual
backward compatibility for the estate's own artifacts and retired legacy encodings (Art. III): a change
to such a form is a total migration of every instance + deletion of the old form. Sealed re-genesis
history (§12.1) is the retained live-stream exception and is not "legacy compatibility."¶
The rapp/1 wire is frozen (rev-15, Art. 18). No revision of this standard may change a form a
rapp/1 artifact is verified by: canonicalization (§4), the hash function and its tags (§5), the rappid
grammar and mint (§6.1, §6.2), the eleven-key envelope and its two addresses (§7.1, §7.3), the consumer
checklist (§7.5), the two wire forms and the /chat shapes (§8), or the egg container and address (§9.1).
A change to any of those is not a revision; it moves the token to rapp/2, specified beside this
document, and rapp/1 artifacts keep verifying under this document forever. A consumer MUST NOT
refuse a rapp/1 artifact because a later token exists. Everything Art. IV names still grows under
rapp/1: registered kinds, egg variants, error codes, registry entry types, vocabulary, subordinate
profiles, and the registry itself. The frozen forms are what a stranger's implementation, written once
and never updated, relies on; they are the reason an independent implementation can be finished.¶
Immutable specification-governance history is a separate narrow exception: rev-5 through rev-13 anchor frames retain interpretable immutable pointer payloads so their ratified normative bytes remain resolvable. They are historical authority records, not accepted live legacy protocol forms. A producer MUST NOT emit the pointer-only revision profile after rev-13.¶
The RAPP/1 revision content is the append-only JSONL stream at
anchor/chain.jsonl. The chain's particle, wave, and predecessor hashes prove
byte integrity and linear history; they do not authenticate who selected a
head or ratified an amendment. Until a separately authenticated RAPP
registry/checkpoint is ratified, authority is selected by owner-ratified
acceptance of a chain snapshot onto protected canonical
refs/heads/main at https://github.com/kody-w/rapp-1. A consumer starting
from an out-of-band pinned accepted commit and head frame hash has the same
immutable selection evidence. An internally valid fork is not authoritative
merely because its hashes verify.¶
Every protocol adjustment MUST append exactly one valid successor frame:¶
spec remains "rapp/1", the envelope remains exactly eleven keys, and the
allowed registered anchor kind is body.pulse; this profile does not create
a new kind or alter canonicalization, hashing, or registered-kind semantics;¶
stream_id remains the bootstrap-pinned anchor stream, seq is contiguous,
prev names the predecessor's payload_hash, prev_wave and sig are
null, and the applicable §7.5 integrity checks hold;¶
the frame's frame_hash is the durable protocol-revision identity.
Human names such as rev-14 are lookup labels/views, never identities.¶
Verification begins from the frozen rapp-anchor-bootstrap/1 profile published
through anchor/bootstrap/index.json. The index names a
anchor/bootstrap/sha256-<raw-profile-sha256>.json object and pins the exact
stdlib verifier at anchor/bootstrap_verify.py by raw SHA-256 and byte length.
The profile fixes the exact-integer JCS subset and limits, particle/wave
domains, eleven frame keys, timestamp/sequence/predecessor rules, canonical
repository and protected ref, anchor stream ID, genesis frame and payload
hashes, body.pulse anchor profile, null signature/wave fields, and byte/depth
limits needed to verify this chain.¶
The content-addressed bootstrap profile is immutable. Changing it requires a
new bootstrap schema/version and a new external ratification; mutating
rapp-anchor-bootstrap/1 in place is refusal. Its initial authenticity is
necessarily external — owner-ratified protected canonical main or an
out-of-band pinned profile hash/immutable commit — because neither a chain nor
code fetched with that chain can circularly authenticate its own parser.
rapp.py remains the complete reference implementation and is cross-checked,
but it is not the mutable bootstrap trust pin.¶
Frames through rev-13 retain their immutable legacy pointer fields:
canonical_repo, a full 40-lowercase-hex commit, safe relative
normative_path, raw-file normative_sha256, and decimal-string
normative_bytes. A resolver MUST construct only an immutable
commit-pinned GitHub Raw URL from those fields, fetch the bytes, and verify
length and raw SHA-256 before use. A branch or tag name in commit, an unsafe
path, malformed UTF-8, a byte-order mark, or a hash/length mismatch is refusal.
Path validation occurs on the original string before path-library
normalization: empty, absolute, ./.., empty components, repeated slash,
leading ./, trailing slash, backslash, percent-encoded ambiguity, or any
value whose reconstructed POSIX form differs is refused.
These pointer payloads are immutable governance history under Article 3's
narrow exception; they MUST NOT be emitted for rev-14 or later.¶
Rev-14 and each successor using this profile carries all normative text inside
the chain frame. Its payload MUST include
schema:"rapp-spec-revision/1", a unique revision, and both
previous_revision and previous_normative_sha256 matching the immediate
predecessor. It preserves the useful legacy fields above, carries the
publication/ratification metadata defined below, and adds exactly this
normative object:¶
json
{
"media_type": "text/markdown; charset=utf-8",
"text": "<exact normative SPEC.md Unicode text>",
"sha256": "<lowercase SHA-256 of text encoded as UTF-8>",
"bytes": 123
}
¶
normative.text MUST encode as UTF-8 without a byte-order mark;
normative.sha256 and normative.bytes MUST match those exact octets and
the corresponding legacy normative_sha256 and normative_bytes. The complete
canonical frame remains subject to §4's 1 MiB limit. Inline revisions require
only the selected, verified chain bytes once fetched; their commit/path fields
are immutable provenance, not a second source for normative text.¶
The payload's publication object, repeated in the beacon and revision index,
records: canonical repository https://github.com/kody-w/rapp-1; protected ref
refs/heads/main; selection by owner-ratified acceptance; the accepted
canonical-main commit as linearization point; prohibition of history
replacement; mandatory rebase/regeneration for a competing append; rev-14
ratification under rev-13 Article 14; application of this chain-append process
from rev-15 onward; and authenticated_registry_checkpoint:null until one
actually exists. This metadata MUST NOT be interpreted as a signature or
authenticated registry.¶
Every chain frame MUST also be published, without a trailing line terminator, at:¶
text
anchor/frames/<frame_hash>.json
¶
A fetch through mutable main is discovery only. Given frame hash F, a
stranger may fetch
https://raw.githubusercontent.com/kody-w/rapp-1/main/anchor/frames/F.json,
parse it under the pinned bootstrap, and accept the object only if its computed
wave equals F. For accepted snapshot commit C, the immutable URL is
https://raw.githubusercontent.com/kody-w/rapp-1/C/anchor/frames/F.json.
Wrong content at the correct hash-derived path is refusal.¶
anchor/index.json is a deterministic generated index from seq, revision
label, frame hash, and payload hash to those objects. It carries no independent
authority: a consumer MUST verify each selected object and match the index
and beacon to the fully verified chain. Resolution by frame hash uses the path
algorithm directly; resolution by payload hash or sequence uses the verified
index; resolution by revision label is a view over it. The fixed pre-profile
rev-5 pulses share one historical label, so the rev-5 view resolves to the
greatest matching seq, while every pulse remains directly resolvable by
sequence and hashes.¶
A consumer MUST verify the chain from the bootstrap-pinned genesis before
using a revision as selected history. It MUST refuse an invalid frame shape,
particle, wave, or prev; a fork or duplicate seq; duplicate frame/payload
hashes; a duplicate profiled revision; an unsupported schema; a legacy
pointer-only frame after rev-13; malformed UTF-8; normative hash/length drift;
bootstrap/index/object drift; or an object absent from the selected chain.¶
orient.json is only a beacon to the head. Its sequence, frame hash, payload
hash, bootstrap/index pins, authority-selection metadata, and every retained
head mirror — including registered_kinds, vocabulary, operational profiles,
foundation, philosophy, and Constitution metadata — MUST be regenerated
from and match the verified head. While the beacon schema remains
rapp/1-anchor, spec.normative_path MUST remain as a compatibility alias
equal to spec.materialized_path. SPEC.md MUST reproduce the selected
head byte-for-byte. The Atom feed and mutable main URLs are discovery only.¶
Materialization and immutable-pointer caching MUST use bounded reads and same-directory atomic replacement without following a symlink in the destination leaf or path. A symlink, unsafe directory component, oversized cache entry, content change during read/write, or compare-and-swap mismatch is refusal; the requested leaf is replaced, never the symlink target.¶
After acceptance, let C be the full 40-hex canonical-main commit containing
the accepted snapshot. The immutable checkpoint URLs are:¶
text
https://raw.githubusercontent.com/kody-w/rapp-1/C/anchor/chain.jsonl
https://raw.githubusercontent.com/kody-w/rapp-1/C/anchor/orient.json
https://raw.githubusercontent.com/kody-w/rapp-1/C/anchor/index.json
¶
The authority linearization point is the owner-ratified acceptance of C onto
protected canonical main, not creation of a local frame, a feed event, or a
hash alone. A stale competing append MUST rebase onto the accepted head and
regenerate. Force-push or history replacement of accepted authority is
prohibited.¶
Rev-14 is ratified under rev-13 Article 14. Owner acceptance of the prepared change onto canonical protected main makes the final rev-14 frame effective; the new chain-append process governs rev-15 onward. Before that acceptance, rev-14 artifacts are unpublished drafts. A later commit in the same owner-ratified change may deterministically replace the one unpublished rev-14 draft line/object and update dependent hashes, but MUST preserve every rev-5 through rev-13 line byte-for-byte and MUST NOT represent the replaced draft as accepted history.¶
The publication generator MUST hold one cross-process exclusive lock across
read, verification, generation, comparison, and publication. Immediately
before replacing chain.jsonl it MUST re-read and verify the on-disk head
and compare the complete expected prefix; a stale writer or changed prefix is
refused. Supporting content-addressed artifacts are published first, the
authority chain is compare-and-swap replaced next, and the beacon is replaced
last, with file and parent-directory synchronization. Because the chain is
authority content and the beacon is only a derived view, interruption after a
valid chain replacement may leave a stale or missing beacon; the next locked
run MUST deterministically regenerate it from the verified chain rather
than refusing the valid head forever.¶
This chain and beacon are unsigned. They provide integrity after authority selection, not authorship, and implementations MUST NOT fabricate a signature or authenticated registry/checkpoint state for them.¶
Each estate selects an owner-controlled canonical_source for its
schema:"rapp/1-registry" document. The kody-w reference estate may publish
its registry through kody-w/RAPP or another owner-authorized location, but
that estate instance cannot alter this protocol. Because §7.6 head resets, §10
key discovery, tombstone revocation, and ownership all resolve through the
selected registry, it is the root of trust for that estate and is itself
authenticated (an unsigned mutable file at the root of the trust graph would
forge that estate).¶
The one bootstrap axiom is the estate_owner rappid string itself: since a keyed tail is
Hb("rapp/1:rappid", SPKI_DER), the rappid is a self-certifying key fingerprint, distributed
out-of-band exactly once (QR, invite, docs) the way a root-CA certificate is.¶
The registry document MUST carry a top-level registry_seq (uint53) and a detached §10 JWS sig over
canonical(registry \ {sig}) with kid = the estate_owner rappid. A consumer MUST verify this
signature against an SPKI whose Hb("rapp/1:rappid", SPKI_DER) equals the anchor rappid's tail (the SPKI
may travel alongside the registry — the tail check authenticates it) and MUST refuse an unsigned or
non-verifying registry.¶
No rollback: a consumer persists the highest registry_seq it verified and refuses any registry with
a lower one (mirrors §7.6). Freshness: a consumer MUST obtain the registry from canonical_source
or a provenance-stamped (Art. VIII) mirror of it, SHOULD refresh before any §7.5-step-6 or §7.6
head-reset decision, and MUST report a verification made against a registry older than its staleness
policy as stale, not clean.¶
schema:"rapp/1-registry")
The registry is an I-JSON document; every entry is append-only (never removed/renamed; retirement is a
deprecated:true flag). Entry types and their exact members:
- protocol {type:"protocol", name, spec_repo, spec_path, spec_hash, deprecated} — an estate
adoption pin, never a power to redefine a protocol. An entry with name:"rapp/1" that is used for a
current-conformance claim MUST set spec_repo:"https://github.com/kody-w/rapp-1",
spec_path:"SPEC.md", and spec_hash to a normative SHA-256 published by a verified frame in this
repository's anchor chain. A historical RAPP/1 pin may be retained only as deprecated:true; it does
not override the current anchor. Other protocol entries are subordinate to their own canonical
authorities and MUST NOT claim the rapp/1 name or namespace.
- kind {type:"kind", kind, family, deprecated} (incl. the three *.re-genesis kinds). A
deprecated:true kind entry still registers kind and its family for §7.5 step 1 (steps 1–5 are
time-independent); deprecation only tells producers to stop emitting new frames of that kind.
- egg-variant {type:"egg-variant", variant, deprecated} · error-code {type:"error-code", code}
(both closed namespaces; unregistered value = not conformant)
- genesis {type:"genesis", stream_id, frame_hash, deprecated, old_stream_id?, new_stream_id?} — every
stream registers its creation genesis; re-genesis appends a new one and deprecates all prior (§12.1 step 3);
§7.6's "registered genesis" is the sole non-deprecated genesis for a stream_id.
- spki {type:"spki", rappid, spki_der_b64, deprecated} — the §10 key-discovery source.
- tombstone {type:"tombstone", rappid, revoked_utc, sig}, sig owner-signed over canonical(entry \ {sig}).
- re-anchor {type:"re-anchor", old_rappid, new_rappid, case:("upgrade"|"rotation"|"compromise"|"tag-migrate"),
utc, sig, old_key_sig?} — sig owner-signed; old_key_sig a §10 JWS by the old key over
canonical(entry \ {sig,old_key_sig}), REQUIRED for case:"rotation". This is the normative succession record (§13.2).
- grail-kernel {type:"grail-kernel", release_scope, grail_id, repository, immutable_ref,
object_format, commit, path, mode, blob, sha256, size_bytes, activated_utc, predecessor, declared_by,
sig} — exactly these members. release_scope is an absolute HTTPS URI selected by the estate owner;
no two entries may share it. grail_id is
"grail:" || Hb("rapp/1:grail", kernel_bytes); repository is an absolute HTTPS URI;
immutable_ref is a full refs/tags/... name that MUST resolve exactly to commit;
object_format is "sha1" or "sha256" and fixes the required lowercase hexadecimal length of
commit and blob; path at commit MUST resolve through the repository tree to exactly one
regular blob with mode "100644" or "100755" and object id blob;
path is a relative NFC POSIX path with no empty, ".", or ".." component; sha256 is the raw
kernel bytes' 64-lowercase-hex SHA-256; size_bytes is their positive uint53 length;
activated_utc has the exact §7.4 form; predecessor is null or another grail_id; declared_by is
a keyed rappid; and sig is a detached §10 JWS whose protected kid equals declared_by, over
canonical(entry \ {sig}). The entry is additionally covered by the registry's §13.1 signature. A
consumer verifies the entry signer as the estate owner in effect at activated_utc, verifies the
referenced bytes, recomputes both hashes, persists the canonical entry on first activation, applies
§11.1, and refuses a missing/mutated prior binding, duplicate grail_id, or locator whose bytes
disagree.
- estate_owner {type:"estate_owner", rappid} (exactly one non-deprecated) · master-plan
{type:"master-plan", repo, path} (Fed. Const. Art. VII).¶
§7.5 steps 1–5 are time-independent (append-only lookups); only step 6 (tombstones) and §13.2 owner tenure are time-scoped, and both are monotone given the §13.1 no-rollback rule.¶
Integrity: every object is domain-separated content-addressed (§5); a hostile mirror cannot alter bytes without breaking the hash, so history is safe given a trusted head.¶
Head freshness is not self-certifying: the chain authenticates history, not which head is current; a hostile mirror may serve a stale/forked head. Consumers counter with the §7.6 monotonic-head rule; swarm heads SHOULD be owner-signed.¶
Cross-stream replay: without §7.5 step 1a, any genesis/segment of stream A replays as stream B (seq=0, prev=null always pass). Step 1a's stream binding is mandatory.¶
Address-space confusion: §5 domain tags make a particle, wave, egg, or rappid tail with equal hex
non-interchangeable; stores key by (space, hash).¶
Canonicalization attacks: the §4 I-JSON input-domain profile (no duplicate keys, no lone surrogates, exact binary64, no normalization ambiguity) removes hash-splitting and NFC-twin vectors.¶
Identity forgery / key compromise: authorship requires a keyed rappid + valid §10 sig; rotation is
§6.3 re-anchor (verifiable authorization, §6.3/§13.3 — a self-asserted _migrated_from is refused);
compromise is a §13 tombstone enforced at verify time. Because a tombstone gates on the frame's
producer-controlled utc, a compromised key can still emit frames stamped just below revoked_utc; after
a compromise the owner SHOULD advance affected stream heads (or re-genesis) past revoked_utc.¶
Root of trust: the registry is the estate's signed root (§13.1); it is authenticated by an owner
signature anchored to the out-of-band estate_owner rappid fingerprint, registry_seq-monotonic against
rollback, and freshness-checked (a stale registry silently un-revokes keys and hides re-geneses).¶
Producer-controlled utc (DoS/merge bias): a future-dated head can brick a stream (successors refused
as earlier) and bias UTC-first merges. A consumer SHOULD refuse a frame whose utc exceeds receipt
time by >300 s, and adversarial-scope merges SHOULD rank by min(utc, first-seen); a bricked stream
converges by re-genesis (§12.1).¶
Sealed-artifact limits: a public mirror sees ciphertext and metadata. AES-GCM protects confidentiality only while the DEK remains secret; a recipient that has lawfully decrypted plaintext can copy it. Revocation stops future wrapped-key release, not prior possession. Shared passwords, embedded master keys, moving-branch URLs, nonce reuse under one DEK, and claims that bytecode cannot be inspected after decryption are non-conformant security postures (§9.2.1).¶
Kernel substitution: testing mutable ring bytes and restoring the Grail only after approval creates
an untested release. §11.1 requires the release-shaped artifact to contain the pinned bytes before its
tests, attestations, and approval; a digest mismatch is a blocking kernel-drift finding.¶
Kernel registry rollback/rebinding: persisting only registry_seq does not preserve an activated
Grail declaration across a malicious higher-sequence registry. Consumers also persist every activated
canonical grail-kernel entry and reject its later removal, mutation, or duplication (§11.1).¶
The specification was hardened through adversarial review rounds recorded in its revision log. The chain of record and its verifier are at [RAPP1-ANCHOR].¶