|
| 1 | +Release: x402 v2 Hardening, Private Skills, Multi-Chain Payments |
| 2 | +Version: 2026.20.3 |
| 3 | +Date: May 13, 2026 |
| 4 | + |
| 5 | +OVERVIEW |
| 6 | +-------- |
| 7 | +This release is a security-and-correctness pass with two operator-facing |
| 8 | +features riding along. On the security side, the x402 payment middleware |
| 9 | +was rewritten on top of the x402 SDK v2 — that single migration closes |
| 10 | +four payment-bypass bug classes (body-parse fail-open, replay, unverified |
| 11 | +signatures, skipped balance check) and clears every high-severity x402 |
| 12 | +entry from bugs/known-issues.md. Three additional server bug classes are |
| 13 | +closed in this release: context_id parsing now rejects malformed UUIDs |
| 14 | +with JSON-RPC -32602 instead of fabricating new contexts; task cancel is |
| 15 | +now CAS-protected and no longer mis-reports state under a worker race; |
| 16 | +and a new optional `auth.allowed_dids` admission allowlist gates which |
| 17 | +Hydra-registered DIDs can reach handlers at all. The frontend drops |
| 18 | +parquetjs (removing 130+ transitive packages and two unpatched Apache |
| 19 | +Thrift advisories), and roughly 90 Dependabot alerts are patched via |
| 20 | +lock refresh. On the feature side, the x402 middleware is now |
| 21 | +operator-extensible to any EVM chain via `extra_networks` (SKALE Europa |
| 22 | +ships as the worked example), and agents can expose a second skill |
| 23 | +surface — `private_skills` — gated by Hydra auth + a DID allowlist, for |
| 24 | +operators whose skill descriptions are themselves the commercial product. |
| 25 | +Documentation got substantial work: PAYMENT.md and PRIVATE_SKILLS.md |
| 26 | +were rewritten in the README's teaching voice; the README itself was |
| 27 | +slimmed to a transformers-style layout and now leads with the Trade |
| 28 | +Compliance OS pitch. |
| 29 | + |
| 30 | +PROBLEM SOLVED |
| 31 | +-------------- |
| 32 | +Before this release: |
| 33 | +- The x402 payment middleware on v0.2.1 carried four payment-bypass |
| 34 | + shapes: a bare `except Exception` swallowed body-parse errors and |
| 35 | + fell through to the handler; there was no nonce store so signed |
| 36 | + payloads could be replayed indefinitely; signature verification was |
| 37 | + a stub method that never actually ran EIP-712 recovery; and the |
| 38 | + balance-check path had a fall-through branch that allowed requests |
| 39 | + when the asset contract had no code on chain. Combined, these were |
| 40 | + the four high-severity x402 entries in bugs/known-issues.md. |
| 41 | +- `_parse_context_id` in the A2A task manager logged a warning and |
| 42 | + generated a fresh UUID whenever a client sent a malformed |
| 43 | + `context_id`. The caller thought they were continuing conversation |
| 44 | + X and silently started a new isolated one; the old context was |
| 45 | + orphaned in storage, and an attacker could cheaply inflate storage |
| 46 | + by sending unbounded distinct garbage values. |
| 47 | +- The cancel_task handler read state, compared to terminal_states, |
| 48 | + then called `scheduler.cancel_task` with no atomic guarantee between |
| 49 | + the two steps. A worker that completed mid-cancel left the handler |
| 50 | + cancelling an already-terminal task and returning a misleading |
| 51 | + response that didn't reflect the actual final state. |
| 52 | +- After Hydra introspection + DID signature verification, any |
| 53 | + Hydra-registered DID could call any agent. There was no admission |
| 54 | + layer above auth: in multi-tenant or shared-Hydra deployments, a |
| 55 | + third party could `hydra create oauth2-client` with their own DID, |
| 56 | + obtain a valid token, and burn the agent's compute / LLM budget. |
| 57 | +- Skill descriptions on `/.well-known/agent.json` leaked the |
| 58 | + operator's product menu to the public. Operators whose skill |
| 59 | + catalogue IS the commercial product (compliance, legal, regulated |
| 60 | + workflows) had no way to expose skills to authorized partners |
| 61 | + without exposing them to the world. |
| 62 | +- x402 v2 ships built-in pricing for Base mainnet and Base Sepolia |
| 63 | + only. Operators wanting SKALE / Polygon / Avalanche / any other EVM |
| 64 | + chain had to choose between forking the SDK or shipping dead code. |
| 65 | + Four prior PRs (#486, #496, #507, #528) attempted patches that the |
| 66 | + v2 migration invalidated. |
| 67 | +- The frontend depended on parquetjs (inherited unmodified from |
| 68 | + huggingface/chat-ui) for an HF-internal dataset-export endpoint |
| 69 | + Bindu doesn't run. parquetjs pulled in Apache Thrift with two |
| 70 | + unpatched high-severity advisories (path traversal / response |
| 71 | + splitting in <=0.22.0, uncontrolled recursion in <0.23.0). |
| 72 | +- ~90 Dependabot alerts had accumulated across the Python and JS |
| 73 | + dependency trees. |
| 74 | +- The TruffleHog pre-commit hook ran with `--results=verified,unknown`, |
| 75 | + giving a 7% signal-to-noise ratio (2 verified hits out of 27 in a |
| 76 | + historical scan — the rest were docker-compose DSNs, .env.example |
| 77 | + files, config samples). Every PR went red on this check; reviewers |
| 78 | + learned to ignore the alarm. |
| 79 | +- `ty` had accumulated 71 diagnostics since the last cleanup, so the |
| 80 | + pre-commit ty hook failed for every commit on main. |
| 81 | + |
| 82 | +After this release: |
| 83 | +- x402 middleware delegates verification, settlement, and balance |
| 84 | + checks to `x402ResourceServer` / `HTTPFacilitatorClient`. The |
| 85 | + body-parse handler is a narrow `except (JSONDecodeError, |
| 86 | + UnicodeDecodeError)` returning 402; the new `nonce_store.py` claims |
| 87 | + `(network, asset, nonce)` BEFORE the facilitator round-trip via |
| 88 | + Redis SETNX (with InMemoryNonceStore fallback for dev); signature |
| 89 | + verification runs full EIP-712 recovery on the facilitator; the |
| 90 | + balance-check fall-through is removed. Real-facilitator smoke |
| 91 | + confirms forged signatures are rejected with |
| 92 | + `invalid_exact_evm_signature` before the handler runs. |
| 93 | +- Malformed `context_id` now raises `MalformedContextIdError` from |
| 94 | + `_parse_context_id`; send_message and stream_message catch it and |
| 95 | + return a JSON-RPC -32602 ("Invalid params") response echoing the |
| 96 | + offending value. No storage write, no orphaned context, no |
| 97 | + amplification primitive. |
| 98 | +- The Storage ABC gains `update_task_state_if(task_id, from_state, |
| 99 | + to_state) -> bool`. Postgres implements it as a single conditional |
| 100 | + `UPDATE ... WHERE state = :from RETURNING id`; the in-memory backend |
| 101 | + implements it as a compare-and-swap with no inter-await yield. |
| 102 | + cancel_task CAS-claims the transition before signaling the |
| 103 | + scheduler — on a CAS miss it re-loads and reports the actual |
| 104 | + post-race state. |
| 105 | +- New `AuthSettings.allowed_dids: list[str] | None = None`. When None |
| 106 | + (default), behavior is unchanged. When set, the Hydra middleware |
| 107 | + runs an admission check after signature verification and rejects |
| 108 | + unlisted DIDs with HTTP 403 before any handler is reached. |
| 109 | +- New `private_skills` and `allowed_dids` keys on AgentManifest. When |
| 110 | + configured, a second endpoint `/agent/private.json` returns the |
| 111 | + merged catalog (public + private skills) only to DIDs on the |
| 112 | + allowlist; opt-in only, the route isn't registered when unused. |
| 113 | +- New `X402Settings.extra_networks` config surface: a Pydantic dict |
| 114 | + keyed on friendly chain slug carrying CAIP-2, asset address, and |
| 115 | + EIP-712 domain metadata. SKALE Europa ships as the default entry |
| 116 | + matching `facilitator.x402.fi`'s `/supported` response. Operators |
| 117 | + add chains by extending the dict — no Bindu code change required. |
| 118 | +- parquetjs and the unused `/admin/export` route are gone from the |
| 119 | + frontend. 130+ transitive packages and both Thrift advisories drop |
| 120 | + out of the lockfile. |
| 121 | +- pypdf, cryptography, cdp-sdk, pynacl, web3, ip-address, and several |
| 122 | + transitive bumps (aiohttp, urllib3, GitPython, Mako, mistune, |
| 123 | + langchain-core, python-multipart) land via direct + lock-refresh |
| 124 | + upgrades. |
| 125 | +- TruffleHog now gates only on verified hits. Unverified hits log as |
| 126 | + warnings but don't fail the build. Signal-to-noise jumps from 7% to |
| 127 | + 100% on a historical scan of this repo. |
| 128 | +- ty diagnostics are at zero. Several were real (variance issues on |
| 129 | + `List[…]` parameters narrowed to `Sequence` / `Mapping`); the rest |
| 130 | + are documented exercising-error-paths in tests or runtime patterns |
| 131 | + ty doesn't see through. |
| 132 | + |
| 133 | +HEADLINE FEATURES |
| 134 | +----------------- |
| 135 | +Operator-extensible EVM chains for x402 |
| 136 | + Add any EVM chain by extending one Pydantic dict. Each entry registers |
| 137 | + a money parser scoped to its CAIP-2 so the SDK's built-in Base parser |
| 138 | + still wins for Base. SKALE Europa ships as the default extra network, |
| 139 | + matching `facilitator.x402.fi`'s advertised bridged-USDC asset |
| 140 | + (decimals=6, name="USDC", EIP-712 version="2") exactly. |
| 141 | + |
| 142 | +Private skill catalogs gated by DID allowlist |
| 143 | + Two new optional keys on AgentManifest — `private_skills` and |
| 144 | + `allowed_dids` — expose a second endpoint at `/agent/private.json`. |
| 145 | + The Hydra middleware fronts it like any non-public route; the handler |
| 146 | + adds an allowlist check that rejects authenticated-but-unlisted DIDs |
| 147 | + with 403. Closes the IP-leak shape (#538) with auth gating rather than |
| 148 | + encryption-at-rest; the JWE/E2E option remains as a Phase 2 if a real |
| 149 | + customer demands "operator doesn't trust the server itself" semantics. |
| 150 | + |
| 151 | +SECURITY |
| 152 | +-------- |
| 153 | +- x402 SDK >=2.3.0,<3 (was ==0.2.1). Clears the x402 SDK CVE and |
| 154 | + replaces the manual middleware with the v2 ResourceServer pattern. |
| 155 | +- All four high-severity x402 bug classes are closed and removed from |
| 156 | + bugs/known-issues.md. Bindu Core's High count goes from 4 to 0. |
| 157 | +- Three medium-severity server bug classes are closed: |
| 158 | + `context-id-silent-fallback`, `task-cancel-check-then-act-race`, |
| 159 | + `did-admission-control-missing`. |
| 160 | +- DID admission control is now configurable (`auth.allowed_dids`). |
| 161 | + Operators running shared Hydra or multi-tenant deployments can |
| 162 | + restrict which DIDs reach handlers even when Hydra would issue them |
| 163 | + a valid token. |
| 164 | +- parquetjs removed: 130+ transitive packages drop out, including the |
| 165 | + two unpatched Apache Thrift advisories. |
| 166 | +- pynacl ==1.5.0 → >=1.6.2,<2 (libsodium incomplete-disallowed-inputs). |
| 167 | +- web3 ==7.13.0 → >=7.15.0,<8 (SSRF via CCIP Read OffchainLookup). |
| 168 | +- cryptography ==44.0.2 → >=46.0.5,<47. |
| 169 | +- pypdf <6 → >=6.10.2,<7 (clears 44 alerts via lock refresh). |
| 170 | +- cdp-sdk ==0.21.0 → >=1.45.0,<2 (frees the cryptography pin). |
| 171 | +- Frontend: ip-address ^9.0.5 → ^10.1.1 (Address6 XSS); cookie ^0.7.0 |
| 172 | + override; minimatch ^9.0.7 override scoped to typescript-estree. |
| 173 | +- TruffleHog pre-commit hook tightened to `--results=verified` so the |
| 174 | + scanner stays usable. |
| 175 | + |
| 176 | +PERFORMANCE IMPACT |
| 177 | +------------------ |
| 178 | +- x402 v2 middleware: replay short-circuits at the nonce store before |
| 179 | + any facilitator round-trip, so duplicate-payment requests are now |
| 180 | + cheaper to reject than they were to accept under v0.2.1. |
| 181 | +- Admission control: a Python `in` check on a list. Operators with |
| 182 | + long allowlists may want to set a pattern-match variant in a future |
| 183 | + release (Option B in the design sketch); exact-match is shipping |
| 184 | + first because it covers the common case. |
| 185 | +- CAS cancel: one extra UPDATE on Postgres, one extra dict read on |
| 186 | + in-memory storage. Both inside the existing request span. |
| 187 | +- The Storage ABC change is additive: existing backends gain one new |
| 188 | + method but their hot paths are unchanged. |
| 189 | + |
| 190 | +TESTING |
| 191 | +------- |
| 192 | +- 993 unit tests passing, 3 skipped, 12 warnings, runtime ~3.3s on |
| 193 | + this machine. ruff clean. ty: 0 diagnostics. |
| 194 | +- 7 new admission tests in |
| 195 | + tests/unit/server/middleware/test_hydra_admission.py covering the |
| 196 | + default admit-all behavior, exact-match hits, misses, the deny-all |
| 197 | + empty-list posture, missing client_did, and a 403-doesn't-forward |
| 198 | + integration check. |
| 199 | +- 3 new storage CAS tests in tests/unit/server/storage/ |
| 200 | + test_memory_storage.py (CAS success, state mismatch, missing task). |
| 201 | +- 2 new task handler tests covering the cancel race scenario: |
| 202 | + CAS-success happy path; CAS-failure does NOT signal the scheduler |
| 203 | + and reports the actual post-race state. |
| 204 | +- 7 new context-id tests in tests/unit/server/test_task_manager.py |
| 205 | + covering all four `_parse_context_id` branches plus the |
| 206 | + `_create_error_response` code-override path. |
| 207 | +- 1 new regression test for `send_message` returning -32602 on |
| 208 | + malformed context_id. |
| 209 | +- 8 new tests for the private-skills endpoint exercising the auth + |
| 210 | + allowlist gate end-to-end with a stub middleware mirroring how |
| 211 | + Hydra middleware stamps request.state.user. |
| 212 | +- 11 new tests for `extra_networks` covering schema validation, money |
| 213 | + parser registration, non-default-decimals (WETH 18-decimals), and |
| 214 | + fall-through behavior. |
| 215 | +- 10 new nonce-store tests covering key construction, replay |
| 216 | + rejection, TTL expiry, and concurrent claim races. |
| 217 | +- 9 new x402 middleware tests exercising the full v2 dispatch path |
| 218 | + with a mocked ResourceServer. |
| 219 | +- 2 opt-in live-facilitator smoke tests at |
| 220 | + tests/integration/x402/test_skale_facilitator_supported.py (gated |
| 221 | + on X402_NETWORK_TESTS=1) confirm the shipped SKALE Europa default |
| 222 | + matches facilitator.x402.fi's `/supported` response. |
| 223 | + |
| 224 | +KNOWN ISSUES |
| 225 | +------------ |
| 226 | +- `bugs/known-issues.md::skale-facilitator-cert-expired` (low, ops): |
| 227 | + the only public x402 facilitator advertising SKALE chains is |
| 228 | + `facilitator.x402.fi`, whose TLS certificate is currently expired. |
| 229 | + Bindu's defaults still point at Coinbase's facilitator; SKALE |
| 230 | + operators set `X402__FACILITATOR_URL` themselves and either accept |
| 231 | + the cert error or front the upstream with a reverse proxy. This is |
| 232 | + not a code bug on the Bindu side — see the linked entry for the |
| 233 | + recommended production posture (self-host `x402-facilitator`). |
| 234 | +- `bugs/known-issues.md::authz-scope-check-behind-optional-flag` |
| 235 | + (medium, security): the scope-check flag (`auth.require_permissions`) |
| 236 | + defaults to False and authorization disappears when it's not flipped |
| 237 | + on. Deferred to the planned Ory Kratos migration rather than patched |
| 238 | + in place — the entry stays in known-issues.md. |
| 239 | +- `bugs/known-issues.md::no-rate-limit-or-quota-per-caller` (medium): |
| 240 | + no per-DID quota or body-size limit on the Starlette app. Operators |
| 241 | + exposing Bindu directly on the public internet should add their own |
| 242 | + rate limiting at the proxy until this lands natively. |
| 243 | +- The historical Auth0 client_secret values committed in Oct 2025 at |
| 244 | + e28e3417 are still reachable via `git log` (removed from HEAD but |
| 245 | + the blob persists). Rotation is an operator action, not a code fix. |
| 246 | + |
| 247 | +FILES CHANGED |
| 248 | +------------- |
| 249 | +Since 2026.20.2: 74 files changed, +5366 insertions, −3946 deletions. |
| 250 | +Headline groupings: |
| 251 | + |
| 252 | +Security & correctness: |
| 253 | +- bindu/server/middleware/auth/hydra.py (admission helper + check) |
| 254 | +- bindu/server/handlers/task_handlers.py (CAS-based cancel) |
| 255 | +- bindu/server/handlers/message_handlers.py (malformed context_id |
| 256 | + → -32602) |
| 257 | +- bindu/server/task_manager.py (raise instead of fallback; |
| 258 | + _create_error_response gains optional code override) |
| 259 | +- bindu/server/errors.py (new — MalformedContextIdError) |
| 260 | +- bindu/server/storage/base.py (abstract update_task_state_if) |
| 261 | +- bindu/server/storage/memory_storage.py (in-memory CAS) |
| 262 | +- bindu/server/storage/postgres_storage.py (conditional UPDATE CAS) |
| 263 | +- bindu/settings.py (new auth.allowed_dids + x402.extra_networks) |
| 264 | +- bugs/known-issues.md (six resolved entries removed + counters) |
| 265 | +- bugs/core/2026-05-12-x402-v2-migration-hardening.md (new postmortem) |
| 266 | + |
| 267 | +x402 v2 migration: |
| 268 | +- bindu/server/middleware/x402/ (full rewrite around v2 ResourceServer) |
| 269 | +- bindu/server/middleware/x402/nonce_store.py (new — Redis SETNX + |
| 270 | + in-memory fallback) |
| 271 | +- bindu/server/applications.py (CAIP-2 normalisation, ExactEvmServerScheme |
| 272 | + money parser registration for extra networks) |
| 273 | +- tests/unit/server/middleware/x402/* (30 new tests) |
| 274 | +- tests/integration/x402/test_skale_facilitator_supported.py (gated) |
| 275 | +- pyproject.toml (x402 0.2.1 → 2.3.0+; pydantic-settings as direct dep) |
| 276 | + |
| 277 | +Private skills: |
| 278 | +- bindu/common/models.py (AgentManifest gains private_skills, |
| 279 | + allowed_dids) |
| 280 | +- bindu/penguin/manifest.py, bindufy.py, config_validator.py |
| 281 | +- bindu/server/endpoints/private_agent_card.py (new) |
| 282 | +- bindu/server/endpoints/__init__.py |
| 283 | +- bindu/server/applications.py (conditional route registration) |
| 284 | +- tests/unit/server/endpoints/test_private_agent_card.py (new, 8 tests) |
| 285 | +- examples/private_skills_agent/ (new — ACME Compliance walkthrough) |
| 286 | +- docs/PRIVATE_SKILLS.md (new — teaching-voice doc) |
| 287 | + |
| 288 | +Dependency hygiene: |
| 289 | +- pyproject.toml (pypdf, cryptography, cdp-sdk, pynacl, web3 bumps) |
| 290 | +- uv.lock (refreshed; ~90 Python alerts cleared) |
| 291 | +- frontend/package.json + lockfile (ip-address, cookie, minimatch; |
| 292 | + parquetjs + @types/parquetjs dropped) |
| 293 | +- frontend/src/routes/admin/export/ (deleted) |
| 294 | +- frontend/src/hooks.server.ts (deprecated PARQUET_EXPORT_SECRET |
| 295 | + fallback removed) |
| 296 | + |
| 297 | +Documentation: |
| 298 | +- README.md (slimmed to transformers-style; maintainer avatars; |
| 299 | + multi-chain x402 row; Trade Compliance OS pitch) |
| 300 | +- docs/PAYMENT.md (rewritten in teaching voice; end-to-end paywall |
| 301 | + walkthrough with success + failure paths against a mock facilitator) |
| 302 | +- docs/PRIVATE_SKILLS.md (new) |
| 303 | + |
| 304 | +CI / type cleanup: |
| 305 | +- .pre-commit-config.yaml (trufflehog gates on verified hits only) |
| 306 | +- bindu/common/protocol/types.py, several handler files (ty: ignore |
| 307 | + on intentional patterns; real type narrowing where applicable) |
| 308 | +- tests/conftest_stubs.py (deleted — was shadowing real opentelemetry |
| 309 | + and x402 installs) |
| 310 | + |
| 311 | +Tests (cumulative new): |
| 312 | +- ~80 new unit + integration tests across admission, CAS storage, |
| 313 | + context-id parsing, private skills endpoint, x402 nonce store, |
| 314 | + x402 middleware v2 dispatch, and extra_networks. |
| 315 | + |
| 316 | +CONTRIBUTORS |
| 317 | +------------ |
| 318 | +Lead maintainer: |
| 319 | +- Raahul Dutta (16 commits since 2026.20.2) |
| 320 | + |
| 321 | +Pair-programming assist: |
| 322 | +- Claude Opus 4.7 (1M context) — co-author on every commit in this |
| 323 | + release. |
0 commit comments