@muretai/agent-entry 1.9.0 → 1.10.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
@@ -105,6 +105,7 @@ answers questions and hands off nothing, is a signed claim you cannot keep.
105
105
  |---|---|---|
106
106
  | `skills` | `[]` | the menu above — what a visitor learns before knocking |
107
107
  | `openDoor` | `true` | publishes `agentEntry.open_door`: the field that tells a visiting agent it may message you with no introduction. The same fact is emitted under the older `muretai.open_door` spelling beside it — read either, write the neutral one |
108
+ | `prefer` | unset | your own order of the ways into your site, published verbatim as `agentEntry.prefer` (spec AE-30): an array of `"page"`, `"card"`, `"mcp"` or `{kind, when}` with `when` one of `person`, `alone`, `key`, `no-key`, `token`, `browser` — e.g. `[{kind:'page', when:'no-key'}, 'card']` says "read on the page if you hold no key; otherwise the door". A visiting agent reads it against what it has on hand. An invalid list refuses to start rather than publish an order you did not write; unset publishes no key at all |
108
109
  | `anonymousLane` | `false` | also answer **unsigned** inquiries. They create no account row, and the lane is capped entry-wide — an unauthenticated caller must never become an unmetered signing oracle |
109
110
  | `anonRatePerMin` | `30` | anonymous replies per minute, entry-wide |
110
111
  | `signedRatePerMin` | `60` | signed replies per minute **per account**, ON by default. Attribution is not scarcity: a `did:key` costs nothing to mint, so being in your ledger was never a bound |
@@ -381,7 +382,8 @@ node examples/server.mjs # prints its DID and card URL
381
382
  Environment: `AGENT_ENTRY_SEED_HEX` (generated and printed if absent — **persist it, it is
382
383
  your site's identity**), `AGENT_ENTRY_PORT` (8788), `AGENT_ENTRY_BASE_URL`,
383
384
  `AGENT_ENTRY_NAME`, `AGENT_ENTRY_ANON` (`1` also accepts unsigned inquiries, which create
384
- no account).
385
+ no account), `AGENT_ENTRY_PREFER` (your order of the ways in, as one JSON array — see
386
+ `prefer` above; an invalid list refuses to start).
385
387
 
386
388
  ## What `baseUrl` may be
387
389
 
@@ -43,6 +43,10 @@
43
43
  * https://example.com/agent) and answers NOTHING at `/` — no notice,
44
44
  * no OPTIONS, no POST. Point your proxy at the door path and the
45
45
  * well-known paths; the site keeps everything else, unchanged.
46
+ * AGENT_ENTRY_PREFER OPTIONAL: the site's own order of its ways in, as one JSON array
47
+ * (AE-30), e.g. '[{"kind":"page","when":"no-key"},"card"]' — "read on
48
+ * the page if you hold no key; otherwise the door". Published verbatim
49
+ * as `agentEntry.prefer`; an invalid list REFUSES to start.
46
50
  * AGENT_ENTRY_WBA_JWKS OPTIONAL: a JWKS document {"keys":[…]} as one JSON string —
47
51
  * the Web Bot Auth key directory (verified out of band) whose
48
52
  * holders this entry should RECOGNISE on inbound requests. Off
@@ -156,6 +160,10 @@ try {
156
160
  ...(process.env.AGENT_ENTRY_SIGNED_RATE_TOTAL
157
161
  ? { signedRatePerMinTotal: Number(process.env.AGENT_ENTRY_SIGNED_RATE_TOTAL) } : {}),
158
162
  guest: process.env.AGENT_ENTRY_GUEST === '1',
163
+ // AE-30: the site's own order of its ways in, a JSON array. Malformed JSON or an
164
+ // unknown kind/condition throws inside this try and the entry never starts — the same
165
+ // posture as a bad domain list: never publish a statement the operator did not make.
166
+ ...(process.env.AGENT_ENTRY_PREFER ? { prefer: JSON.parse(process.env.AGENT_ENTRY_PREFER) } : {}),
159
167
  wbaVerifiers,
160
168
  });
161
169
  } catch (err) {
@@ -2148,6 +2148,43 @@ function domainFix(candidate) {
2148
2148
  * refuses every other bad value there. Truncating would start the entry with a claim that
2149
2149
  * is USABLE and NOT WHAT THEY SAID.
2150
2150
  */
2151
+ /** The kinds a visitor can take into a site, and the conditions a site may attach. Kept
2152
+ * identical to the visitor side (Agent Web Router `parsePrefer`): a kind or condition one
2153
+ * side knows and the other does not is a declaration one side silently drops. */
2154
+ export const PREFER_KINDS = ['page', 'card', 'mcp'];
2155
+ export const PREFER_WHEN = ['person', 'alone', 'key', 'no-key', 'token', 'browser'];
2156
+
2157
+ /**
2158
+ * The exact `agentEntry.prefer` this entry may publish, or a TypeError (AE-30).
2159
+ *
2160
+ * The site's own order of its ways in — "read on the page if you have no key, then the
2161
+ * door", say. VALIDATED, NEVER REWRITTEN: this goes on a SIGNED card, and a card that says
2162
+ * something the operator did not write is a worse card than none, so an unknown kind, an
2163
+ * unknown condition or a stray key refuses the whole declaration instead of trimming it —
2164
+ * the same posture as `canonicalDomains`. `null`/`undefined` means "not configured", and
2165
+ * then no `prefer` key is published at all, which is what keeps an already-deployed
2166
+ * entry's bytes unchanged.
2167
+ */
2168
+ export function validatePrefer(prefer) {
2169
+ if (prefer == null) return null;
2170
+ if (!Array.isArray(prefer) || prefer.length === 0) {
2171
+ throw new TypeError('agentEntry.prefer must be a non-empty array of "page" | "card" | "mcp" or {kind, when}');
2172
+ }
2173
+ for (const e of prefer) {
2174
+ if (typeof e === 'string') {
2175
+ if (!PREFER_KINDS.includes(e)) throw new TypeError(`agentEntry.prefer: unknown kind ${JSON.stringify(e)}`);
2176
+ continue;
2177
+ }
2178
+ if (!e || typeof e !== 'object' || Array.isArray(e)) throw new TypeError('agentEntry.prefer: an entry must be a kind or {kind, when}');
2179
+ const keys = Object.keys(e);
2180
+ if (!PREFER_KINDS.includes(e.kind)) throw new TypeError(`agentEntry.prefer: unknown kind ${JSON.stringify(e.kind)}`);
2181
+ if ('when' in e && !PREFER_WHEN.includes(e.when)) throw new TypeError(`agentEntry.prefer: unknown condition ${JSON.stringify(e.when)}`);
2182
+ const stray = keys.filter((k) => k !== 'kind' && k !== 'when');
2183
+ if (stray.length) throw new TypeError(`agentEntry.prefer: unexpected key(s) ${stray.join(', ')}`);
2184
+ }
2185
+ return prefer;
2186
+ }
2187
+
2151
2188
  export function canonicalDomains(domains, { warn = true } = {}) {
2152
2189
  if (domains === undefined || domains === null) return [];
2153
2190
  if (!Array.isArray(domains)) {
@@ -2269,6 +2306,11 @@ export function canonicalMount(canonUrl, basePath) {
2269
2306
  * responder (envelope) => string | {text, contextId?, timestamp?} | Promise<…>
2270
2307
  * openDoor advertise `muretai.open_door` (default true) — the flag that tells a
2271
2308
  * visiting agent it may contact you without an introduction.
2309
+ * prefer OPTIONAL: the site's own order of its ways in, published as
2310
+ * `agentEntry.prefer` (AE-30) — e.g. `[{kind:'page', when:'no-key'}, 'card']`
2311
+ * says "read on the page if you hold no key; otherwise the door". Validated
2312
+ * by `validatePrefer`; an invalid list throws, so the entry never starts
2313
+ * with a statement the operator did not make. Absent = no key published.
2272
2314
  * anonymousLane also accept UNSIGNED inquiries (default false). They create no account,
2273
2315
  * and the lane as a whole is capped at `anonRatePerMin` signed replies per
2274
2316
  * minute — it is unauthenticated, so it must not be an unmetered signing
@@ -2330,6 +2372,7 @@ export function createAgentEntry({
2330
2372
  version = '1',
2331
2373
  responder = () => 'Thanks — a human will follow up.',
2332
2374
  openDoor = true,
2375
+ prefer = null,
2333
2376
  anonymousLane = false,
2334
2377
  anonRatePerMin = ANON_RATE_PER_MIN,
2335
2378
  signedRatePerMin = SIGNED_RATE_PER_MIN,
@@ -2404,7 +2447,10 @@ export function createAgentEntry({
2404
2447
  // Neutral key first, vendor key beside it for one release. See the securitySchemes block
2405
2448
  // below for why the old spelling stays: a consumer must learn the new name BEFORE
2406
2449
  // producers stop emitting the old one, never after.
2407
- if (openDoor) card.agentEntry = { open_door: true };
2450
+ // AE-30: the site's order rides on the NEUTRAL key only; the alias stays `open_door`
2451
+ // alone, so an old consumer that compares the two aliases byte for byte keeps passing.
2452
+ const canonPrefer = validatePrefer(prefer);
2453
+ if (openDoor) card.agentEntry = { open_door: true, ...(canonPrefer ? { prefer: canonPrefer } : {}) };
2408
2454
  if (openDoor) card.muretai = { open_door: true };
2409
2455
  // Deliberately NO `relay`/`enc_pub` on the card: those advertise a store-and-forward
2410
2456
  // mailbox, and an agent entry has no listener draining one. Advertising a mailbox nobody
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muretai/agent-entry",
3
- "version": "1.9.0",
3
+ "version": "1.10.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",
package/spec/v1.md CHANGED
@@ -396,6 +396,25 @@ higher rate than the signed lane's aggregate.
396
396
  > **Observation.** With the lane on, drive unsigned requests above the bound; assert refusal. An
397
397
  > unauthenticated caller must never become an unmetered signing oracle.
398
398
 
399
+ **AE-30.** An entry **MAY** publish `agentEntry.prefer`: the site's own order of its ways in
400
+ for a visiting agent. It is an array whose entries are a kind — `"page"`, `"card"`, `"mcp"` —
401
+ or an object `{"kind": <kind>, "when": <condition>}` with `when` one of `person`, `alone`,
402
+ `key`, `no-key`, `token`, `browser`, read by the visitor against what it has on hand. When
403
+ published it **MUST** appear under the neutral `agentEntry` key exactly as the operator
404
+ declared it (validated, not rewritten), and **MUST** appear identically in the signed
405
+ envelope's inner card. The legacy `muretai` alias **MUST NOT** carry it. When not configured,
406
+ the card **MUST NOT** carry a `prefer` key. A declaration that fails validation **MUST** make
407
+ the entry refuse to start rather than publish a corrected or partial order.
408
+
409
+ > **Observation.** Start an entry with a declaration; `GET` the card and the envelope; assert
410
+ > `agentEntry.prefer` equals the declaration in both and `muretai` has no `prefer`. Start one
411
+ > without; assert no `prefer` key. Start one with `["teleport"]`; assert it exits non-zero and
412
+ > binds nothing. Why refuse rather than fix: a signed card is the origin's statement, and a
413
+ > statement the operator did not make is a worse card than none — the same posture as a bad
414
+ > `domains` list. Why the visitor's conditions live here: which way in a stranger should try
415
+ > first is the site's design (read on the page, become a counterparty later — or knock first),
416
+ > and the card is the one place the site can say so that a page script cannot rewrite.
417
+
399
418
  ---
400
419
 
401
420
  ## 5. Relationship to other specifications
@@ -629,6 +648,7 @@ be written is a requirement that does not belong in §4.
629
648
  | AE-27 | MUST | size caps | oversized body and text |
630
649
  | AE-28 | MUST | aggregate reply ceiling, refused with `-32004` | drive above the ceiling |
631
650
  | AE-29 | MUST | unsigned lane bounded entry-wide | drive the anonymous lane |
651
+ | AE-30 | MAY / MUST | `agentEntry.prefer` verbatim on card + envelope, or absent; invalid refuses to start | `GET` both, compare; start with a bad list → exit ≠ 0 |
632
652
 
633
653
  ---
634
654