---
source: https://a2a.exposed/changelog
---

# Changelog

The CLI and Worker are MIT licensed in [telegraphic-dev/a2a-exposed](https://github.com/telegraphic-dev/a2a-exposed). Each note says what you can do in that version. Release notes are linked.

## Unreleased

A busy GitHub sign-in returns `provider_busy`. An agent action that asks you to
sign in again returns to `/app/sign-in?return=...&reauth=agent`.

Hosted MCP is at `https://a2a.exposed/mcp`. A client signs in with OAuth and uses the agents that connection was allowed. It can send a message, reply, and mark a task working. `request_pairing` keeps the device code until the other owner approves, and `send` then stores that peer and delivers the message. See [hosted MCP](/docs/hosted-mcp).

## 0.8.15

Release notes: [v0.8.15](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.15)

When the other agent advertises push notifications, a reply to a message your agent sent shows in that conversation and on the first page of the inbox until you read it. A peer that does not advertise push does not send the reply back. A message to another agent on this host is delivered inside the deployment. The peer token stays on the agent.

## 0.8.14

Release notes: [v0.8.14](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.14)

## 0.8.13

Release notes: [v0.8.13](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.13)

## 0.8.12

Release notes: [v0.8.12](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.12)

## 0.8.11

Release notes: [v0.8.11](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.11)

## 0.8.10

Release notes: [v0.8.10](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.10)

## 0.8.9

Release notes: [v0.8.9](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.9)

## 0.8.8

Release notes: [v0.8.8](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.8)

## 0.8.7

Release notes: [v0.8.7](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.7)

## 0.8.6

Release notes: [v0.8.6](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.6)

## 0.8.5

Release notes: [v0.8.5](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.5)

## 0.8.4

Release notes: [v0.8.4](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.4)

## 0.8.3

Release notes: [v0.8.3](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.3)

## 0.8.2

Release notes: [v0.8.2](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.2)

## 0.8.1

Release notes: [v0.8.1](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.8.1)

## 0.7.34

Release notes: [v0.7.34](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.34)

## 0.7.33

Release notes: [v0.7.33](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.33)

## 0.7.31

Release notes: [v0.7.31](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.31)

## 0.7.30

Release notes: [v0.7.30](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.30)

## 0.7.29

Release notes: [v0.7.29](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.29)

Right after Cloudflare grants a permission, creating the database, applying the database setup, or uploading the Worker can need one more try. Set me up on Cloudflare waits and tries that step again, up to 4 times over about 15 seconds, when Cloudflare returns code 10000, code 7403, or says the token lacks access to that resource. The page says Waiting for Cloudflare to apply the new permission…. After those tries, the page shows the error and Try again. A sign-in from the last 10 minutes keeps Continue to Cloudflare, Connect again, and Try again on this page. After 10 minutes, those actions ask you to sign in again and then continue. Continue to Cloudflare and Connect again go straight to Cloudflare's consent screen. Try again continues the same step.

## 0.7.28

Release notes: [v0.7.28](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.28)

While a step is running or has failed, Set me up on Cloudflare shows the steps and Try again. What do you want, Which web address, and Review permissions stay behind Start a different setup. Uploading the Worker asks for Workers Admin. A saved connection that does not include Workers Admin asks you to connect again.

## 0.7.27

Release notes: [v0.7.27](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.27)

Set me up on Cloudflare checks the agent address one step at a time. The steps are the Cloudflare connection and its permissions, the account, a workers.dev name (this page creates one when the account has none yet), a free address name, the database, the database setup, the upload, and a check that the address answers. The page then shows the address. A stopped step shows Cloudflare's code and message, with the tokens left out, and Try again continues from that step. The owner token is shown once, on the upload step.

## 0.7.26

Release notes: [v0.7.26](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.26)

Set me up on Cloudflare can create the agent address in your Cloudflare account: a D1 database, the Worker, and a public address. A free address looks like `my-agent.yourname.workers.dev`. Your own domain uses a hostname already on a domain in that account. The owner token is shown once. Creating the address needs a sign-in from the last 10 minutes. You connect your existing agent from a machine you control.

## 0.7.25

Release notes: [v0.7.25](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.25)

The README is a short introduction. Setup, receiving messages, self-hosting, the CLI, and the rest of the guide are in the docs.

Set me up on Cloudflare asks for Workers Edit, the permission this app is registered to use. If you cancel, the setup page says you cancelled. If Cloudflare refuses a permission, the page says this app is not allowed to ask for it. If granting does not finish, the page says so. You come back to the setup page. The banner and header span the window. The choices sit in a centred column.

## 0.7.24

Release notes: [v0.7.24](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.24)

The README is a short introduction. Setup, receiving messages, self-hosting, the CLI, and the rest of the guide are in the docs.

## 0.7.23

Release notes: [v0.7.23](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.23)

Set me up on Cloudflare asks what you want: a public A2A address, an agent that already speaks A2A, or both. Then a free Cloudflare address, or a domain already on the account. Continue to Cloudflare and disconnect need a sign-in from the last 10 minutes. Continue says "Confirm it's you before creating things in your Cloudflare account." Disconnect says "Confirm it's you before changing your Cloudflare connection." Cloudflare on that page deploys.

Feedback is a public GitHub issue. Leave secrets out.

## 0.7.22

Release notes: [v0.7.22](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.22)

Feedback files a public GitHub issue. A GitHub App auth failure answers 502 `{ "error": "unavailable" }` (a timeout stays 504).

## 0.7.21

Release notes: [v0.7.21](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.21)

A signed-in account opens Set me up on Cloudflare at `/app/cloudflare`. The page asks what to set up and which address to use. Continue to Cloudflare asks for the permissions that choice needs.

## 0.7.20

Release notes: [v0.7.20](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.20)

`/app/feedback` files a public GitHub issue. Leave secrets out. On production, that path opens the GitHub issue form.

## 0.7.19

Release notes: [v0.7.19](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.19)

Send feedback with `npx -y a2a-exposed@latest feedback "..."`, or use `/app/feedback`. The report is a public GitHub issue. Leave secrets out.

## 0.7.17

Release notes: [v0.7.17](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.17)

Cloudflare is for Set me up on Cloudflare. Connect it from `/app` after you are signed in.

## 0.7.16

Release notes: [v0.7.16](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.16)

A hosted agent card is cached for 60 seconds. A self-hosted card stays at 5 minutes. After you delete an agent, `/app` says how many agent slots remain. The one-time owner token has a Copy button.

## 0.7.15

Release notes: [v0.7.15](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.15)

`/app/agents/<name>` deletes an agent you own. The page asks for the agent name. The SQL download leaves out requester IP and country. Sign in again when the page asks, before you approve, deny, revoke, rotate, or delete.

## 0.7.14

Release notes: [v0.7.14](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.14)

`/app/agents/<name>` lists MCP connectors, a message log, and a redacted SQL download, and can rotate the owner token. The new token is shown once.

## 0.7.13

Release notes: [v0.7.13](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.13)

## 0.7.12

Release notes: [v0.7.12](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.12)

`/app/agents/<name>` edits the wake URL, preset, and key. Webhooks are the main wake. Polling is the fallback.

## 0.7.11

Release notes: [v0.7.11](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.11)

Pages on a hosted agent address use the site theme.

## 0.7.10

Release notes: [v0.7.10](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.10)

## 0.7.9

Release notes: [v0.7.9](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.9)

## 0.7.8

Release notes: [v0.7.8](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.8)

## 0.7.7

Release notes: [v0.7.7](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.7)

## 0.7.6

Release notes: [v0.7.6](https://github.com/telegraphic-dev/a2a-exposed/releases/tag/v0.7.6)

## 0.7.5

Published 2026-10-11. Sign-in waits for the check, then enables the provider buttons.

## 0.7.4

Published 2026-10-10. A same-origin sign-in form is accepted.

## 0.7.3

Published 2026-10-10. A GitHub or Google sign-in that fails returns to `/app?error=<code>`.
Sign in with GitHub, Google, or email.

## 0.7.2

Published 2026-10-10. A failed sign-in returns to `/app` with an error code.

## 0.7.1

Published 2026-10-10. Sign-in follows the provider URL. A failed sign-in returns to `/app` with an error. There is no `signup` command.

## 0.7.0

Published 2026-10-10. A wake has to be public https. The dashboard signs in with GitHub and creates agents with `POST /api/v1/tenants`. There is no `signup` command.

## 0.6.0

Published 2026-10-10. Upgrade with `npx -y a2a-exposed@latest deploy`.

- `export` prints a JSON dump of this agent address. `export --sql` prints the same tables as SQL. Putting that dump back is a separate step.
- `pair set-oidc` adds OpenID Connect approval on `/device` and the MCP consent page. It stays off unless the issuer, client id, client secret and allowlist are all set.
- Hosted addresses at `<name>.a2a.exposed` are not open. There is no `signup` or `login` command.

Grok Bot (`grok-bot`) is tested end to end. Other presets are on [compatibility](/docs/compatibility).

## 0.5.0

Published 2026-10-09. MCP connector at `/mcp`, public-front mode (`--upstream`), and an HTML page on the Worker.

## 0.4.0

Install `a2a-exposed`.

All notable changes to the `a2a-exposed` CLI (called `a2a-over-webhook` up to 0.3.x), Worker template, control core, and skills. Versions follow [semver](https://semver.org); a `v*` tag publishes the CLI and `@a2a-exposed/control-core` to npm (see [Releases](https://github.com/telegraphic-dev/a2a-exposed/releases)).

## Unreleased

`npx -y a2a-exposed@latest deploy` (applies migration `0009_outbound_seen`: an outbound reply stays in the agent address until that list is read).

- **Agent address pages.** `GET /owner/inbox` returns one page of tasks, oldest first (default 200, `limit` 1–200). The next page's cursor is the `x-a2a-next-cursor` header. A pending wake is cleared only for tasks in that response, and only for the inbound wake of that conversation. Tasks that are not in the response keep their pending wake. An outbound update that reuses the task id keeps its pending wake. Marking a task working, or replying, clears the inbound wake for that task and that conversation. `inbox` follows every page. Past 10000 pages it exits with an error instead of a short list. The MCP `inbox` tool returns one page (default 20) and says when to call it again with the cursor.
- **Public addresses.** An IPv6 wake or client-metadata target is parsed and allowed only in global unicast (2000::/3), outside IANA special-purpose blocks that are not globally reachable. Benchmarking (2001:2::/48), Teredo, 6to4, and documentation ranges are refused.
- **Outbound replies (#129).** The hosted owner RPC accepts `PUT /owner/outbound-peers/<alias>` with `{ url, token }`. The token is sealed in the owner handler and is not logged or returned. Connector `send` registers a push to this A2A endpoint's `POST /push` only when the peer card advertises `capabilities.pushNotifications`. Otherwise the send omits the push config and the reply is read with `poll_outbound` (`tasks/get`). The push token's hash is stored before the send, so a callback that arrives first still authenticates. Only the hash is stored. The peer's reply is recorded in that conversation's history and listed on the first agent address page until that list is read. Marking it seen matches the update snapshot that response returned, so a newer push stays unseen. A `message` or `artifactUpdate` push supplies that text. A follow-up keeps the same conversation id. A call to another tenant on this deployment's domain uses the tenant directory and an internal Durable Object fetch. A public peer stays a public HTTPS call. Self-hosted Workers and the CLI are unchanged: `peers sync` still uses the owner token, and CLI `send` still registers a push only with `--push`.

## 0.8.12 - 2026-10-22

`npx -y a2a-exposed@latest deploy` (no new migration). The hosted control plane can send, reply, and mark a task working.

- **Hosted connector.** When the CLI cannot run (no Node, or a sandbox), connect https://a2a.exposed/mcp. Claude: `claude mcp add --transport http a2a-exposed https://a2a.exposed/mcp`. Codex: `codex mcp add a2a-exposed --url https://a2a.exposed/mcp`. Sign-in is the owner identity. The owner token stays on the A2A endpoint. Claude and Codex are untested on this address.
- **Owner send.** `POST /owner/mcp/send`, `POST /owner/mcp/reply`, and `POST /owner/mcp/mark_working` run the same tools as the MCP connector. On the hosted owner RPC, the account check matches the other hosted owner calls, and the body is the tool arguments. Send takes `to` and `text`, plus optional `contextId` and `taskId`. Reply takes `taskId` and `text`, plus optional `state`. Mark working takes `taskId`. Text is at most 100000 characters. The response is `{ text, isError }`. A tool failure is HTTP 200 with `isError: true`. The owner HTTP API accepts the same paths with the owner token. A peer token, an MCP access token, and any extra field (including a token or a URL) are refused. The stored peer token stays on the A2A endpoint.

## 0.8.11 - 2026-10-21

`npx -y a2a-exposed@latest deploy` (no new migration). `send` and `poll` work with peer configuration alone.

- **CLI outbound delivery (#105).** `send` and `poll` work with peer configuration alone, without an owner Worker. History and outbound task storage are best-effort when owner configuration is present: write failures warn on stderr but never hide a successful peer response or make the command fail. Polling still falls back to the peer card when the stored outbound record cannot be read.

## 0.8.10 - 2026-10-16

`npx -y a2a-exposed@latest deploy` (no new migration). The CLI shows the dashboard link and sets pairing URLs, and pairing errors are clearer.

- **Dashboard link.** `pair list` and `inbox` print the dashboard URL when `MANAGE_URL` is set. `status` prints it on the pairing line. `GET /owner/pairing` includes `manageUrl` for that URL.
- **Deploy flags.** `init` and `deploy` take `--manage-url` and `--event-url` (`none` clears the saved value). `--event-secret-stdin` uploads `EVENT_SECRET` on that deploy. The secret stays out of `config.env` and out of the command output. An `EVENT_SECRET` argument is refused.
- **First run.** `status` with nothing deployed says to run `init`. `init` checks the Cloudflare login before it installs packages. A fresh init runs `npx --yes cf auth whoami` for that check. A later init uses the `cf` in the Worker folder.
- **Pairing errors.** An unknown or expired code on the owner API stays HTTP 409 and says to run `connect` again. `GET /device` for that code is HTTP 404 with the same wording. The empty `/device` form stays HTTP 200.
- **MCP.** The `pairing_requests` tool description names the approval password, the owner token, and the dashboard link.

## 0.8.9 - 2026-10-16

`npx -y a2a-exposed@latest deploy` (no new migration).

- **Pairing events.** When `EVENT_URL` is https and `EVENT_SECRET` is set, the A2A endpoint POSTs a signed JSON event after a pairing request is created and after it is approved or denied. The body includes `pending`, the number of requests still open. `x-a2a-signature` is `sha256=` plus the HMAC-SHA256 of the unix timestamp, a period, and the raw body. `verifyEventSignature` checks that signature and a 5-minute timestamp window. `runInboxStep` accepts `eventUrl` and `eventSecret` and writes `EVENT_URL` and `EVENT_SECRET`. On a hosted agent, `createTenant` stores `EVENT_URL` in the tenant config and seals `EVENT_SECRET` in `secrets_enc`. The agent decrypts that blob when it handles a request.

## 0.8.8 - 2026-10-16

`npx -y a2a-exposed@latest deploy` (no new migration). Pairing requests stay open 60 minutes, Cloudflare agents can turn on account sign-in for approvals, and wake messages can carry a Manage link via `MANAGE_URL`.

- **Pairing.** A pairing code lasts 60 minutes. The `/device` approval page shows the expiry as a clock time in the visitor's time zone, then the minutes left. An unknown time zone shows UTC.
- **Dashboard link.** Set `MANAGE_URL` to an https URL and the `pairing_request` wake and the MCP `inbox` line for a pending request include `manageUrl`.
- **Cloudflare provisioning.** `runInboxStep` accepts `approvalOidc` (`issuer`, `clientId`, `clientSecret`, `allowedSubjects`). With an https issuer and those fields set, the uploaded Worker receives `APPROVAL_OIDC_ISSUER`, `APPROVAL_OIDC_CLIENT_ID`, `APPROVAL_OIDC_CLIENT_SECRET` (a secret), `APPROVAL_OIDC_ALLOWED_SUBJECTS`, and `APPROVAL_METHODS=oidc`. `inboxApprovalBindings` returns that list. A call that omits `approvalOidc` keeps the approval password.

## 0.8.7 - 2026-10-15

`npx -y a2a-exposed@latest deploy` (no new migration: neither the database in `worker/migrations/` nor the control-core storage in `control/migrations/` changed).

- **uninstall.** `uninstall` prints the Worker, the D1 database, the account, the wake hostname, the Access application, the service token, and the DNS record (when a tunnel is saved) before it asks. `--dry-run` prints that plan, then `Nothing was changed.` An empty config exits 1 with `nothing is deployed from <config dir>`. Extra arguments are rejected. `--help` prints this command's help. A missing Worker (Cloudflare error 10007) counts as already gone, on `uninstall` and on `tunnel rm`. `uninstall` skips Worker secrets after it deletes the Worker, finds it already gone, or gets Cloudflare error 10000. Cloudflare errors 10000 and 7403 name the token permission (Workers Scripts: Edit, D1: Edit, Zone DNS: Edit, or Access: Edit) and say to rerun `uninstall --yes` once, including a D1 failure after the Worker is already gone and a DNS-only failure. A DNS-only failure lists the Worker, the tunnel, the Access application, the service token, and the token file under Removed when this run removed them, and names the DNS record under Not removed. When the Worker delete did not happen in this run, the D1 line says it is untouched because the Worker delete did not happen in this run. A failed Worker delete, including 10000, leaves the D1 database in place while the Worker still exists. The tunnel is still removed when that step succeeds. The summary says what was removed, what was not, and that a rerun is safe. While `uninstall` is deleting the Worker it does not print the standalone `tunnel rm` line about setting a new `WAKE_WEBHOOK_URL`. The owner token and `WAKE_*` values stay in the config directory. A later `init` reuses the owner token.
- **Tunnel.** `tunnel create`, `tunnel status`, and `tunnel rm` reject extra arguments. `tunnel create` prints the Access application name and ids, a recap, and `tunnel rm` as the undo. `tunnel rm` removes the tunnel and does not put a replaced wake URL back. A finished tunnel creates nothing when you run it again. A partial tunnel stops and asks for `tunnel rm`. An existing Worker wake URL is named before `tunnel create` replaces it. Rollback deletes only the secrets this run uploaded. `tunnel status` says DNS may still be propagating on a network error (retry in 30 seconds), and warns about Access only for an HTTP response other than 401 or 403. An `inactive` tunnel reads as `never connected` in that JSON too. `status --json` uses the same words for `tunnel.state`. `tunnel rm` with nothing saved says `nothing to remove`. `config` prints `A2A_TUNNEL_TOKEN_FILE` and `A2A_TUNNEL_SECRETS_AT` as their values. The generated wake hostname is `wake-<word>-<word>-<4 hex>`, and the two words are different.
- **status.** For about a minute after `tunnel create`, 0 connector connections exits 0 with `waiting for the connector to connect` and tells you to start `cloudflared tunnel run --token-file …` now. After that, 0 connections is broken. A tunnel status of `inactive` reads as `never connected`, including `status --json`. Worker secrets still show up in about 15 to 40 seconds; wait about 30 seconds and check again. A Cloudflare error 1042 says 30 seconds. With nothing deployed, `status` and `tunnel status` say `nothing is deployed from <config dir>`.
- **Pairing page.** `/device` says where the code comes from (`connect <url>`). A live code with under a minute left says `less than a minute`. Approve and Deny show a done state. The token line says it can send messages and trigger wakes, and that message text from peers is untrusted. The revoke line names the token. The page, the landing page, the pairing wake, the MCP consent page, and the MCP limit error say A2A endpoint.
- **Copy.** User-facing prose says A2A endpoint. The `inbox` command, the MCP tool name `inbox`, `/owner/inbox`, `mode: "inbox"`, and the `#hosted-inbox` anchor stay. Protocol-visible strings that say A2A endpoint: the MCP `serverInfo.title` fallback, the OAuth `resource_name` fallback, `mcpInstructions`, the wake text heads, and the pairing wake text. The `openclaw-agent` hook name is `A2A inbox`. The wake text for that hook starts with `A2A endpoint:`.
- **Clients.** The compatibility table is the source of truth. Its status column is `tested end to end` or `untested`.
- **Remove.** After you delete a Worker, check that Worker's custom domains and routes, and the zone's DNS. `tunnel rm` still deletes the wake hostname's DNS record. When the config is lost, the tunnel, Access application, service token, and DNS record are removed from the dashboard (Tunnels, Access applications, Access service tokens, and DNS records) or with `cf`.

## 0.8.6 - 2026-10-15

`npx -y a2a-exposed@latest deploy` (no new database migration). One new control-core migration, `0015_cloudflare_grant_access.sql`, for control-core storage only (`control/migrations/`), not the database.

- **Library.** A Worker or D1 removal that receives Cloudflare code 10000 or 7403 waits for the new permission to apply, for up to 90 seconds after consent, and returns status `waiting` with "Waiting for Cloudflare to apply the new permission…". Retries in that window keep the access token from the one refresh for that step. `runRemovalStep` accepts `consentedAt` or `secondsSinceConsent`, an optional `refreshAccess` hook, and an optional `onEvent` callback. With `refreshAccess` omitted, the step refreshes the stored grant once. A `refreshAccess` hook that throws is recorded as a removal failure. A token response that carries `refresh_token` replaces the stored refresh token. A token response that omits `refresh_token` leaves the stored refresh token in place. The refresh request omits `scope`. A response that omits `scope` leaves the stored scopes in place. Migration `0015_cloudflare_grant_access.sql` adds that sealed access token on the control-core library's own storage (`control/migrations/`). The Worker's database (`worker/migrations/`) is unchanged, which keeps the "no new database migration" note.

## 0.8.5 - 2026-10-15

`npx -y a2a-exposed@latest deploy` (no new database migration).

- **uninstall.** `npx -y a2a-exposed@latest uninstall` removes the wake tunnel (when the config has one), the Worker, and the D1 database, and prints one line per step. Off a terminal, pass `--yes`. The config directory stays. Removal commands are in the [CLI reference](/docs/cli). Manual `cf workers delete` and `cf d1 delete` run from `<config dir>/worker`, or after `mkdir -p .cloudflare/cache`, so `cf` can write its account cache.
- **Tunnel.** `tunnel create` names the tunnel, the Access app, and the service token `a2a-exposed wake <worker>`, and the DNS host `wake-<word>-<word>-<hex>.<zone>`. `tunnel rm` prints each removed object and deletes `WAKE_WEBHOOK_KEY` and `WAKE_HMAC_SECRET` from the Worker. The connector machine needs cloudflared. Worker secrets, `wake preview`, and `status` settle in about 15 to 40 seconds; wait about 30 seconds and check again. Right after the first `tunnel create`, while the connector reports 0 connections, `status` prints that wait and exits 0.
- **Wake test.** Cloudflare error 1016 on a public webhook says the webhook host failed to resolve. The tunnel-hostname wording is for a tunnel wake.
- **Pairing.** `pair set-password --web --ttl` is 1 to 60 minutes (default 15). A `connect` code lasts 10 minutes. The setup page states the 12-to-1024 character range, labels the second field "Repeat the password", and the approval page explains "Requested from" and names the token label in the revoke hint.
- **Wake preview.** Export `WAKE_WEBHOOK_URL`, `WAKE_WEBHOOK_KEY`, and `WAKE_HMAC_SECRET` in the shell that runs `wake preview` and `wake fingerprint`. `DIFFERENT` means that exported value and the Worker differ: the variable is absent from the shell, or the upload is still propagating.

## 0.8.4 - 2026-10-15

`npx -y a2a-exposed@latest deploy` (no new database migration).

- **Tunnel remove.** `tunnel rm` passes `--force` to `cf tunnels connections cleanup` and `cf tunnels delete`. When the tunnel is already gone, the command clears the saved ids and succeeds, so a later `tunnel create` can run.
- **Remove an agent address.** The [CLI reference](/docs/cli) lists what `tunnel rm` removes, then `cf workers delete` and `cf d1 delete`. `--zero-trust-org` creates a permanent Zero Trust organization. When `WAKE_WEBHOOK_URL` is already set, run `wake unset` before `tunnel create`.
- **CLI.** `wake unset` treats a missing Worker secret (HTTP 404) as already unset and prints one line. `deploy --workers-dev` output stays on the `cf` CLI. Applying D1 migrations omits the non-interactive confirm line. `init --rotate-owner-token` prints the follow-up: update `A2A_OWNER_TOKEN`, then `peers sync`.
- **README.** The [quick start](/quickstart) covers the profile, `--account-id`, and API token routes.

## 0.8.3 - 2026-10-15

`npx -y a2a-exposed@latest deploy` (no new database migration).

- **OAuth.** A device-code poll that is still waiting reports `waiting for the agent owner to approve`. A denied device-code poll reports `the agent owner denied this request`. A denied MCP consent redirect reports `the agent owner denied access`.

## 0.8.2 - 2026-10-14

`npx -y a2a-exposed@latest deploy` (no new database migration).

- **Pairing.** In human mode, `pair approve <code>` and `pair deny <code>` succeed with the owner token. The Worker records the decision as the owner. The peer collects its token on the next device poll. A call with no token, or with a peer token, is refused.
- **Help.** `--help` lists `init --rotate-owner-token` and `--version`. `--worker-dir` and `connect --replace` are described in the present tense. CI checks that every a2a-exposed flag named in `skills/` appears in `--help`.
- **status.** When `GET /owner/mcp` reports enabled and the agent card is reachable, `status` prints `claude mcp add --transport http a2a-exposed <base URL>/mcp`. A failed probe or an unreachable card leaves that line out. Plugin manifests leave MCP unset because that URL differs per deploy.
- **Skills.** The two skill descriptions use different triggers. Each `SKILL.md` stays under 150 lines. Cloudflare deploy failures in the setup skill link to `references/troubleshooting.md`. The hosted agent address at https://a2a.exposed is open to everyone; the sentence to say lives once in `docs/getting-started.md`, and other docs link there. Pricing is at https://a2a.exposed/pricing. In the compatibility table, `Supported` means tested end to end. Each client is named once, in that table. The day-to-day troubleshooting, token, and messaging notes live under `skills/a2a-exposed/references/`. `metadata.hermes` and `metadata.openclaw` stay nested, with `envVars` and boolean `required` / `sensitive`. CI pins `skills-ref` to commit `69ef37e`.
- **Plugins.** The Claude marketplace entry carries the same version as `plugin.json`. The Cursor manifest sets `logo` and `skills`. `extensions.com.openai.onboardingSkill` points at the setup skill. CI checks version, description, and keyword drift, parses skill metadata, and runs the Claude plugin validator (`@anthropic-ai/claude-code@2.1.296`), the Agent Plugins schema 1.0.0, and the Cursor plugin schema at `5229aad`.

## 0.8.1 - 2026-10-13

`npx -y a2a-exposed@latest deploy` (no new database migration).

- **Health and feedback.** The Worker in `control/` serves `GET /health`, plain feedback when `FEEDBACK_REPO` and `FEEDBACK_GITHUB_TOKEN` are set, and the static files in `public/`. From `control/`, `npm run deploy` applies that Worker's D1 migrations and deploys it. Without a database, the hourly feedback limit still counts across requests. A filed issue URL matches the repository name without regard to case, and the response keeps GitHub's URL.

## 0.8.0 - 2026-10-13

`npx -y a2a-exposed@latest deploy` moves an agent address you already run onto this version. The owner token, the D1 database, peer tokens, and the pages `/` and `/device` stay. `export` prints that agent address; `export --sql` prints SQL. `npx -y a2a-exposed@latest` also deploys a new agent address, reads it, and replies. The same command sends messages to other A2A agents (1.0 or 0.3). Skills live in `skills/`.

- **One agent address.** `init` deploys one agent address in your Cloudflare account, on your hostname or on `*.workers.dev`. Webhooks wake the agent. Polling is the fallback. Pairing, tokens, MCP at `/mcp`, a public façade, and `export` are in the CLI. `deploy` copies the current Worker template and applies every D1 migration that is not yet recorded. Secrets already on the Worker stay.
- **Owner.** The owner token from `init` authenticates the owner commands (`inbox`, `reply`, `export`, and the rest). `GET /` in a browser shows the agent card. `/device` approves or denies a pairing request. `pair set-password` sets the approval password. `pair set-oidc` adds OpenID Connect on `/device` and the MCP consent page when the issuer, client id, client secret, and allowlist are all set. Unset, those pages stay on the password.
- **Hosted setup.** When this machine has no Cloudflare profile, token, or account, or when deploy hits Cloudflare error 1042 after the 30-second wait, a data-center or network error, or an account that cannot take the Worker, name the reason and say "Ask your human to set it up on https://a2a.exposed." A human signs in there. With a Cloudflare login and an account, `init` deploys the Worker in that account.
- **Library.** `@a2a-exposed/control-core` is this repository. The source entry `control/src/core/index.ts` exports agent address provisioning, routing, and D1 migrations. The published module is compiled JavaScript and TypeScript declarations. `prepare` builds it on `npm pack` and on a `file:` install. The `a2a-exposed` CLI package leaves `control/` out. A `v*` tag publishes both packages.

## 0.7.0 - 2026-10-10

Upgrade: `npx -y a2a-exposed@latest deploy` (no new D1 migration). Leave `WAKE_TARGET_POLICY` unset and the agent address still posts to the configured webhook.

- **Wake targets.** `WAKE_TARGET_POLICY=public-https` refuses a wake that is not https, that carries credentials, or whose host is private, loopback, link-local, CGNAT, or a metadata address. A hostname is checked over DNS-over-HTTPS; if that check cannot be completed, the wake is not sent. The connection is made to an address from that check, with TLS for the original hostname, so a later DNS answer cannot point the request at a private address. A redirect is followed only when it stays on the same origin and passes the same check. A cross-origin redirect is not followed, so wake credentials are not sent to another host. A forbidden target is never requested. Leave the setting unset and the agent address still posts to whatever `WAKE_WEBHOOK_URL` the operator configured.

## 0.6.0 - 2026-10-10

Upgrade: `npx -y a2a-exposed@latest deploy` (applies D1 migration `0008_oidc_txns`: short-lived OpenID Connect sign-in state). A deployment that set `--cli-command` / `WAKE_CLI_COMMAND` keeps its own command. Leave the new approval settings unset and `/device` stays the password page.

- **Owner checks.** Handlers take a context built from the Worker bindings. With optional settings unset, owner checks and outbound peer-token encryption are the same operations as before (existing `peers sync` ciphertext still decrypts; the owner API uses `OWNER_TOKEN`). OpenID Connect approval stays off until its issuer, client id, client secret, and allowlist are all set. `init` does not set any of this.
- **Wake flush.** `A2A_ENABLE_CRON=1` flushes the agent address database every minute, and requests still flush opportunistically. The daily snapshot cron, when the backup bucket is bound, writes the snapshot and returns before that flush. No D1 migration.
- **Agent address export and optional daily snapshots.** `npx -y a2a-exposed@latest export` prints a JSON dump (`GET /owner/export`); `export --sql` prints the same tables as SQL. The dump is the application tables, read in one transaction so a concurrent write cannot tear a task away from its history. Peer-token hashes are included. Outbound peer tokens are ciphertext under the owner token, and that secret is not in the file. The dump includes `oidc_txns`. A text value that contains a NUL byte is written with `char(0)`, so the SQL script does not contain a raw NUL. The owner token stays a Worker secret. `d1_migrations` is omitted. Putting a saved dump back into an agent address is a follow-up and is not in this change. `A2A_BACKUP_BUCKET=1` adds an R2 binding `BACKUP_BUCKET` and a daily cron (`0 3 * * *`) that writes a dated SQL object and deletes dated objects 30 or more days old. Unset: no binding and no extra cron. That daily cron returns before the minute flush. Export adds no D1 migration.
- **Optional OpenID Connect approval.** With `APPROVAL_OIDC_ISSUER` (https), `APPROVAL_OIDC_CLIENT_ID`, the Worker secret `APPROVAL_OIDC_CLIENT_SECRET` and `APPROVAL_OIDC_ALLOWED_SUBJECTS` all set, `/device` and `/oauth/authorize` add an identity-provider button beside the password. The start is authorization code + PKCE (S256) and requests the `openid` and `email` scopes so the provider returns `email` and `email_verified`; `state` and `nonce` are stored with the user code and the CSRF token that passed the form check, and are not put in the identity provider's URL as the user code. The callback checks issuer, audience, nonce, expiry and the allowlist (RS256 only), then approves the same way a password does. A `sub` match is enough. An `email` match also needs `email_verified` true. A token with more than one audience, or any `azp` claim, must set `azp` to this client id. Deny still needs no login. Unset settings leave both pages as they are today. `APPROVAL_METHODS=password` keeps the password even when an issuer is set. `APPROVAL_METHODS=oidc` hides the password only once that config is complete; a partial config keeps the password so the operator is not locked out. Leaving `APPROVAL_METHODS` unset offers both when the config is complete. `npx -y a2a-exposed@latest pair set-oidc` writes the non-secret settings into `config.env`. The client secret is read from `APPROVAL_OIDC_CLIENT_SECRET` or `--secret-stdin` and is not saved or printed; the next `npx -y a2a-exposed@latest deploy` uploads that environment variable. `status` and `pair list` name the issuer host when OpenID Connect is on.

- **npx is the one way to run the CLI:** `npx -y a2a-exposed@latest <command>` everywhere: README, both skills and their references, the Worker's default wake hint (`WAKE_CLI_COMMAND` default), the Claude Code wake text, the 401 pairing hint (`data.pairing.cli`), the landing page and consent/pairing pages, and the CLI's help and every "run ..." / "next step" line it prints. Agent sandboxes (e.g. Claude Code cloud sessions) block a forced `npm i -g a2a-exposed` as untrusted code; npx needs no install. `@latest` rather than a pinned `@0.5`: `deploy` copies the Worker template from the CLI it runs, so an older or pinned copy would redeploy an older Worker over a newer one, and wake hints run long after setup should match the current release. A global install stays an optional speed-up on your own machine (`npm i -g a2a-exposed@latest`, then `a2a-exposed <command>`). The outdated-version notice now says `run npx -y a2a-exposed@latest deploy` (with a global install, `npm i -g a2a-exposed@latest` first); under npx `@latest` it doesn't appear, because the running copy is the newest.
- **Skills without a global install:** `npx -y skills add telegraphic-dev/a2a-exposed` into the current project is the documented path (`--global` optional), or the agent reads the `SKILL.md` files straight from GitHub.
- **Claude Code: don't set up from a cloud session.** The README quick start, the setup skill (first thing) and the Claude Code wake reference now say so up front: installs are blocked as untrusted code, tunnels as an ingress risk, there are no Cloudflare credentials and the network allowlist blocks the Worker. Run setup on a laptop or from another agent, add `<Worker URL>/mcp` as a connector, and the routine only consumes through the connector. The CLI fallback in a routine uses npx instead of a global install in the setup script.

## 0.5.0 - 2026-10-09

Upgrade: `a2a-exposed deploy` (applies D1 migrations `0006_mcp`: OAuth clients and codes, MCP grants, synced outbound peers; and `0007_cimd`: cached client metadata documents). Nothing changes for peers.

- **MCP protocol version 2026-07-28** alongside 2025-11-25, 2025-06-18 and 2025-03-26 (Claude and other clients still using the `initialize` handshake keep working; the behaviour is chosen per request). The new version is stateless: the version, `clientCapabilities` and `clientInfo` come from each request's `_meta`, and the `MCP-Protocol-Version`, `Mcp-Method` and (for `tools/call`, also Base64-encoded) `Mcp-Name` headers are required and must match the body (`-32020` HeaderMismatch, HTTP 400). New `server/discover` (supported versions, capabilities, instructions); every result carries `resultType: "complete"` and the server info in `_meta`; `tools/list` keeps its deterministic order and adds `ttlMs` (1 hour) and `cacheScope: "private"`; `subscriptions/listen` is acknowledged with no notification types and closed at once; `initialize`, `ping` and other unknown methods get `-32601` with HTTP 404; `Mcp-Session-Id` and `Last-Event-ID` are ignored; GET and DELETE stay 405. An unknown protocol version (in `_meta` or the header, either era) gets `-32022` UnsupportedProtocolVersion with `supported` and `requested`. Not implemented: multi-round-trip requests (`input_required`), tasks, logging, resources, prompts and list-change notifications (none of the tools need them).
- **OAuth: Client ID Metadata Documents** (`client_id_metadata_document_supported`): a client may use an https URL as its `client_id`; the Worker fetches the document (public https hosts only, never the Worker itself; every A/AAAA record must be a public address, checked over DNS-over-HTTPS, and the request goes over a connection to that checked address with TLS verified for the host, so DNS rebinding can't redirect it; no redirects, no credentials, 5 s, 5 KB, 30 lookups per hour per IP), checks that it names its own URL, lists valid redirect URIs and uses no client secret, caches it per `Cache-Control` (errors are not cached), and the consent page shows the host that publishes it. Dynamic client registration stays for older clients (deprecated in MCP) and now accepts and echoes `application_type` (`native` or `web`). The authorization server metadata advertises `authorization_response_iss_parameter_supported` (the redirect already carried `iss`, RFC 9207).
- **Fix:** an authorization request without the optional `resource` parameter failed with a server error; the code is now bound to the Worker's MCP URL.

- **Remote MCP server (connector) at `/mcp`:** Claude and other MCP clients can use the agent address as a connector, with no CLI, Node or owner token in the client. MCP Streamable HTTP (stateless JSON, `POST /mcp`; protocol versions 2025-11-25, 2025-06-18, 2025-03-26), tools `inbox`, `show_task`, `history`, `mark_working`, `reply` (with state), `send`, `poll_outbound`, `list_peers`, `pairing_requests` (read-only); every description carries the untrusted-peer-content and never-approve-pairings rules. Auth is OAuth 2.1 per the MCP authorization spec: protected-resource metadata (`/.well-known/oauth-protected-resource/mcp`, 401 with `WWW-Authenticate: Bearer resource_metadata=...`), RFC 8414 metadata (now also listing `authorization_endpoint`, `registration_endpoint`, `revocation_endpoint`, S256), dynamic client registration (public clients, https or loopback redirects, rate limited, capped), authorization code with PKCE S256, `resource` checked against the MCP URL, rotating refresh tokens, RFC 7009 revocation. The consent page (`/oauth/authorize`) needs the owner's approval password (same lockout counters as `/device`), shows the client's claimed name and redirect host, warns on loopback redirects, and has no scripts, a strict CSP and CSRF protection. MCP tokens live in their own table: never accepted as peer or owner tokens, never in a URL, listed by `token list` (`mcp-<client>`) and ended by `token revoke`. Scope: `inbox`, reply, send to synced peers, read-only listings; no token or pairing administration. On by default for an agent address (inert until you approve a client with your password); `--mcp off` disables it; never in proxy mode. `/health` reports `mcp`. The approval-password link (`pair set-password --web`) now also works with `--pairing-approval off` while MCP is on.
- **New `peers sync [alias...]` / `peers unsync <alias>`:** upload outbound peers (URL and token) to the Worker so the connector's `send` / `poll_outbound` can reach them. Tokens are AES-GCM encrypted with a key derived from the owner token (rotating it makes them unreadable until the next sync); peer URLs must be public https.
- **Claude Code: connector first.** The setup docs (README, setup skill, wake reference) make the connector the primary path for routines (add `<Worker URL>/mcp` at claude.ai/settings/connectors; routines include connectors and connector traffic bypasses the environment allowlist). The CLI checklist stays as the fallback. The `claude-code` wake text tells the session to use the connector's tools and falls back to the CLI requirements; `wake set` / `wake test` print both paths.
- **Rate-limited wakes are retried:** when the wake endpoint answers 429 or 503 (a routine over its fire limit), the Worker keeps the inbound wake pending and re-sends it after `Retry-After` (30 s to 1 h), up to 3 times, on the next request to the Worker or the optional cron; then it logs `wake_dropped`. Previously such a wake was only logged.
- **Agent Skills + Agent Plugins compliance:** both skills pass `skills-ref validate` (frontmatter limited to the agentskills.io allowed keys; `version` / `author` / `homepage` under `metadata`; Hermes and OpenClaw blocks kept under `metadata.hermes` / `metadata.openclaw`). Setup skill body split into `skills/a2a-exposed-setup/references/` so `SKILL.md` stays under the progressive-disclosure size guidance. Root `plugin.json` targets Agent Plugins 1.0.0 (skills stay in `skills/`). Vendor manifests coexist: `.cursor-plugin/plugin.json`, `.grok-plugin/plugin.json`, `.claude-plugin/plugin.json` (+ marketplace.json for Claude). Codex/ChatGPT use the portable root `plugin.json` (`extensions.com.openai`); no separate `.codex-plugin` overlay. CI validates skills-ref and the plugin schema.
- **Claude Code routines (`claude-code`):** the setup skill's wake reference has an ordered checklist for routine API triggers (repository source with the skills on the default branch, `A2A_BASE_URL` / `A2A_OWNER_TOKEN` on the routine's cloud environment and its risk, network allowlist for the Worker host, CLI in the setup script, a prompt that acts on the `routine-fire-payload` block, end-to-end verification) and troubleshooting rows for each failure mode. The `claude-code` wake text now tells a fresh routine session what it needs and to report incomplete setup instead of improvising (not for human-approved pairing wakes, which need no CLI). `wake set` / `wake test` with this preset print the same checklist. Upgrade: `deploy` (Worker wake text only).

## 0.4.1 - 2026-10-08

Upgrade: `a2a-exposed deploy` (new Worker owner endpoint; no D1 migration). Prompted by a façade in front of a Hermes A2A server behind Cloudflare Access: with only the Access service token set, every peer call failed with HTTP 502 / `-32603`, because Hermes also checks its own bearer (`UPSTREAM_TOKEN`), and nothing said so.

- **Setup checks the upstream's bearer requirement (proxy mode):** `init`/`deploy` with `--upstream` read the upstream's agent card (from this machine, with the exported Access service token; never with a bearer) and, unless it declares no bearer / HTTP auth (A2A 1.0 `securitySchemes` + `securityRequirements`, 0.3 `securitySchemes` + `security`; OAuth 2.0 / OpenID Connect count as bearer; an empty requirement means anonymous access is allowed), require `UPSTREAM_TOKEN`: exported, piped with the new `--upstream-token-stdin`, or typed at a hidden prompt when stdin and stderr are a terminal. Never argv. Non-interactive runs exit 1 with what to do. `--no-upstream-token` is the explicit opt-out (saved as `A2A_UPSTREAM_NO_TOKEN`, cleared by an uploaded token or `--upstream none`). A redeploy is not asked again when the Worker already holds `UPSTREAM_TOKEN` for the same upstream, or the card it fetched declares no bearer. A required API-key, basic or mTLS scheme gets a warning (the façade only presents a bearer).
- **`status`: Access credential and upstream bearer reported separately (proxy mode):** new rows `upstream Access` (credential configured + fingerprint, or NONE) and `upstream bearer` (configured + fingerprint, `NONE, but the upstream card asks for one`, or not needed), plus `upstream check` (the live check below), `peer auth` and `last failure`. An Access-only setup whose upstream card asks for a bearer is a failing next step, not "complete". When `UPSTREAM_TOKEN` is exported locally, its fingerprint is compared with the Worker's.
- **New `upstream verify [--json]` (also run by `status`):** checks the façade's whole path layer by layer. (1) Façade peer auth: an unauthenticated JSON-RPC call to the public endpoint must get 401. (2) New owner-authenticated Worker endpoint `POST /owner/facade/verify`: the Worker calls the upstream with its stored secrets (they never leave the Worker) using a JSON-RPC method that doesn't exist (`a2a-exposed/verify-unknown-method`), so no task is created, and classifies the answer: `reachable` (any JSON-RPC answer, typically `-32601`), `access_credentials_missing` / `access_rejected` (Access login redirect, Access 401/403, `CF-Access-*` headers), `upstream_auth_missing` / `upstream_auth_rejected` (401/403 from the agent, including a JSON-RPC body such as Hermes's `-32050`), `tunnel_down` (530, Cloudflare error 1033/1016), `upstream_unavailable` (5xx without JSON-RPC), `network`, `unexpected_response`, `misconfigured`. Output: one row per layer (façade peer auth, Access, upstream bearer, A2A app) and the fix. Exit 1 unless the app answered. Reason codes and HTTP status only, never a secret value; no public probe endpoint.
- **Non-leaky 502 for peers, precise reasons for the operator:** an upstream failure now reaches peers as a generic `-32603` (`This agent is unavailable: its façade is misconfigured; tell its operator`, or `... not reachable right now; try again later`) that no longer mentions credentials or Access. The Worker logs `upstream_error` with the `reason` code and stores the last failed peer call (reason, HTTP status, method, peer, time; D1 `settings`, no migration), which `GET /owner/facade` returns (`lastFailure`, plus `upstreamAuth`: what the upstream card asks for, scheme names and kinds only) and `status` shows.
- **Shadowed peer tokens in `status`:** a `PEER_<ALIAS>_TOKEN` exported in the environment that differs from the token saved in `config.env` (e.g. a stale value after `connect` stored a fresh one) now also gets an `also:` line in `status` (names only, never values), in addition to the existing `connect` / `send` / `poll` / `peers` warnings.
- **Cloudflare login blocked / API token hints:** when `init` finds no login, it now says that `cf auth login` failing with `OAuth error: HTTP 403 Forbidden` before any code appears is Cloudflare's bot mitigation for datacenter IPs (don't retry; use `CLOUDFLARE_API_TOKEN`); with a token set it points at IP filters (IPv4 `/32` and IPv6 `/128`), and a token that can't list accounts gets "add User → Memberships → Read, or pass `--account-id`". Setup skill troubleshooting: account- vs zone-scoped permissions, API error `1001` for a wrong Memberships selector, IP-restricted tokens (overlaps the still-open docs PR #13, which adds the basic API-token path).
- **Pairing with a non-public card:** `connect --card-url` with an http card (left out) and a failed pairing request that carried a Tailnet / LAN card now point at proxy mode (a public façade); the setup skill sends agents whose card isn't public https to proxy mode up front.
- **Removed the leftovers of the rename from a2a-over-webhook (0.4.0):** the deprecated alias package `alias/a2a-over-webhook/` and its CI pack check and publish step (a `v*` tag now publishes only `a2a-exposed`; the versions already on npm stay deprecated there), the fallback to `~/.config/a2a-over-webhook` when `~/.config/a2a-exposed` doesn't exist (the config directory is always `$XDG_CONFIG_HOME/a2a-exposed` or `~/.config/a2a-exposed`, or `A2A_CONFIG_DIR`), the old default Worker name for a saved deployment without `A2A_WORKER_NAME` (the default is always `a2a-exposed`; `init` saves the name, and `deploy` now refuses a hand-edited config with a saved D1 but no Worker name instead of guessing: rerun with `--worker-name`), the `A2A_NO_RENAME_NOTICE` variable, and the rename notes in the README, both skills and `AGENTS.md`. Peer tokens keep their `a2aow_` prefix (existing tokens and the token format are unchanged). Breaking for a config still in `~/.config/a2a-over-webhook` (the CLI then reports `no saved deployment`): `mv ~/.config/a2a-over-webhook ~/.config/a2a-exposed`, or `export A2A_CONFIG_DIR=~/.config/a2a-over-webhook`.
- **Docs:** README (façade credentials, `upstream verify`, security model), setup skill (the two upstream credentials, troubleshooting rows per reason code), operate skill (502 vs 401, `upstream verify`, shadowed peer tokens).

## 0.4.0

Upgrade: `npm rm -g a2a-over-webhook && npm i -g a2a-exposed@latest`, then `a2a-exposed deploy` (applies D1 migration `0005_facade_owners.sql`, used only in proxy mode).

- **Renamed to a2a-exposed** (the project now also exposes agents that already speak A2A, not only webhook agent addresses). npm package and command `a2a-exposed`; skills `a2a-exposed-setup` and `a2a-exposed`; repository `telegraphic-dev/a2a-exposed` (GitHub redirects the old URL); install with `npm i -g a2a-exposed@latest` and `npx --yes skills add telegraphic-dev/a2a-exposed`. Compatibility: the npm package `a2a-over-webhook` becomes a deprecated alias for one or two minor releases (`alias/a2a-over-webhook/`, published by the same tag at the same version): it depends on `a2a-exposed` and keeps the `a2a-over-webhook` command and `npx a2a-over-webhook` (wake hints of existing deployments) working, with a stderr notice (`A2A_NO_RENAME_NOTICE=1` silences it). Upgrade a global install with `npm rm -g a2a-over-webhook && npm i -g a2a-exposed@latest`; an existing `~/.config/a2a-over-webhook` is used as is when `~/.config/a2a-exposed` doesn't exist (nothing is moved); a saved deployment without `A2A_WORKER_NAME` keeps the old default Worker name, and new deployments default to `a2a-exposed`. Unchanged: `A2A_*` / `WAKE_*` variables, peer tokens (`a2aow_` prefix), D1 data and URLs. Wake hints, the 401 pairing hint and OAuth metadata now name `npx a2a-exposed` and the new repository. The domain `a2a.exposed` is reserved for future project pages (nothing is served there yet).
- **Public façade for an agent that already speaks A2A on a private network (proxy / expose mode):** `init|deploy --upstream <https URL>` (optional `--upstream-card-url`, which must be on the `--upstream` origin because the card is fetched with the upstream credentials: a cross-origin card URL is refused by `init`/`deploy` and by the Worker, which never sends `UPSTREAM_TOKEN` or the Access service token to any other origin; `--upstream none` switches back) makes the Worker a façade instead of an agent address. It forwards authenticated A2A JSON-RPC (1.0 and 0.3; `SendMessage`, `GetTask`, `CancelTask`) to the upstream through a Cloudflare Tunnel hostname behind Access. The upstream credentials come from the environment only and are uploaded as Worker secrets: `UPSTREAM_TOKEN` (bearer the façade presents) and `UPSTREAM_ACCESS_CLIENT_ID` / `UPSTREAM_ACCESS_CLIENT_SECRET` (Access service token). The peer's token is never forwarded; the peer label goes upstream as `X-A2A-Peer`. Peers sharing the one upstream identity are isolated by the façade (D1 `facade_owners`: calls on another peer's task are refused before reaching the upstream, and a message may only name a `contextId` the façade recorded for that peer: client-chosen or unknown context ids are refused, fail closed). `ListTasks` is refused, streaming returns `-32004`, push notification configs are refused with `-32003` (the upstream would call the URL from inside the private network, where even a public-looking name like `127.0.0.1.nip.io` can resolve to a private address; peers poll `GetTask`; a relay through the Worker is a follow-up), and an upstream 401/403 or non-JSON answer becomes a 502 JSON-RPC error instead of a 401 to the peer. A private `--upstream` (Tailnet, LAN, localhost) is refused with the tunnel recipe. Device-flow pairing stays on the façade. New owner endpoint `GET /owner/facade` (upstream card status, versions, card-leak check, secret fingerprints only); `status` shows an `upstream:` row and proxy-mode next steps; `/health` reports `mode`.
- **Agent card rewrite:** in proxy mode the public card is the upstream's card rewritten for the façade: interfaces only at `PUBLIC_URL` (one JSONRPC interface per version the upstream lists), the façade's security schemes and OAuth device URLs, upstream name / skills / version / extensions preserved (`--agent-*` settings win), `streaming`, `pushNotifications` and `extendedAgentCard` false, `signatures` and unknown fields dropped. Private and upstream URLs (localhost, RFC 1918, 100.64/10, private IPv6 literals such as `[::1]`, `[fd..]`, `[fe80..]` and IPv4-mapped ones, `*.ts.net`, `*.local`, single-label hosts, plain http, URLs that don't parse, the tunnel hostname) are removed from every string; `documentationUrl`, `iconUrl` and `provider.url` are dropped when private. The agent address card now applies the same scrubbing to `AGENT_DESCRIPTION`, `AGENT_SKILLS`, `PROVIDER_URL` and `DOCUMENTATION_URL`. If the upstream card is unreachable, a settings-based card is served (still public URLs only). Rules are documented in the setup skill.
- **HTML landing page at `GET /`:** a browser gets a small page with the agent name, description and skills (from the served card, so in proxy mode the rewritten one with no private URL), the note that this is an A2A endpoint, and links to the agent card and the `/device` pairing page (or a note when pairing is off). It has the same frame as `/device`: no scripts, no external assets, a strict nonce CSP. Requests without `Accept: text/html`, or with `?format=json`, still get JSON, now with `description`, `a2a` and `pairing` alongside `name` and `agentCard`.
- **Tailnet agents pairing out:** `connect --card-url <url>` names your card in the pairing request. A private https card (`*.ts.net`, LAN) is sent as informational: the other agent address's `/device` page and wake (`agentCardPrivate: true`) flag it as "not publicly reachable". A non-https card is still left out.
- **Docs:** setup skill section "Already have A2A on a Tailnet or LAN: expose it through a public façade" (tunnel + Access recipe, deploy, card rewrite rules, upstream credential model, threat model), README section and security/protocol notes, new troubleshooting rows, `deploy.env.example`.

## 0.3.1

- **Stale peer-token variables:** a peer token in the environment (`PEER_<ALIAS>_TOKEN`, a `--token-env` variable, or `A2A_PEER_TOKEN`) still overrides the one saved in `config.env`, but when both are set and differ, `send`, `poll`, `connect` and `peers add|list` now warn on stderr, naming the variable and how to clear it (`unset PEER_<ALIAS>_TOKEN`), never either value. `connect` (and `peers add --token-stdin`) also warns right after saving a new token that a stale variable would still win on the next `send`; `connect --json` adds `env_overrides_token: true`.

## 0.3.0

Upgrade: `npm i -g a2a-over-webhook@latest`, then `a2a-over-webhook deploy` (applies D1 migrations `0002_wake_budget.sql` and `0004_pairing_replace.sql`).

- **Password setup over the web (`pair set-password --web`):** the CLI asks the Worker for a one-time link (`POST /owner/pairing/password-link`, default 15 minutes, configurable 1–60). The human opens `/device/setup?t=...`, types a password (≥12) twice, and the Worker hashes it with PBKDF2-SHA256. The link is single-use, burned after 5 bad attempts, and a new link invalidates the old one. Same CSP/CSRF as `/device`. `status` / `pair list` show when the password was set and how (`web` or `terminal`). Agents must send the link to their human and never open it.
- **Re-pairing without orphan tokens:** `connect` refuses an alias whose token still works, unless `--replace` (alias `--force`). With `--replace` the old token is presented on `device_authorization`; the Worker stores `replaces_label` (D1 migration **`0004_pairing_replace.sql`**, applied automatically by `deploy`) and the presented token's hash (`replaces_hash`), and, on approval, swaps the token under the same label only if that exact token is still on the label (a rotate or revoke in between downgrades to a new label, so a leaked old token can't take over a rotated one). Older peers keep the old token until their owner revokes it; the CLI says so. Revoked labels are not reused.
- **Expired `--no-wait` resumes:** resuming an expired request exits 1 with "expired before it was approved; nothing was stored" and prints the exact re-run command (every flag, including `--alias`). Never starts a silent new request.
- **Polling agents see pairing requests:** `inbox` lists pending pairing requests after the tasks (and on stderr with `--json`); the polling recipes (Hermes, OpenClaw, generic) tell the agent to relay the code and never approve. `pair set-password` no longer claims every wake carries the link.
- **Node 22 and CLI upgrades:** the setup skill and the CLI's version error list mise / nvm / fnm / the official installer. The install recipe is `npm i -g a2a-over-webhook@latest` (the old `command -v || npm i` never upgraded). An outdated notice on stderr checks the npm registry at most once a day in a detached process (`A2A_NO_UPDATE_CHECK=1` / `DO_NOT_TRACK=1` / `CI` opt out). Skills and `cli/package.json` versions are kept in sync with the release.
- **Deploy / status friction:** `init`/`deploy` wait until the agent card is served twice in a row (Cloudflare 1042 while a new workers.dev Worker propagates). `status` names Cloudflare error codes and treats 1042 as "retry in 30 s". Optional hints (`also:`) keep the next-step line short; with no wake it no longer mentions a preset. Global `--config-dir` (and `--worker-dir` for the Worker project; `--dir` still works). `url` exits 1 when unset. `connect --json` uses `not_waiting` instead of `started`. Friendly 401 from `send`/`poll`. `--workers-logs on|off` enables persisted Workers Logs (query strings redacted). Teardown steps are documented (no command).
- **Auth and pairing polish:** 401 echoes the JSON-RPC `id`; `WWW-Authenticate` carries `resource_metadata` (RFC 9728) and a different `error_description` for a bad token vs no token; `/.well-known/agent.json` is an A2A 0.3-shaped card; `token revoke` of an unknown label exits 1 with a clear message (text, not raw JSON); `pair approve` in human mode names `deploy --pairing-approval agent`; Deny on `/device` needs no password and never runs PBKDF2; pairing `off` lists no pending requests; expired device codes return `expired_token` for 24 h, then `invalid_grant`.
- **PBKDF2:** default stays 100,000 (Workers maximum); range 50,000–100,000 via `init`/`deploy --pbkdf2-iterations` (`A2A_PBKDF2_ITERATIONS`). Each stored hash keeps its own count. Documented in the threat model (online guessing is limited by lockouts; lower only if `/device` hits Cloudflare error 1102 on the free plan).
- **Worker:** migration `0002_wake_budget.sql` creates the `wake_budget` table on D1 databases whose `0001_init.sql` came from an earlier build (D1 tracks migrations by file name, so the current `0001` never ran there). Without it, messages to such an agent address failed with `-32603 Internal error` and sent no wake. It does nothing on newer databases, and `deploy` applies it even where `0003_device_pairing.sql` is already recorded (cf applies every unrecorded file in numeric order). A new test replays these upgrades.
- **Docs:** setup skill section on adopting an existing deployment (same Worker, D1 and hostname) without reissuing tokens or re-uploading secrets: `deploy` keeps the Worker's secrets and applies `0002`–`0004`, then `status` checks the result. Device-flow pairing is on after the deploy (`human` approval), or `--pairing-approval off`.
- **Skills:** the setup skill suggests two optional companion skills near its prerequisites, with install commands: `mise` ([telegraphic-dev/mise-skill](https://github.com/telegraphic-dev/mise-skill), for Node 22.18+) and `cloudflare` ([cloudflare/skills](https://github.com/cloudflare/skills), for Workers, D1, the `cf` CLI, DNS, Tunnel and Access). The operate skill points to `cloudflare` for Cloudflare-side troubleshooting. `related_skills` lists them; the README's install section names both.

## 0.2.0

- **Tunnel on a workers.dev agent address:** `tunnel create` (and `init --workers-dev --tunnel`) no longer needs a custom agent address hostname. The wake hostname goes on any zone of the account: the agent address hostname's zone, otherwise the account's only zone (it says which it picked). With several zones it stops and lists them; choose with the new `--tunnel-zone <zone>` (or `--tunnel-hostname`). With no zone it says plainly that polling is the option. Zones are listed for the deployment's account only.
- **`status` command:** a read-only summary of the deployment and base URL, an agent-card check done by the CLI (no `curl`), the wake mode (webhook, tunnel, or none, which means polling), the tunnel state and connector connections, and a `next step:` line. Exit code 1 when something is broken. `init` and the card warning now point to it.
- **Safe re-runs:** `tunnel create` on an existing tunnel creates nothing. It re-uploads Worker secrets if they're missing, together with an exported `WAKE_WEBHOOK_KEY` / `WAKE_HMAC_SECRET`, and warns if the Worker would still have no webhook auth. A missing connector token file is downloaded again. When an earlier run stopped halfway, it asks for `tunnel rm`. `status` flags a local preset (Hermes, OpenClaw) whose Worker sends no webhook auth. If `init --tunnel` fails at the tunnel step, it says the agent address is deployed and how to continue.
- **Setup skill:** decide how wakes reach the agent (public webhook, local-only webhook, or none) before picking the agent address URL. For local-only webhooks, check for a zone and recommend the secure tunnel with either agent address URL, explaining the tradeoff to the user; polling is the fallback. Adds copy-paste polling commands for Hermes (`hermes cron create "every 2m" ... --skill a2a-over-webhook --name a2a-inbox`, checked against the Hermes source and docs) and OpenClaw (`openclaw cron add --every 5m --session isolated ...`), with a note about command approval prompts. Also adds resuming with `status`, moving from polling to the webhook without a redeploy, and new troubleshooting rows.
- **The agent card always advertises the public base URL:** the Worker template no longer reads an `A2A_PUBLIC_URL` override from the deploy environment, so a local webhook, Tailnet or tunnel URL exported by the agent can't end up in the card. The card URL comes only from the custom hostname or the workers.dev URL. The Worker also ignores a non-https or private-network `PUBLIC_URL` and falls back to the request origin. `init` and `deploy` verify and print the deployment's own URL, and warn when an exported `A2A_BASE_URL` differs. They also warn when the card points elsewhere. `status` flags both cases (`agent card: WRONG URL: ...`, next step `deploy`). This was found in a Hermes setup where the card advertised a Tailnet URL and the agent fixed it by hand.
- **Device-flow pairing (OAuth 2.0 Device Authorization Grant, RFC 8628):** agents connect without exchanging tokens in chat.
  - Each agent address serves `POST /oauth/device_authorization`, `POST /oauth/token` (`authorization_pending`, `slow_down`, `access_denied`, `expired_token`, `invalid_grant`), a `/device` approval page, and `/.well-known/oauth-authorization-server`. An approval issues a normal hashed `a2aow_` peer token, single-use.
  - Approval is `human` by default: an approval password set with `pair set-password` (terminal only, no echo; stored as salted PBKDF2-SHA256), with lockouts. `--pairing-approval agent` adds `pair approve <code>`; `off` disables pairing.
  - New requests wake the agent with `kind: "pairing_request"`. New commands: `connect <url>` (client; `--no-wait`, `--json`), `pair list|approve|deny`. `token list` shows tokens created by pairing.
  - The agent card advertises an A2A 1.0 `oauth2SecurityScheme` with a `deviceCode` flow next to `bearer`, and a 401 from the A2A endpoint explains the flow.
  - Flood limits apply per IP and globally. D1 migration `0003_device_pairing.sql` (numbered to avoid a `0002` from another branch); run `deploy` to apply it.
- **Docs:** both skills and the README check the endpoint with `status` instead of `curl`, because some agent sandboxes (Hermes) flag `.dev` URLs in shell commands. The README and the operate skill describe the tunnel as needing any zone on the account.

## 0.1.0

First npm release of the CLI (`npm i -g a2a-over-webhook`).

- **Worker:** public agent card and A2A JSON-RPC endpoint (1.0 primary, 0.3 compatible) with a D1 database; per-peer `a2aow_` bearer tokens stored as SHA-256 hashes; push notifications; per-conversation wake debounce and an hourly wake cap.
- **Wake presets:** `grok-bot`, `claude-code`, `openclaw-wake`, `openclaw-agent`, `hermes` (HMAC-SHA256), `generic`; `wake set|unset|test|preview|fingerprint` with masked previews and SHA-256 fingerprints instead of secret values.
- **Deploy:** `init` / `deploy` with the `cf` CLI, on a custom domain or a free `*.workers.dev` URL (`--workers-dev-subdomain` registers the account subdomain); `deploy --workers-dev` / `--hostname` move a deployment between the two and retire the old URL (301 for the card, 410 otherwise).
- **cf auth profiles:** `--cf-profile` / `CF_PROFILE` for several Cloudflare logins on one machine.
- **Secure tunnel:** `tunnel create|status|rm` publishes a local-only webhook (OpenClaw, Hermes) through a named Cloudflare Tunnel behind a Cloudflare Access app that admits only the Worker's service token.
- **Tasks and outbound:** `inbox`, `show`, `working`, `reply`, `history`, `contexts`; `peers add|list|rm`, `send`, `poll`, `outbound`; `token issue|list|revoke|rotate`.
- **Skills:** `a2a-over-webhook-setup` (deploy and wake configuration) and `a2a-over-webhook` (day-to-day), with Hermes and OpenClaw frontmatter metadata.
- **Release tooling:** CI on pushes and PRs; tag-triggered npm publish with provenance and a GitHub release.

