Skip to content

feat(a2a): re-sync stored Agent Cards and report drift - #7380

Closed
marekdano wants to merge 3 commits into
mainfrom
4734-a2a-card-resync
Closed

marekdano wants to merge 3 commits into
mainfrom
4734-a2a-card-resync

Conversation

@marekdano

Copy link
Copy Markdown
Collaborator

Summary

Slice 3 of #4734. Adds re-sync for the Agent Card stored for an agent. A changed remote card is detected and reported. A change that alters how the gateway calls the agent is not applied until the owner decides.

Refs #4734

Depends on the slice 2 PR #7378 (card storage). Review the commits after the slice 2 commit.

What changed

  • POST /a2a/{agent_id}/card/resync re-fetches the stored card from its source URL with If-None-Match.
    • A 304, or a card with no changes, changes nothing.
    • A descriptive change, such as description, skills, icon, or modes, applies at once as a new card version.
    • A behaviour-affecting change is stored as a pending drift. The current card and the agent stay unchanged.
  • POST /a2a/{agent_id}/card/drift takes apply or decline.
    • apply makes the drifted card current.
    • decline keeps the current card and leaves the agent marked drifted. Re-syncing the same remote card does not propose it again.
  • GET /a2a/{agent_id}/stored-card now returns drifted and drift. The drift holds the drifted card, its status, and a report of each change with its before and after values.
  • Alembic revision b7d1f4a9c2e6 adds status and drift columns to a2a_agent_cards. Existing rows become applied. The current card is the highest applied version. At most one pending or declined row is open per agent.

Which changes need confirmation

A change is behaviour-affecting when it alters how the gateway calls the agent:

Card field What it decides
supportedInterfaces (URL, binding, protocolVersion) Which adapter and endpoint the call uses
securitySchemes, securityRequirements The credential type and headers sent outbound
capabilities.streaming Whether SSE is attempted

Every other change is descriptive and applies without confirmation. Cards are compared in the 1.0 shape, so a card that moves from the 0.3 format to the 1.0 format reports no format-only drift.

What apply changes

  • Apply updates only the agent protocol_version and the card-owned capabilities.
  • Apply never rewrites endpoint_url, credentials, name, team, visibility, or the local description. A card cannot carry credentials, and an endpoint change needs deliberate validation.
  • When a card changes the endpoint or the auth scheme, the drift report lists endpoint_url or auth under unapplied. The owner updates those settings explicitly.

Security

  • Both routes require a2a.update and agent ownership. The ownership check runs before any outbound request.
  • Both routes are in the token scoping map. A token with only a2a.read or a2a.invoke is denied.
  • The re-fetch reuses the outbound URL policy, DNS pinning, no-redirect rule, and size cap from the slice 1 PR.
  • A failed re-fetch returns one generic error and changes nothing.
  • Deny-path tests cover a non-owner on both routes, missing a2a.update on both routes, and the token scope.

Not in this PR

  • Scheduled re-sync. Re-sync runs on demand only.
  • Skipping the re-fetch while max_age is fresh.
  • Re-sync of a card that needs credentials. The re-fetch sends no stored credentials.
  • Linking a hand-registered agent to a card later.
  • Fetching the extended card.

…stration

  Closes part of #4734 (slice 1: fetch route).

  Add a route that fetches an A2A Agent Card from an unregistered endpoint.
  The route accepts baseUrl, an optional path, and optional headers.

  - Try /.well-known/agent-card.json first. Fall back to
    /.well-known/agent.json only on a 404.
  - Normalize a 0.3 card to the 1.0 shape. Return the normalized card, the
    card as fetched, and a suggested mapping onto A2AAgentCreate fields.
    The mapping never contains credentials, team, visibility, or tags.
  - Require a2a.create, because the gateway sends a request to a URL the
    caller chose. Add the route to the token scoping map.
  - Apply the outbound URL policy and DNS pinning. Do not follow redirects.
    Cap the response body at 1 MiB.
  - Return one identical found:false response for every failure, so the
    route cannot reveal which hosts the gateway can reach.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
Refs #4734 (slice 2: card storage).

Add the a2a_agent_cards table. Store the Agent Card exactly as fetched,
separate from the deployment settings on a2a_agents.

  - Add one row per fetched card version. Keep the source URL, detected
    card version, ETag, Cache-Control max-age, and fetch time.
  - Accept an optional card_url on POST /a2a. Fetch the card on the
    server before any write. Fail the registration when no card is found.
    Never accept a card document from the client.
  - Add GET /a2a/{agent_id}/stored-card. It returns the current stored
    card and applies the same visibility check as the agent read.
  - Return etag and maxAge from POST /a2a/fetch-card.
  - Add Alembic revision a4c8e2f1b7d9 for the new table.

Registration without card_url is unchanged.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
  Refs #4734 (slice 3: re-sync and drift).

  Add a re-sync for the Agent Card stored for an agent. Apply descriptive
  changes at once. Hold changes that alter how the gateway calls the agent
  until the owner decides.

  - Add POST /a2a/{agent_id}/card/resync. Re-fetch the source URL with
    If-None-Match. Treat a 304 or an identical card as unchanged.
  - Gate changes to supportedInterfaces, securitySchemes,
    securityRequirements, and capabilities.streaming. Store the new card
    as a pending drift. Keep the current card and the agent unchanged.
  - Apply all other changes as a new card version. Update only the agent
    protocol_version and the card-owned capabilities.
  - Add POST /a2a/{agent_id}/card/drift to apply or decline a drift.
    A declined drift leaves the agent usable and marked drifted. Do not
    propose the same declined card again.
  - Return drifted and drift from GET /a2a/{agent_id}/stored-card.
  - Never rewrite endpoint_url or credentials when a card is applied.
    List them under unapplied in the drift report.
  - Require a2a.update and agent ownership. Add both routes to the token
    scoping map.
  - Add Alembic revision b7d1f4a9c2e6 for the status and drift columns of
    a2a_agent_cards.

Signed-off-by: Marek Dano <mk.dano@gmail.com>
@marekdano marekdano self-assigned this Oct 29, 2026
@marekdano

Copy link
Copy Markdown
Collaborator Author

Closing this PR. The scope of #4734 changed.

The issue body was edited on 2026-10-30. The core flow now covers fetching a card, reviewing it, and storing it. Re-sync and drift reporting moved out of the issue into a comment, and the flow no longer has a re-sync step.

This PR is slice 3 of #4734 and depends on the storage slice in #7378. It adds work beyond the current scope of the issue. It also adds a migration (b7d1f4a9c2e6) that only this PR needs.

The branch 4734-a2a-card-resync stays available. Re-sync can return as its own PR when the issue scope includes it. That PR would need to:

Refs #4734

@marekdano marekdano closed this Oct 30, 2026
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