ANLA-MVP v0.1 — Normative Format Specification
Format identifier: ANLA-MVP Format version: 0.1 Status: Frozen research profile. Implemented twice and cross-verified. File extension: .anla Media type (provisional): application/vnd.evemiss.anla
0. What this document is, and what it is not
ANLA-MVP v0.1 is a small, complete, frozen profile of the Agent-Native Lossless Archive format. It is not the full format described in the ANLA whitepaper. The whitepaper defines a target: BLAKE3, Zstandard, deterministic CBOR manifests, FastCDC, append-only snapshots, cross-platform metadata namespaces, signatures, parity. This profile deliberately defines the smallest subset that can be implemented end to end, in two independent languages, and verified byte for byte — so that the whitepaper's central claim stops being an assertion and becomes a test.
That claim is:
An AI may plan how to pack. A public, deterministic, model-independent decoder must recover every byte that was declared into the archive.
This document is normative for ANLA-MVP v0.1 only. Divergences from the whitepaper's future v1 layout are listed in §13.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
1. Design constraints of this profile
- A conforming decoder MUST NOT require a language model, an embedding model, a remote inference service, or any network access to extract an archive.
- A conforming decoder MUST reproduce declared file contents bit for bit.
- Every algorithm used is a published standard with widely available implementations: SHA-256, CRC-32 (ISO-HDLC), DEFLATE, zlib, JSON.
- A conforming writer MUST be able to run entirely inside a web browser tab with no backend. This constraint is why the profile uses SHA-256 and DEFLATE rather than BLAKE3 and Zstandard: both are available as platform primitives (
crypto.subtle,CompressionStream) in current browsers. - Given identical inputs and identical values for the two inputs that are inherently variable (archive UUID and creation timestamp), two conforming writers MUST emit byte-identical archives. See §10.
2. Overall layout
┌────────────────────────────────┐ offset 0
│ Bootstrap Header — 64 bytes │
├────────────────────────────────┤ offset 64
│ Record stream │
│ CHNK … CHNK (payload) │
│ MANF (manifest) │
├────────────────────────────────┤
│ Footer — 96 bytes │
└────────────────────────────────┘ end of file
All integers are little-endian and unsigned. There is no alignment padding anywhere in the format: records are packed end to end, and the footer begins at filesize - 96.
An archive MUST be at least 160 bytes long (header plus footer).
3. Bootstrap Header (64 bytes)
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 8 | magic | 41 4E 4C 41 0D 0A 1A 0A (ANLA\r\n\x1A\n) |
| 8 | 2 | version_major | 0 |
| 10 | 2 | version_minor | 1 |
| 12 | 4 | reserved_a | 0 |
| 16 | 16 | archive_uuid | 16 raw bytes |
| 32 | 28 | reserved_b | all zero |
| 60 | 4 | header_crc32 | CRC-32 of header bytes [0, 60) |
The magic ends in \r\n\x1A\n for the same reason PNG does: it makes naive text mode transfer corruption detectable immediately.
A decoder MUST reject the archive if the magic does not match, if version_major != 0 or version_minor != 1, or if the header CRC does not match. A decoder MUST NOT reject non-zero reserved_a / reserved_b — those are covered by the CRC and are reserved for a future minor version — but a writer MUST write zeros.
archive_uuid is 16 bytes of entropy. It is a UUIDv4 when the platform provides a CSPRNG. Its textual form (used in the manifest) is the canonical hyphenated lowercase hex form 8-4-4-4-12.
4. Record frame
Every record in the record stream has this 40-byte frame, followed by a JSON record header, followed by an opaque payload:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | magic = ANLR |
| 4 | 4 | type, four ASCII bytes |
| 8 | 2 | record_version = 1 |
| 10 | 2 | flags = 0 |
| 12 | 4 | header_length — byte length of the JSON record header |
| 16 | 8 | payload_length |
| 24 | 8 | sequence — see §4.3 |
| 32 | 4 | header_crc32 — CRC-32 of the JSON record header bytes |
| 36 | 4 | reserved = 0 |
record_total_length = 40 + header_length + payload_length
The JSON record header MUST be canonical JSON (§6). The payload is bytes; its meaning is defined per record type.
A decoder MUST reject a record whose magic is wrong, whose header_crc32 does not match, whose header_length exceeds 16 MiB, or whose declared extent (offset + record_total_length) exceeds the archive size. These checks MUST be performed before any allocation sized from the declared lengths.
Record types in this profile:
| Type | Payload | Required to extract |
|---|---|---|
CHNK | one encoded chunk | yes |
MANF | the snapshot manifest | yes |
flags is reserved in this profile and MUST be 0. Unlike the full format, ANLA-MVP v0.1 has no optional record classes: a decoder that encounters an unknown record type MUST fail rather than skip, because it cannot know whether the record was required. (The full format solves this with a AUXILIARY_DISPOSABLE flag bit; this profile does not need it, and guessing is worse than refusing.)
4.3 Record sequence
The record stream of this profile contains exactly one CHNK record per unique chunk, followed by exactly one MANF record. Sequence numbers are therefore fully determined, and a decoder MUST enforce it:
- every record has
sequence >= 1; - each
CHNKrecord's sequence lies in1 .. len(chunks), and no two share one; - the
MANFrecord's sequence equalslen(chunks) + 1.
This is stated as arithmetic rather than as "strictly increasing" because the looser phrasing is unenforceable by a reader that does not walk the whole stream — and a reader that follows the footer to the manifest and then jumps to each chunk descriptor, which is the access pattern the format is designed for, does not walk it. An invariant a conforming decoder cannot check is not an invariant; it is a comment.
Differential fuzzing is what surfaced this. A mutant with sequence = 2^63 was accepted by the Python reader, which has unbounded integers and never looked at the field, and refused by the JavaScript reader, which could not represent the value. Both behaviours were defensible and neither was checking the rule.
4.1 CHNK
Record header:
{"chunk_id":"<64 hex>","codec":"store|deflate","payload_sha256":"<64 hex>","raw_size":<int>}
Payload: the chunk encoded with codec (§7).
4.2 MANF
Record header:
{"encoding":"canonical-json","payload_sha256":"<64 hex>","preservation_required":true}
Payload: the manifest, canonical JSON, UTF-8 (§8).
An archive contains exactly one MANF record, and it is the last record in the stream.
5. Footer (96 bytes)
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | magic = 41 4E 4C 41 46 54 52 00 (ANLAFTR\0) |
| 8 | 2 | version_major = 0 |
| 10 | 2 | version_minor = 1 |
| 12 | 4 | reserved_a = 0 |
| 16 | 8 | manifest_record_offset |
| 24 | 8 | manifest_record_length — full record length, including the frame |
| 32 | 16 | archive_uuid — MUST equal the header's |
| 48 | 32 | manifest_payload_sha256 |
| 80 | 12 | reserved_b = all zero |
| 92 | 4 | footer_crc32 — CRC-32 of footer bytes [0, 92) |
The footer is authoritative for locating the manifest. A decoder MUST verify, in this order: footer magic, footer CRC, archive_uuid equality with the header, that manifest_record_offset points at a well-formed MANF record whose total length equals manifest_record_length, and that the SHA-256 of the manifest payload equals manifest_payload_sha256. Only then may the manifest be parsed.
6. Canonical JSON
Record headers and the manifest are encoded with a canonical JSON profile so that the same logical structure always produces the same bytes — which is what makes the manifest hash, and therefore reproducibility, meaningful.
Rules:
- UTF-8, no byte-order mark.
- No insignificant whitespace anywhere.
- Object keys sorted ascending by Unicode code point. All keys in this profile are ASCII, so a byte-wise sort is equivalent.
- No duplicate keys.
- Strings use the shortest JSON escaping:
",\, and the C0 controls are escaped (\b \t \n \f \rwhere defined,\u00XXotherwise); every other character, including all non-ASCII, is emitted literally as UTF-8. Notably,/is not escaped and non-ASCII is not\u-escaped. - Numbers are integers only, in the closed range
[-(2^53 - 1), 2^53 - 1], written with no leading+, no leading zeros, no decimal point and no exponent. Floating point numbers MUST NOT appear anywhere in a conforming archive. Values that can exceed 2^53 — nanosecond timestamps — are carried as decimal strings. true,false,nullare lowercase literals.nullis not used by this profile.- Arrays preserve their defined order; array order is normative wherever this spec defines one.
A decoder MUST NOT require that a manifest it reads is canonical — it verifies integrity through the payload hash, not through re-encoding. A writer MUST emit canonical JSON.
7. Codecs
codec | Encoding |
|---|---|
store | identity; payload is the raw chunk |
deflate | zlib format (RFC 1950): a 2-byte zlib header, DEFLATE data (RFC 1951), and an Adler-32 trailer |
deflate is the zlib wrapper, not raw DEFLATE. This is exactly what the web platform's CompressionStream("deflate") produces and DecompressionStream("deflate") consumes, and what zlib.compress / zlib.decompress produce and consume in Python. Raw DEFLATE (deflate-raw in the web platform) is not part of this profile.
For store, a decoder MUST verify payload_length == raw_size. For any codec, a decoder MUST verify that the decoded length equals raw_size before hashing, and MUST enforce its own output-size limit while decoding so that a hostile chunk cannot exhaust memory (§11).
A decoder MUST fail on any other codec string. Compression level is a writer-side choice with no effect on decoding, and is not recorded per chunk; the requested level is recorded once in the packing plan for auditing.
8. Manifest
The manifest is a JSON object with exactly these members:
{
"format": "ANLA-MVP",
"format_version": "0.1",
"archive_uuid": "3bd605e8-451b-43d6-a238-2e6c67f3649b",
"created_unix_ns": "1752732000000000000",
"hash_algorithm": "sha256",
"manifest_encoding": "canonical-json",
"snapshot_sequence": 1,
"source_name": "workspace",
"plan": { "…": "…" },
"preservation": {
"lossless": true,
"decoder_requires_ai": false,
"object_coverage": "all-selected-objects"
},
"objects": [ "…" ],
"chunks": { "…": "…" },
"statistics": { "…": "…" },
"auxiliary": { "decision_log": [], "disposable": true }
}
A decoder MUST reject the archive if format != "ANLA-MVP", if format_version != "0.1", or if archive_uuid does not match the textual form of the header UUID.
created_unix_ns is a decimal string: nanoseconds since the Unix epoch, UTC. snapshot_sequence is 1; this profile has no append-only snapshots.
8.1 objects
An array. Order is normative: ascending by the UTF-8 bytes of path, ties broken by ascending type bytes. Paths are unique across the array.
The record stream is ordered too, and for the same reason — reproducibility. Files are processed in ascending UTF-8 path order, and each file's chunks in offset order; a CHNK record is emitted the first time its content is encountered and never again. sequence numbers therefore run 1..n over the CHNK records in that order, with the MANF record last. The auxiliary.decision_log array is in the same order as the CHNK records.
A directory object:
{"type":"directory","path":"docs","metadata":{}}
A file object:
{"type":"file","path":"docs/readme.txt","size":21,"sha256":"<64 hex>",
"chunks":[{"id":"<64 hex>","length":4}],"metadata":{"mtime_ns":"1700000000000000000"}}
pathis a relative POSIX path; see §9.sizeis the file length in bytes.sha256is the hash of the whole file content, lowercase hex.chunksis the ordered chunk reference list. Concatenating the referenced raw chunks in order MUST reproduce the file exactly, sosum(length) == sizeis REQUIRED. An empty file has an empty list.metadatacarriesmtime_ns(decimal string) when the writer preserved it, and is otherwise{}. This profile defines no other metadata key. A decoder MUST ignore metadata keys it does not know rather than fail: metadata is descriptive, and no content depends on it.
Object types other than directory and file MUST be rejected. Symbolic links, hard links, sparse ranges, ACLs, extended attributes and alternate data streams are outside this profile — an archive cannot claim to preserve them, which is the point of stating so here.
8.2 chunks
An object mapping chunk_id to a descriptor:
"9f64a747…806a": {
"record_offset": 64, "record_length": 236,
"payload_offset": 187, "payload_length": 4,
"raw_size": 4, "codec": "store", "payload_sha256": "<64 hex>"
}
chunk_id is the SHA-256 of the raw (decoded) chunk bytes, lowercase hex. Content identity is therefore independent of the codec used to store it, which is what makes deduplication across differently-compressed occurrences possible.
payload_sha256 is the SHA-256 of the stored (encoded) payload, which lets a verifier detect medium corruption without paying for decompression.
The offsets are redundant with the record stream on purpose: they give random access without a scan, and they give the verifier a second, independent statement to cross-check. A decoder MUST verify that the record at record_offset is a CHNK record whose total length equals record_length and whose chunk_id, codec and raw_size match the descriptor. A decoder SHOULD verify payload_offset and payload_length against the parsed record rather than trusting the descriptor.
Keys are serialized in ascending hex order, as canonical JSON requires.
8.3 plan
The packing plan: the machine-readable statement of what the writer was asked to do. It is part of the preservation plane because it is audit evidence, not because extraction needs it.
{"plan_version":"0.1","chunk_size":1048576,"compression":"auto|deflate|store",
"deflate_level":6,"exclude_globs":[],"preserve_mode":false,"preserve_mtime":true,
"verification":"full"}
Chunking is fixed-size unless a chunking member is present. Fixed-size means file bytes are cut at chunk_size boundaries, so chunk i covers [i·chunk_size, min(size, (i+1)·chunk_size)), and chunk_size MUST be at least 1.
8.3.1 Content-defined chunking — the anla-cdc-1 profile
Fixed-size chunking deduplicates badly. Insert one byte at the front of a file and every later boundary moves, so every chunk gets a new content id and nothing is shared with the previous version. Content-defined chunking cuts on the data itself, so an edit only disturbs the chunks around it.
A writer using it MUST record the complete profile:
"chunking": {
"algorithm": "fastcdc", "version": "anla-cdc-1", "gear_table_id": "anla-gear-1",
"gear_table_sha256": "ecdce4099dbb06b791d1255eb242b2ca9a0454541b6d6c376b5df5d17a7e66c2",
"min": 65536, "avg": 262144, "max": 1048576, "normalization": 2,
"fingerprint": "gear32", "boundary": "top-bits-zero"
}
The whitepaper's open question 3 asks how FastCDC parameters can become a permanently stable profile. Writing "fastcdc" and three sizes is not enough: implementations would still disagree about the gear table and the mask, and therefore about where chunks begin. anla-cdc-1 pins all of it.
Fingerprint. 32-bit unsigned, fp = ((fp >> 1) + gear[byte]) mod 2^32. 32 bits so that a JavaScript implementation, whose bitwise operators are exactly 32-bit, can be exact without bignum arithmetic.
Gear table. Derived, not published as 256 constants:
gear[i] = big-endian uint32 of the first four bytes of
SHA-256( "anla-gear-1" || 0x00 || i ) for i in 0..255
gear_table_sha256 is SHA-256 over the 256 words written big-endian and concatenated, and MUST equal the value above. A table that has to be transcribed between codebases is a table that will one day be transcribed wrongly; a derived one can be regenerated and checked in three lines.
Boundary. A cut occurs after byte i when fp >> (32 - k) == 0, that is, when the top k bits are zero. The top bits, because in gear hashing the accumulated history lives there; masking the low bits would cut on almost nothing. Expected chunk length is 2^k.
Normalization. With k = log2(avg), the predicate uses k + normalization bits while the candidate chunk is shorter than avg, and k - normalization bits after that. This is FastCDC's normalized chunking, which pulls the size distribution towards the average instead of the exponential tail plain CDC produces.
Search window. The scan starts min bytes into the chunk and stops at max, which is then the cut. A remainder of min bytes or fewer is one final chunk.
Constraints. 1 <= min <= avg <= max, avg a power of two, 0 <= normalization <= 3, and 1 <= k ± normalization <= 31.
The paper's spread masks are deliberately not used: they are tuned for a 64-bit fingerprint, and reproducing a mask constant by hand across implementations is exactly the failure this profile exists to prevent. The gear table supplies the diffusion; the mask only has to be unambiguous.
Two consequences worth stating plainly:
- A reader needs to know none of this. Chunk references are chunk references and the chunk map is explicit, so a decoder that has never heard of
anla-cdc-1reads a content-defined archive exactly as it reads any other. Content-defined chunking therefore requires noformat_versionbump: it is a writer capability, andchunkingis descriptive, like the rest ofplan. - When
chunkingis present,chunk_sizecarries no meaning. It stays in the plan because the plan's shape is frozen; a writer records whatever value it was configured with, and nothing reads it.
The absence of chunking means fixed-size at chunk_size. That is why archives written before this profile existed are still byte-identical to what they were — nothing was added to their plan.
compression: "auto" means: try deflate, keep it only if len(compressed) + 8 < len(raw), otherwise fall back to store. The + 8 is the margin at which compressing stops paying for its own decode cost on small chunks. "deflate" forces the compressed representation even when it is larger; "store" never compresses. The chosen codec is recorded per chunk, so a decoder never needs to know which mode was used.
exclude_globs is the list of exclusion patterns applied before the object set was formed. Its presence is what keeps "we did not pack this" distinguishable from "we packed this losslessly": excluded paths were never members of the declared set, and the archive does not claim to contain them.
The pattern dialect is small and fully specified, because two writers that disagree about which files a pattern covers disagree about what the archive contains:
| Token | Matches |
|---|---|
** | any sequence of characters, including / |
* | any sequence of characters except / |
? | exactly one character, except / |
| anything else | itself, literally |
A pattern is matched against the whole path, anchored at both ends — there is no partial match and no implicit prefix. Matching is case-sensitive and applies no Unicode normalization.
Exclusion is per path, and a directory is its own path. .git/** removes everything inside .git and leaves .git itself, as an empty directory object; removing the subtree takes both .git and .git/**. This is stated because it is otherwise discovered by accident.
8.4 statistics
Derived counters: objects, files, directories, unique_chunks, chunk_references, logical_bytes, stored_payload_bytes. A verifier MAY recompute and compare them; they are convenience, not authority.
8.5 auxiliary — the intelligence plane
{"disposable": true,
"decision_log": [
{"chunk_id":"<64 hex>","raw_size":4,"stored_size":4,"codec":"store",
"reason":"compression-not-beneficial"}
]}
reason is one of smaller-representation, compression-not-beneficial, forced-by-plan.
This member is the entire intelligence plane of the profile, and it is disposable by definition: replacing auxiliary with {"decision_log":[],"disposable":true} changes the manifest bytes, the manifest hash and the footer, but MUST NOT change what any decoder extracts. Conformance test T-AUX-1 (§12) asserts exactly this.
An implementation MAY therefore rewrite an existing archive with the plane emptied, and this is a supported operation rather than a trick: keep the byte range up to manifest_record_offset unchanged, re-emit the MANF record with the stripped manifest, and write a new footer. Every chunk record keeps its bytes and its offset, so every chunk descriptor stays true and the result verifies without touching the payload. A decision log records what a planner was told and what it chose, which is not always something to hand over along with the data.
9. Paths
Paths in the manifest are relative POSIX paths: /-separated, no drive letter, no leading /, no . or .. component, no empty component, no NUL. Writers convert \ to / at pack time.
A writer MUST reject an unsafe path rather than sanitize it silently. A decoder MUST validate every path again at open time — before any extraction — and MUST reject the archive if a path is unsafe or duplicated. Validating at write time is a convenience; validating at read time is the security boundary.
Path bytes are preserved as the writer saw them. This profile does not implement the whitepaper's native / legacy name model: it stores one UTF-8 path per object and makes no claim about recovering a non-UTF-8 original name. An implementation that needs that guarantee MUST NOT use this profile.
Unicode normalization is not applied. Two paths that differ only by normalization form (NFC vs NFD) or only by case are distinct paths here.
Such an archive is perfectly valid, and there are filesystems that cannot restore it: Windows folds case, and macOS folds NFC against NFD. A decoder that extracts to a real filesystem MUST detect that two objects landed on the same file and MUST fail with a fidelity error naming both archive paths. It MUST NOT let one overwrite the other, and MUST NOT report success.
Predicting the collision from the path strings is not sufficient — the folding rules belong to the target filesystem, not to the decoder. The reference implementations therefore check empirically: each restored file's identity (st_dev, st_ino) is recorded, and a target that resolves to an already-written identity is a collision. This catches case folding, normalization folding, and whatever else a filesystem does, without the decoder having to model any of it.
10. Reproducibility
Two inputs to packing are inherently variable: archive_uuid and created_unix_ns. Everything else is a function of the input files, their paths, their preserved mtimes, and the plan.
A conforming writer MUST therefore support reproducible mode: the caller supplies both variable inputs, and the writer emits a byte-exact archive.
pack(files, plan, uuid=U, created_ns=T) → identical bytes, on any conforming writer
This is the profile's strongest self-check, and the reason the canonical JSON rules and the object ordering rule are normative rather than advisory. The repository's cross-implementation test asserts that the Python writer and the JavaScript writer, given the same tree and the same (U, T), produce archives that are equal byte for byte — not merely mutually readable.
Note the one thing reproducibility does not cover: compression: "deflate" or "auto" embeds the output of a DEFLATE encoder, and different encoders (zlib at level 6 versus a browser's CompressionStream) legitimately produce different valid compressed bytes for the same input. Byte-exact cross-implementation equality is therefore asserted for compression: "store", and for "auto" / "deflate" archives the assertion is that each implementation reproduces its own output exactly and that both decode the other's output to identical raw bytes. A future profile that wants byte-exact compressed output across implementations would have to pin the encoder, not just the format.
11. Decoder safety
A conforming decoder MUST enforce, before allocating from any declared length:
header_length ≤ 16 MiB.- Every record extent lies inside the archive.
- Every declared offset and length is checked with arithmetic that cannot silently overflow. Python integers are unbounded; a JavaScript decoder MUST reject any 64-bit field above
Number.MAX_SAFE_INTEGERrather than round it into a plausible-looking offset, and MUST classify that rejection as a malformed manifest rather than a resource limit. The distinction matters for interoperability: no runtime can hold 2^53 bytes in memory, so a field that large necessarily points outside the archive — it is nonsense, not merely large, and a decoder with unbounded integers will reach the same conclusion by comparing the extent against the archive size. Differential fuzzing found the two reference implementations refusing the same byte for different stated reasons, which is how this sentence came to exist. - A configurable cap on total decoded output and on per-chunk decoded size, which MUST be checked while decoding, not after.
- Path validation per §9 for every object.
- No duplicate paths.
- No unknown record type, no unknown codec, no unknown object type.
A decoder MUST NOT execute anything found in an archive, MUST NOT fetch any external content — this profile has no external chunk references — and MUST NOT create special files, apply permissions, or follow links, because this profile stores none of those.
Failure MUST be explicit. A decoder MUST NOT emit a partial tree while reporting success. The reference implementations verify the whole archive before writing any output file.
12. Conformance
An implementation claiming ANLA-MVP v0.1 conformance MUST pass the conformance suite in