Skip to content

fix: A2A interop with real external agents (CLEAN-97) - #90

Merged
maksymhryzodub-prog merged 3 commits into
mainfrom
fix/CLEAN-97-a2a-interop
Sep 17, 2026
Merged

maksymhryzodub-prog merged 3 commits into
mainfrom
fix/CLEAN-97-a2a-interop

Conversation

@maksymhryzodub-prog

@maksymhryzodub-prog maksymhryzodub-prog commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Jira: CLEAN-97

External A2A agents mostly did not work with Ranch. The import and delegation circuit was fine; the interop was not. This fixes the four critical and three minor defects found by running real public agents through the real code, plus two more honesty gaps the same run surfaced.

Before and after

The same 41 public A2A 1.0 agents from a2aregistry.org, no auth, through the real PeerService.previewByUrl and DelegationService.run with production SSRF guards on:

Before After
Imported 40 40
Answered with content 6 36
Answered, but empty 3 0
Rejected: "answered without a task" 23 0
Skipped: first interface not JSON-RPC 4 0
Failed with a misleading cause 4 0
Failed, peer's own reason passed on 0 4

The 4 remaining failures are agents that demand structured input instead of text, and they now say exactly that. The five spellings of our own agent's URL that previously slipped past the self-import check on production are all refused now, before any request.

What changed

Critical

  • Message replies accepted. A blocking SendMessage may return a Message instead of a Task; most public agents do. The client returns either and delegation reads both.
  • The card's interface URL is vetted at import and external refresh. SSRF-guarded and refused when it points back into this installation. Existing rows fail delegation with PEER_ADDRESS_REFUSED instead of "it answered with an error".
  • Self-import check compares a normalized address, not a string prefix: host case, trailing dot, port, decoded and collapsed path, scheme ignored. A card whose interface names our base is refused whatever host served it.
  • Interface choice: the first JSON-RPC interface on 1.0, the same rule in import, delegation and the console's address line. A 1.0 card without JSON-RPC is refused with PEER_BINDING; an existing one fails with PEER_UNSUPPORTED.

Minor

  • Reply text from every part. Data parts become compact JSON, links stay links, binary is named but not inlined; empty artifacts fall back to the task's status message. Only when nothing is readable is the reply recorded as an explicit empty answer, and the tool tells the model not to invent one.
  • A 0.3 card reports its version instead of "not an agent card".
  • Card URLs that already name a .json document are kept as is, so /.well-known/agent.json and custom paths are no longer mangled.

Found during verification, same class

  • A JSON-RPC error sent with a 4xx status now passes the peer's reason on instead of "could not be reached".
  • Redirects are still never followed, but redirect: 'manual' reports them with their target, so the operator knows which address to paste instead of seeing "fetch failed".

Verification

Check Result
API tests 1073 passing, 83 suites
Peer slice tests 274 passing, including new a2a.types.spec.ts
API tsc --noEmit clean
Admin and app nuxt typecheck against a freshly generated SDK clean
ESLint on changed API files clean
Live run against 41 public agents table above

Notes for review

  • Generated SDKs are not committed. Only an API DTO description changed. Regenerating on this machine reformats every generated file, a 17k-line formatting diff, and the consoles regenerate the SDK on prebuild and pretypecheck anyway.
  • Pre-existing, not from this branch: npx nuxt typecheck in admin against the committed SDK fails in the knowledge slice, because CLEAN-96 added the retrying status without regenerating the committed client. bun run typecheck regenerates first and passes.
  • Not verified on production: the manual flow needs this deployed. The import, preview and delegation paths above ran with the real code against real agents; the chat side and the armed indicator are unchanged.
  • Version bumped to 0.3.54.

🤖 Generated with Claude Code

maksymhryzodub-prog and others added 3 commits September 17, 2026 18:50
A live run of 41 public A2A 1.0 agents from a2aregistry.org through the real
import and delegation code showed the circuit works and interop does not:
40 imported, 6 answered with content. After this change the same run gives
36 answers with content; the remaining 4 are agents that require structured
input, and they now say so.

- Message replies. A blocking SendMessage may be answered with a Message
  instead of a Task; most public agents do exactly that. The client returns
  either, and delegation reads the answer from both.
- Reply text. Every artifact part is read, not only the first artifact's
  text: data parts become compact JSON, links stay links, binary is named but
  not inlined. An empty artifact list falls back to the task's status
  message. Only when nothing anywhere is readable is the reply recorded as an
  explicit empty answer, and the tool tells the model not to invent one.
- Interface choice. The first JSON-RPC interface on 1.0 is used, not the
  first interface listed, in import and delegation alike. A 1.0 card without
  JSON-RPC is refused at import with PEER_BINDING.
- The card's own address. Import and external refresh now vet the chosen
  interface URL: SSRF-guarded, and refused when it points back into this
  installation. Rows saved before this fail delegation with
  PEER_ADDRESS_REFUSED or PEER_UNSUPPORTED instead of "it answered with an
  error".
- Self-imports. "Is this our own agent" compares a normalized address (host
  case, trailing dot, port, decoded and collapsed path, scheme ignored)
  instead of a string prefix. Doubled slashes and %61gents no longer pass,
  and a card whose interface names our base is refused whatever host served it.
- Honest causes. A 0.3 card reports its version instead of "not an agent
  card". A JSON-RPC error sent with a 4xx status passes the peer's reason on
  instead of "could not be reached". Redirects are still never followed, but
  are now reported with their target, so the operator knows what to paste.
- Card URLs that already name a .json document are kept as they are, so
  /.well-known/agent.json and custom card paths are no longer mangled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The card address line showed the first interface a card lists. The API now
calls the first JSON-RPC interface on 1.0, so a card that prefers HTTP+JSON
showed one address while delegations went to another. Both the card view and
the A2A tab header use the same rule as the API now.

The delegation feed also names the two new causes: a private interface
address that was refused before anything was sent, and a card with no
JSON-RPC interface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maksymhryzodub-prog
maksymhryzodub-prog merged commit 8b965f6 into main Sep 17, 2026
1 check 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.

1 participant