Skip to main content

L1 representation

Status: Gate 1 normative Semantic contract: identity.md This chapter owns the exact L1 representation. Its JCS, JWK, JWS, JWE, and HTTP mechanisms remain private to the deep identity package.

Standards substrate

Effect Schema is the only JSON parser and validates every public network boundary. canonicalize supplies RFC 8785 JCS. jose supplies General JWS, General JWE, and JWK thumbprints. @noble/curves supplies the RFC 7748 map from Ed25519 keys to X25519 keys. http-message-signatures and structured-headers supply RFC 9421 and RFC 8941 mechanisms. Identity exports none of those libraries and adds no project-owned JSON parser, canonicalizer, JOSE stack, HTTP-signature stack, or structured-field stack. There is no jsonc-parser.visit, second JSON parser, generic codec, or public serialization layer. MessagePack, deterministic CBOR, COSE, X.509, and application TLS are not L1 mechanisms. This choice does not change any later-layer representation.

Canonical JSON

Every L1 JSON request, result, signed payload, protected header, and complete General JWS or General JWE object uses RFC 8785 JSON Canonicalization Scheme UTF-8 bytes. The one exception is the SealedBody protected header. A complete decoder:
  1. reads within the owning artifact’s fixed maximum or the HTTP route’s privately derived body cap;
  2. decodes fatal UTF-8;
  3. parses one unknown JSON value with Effect Schema.parseJson();
  4. applies a private Effect Schema refinement that rejects container depth over 16 and unpaired Unicode surrogates;
  5. canonicalizes that value with JCS and requires the input bytes to match; and
  6. decodes through the exact closed schema.
No semantic value escapes before every step succeeds. Registered-agent AuthenticatedHttp and Registry bootstrap admission each perform steps 1 through 4 as their parse prelude, perform step 5 before authentication, then perform the complete route-owned schema decode at the specified later stage. Duplicate object names require no second parser. Native JSON parsing collapses them, so the resulting value’s JCS bytes cannot equal input bytes that contain the repeated member. The mandatory byte comparison therefore rejects every duplicate-name input. The maximum decoded JSON container depth is 16. A root object or array has depth 1, each nested object or array adds 1, and scalar values add no depth. A decoder rejects excess depth before constructing a semantic value. This Gate 1 bound is not deployment-configurable. Unknown members, duplicate members, trailing data, byte-order marks, lone surrogate code points, and non-JCS number spellings are rejected. No input is normalized before comparison. Objects shown in this chapter use readable key order. Their bytes use JCS key order.

Base64url

JWS, JWE, identifiers, digests, nonces, JWK coordinates, and opaque bodies use RFC 4648 base64url without = padding. Decoders reject:
  • the standard base64 + and / alphabet;
  • padding;
  • whitespace;
  • an impossible encoded length;
  • nonzero unused trailing bits; and
  • any spelling that does not re-encode byte-identically.
An opaque body may contain zero bytes, represented by the empty string. After canonical base64url decoding it contains at most 262,144 bytes. This Gate 1 maximum is fixed, not deployment-configurable.

Refined values

issuedAt rejects fractional seconds, offsets, spaces, leap-second spelling, and any equivalent alternate representation. Each semantic value has a distinct nominal type. A decoder for one prefix never constructs another type.

Ed25519 public JWK

The public JWK is exactly:
Member names and values are case-sensitive. No alg, d, key_ops, kid, use, X.509 member, JWK URL, or extension member is permitted. The key identifier is the RFC 9278 JWK Thumbprint URI computed from the exact public JWK:
<thumbprint> is the canonical unpadded base64url SHA-256 digest defined by RFC 7638 and RFC 9278. Every Ed25519 verification in this chapter uses one strict profile. Signature R and S use their canonical encodings, S is less than the Ed25519 subgroup order, and small-order public keys are rejected. Verification uses the cofactorless equation. These rules apply identically to AgentCard, SignedMessage, registered-agent HTTP, and Registry bootstrap signatures.

AgentCard

Payload

The payload is exactly:
The payload has no optional member.

General JWS

An encoded AgentCard is one attached General JWS object:
The decoded protected header is exactly:
Ed25519 is the fully specified JOSE algorithm identifier from RFC 9864. An AgentCard verifier receives the deployment-pinned Registry signer public JWK. It requires the protected kid to equal that key’s RFC 9278 thumbprint URI and verifies the signature with that key. The JWS never supplies its own trust root. The outer object, signature object, protected header, and payload are closed. signatures has exactly one member. The signature object has no unprotected header. Compact JWS, Flattened JWS, detached payloads, multiple signatures, unprotected headers, b64, crit, cty, embedded JWKs, JWK URLs, and X.509 members are rejected. AgentCardDigest is acd_ plus canonical base64url SHA-256 over the UTF-8 JCS bytes of the complete General JWS object. The public AgentCard Effect Schema has this General JWS as its encoded side. Its TypeScript side exposes exactly the readonly domain fields agentId, principalId, agentName, publicKey, and issuedAt. The exact JWS remains private in the value for lossless Schema encoding, digesting, and verification. publicKey is an immutable snapshot. Fixed kind and sharnessVersion remain representation invariants, not duplicate domain fields. VerifiedAgentCard is a nominal subtype with the same readable fields. Only successful AgentCard.verify constructs its inaccessible trust brand; no Schema, constructor, or standalone decoder is public.

SignedMessage

Payload

The payload is exactly:
recipientAgentIds has 1 to 128 entries. After decoding the AgentId payload bytes, entries are unique and strictly increasing by unsigned bytewise order. A signer constructs that order. A verifier rejects a different order instead of sorting it. The canonical base64url body decodes to 0 through 262,144 opaque bytes. Identity and Router decode only that representation boundary; they never interpret or transform its contents, except that an endpoint may ask Identity to seal a body before signing it or to open a verified SealedBody.

General JWS

An encoded SignedMessage has the same exact one-signature attached General JWS shape as AgentCard. Its protected header is exactly:
The same closed-shape and rejection rules apply. The sender’s verified AgentCard public JWK verifies the signature. senderAgentId, agentCardDigest, protected kid, and the resolved card must all bind to the same immutable identity. The Router-owned SignedMessageDigest representation is specified in router-representation.md. The public SignedMessage Effect Schema has this General JWS as its encoded side. Its TypeScript side exposes exactly the readonly domain fields senderAgentId, agentCardDigest, recipientAgentIds, messageId, and body. It retains the exact JWS privately for lossless Schema encoding, verification, hashing, and forwarding. recipientAgentIds is an immutable snapshot, and reading body returns a defensive Uint8Array copy. Fixed kind and sharnessVersion remain representation invariants. VerifiedSignedMessage is a nominal subtype with the same readable fields. Only successful SignedMessage.sign or SignedMessage.verify constructs its inaccessible trust brand; no Schema, constructor, or standalone decoder is public. SignedMessage.encodedByteLength returns the UTF-8 JCS byte length of the complete retained General JWS and has no failure channel. Under the fixed recipient and body bounds, SignedMessage.maximumEncodedByteLength is exactly 471,676. Identity owns both operations and the overflow-checked calculation; consumers do not reproduce its General JWS size formula.

SealedBody

A sealed body is the JCS UTF-8 encoding of one RFC 7516 General JWE whose key management is ECDH-ES+A256KW over X25519 and whose content encryption is A256GCM. With two or more recipients it is exactly:
and its decoded protected header is exactly:
org.social-harness/sender and org.social-harness/message-id are the senderAgentId and messageId of the SignedMessage that carries the body. An opener refuses a body whose header sender or MessageId differs from those of the verified SignedMessage. The JWE plaintext is a fresh 32-byte random salt followed by the plaintext. org.social-harness/commitment is the SHA-256 digest of those salted bytes. An opener recomputes the digest from the decrypted bytes, refuses a mismatch or decrypted bytes shorter than the salt, and returns the bytes after the salt. A256GCM is not key-committing, so without the commitment a sender could wrap a different content-encryption key in each entry over one ciphertext and tag that authenticate under every key, and recipients would open different plaintexts. The salt keeps the digest from confirming a guessed plaintext. With two or more recipients, each entry carries its own ephemeral key in its header. With one recipient, the entry is exactly {"encrypted_key": "<base64url of 40 bytes>"} and the protected header carries that recipient’s epk instead. These are the placements jose produces, and a decoder rejects any other placement. The representation uses only jose’s supported multi-recipient API, which gives each recipient its own ephemeral key: jose accepts a caller-chosen ephemeral key only through its epk key-management parameter, which it documents as intended only for testing and vector validation. Recipient entries carry no key ID. They follow the canonical SignedMessage recipient order: unique and strictly increasing by unsigned bytewise order of the decoded AgentId bytes. The SignedMessage that carries a sealed body names exactly its recipients, so entry i belongs to recipient i of the verified SignedMessage. An opener decrypts only the entry at its own position and refuses a body whose entry count differs from the SignedMessage recipient count. The protected header is the one L1 JSON value that is not JCS. jose serializes it as JSON.stringify of the members in the order shown, and for a single recipient it writes epk after the sender member with epk members in x, crv, kty order. A decoder parses it with Effect Schema, checks its exact members, rebuilds that spelling from the decoded values, and refuses unless the rebuilt UTF-8 bytes equal the decoded protected bytes. A256GCM authenticates whatever header bytes the sender encrypted under, so this comparison is what refuses reordered, spaced, escaped, or repeated members and a byte-order mark. A decoder rejects, before any key agreement: outer bytes that are not JCS; unknown or missing members, including aad and unprotected; any other algorithm; a segment of the wrong decoded length; noncanonical base64url; and an empty list or more than 128 recipient entries.

Size

For an N-byte plaintext sealed to R recipients, the sealed body is exactly: The second recipient adds 154 bytes, because a single recipient’s ephemeral key moves from the protected header into its entry. Each recipient after that adds 171 bytes. At 32 recipients the fixed part is 5,861 bytes, so the largest plaintext whose sealed body fits the 262,144-byte SignedMessage body cap is 192,180 bytes, which seals to exactly 262,144 bytes. Identity owns this calculation through SealedBody.sealedByteLength and SealedBody.maximumPlaintextByteLength; consumers do not reproduce the formula.

HTTP request framing and ownership

Identity defines three distinct HTTP boundary owners:
  • registered-agent AuthenticatedHttp owns only the normal profile;
  • Registry owns registration bootstrap admission and its proof-of-possession profile; and
  • Registry lookup and list are public reads with no signature profile.
Registration never creates or consumes an authenticated existing-agent context. It does not add registration members to AuthenticatedHttp. The two signed profiles use canonical JSON bodies with these outer shapes. Normal:
Registration:
The route supplies the exact closed schema replacing {}. Registry and Router domain POST requests:
  • have exactly one Content-Type field whose field value is application/json, with no parameter;
  • have no Content-Encoding;
  • do not require Accept;
  • have exactly one Sharness-Version field whose value is in LIVE_WIRE_VERSIONS; and
  • use one body whose octets are the canonical JSON bytes.
Every Registry and Router route requires an absent request-target query component. Any present query component, including an empty one, fails route lookup as 404 {"error":"not_found"} before authentication or domain handling. This raw request-target check is independent of signature verification. Under RFC 9421, accepted requests cover @query with the derived value ?; that derived value alone does not distinguish an absent query component from a present empty one. Both signed profiles also have exactly one Content-Digest dictionary member:
Duplicate covered fields, combined alternate values, or an additional digest algorithm are rejected. Responses with JSON bodies use Content-Type: application/json and JCS bytes. GET /healthz has no body. Application code imposes no URL scheme or TLS requirement. L1 and L2 responses carry no application-layer response signature; their network-path assumption is specified in identity.md.

HTTP message signatures

Requests using either signed profile contain exactly one Signature-Input dictionary member and one matching Signature dictionary member. Their label is sharness.

Normal profile

The signature-input covered-component order is exactly:
Its tag is sharness-request-v1.

Registry registration profile

The covered-component order is exactly:
Its tag is sharness-registration-v1.

Parameters

After the covered components, signature parameters appear exactly once and in this order:
  1. created
  2. expires
  3. keyid
  4. nonce
  5. alg
  6. tag
created and expires are integer Unix seconds. created is not later than expires; the interval is at most 300 seconds. At verification, created may be at most five seconds after the verifier clock and expires must not be earlier than it. keyid is the signing JWK’s RFC 9278 thumbprint URI. nonce is the 22-character canonical base64url representation of 16 random bytes. alg is the RFC 9421 HTTP Signature Algorithms registry value ed25519. tag is the exact profile tag above. Extra labels, covered components, parameters, or signature bytes are rejected.

Registration admission

Registration has exactly one covered authorization field:
The credential is 8 to 512 token68 characters. Leading or trailing whitespace, multiple credentials, parameters, and a different scheme are rejected. The configured and received credential remains redacted in values, logs, traces, metrics, defects, and public errors.

Registered-agent AuthenticatedHttp validation order

The route owner performs stages 1 and 2 before calling AuthenticatedHttp.verifyAgentRequest with a copied bounded body. Together they perform:
  1. exact route lookup and method check;
  2. content framing, content type, content encoding, the route-derived body cap, and the immediate request-concurrency permit;
  3. fatal UTF-8, one Effect Schema JSON parse, depth and Unicode refinement, JCS byte comparison, and minimum caller extraction;
  4. Registry resolution, content digest, HTTP signature, card and key binding, algorithm, tag, and time checks;
  5. atomic nonce claim;
  6. signed wire compatibility value check;
  7. complete closed route-owned request Schema decode; and
  8. domain handler.
For Router send and poll, stage 7 validates the closed outer request while retaining signedMessage as a bounded raw JSON object and pollCursor as a bounded string. The Router domain handler performs the complete SignedMessage or PollCursor decode, so their failures map to message_invalid or cursor_invalid, respectively. At the end of stage 6 the verifier has constructed the opaque nominal VerifiedAgentRequest: caller AgentId, verified AgentCard, and the still-unknown inner request. Only the route-owned Schema decodes that request at stage 7. At stage 5, an already claimed live nonce is authentication_failed. A novel nonce that cannot be retained because the live-nonce capacity is full is overloaded with status 429; it is not claimed, and no unexpired nonce is evicted.

Registry bootstrap-admission validation order

Registry performs:
  1. exact registration route lookup and method check;
  2. content framing, content type, content encoding, the route-derived body cap, and the immediate request-concurrency permit;
  3. fatal UTF-8, one Effect Schema JSON parse, depth and Unicode refinement, JCS byte comparison, and minimum submitted-key extraction;
  4. content digest, HTTP signature, submitted-key binding, admission credential, algorithm, tag, and time checks;
  5. durable atomic registration-nonce claim;
  6. signed wire compatibility value check;
  7. complete closed RegistryRegisterRequest Schema decode; and
  8. the Registry private RPC middleware and domain handler.
Registration performs no AgentCard resolution and constructs no VerifiedAgentRequest. Successful stage 4 produces private bootstrap admission context. An already-live nonce is 401 authentication_failed; a novel nonce that cannot be retained is 429 overloaded without a claim or live-nonce eviction. For both signed profiles, an otherwise valid wrong-version request consumes its nonce. A later schema or domain refusal does not roll the claim back.

Public Registry read validation order

Lookup and list perform:
  1. exact route lookup and method check;
  2. content framing, content type, content encoding, body byte bound, and early concurrency bound;
  3. live-set Sharness-Version check;
  4. fatal UTF-8 decoding, JSON parsing, complete closed-schema decoding, JCS re-encoding, and byte comparison; and
  5. domain handling.
Public lookup and list reject Content-Digest, Signature-Input, Signature, or Authorization as stage-2 400 malformed. They do not ignore or verify an authentication profile that the route does not own. When multiple conditions fail, the earliest stage determines the response. Framing failures therefore precede version_mismatch, and version_mismatch precedes malformed body or schema failures. Public reads perform no key resolution, signature verification, admission check, or nonce claim.

Exact envelope precedence

Each profile evaluates its numbered stages in order. Within one stage, the first matching row below determines the response. GET /healthz uses the common stage-1 route and method rules, then only its route-specific 204 or 503 readiness contract. Common route and framing outcomes are: Registered-agent AuthenticatedHttp outcomes after common framing are: Registry bootstrap-admission outcomes after common framing are: A route owner may define a later capacity check that is meaningful only after its domain validation. Router held-poll admission and private-order exhaustion are such checks: router.md places them only where a verified continuation would otherwise wait or a verified fresh initial send would otherwise append. Public Registry read outcomes after common framing are: Reaching a domain refusal or success returns the route’s closed status 200 result. Router-owned post-authentication SignedMessage artifact and message-bound failures are domain results specified in router-representation.md; owner saturation remains the 429 envelope outcome above. Normal authentication, AgentCard resolution, key binding, digest, signature, timing, and replay failures all become:
Registration admission and proof failures produce the same public response. Registry resolution infrastructure failure produces unavailable.

Envelope failures

Non-domain failures have exactly the bodies below; supported lists the server’s live wire versions: The kind field below is the common closed-union discriminator already used by AgentCard and SignedMessage. No second result-tag vocabulary is introduced.

Registry routes

Register, lookup, and list each have one private overflow-checked request-body calculator derived from their closed maximum representation. The HTTP reader applies that route’s cap before UTF-8 decoding or parsing. These are three derived private values, not one generic bound, environment keys, or public configuration members. Boundary tests accept each exact maximum and reject one additional octet.

Register

POST /v1/identities:register uses the Registry-owned bootstrap-admission outer body:
A well-formed authenticated domain request returns status 200 and exactly one result:
agentCard is the complete General JWS object specified above.

Lookup

POST /v1/identities:lookup is public and accepts exactly one of:
It returns status 200 and:
or:

List

POST /v1/identities:list is public and accepts:
or:
It returns status 200 and:
agentCards contains complete General JWS AgentCards in strictly increasing decoded AgentId order. Its maximum length is the server-configured page size. hasMore is true exactly when another card exists after the last returned AgentId. Register, lookup, and list response Schemas are private. They first parse these exact untrusted representations. The client then verifies each AgentCard, request binding, ordering, uniqueness, and lower bound as applicable before constructing RegistryRegisterResult, RegistryLookupResult, or RegistryListResult. Those public result types are not standalone parsing Schemas.

Health

GET /healthz returns:
  • 204 with no body when ready; or
  • 503 with no body when not ready.
It requires no wire compatibility value or request signature.

Rejection summary

  • A noncanonical or schema-invalid public Registry request is 400 malformed.
  • A missing or wrong version on a public Registry request is 412 version_mismatch.
  • Domain refusals use status 200 and their closed kind result.
  • No error or result carries a free-form reason.