Skip to content

fix(security): scope A2A task routes to their agent, and stop the ruff pin drifting - #6

Merged
piotrlaczkowski merged 2 commits into
mainfrom
claude/agentomatic-cli-templates-stb46v
Sep 9, 2026
Merged

piotrlaczkowski merged 2 commits into
mainfrom
claude/agentomatic-cli-templates-stb46v

Conversation

@piotrlaczkowski

Copy link
Copy Markdown
Contributor

The two items deferred out of #5, now that it has merged and a tooling change can't bury it.


1. Cross-agent access on the per-agent A2A task routes

GET /{agent}/a2a/tasks/{id}, its /events and /cancel accepted any task id, regardless of which agent owned it. Zero-trust derives its policy purely from the agent segment of the path, so a caller permitted on a permissive agent could name a restricted agent's task on their own agent's route.

Verified on a two-agent platform before the fix:

GET  /api/v1/faq/a2a/tasks/<payroll id>          -> 200   payload leaked
GET  /api/v1/faq/a2a/tasks/<payroll id>/events   -> 200   payload leaked
POST /api/v1/faq/a2a/tasks/<running id>/cancel   -> 200   task cancelled

The cancel is the sharper end — a cross-agent write that stops another agent's work. (My first probe showed 409 and I nearly wrote it off; that was only because the task had already finished. Re-testing against a running task showed it succeeding.)

All three now answer 404 — the same answer as a nonexistent id, so the response never confirms someone else's task exists.

TaskRecord already carries target, so this is a local check on routes that were already namespaced per agent. The shared task board at /api/v1/tasks/{id} is cross-agent by design and is deliberately untouched — a test asserts it still works.

2. The ruff pin drift

.pre-commit-config.yaml pinned ruff v0.8.6 while the dev dependency resolved to 0.15.x. On this tree the pinned version reports 34 lint errors and wants to reformat 9 files that the project's own ruff considers completely clean. Because the gate runs pre-commit over a PR's changed files, any PR touching one of those files failed on code it hadn't written — which happened twice on #5.

I had told you a bump would reformat the repo. That was backwards: the tree is already clean under ruff 0.15 (362/362 formatted, zero findings). The churn comes from staying on 0.8.6.

Rather than bump a number that will drift again, ruff now runs from uv run — exactly as the mypy hook already does — so there is one ruff, the one in uv.lock. Drift stops being possible instead of being corrected and re-drifting.

No source changes. The dev floor moves to ruff>=0.15 because the code genuinely no longer lints clean under 0.8, and claiming otherwise was misleading.


Testing

  • Both exploits reproduced, then asserted closed — including that a foreign task is indistinguishable from a missing one, and that the owning agent can still read, stream and cancel.
  • A test pins the deliberate boundary: the shared task board still serves cross-agent reads.
  • The new hooks verified in all three directions: an unused import fails ruff, bad spacing fails ruff-format, a clean file passes both. (A hook that silently did nothing would be worse than the drift.)

Gate: ruff check, ruff format --check, mypy (230 files), mkdocs build --strict clean; 2746 passed.

The one pre-existing failure is unrelated and untouched: test_save_read_only_storage_returns_actionable_conflict chmods a directory read-only, which root ignores. It passes on CI's non-root runner.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pk5bxkw7Q3j8VN1TvxeQ5V


Generated by Claude Code

… hook

The ruff-pre-commit hook pinned v0.8.6 while the dev dependency resolved to
0.15, and the two disagreed: on this tree the pinned version reports 34 lint
errors and wants to reformat 9 files that the project's own ruff considers
clean. Because the PR gate runs pre-commit over a PR's *changed files*, any
PR touching one of those files failed on code it had not written — which
happened twice on the streaming PR, once for formatting and once for UP038.

Source ruff from ``uv run``, as the mypy hook already does, so there is one
ruff: the one in uv.lock. Drift between the hook and the dependency stops
being possible rather than being corrected and re-drifting.

No source changes: the tree is already clean under the project's ruff (362
files formatted, no lint findings), so this is a tooling fix, not a
reformat. The dev floor moves to ruff>=0.15 because the code genuinely no
longer lints clean under 0.8, and claiming otherwise was misleading.

Verified the hooks still bite: an unused import fails ruff, bad spacing
fails ruff-format, and a clean file passes both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk5bxkw7Q3j8VN1TvxeQ5V
``GET /{agent}/a2a/tasks/{id}``, its ``/events`` and ``/cancel`` accepted any
task id regardless of which agent owned it. Zero-trust derives its policy
from the agent segment of the path, so a caller permitted on a permissive
agent could name a restricted agent's task on their own agent's route.

Verified before the fix, on a two-agent platform:

    GET  /api/v1/faq/a2a/tasks/<payroll id>          -> 200, payload leaked
    GET  /api/v1/faq/a2a/tasks/<payroll id>/events   -> 200, payload leaked
    POST /api/v1/faq/a2a/tasks/<running id>/cancel   -> 200, task cancelled

The cancel is the sharper end: a cross-agent *write* that stops another
agent's work. All three now answer 404 — the same answer as a nonexistent
id, so the response never confirms someone else's task is there.

TaskRecord already carries ``target``, so this is a local check on routes
that were already namespaced per agent. The shared task board at
``/api/v1/tasks/{id}`` is cross-agent by design and is deliberately
untouched; a test asserts it still works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk5bxkw7Q3j8VN1TvxeQ5V
@piotrlaczkowski
piotrlaczkowski merged commit 0a1eab4 into main Sep 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants