Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# A2A Platform Plugin — Design
|
||||
|
||||
Consolidates the entire A2A (Agent-to-Agent) feature cluster (#514 and friends)
|
||||
into one **plugin** with **zero core edits**, built on capabilities the current
|
||||
codebase already exposes. Implements **A2A Protocol v1.0** (JSON-RPC binding).
|
||||
|
||||
## Why a plugin, not a core feature
|
||||
|
||||
Earlier A2A attempts (#4135, #4948, #4952, #11025) added a standalone server
|
||||
package (`a2a_adapter/`) and/or patched `gateway/run.py` + `gateway/config.py`.
|
||||
Since then the codebase grew `ctx.register_platform()` (the plugin
|
||||
platform-adapter API — used by irc, line, teams, ntfy, simplex, …) and
|
||||
`ctx.register_tool()`. That makes the standing policy achievable: **plugins
|
||||
must not touch core files.** A2A now lives entirely under
|
||||
`plugins/platforms/a2a/`.
|
||||
|
||||
## Two directions
|
||||
|
||||
### Outbound — client tools (`a2a` toolset)
|
||||
- `a2a_discover(url)` — fetch + summarize a peer's Agent Card (v1.0
|
||||
`supportedInterfaces` aware, tolerates 0.3 cards).
|
||||
- `a2a_call(agent, message, context_id?)` — send a JSON-RPC `message/send`
|
||||
task to a peer, return the reply. Multi-turn via `context_id` (carried
|
||||
inside the Message per v1.0). Surfaces `TASK_STATE_INPUT_REQUIRED` so the
|
||||
model knows to answer and continue the context.
|
||||
- `a2a_list()` — configured peers + persisted conversations + metrics.
|
||||
- `a2a_history(context_id, limit?)` — recall a persisted conversation
|
||||
(this is the production consumer of the persistence layer).
|
||||
- `a2a_orchestrate(capability, message, mode?)` — fan-out one task to every
|
||||
configured peer advertising a capability. Modes: `all` (every reply),
|
||||
`first` (first success), `best` (longest successful reply — a deliberately
|
||||
coarse heuristic; errors never win, and an all-error fan-out reports the
|
||||
failures instead of picking one).
|
||||
|
||||
Peers resolved from `config.yaml` → `a2a_agents`, or a direct URL.
|
||||
|
||||
### Inbound — platform adapter
|
||||
- Stdlib `http.server` on a daemon thread (no asyncio loop needed at
|
||||
`register()` time — sidesteps the a2a_fleet "register outside a loop" bug
|
||||
class that killed inbound serving in forks). The request handler is a
|
||||
module-level class (`A2ARequestHandler`) reached through
|
||||
`server.adapter`, so RPC handlers are unit-testable without HTTP.
|
||||
- Agent Card at `GET /.well-known/agent-card.json` (canonical v1.0 path; legacy `agent.json` also answers) (v1.0: `supportedInterfaces[]`,
|
||||
`provider`, `capabilities.extendedAgentCard`). **Dynamic**: skills are
|
||||
built from the live tool registry at serve time
|
||||
(`A2A_ADVERTISED_TOOLSETS` / `extra.advertised_toolsets` restricts them).
|
||||
- JSON-RPC methods: `message/send`, `message/stream` (SSE), `tasks/get`,
|
||||
`tasks/list`, `tasks/cancel`, `tasks/subscribe`,
|
||||
`tasks/pushNotificationConfig/create` (legacy `set` names accepted).
|
||||
- **Live-session injection (the #11025 insight):** inbound tasks route through
|
||||
the normal `MessageEvent` → `handle_message` path keyed by the A2A
|
||||
`contextId`, so the agent that answers is the same one serving the user —
|
||||
full memory/context, not a clone. The reply returns through `adapter.send()`,
|
||||
which fulfils the pending per-**task** `Future` the HTTP request is blocked
|
||||
on (per-context FIFO, so concurrent same-context requests can't cross-talk);
|
||||
`on_processing_complete` resolves failures/cancellations promptly.
|
||||
- **Task store:** every task (including terminal ones, bounded to the last
|
||||
500) stays queryable via `tasks/get` / `tasks/list`, and `tasks/subscribe`
|
||||
reattaches to a running task's stream via store watchers. A watchdog fails
|
||||
orphaned tasks after 5 minutes (idempotent transitions — no double
|
||||
counting in metrics).
|
||||
- **input-required:** the platform hint tells the agent to start a reply with
|
||||
`[INPUT_REQUIRED]` when it needs clarification; the adapter maps that to
|
||||
`TASK_STATE_INPUT_REQUIRED` with the question in `status.message`.
|
||||
- **Push notifications:** config accepted inline in `message/send`
|
||||
(`configuration.taskPushNotificationConfig`) or via the create method
|
||||
(returns `configId` + `createdAt`). On terminal transition the callback
|
||||
receives a v1.0 `StreamResponse` (`statusUpdate`) payload, HMAC-SHA256
|
||||
signed (`X-A2A-Signature`, secret `A2A_PUSH_SECRET` falling back to the
|
||||
bearer token), with SSRF-guarded callback URLs.
|
||||
|
||||
## v1.0 wire format notes
|
||||
- Task states / roles are SCREAMING_SNAKE_CASE (TASK_STATE_*, ROLE_*).
|
||||
- Parts are member-presence discriminated — no kind field. All three
|
||||
Part types are supported: text (text + mediaType), file
|
||||
(url|raw + filename + mediaType), and data (data + mediaType).
|
||||
extract_text renders file/data Parts into the text stream (URL +
|
||||
filename for files, JSON for data) so the agent sees them; it also
|
||||
accepts v0.3 (kind) and pre-0.3 (type) shapes from older peers.
|
||||
Outbound replies are still text-only — the agent produces text, and
|
||||
file/data Parts are for inbound richness.
|
||||
- Push notification config: full CRUD — create (inline in message/send
|
||||
via configuration.taskPushNotificationConfig, or via the create
|
||||
method), get, list, delete. Each config has a configId and createdAt.
|
||||
One config per task (v1.0 allows multiple; we keep one).
|
||||
- SSE events are StreamResponse objects (statusUpdate / artifactUpdate
|
||||
members); stream closure signals the terminal state — no final field.
|
||||
- contextId lives inside the Message (legacy top-level accepted inbound).
|
||||
- Timestamps are ISO 8601 with millisecond precision; Tasks carry
|
||||
createdAt / lastModified.
|
||||
- Error codes: A2A-reserved codes are used only with their spec semantics
|
||||
(`-32001` TaskNotFound, `-32002` TaskNotCancelable); custom errors sit at
|
||||
`-32050..-32052` (unauthorized / rate-limited / untrusted).
|
||||
|
||||
## Security (on by default)
|
||||
- **Bind safety:** no token configured (`A2A_BEARER_TOKEN` or
|
||||
`A2A_PEER_TOKENS`) ⇒ bind `127.0.0.1` only. A token alone does not widen
|
||||
the bind; remote exposure requires token **and** explicit `A2A_HOST`.
|
||||
- **Peer identity:** `A2A_PEER_TOKENS="alice:tok1,bob:tok2"` gives each peer
|
||||
its own credential; the matched name is the authenticated identity used
|
||||
for rate limiting, the trust gate, message framing, and audit. A shared
|
||||
`A2A_BEARER_TOKEN` authenticates as `ip:<addr>`. Nothing in the request
|
||||
body can assert identity. Comparisons are constant-time.
|
||||
- **Trust gate:** `A2A_TRUSTED_PEERS` (or config `a2a.trusted_peers`)
|
||||
optionally restricts which authenticated identities may run tasks.
|
||||
- **Injection filters:** ALL inbound text (including `/`-prefixed — remote
|
||||
peers can never reach operator slash commands) is defanged (ChatML /
|
||||
role-prefix / override patterns → `[filtered]`) and framed with a privacy
|
||||
prefix marking it untrusted peer input.
|
||||
- **Outbound redaction:** credential-shaped strings (`sk-…`, `ghp_…`, JWTs,
|
||||
bearer tokens, emails) scrubbed before anything leaves.
|
||||
- **Rate limiting:** sliding window per authenticated identity
|
||||
(`A2A_RATE_LIMIT`/min).
|
||||
- **Anti-loop:** per-context turn cap (`A2A_MAX_PINGPONG_TURNS`, default 5,
|
||||
hard max 20) rejects (v1.0 `TASK_STATE_REJECTED`) runaway agent↔agent
|
||||
ping-pong; `tasks/cancel` resets the counter for the task's context.
|
||||
- **Audit log:** append-only `~/.hermes/a2a_audit.jsonl` for every exchange.
|
||||
|
||||
## State placement
|
||||
Task store, turn tracker, and rate limiter are **adapter-instance** objects
|
||||
(classes in `protocol.py`). The metrics counter bag stays a module singleton
|
||||
because it is intentionally shared between the inbound adapter and the
|
||||
outbound client tools (`/metrics` and `a2a_list` report both directions).
|
||||
|
||||
## Persistence (survives compaction)
|
||||
A2A conversations are written to `~/.hermes/a2a_conversations/<context>.jsonl`,
|
||||
outside the context-compaction pipeline — compaction and restarts can't lose
|
||||
them (#11025 requirement). The `a2a_history` tool recalls them by context id.
|
||||
|
||||
## Requirements traced to the cluster
|
||||
|
||||
| Source | Requirement | Where |
|
||||
|---|---|---|
|
||||
| #514, #23871, #4135 | Agent Card discovery | `protocol.build_agent_card`, adapter GET |
|
||||
| #4135, #14559, #8948 | Client: discover / call / list | `tools.py` |
|
||||
| #11025 | Live-session injection (not a clone) | `adapter._prepare_task` |
|
||||
| #11025 | Privacy filters + outbound redaction + audit | `security.py` |
|
||||
| #11025 | Conversation persistence outside compaction | `protocol.persist_message`, `a2a_history` |
|
||||
| #514, #11025 | Auth, localhost-default | `security.authenticate`, `resolve_bind_host` |
|
||||
| #56434 | Trusted peer approval | `security.is_trusted_peer` |
|
||||
| #56435 | Task completion notifications | push notifications (`_send_push_notification`) |
|
||||
| #25176, #689 | Agent↔agent messaging across machines | client tools + inbound adapter |
|
||||
| #7517 et al. | Multi-peer orchestration | `a2a_orchestrate` |
|
||||
|
||||
## Deliberately out of scope (future, not this pass)
|
||||
- **a2a-sdk / gRPC + HTTP+JSON bindings.** Only the JSONRPC binding is
|
||||
served; the card advertises exactly that.
|
||||
- **`tenant` field, extended Agent Card, `stateTransitionHistory`.**
|
||||
- **True task abort:** `tasks/cancel` marks the task canceled and drops the
|
||||
reply, but cannot abort the live session's in-flight turn.
|
||||
- **DID / Ed25519 identity, OAuth2 scopes, x402 micropayments** (#14559
|
||||
bindu) — heavy, niche; revisit if there's real demand.
|
||||
|
||||
## Files
|
||||
```
|
||||
plugins/platforms/a2a/
|
||||
├── plugin.yaml # manifest (kind: platform)
|
||||
├── __init__.py # register(): platform adapter + client tools
|
||||
├── adapter.py # inbound A2A v1.0 server (stdlib http.server)
|
||||
├── tools.py # outbound client tools
|
||||
├── protocol.py # Agent Card, JSON-RPC framing, task store, persistence
|
||||
├── security.py # auth/identity, injection filters, redaction, audit
|
||||
├── DESIGN.md
|
||||
└── README.md
|
||||
```
|
||||
Reference in New Issue
Block a user