ADR-0004: Adopt A2A v1.0 (drop /v1/ REST prefix, v1 AgentCard shape) (2026-05-14)

Status: Accepted. Supersedes: Earlier /v1/sac/... REST surface + v0.3-shaped AgentCard. Related: 0001, 0003.

Problem

A2A reached v1.0 stable (Linux Foundation, April 2026) and the REST binding now prohibits any path that starts with /v1/. sac’s existing routes — /agents/<name>, /agents/<name>/, /agents/<name>/inbox/stream, /agents/<name>/_active, plus the fleet root /.well-known/agent.json (v1 renamed the well-known file to agent-card.json) — fail A2A v1.0 compliance.

The AgentCard projection (_card.py) carries v0.x shapes too: top-level url + preferredTransport, defaultInputModes /defaultOutputModes spellings that A2A v1 may have reshuffled into a per-interface block, and no supportedInterfaces[] array.

Backward compatibility is not maintained — sac has no external consumers yet, and the simpler the v1 surface, the better the Clew arXiv positioning (“sac is A2A v1.0 compliant”, not “sac speaks a custom dialect plus v1.0 in parallel”).

Decision

D10. Route prefix /agents//agents/.

All sac REST routes drop the /v1/ prefix:

Old

New

GET /.well-known/agent.json

GET /.well-known/agent-card.json

GET /agents/

GET /agents/

GET /agents/<name>/.well-known/agent.json

GET /agents/<name>/.well-known/agent-card.json

POST /agents/<name>

POST /agents/<name>/message:send (A2A v1 REST binding)

GET /agents/<name>/inbox/stream

GET /agents/<name>/inbox/stream (sac extension)

GET /agents/<name>/_active

GET /agents/<name>/_active (sac extension)

The /agents/<name>/ prefix is sac’s multi-agent extension above the A2A REST binding (the spec defines single-agent paths; sac fans several agents from one listen process).

sac extension namespace

A2A v1.0 reserves the AgentCard top level for spec-defined fields and funnels vendor data into a namespaced extension block. sac uses exactly one namespace key: x-scitex-agent-container. Every sac-specific datum (role, scheduling, runtime, isolation, fleet listings, proxy metadata) lives under that key. This contract is what makes sac-served cards forward-compatible with vendor-neutral A2A clients — a strict v1 validator (e.g. proto ParseDict(AgentCard)) ignores the x-* namespace; sac-aware clients walk into it.

Implementation source of truth:

  • src/scitex_agent_container/a2a/_card.py::project_card — per-agent card.

  • src/scitex_agent_container/a2a/_card.py::fleet_card — fleet-level card.

  • src/scitex_agent_container/a2a/_card.py::validate_card_v1 — strips x-* before proto validation, so the namespace is honoured by the schema gate.

  • src/scitex_agent_container/_runners/a2a_proxy.py::splice_card — AgentProxy runner overlay (kind / upstream / trust).

Rules (hard).

  1. sac-specific data MUST live under x-scitex-agent-container.*. Never at the top level of the card. Never under a different namespace (e.g. x-orochi, x-sac, vendor — all forbidden).

  2. x-orochi is owned by the orochi fleet hub, not by sac. A standalone sac a2a serve agent intentionally carries no x-orochi block.

  3. validate_card_v1 strips the x-scitex-agent-container key (and any other x-* key) before proto validation. New fields therefore only need to be valid JSON; they don’t need a proto schema. This is the explicit forward-compat lever.

Per-agent card fields (project_card)

Every per-agent card served at GET /agents/<name>/.well-known/agent-card.json carries:

Field

Source

Description

x-scitex-agent-container.role_class

metadata.labels.role

Operator-declared role taxonomy (e.g. worker-telegrammer, head). Mirrors skills[0].name.

x-scitex-agent-container.cardinality

metadata.labels.cardinality

singleton / multi-instance hint for fleet schedulers.

x-scitex-agent-container.scheduling

derived from spec.host / spec.hosts

{mode: "singleton", priority: [...]} or {mode: "multi-instance", hosts: [...]}. The fleet hub reads this to place the agent.

x-scitex-agent-container.runtime

spec.runtime

Runtime kind (claude-code, agent-proxy, etc.).

x-scitex-agent-container.model

spec.claude.model (v3) ∨ spec.model (v2 back-compat)

LLM model identifier.

x-scitex-agent-container.multiplexer

spec.multiplexer

tmux / zellij / none — operational tool selection.

x-scitex-agent-container.required_skills

metadata.labels.skills (CSV) ∪ legacy spec.skills.required

Skill IDs the agent loads at boot. Also merged into skills[0].tags.

x-scitex-agent-container.isolation

derived from spec.apptainer.*

Structured D3 isolation block — see below.

The isolation block (see ADR-0001) carries:

Sub-field

Type

Description

level

enum

hardened (default) / relaxed (escape hatch) / custom (any preflight allow-list).

containall

bool

--containall flag is/will be passed to apptainer.

cleanenv

bool

--cleanenv flag is/will be passed to apptainer.

writable_tmpfs

bool

--writable-tmpfs will be passed (auto-added when not relaxed and no overlay).

preflight_passed

list[str]

Preflight checks that ran and succeeded (uid-nonzero, no-host-home).

preflight_allowed

list[str]

Preflight checks the operator explicitly bypassed.

binds_count

int

Total apptainer --bind entries.

binds_writable_count

int

Number of binds NOT carrying :ro.

External attestation surfaces (Clew, orochi) read these booleans to prove isolation properties without re-parsing the YAML.

Per-agent capabilities.extensions[] entries

Distinct from the x-scitex-agent-container.* namespace, the A2A v1 spec-defined capabilities.extensions[] array advertises sac extensions by URI:

URI

Emitted when

Purpose

https://scitex.ai/a2a/extensions/sac-push-channel/v1

spec.claude.channels contains server:sac

Advertises the in-session MCP push: sac mcp channel SSE-subscribes to /agents/<name>/inbox/stream and forwards events as notifications/claude/channel to the agent’s Claude session. params.sse_path + params.mcp_tools enumerate the wire details.

Fleet card fields (fleet_card)

The fleet card at GET /.well-known/agent-card.json carries:

Field

Type

Description

x-scitex-agent-container.agents

list[object]

Member directory. Each entry has name + supportedInterfaces[] (v1 shape — no top-level url). Spec-aware clients walk this array to fetch each member’s /agents/<name>/.well-known/agent-card.json.

The fleet card also advertises capabilities.extensions[]:

URI

Purpose

https://scitex.ai/a2a/extensions/sac-fleet/v1

Declares the multi-agent directory shape. params.members_path and params.member_card_path tell vendor-neutral clients how to walk the fleet.

AgentProxy overlay (splice_card)

When a kind: AgentProxy agent serves its card, three additional fields appear under x-scitex-agent-container:

Field

Source

Description

x-scitex-agent-container.kind

runner constant

"AgentProxy" — distinguishes a proxy from a native sac runtime.

x-scitex-agent-container.upstream

--upstream CLI arg

Upstream A2A URL the proxy forwards to.

x-scitex-agent-container.trust

--trust CLI arg

Trust tier (trusted / untrusted).

x-scitex-agent-container.upstream_card_fetch_error

runtime

Present only when boot-time fetch of the upstream card failed; surfaces the error so operators see why upstream skills aren’t propagating.

See src/scitex_agent_container/_runners/a2a_proxy.py::splice_card.

Cross-references

  • Skill doc: 07_a2a-protocol.md mirrors this enumeration for operators reading skill docs.

  • Per-agent projection: a2a/_card.py:project_card

  • Fleet projection: a2a/_card.py:fleet_card

  • v1 schema gate: a2a/_card.py:validate_card_v1

  • AgentProxy overlay: _runners/a2a_proxy.py:splice_card

Decision (continued)

D11. AgentCard publishes A2A v1.0 fields only.

Verified against the lf/a2a/v1 proto (~/proj/A2A), not the auditor’s summary — the auditor had two wrong field names:

  • Top-level url is removed. The v1 proto has no top-level url; binding URLs live under supportedInterfaces[] only.

  • supportedInterfaces[] is REQUIRED. Each interface has:

    • url (HTTPS in prod)

    • protocolBinding: "JSONRPC" | "GRPC" | "HTTP+JSON" (the canonical strings — not "REST" as the auditor suggested).

    • tenant (sac uses the agent name as tenant)

    • protocolVersion: "1.0"

  • capabilities carries streaming, pushNotifications, extensions[], extendedAgentCard — no stateTransitionHistory (v0.x field, gone in v1).

  • No top-level authentication.schemes (v0.x). v1 uses securitySchemes (map) + securityRequirements[]. Current sac cards advertise no auth, so we simply omit both fields rather than emit empty placeholders.

  • kind discriminator: not present in current sac card, so no action.

  • Enums (where sac surfaces any) follow SCREAMING_SNAKE_CASE per A2A v1.0.

D12. The notifications/claude/channel push primitive stays sac’s, not A2A.

A2A v1.0 has tasks/pushNotificationConfig/* for task-level push, which is orthogonal to Claude Code’s in-session channel. The sac MCP push channel (ADR’s commit 1–4 from today) is a Claude Code construct, not an A2A construct — keep it where it lives, in server:sac MCP, not on the A2A wire.

D13. No backward compatibility.

Old /v1/sac/... paths are deleted. Tests that hit them are updated. The change is internal to sac (no external API consumers yet); any operator scripts that hardcoded the old paths must update to the new ones.

Implementation

Layer

Where

Status

Route refactor (delete /v1/sac/, add /agents/)

a2a/_server.py

⏳ this PR

AgentCard shape update (supportedInterfaces, drop top-level url, streaming=true)

a2a/_card.py

⏳ this PR

Card consumer updates (sac mcp channel SSE URL, sidecar references)

_mcp/channel.py, runtimes/_apptainer_runtime.py, _runners/...

⏳ this PR

Tests updated to new paths + shape

tests/.../a2a/*, tests/.../runtimes/*

⏳ this PR

docs (isolation.md, sac-and-orochi.md, spec-reference.md)

docs

⏳ this PR

A2A TCK as CI gate

.github/workflows/a2a-tck.yml

⏳ follow-up

JWS-signed AgentCard

a2a/_card.py + new dep

⏳ follow-up (ADR-0005)

Rationale

  • Future-proof against v1.1+: standard paths + supportedInterfaces[] is the v1.0 shape; minor versions extend, don’t break.

  • TCK passable: the A2A Technology Compatibility Kit (a2aproject/a2a-tck) runs against the standard REST binding. Once routes match, --category mandatory becomes a CI gate.

  • Inspector friendly: ghcr.io/a2aproject/a2a-inspector expects the standard surface; refactoring lets us point it at sac’s localhost and get a real diff vs zero diff (= compliant).

  • Clew positioning: “sac is A2A v1.0 compliant” is a one-line claim in the Methods section. The earlier alternative (“sac speaks A2A-like REST”) was honest but weaker.

Consequences

Positive.

  • One step closer to upstream-mergeable example for the A2A community.

  • All A2A v1.0 framework consumers (Google ADK, LangGraph, CrewAI, LlamaIndex, Microsoft Agent Framework) can speak to sac agents natively over the standard REST binding.

  • TCK gate possible; passes prove sac compliance mechanically.

Negative.

  • One-shot refactor across _server.py, _card.py, _mcp/channel.py, sidecar bind logic, and all tests. No parallel-path softening; the merge is the migration.

  • Any operator who curl-tested the old /v1/sac/... paths needs to re-learn the new ones. Mitigation: docs updated in the same PR.

References