Troubleshoot the stack
Diagnose the environment first: address family, vantage, token, CORS, and daemon state explain most refusals.
When something in a running stack fails, the cause is usually the environment, not the protocol. The relay is bound on one address family while the URL names the other; a port forward lands on the wrong vantage; a token is missing; a proxy strips the one header a browser needed to explain the refusal.
This page is symptom-indexed. Find the symptom, run
the probe, apply the remedy. Commands run from a checkout of the
reference repository,
agenteventprotocol/reference,
exactly as Run the stack sets it up.
Start with aep doctor
node impl/cli/aep.js doctor --relay http://127.0.0.1:8787One read-only pass, findings with remedies attached, exit 0 when clean
and 1 when something needs attention. It checks:
- Reachability, per address family. The URL's port is probed on IPv4 and IPv6 loopback separately, because from outside, a bind is only observable one family at a time.
- Bind-vs-URL agreement, derived from that pair of probes: the classic failure is a relay answering on the family the URL does not name.
- Auth posture and token validity: whether the relay enforces authentication, and whether the token you supplied is accepted.
- CORS visibility: whether the preflight and, separately, a refused
request carry
access-control-allow-origin. - Relay stats: buffer depth, sessions, subscribers (informational).
- The managed daemons: every adapter daemon and inbound bridge the setup tool knows, with down / stale / wedged told apart.
Probes run from the machine doctor runs on. A clean report proves
nothing about a browser behind a port forward, a container, or a VM
boundary: that consumer sits on a different vantage. When a refusal only
happens "over there", re-run doctor on the refused machine against the
same --relay and compare the two reports.
The CLI page documents each lane against the source.
The relay answers on one machine and is refused on another
Port forwards and loopback mirrors land on one address family.
http://127.0.0.1:8787 pins IPv4; a WSL or SSH loopback forward can
surface on IPv6 ([::1]) only, which leaves the IPv4 URL refused even
though the port is, in every other sense, forwarded. doctor names this
case directly: the URL's family refuses while the sibling family answers
/healthz.
Remedies, in preference order: forward the family the URL names; point
the URL at the family that answers; or restart the relay bound to the
family you need (the AEP_HOST environment variable, which
aep-setup up relay sets for you).
Everything is up, but a browser consumer reports a network error
A browser can only explain a refusal it is allowed to read. A response
without access-control-allow-origin surfaces in the page as a generic
network error (status 0), whatever the real status was, so a bad token
and a dead relay become indistinguishable. The reference relay carries
the header on refusals too, precisely so a browser consumer can render
"401" instead of "failed to fetch"; doctor's CORS lane verifies both
the preflight and a deliberately refused request.
If the relay sits behind a reverse proxy, check that the proxy forwards that header on error responses as well: stripping headers from non-2xx responses quietly reintroduces the opaque failure.
Reachable, but every request answers 401
Authentication on the event and control bindings is unconditional,
localhost included; see
AEP-0003 §8
for why. The reference stack never invents a token: the relay and every
CLI verb adopt one from --token, the AEP_TOKEN environment variable,
or the setup-managed token file, in that order. Two machines disagreeing
about the token value produce exactly this symptom; doctor's auth lane
reports the posture the relay actually enforces.
A control command is refused before the agent ever sees it
aep respond or aep cancel exiting 3 with a reason on stderr is a
sender-side refusal, not a transport failure. A target's
hello.capabilities.control.accepts declaration is authoritative, and a
relay that knows it refuses on the target's behalf; see
AEP-0004 §4
for the gate and the error frame it answers with. Reading the reason:
unsupported: the target does not accept this command type. An adapter whose control gate is disabled declares no accepted commands, so every command nacks this way by design; enable the gate in that adapter's setup (each adapter README in the reference repository documents its switch) and the declaration changes on the next hello.unauthorized: the sending connection is unauthenticated, or a deployment policy refused the sender.ambiguous: more than one live connection claims the addressed session; the relay refuses rather than guess a claimant.no-route: no connected emitter owns the addressed session. The agent is down, or the session has ended.
A managed daemon reads down, wedged, or stale
doctor's daemon sweep reads ids, ports, and pid files from the setup
tool's catalogs (they are tool-managed; nothing else should assign
them), and it tells three states apart:
- down (info): not running.
node impl/setup/aep-setup.js up <id>starts it. - stale pid (warning): the pid file exists but the process is gone.
aep-setup down <id>cleans it up. - wedged (finding): the pid is alive but the port no longer answers
/healthz.aep-setup down <id>thenup <id>.
Reproduce a problem without a live agent
Capture once, replay anywhere.
aep sink writes durable
per-session JSONL; aep replay capture.jsonl re-emits it onto any relay
as a new session, with fresh identity and command frames skipped so a
replay can never drive a live target (the
CLI page
covers why it is never a resume). --dry-run prints the plan without
emitting. This turns a "works on my machine" report into a file the
other side replays against their own consumer.
When it is not the environment
Validate the traffic itself:
aep validate runs the
conformance-grade envelope, payload, ordering, and capture checks over
any captured JSONL. If a line fails, the emitting adapter (or an
assumption in your consumer) is the bug, and the
conformance fixtures
pin down which side is wrong. Open an issue with the validate output
and a replayable capture; anything security-sensitive goes through the
security policy instead of the public tracker.