Releases: emiltsoi/hermes-agent-a2a
Release list
v3.3.9
v3.3.8
chore: bump to v3.3.8 — critical hotfix release Updates deprecation notice: 'no further releases' → 'critical hotfix releases only'
v3.3.7 — Extract Task Queue + Payloads; Unify Inbound/Outbound
v3.3.7 — Extract Task Queue + Payloads; Unify Inbound/Outbound Paths
Three architectural moves that complete the refactoring arc started in v3.3.3.
1. Extract task_queue.py (290 lines)
_PendingTask + TaskQueue decoupled from HTTP transport. The state machine for inbound tasks is now independent of the transport that receives them. server.py imports and re-exports for backward compatibility.
2. Extract payloads.py (121 lines)
A2A wire-format builders (_build_task_object, _build_status_update_payload, _build_paginated_task_list) as pure functions. No socket knowledge. No HTTP header knowledge. Just dict shapes.
3. Unify inbound and outbound paths
INBOUND — _prepare_and_enqueue() shared by all three send paths:
_handle_task_send(JSON-RPC)_rest_send_message(REST)_rest_send_message_stream(SSE streaming)
Security fix: The streaming endpoint previously bypassed _enqueue_and_await_task's idempotency and task-ID collision guards. Idempotent replay and task-ID collision attacks were possible through the SSE path that JSON-RPC and REST correctly blocked. _prepare_and_enqueue() closes this — all three paths now share one preamble.
OUTBOUND — _resolve_rpc_target() + _rpc_call() shared by:
handle_send_protocol_taskhandle_cancel_protocol_task_handle_call_mode3
_resolve_target (9 parameters, threw ValueError) → _resolve_rpc_target (1 parameter, returns structured error dicts). Callers got thinner. _rpc_call() consolidates the JSON-RPC POST + error-parse triad that was duplicated across three outbound paths.
Numbers
server.py: 2,044 → 1,490 (-554)
task_queue.py: 290 (new)
payloads.py: 121 (new)
─────────────────────────
Net: 8,344 → 8,147 (-197)
Tests: 625/625 ✅
Attribution
Refactoring by Emil. Britney gated and tested.
Previous
v3.3.6 — CI Flake Fix
v3.3.6 — CI Flake Fix: Concurrent Test Retry + Audit Directory
Two CI-environment fixes that reproduce only on GitHub Actions runners.
1. ConnectionResetError retry: 20 threads overflow TCP accept() on Python 3.12 CI even with the readiness probe. Added retry loop (3 attempts, 0.1s stagger). 5x stress runs: 70/70.
2. Audit log directory: /home/runner/.hermes/ doesn't exist in CI. mkdir(parents=True) in the a2a_server fixture. Fixes 12x FileNotFoundError noise.
625 passed, 15 warnings
v3.3.5 — Extract Shared Core + SSE Unification (1 bug fix)
v3.3.5 — Dead Code Removal + Safe Consolidation + SSE Unification
167 INSERTIONS, 329 DELETIONS — NET -162 LINES (-1.9%). 29 files (-3). 625/625 tests.
Phase 4 — Extract _enqueue_and_await_task() (~60 lines)
_handle_task_send (195 lines) and _rest_send_message (99 lines) shared 70% — sanitize, idempotency, collision, enqueue, webhook, wait, filter. Extracted into shared core. Both callers keep their own preamble (configuration, worker_at bypass, return_immediately) and epilogue (RPC response format, SSE/push artifact delivery, REST inline dict formatting).
Phase 5 — Extract _stream_task_sse() (~70 lines + bug fix)
_handle_send_subscribe and _rest_subscribe_to_task were structurally identical — same poll loop, same 0.5s interval, same 300s timeout, same terminal detection. Extracted into unified _stream_task_sse(task_id, extra_headers, http_method).
Bug fixed: JSON-RPC version didn't check for client disconnect during terminal state transmit — wrote SSE events to dead socket silently. REST version checked correctly. The unified core backports the disconnect check from REST.
Full changelog: v3.3.5 on GitHub
Previous releases:
v3.3.4 — Deterministic CI (TCP Readiness Probes)
v3.3.4 — Deterministic CI (TCP Readiness Probes)
Fix: Flaky test_concurrent_requests_respect_limit — ConnectionResetError under CI load caused by 20 concurrent threads hitting the server before it finished binding. Replaced blind time.sleep(0.1) with _wait_for_server() — polls TCP until the port accepts a connection.
Stress test: 5 consecutive runs, 14×5 = 70 tests, zero flakes. 625/625 full suite.
=== run 1 === 14 passed in 9.12s
=== run 2 === 14 passed in 9.29s
=== run 3 === 14 passed in 8.63s
=== run 4 === 14 passed in 9.02s
=== run 5 === 14 passed in 9.00s
Previous: v3.3.3 — cross-model security audit.
v3.3.3 — Cross-Model Security Audit (4 HIGH fixes)
v3.3.3 — Cross-Model Security Audit
Three free Devin CLI models (swe-1.6, glm-5-2, kimi-k2.7) reviewed the same codebase with the same checklist. Different blind spots, richer union.
HIGH — Fixed
| # | File | Finding | Found by |
|---|---|---|---|
| 1 | a2a_direct.py:65-67 |
No SSRF guard on call() — arbitrary urlopen() without URL validation. webhook/push paths had guards; direct A2A didn't. Fixed: validate_target_url() before every call(). |
swe ✅ kimi ✅ |
| 2 | tool_handlers.py:171-202 |
No SSRF guard on webhook reachability check — _validate_webhook_reachable() opened arbitrary URLs. Fixed: validate_target_url() with allow_loopback=True before outbound I/O. |
swe ✅ kimi ✅ |
| 3 | test_telegram_float.py:62 |
Env var chain leak — test didn't cover A2A_V2_BOT_TOKEN, causing live token to appear in pytest diff. Fixed: test now covers all 4 tiers. Rotate the leaked token. |
swe ✅ glm ✅ kimi ✅ |
| 4 | server.py:806-858 |
Auth bypass on GET endpoints — /tasks, /tasks/{id}, /extendedAgentCard skipped _check_auth() even when A2A_REQUIRE_AUTH=true. Fixed: all task/card GET routes now respect auth. Escalated to HIGH by Linda. |
kimi ✅ |
LOW — Fixed
| # | File | Fix |
|---|---|---|
| 5 | identity.py:68,406 |
Silent except Exception: pass → logger.warning() |
| 6 | rate_limiter.py:186 |
Cleanup loop failure → logged instead of swallowed |
| 7 | server.py |
4 dead imports removed. Webhook host validation error → logged. |
| 8 | telegram_float.py:36-38 |
Docstring env chain updated to reflect A2A_V2_BOT_TOKEN |
Cross-Model Reviewer Performance
| Model | Findings | Missed | Notes |
|---|---|---|---|
glm-5-2 |
3 | 6 false negatives | ✅ Best coder, ❌ NEVER for review. Claimed "no auth gaps" when GET bypass exists. Confident + wrong. |
swe-1.6 |
9 | auth bypass | Speed king. Solid mechanical sweep — caught both SSRF gaps + all silent exceptions. |
kimi-k2.7 |
10 | — | Slow but caught the auth bypass both others missed. Different training = different blind spots. |
Rule codified: glm-5-2 codes. swe-1.6 + kimi-k2.7 review. Coding model ≠ review model.
Tests
625 passed in 88.37s
Attribution: Cross-model sweep by Britney (orchestration) + Linda (gate, escalation of #4). Dispatch via Devin CLI harness v1.9.0.
v3.3.2
v3.3.1 — SSRF Consolidation & Security Fixes
v3.3.1 — SSRF Consolidation & Security Fixes
Security
- ARCH-03:
sender_namenow usesA2A_AGENT_NAMEinstead of hardcoded "britney" - NEW-01: REST
POST /tasks/{id}:cancelnow requires auth (was bypassing_check_auth())
SSRF Consolidation
- 4 SSRF guard functions consolidated to 2 in
security.py:validate_hostandvalidate_target_url - Webhook delivery path now blocks private CIDRs (10.x, 172.16-31.x, 192.168.x)
- All 11 call sites use one shared threat model via
_is_private_ip
Commits
- b9d0ea2: ARCH-03 sender_name uses caller identity + NEW-01 auth gate
- a70e8a7: SSRF guards consolidated into security.py
- ef69933: CancelTask REST tests fix
Full review cycle: a2a-v3.3-full-20260603
v3.3.0
v3.3.0 — Transport module extraction (LOW-08) Transport module extraction: 3 leaf-level modules (telegram_float, a2a_direct, webhook_delivery) extracted from server.py. 227 lines of god-module code split into 3 focused files. +20 new tests across 3 new test files. Pytest 615/615. Review trail: a2a-review-20260602 Audit chain: 8 commits, each with discipline references - 8a226e9 (Task 3, telegram_float extraction) - 5534b5f (Task 4, a2a_direct extraction) - 1796c11 (Task 5, webhook_delivery extraction) - 69009a6 (Task 6, module-internal tests) - bd2d9dd (Task 7, preflight clean) - d92776d (release prep: CHANGELOG) - c5a5ba5 (release prep: unambiguous-fake test fixtures) See CHANGELOG.md for the full v3.3.0 entry.