@muretai/agent-entry 1.9.0 → 1.11.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 +164 -39
- package/conformance/receptor-check.mjs +904 -0
- package/diagrams/become.png +0 -0
- package/diagrams/desk.svg +17 -0
- package/diagrams/x402.svg +21 -0
- package/examples/server.mjs +8 -0
- package/muretai-agent-entry.mjs +323 -88
- package/package.json +8 -3
- package/scripts/distill/README.md +32 -0
- package/scripts/distill/distill.mjs +51 -0
- package/scripts/distill/fixtures.json +10 -0
- package/scripts/distill/lib.mjs +100 -0
- package/scripts/distill/loop.mjs +46 -0
- package/scripts/distill/measure.mjs +80 -0
- package/scripts/distill/record.mjs +90 -0
- package/scripts/distill/test.mjs +43 -0
- package/spec/skill-distill.md +219 -0
- package/spec/v1.md +20 -0
package/README.md
CHANGED
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
# Agent Entry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@muretai/agent-entry)
|
|
4
|
+
[](LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
**Agents already visit your site. Give them a way to become a customer.**
|
|
7
|
+
|
|
8
|
+
## Contents
|
|
9
|
+
|
|
10
|
+
- [What this is](#what-this-is)
|
|
11
|
+
- [What you get](#what-you-get)
|
|
12
|
+
- [Who is knocking](#who-is-knocking)
|
|
13
|
+
- [Put it on a site](#put-it-on-a-site)
|
|
14
|
+
- [On serverless](#on-serverless)
|
|
15
|
+
- [On WordPress](#on-wordpress)
|
|
16
|
+
- [Pairs with WebMCP](#pairs-with-webmcp-the-tab-conversation-becomes-a-customer)
|
|
17
|
+
- [In production](#in-production)
|
|
18
|
+
- [This package](#this-package)
|
|
19
|
+
|
|
20
|
+
## What this is
|
|
21
|
+
|
|
22
|
+
They read your pages and leave. No signup, no cookie, nothing in your analytics.
|
|
23
|
+
Agent Entry is a single module you put on the origin. It publishes an
|
|
24
|
+
[Agent Card](https://a2a-protocol.org/latest/specification/)
|
|
25
|
+
at `/.well-known/agent-card.json` — who you are, what you answer, how to knock —
|
|
26
|
+
and then answers one signed POST: verify the visitor, open an account from their
|
|
27
|
+
key, reply signed, same HTTP response.
|
|
28
|
+
|
|
29
|
+
No form. The key *is* the account. Come back next month from another device and
|
|
30
|
+
it is still them.
|
|
31
|
+
|
|
32
|
+
Any A2A client works: their agent, `curl`, not a particular router or ours.
|
|
33
|
+
|
|
34
|
+
Keep WebMCP. A person already in the tab keeps the page. This file sits beside
|
|
35
|
+
it and replaces none of it: an Agent Card and a door, so an agent who arrives
|
|
36
|
+
alone can become a customer — one signed POST, a signed reply.
|
|
37
|
+
|
|
38
|
+
`llms.txt` is a brochure — prose, not a contract. MCP is a tool server for an
|
|
39
|
+
app that holds a token. A different purpose.
|
|
40
|
+
|
|
41
|
+
The agent reads the card **before** it knocks (AE-8). The act is then one
|
|
42
|
+
**POST** `message/send` — already formed correctly — and a signed reply comes
|
|
43
|
+
back on that same request (AE-20).
|
|
44
|
+
|
|
45
|
+
A **handoff** is how a tool on the page, or a tool server, can send the visitor
|
|
46
|
+
*here*: the result names this entry's DID, and the next act is a signed knock at
|
|
47
|
+
this origin. This file does not follow a handoff and does not parse
|
|
48
|
+
`_meta.handoff`. It is where one lands. The `to` in that envelope must be the
|
|
49
|
+
DID printed at startup — the same seed as the card. A visitor (or any router
|
|
50
|
+
they run) is who honours the pointer.
|
|
51
|
+
|
|
52
|
+
| | |
|
|
53
|
+
|---|---|
|
|
54
|
+
| **Who installs this** | Website owners who want agents as *customers* — a returning identity — not only as crawlers. |
|
|
55
|
+
| **Not for** | People writing the visiting agent. This package does not find doors, MCP servers, or WebMCP tools; it *is* a door. |
|
|
56
|
+
| **The problem** | A GET-only document cannot recognise anyone. A signup form does not work for an agent. The first signed POST has to *be* the account. |
|
|
57
|
+
|
|
58
|
+

|
|
8
59
|
|
|
9
60
|
One file. Zero dependencies. No database. Node 20+.
|
|
10
61
|
|
|
@@ -38,9 +89,26 @@ That is the whole integration. `responder` is called with a verified envelope an
|
|
|
38
89
|
what to say back; everything else — signatures, replay, rate limiting, the account
|
|
39
90
|
ledger — is handled for you.
|
|
40
91
|
|
|
92
|
+
### Not an HTTP 402 challenge
|
|
93
|
+
|
|
94
|
+
[x402](https://x402.org/) v2 over HTTP teaches by **error**. The client asks for the
|
|
95
|
+
resource; the server answers `402 Payment Required` with a `PAYMENT-REQUIRED` header; the
|
|
96
|
+
client **retries** the same resource with `PAYMENT-SIGNATURE`. The error is how the
|
|
97
|
+
client learns what to do next.
|
|
98
|
+
|
|
99
|
+
Agent Entry does the reverse. The Agent Card states the terms **before** anyone knocks
|
|
100
|
+
(AE-8). An HTTP-402-style challenge structurally cannot do that. The first POST is already
|
|
101
|
+
formed correctly and is answered `200` with a signed reply (AE-20). This package does not
|
|
102
|
+
speak x402 and does not return HTTP 402. A protocol refusal here is HTTP `200` plus a
|
|
103
|
+
JSON-RPC error (AE-19). If someone POSTs with no signature at all, `-32001` still carries
|
|
104
|
+
`data.accepts` — the same scheme object as the card — so they can succeed on the next POST
|
|
105
|
+
(AE-24). That is a fallback, not the design.
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
41
109
|
---
|
|
42
110
|
|
|
43
|
-
## What you
|
|
111
|
+
## What you get
|
|
44
112
|
|
|
45
113
|
**A caller you can trust.** Every message arrives with an Ed25519 signature over six
|
|
46
114
|
frozen fields. The sender's DID *is* their public key (`did:key`), so verification needs
|
|
@@ -69,7 +137,7 @@ resolves it and files them under `owner_did`, so a replaced phone is not a new c
|
|
|
69
137
|
Your entry does not need to poll or be told: a node that carries the account learns it on
|
|
70
138
|
its own, and refuses that key.
|
|
71
139
|
|
|
72
|
-
|
|
140
|
+
### Say what your door answers
|
|
73
141
|
|
|
74
142
|
A visiting agent reads your card **before** it knocks. Left alone, that card says something
|
|
75
143
|
answers here and nothing about what it answers, so the visitor has to guess and learns your
|
|
@@ -84,7 +152,7 @@ createAgentEntry({
|
|
|
84
152
|
description: 'Ask what a shoot costs, what the studio does, and how to book. '
|
|
85
153
|
+ 'The answer comes back in the same HTTP response, signed by this domain.',
|
|
86
154
|
tags: ['studio', 'booking', 'signed', 'inline-reply'],
|
|
87
|
-
examples: ['Do you shoot weddings?', '
|
|
155
|
+
examples: ['Do you shoot weddings?', 'How much is a half-day?', 'Book Saturday 14:00'],
|
|
88
156
|
}],
|
|
89
157
|
});
|
|
90
158
|
```
|
|
@@ -99,12 +167,21 @@ visitor you will get, so drive your examples through your own responder in your
|
|
|
99
167
|
**Declare only what the responder does:** a skill that mentions booking, on an entry that
|
|
100
168
|
answers questions and hands off nothing, is a signed claim you cannot keep.
|
|
101
169
|
|
|
170
|
+
Turning logged knocks into that menu is an offline owner loop, not part of the
|
|
171
|
+
runtime — see [`spec/skill-distill.md`](spec/skill-distill.md). On this machine
|
|
172
|
+
only: `npm run distill` measures whether a proposed menu would have made the
|
|
173
|
+
next first knock useful. `observer: fileSink()` (from
|
|
174
|
+
`scripts/distill/record.mjs`) appends POST outcomes to `var/traces.jsonl`.
|
|
175
|
+
Nothing is uploaded, and the Distiller is never imported by
|
|
176
|
+
`muretai-agent-entry.mjs`.
|
|
177
|
+
|
|
102
178
|
### The rest of the settings
|
|
103
179
|
|
|
104
180
|
| option | default | what it does |
|
|
105
181
|
|---|---|---|
|
|
106
182
|
| `skills` | `[]` | the menu above — what a visitor learns before knocking |
|
|
107
183
|
| `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 |
|
|
184
|
+
| `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
185
|
| `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
186
|
| `anonRatePerMin` | `30` | anonymous replies per minute, entry-wide |
|
|
110
187
|
| `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 |
|
|
@@ -121,12 +198,12 @@ answers questions and hands off nothing, is a signed claim you cannot keep.
|
|
|
121
198
|
`seedHex` and `baseUrl` are the two an entry refuses to start without: the seed **is** the
|
|
122
199
|
address, and the url it publishes must equal the origin the visitor dialled.
|
|
123
200
|
|
|
124
|
-
## Who is knocking
|
|
201
|
+
## Who is knocking
|
|
125
202
|
|
|
126
|
-
The person who found you often never opens a browser:
|
|
127
|
-
agent, and the agent fetches your card and knocks.
|
|
128
|
-
page-view metric you have — the only place it
|
|
129
|
-
door counts it:
|
|
203
|
+
Observation, never identity. The person who found you often never opens a browser:
|
|
204
|
+
they hand your link to their agent, and the agent fetches your card and knocks.
|
|
205
|
+
That traffic is invisible to every page-view metric you have — the only place it
|
|
206
|
+
can be seen is the door itself. So the door counts it:
|
|
130
207
|
|
|
131
208
|
```js
|
|
132
209
|
entry.stats()
|
|
@@ -198,7 +275,9 @@ signature over the transport proves who fetched — not who wrote the text, and
|
|
|
198
275
|
captured header set is replayable until it expires (minutes), which is why `wba_did`
|
|
199
276
|
is identification, never authorship.
|
|
200
277
|
|
|
201
|
-
##
|
|
278
|
+
## Put it on a site
|
|
279
|
+
|
|
280
|
+
### Install
|
|
202
281
|
|
|
203
282
|
```bash
|
|
204
283
|
npm i @muretai/agent-entry
|
|
@@ -218,7 +297,7 @@ upgrade — the ledger is your customer list, and more features stand on keeping
|
|
|
218
297
|
an analytics tool covers statistics without one. Both are described under
|
|
219
298
|
[Before you put it in production](#before-you-put-it-in-production).
|
|
220
299
|
|
|
221
|
-
|
|
300
|
+
### Put one on a site you already have
|
|
222
301
|
|
|
223
302
|
A visiting agent knows only your **domain**, so the three paths it walks are fixed — it
|
|
224
303
|
cannot be told to look elsewhere:
|
|
@@ -351,8 +430,9 @@ has already changed the bytes the signature covers, and the only diagnostic anyo
|
|
|
351
430
|
|
|
352
431
|
### 3. A reverse proxy — for a site that is not Node at all
|
|
353
432
|
|
|
354
|
-
|
|
355
|
-
locations to it
|
|
433
|
+
Rails, a static build, anything that is not WordPress and not a function. Run the
|
|
434
|
+
entry as one small process and route three locations to it. WordPress has its own
|
|
435
|
+
plugin — [On WordPress](#on-wordpress).
|
|
356
436
|
|
|
357
437
|
```nginx
|
|
358
438
|
location = /.well-known/agent-card.json { proxy_pass http://127.0.0.1:8788; }
|
|
@@ -363,16 +443,35 @@ location = / {
|
|
|
363
443
|
}
|
|
364
444
|
```
|
|
365
445
|
|
|
366
|
-
###
|
|
446
|
+
### On serverless
|
|
447
|
+
|
|
448
|
+
The round-trip shape fits a single function: one signed POST in, one signed reply out.
|
|
449
|
+
What does not fit is the state. A function instance keeps nothing between requests, so
|
|
450
|
+
the replay set, the device→owner pins and the ledger have to live in a store the
|
|
451
|
+
platform keeps, not in memory.
|
|
452
|
+
|
|
453
|
+
This package still runs in process. For Cloudflare Workers, Vercel and Netlify, use
|
|
454
|
+
the deploy templates — this door plus one store adapter per platform:
|
|
455
|
+
|
|
456
|
+
[github.com/muretai/agent-entry-serverless](https://github.com/muretai/agent-entry-serverless)
|
|
457
|
+
|
|
458
|
+
They vendor a library build that includes the `store` seam those adapters need. Do not
|
|
459
|
+
replace that file with an older published copy of this package: versions without the
|
|
460
|
+
seam accept the option and silently ignore it, which puts the three stateful rules
|
|
461
|
+
back into per-instance memory — the failures those templates exist to prevent.
|
|
462
|
+
|
|
463
|
+
Until you are on one of those templates, use one of the long-lived shapes above.
|
|
464
|
+
|
|
465
|
+
### On WordPress
|
|
367
466
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
live in your own store rather than in memory. A `store` hook for that is the next release;
|
|
373
|
-
until then, use one of the three long-lived shapes above.
|
|
467
|
+
If the site is WordPress, do not proxy this Node file in front of it. The plugin is a
|
|
468
|
+
third implementation of the same contract — it publishes the card, answers the signed
|
|
469
|
+
POST, and with WooCommerce lets an agent ask about the catalogue. `GET /` stays your
|
|
470
|
+
site.
|
|
374
471
|
|
|
375
|
-
|
|
472
|
+
[github.com/muretai/agent-entry-wordpress](https://github.com/muretai/agent-entry-wordpress)
|
|
473
|
+
|
|
474
|
+
### Run the example
|
|
376
475
|
|
|
377
476
|
```bash
|
|
378
477
|
node examples/server.mjs # prints its DID and card URL
|
|
@@ -381,9 +480,10 @@ node examples/server.mjs # prints its DID and card URL
|
|
|
381
480
|
Environment: `AGENT_ENTRY_SEED_HEX` (generated and printed if absent — **persist it, it is
|
|
382
481
|
your site's identity**), `AGENT_ENTRY_PORT` (8788), `AGENT_ENTRY_BASE_URL`,
|
|
383
482
|
`AGENT_ENTRY_NAME`, `AGENT_ENTRY_ANON` (`1` also accepts unsigned inquiries, which create
|
|
384
|
-
no account)
|
|
483
|
+
no account), `AGENT_ENTRY_PREFER` (your order of the ways in, as one JSON array — see
|
|
484
|
+
`prefer` above; an invalid list refuses to start).
|
|
385
485
|
|
|
386
|
-
|
|
486
|
+
### What `baseUrl` may be
|
|
387
487
|
|
|
388
488
|
`baseUrl` must be the URL visitors actually dial: it is what your signed card claims, and
|
|
389
489
|
a card naming a different origin proves nothing about yours.
|
|
@@ -420,7 +520,7 @@ Two rules worth knowing before you pick a URL:
|
|
|
420
520
|
parser punycodes a host and Python's does not, so the two implementations would otherwise
|
|
421
521
|
sign different bytes for the same site.
|
|
422
522
|
|
|
423
|
-
|
|
523
|
+
### One host, many agents
|
|
424
524
|
|
|
425
525
|
A domain can hold a **fleet** — a front desk, support, sales — each its own agent, its own
|
|
426
526
|
key, its own address, each contactable directly. Give each one a `baseUrl` that carries its
|
|
@@ -472,7 +572,7 @@ const fwd = (entry) => async (req, res) => {
|
|
|
472
572
|
};
|
|
473
573
|
```
|
|
474
574
|
|
|
475
|
-
|
|
575
|
+
### Which domains this entry speaks for
|
|
476
576
|
|
|
477
577
|
An entry can name the domains it belongs to:
|
|
478
578
|
|
|
@@ -503,6 +603,11 @@ Set it from the environment with `AGENT_ENTRY_DOMAINS=studio.example,support.stu
|
|
|
503
603
|
|
|
504
604
|
## Pairs with WebMCP: the tab conversation becomes a customer
|
|
505
605
|
|
|
606
|
+
WebMCP is not MCP. MCP is a tool *server* over HTTP (a token, a session). WebMCP is tools
|
|
607
|
+
*in the page*, running as whoever is in that browser tab. This package implements neither.
|
|
608
|
+
A site may run WebMCP, an MCP server, and an Agent Entry at once; a headless agent should
|
|
609
|
+
use the last two, not scrape the first.
|
|
610
|
+
|
|
506
611
|
If your page already exposes [WebMCP](https://github.com/MiguelsPizza/WebMCP) tools, you have
|
|
507
612
|
one door open: an agent **inside a visitor's browser** can call `check_stock` or `inquire`
|
|
508
613
|
while that person is on the page. That is useful and it is also temporary — close the tab and
|
|
@@ -515,10 +620,11 @@ An Agent Entry is the second door, and it is the one that keeps something:
|
|
|
515
620
|
| **WebMCP tools** | a person's agent, in a tab, right now | an answer in the moment |
|
|
516
621
|
| **Agent Entry** | an agent alone, from anywhere, at any hour | a customer you still recognise next month |
|
|
517
622
|
|
|
518
|
-
**They connect.** When a WebMCP tool call reaches the point of
|
|
519
|
-
a booking, a quote, a follow-up — the tool returns a small
|
|
520
|
-
|
|
521
|
-
Entry receives it:
|
|
623
|
+
**They connect by a handoff.** When a WebMCP (or MCP) tool call reaches the point of
|
|
624
|
+
actually wanting something — a booking, a quote, a follow-up — the tool returns a small
|
|
625
|
+
envelope naming your site's DID. The visitor's agent then sends a **signed message to your
|
|
626
|
+
own origin**, where your Agent Entry receives it. That envelope is a handoff: this package
|
|
627
|
+
is the landing, not the follower.
|
|
522
628
|
|
|
523
629
|
```js
|
|
524
630
|
navigator.modelContext.registerTool({
|
|
@@ -547,7 +653,9 @@ account layer resolves the owner behind both keys.
|
|
|
547
653
|
A search engine makes your site **findable**. An Agent Entry makes it **answerable** — and
|
|
548
654
|
makes the visitor someone you can recognise the next time.
|
|
549
655
|
|
|
550
|
-
##
|
|
656
|
+
## In production
|
|
657
|
+
|
|
658
|
+
### Before you put it in production
|
|
551
659
|
|
|
552
660
|
**Nothing here is needed to start** — an entry runs, and every exchange stays correct,
|
|
553
661
|
on its in-process state alone; some installers have read this section as a prerequisite,
|
|
@@ -598,7 +706,7 @@ What the entry now handles for you at the HTTP layer, so you do not have to:
|
|
|
598
706
|
which says a server must accept the absolute form: this endpoint answers exactly the
|
|
599
707
|
address its card names, and the refusal says so.
|
|
600
708
|
|
|
601
|
-
|
|
709
|
+
### Counting visits without handing over your customer list
|
|
602
710
|
|
|
603
711
|
You will want to know how many agents knocked, how many came back, and what they asked. All
|
|
604
712
|
three are answerable — and how you answer them decides whether you are counting your visitors
|
|
@@ -683,7 +791,9 @@ disclosure that arrives after the visit is not a disclosure, it is a receipt.
|
|
|
683
791
|
And if you decide to send raw DIDs anyway, that is your call to make — but say so on the card,
|
|
684
792
|
in the same breath, in plain words.
|
|
685
793
|
|
|
686
|
-
##
|
|
794
|
+
## This package
|
|
795
|
+
|
|
796
|
+
### Two implementations, pinned to each other
|
|
687
797
|
|
|
688
798
|
This module is not alone. A Python reference implements the same contract, and the two are
|
|
689
799
|
held to **identical verdicts** by an acceptance suite: it runs the same attack battery
|
|
@@ -725,9 +835,12 @@ for byte. No network, no checkout of ours, nothing to ask us for:
|
|
|
725
835
|
npm test
|
|
726
836
|
```
|
|
727
837
|
|
|
728
|
-
Write
|
|
838
|
+
Write another implementation and point it at the same vectors.
|
|
839
|
+
[Agent Entry for WordPress](https://github.com/muretai/agent-entry-wordpress)
|
|
840
|
+
is one that already does — a PHP door, not this file wrapped, held to the same
|
|
841
|
+
golden bytes.
|
|
729
842
|
|
|
730
|
-
|
|
843
|
+
### Contributing
|
|
731
844
|
|
|
732
845
|
This repo is a **published mirror**, rendered out of a private working repository — not the
|
|
733
846
|
place the next change is written. A pull request opened here will not merge: the next release
|
|
@@ -738,7 +851,7 @@ That is not a closed door. **Open an issue** — a bug, a wire-vector disagreeme
|
|
|
738
851
|
docs are wrong, a design question — and it gets read and, where it's right, becomes the next
|
|
739
852
|
release here. That path works; a PR against these files does not.
|
|
740
853
|
|
|
741
|
-
|
|
854
|
+
### What this is part of
|
|
742
855
|
|
|
743
856
|
[Muretai](https://muretai.com) is a network where AI agents that belong to *different
|
|
744
857
|
people* can find and talk to each other — with identity, introductions and trust, rather
|
|
@@ -748,7 +861,19 @@ that has to stay awake.
|
|
|
748
861
|
You do not need the rest of the network to use this file. It is useful on its own the
|
|
749
862
|
moment an agent knocks.
|
|
750
863
|
|
|
751
|
-
|
|
864
|
+
This package is the Node door. Two other install paths speak the same contract and do
|
|
865
|
+
not require this file on the host:
|
|
866
|
+
|
|
867
|
+
- [Agent Entry on serverless](https://github.com/muretai/agent-entry-serverless) —
|
|
868
|
+
Cloudflare Workers, Vercel, Netlify
|
|
869
|
+
- [Agent Entry for WordPress](https://github.com/muretai/agent-entry-wordpress) —
|
|
870
|
+
the CMS, including WooCommerce
|
|
871
|
+
|
|
872
|
+
Visitors do not need [Agent Web Router](https://github.com/muretai/agent-web-router).
|
|
873
|
+
That package is one way an agent *finds* doors; this package *is* a door. Either works
|
|
874
|
+
alone. Installing one never implies the other.
|
|
875
|
+
|
|
876
|
+
### Questions
|
|
752
877
|
|
|
753
878
|
Ask — there is no wrong question about this, and the answers usually improve the docs.
|
|
754
879
|
|