feat: A2A v2 — external agents, armed indicator, delegation that fires (CLEAN-95) - #86
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
…CLEAN-74) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
… listing (CLEAN-74) Phase 1-2 (part): env vars, the agent/peer slice skeleton with the A2A 1.0 protocol types, the AgentPeer + AgentDelegation models and their additive migration, getApiPublicUrl on the infra config gateway, and a per-request tool-listing filter in the MCP runtime. PUBLIC_API_URL is reused rather than a new variable: CLEAN-75 already introduced it for the OAuth callback, and it is the same public origin an agent card URL needs. It was undocumented, so it is now in .env.example and the k8s deployment too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
…t (CLEAN-74) Phase 2 (rest). The hub now remembers which turn an agent is in the middle of, learned purely from the thinking events it already relays. That is what lets the API add a delegation step to a timeline the runtime opened, instead of minting a turnId of its own — a step under an unknown turn closes the runtime's block in every console that renders thinking. The register/resolve/timeout dance of POST /message/sync moves into BridleSyncService so the A2A server can wait for an agent reply without being an HTTP controller. The route's own behaviour is unchanged, timeout sentence included; the service reports a timeout as a flag instead, because a peer that answers "Timeout: no response from agent" as if it were an answer is the silent failure this feature exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
US1. Every agent now has an A2A 1.0 card, derived on every read from its name, description, template skills and bound knowledge bases — so an edit shows up without a "regenerate" button existing at all. It is served at the spec's well-known path, prefixed per agent because one host serves many, and it needs a credential: anonymous discovery would let anyone enumerate what an installation can do. The JSON-RPC endpoint turns an inbound task into one synchronous conversation with the runtime. Every failure is a stated task state rather than silence: an offline peer fails at once instead of after the full wait, a timeout says so, and a chain that would loop or run too deep is refused before the hub is touched. The caller is a language model — it would fill any silence itself. Loop and depth checks landed here rather than as a placeholder for US5: the rule is fifteen lines and belongs on the receiving side of every hop, which is also the only place that cannot be talked past by the agent doing the asking. Both A2A routes answer raw bodies. The response interceptor gained a @RawResponse escape hatch for exactly this: a route obeying somebody else's published protocol. It already injected Reflector without using it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
…-74) US2 (API half). A connection is created before its card is read, because the card route needs the credential — so reading the card IS the proof that the connection works, and a failed read deletes the row. "Connected" therefore always means "we checked", and an operator can never hold a peer that quietly does not answer. Refresh keeps the old snapshot when a read fails: a stale description is worth more than none, and the operator is told what broke. Removing a connection is what revokes the credential — there is no second copy of it anywhere. The controller projects each connection into its DTO field by field. The service already drops the credential; naming the fields means a later change to either side cannot put it back. A poisoned stub in the spec proves it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
… (CLEAN-74) US2 (console half). The tab leads with the agent's own card, because "what do other agents see of me" is the question an operator has before they can judge anybody else's. The picker shows a candidate's card before anything is saved: connecting decides what a model will be told it can ask for, so the operator reads the same words it will. Two things the UI says that nothing else would. A pod reads its tool list once at boot, so a connected peer is invisible until the agent restarts — the tab says so rather than leaving someone to wonder why nothing happened. And two peers whose descriptions read alike get an amber note, because the model is then choosing between them by chance. The console gateway unwraps errors rather than only data: the generated client returns a failure in `error` instead of throwing, and "Could not read the card of «B». The card answered 401" is precisely the sentence that would have been dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
US3 and the server half of US4. The tool is served through the MCP channel agents already use, so peers ship without rebuilding a single runtime image. It is listed only for agents that actually have peers: a description saying "you have none" would still be an advertisement for colleagues that do not exist, and models act on tool lists rather than on caveats inside them. Ordering carries the guarantees. The audit row opens before the outbound call, so a crash mid-flight still leaves evidence. The step is pushed before the wait, so the chat never looks idle. The row is finished exactly once, whatever happened. Every failure comes back as a sentence ending in "do not answer on its behalf". That instruction is the only thing standing between a peer being unreachable and the calling model quietly inventing what it would have said. The step joins the turn the runtime already opened, learned from the hub. With no turn to join the delegation runs anyway and the step is simply not shown — inventing a turnId would close the runtime's own block in every console. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
US4 (console half). The delegation step gets a layout in the admin chat: the peer, the card skills that made it the choice, the agent's own reason, the task as the peer received it, and a clock that runs while the answer is awaited. The clock ticks only while waiting — a frozen number reads as a stalled chat, which is the dead air this timeline exists to remove. The markdown detail is suppressed for these steps and only these: it exists so surfaces without this layout still get the whole story, and showing both here would say everything twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
…N-74) Lint on the new slice found a real defect rather than only style: the "card never mentions peers" assertion had a literal backspace where a word boundary was meant, so it had been matching nothing. It now checks the skill text word by word, which is also readable without knowing regex trivia. The spec harnesses hold their mocks directly instead of reading methods off a casted interface. The two controller specs keep one justified rule disable: Nest's Reflector genuinely wants the method reference. Two research facts are marked superseded rather than quietly edited, because the branch was rebased onto work that changed them: the user console does render thinking steps now (CLEAN-76), which turns the graceful-degradation case from hypothetical into something a real surface exercises; and the public API origin is the existing PUBLIC_API_URL, not a new variable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
…t (CLEAN-74) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ9dCrzZqd5SwR4erLYe12
…LEAN-90) ConnectPeerDto used @IsUUID which rejected real agent ids (`agent-…`). Admin create/edit now writes config.description for the A2A card. Co-authored-by: Cursor <cursoragent@cursor.com>
… (CLEAN-91) Implements the 'A2A Peers' design canvas: card summary header with address copy and advertise warning, peers drawn as a tree off a hub node with status dots and per-peer Feed filter, delegation feed that polls every 5 s with Pause/Resume and expandable task/reason/response rows, and the Add peer picker moved into a modal (card preview before Connect kept). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…N-90) The committed clients were missing the peer endpoints entirely in app and partially in admin; regenerated from the merged api so both match what build:api produces locally and in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…N-74) Same reason as on the CLEAN-90 branch: the committed clients were missing peer endpoints (app entirely); regenerated from this branch's api so they match what build:api produces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-and-description # Conflicts: # admin/slices/setup/api/data/repositories/api/schemas.gen.ts # admin/slices/setup/api/data/repositories/api/types.gen.ts # app/slices/setup/api/data/repositories/api/schemas.gen.ts # app/slices/setup/api/data/repositories/api/types.gen.ts
The Add peer preview rendered below the whole candidate list — under the fold with ten agents, so selecting looked like nothing happened. Each row is an accordion now: the card and Connect button open right under the click, and an Info pill with a chevron says the row is clickable and which one is open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…AN-91) DateTimeAgo stacks the absolute date over the relative one, which wrapped awkwardly after the "card read" label and inside the feed's meta line. New formatTimeAgo renders a single relative phrase with the full date in the tooltip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… helper (CLEAN-91) Same useTimeAgoIntl the stacked DateTimeAgo already uses, so the wording matches and the label keeps ticking without a re-render; drops the formatTimeAgo duplicate added one commit earlier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0.3.51 is already tagged; a matching tag would silently skip the release run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (CLEAN-95) Production baseline captured 2026-09-16: Rancher+Skyhunter connected, empty card, zero delegations ever. Three clarification markers open (Q1 замещение scope, Q2 prefer-the-peer policy, Q3 restart-to-arm). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… one-click restart (CLEAN-95) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(CLEAN-95) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…legacy alias (CLEAN-95) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…R-011 tool text (CLEAN-95) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security-review findings on the previous commit: outbound fetches now refuse redirects (a 302 was a clean guard bypass); IPv6 literals are allowed only in global unicast 2000::/3 (covers ::ffff-mapped v4, unabbreviated loopback, ULA, link-local); curl-style numeric IPv4 shorthand is refused; and a pre-flight DNS check catches public names resolving to private addresses on all three external paths. Residual rebinding TOCTOU documented as accepted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion transport (CLEAN-95) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…by URL (CLEAN-95) Tab renamed Peers → A2A (key too; legacy ?tab=peers aliased). Armed / pending-restart chip from peers/state; restart banner is a one-click action now. Add peer dialog gains the import-by-URL path with the card preview before Connect; external rows badged, no live status, no agent link; feed filter keys on the connection id and marks external rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DK (CLEAN-95) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'advertises nothing' surfaces now say the consequence (reachable by name only, never by topic) and the fix, in place: the A2A tab explains what fills the card (description via Edit, template skills, knowledge bases) with a jump to the Knowledge tab; the picker warns at the moment of Connect — internal candidates link straight to their Knowledge tab, external ones say to ask the owner and Re-read; peer rows carry the same guidance. CardView keeps the bare fact so the embedding surfaces don't repeat it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maksymhryzodub-prog
changed the base branch from
feat/CLEAN-94-add-peer-inline-info
to
main
September 16, 2026 17:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A2A v2 (spec:
specs/014-a2a-protocol-upgrade/). Production baseline that motivated it: Rancher + Skyhunter connected, empty card, zero delegations ever.ask_agentdescription now carries the give-up rule — never answer "I don't know" while a plausible peer is untried; a named peer is always asked. The tool records which peer set the running pod actually received.GET /agents/:id/peers/statecompares the served membership hash with the current peer set; the A2A tab shows the chip and the restart banner gained a one-click Restart now.POST /agents/:id/peersaccepts{url, token?};POST peers/previewreads a card without saving (the operator reviews the exact text first). Both URL forms canonicalize to one identity; re-import updates in place (partial unique index enforces it); external rows are badged, have no live status, and delegate with the stored write-only credential.A2A_ALLOW_PRIVATE_PEERS=truelifts the guard for local dev only.?tab=peersdeep links are aliased.Validation
PEER_URL_INVALID(SSRF),PEER_BODY,PEER_URL_UNREACHABLEin 74 ms.Remaining
Jira: https://dreamvention.atlassian.net/browse/CLEAN-95
🤖 Generated with Claude Code