docs(a2a): security reach, ports and limits

This commit is contained in:
EugeneTes
2026-10-07 14:32:33 +00:00
parent fdb0bde798
commit 3360c320e7
4 changed files with 51 additions and 15 deletions

View File

@@ -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.