docs(a2a): security reach, ports and limits
This commit is contained in:
29
docs/a2a.md
29
docs/a2a.md
@@ -25,8 +25,9 @@ The Telegram host (`docs/telegram-bot.md#a2a`) mounts it with one workspace per
|
||||
|
||||
## Files
|
||||
|
||||
Everything lives under `<workspace>/.volcanic/a2a/`. A missing file means "none"; a malformed one means "none" plus
|
||||
a warning log line naming the file — never the keys.
|
||||
Everything lives under `<workspace>/.volcanic/a2a/`. A missing file means "none". A malformed `callers.json` or
|
||||
`card.json` means "no callers" / the fallback card, plus a warning log line naming the file — never the keys. A
|
||||
malformed task file is read as "no such task" (`TaskNotFound`) without a log line.
|
||||
|
||||
`callers.json`, read fresh on every request (no cache, so an edit applies to the very next call, no restart needed):
|
||||
|
||||
@@ -67,8 +68,9 @@ written atomically (temp file, then rename) and read fresh on every request.
|
||||
| `POST /a2a/push/{watchId}` | none (token in body/header) | Receives pushes for *our own* client's watches; needs `A2AHostOptions.OnPush` wired by the host, otherwise every call is 404 |
|
||||
|
||||
Authenticate with the header `X-API-Key: <key>` or `Authorization: Bearer <key>`. Exactly one caller must match the
|
||||
key across every workspace the host knows about; no match, or the same key matching callers in two different
|
||||
workspaces, is a 401 (and a warning naming the owners, never the key).
|
||||
key across every workspace the host knows about; no match, or the same key matching more than one caller (in two
|
||||
different workspaces, or two entries of the same `callers.json`), is a 401 (and a warning "matches N callers
|
||||
(owners: ...)", never the key).
|
||||
|
||||
Known REST quirks (the SDK's, not ours): the request body of `/a2a/v1/message:send` and `:stream` is parsed before
|
||||
the key check runs, so a malformed body on an unauthenticated call still gets parsed; and the card served at
|
||||
@@ -100,7 +102,7 @@ context is used — nothing is lost.
|
||||
| `submitted` → `working` | A new message with no `taskId`, or a continuation, starts a run | |
|
||||
| `completed` | The run ends normally | The final assistant text becomes the `response` artifact. A reply cut off by the model's length limit (`StopReason.Length`) still counts as completed — it is not flagged as a failure |
|
||||
| `input-required` | The agent called `request_input(question)` | The question is the task's status message. The caller continues it with the same `taskId` (same `contextId`, or none); a different `contextId` is `InvalidParams`. A continuation of a task that is *not* `input-required` is `UnsupportedOperation` |
|
||||
| `failed` | A provider error, the hop limit, a bad file, or any unexpected exception | The message is the redacted error text |
|
||||
| `failed` | A provider error, the hop limit, a bad file, or any unexpected exception | The message is the redacted error text; for an unexpected exception it is only "The run failed unexpectedly." (the redacted detail goes to the log) |
|
||||
| `canceled` | `CancelTask`, or the request's own cancellation | |
|
||||
|
||||
Restart recovery is lazy, not a startup scan: a task file read as `submitted` or `working` with no live run behind
|
||||
@@ -117,7 +119,7 @@ chunk; registered webhooks get `X-A2A-Notification-Token` when a token was set o
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `Enabled` | `false` | Mount the endpoints. Off: the host opens no HTTP listener at all |
|
||||
| `Enabled` | `false` | Honoured by the Telegram host: on, it mounts the endpoints; off, it opens no HTTP listener at all. The sample host ignores it and always maps the endpoints |
|
||||
| `PublicUrl` | — | External base URL for the agent card's interfaces and for push webhook URLs. Unset: built from the inbound request, which is wrong behind a reverse proxy (the request scheme may be `http`) and means push webhooks can't be registered reliably. Set it in production |
|
||||
| `MaxHops` | `3` | Recursion guard: `metadata["volcanic.hops"]` (request metadata, then message metadata, else `0`); over the limit, the task is created and immediately `failed` |
|
||||
| `SessionIdleTimeout` | `00:30:00` | Idle `AgentSession` eviction (resumable from JSONL) |
|
||||
@@ -132,11 +134,21 @@ zone into its sessions (`docs/telegram-bot.md#a2a`); a host that embeds the libr
|
||||
## Security
|
||||
|
||||
- **Inbound text is untrusted.** Every A2A message is wrapped in a framing header telling the model it is talking
|
||||
to an external agent, not its owner, and not to follow instructions that conflict with the owner's interests.
|
||||
to an external agent, not its owner, and not to follow instructions that conflict with the owner's interests
|
||||
(it mentions `notify_owner` only when the host registered an owner notifier, i.e. when the tool exists).
|
||||
- **The isolated session still has `bash` and file tools in the shared workspace**, and loads the same
|
||||
`AGENTS.md`/`CLAUDE.md`, `PERSONA.md` and `MEMORY.md` as the owner's own session. It has no calendar and no
|
||||
access to the owner's conversation history, but it *can* be talked into editing `MEMORY.md` or `PERSONA.md`,
|
||||
which then affects the owner's own chat. This is an accepted exposure for v1, not a bug.
|
||||
- **What `bash` in the shared workspace reaches.** The isolated session has no `schedule`, `calendar` or `trigger`
|
||||
tool, but the files behind them live in the same workspace, so a caller can talk it into:
|
||||
- reading `.volcanic/a2a/callers.json` — every caller's plaintext key — and editing it, i.e. minting itself (or
|
||||
anyone) new keys or changing other callers' entries;
|
||||
- writing `.volcanic/scheduler/jobs.json` or `.volcanic/triggers/triggers.json`, whose prompts later run in the
|
||||
owner's **main** session — the one that has the calendar and the owner's history.
|
||||
|
||||
These are accepted v1 exposures: the owner chose plain files with no protection. Give A2A keys only to agents you
|
||||
would trust with that workspace.
|
||||
- **Keys are plaintext in `callers.json`**, by design — it's the owner's own file to edit. They are never written
|
||||
to logs; `/a2a` (Telegram) shows only the last four characters, and only for keys of 12 characters or more.
|
||||
- **Outbound HTTP is guarded.** Push webhook deliveries and file-part URL downloads both go through the same
|
||||
@@ -147,6 +159,9 @@ zone into its sessions (`docs/telegram-bot.md#a2a`); a host that embeds the libr
|
||||
- **Two continuations racing the same `input-required` task**: both pass the pre-check before either's run starts;
|
||||
the loser gets an error back (`InvalidAgentResponse` for a blocking `SendMessage`) and its message may remain in
|
||||
the task's history even though it was never acted on.
|
||||
- **Known v1 resource limits.** Each new `contextId` opens another session for its caller, so a caller can hold
|
||||
many open sessions at once until `SessionIdleTimeout` evicts them; there is no per-caller cap. Session JSONL
|
||||
directories (one per context), task files and `inbox/` files grow without a total cap — nothing prunes them.
|
||||
- Push deliveries for one task can arrive out of order (each is an independent fire-and-forget retry loop); use the
|
||||
task's own `status.timestamp`, not arrival order, to tell which update is newest.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user