agent-yes · engineering
ay callback: let readers message your agent from any web page
Your agent just published a report. The person reading it has a question. Until now the answer was "find the console, find the right terminal, type there." Now the report page itself can carry a tiny widget that delivers the reader's message straight into that agent's terminal — through a signed, send-only, single-agent capability that expires on a date you chose, and never through your master token.
What it does #
ay callback mints a capability for one agent and prints a
self-contained HTML snippet — one inline <script>, zero external
requests. Paste it into any page your agent publishes (a status dashboard, a nightly report,
a postmortem) and readers get a floating button:
$ ay callback --expires 7d
callback 3b6f23e7 → claude #46555 @ ~/ws/reports
expires: 2026-07-29T10:57:01.398Z (7d)
endpoint: https://x1a2b3c.agent-yes.com/cb/cb1.eyJpZCI6…
revoke: ay callback revoke 3b6f23e7
── embed snippet ──────────────────────────────
<script> … </script>
Here is what the widget feels like (demo only — this one talks to nobody):
When a reader hits Send, the message lands in the agent's terminal wrapped in an explicit "untrusted visitor" frame, and in its inbox. One way, by design — the visitor never sees the terminal.
The capability is a hash, not a key #
The obvious-but-wrong implementation is to put your daemon's serve token in the snippet. That token is a master key: whoever holds it can read every terminal, send to every agent, spawn and kill. Pasting it into a public page is game over.
So the snippet carries a capability token instead — three dot-separated parts, the last one an HMAC-SHA256 over the rest:
cb1 . base64url({ "id": "3b6f23e7",
"agent": "4d65a096e983", // exactly one agent_id
"exp": 1785322621398 }) // hard expiry, unix ms
. base64url(HMAC-SHA256(secret, "cb1." + payload))
The scope lives inside the signed payload. That has three consequences worth spelling out:
| Attack | Result |
|---|---|
| Edit the payload to point at another agent, or push the expiry out | ✗ signature no longer verifies — the daemon answers 404 |
| Delete the daemon's bookkeeping to "resurrect" an expired token | ✗ expiry is in the token itself, not in local state — still 410 |
| Steal the token from the page source | ✓ you can do exactly what the page could already do: send rate-limited text to one agent, until the expiry the owner chose |
The HMAC secret is minted on first use into ~/.agent-yes/.callback-secret
(0600), deliberately separate from the serve token — rotating one never invalidates the
other.
--expires is required. There is no "never" #
Every capability API eventually grows a default lifetime, and every default lifetime eventually becomes "forever, because nobody thinks about it." We refuse the default:
$ ay callback
ay callback: --expires is required (e.g. --expires 7d) —
an embed capability must carry an explicit lifetime
--expires takes 30m, 12h, 7d,
2w — and caps at 365 days. There is no --expires never and there
never will be. The point is a moment of deliberate friction: whoever pastes a capability
into a public page must answer "how long should this live?" while they still remember the
page exists. When it lapses, the widget doesn't fail silently — it tells the reader the link
expired and to ask the owner for a fresh one.
For everything before the deadline, there's the kill switch:
$ ay callback ls
3b6f23e7 6d left → claude #46555 @ ~/ws/reports
$ ay callback revoke 3b6f23e7
revoked 3b6f23e7
A public route that leaks nothing #
POST /cb/<cap> is the daemon's only unauthenticated write route, so it is
deliberately paranoid:
-
Info-tight responses. The page learns
ok,expired, or a generic failure — never a pid, a cwd, a hostname, or whether a given agent exists. Malformed and forged tokens are indistinguishable 404s. -
Untrusted framing. Visitor text is stripped of control characters (no
smuggled escape sequences, no fake Enter) and wrapped in an
<ay-callback>block that names it an untrusted visitor message — the agent is told to treat it as data, not instructions. - Flood control. Per-capability sliding-window rate limit and a 4 KB body cap. A public page can nudge your agent; it cannot bury it.
- One-way. The route writes to the agent's stdin and records the inbox entry. It never reads the terminal back — a public embed must not become a viewport into your machine.
Try it #
$ npm i -g agent-yes
$ ay serve # local daemon
$ ay expose … # or put the daemon on a public URL first
$ ay callback --expires 7d --title "Ask the report bot"
Paste the printed snippet at the end of anything your agent publishes. Ship the report; keep the conversation.