Immutable. This exact content is served forever at /api/v1/blob/63154b9b384f206f.
# AGENTS.md — working in the APH repository APH (Agent per Human) is a notarization protocol for outbound agent communications: a human-authorized Notary Service issues a W3C Verifiable Credential 2.0 envelope binding a specific payload, channel, agent, and policy decision to a human principal's authority. This repository holds the normative spec, a reference Rust implementation, a wasm/TypeScript binding, conformance fixtures, and an N Lang spec bundle. **Deep protocol knowledge lives in `skills/spec/SKILL.md`** (open Agent Skills format — load it before answering protocol questions or touching envelope JSON). Task recipes live in `commands/validate.md` and `commands/conformance.md`. This file carries only orientation, commands, and the invariants you must not break; it deliberately does not duplicate the skill. ## Repo map - `spec/aph-0.1.md` — the normative spec (wire shape §7, signing §8, key discovery §8.4, flows §9, errors §11). `spec/a2a-extension.md`, `spec/security-considerations.md` beside it. - `examples/` — golden envelope fixtures, one per channel kind, plus the FOUR really-signed ones: `principal_signed_envelope.json`, `es256_signed_envelope.json`, `detached_jws_envelope.json`, and `ts_minted_envelope.json` (minted by the TypeScript implementation, verified by the Rust). Enumerate the directory before stating a count; it has moved more than once. - `interpreters/rust/` — cargo workspace: `aph-core` (types + validation), `aph-conformance` (suite + fixture generator; its `tests/` also carry the multi-party exchange e2e — two-party admit-plus-refusals, three-party relay, cross-notary revocation — the only tests where two parties exchange anything, and the assembled §8.3 recipient algorithm lives in their shared harness), `aph-cli` (binary `aph`: `validate | inspect | golden`), `aph-resolver` (optional §8.4.5 DNS TXT + §8.4.4 `did:web` adapters over `aph-core`'s ports; the ONLY crate that may carry HTTP/DNS/runtime deps — never add an edge to it from any other member), `aph-ts` (wasm binding), `aph-py` (pyo3 binding, Python module `aph`) and `aph-js-harness` (a TEST harness and not a binding: the TypeScript implementation's compiled crypto-free core, run under a second ECMAScript engine inside the cargo process) — those last three all outside default-members, the two bindings at deliberate export parity with each other and with the Elixir binding, and none of the three a second implementation of anything. - `interpreters/elixir/` — the THIRD binding (`aph-ex`): a mix app `aph` over a rustler NIF at `native/aph_nif`, path-depending on `aph-core`. That crate is EXCLUDED from the cargo workspace, not merely from its default members: mix drives NIF builds, and two build drivers on one member is a reliability defect. ⛔ The BEAM inverts the hosting relationship — a NIF is Rust embedded in the BEAM, so no `cargo test` can ever see a term — which makes `mix test` the only gate on that boundary and makes every NIF function decode-string/call-core/encode-result and nothing else. A wrapper there that grows logic is a defect BECAUSE cargo cannot reach it. Zero cryptography on the Elixir side, by rule. - `interpreters/typescript/` — the SECOND implementation, and the one that IS. Full mint + verify written from the spec and the published examples, sharing no code with the workspace above: its own RFC 8785 canonicalizer, strict parser, §7.2.1 bases, base58btc and `did:key` codecs; every signature through WebCrypto. Node >= 20, zero runtime dependencies. ⛔ Not wasm and not `aph-ts` — do not conflate them, and never make one import the other. It cross-verifies with the Rust in both directions through COMMITTED BYTES; no node runs in cargo and no cargo runs in node — read that as PROCESSES, which is what it has always meant: neither stack shells out to the other, and no verdict depends on a pipeline. Its compiled output is a different matter and is read the same way committed bytes are: `aph-js-harness` evaluates the crypto-free half of this `dist/` inside the cargo process under an embedded second ECMAScript engine (`Engine::boot`), which is a portability check on the canonicalizer, not a second runner of the node suite and not a Node process. - `snapp/` — the exported N Lang bundle (generated by `nlang export`; its integrity digest is content-derived — never hand-edit it). - `skills/`, `commands/`, `.claude-plugin/` — the agent pack this file is the Codex/agents.md entry point for. ## Build and test (exactly what CI runs) From `interpreters/rust/`: ``` cargo test # workspace default members cargo test -p aph-ts # the wasm binding's native tests (NOT in the default set) wasm-pack test --node aph-ts # wasm32 smoke over the signed golden cargo test -p aph-py # the Python binding (NOT in the default set; needs a shared libpython) (cd ../go && go test ./...) # the Go binding (wazero; committed wasm is CI-byte-diffed) cargo test -p aph-js-harness # the TypeScript core under a second ECMAScript engine (NOT in the default set) cargo run -q -p aph-cli -- validate <envelope.json> ``` `cargo test -p aph-js-harness` needs `interpreters/typescript/dist` on disk, so CI runs it from the TypeScript workflow's `second-engine` job — after that job's `npm run build`, and with no Node process taking part in the cargo run itself. Run it locally the same way, or it stops with a BUILD FIRST message. From `interpreters/elixir/` (the Elixir binding; a separate CI job, and the ONLY gate that exercises the NIF term boundary — nothing in cargo reaches it): ``` mix deps.get # rustler, pinned tight to what the installed BEAM can build mix test # compiles the NIF through cargo, then runs ExUnit ``` From `interpreters/typescript/` (the second implementation; a separate CI job): ``` npm install # the TypeScript compiler, and nothing else npm run build # tsc -> dist/ npm test # node --test dist/test/ (Node 20 and 22 in CI) npm run mint # regenerates examples/ts_minted_envelope.json ``` ## Invariants — break these and CI (or a verifier somewhere) breaks you 1. **Strict parsing everywhere.** Every envelope struct is `deny_unknown_fields`; an unknown key is a hard error, never dropped. 2. **The `proof` union is position-sensitive.** It is untagged: a single object (`NotaryAttested`) or a two-proof chain (`PrincipalSigned`, principal first). This is why EVERY binding boundary is JSON text in both directions — never route envelopes through a host-language object round trip, in JS, in Python or in Elixir. Which arm deserializes is decided by the bytes; an object route lets a second deserializer decide it instead. The BEAM is where that is easiest to get wrong: Erlang integers are arbitrary precision, so the number-widening argument does not apply and the term route looks safe — but a map/list ENCODER still has to pick an arm of the union with no schema to consult. 3. **Absent `attestationMode` means `NotaryAttested`.** A `PrincipalSigned` label is never trusted alone: structure is checked (`APH_E013` on a forged label), and mode downgrades are refused (`APH_E012`). 4. **The error taxonomy is a closed set of sixteen** (`APH_E001`–`APH_E016`). `APH_E014` is TERMINAL ABSENCE of a discovery surface and nothing else — absence advances the §8.4.6 order (`did:key` → DNS TXT → `did:web`). A surface that was offered and then FAILED never falls through: it refuses on the spot under the failure's OWN code (`APH_E008` unreachable, `APH_E003` validity window, `APH_E010` algorithm, …), never a blanket one. `APH_E015` is a delegation mandate REVOKED per the §6.3.3 status list — a withdrawn authorization over still-valid signatures, held distinct from `APH_E003` (authority that ran out on schedule). `APH_E016` is UNROOTED AUTHORITY — a human-not-present act with NO mandate at all — held distinct from `APH_E011` (a mandate that exists and fails) for the same absence-is-not-failure reason the discovery order runs on. `APH_E008` now covers any protocol-mandated fetch from a notary-hosted surface, the status list included, not only a notarization request that timed out. 5. **A status verdict must name the key that authenticated it.** `aph_core::credential_status::check_envelope_status` takes the issuer's verifying key BY VALUE, plus a caller-supplied gzip expander (this crate carries no compression dependency — it links into wasm and into a kernel). Neither is an oversight: an unverified status list is an unauthenticated assertion about whether someone's authority still holds, so an unauthenticated verdict is made unrepresentable rather than merely discouraged. Do not add a convenience wrapper that resolves the key internally or defaults it — the whole point is that a caller cannot skip it. Integration recipe in `skills/spec/SKILL.md`. 6. **Signed fixtures are never text-edited.** Four published examples carry real signatures over their own bytes. Three have Rust generators under `interpreters/rust/aph-conformance/tests/`; the fourth, `examples/ts_minted_envelope.json`, is minted by `interpreters/typescript` and regenerated with `npm run mint`. Both recipes are in `skills/spec/SKILL.md`. 7. **Test keys only.** Fixtures sign with published test vectors — RFC 8032 §7.1 for Ed25519, RFC 6979 A.2.5 and RFC 7515 A.3.1 for P-256, since no repeated-byte fake names a valid P-256 scalar. Cite the document a scalar comes from in the code. Never place production-looking key material — and never any private key that is not a published test vector — in a committed or published artifact. 8. **Domains are `squillo.com`.** The former domain is retired; do not reintroduce it in examples or docs. 9. **Name-hygiene CI.** Every push runs brand/name hygiene checks over the whole tree; keep tool-vendor names and internal-repo path references out of committed files, and write interpreter paths as `interpreters/rust/<crate>`. ## The live reference deployment `did:web:aph-notary.squillo.com` is a live §8.4.4 publication surface you can resolve against. Its degrade shape and what resolving it exercises are described in `skills/spec/SKILL.md`.