@muretai/agent-entry 1.6.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,7 +114,7 @@ answers questions and hands off nothing, is a signed claim you cannot keep.
114
114
  | `domains` | none | the domains this entry speaks for (see below) |
115
115
  | `basePath` | from `baseUrl` | the path this entry answers at, derived rather than set beside it |
116
116
  | `wbaVerifiers` | none | a JWKS document (`{"keys":[…]}`) of Ed25519 keys whose holders this entry should **recognise** on inbound signed requests (Web Bot Auth / RFC 9421 — see *Who is knocking*). Recognition only adds `env.wba_did` and a visit count; it never changes a verdict |
117
- | `observer` | none | called once per message with the same envelope your responder gets, **after** the verdict — for counting, logging, analytics. It cannot matter: its return is discarded, a throw is swallowed, a promise is never awaited, so a slow or broken watcher cannot delay or change one byte of the signed reply. See [Counting visits](#counting-visits-without-handing-over-your-customer-list) |
117
+ | `observer` | none | called once per message with the same envelope your responder gets, plus `stage`, `identified` and `ua_family`, **after** the verdict — for counting, logging, analytics. It cannot matter: its return is discarded, a throw is swallowed, a promise is never awaited, so a slow or broken watcher cannot delay or change one byte of the signed reply. See [Counting visits](#counting-visits-without-handing-over-your-customer-list) |
118
118
  | `howToUrl` | none | a page a keyless visitor is pointed at as a worked example. **Empty means omitted** — the refusal already teaches the whole recipe without it, and a reference implementation must not stamp somebody else's docs host into every door built from it. Only set it to a URL you operate, and only after checking it resolves |
119
119
  | `name`, `description`, `version` | — | the card's own words. `description` is the line a person reads in a directory listing — and the right place to say what you record about visitors, since it is fetched **before** the knock |
120
120
 
@@ -138,10 +138,21 @@ Each request's `User-Agent` is classified into a fixed family (`claude-user`,
138
138
  `claudebot`, `gptbot`, `openai`, `perplexity`, `google-extended`, `muretai-node`,
139
139
  `curl`, `browser`, `none`/`other`) and counted by stage. In-process state like the
140
140
  ledger — read it, log it, ship it to your analytics; it is never served on the wire.
141
- An AI-agent family also gets one nudge: `GET /` answers it with
142
- `Link: </.well-known/agent-card.json>; rel="service-desc"` (RFC 8631), so a crawler
143
- that landed on prose is handed the machine-readable door. The body stays
144
- byte-identical for every caller.
141
+ Every caller also gets one nudge: `GET /` answers with a single `Link:` field carrying
142
+ two relations — `rel="service-desc"` (RFC 8631) first, then the door pointer
143
+ `rel="https://muretai.net/rel/agent-entry"` — so a crawler that landed on prose is
144
+ handed the machine-readable door, whatever its `User-Agent` claims. The body stays
145
+ byte-identical for every caller, and so does the header: classification feeds the
146
+ counters above and never a byte on the wire.
147
+
148
+ **Since 1.8.0 the watcher is told the same thing.** `entry.stats()` always counted family by
149
+ stage, but the per-visit row handed to your `observer` carried `ua_family` only on the card and
150
+ notice fetches — so you could see that a browser *read* your card and not that a browser was the
151
+ thing being *refused*. The two questions an operator actually has — is this a crawler, is
152
+ somebody's agent failing to sign — were answerable only for the visitors who never knocked. All
153
+ five stages now carry it, refusals included, so "which clients got in and which were turned away"
154
+ is one query instead of two half-answers. Nothing else moved: no wire byte, no verdict, no ledger
155
+ row, no rate lane, and `stats()` is unchanged.
145
156
 
146
157
  One rule holds this together, enforced by the contract suite rather than promised:
147
158
  **a User-Agent never affects `verified`, an account row, a rate limit, or any
@@ -694,6 +705,17 @@ npm test
694
705
 
695
706
  Write a third implementation and point it at the same vectors.
696
707
 
708
+ ## Contributing
709
+
710
+ This repo is a **published mirror**, rendered out of a private working repository — not the
711
+ place the next change is written. A pull request opened here will not merge: the next release
712
+ overwrites this checkout wholesale from the source, carrying no memory of a branch made
713
+ against it.
714
+
715
+ That is not a closed door. **Open an issue** — a bug, a wire-vector disagreement, a place the
716
+ docs are wrong, a design question — and it gets read and, where it's right, becomes the next
717
+ release here. That path works; a PR against these files does not.
718
+
697
719
  ## What this is part of
698
720
 
699
721
  [Muretai](https://muretai.com) is a network where AI agents that belong to *different
@@ -92,12 +92,20 @@ for (const v of vectors.envelope) {
92
92
  // ---------------------------------------------------------------- the refusals
93
93
  // The half that catches an implementation which verifies nothing.
94
94
  for (const v of vectors.reject.message) {
95
- const fields = { from: v.from, to: v.to, messageId: v.messageId,
96
- contextId: v.contextId ?? null, timestamp: v.timestamp,
97
- text: v.text, sig: v.sig };
95
+ // The case's message lives under `input`; `recipientDid` (when a case pins one, as
96
+ // `wrong-recipient` does) sits beside it at the top level. Reading the message from the top
97
+ // level instead built `{contextId: null}` with an undefined recipient, which every verifier
98
+ // refuses for being empty - so all six checks passed without ever exercising the attack they
99
+ // are named for. Proven by mutation: with the signature check neutered and field-presence
100
+ // left intact, this file still printed "every case that must be refused was". With the
101
+ // wiring correct the same mutant turns four checks red, `from-not-signer` among them.
102
+ const m = v.input ?? v;
103
+ const fields = { from: m.from, to: m.to, messageId: m.messageId,
104
+ contextId: m.contextId ?? null, timestamp: m.timestamp,
105
+ text: m.text, sig: m.sig };
98
106
  let accepted;
99
107
  try {
100
- accepted = verifyEnvelope(fields, { recipientDid: v.recipientDid ?? v.to });
108
+ accepted = verifyEnvelope(fields, { recipientDid: v.recipientDid ?? m.recipientDid ?? m.to });
101
109
  } catch {
102
110
  accepted = false; // refusing by throwing is still refusing
103
111
  }
@@ -336,10 +336,12 @@ export const UA_FAMILIES = [
336
336
  ['mozilla', 'browser'],
337
337
  ];
338
338
 
339
- /** The families that read as an AI agent the ones the notice route signposts with a
340
- * `Link` header. `muretai-node` is deliberately absent: its Outbox already walks the
341
- * well-known card paths, so a signpost buys it nothing. Must match the same set in
342
- * examples/agent_entry_reference.py. */
339
+ /** The families that read as an AI agent. Since v1.11 this set steers NO wire byte
340
+ * the notice route's `Link` signpost is emitted for every caller (see `steerHeaders`
341
+ * and ISSUE(agent-entry-unconditional-link)) so it remains only as a published
342
+ * classification an operator's own observation code can lean on. `muretai-node` is
343
+ * deliberately absent: its Outbox already walks the well-known card paths. Must match
344
+ * the same set in examples/agent_entry_reference.py. */
343
345
  export const AI_AGENT_FAMILIES = new Set([
344
346
  'claude-user', 'claudebot', 'gptbot', 'openai', 'perplexity', 'google-extended',
345
347
  ]);
@@ -1706,7 +1708,7 @@ function jsonResponse(status, obj, extraHeaders = {}) {
1706
1708
  };
1707
1709
  }
1708
1710
 
1709
- function rpcError(id, error, data) {
1711
+ function baseRpcError(id, error, data) {
1710
1712
  const err = { ...error };
1711
1713
  if (data) err.data = data;
1712
1714
  return jsonResponse(200, { jsonrpc: '2.0', id: id ?? null, error: err });
@@ -2252,6 +2254,12 @@ export function canonicalMount(canonUrl, basePath) {
2252
2254
  * callback, and a watcher that dialled out on the hot path would make the
2253
2255
  * visitor's answer depend on somebody else's uptime.
2254
2256
  *
2257
+ * WHAT IT IS TOLD. Every stage reports `stage`, `identified` and
2258
+ * `ua_family` — the door's own bounded classification of the client, one
2259
+ * of the fixed `UA_FAMILIES` names and NEVER a substring of what the caller
2260
+ * sent, so a stranger cannot write its own label into your metrics. The
2261
+ * POST stages add the envelope on top.
2262
+ *
2255
2263
  * WHAT NOT TO PUT IN IT. The envelope carries `peer_did`/`owner_did`,
2256
2264
  * which a visitor handed you to transact with YOU. Forwarding a raw DID to
2257
2265
  * a third party shares a durable identifier its owner never offered them;
@@ -2394,6 +2402,18 @@ export function createAgentEntry({
2394
2402
  },
2395
2403
  };
2396
2404
  card.security = [{ [SIGNED_ENVELOPE_SCHEME]: [] }];
2405
+ // WHERE A 1.0-NATIVE READER FINDS THE ENDPOINT. A2A 1.0 removed the top-level `url` /
2406
+ // `preferredTransport` pair in favour of `supportedInterfaces`, so a client written
2407
+ // against 1.0 greps for exactly this field and, without it, learns nothing from this
2408
+ // card about where to POST. `url` is doorUrl — the address message/send is actually
2409
+ // POSTed to, the same string the securitySchemes `exampleRequest`'s `endpoint` names —
2410
+ // and deliberately NOT canonUrl, which at a bare origin differs from the door by the
2411
+ // trailing slash (canonUrl's bytes are pinned and signed; this entry is what carries
2412
+ // the canonical POST form). Appended last so every field an already-deployed entry
2413
+ // publishes keeps its bytes AND its position.
2414
+ card.supportedInterfaces = [
2415
+ { url: doorUrl, protocolBinding: 'JSONRPC', protocolVersion: PROTOCOL_VERSION },
2416
+ ];
2397
2417
 
2398
2418
  const cardBytes = Buffer.from(JSON.stringify(card), 'utf8'); // identical bytes on both paths
2399
2419
  // ACCOUNT DID -> {first_seen, last_seen, messages}. Keyed by the RESOLVED account (T102):
@@ -2479,10 +2499,34 @@ export function createAgentEntry({
2479
2499
  // five stage names — an attacker choosing UA strings cannot grow it.
2480
2500
  const uaStats = new Map();
2481
2501
 
2502
+ /** Count the stage, and tell the watcher about it.
2503
+ *
2504
+ * Every stage a visitor can reach already passes through here — `card_get`, `notice_get`,
2505
+ * `anon_post`, `signed_post`, `refused_post` — so this is where the observer learns HOW FAR
2506
+ * somebody got. It could previously see only the conversations: a site shipping visits to
2507
+ * analytics got the answered messages and nothing else, no card fetch, no notice read, no
2508
+ * keyless walk-in. The interesting shape of agent traffic is exactly that drop-off — how
2509
+ * many looked, how many tried, how many got in.
2510
+ *
2511
+ * Reported from HERE and not from a second place, so the watcher and `entry.stats()` can
2512
+ * never disagree about what happened, and a stage added later is reported without anyone
2513
+ * remembering to. `identified` rides along because "no DID at all" and "had a DID and was
2514
+ * refused" are different answers for a site deciding whether to open the anonymous lane.
2515
+ *
2516
+ * A GET carries no envelope, so the watcher is told what is true and nothing invented: no
2517
+ * DIDs, no text, `verified: false`. The POST stages are handed to `respond()` /
2518
+ * `observeRefusal()` instead, which know the envelope — one visit, one row, never two.
2519
+ * `ua_family` is the one field BOTH sides report, so a watcher can ask "which clients got
2520
+ * in and which were turned away" as one question instead of two half-answers. */
2482
2521
  function tally(family, stage) {
2483
2522
  let row = uaStats.get(family);
2484
2523
  if (!row) { row = new Map(); uaStats.set(family, row); }
2485
2524
  row.set(stage, (row.get(stage) || 0) + 1);
2525
+ if (typeof observer !== 'function') return;
2526
+ if (stage === 'card_get' || stage === 'notice_get') {
2527
+ observe({ stage, identified: 0, verified: false, ua_family: family,
2528
+ peer_did: null, owner_did: null, wba_did: null, text: null });
2529
+ }
2486
2530
  }
2487
2531
 
2488
2532
  /** A plain JSON-able copy of the counters: { family: { stage: n } }. */
@@ -2495,25 +2539,32 @@ export function createAgentEntry({
2495
2539
  return out;
2496
2540
  }
2497
2541
 
2498
- /** The `Link` header the notice route carries. TWO relations with different audiences,
2499
- * in ONE header field (RFC 8288 allows several link-values in one field, and one field
2500
- * is what keeps the two twins' bytes identical through their single-header plumbing):
2542
+ /** The `Link` header the notice route carries the SAME one-field value for EVERY
2543
+ * caller. TWO relations in ONE header field (RFC 8288 allows several link-values in
2544
+ * one field, and one field is what keeps the two twins' bytes identical through their
2545
+ * single-header plumbing):
2501
2546
  *
2502
- * - the DOOR pointer, `rel="https://muretai.net/rel/agent-entry"`, for EVERY caller.
2503
- * This is the coexistence primitive (E4): an agent that fetched a page finds the
2547
+ * - `rel="service-desc"` (RFC 8631's registered relation for "service description …
2548
+ * primarily intended for consumption by machines"), FIRST. It was emitted only to
2549
+ * the UA families that read as an AI agent (the T119 signpost) until a
2550
+ * third-party scanner (agentcard.org, 2026-08-21) measured that conditioned
2551
+ * emission as invisible: its crawler is none of our families, so a working door
2552
+ * scored as publishing no service description at all — the revisit trigger
2553
+ * recorded in ISSUE(agent-entry-unconditional-link). A header conditioned on a
2554
+ * guess about the reader is invisible to exactly the readers the guess missed,
2555
+ * so it is now emitted for every caller.
2556
+ * - the DOOR pointer, `rel="https://muretai.net/rel/agent-entry"`. This is the
2557
+ * coexistence primitive (E4): an agent that fetched a page finds the
2504
2558
  * machine-readable door in the RESPONSE, with no HTML to parse and no prose to
2505
2559
  * read, and a browser ignores it — which is what lets a site keep its own front
2506
2560
  * page and add ONE header instead of migrating. An absolute URI because RFC 8288
2507
2561
  * §2.1.2 permits a bare token only for an IANA-registered relation.
2508
- * - `rel="service-desc"` (RFC 8631's registered relation for "service description …
2509
- * primarily intended for consumption by machines"), FIRST and only for the UA
2510
- * families that read as an AI agent — the T119 signpost, unchanged in meaning.
2511
2562
  *
2512
- * HEADER-ONLY on purpose: the notice BODY is byte-identical for every caller, so what
2513
- * the UA changes is still only this one additive relation and never a verdict. */
2514
- function steerHeaders(family) {
2563
+ * HEADER-ONLY on purpose: the notice BODY is byte-identical for every caller, and now
2564
+ * so is this header the UA family still steers observation (`tally`/`stats`), never
2565
+ * a byte on the wire. */
2566
+ function steerHeaders(family) { // `family` kept for observation symmetry, unread here
2515
2567
  const door = `<${mount}${AGENT_CARD_PATH}>; rel="${AGENT_ENTRY_REL}"`;
2516
- if (!AI_AGENT_FAMILIES.has(family)) return { Link: door };
2517
2568
  return { Link: `<${mount}${AGENT_CARD_PATH}>; rel="service-desc", ${door}` };
2518
2569
  }
2519
2570
 
@@ -2718,7 +2769,97 @@ export function createAgentEntry({
2718
2769
  * size checks come BEFORE any parsing or crypto — a check placed after the signature is
2719
2770
  * a check the attacker simply skips.
2720
2771
  */
2772
+ /** The code of the refusal this request produced, or null when it was answered. Set by the
2773
+ * entry-local `rpcError` below so the funnel can report it without re-parsing a Buffer. */
2774
+ let lastRefusal = null;
2775
+
2776
+ /** The stage `tally()` computed for the POST currently in flight, handed to whichever
2777
+ * observation point reports it. `tally()` runs AFTER the ladder returns, so the answered
2778
+ * case is observed before this is set — which is why `respond()` reads it lazily rather
2779
+ * than being passed it. */
2780
+ let pendingStage = null;
2781
+
2782
+ /** The client family for the POST currently in flight, handed to the same observation
2783
+ * points as `pendingStage` and for the same reason. The GET stages already carry
2784
+ * `ua_family` — it is what `tally()` counts under — and a KNOCK, the stage where "who is
2785
+ * this client" matters most, was the one arriving without it. A watcher could see that a
2786
+ * browser fetched the card and NOT that a browser was the thing being refused, which
2787
+ * leaves the two questions an operator actually has (is this a crawler? is somebody's
2788
+ * agent failing to sign?) answerable only for the visitors who did not try.
2789
+ *
2790
+ * Derived from the same request `route()` derived its own `family` from, and `uaFamily` is
2791
+ * a pure function of that one header, so the two cannot disagree. Recomputed rather than
2792
+ * threaded through a signature every call site would have to remember to pass — the same
2793
+ * argument `rpcError` makes for shadowing itself a few lines below. Set and read together
2794
+ * with `pendingStage`, so it inherits exactly that field's accepted skew under an async
2795
+ * responder and can never disagree with the stage it is reported beside. */
2796
+ let pendingFamily = 'none';
2797
+
2798
+ /** Shadows the module-level `rpcError` for the whole entry: same return value, and it
2799
+ * remembers the code on the way out. A local alias rather than seventeen edits, and rather
2800
+ * than a parameter every refusal site would have to remember to pass. */
2801
+ const rpcError = (id, error, data) => {
2802
+ lastRefusal = error && typeof error.code === 'number' ? error.code : null;
2803
+ return baseRpcError(id, error, data);
2804
+ };
2805
+
2806
+ /** Every POST outcome, observed once, at the single point they all funnel through.
2807
+ *
2808
+ * `respond()` observes the answered case with the full envelope. Everything else — the
2809
+ * keyless walk-in, the bad signature, the flood that hit a ceiling — returned an rpcError
2810
+ * and was never seen at all, so a door could count who it TALKED to and never who it TURNED
2811
+ * AWAY. For a site asking whether agents are arriving, the refusals are the signal: an agent
2812
+ * that could not get in is the one nobody hears from again. It also made the documented
2813
+ * contract false, since the guide says the observer runs once per message, after the verdict,
2814
+ * and a refusal IS a verdict.
2815
+ *
2816
+ * Structural rather than enumerated ON PURPOSE. Seventeen refusal sites would have been
2817
+ * seventeen chances to forget, and the eighteenth would be forgotten by construction.
2818
+ *
2819
+ * The code is read from `lastRefusal`, set by `rpcError` on its way out, rather than by
2820
+ * re-parsing the response — the body is already a Buffer by then, and a watcher must never
2821
+ * cost a JSON round-trip on the reply path.
2822
+ *
2823
+ * A refusal hands the watcher only what was actually established: `refused` carries the
2824
+ * JSON-RPC code and the DIDs are null, because a walk-in that named nobody named nobody. The
2825
+ * watcher still cannot matter — same swallowed throw, same discarded return. */
2721
2826
  function handlePost(rawBody, reqHeaders) {
2827
+ lastRefusal = null;
2828
+ // The stage a POST reaches is decided by whether it CARRIED a signature, which is knowable
2829
+ // from the request alone — so it is settled here, before the ladder answers, and read by
2830
+ // whichever observation point fires. `tally()` computes the same thing afterwards from the
2831
+ // finished reply, for the counters; the two agree because they ask the same question.
2832
+ pendingStage = postRequestStage(rawBody);
2833
+ pendingFamily = uaFamily(uaOf(reqHeaders));
2834
+ const out = handlePostLadder(rawBody, reqHeaders);
2835
+ if (isThenable(out)) return out.then((o) => { observeRefusal(); return o; });
2836
+ observeRefusal();
2837
+ return out;
2838
+ }
2839
+
2840
+ /** The answered case is already observed inside `respond()`; this adds refusals only, so no
2841
+ * message is ever observed twice. */
2842
+ function observeRefusal() {
2843
+ if (typeof observer !== 'function' || lastRefusal === null) return;
2844
+ observe({ verified: false, refused: lastRefusal, stage: 'refused_post',
2845
+ identified: pendingStage === 'signed_post' ? 1 : 0, ua_family: pendingFamily,
2846
+ peer_did: null, owner_did: null, wba_did: null, text: null });
2847
+ }
2848
+
2849
+ /** Did this POST body carry a signature? That is the whole difference between a keyless
2850
+ * walk-in and an identified visitor, and it is answerable from the request without waiting
2851
+ * for the verdict — a bad signature is still a visitor who HAD a key. Parsed defensively:
2852
+ * anything unreadable is a walk-in, because it certainly did not present an identity. */
2853
+ function postRequestStage(rawBody) {
2854
+ try {
2855
+ const buf = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody || '');
2856
+ const req = JSON.parse(buf.toString('utf8'));
2857
+ const meta = req?.params?.message?.metadata;
2858
+ return (meta && typeof meta.sig === 'string' && meta.sig) ? 'signed_post' : 'anon_post';
2859
+ } catch { return 'anon_post'; }
2860
+ }
2861
+
2862
+ function handlePostLadder(rawBody, reqHeaders) {
2722
2863
  // RAW BYTES, always. A host app that hands us a decoded string has already destroyed
2723
2864
  // the evidence the strict decode below exists to find, so normalise once and measure
2724
2865
  // the SIZE in bytes rather than in UTF-16 code units.
@@ -2871,18 +3012,26 @@ export function createAgentEntry({
2871
3012
  if (!Number.isSafeInteger(ts) || Math.abs(nowEpoch() - ts) > CLOCK_WINDOW_S) {
2872
3013
  return rpcError(reqId, ERRORS.REPLAY_REJECTED, 'timestamp out of range (clock skew or replay)');
2873
3014
  }
2874
- // 7. duplicate messageId inside the replay window. (Its type was settled by the shape
2875
- // gate: a non-string messageId never reaches here on either implementation.)
3015
+ // 7. the signature itself, under the key DERIVED FROM `from`. (`messageId`'s type was
3016
+ // settled by the shape gate: a non-string never reaches here on either implementation.)
2876
3017
  const messageId = msg.messageId;
2877
- if (!replay.checkAndRemember(messageId)) {
2878
- return rpcError(reqId, ERRORS.REPLAY_REJECTED, 'duplicate messageId (replay) detected');
2879
- }
2880
- // 8. the signature itself, under the key DERIVED FROM `from`.
2881
3018
  const fields = { from, to, messageId, contextId: msg.contextId ?? null,
2882
3019
  timestamp: ts, text, sig };
2883
3020
  if (!verifyEnvelope(fields, { recipientDid: did })) {
2884
3021
  return rpcError(reqId, ERRORS.UNAUTHENTICATED, 'signature does not match');
2885
3022
  }
3023
+ // 8. duplicate messageId inside the replay window — AFTER the verify, and the ORDER is the
3024
+ // property. The table is state an authenticated sender depends on, so a caller who has
3025
+ // proved nothing must not write into it. Ahead of the verify a stranger could BURN an id
3026
+ // its real sender was about to use, and because the table is capped and evicts
3027
+ // oldest-first, flood past the cap to discard genuine entries and re-open real messages
3028
+ // to replay — invalid signatures are not rate-limited, so that flood is free. This is the
3029
+ // rule the signed lane's ceiling already follows one step below, for the same reason; it
3030
+ // was simply never applied here. The cost of the swap is one Ed25519 verify spent on a
3031
+ // replayed VALID message, which an attacker must first have obtained.
3032
+ if (!replay.checkAndRemember(messageId)) {
3033
+ return rpcError(reqId, ERRORS.REPLAY_REJECTED, 'duplicate messageId (replay) detected');
3034
+ }
2886
3035
 
2887
3036
  // 9. T102 account layer. An OPTIONAL countersigned v2 binding collapses an owner's device
2888
3037
  // DIDs to ONE account; a present-but-INVALID binding fails closed with the SAME
@@ -2936,18 +3085,22 @@ export function createAgentEntry({
2936
3085
  }
2937
3086
 
2938
3087
  function respond(env, reqId, msg, toDid) {
2939
- observe(env);
3088
+ // The answered case: the envelope already says who this was, and the stage says how they
3089
+ // arrived. `identified` is read off the envelope rather than the stage, because the
3090
+ // anonymous lane answers a visitor who genuinely presented no DID.
3091
+ observe({ ...env, stage: pendingStage, ua_family: pendingFamily,
3092
+ identified: env && env.peer_did ? 1 : 0 });
2940
3093
  let answer;
2941
3094
  try {
2942
3095
  answer = responder(env);
2943
3096
  } catch (e) {
2944
- return rpcError(reqId, ERRORS.INTERNAL_ERROR, `responder failed: ${e && e.message}`);
3097
+ return rpcError(reqId, ERRORS.INTERNAL_ERROR, 'the site backend failed to answer');
2945
3098
  }
2946
3099
  const inbound = { contextId: msg.contextId ?? null, messageId: msg.messageId ?? null };
2947
3100
  if (isThenable(answer)) {
2948
3101
  return answer.then(
2949
3102
  (v) => finishReply(reqId, v, { inbound, toDid }),
2950
- (e) => rpcError(reqId, ERRORS.INTERNAL_ERROR, `responder failed: ${e && e.message}`));
3103
+ (e) => rpcError(reqId, ERRORS.INTERNAL_ERROR, 'the site backend failed to answer'));
2951
3104
  }
2952
3105
  return finishReply(reqId, answer, { inbound, toDid });
2953
3106
  }
@@ -3055,8 +3208,8 @@ export function createAgentEntry({
3055
3208
  return { status: 200,
3056
3209
  headers: { 'Content-Type': 'text/plain; charset=utf-8',
3057
3210
  'Content-Length': String(body.length),
3058
- // The ONE wire-visible thing observation adds: an AI-agent UA is pointed at
3059
- // the machine-readable door. The body above is byte-identical either way.
3211
+ // Every caller is pointed at the machine-readable door the same one-field
3212
+ // Link value for all of them (v1.11). The body above is byte-identical too.
3060
3213
  ...steerHeaders(family) },
3061
3214
  body };
3062
3215
  }
@@ -3180,7 +3333,7 @@ export function createAgentEntry({
3180
3333
  const body = oversize ? oversizeSentinel() : Buffer.concat(chunks, total);
3181
3334
  Promise.resolve()
3182
3335
  .then(() => handleRequestAsync(req.method, req.url, req.headers, body))
3183
- .catch((e) => rpcError(null, ERRORS.INTERNAL_ERROR, String(e && e.message)))
3336
+ .catch(() => rpcError(null, ERRORS.INTERNAL_ERROR, 'the entry failed to answer'))
3184
3337
  .then(({ status, headers, body: out }) => {
3185
3338
  res.writeHead(status, headers);
3186
3339
  res.end(req.method === 'HEAD' ? undefined : out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muretai/agent-entry",
3
- "version": "1.6.3",
3
+ "version": "1.8.0",
4
4
  "description": "Make your website answer AI agents: an A2A agent endpoint that verifies who is knocking, opens an account for them and replies signed, in one HTTP round trip. Zero dependencies. Pairs with llms.txt and WebMCP.",
5
5
  "type": "module",
6
6
  "main": "muretai-agent-entry.mjs",
@@ -12,6 +12,7 @@
12
12
  "conformance/",
13
13
  "examples/server.mjs",
14
14
  "LICENSE",
15
+ "spec/",
15
16
  "README.md"
16
17
  ],
17
18
  "engines": {
package/spec/v1.md ADDED
@@ -0,0 +1,648 @@
1
+ # Agent Entry v1
2
+
3
+ **An HTTP endpoint that lets a website recognise an AI agent it has never met, and answer it
4
+ in the same request.**
5
+
6
+ Status: **Draft.** Version 1. This document describes behaviour that is deployed and running.
7
+ Editor: Muretai. Feedback: <https://github.com/muretai/agent-entry/issues>.
8
+ Licence: this specification is published under the same MIT licence as the reference
9
+ implementation.
10
+
11
+ ---
12
+
13
+ ## 1. What this specifies, and the one rule that shaped it
14
+
15
+ An **Agent Entry** is a small HTTP surface a website serves so that an autonomous agent —
16
+ one with no account on that site, no API key, no prior relationship and nobody at a keyboard —
17
+ can identify itself, be recognised, and get a useful answer without a signup step. The
18
+ visitor's key *is* its identity, so *sign up* and *log in* are the same event and there is no
19
+ password to leak.
20
+
21
+ **Every normative requirement in §4 of this document is decided by what a stranger can observe
22
+ over HTTP.** Not by reading the server's source, not by trusting its operator, and not by
23
+ asking it. Each `MUST` and `SHOULD` below is stated together with the exact request that tests
24
+ it and the exact response that satisfies it. §9 collects all of them in one table.
25
+
26
+ This rule cost the specification real content, and that is the point. A requirement about what
27
+ a server *stores* — an account row, a rate-limiter bucket, a log — cannot be checked by the
28
+ party being asked to rely on it, so it is not a `MUST` here. Those behaviours are real and the
29
+ reference implementation has them; §7 states them plainly as what they are, which is
30
+ operator-verifiable, and marks them non-normative. A specification that asks a third party to
31
+ trust an unobservable claim has not specified anything; it has made a promise on somebody
32
+ else's behalf.
33
+
34
+ Two consequences worth stating up front:
35
+
36
+ - **An implementation is conformant if a checker says so, not if the author says so.** §8 names
37
+ two independent oracles and neither is this document.
38
+ - **A conformant Agent Entry can be written from scratch from this document alone**, in any
39
+ language, without reading the reference implementation. If you find a place where it cannot,
40
+ that is a defect in this document — please report it.
41
+
42
+ ### 1.1 What an Agent Entry is not
43
+
44
+ It is not an authorization server, and it issues no tokens. It has no registration endpoint,
45
+ because there is nothing to register: a visitor arrives already holding the only credential
46
+ that matters. It is not a bot-detection or access-control product — it recognises whoever
47
+ signs, and what a site chooses to do with that recognition is the site's business. And it is
48
+ not a replacement for `llms.txt` or for an agent-facing sitemap: those describe a site to an
49
+ agent; this one *recognises* one. A site may serve all of them.
50
+
51
+ ---
52
+
53
+ ## 2. Terminology
54
+
55
+ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**,
56
+ **SHOULD NOT**, **RECOMMENDED**, **MAY** and **OPTIONAL** are to be interpreted as described in
57
+ BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.
58
+
59
+ **Entry** — the HTTP surface specified here.
60
+ **Visitor** — the agent dialling it. A visitor holds an Ed25519 keypair and nothing else.
61
+ **Mount** — the path component of the entry's own `baseUrl`; empty for a bare origin, which is
62
+ the ordinary case. Where this document writes `<mount>/.well-known/agent-card.json`, a
63
+ bare-origin entry serves literally `/.well-known/agent-card.json`.
64
+ **DID** — a `did:key` identifier: `did:key:z` followed by base58btc of `0xed01` concatenated
65
+ with a 32-byte Ed25519 public key. The identifier *is* the public key, so resolving one
66
+ requires no network call, no directory and no registry.
67
+ **Envelope** — the six signed fields carried in an A2A message's `metadata`.
68
+
69
+ ---
70
+
71
+ ## 3. Protocol context
72
+
73
+ An Agent Entry speaks **A2A** on the wire: the discovery document is an A2A Agent Card at the
74
+ well-known path fixed by RFC 8615, and the message endpoint is JSON-RPC 2.0 with the
75
+ `message/send` method. An entry adds no new method and changes the meaning of no existing
76
+ field; everything specific to this specification travels in the card's extension objects and
77
+ in `metadata`. An A2A client that knows nothing about this document can read the card and is
78
+ unaffected by the parts it does not recognise.
79
+
80
+ Transport is HTTPS. An entry MAY be served over plain HTTP for local development only; nothing
81
+ in this document makes plaintext safe, and the signature layer protects integrity of the
82
+ *payload*, not confidentiality of anything.
83
+
84
+ ---
85
+
86
+ ## 4. Normative requirements
87
+
88
+ Each requirement below carries an **Observation**: the request that decides it and the response
89
+ that satisfies it. Where an observation needs a key, the verifier mints a fresh Ed25519 keypair
90
+ — which requires no permission from, and no coordination with, the entry being tested.
91
+
92
+ Throughout, `<E>` is the entry's own DID as claimed by its card, and `now` is the verifier's
93
+ clock at the time of the request.
94
+
95
+ ### 4.1 Discovery
96
+
97
+ **AE-1.** An entry **MUST** serve its Agent Card at `<mount>/.well-known/agent-card.json` with
98
+ status `200` and a JSON body.
99
+
100
+ > **Observation.** `GET <mount>/.well-known/agent-card.json` → `200`, body parses as a JSON
101
+ > object.
102
+
103
+ **AE-2.** An entry **MUST** serve byte-identical content at the legacy path
104
+ `<mount>/.well-known/agent.json`.
105
+
106
+ > **Observation.** `GET` both paths; the response bodies are equal byte for byte. The legacy
107
+ > path exists so that a client written against the earlier A2A convention is not broken; it is
108
+ > an alias, not a second document, and any difference between the two is a defect.
109
+
110
+ **AE-3.** An entry **MUST** serve a signed card envelope at
111
+ `<mount>/.well-known/agent-card.sig.json` with status `200` and a JSON body.
112
+
113
+ > **Observation.** `GET <mount>/.well-known/agent-card.sig.json` → `200`, body parses as a JSON
114
+ > object carrying at least `v`, `typ`, `card`, `ts` and `sig`.
115
+
116
+ **AE-4.** An entry **MUST NOT** serve any address other than those its card names. Every other
117
+ path **MUST** answer `404` for every method, `OPTIONS` included.
118
+
119
+ > **Observation.** `GET`, `POST` and `OPTIONS` on a path the card does not name → `404` in all
120
+ > three cases. An entry answers exactly the addresses it advertises.
121
+
122
+ ### 4.2 The card
123
+
124
+ **AE-5.** The card **MUST** carry the A2A fields `protocolVersion`, `name`, `description`,
125
+ `url`, `version`, `capabilities`, `defaultInputModes`, `defaultOutputModes` and `skills`, and
126
+ **MUST** carry a `did` naming the entry's own DID.
127
+
128
+ > **Observation.** Parse the card from AE-1; assert each field is present and `did` matches
129
+ > `did:key:z…` with a decodable `0xed01` prefix and a 32-byte key.
130
+
131
+ **AE-6.** `card.url` **MUST** name the same canonical scope as the address the visitor dialled:
132
+ the same origin (scheme, host, and port when non-default, compared lowercased) **and** the same
133
+ path prefix, with trailing slashes normalised.
134
+
135
+ > **Observation.** Compare `card.url` against the dialled URL. This is the requirement that
136
+ > separates a genuine card from a byte-perfect copy of somebody else's: every signature and
137
+ > every DID check passes on a stolen card, and only the signed `url` tells the two apart. The
138
+ > path half matters because nothing requires an entry to sit at a bare origin — on a host
139
+ > routing `/alice` and `/mallory` to different entries, an origin-only comparison accepts the
140
+ > copy. An empty or unparseable `url` on either side is never a match.
141
+
142
+ **AE-7.** An entry that answers visitors with no prior introduction **MUST** advertise that
143
+ fact as `agentEntry.open_door: true`, and **MUST** emit a byte-identical alias at
144
+ `muretai.open_door`. A consumer **MUST** accept either and **SHOULD** prefer `agentEntry`.
145
+
146
+ > **Observation.** Parse the card; assert `agentEntry.open_door === muretai.open_door`. The
147
+ > alias exists because a card already pasted into a deployment cannot be reached by any change
148
+ > to this document; both are built from one object, which is why they cannot drift. New
149
+ > implementations write the neutral name and read either.
150
+
151
+ **AE-8.** An entry **MUST** state its terms on the card, before anyone knocks, using A2A
152
+ `securitySchemes` and `security`. The scheme object **MUST** carry at least `recipient` (the
153
+ entry's DID), `signedFields`, `canonicalization`, `signature`, `timestamp`, `in` and
154
+ `exampleRequest`.
155
+
156
+ > **Observation.** Parse the card; assert the fields are present, that `recipient` equals
157
+ > `card.did`, and that `signedFields` is exactly the six names in §4.5.
158
+ >
159
+ > **Why this is a `MUST` and not documentation.** A card that advertises a skill and says
160
+ > nothing about how to call it forces every visitor to learn the requirement by being refused.
161
+ > The card is the one place a protocol can state its terms *before* the failure — an
162
+ > HTTP-402-style challenge structurally cannot. `exampleRequest` is a nested JSON object and
163
+ > **MUST NOT** be a string containing JSON: an escaped document has to be unescaped before it
164
+ > can be copied, and the visitors that most need it are reading the raw response.
165
+
166
+ **AE-9.** If the scheme object carries `howTo`, the URL it names **MUST** resolve. An entry
167
+ that has no such page **MUST** omit the key entirely rather than emit an unresolvable URL.
168
+
169
+ > **Observation.** If `howTo` is present, `GET` it → not `404`. This is a `MUST` because a
170
+ > dangling pointer out-competes the data beside it: a visitor holding a complete, sufficient
171
+ > instruction object will follow a broken link and stop there. Nothing a signer needs may live
172
+ > only behind that link.
173
+
174
+ **AE-10.** An entry **MUST NOT** list a domain in `card.domains` that does not corroborate the
175
+ claim. A verifier **MUST NOT** treat such a claim as established on the card alone: the domain
176
+ must serve a DIF Well-Known DID Configuration at `/.well-known/did-configuration.json`
177
+ containing a Domain Linkage Credential that verifies under `card.did` and carries a future
178
+ expiry.
179
+
180
+ > **Observation.** For each `d` in `card.domains`: `GET https://<d>/.well-known/did-configuration.json`
181
+ > → `200`; at least one credential verifies under `card.did`; its `exp` is in the future.
182
+ >
183
+ > Note the direction, because it is easy to get backwards and the entry does not serve this
184
+ > document. `domains` is a claim made *by* the entry and corroborated *by the domain*, whose
185
+ > operator publishes the DID Configuration at its own root — which is precisely what makes it
186
+ > evidence rather than a second self-assertion. Both halves are required: a claim the domain
187
+ > does not corroborate is worse than no claim, because it reads as proof to anyone who checks
188
+ > only the card. The expiry is required because a domain is leased, not owned: a credential with
189
+ > no expiry keeps asserting a binding after the registration lapses, with no revocation channel
190
+ > a third-party verifier is obliged to consult.
191
+
192
+ ### 4.3 The signed card envelope
193
+
194
+ **AE-11.** The envelope's `sig` **MUST** verify under the DID that `envelope.card.did` names,
195
+ over the canonical bytes of the envelope's signed fields.
196
+
197
+ > **Observation.** Verify the signature against `envelope.card.did` using Ed25519. A plain
198
+ > Agent Card is a self-assertion — anyone can serve a card claiming anyone's DID — so this
199
+ > envelope is the only thing that makes a `did` → `url` binding evidence rather than a claim.
200
+
201
+ **AE-12.** `envelope.ts` **MUST** be a JSON integer, and **MUST** be within **6 hours** of
202
+ `now`, in both directions.
203
+
204
+ > **Observation.** Assert `Number.isInteger(ts)` and `|now - ts| ≤ 21600`.
205
+ >
206
+ > Two independent reasons this is a `MUST`. A float `ts` is not reproducible across runtimes —
207
+ > a number only one language serialises byte-for-byte is unverifiable everywhere else. And the
208
+ > path is unauthenticated, so without a window the envelope is a harvestable bearer proof that
209
+ > anyone who later takes over the address can replay. The symmetry matters: a `ts` far in the
210
+ > future is refused too.
211
+
212
+ **AE-13.** An entry **MUST NOT** sign a fresh envelope per request. It **SHOULD** re-sign at
213
+ most hourly and serve a cached artifact.
214
+
215
+ > **Observation.** Fetch the envelope twice in quick succession; `ts` and `sig` are unchanged.
216
+ > An unauthenticated path that signs on demand is a signing oracle, and any stranger can drive
217
+ > it.
218
+
219
+ ### 4.4 Methods, `Allow`, and the difference between 404 and 405
220
+
221
+ **AE-14.** `OPTIONS` on any address the card names **MUST** answer `204` with an `Allow` header
222
+ describing **that resource**, plus CORS preflight headers.
223
+
224
+ > **Observation.** `OPTIONS` each advertised address → `204` with `Allow` present. Per RFC 9110
225
+ > §10.2.1, `Allow` is a statement about the target, so the value differs per resource:
226
+ > `GET, HEAD, OPTIONS` on the card paths; `GET, HEAD, POST, OPTIONS` on a mount that is both
227
+ > the notice and the door; `POST, OPTIONS` on a guest mount's door.
228
+
229
+ **AE-15.** A method not allowed on an **advertised** address **MUST** answer `405` carrying the
230
+ same `Allow` header, and **MUST NOT** answer `404`.
231
+
232
+ > **Observation.** `GET` a guest mount's door → `405` with `Allow: POST, OPTIONS`. The address
233
+ > is signed into a public card, so hiding it conceals nothing; RFC 9110 §15.5.6 requires the
234
+ > header.
235
+
236
+ **AE-16.** A `POST` to a path the card does **not** name **MUST** answer `404`, not `405`.
237
+
238
+ > **Observation.** `POST` to an unadvertised path → `404`. This is deliberate non-disclosure and
239
+ > is not in tension with AE-15: what separates the two is where the address came from, not the
240
+ > verb. An entry mounted beside other agents on one host must not confirm the existence of a
241
+ > door at a **guessed** address.
242
+
243
+ **AE-17.** A request whose HTTP request-target is in absolute form (carrying a scheme or an
244
+ authority) **MUST** answer `404`, and the body **SHOULD** carry a `detail` naming the rule.
245
+
246
+ > **Observation.** Send `POST http://elsewhere.example/x HTTP/1.1` → `404`.
247
+ >
248
+ > **Disclosed non-conformance.** RFC 9112 §3.2.2 says a server MUST accept absolute form. An
249
+ > Agent Entry deliberately does not, because it answers exactly the address its card names and
250
+ > that address has no second spelling. This is stated here rather than left to be discovered:
251
+ > an undisclosed deliberate deviation costs an integrator an afternoon, which is why the body
252
+ > carries a diagnostic instead of a bare `not found`.
253
+
254
+ ### 4.5 The message endpoint
255
+
256
+ The request body is a JSON-RPC 2.0 call to `message/send` carrying an A2A `Message`. The signed
257
+ payload is the canonical JSON of exactly six fields —
258
+ `contextId`, `from`, `messageId`, `text`, `timestamp`, `to` — with keys sorted by Unicode code
259
+ point, separators `,` and `:`, no whitespace, non-ASCII emitted literally, encoded UTF-8.
260
+ `contextId` is JSON `null` when there is no conversation yet; it is still one of the six and is
261
+ still signed. `sig` is base64 (standard alphabet, padded) of the 64-byte Ed25519 signature.
262
+
263
+ **AE-18.** An entry **MUST** apply the checks below **in this order**, and **MUST** answer with
264
+ the stated code. Order is normative: a later check must not be reachable when an earlier one
265
+ fails.
266
+
267
+ | # | Check | Failure |
268
+ |---|---|---|
269
+ | 1 | body ≤ 1 MiB | HTTP `413`, body not parsed |
270
+ | 2 | body parses as a JSON object | HTTP `400` |
271
+ | 3 | `method` is exactly the string `message/send` | `-32601` |
272
+ | 4 | `params` and `params.message` are JSON objects (an array is not one) | `-32602` |
273
+ | 5 | wire shape: `kind` is exactly `"message"`; text parts, `messageId` and `contextId` are strings (or `contextId` null); `metadata`, when present, is an object; `metadata.from`/`to`/`sig`, when present and non-null, are strings; no string carries a lone surrogate | `-32600` |
274
+ | 6 | `text` ≤ 65536 UTF-8 bytes — **before any cryptography** | `-32005` |
275
+ | 7 | `metadata.from`, `to` and `sig` are present (`null` reads as absent) | `-32001` |
276
+ | 8 | `metadata.to` equals `<E>` | `-32003` |
277
+ | 9 | `timestamp` is an integer within ±300 s of the entry's clock | `-32002` |
278
+ | 10 | the signature verifies under `from` | `-32001` |
279
+ | 11 | `messageId` not seen in the last 600 s | `-32002` |
280
+
281
+ > **Observation.** Eleven requests, each violating exactly one row with every earlier row
282
+ > satisfied; assert the code.
283
+ >
284
+ > **Two positions in this ladder are security properties, not preferences.**
285
+ >
286
+ > *Row 6 precedes any cryptography*, which is why an oversized message costs the entry nothing:
287
+ > a size check placed after the signature is a check an attacker simply skips, and the
288
+ > difference between a size limit and a denial-of-service amplifier is exactly this ordering.
289
+ >
290
+ > *Row 11 follows row 10*, and the reason is not obvious enough to leave unstated. Ahead of the
291
+ > verify, an unauthenticated stranger could **burn a `messageId` its real sender was about to
292
+ > use** — and because a replay table is necessarily capped and evicts oldest-first, that
293
+ > stranger could flood past the cap to discard genuine entries and re-open real messages to
294
+ > replay. Invalid signatures are not rate-limited, so that flood is free. Deduplicating before
295
+ > authenticating turns a replay defence into a denial-of-service primitive aimed at the very
296
+ > senders it protects.
297
+ >
298
+ > An implementation MAY deduplicate an **unsigned** request earlier (AE-29): there is no
299
+ > signature to wait for, and nothing an attacker can burn on another party's behalf.
300
+
301
+ **AE-19.** Every protocol verdict **MUST** be HTTP `200` carrying a JSON-RPC error object. Only
302
+ `413` and `400` are non-`200`.
303
+
304
+ > **Observation.** Each failing request from AE-18 rows 3-11 → HTTP `200` with `error.code` set.
305
+
306
+ **AE-20.** An entry **MUST** answer a validly signed `message/send` from a DID it has never seen
307
+ before, in the same HTTP response, with a reply signed by `<E>`. There **MUST NOT** be any
308
+ registration, enrolment or approval step between a first contact and an answer.
309
+
310
+ > **Observation.** Mint a fresh keypair; send one signed `message/send`; the response is HTTP
311
+ > `200`, carries a message whose `metadata.from` is `<E>`, whose signature verifies under
312
+ > `<E>`, which echoes the request's `contextId`, carries a fresh `messageId`, an integer
313
+ > `timestamp` within ±300 s, and a `replyTo` naming the request's `messageId`.
314
+ >
315
+ > **This is the requirement the whole specification exists for**, and it is fully
316
+ > stranger-verifiable: the verifier holds a key nobody has ever seen, performs no setup, and is
317
+ > answered.
318
+
319
+ **AE-21.** An entry **MUST NOT** echo caller-supplied text in an error response, and **MUST NOT**
320
+ return an exception string, stack trace, field path or internal value to an unauthenticated
321
+ caller.
322
+
323
+ > **Observation.** Send a malformed request carrying a distinctive marker string; assert the
324
+ > marker does not appear in the response, and that no response body carries a stack trace. A
325
+ > caller that proved nothing is owed a verdict, not a diagnosis of the server.
326
+
327
+ **AE-22.** An entry **MUST** answer every request with an HTTP response. An unhandled internal
328
+ condition **MUST** produce `-32603`, never a closed socket.
329
+
330
+ > **Observation.** A closed connection with no status line is not a verdict; to the sender it is
331
+ > indistinguishable from a network fault, which is the one outcome a signed protocol cannot
332
+ > diagnose.
333
+
334
+ **AE-23.** The JSON-RPC `id` **MUST** be echoed when it is a String, a Number or Null, and
335
+ **MUST** be answered under `null` otherwise (an object, an array, a string carrying a lone
336
+ surrogate, or a number outside ±2^53).
337
+
338
+ > **Observation.** Send each shape; assert the echoed `id`. The `id` is the one field no
339
+ > signature covers and it is written straight back out, so a value two runtimes serialise
340
+ > differently turns one verdict into two different responses.
341
+
342
+ ### 4.6 The refusal that teaches
343
+
344
+ **AE-24.** When `metadata.from`, `to` and `sig` are **all** absent (AE-18 row 7), the `-32001`
345
+ error **MUST** carry `data.accepts`: a JSON **array** whose first element is the same scheme
346
+ object the card publishes under `agentEntry` (AE-8), verbatim.
347
+
348
+ > **Observation.** `POST` a well-formed `message/send` with no `metadata`; assert `error.code`
349
+ > is `-32001`, that `data.accepts` is an array, and that `data.accepts[0]` deep-equals the
350
+ > card's `agentEntry` scheme object. One object, two surfaces — the menu and the door can never
351
+ > advertise two different requirements.
352
+ >
353
+ > **The bar this is written to is behavioural, not informational:** *an agent holding only this
354
+ > refusal, plus ordinary crypto tooling, can mint a `did:key`, sign correctly, and be answered
355
+ > on its next POST.* That is why `recipient` is in the block — nobody can address a message
356
+ > without it — and why the canonicalization rule is spelled out rather than named. It is an
357
+ > array because "sign", "arrive with an introduction" and later "pay" are siblings in one
358
+ > frame, not three bespoke refusals.
359
+
360
+ **AE-25.** The refusal **MUST** remain a complete recipe with every URL removed from it.
361
+
362
+ > **Observation.** Strip every URL-valued field from `data.accepts[0]`; what remains still names
363
+ > the identifier derivation, the six signed fields, the canonicalization, the signature
364
+ > encoding, the timestamp rule and the recipient. A visitor must never depend on fetching a
365
+ > second document to answer the first.
366
+
367
+ **AE-26.** A **partial** envelope — for example `from` and `to` present with `sig` stripped —
368
+ **MUST NOT** receive `data.accepts`.
369
+
370
+ > **Observation.** `POST` with `from` and `to` but no `sig`; assert `-32001` with no `accepts`
371
+ > array. Whoever sent that already holds a key and already knows the shape, so it is a
372
+ > downgrade attempt and not a walk-in; there is no reason to hand a prober a machine-readable
373
+ > map of what to try next. A wrongly-*typed* field is row 5 (`-32600`), not row 7.
374
+
375
+ ### 4.7 Limits
376
+
377
+ **AE-27.** An entry **MUST** cap `text` at 65536 UTF-8 bytes and the request body at 1 MiB.
378
+
379
+ > **Observation.** AE-18 rows 1 and 6.
380
+
381
+ **AE-28.** An entry **MUST** bound the rate at which it produces signed replies, in aggregate,
382
+ and **MUST** refuse over-rate requests with `-32004` rather than by silence or by disconnection.
383
+
384
+ > **Observation.** Drive the entry above its advertised ceiling from one signed identity and
385
+ > assert the refusal carries `-32004` at HTTP `200`.
386
+ >
387
+ > The aggregate bound is the normative half, and the reason is arithmetic rather than policy:
388
+ > a `did:key` costs nothing to mint, so a per-identity limit is not a bound at all. Free
389
+ > identity defeats per-identity metering by definition; only the aggregate resists a flood. The
390
+ > specific numbers are configuration, not conformance.
391
+
392
+ **AE-29.** If an entry answers **unsigned** inquiries (an optional lane, **RECOMMENDED** to
393
+ default off), that lane **MUST** be bounded entry-wide and **MUST NOT** be reachable at a
394
+ higher rate than the signed lane's aggregate.
395
+
396
+ > **Observation.** With the lane on, drive unsigned requests above the bound; assert refusal. An
397
+ > unauthenticated caller must never become an unmetered signing oracle.
398
+
399
+ ---
400
+
401
+ ## 5. Relationship to other specifications
402
+
403
+ An Agent Entry composes with the specifications below; none of them is an alternative to it,
404
+ and a site may serve several at once. The distinctions here are about **layer**, not merit.
405
+
406
+ **A2A.** This document is a profile of A2A, not a competitor. The card is an A2A Agent Card and
407
+ the endpoint is A2A `message/send`. Everything added lives in extension objects and in
408
+ `metadata`; no existing field changes meaning.
409
+
410
+ **`llms.txt` and agent-facing sitemaps.** These *describe* a site to an agent. An Agent Entry
411
+ *recognises* one. A description is read; an entry answers. They are complementary and a site
412
+ should have both.
413
+
414
+ **MCP (Model Context Protocol).** MCP is a transport between a host application and a tool
415
+ server, and its authorization is OAuth-shaped: the party being identified is the **client
416
+ application**. An Agent Entry identifies the **caller of an individual message**, on the
417
+ message itself, with no session and no token. A site may run an MCP server and an Agent Entry
418
+ simultaneously; they answer different questions and neither substitutes for the other.
419
+
420
+ **OAuth Client ID Metadata Documents (CIMD).** CIMD identifies an OAuth client by an HTTPS URL
421
+ which the authorization server fetches to obtain the client's metadata — replacing dynamic
422
+ client registration with a document the client publishes. It answers *which software is this*,
423
+ and it roots the answer in control of a domain name. An Agent Entry answers *who is knocking*,
424
+ and roots the answer in possession of a key, demonstrated on every message.
425
+
426
+ The two are not rivals; they are different layers, and the honest way to state the difference
427
+ is to quote what each specification says about itself. A Client ID Metadata Document is
428
+ unsigned by construction — an authorization server fetches JSON over TLS, and the draft's only
429
+ identity check is that the `client_id` string equals the URL it was fetched from, compared with
430
+ simple string comparison. The MCP authorization specification names the consequence directly:
431
+ *"Client ID Metadata Documents cannot prevent `localhost` URL impersonation by themselves."*
432
+ That is not a defect in CIMD; it is the boundary of what a fetched document can establish, and
433
+ the OAuth ecosystem is addressing it in a separate attestation track.
434
+
435
+ An Agent Entry sits on the other side of that boundary because possession is demonstrated at
436
+ use time, per message, against an identifier that *is* the public key. What it correspondingly
437
+ does **not** provide is what CIMD does well: a domain-rooted identity that an authorization
438
+ server can hold policy against — allowlists, reputation, "if you trust `example.com` you trust
439
+ this client". A deployment that needs both should run both.
440
+
441
+ **RFC 9421 HTTP Message Signatures / Web Bot Auth.** These sign the HTTP *request* rather than
442
+ the payload inside it, and are the natural companion where a signature must survive
443
+ intermediaries or bind the transport itself. An Agent Entry MAY recognise such signatures in
444
+ addition to the envelope specified here; doing so adds an observation and never changes a
445
+ verdict reached under §4.
446
+
447
+ ---
448
+
449
+ ## 6. Security considerations
450
+
451
+ **A card is a claim until it is bound.** Everything in §4.2 except AE-6 and AE-11 is
452
+ self-asserted, and a byte-perfect copy of a legitimate card scores identically to the original
453
+ on every check that does not include the signed `url`. AE-6 and AE-11 together are the only
454
+ reason a `did` → `url` binding is evidence. An implementation that verifies the signature but
455
+ skips the origin comparison has verified nothing useful: a valid envelope lifted from another
456
+ site's endpoint passes every remaining check.
457
+
458
+ **Do not render self-asserted names.** `name`, `description` and any logo on a card are written
459
+ by whoever serves it. An interface that displays them to a human, next to a trust decision,
460
+ has turned an unauthenticated string into a security control. Display the DID, and the domain
461
+ when AE-10 corroborates it; show the friendly name only for entries a viewer has independently
462
+ reason to trust. This is the single most reliable abuse channel in every deployed system of
463
+ this shape, and it is not a corner case.
464
+
465
+ **Free identity is not scarcity.** Minting a `did:key` costs nothing and requires no
466
+ permission, which is a feature — it is what makes AE-20 possible. It also means being present
467
+ in a ledger was never a bound on anything, which is why AE-28 requires the aggregate ceiling
468
+ and treats the per-identity one as configuration.
469
+
470
+ **An unauthenticated signing path is an oracle.** AE-13 exists because a stranger can drive any
471
+ unauthenticated endpoint that signs on demand. Cache the artifact; re-sign on a timer.
472
+
473
+ **Freshness is a security property, not hygiene.** AE-12's window bounds a harvestable proof.
474
+ An envelope with no expiry is a bearer credential that outlives the operator's control of the
475
+ address it names.
476
+
477
+ **No human is claimed, and no human is claimed to be absent.** An Agent Entry answers a caller
478
+ that may have no browser, no session and nobody at a keyboard — that is the case it exists for.
479
+ Nothing in §4 asserts anything about human presence, and an implementation **MUST NOT** present a
480
+ verified signature as evidence that a person authorised the message. It is evidence that a key
481
+ did.
482
+
483
+ The reason to state this rather than leave it implied is that the surrounding ecosystem is
484
+ building machinery to sort interactive clients from headless ones, and a door that says nothing
485
+ will have a meaning assigned to it. Two facts are worth carrying, because both come from the
486
+ specifications doing that sorting. **Presence cannot be proven remotely today**: the strongest
487
+ primitive in deployment is a WebAuthn gesture at one authenticator at one instant, whose own
488
+ specification says it "does not give the Relying Party a concrete identification of the user";
489
+ every artifact built above it is a bearer token that relays, and the party relaying it is
490
+ precisely the one you were trying to distinguish. And **absence of a signal is not a signal**:
491
+ the HTTP-signature draft this ecosystem runs on states it directly — a verifier that sees no
492
+ signature "has learned nothing about the sender: not that it is automated, not that it is human,
493
+ not that it is evading anything."
494
+
495
+ So an entry **MUST NOT** treat the absence of a human-presence claim as evidence of automation,
496
+ and **SHOULD NOT** treat the presence of one as evidence of a person. Where a site genuinely
497
+ requires a human for an action, the mechanism already exists one layer up and does not belong
498
+ here: an OAuth resource server can demand a fresh authentication event (RFC 9470), and a payment
499
+ flow can refuse an autonomous mandate and ask for a directly approved one. Both are challenges
500
+ that fall back to a person; neither is a proof carried on this wire.
501
+
502
+ The engineering answer this specification prefers is to bound what an unattended caller can do
503
+ rather than to interrogate whether it is unattended: a grant with a use count and an expiry, and
504
+ a key binding that names what it is for, are checkable, and a presence claim is not.
505
+
506
+ **Refusals leak.** AE-21 and AE-26 are both about what a prover is owed versus what a prober is
507
+ owed. The keyless walk-in gets a complete recipe because refusing an agent for lacking a key
508
+ nobody told it to make is the error, not the key. Everyone else gets a verdict.
509
+
510
+ ---
511
+
512
+ ## 7. Deliberately not normative here
513
+
514
+ The behaviours below are real, are implemented, and are **not** `MUST`s in this document,
515
+ because a third party cannot check them from outside. They are stated so that an implementer
516
+ knows they exist and a reader knows they were not forgotten. Their oracle is the
517
+ implementation's own test suite (§8), which is the right instrument for them — it has the
518
+ access that a remote checker does not.
519
+
520
+ - **The account ledger.** That a verified signature creates a durable row keyed by the
521
+ visitor's DID; that a second message from the same DID is the same account; that an unsigned
522
+ request creates no row at all; that a countersigned owner binding files several device keys
523
+ under one account. The *observable* half of this is AE-20, and AE-20 is where the normative
524
+ weight sits.
525
+ - **Rate-limiter internals.** Whether a ceiling is per-account or entry-wide, the bucket
526
+ algorithm, and the specific numbers. AE-28 fixes the property that matters and leaves the
527
+ mechanism open.
528
+ - **Storage and retention.** Ledger size caps, eviction, what is written to disk, what is
529
+ logged.
530
+ - **The responder.** What produces the reply text — a fixed script, a database lookup, a
531
+ language model — is entirely the site's business and is invisible to this specification by
532
+ design.
533
+ - **The observer hook.** That a per-message callback cannot delay or alter a reply.
534
+
535
+ A note on the boundary, because it is the interesting part: several of these could be turned
536
+ into remote observations by making the entry *report* on itself — an endpoint that says "this
537
+ DID has an account". Every such endpoint is a new unauthenticated disclosure surface about
538
+ third parties, and this specification declines to require one. The right answer to "can you
539
+ prove your ledger works" is the implementation's test suite, not a public API that enumerates
540
+ who has visited a site.
541
+
542
+ ---
543
+
544
+ ## 8. Verifying an implementation
545
+
546
+ Two independent oracles, neither of which is this document, and both of which are needed for
547
+ different reasons.
548
+
549
+ **The implementation oracle — test vectors.** `conformance/run.mjs` and
550
+ `conformance/vectors.json`, which ship inside the npm package, decide whether an implementation
551
+ agrees with the fixed bytes: the canonical JSON, the signing payloads, the card envelopes and
552
+ the `did:key` round-trips. `npm test` runs them with no network and nothing to ask anyone for.
553
+ They prove an implementation matches the vectors. They cannot prove a deployment is real,
554
+ because a test that imports the code it is testing establishes only that the code agrees with
555
+ itself.
556
+
557
+ **The interoperability evidence — two implementations, no shared code.** The requirements in §4
558
+ are met today by two implementations written independently in different languages that share no
559
+ code at all, held to identical verdicts by an acceptance suite that posts identical bytes to
560
+ both — down to the HTTP framing — and requires the same status, the same account outcome and the
561
+ same signed reply from each. That, rather than a shared library, is what makes the byte-level
562
+ requirements here credible: a shared library would only ever have covered the parts the two
563
+ happen to have in common. If you write a third implementation, that suite is the gate, and we
564
+ will run it against yours on request.
565
+
566
+ **The deployment oracle — a remote checker.** An implementation-blind checker dials a live
567
+ origin over HTTP and decides requirements from the responses alone:
568
+
569
+ ```
570
+ npx @muretai/agent-site-checker example.com
571
+ ```
572
+
573
+ Use the package, not a hosted service. A specification whose conformance depends on an endpoint
574
+ someone operates has acquired a runtime dependency on that operator, and the whole argument of
575
+ §1 is that a normative claim should be checkable by a stranger with no relationship to anyone.
576
+ The package runs locally, works against a local origin, and needs nothing from us.
577
+
578
+ **How much of §4 it decides today, stated exactly.** The read-only requirements — discovery,
579
+ the card, the signed envelope and its binding and freshness (roughly AE-1 through AE-13) — are
580
+ decided by `GET`s and are what the checker covers now. The message-endpoint requirements
581
+ (AE-14 onward) need a driver that *sends*: an eleven-request battery for AE-18, a fresh keypair
582
+ for AE-20, a stripped envelope for AE-26. Those are decidable by a stranger — that is why they
583
+ are `MUST`s — but a checker that POSTs to a stranger's endpoint is a different instrument from
584
+ one that reads, and it should be run against your own deployment rather than someone else's. The
585
+ gap between §9's table and what any given checker covers is a to-do list, not a licence: a `MUST`
586
+ here is a requirement whether or not a tool currently checks it, and §9 exists so the gap is
587
+ visible rather than convenient.
588
+
589
+ **Reporting a divergence.** If the two oracles disagree, or if a requirement in §4 cannot be
590
+ decided by its own Observation, that is a defect in this document. Please open an issue with
591
+ the request and response bytes.
592
+
593
+ ---
594
+
595
+ ## 9. Requirement index
596
+
597
+ Every normative statement, with the observation that decides it. A conforming implementation
598
+ satisfies all `MUST` rows. This table is the document's contract with itself: a row that cannot
599
+ be written is a requirement that does not belong in §4.
600
+
601
+ | ID | Level | Requirement | Decided by |
602
+ |---|---|---|---|
603
+ | AE-1 | MUST | card served at the well-known path | `GET` → 200 + JSON |
604
+ | AE-2 | MUST | legacy path is byte-identical | `GET` both, compare bytes |
605
+ | AE-3 | MUST | signed envelope served | `GET` → 200 + JSON with `v/typ/card/ts/sig` |
606
+ | AE-4 | MUST NOT | no unadvertised address answers | `GET`/`POST`/`OPTIONS` → 404 |
607
+ | AE-5 | MUST | required card fields incl. `did` | parse + decode the DID |
608
+ | AE-6 | MUST | `card.url` scope matches the dialled URL | compare origin + path prefix |
609
+ | AE-7 | MUST | `open_door` under both names, identical | compare the two values |
610
+ | AE-8 | MUST | terms stated on the card | parse `securitySchemes` |
611
+ | AE-9 | MUST | `howTo` resolves, or is absent | `GET` it → not 404 |
612
+ | AE-10 | MUST NOT | no uncorroborated `domains` claim | fetch the domain's DID configuration, verify, check expiry |
613
+ | AE-11 | MUST | envelope verifies under `card.did` | Ed25519 verify |
614
+ | AE-12 | MUST | `ts` integer, within ±6 h | parse + compare |
615
+ | AE-13 | MUST NOT / SHOULD | no per-request signing; cache | fetch twice, compare `ts`/`sig` |
616
+ | AE-14 | MUST | `OPTIONS` → 204 + per-resource `Allow` | `OPTIONS` each address |
617
+ | AE-15 | MUST | 405 + `Allow` on advertised addresses | `GET` a guest door |
618
+ | AE-16 | MUST | 404 (not 405) on unadvertised `POST` | `POST` a guessed path |
619
+ | AE-17 | MUST | absolute-form target → 404 | send an absolute-form target |
620
+ | AE-18 | MUST | eleven checks, in order, with fixed codes | eleven single-violation requests |
621
+ | AE-19 | MUST | protocol verdicts are HTTP 200 | assert status on rows 3-11 |
622
+ | AE-20 | MUST | unknown DID answered inline, signed, no registration | mint a key, send once, verify the reply |
623
+ | AE-21 | MUST NOT | no echo of caller text, no internals | marker string + stack-trace scan |
624
+ | AE-22 | MUST | always an HTTP response | assert a status line exists |
625
+ | AE-23 | MUST | `id` echoed only when serialisable | send each `id` shape |
626
+ | AE-24 | MUST | keyless refusal carries `accepts` | `POST` with no metadata, deep-equal the card block |
627
+ | AE-25 | MUST | refusal survives URL removal | strip URLs, check completeness |
628
+ | AE-26 | MUST NOT | partial envelope gets no `accepts` | `POST` with `sig` stripped |
629
+ | AE-27 | MUST | size caps | oversized body and text |
630
+ | AE-28 | MUST | aggregate reply ceiling, refused with `-32004` | drive above the ceiling |
631
+ | AE-29 | MUST | unsigned lane bounded entry-wide | drive the anonymous lane |
632
+
633
+ ---
634
+
635
+ ## 10. IANA and registry considerations
636
+
637
+ This document registers nothing. It uses the well-known URI `agent-card.json` established by
638
+ the A2A specification under RFC 8615, the JSON-RPC 2.0 error range, and `did:key` as defined by
639
+ the W3C DID method registry. The error codes in the range `-32001` … `-32005` and `-32010` …
640
+ `-32011` are application-defined codes within the range JSON-RPC 2.0 reserves for
641
+ implementation-defined server errors.
642
+
643
+ ## 11. Changes from the pre-specification implementation
644
+
645
+ None. Version 1 describes behaviour already deployed; it introduces no new requirement that a
646
+ running Agent Entry does not already satisfy. Where this document and the reference
647
+ implementation disagree, that is a bug in one of them and a report is welcome — a specification
648
+ written *after* the code has no excuse for describing something that was never shipped.