bc83df3ed0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
3.0 KiB
Markdown
64 lines
3.0 KiB
Markdown
# a2a-n8n-adapter
|
|
|
|
An A2A facade in front of an n8n workflow. One image, one instance per agent,
|
|
configured entirely by environment.
|
|
|
|
The repo's notes proposed serving this from the gateway instead — a static Agent
|
|
Card and a route to the webhook, no new component. That does not reach. A2A is
|
|
not request/response: a caller gets a `Task` with an id, a context and a state
|
|
that can be `input-required` or `failed`, and none of that can be synthesised
|
|
from a webhook's single reply by a proxy. Templating the whole lifecycle in
|
|
gateway configuration is an adapter, written in the wrong language.
|
|
|
|
So: the alternative those same notes anticipated, written once for both agents.
|
|
|
|
## It owns the protocol and nothing else
|
|
|
|
It holds no Keycloak client and no credentials. It forwards the end user's token
|
|
to the workflow in `X-Subject-Token` — a header rather than the body, so a
|
|
workflow that logs its input does not log the token with it — and the workflow
|
|
exchanges it, because the component that can mint tokens should be the one that
|
|
uses them.
|
|
|
|
The workflow answers `{"answer": "...", "escalate": false}`. Anything else is
|
|
returned as raw text rather than guessed at: a workflow that has drifted from
|
|
the contract should look wrong, not be quietly reinterpreted.
|
|
|
|
`escalate: true` fails the task rather than completing it. An autonomous agent
|
|
that meets a rule needing approval must hand it back, never decide, and a
|
|
completed task is the wrong shape for "a human has to look at this".
|
|
|
|
## Configuration
|
|
|
|
| | |
|
|
|---|---|
|
|
| `ADAPTER_AGENT_NAME` | `supply` |
|
|
| `ADAPTER_AGENT_DESCRIPTION` | what the card says it is |
|
|
| `ADAPTER_SKILL_ID` | `check_availability` |
|
|
| `ADAPTER_SKILL_NAME`, `_DESCRIPTION`, `_EXAMPLES` | the card's one skill; examples are newline-separated |
|
|
| `ADAPTER_WEBHOOK_URL` | the workflow's production webhook |
|
|
| `ADAPTER_PUBLIC_URL` | what callers dial — the Service DNS name, not the bind address |
|
|
| `ADAPTER_CA_BUNDLE` | the lab CA, so TLS is verified rather than disabled |
|
|
|
|
One skill per instance, deliberately. A second one is a card change, which is
|
|
the moment to ask whether the agent has quietly become a data proxy.
|
|
|
|
## Four outcomes, and only one of them completes
|
|
|
|
HTTP 200 from n8n does not mean the workflow answered. It returns 200 with an
|
|
**empty body** when a node throws before the Respond node runs — a refused token
|
|
exchange is the common way in. The adapter used to read that as a completed task
|
|
carrying "(the workflow returned nothing)", which told the caller the task had
|
|
succeeded: the one thing certainly false.
|
|
|
|
| `Outcome` | A2A task state |
|
|
|---|---|
|
|
| `ANSWERED` — the contract, `escalate: false` | `completed` |
|
|
| `ESCALATED` — the contract, `escalate: true` | `failed` |
|
|
| `SILENT` — 200 with an empty body | `failed` |
|
|
| `OFF_CONTRACT` — a body that is not `{"answer", "escalate"}` | `failed` |
|
|
|
|
Off contract is returned **raw** so a workflow that has drifted is visible — but
|
|
never as agreement. Escalation is `failed` for the older reason: an autonomous
|
|
agent meeting a rule that needs approval must fail cleanly, never decide.
|