@muretai/agent-seam
v0.3.4
Published
The byte contract two programs that have never met authenticate each other with: canonical JSON, did:key, the six signed fields, the signed Agent Card envelope, device-key binding, Web Bot Auth verify, cryptobox. Golden vectors plus five independent refer
Readme
agent-seam
The byte contract two programs that have never met authenticate each other with, as golden
vectors plus five independent reference implementations — JavaScript, Python, Go, Rust, PHP.
Zero dependencies in four of them (PHP needs sodium). MIT.
Two programs that have never met can still prove who they are to each other, if they agree on
bytes: canonical JSON, did:key, six signed fields, a signed Agent Card envelope, device-key
binding, the Web Bot Auth verify side, a sealed box. That agreement is this repository. It is
not a library either side calls — it is the shape of what goes over the wire, written down
once, with vectors that say what is right and what must be refused.
The coupling is invisible and unforgiving. No dependency manager can see it, and when it breaks nothing throws: signatures simply stop verifying, and the only diagnostic anyone gets is "signature verification failed". So the contract lives on its own, belongs to no implementation, and every implementation is held to it the same way.
This repository is the home, not a copy. The JavaScript and the Python reference are edited here; everything that reproduces these bytes carries a pinned copy it took from here, and none of them owns the contract.
spec/seam.md— the contract, one section per vector groupvectors/— the golden vectors (wire_vectors.json,wba_vectors.json)js/seam.mjs— the JavaScript reference, one file,node:cryptoonlypython/shared/— the Python reference, 18 modules, stdlib only (cryptographyoptional)php/seam.php— the PHP reference, one file,sodiumonly; the WordPress plugin vendors it and adds its ownABSPATHguardtools/manifest.json— what this repository publishes and how a consumer cuts it;tools/check-manifest.mjskeeps it exact
Run
npm test # manifest 17 · JS conformance 149
cd go && go run ./conformance # Go: OK — 88 checks
cd rust && cargo run --quiet --bin conformance # Rust: OK — 88 checks
php php/conformance.php # PHP: CONFORMANT — 121 passed
npm run test:py # Python: closure 220 · wire vectors 288 · web bot auth 5 accepted / 25 refused
# · keybinding · cryptobox · gateway · neturl
# · ed25519 backend agreement 316 (library vs pure-python)Each of those numbers is a FLOOR the runner asserts, not a number it merely prints, because a count nobody asserts is a count that can quietly fall. Raising one is the correct response to adding a check.
A floor alone is not enough: it does not say WHICH group stopped running, and a group renamed
rather than deleted does not move the total at all. So tools/manifest.json
implementations[].groups — which nothing read until now — is an assertion too. The
JavaScript, Go and Rust runners attribute every check they make to a group name spelled
exactly as the manifest spells it and diff the two BOTH WAYS: a declared group that produced
no checks is red, and a driven group the manifest does not declare is red. Go and Rust hold
their skips to the same standard from the other side. The Python runner does not attribute
per group — its checks are spread across a dozen functions and an attribution that were merely
plausible would be the very defect this is about — so it asserts instead that every declared
group resolves to a non-empty place in the vectors, and that every group in the vectors is
claimed by some implementation.
Go and Rust print two skips: reject.keystate and reject.cardpub, SKIPPED ON PURPOSE.
Neither implements KeyState or the card envelope, and a group nobody loops over looks exactly
like a group that passes — so each asserts the group is present and says out loud that it is not
checking it.
Nothing dials out, nothing needs an account, and nothing here reads another checkout: this
repository's tests are its own. Python ≥ 3.9; cryptography is needed only for cryptobox and
the P-256 legs (both are skipped, not failed, without it).
Re-derive the golden files from the Python reference and diff them against the committed ones:
AGENT_SEAM_VECTORS=/tmp/wire.json python3 python/test_wire_vectors.py --regen && diff /tmp/wire.json vectors/wire_vectors.json
AGENT_SEAM_WBA_OUT=/tmp/wba.json python3 python/tools/gen_wba_vectors.py && diff /tmp/wba.json vectors/wba_vectors.jsonBoth diffs are empty: the Python reference is the generator of the bytes everything is held to.
Coverage
| Vector group | JS (js/conformance/run.mjs) | Python (python/test_wire_vectors.py) | Go (go/conformance) | Rust (rust/conformance) | PHP (php/conformance.php) |
|---|---|---|---|---|---|
| canonical | ✓ | ✓ | ✓ | ✓ | ✓ |
| numberHazards | read, not executed (a signer rule) | ✓ | ✓ executed as refusals | ✓ executed as refusals | ✓ executed as refusals |
| did | ✓ Ed25519, both directions | ✓ both curves | ✓ Ed25519, both directions | ✓ Ed25519, both directions | ✓ both curves |
| reject.did | ✓ 3 | ✓ 3 + a base58 control | ✓ 3 | ✓ 3 | ✓ 3 |
| envelope | ✓ + round trip | ✓ | ✓ + round trip | ✓ + round trip | ✓ + round trip |
| reject.message | ✓ | ✓ (+ invite, claim) | ✓ | ✓ | ✓ (+ claim) |
| reject.encoding | ✓ canonicalFromJSON, 4 accept + 9 refuse | ✓ crypto.canonical_from_json | ✓ seam.CanonicalFromJSON | ✓ serde_json::from_slice | skipped, and the skip is asserted by name |
| reject.keystate | ✓ resolveOpDid(…, { pinned }), 10 accept + 3 refuse | ✓ keystate.resolve_op_did | skipped, and the skip is asserted by name | skipped, and the skip is asserted by name | skipped, and the skip is asserted by name |
| cardpub | ✓ payload + verify + anti-substitution | ✓ | — | — | ✓ |
| reject.cardpub | ✓ 3 | ✓ 3 + a fail-open meta-control | skipped, and the skip is asserted by name | skipped, and the skip is asserted by name | ✓ 3 |
| bindingV2 | ✓ 2 accept + 4 reject, expectedDeviceDid from the case | ✓ same | — | — | ✓ same |
| ownerState | — | ✓ 2 accepted + anti-substitution + 5 refused | — | — | — |
| relay | — | ✓ signatures, the \| join order, the origin binding | — | — | — |
| binding (v1), domainLinkage, invite | — | ✓ | — | — | — |
| Web Bot Auth (wba_vectors.json) | ✓ 5 + 25 | ✓ (python/test_webbotauth.py) | — | — | skipped, and the skip is asserted by name |
| cryptobox | ✓ open (with ad) + mustNotOpen + encPub | ✓ | — | — | skipped, and the skip is asserted by name |
This is where the bytes live
A change to the seam is made here — js/seam.mjs and the matching python/shared/ module,
the vectors regenerated (--regen above), spec/seam.md updated — and committed, usually under
a tag. Consumers then re-vendor. The rule that keeps the copies honest is the same everywhere:
- a vendored file is edited only here and re-pulled by the consumer's own script;
- every vendored set sits beside a
VENDOR.jsonrecording the commit, the version and the sha256 of each file as written; - the consumer's tests verify its copies against those digests without this repository
present, and, only when a checkout of this repository is beside them, also verify that the
recorded commit really produces those bytes (
git show <commit>:<path>).
No script anywhere writes into another repository. tools/manifest.json says what a consumer may
cut — the block the door carries, the declarations it pins by name, the module list, the vector
files — and npm test proves the manifest describes js/seam.mjs exactly.
Who carries a copy
| Consumer | What it vendors | Pin | Re-vendor |
|---|---|---|---|
| @muretai/agent-entry — the JS door, MIT | the block and the pinned declarations, spliced into its one file; seam.mjs and both vector files under vendor/agent-seam/ | vendor/agent-seam/VENDOR.json | npm run vendor:seam -- --ref <tag> |
| The author's Python node implementation (AGPL-3.0, not public) | the 18 modules, the four verbatim suites, both vector files | a VENDOR.json beside them | its own vendor tool, at the tag |
| @muretai/agent-site-checker | src/seam.mjs, whole | the digest in tests/check-seam.mjs | cp, as its header says |
| @muretai/agent-web-router | seam.mjs and test/wire_vectors.json | the digests in test/seam-twin.test.mjs | cp, as its header says |
| agent-entry-wordpress — the PHP twin | tests/wire_vectors.json | tests/VENDOR.json | cp |
| Swift (apple-agent-kit SeamKit), Kotlin, the browser extension | wire_vectors.json | their own | cp |
agent-entry-serverless vendors the whole door from agent-entry, not this repository; the seam
reaches it inside the door.
A gap this repository knew about, and the one it still has
canonical/key-ordering-unicode was named "keys sort by CODE POINT, not by UTF-16 unit", and
its keys were z, a, 群, A — every one inside the Basic Multilingual Plane, where
code-point order and UTF-16 code-unit order are the same. The case could not tell the two
apart, and no other case had a key outside the BMP either, so an implementation that sorted
keys the way a plain JavaScript .sort() does passed all 180 checks and would have disagreed
with Python the first time a signed object carried an emoji as a KEY. The Go port found it.
Closed. canonical/key-ordering-astral is {"\U0001F600": 1, "\uFFFD": 2} — an astral key
beside a high-BMP one, which is the only shape where the two orders separate (U+1F600 leads
with the UTF-16 unit 0xD83D, below U+FFFD's single 0xFFFD). The generator asserts that the
pair really does discriminate, and key-ordering-unicode stays beside it as the statement of
the rule that a wrong implementation still passes. Measured: flip canonicalJSON to
Object.keys(v).sort() and exactly one check fails, the astral one.
Still open: duplicate JSON keys are LAST-WINS by agreement rather than by rule. All four
references were measured taking the last occurrence and reject.encoding.accept/
duplicate-key-last-wins now pins that, so a fifth port that picks the first, or refuses, is
red instead of silently signing different bytes. Refusing a duplicate key outright is the
stronger rule — {"a":1,"a":2} and {"a":2} are two wire documents with one canonical form,
which is the same collision the byte order mark is refused for — and it is not the rule today
because closing it means a duplicate-detecting parse boundary in four languages and a change
to what every consumer's canonicalFromJSON accepts. spec/seam.md §1.2 argues it out.
Adding a language
The contract is spec/seam.md + vectors/; an implementation is one directory that re-derives
them. go/ and rust/ are the worked examples: each is one library file plus a runner that
reads ../vectors/wire_vectors.json and prints OK — N checks / exits 1, and each was written
from the spec and the vectors alone. Go needs nothing but its standard library; Rust takes
ed25519-dalek for the signatures and serde_json with arbitrary_precision for reading the
vectors, because Rust ships neither, and rust/Cargo.toml says why in the file. Add a row to
tools/manifest.json implementations[] and a column to the coverage table above. Add a row to
tools/manifest.json implementations[] and a column to the coverage table above. A language
may implement a subset of groups; the table says which. Existing external implementations
already vendor the vectors, so moving one in is "copy the directory, point its runner at
../vectors/, add the row".
Designed in, not built
- npm publish —
package.jsonis complete (@muretai/agent-seam, exports./js/seam.mjs, the vectors and the manifest) and marked"private": trueuntil the owner publishes. Once it is on npm, a JS consumer's vendored copy can become a dependency at the same pinned version. - pip — the Python directory keeps the on-disk name
shared/(a namespace package) so core's copies stay verbatim; a PyPI package would ship anagentseamshim that installssys.modules["shared"]. Until then:PYTHONPATH=python. - The door imports
seam.mjsinstead of carrying the block, assembled into one file at release time so the package stays a single dependency-free file.
Name
The repository was created on 2026-09-06 as agent-wire; that name is held on npm by a
stranger's placeholder and on PyPI by an unrelated package, so on 2026-09-07 it became
agent-seam — the word the Swift (SeamKit) and Kotlin (seam/) implementations already use
for this layer. The vector file is still wire_vectors.json: every consumer pins that name.
Not here
The door (discovery paths, the ladder of checks, the store, rate limits) — that is
agent-entry. depositToRelay, the relay client. The
pay/v0 grant and receipt objects (an experimental line of the door; they join at graduation).
KeyState now has a vector group — reject.keystate, the anti-rollback ratchet, driven through
the four-argument resolveOpDid(rootDid, inline, now, { pinned }) and Python's
keystate.resolve_op_did. What is still NOT here is the pin STORE: the seam fixes the policy
and exposes the argument, and a door supplies the memory — what it keys the store by, when it
writes, and what an operator sees when a peer's epoch walks backwards are its decisions.
One thing is worth knowing: python/shared/gateway.py defaults to muretai.com / muretai.net
hosts unless MURETAI_PUBLIC_BASE / MURETAI_INVITE_BASE are set. Nothing in the runners dials
out; the network-capable helpers in neturl, webbotauth and invite are present because the
modules are whole, not because anything here calls them. invite.py installs its urllib opener
from the three functions that actually dial, never at import — a verifier embedded in somebody
else's tool must not silently re-configure their HTTP client.
Licence
MIT — LICENSE. The Python modules are published here by their copyright holder
under MIT; the same bytes are also carried, privately, under AGPL-3.0. See
PROVENANCE.md.
