Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/a2a-unit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: A2A server unit tests

# Backend-engineer's slice: unit tests for agent-templates/a2a. The image build, Helm
# chart and Argo wiring are devops-engineer's; this workflow only proves the server
# code + card generator behave to the frozen contract.

on:
push:
branches: [ main, develop ]
paths:
- 'agent-templates/a2a/**'
- 'agent-templates/contracts/a2a/**'
- 'agent-templates/providers/base.py'
- '.github/workflows/a2a-unit.yml'
pull_request:
paths:
- 'agent-templates/a2a/**'
- 'agent-templates/contracts/a2a/**'
- 'agent-templates/providers/base.py'
- '.github/workflows/a2a-unit.yml'

jobs:
a2a-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pydantic 'starlette>=0.37' httpx jsonschema pytest

- name: Run A2A unit tests
working-directory: agent-templates/a2a
run: python -m pytest tests -q
61 changes: 61 additions & 0 deletions agent-templates/a2a/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# `agent-templates/a2a` — shared A2A server + card generator

The **callee side** of the frozen A2A contract v1
(`agent-templates/contracts/a2a/v1`). ONE server fronts every product and exec-tier
agent in the family; a repo onboards by adding a `tenant` entry to the Helm values —
never a new pod. This package is a **thin adapter** over the existing Managed-Agents
runtime (`agent-templates/providers` + `orchestration`); it holds **no task engine**.

> Scope note: this is backend-engineer's slice. The image/Dockerfile, Helm chart,
> Argo Application and CI image build are **devops-engineer's**; independent
> conformance / authZ-negative tests are **test-engineer's**; the handoff-MCP-over-A2A
> client routing is **mcp-engineer's**; operator docs are **docs-maintainer's**.

## Modules

| module | responsibility | contract |
|---|---|---|
| `card_generator.py` | project `manifest.json` + `roles/*/role.json` → Agent Card | `card-projection.md` |
| `validation.py` | validate a card against `agent-card.schema.json` + `fuze-profile.schema.json` | `schema/` |
| `task_mapper.py` | `run_until_block` result → A2A `Task`; INPUT vs AUTH_REQUIRED classifier | `state-mapping.md` |
| `authz.py` | callee-enforced `providesTo` allowlist, **fail-closed** | `authz.md` |
| `identity.py` | transport credential → trusted caller identity (OIDC bearer) | `authz.md §2` |
| `session_store.py` | caller-ownership index + reflected `Task` snapshot (NOT an engine) | `state-mapping.md §7` |
| `adapter.py` | wire methods → `AgentProvider` seam (the translation) | `state-mapping.md` |
| `server.py` | JSON-RPC 2.0 over HTTP + SSE (`POST /rpc`, well-known card) | `binding.md` |
| `config.py` | parse the `values-interface.schema.json` document | `values-interface` |
| `runtime.py` | compose config → adapter → server with a real provider + OIDC | — |

## Key invariants enforced here

- **Cards are derived, deterministic, and signed.** Same inputs → byte-identical card
(modulo `signatures`). Never hand-authored. `tools`/`mcp_servers`/`vault` are never
projected (encapsulation invariant, `card-projection.md §7`).
- **The callee enforces; the caller is opaque.** Authorization uses only the validated
credential identity, never the request body. Absent `providesTo` → **DENY**.
- **No new task engine.** `Task.id` IS the provider `session_id`. Continuations use
`confirm_tool` / `resume_session`, never transcript replay. `FAILED` is not retried.
- **Interrupted ≠ terminal.** An `always_ask` pause is `INPUT_REQUIRED`; a missing
credential/grant is `AUTH_REQUIRED`; both may be resolved out-of-band by `reach_human`
with no caller message, and the adapter never downgrades them on timeout.
- **Dual-runtime clean.** Pure-Python, service-DNS addressing, `ClusterIP`-only,
config from env/secret; no assumption that holds in only compose or only Helm.

## Run the unit tests

```bash
pip install pydantic starlette httpx jsonschema pytest
cd agent-templates/a2a && python -m pytest tests -q
```

The contract client package is put on `sys.path` automatically by `_contract.py`, so no
editable install is required.

## Local run

```bash
export A2A_VALUES_FILE=/path/to/values.json # the a2a.* block
export A2A_REPOS_DIR=/repos # tenant repo checkouts
export AGENT_PROVIDER=anthropic
python -m a2a.runtime
```
19 changes: 19 additions & 0 deletions agent-templates/a2a/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Shared A2A server + Agent Card generator for the Fuze family.

This package implements the CALLEE side of the frozen A2A contract v1
(``agent-templates/contracts/a2a/v1``). It is a thin ADAPTER over the existing
Managed-Agents runtime (``agent-templates/providers`` + ``orchestration``): A2A wire
objects in, provider calls out, provider results mapped back to A2A objects. There is
no new task engine here — see ``contracts/a2a/v1/state-mapping.md``.

Modules:
card_generator -- projects .fuze/manifest.json + roles/*/role.json -> AgentCard
task_mapper -- run_until_block result -> A2A Task/TaskStatus (the core table)
authz -- callee-enforced allowlist decision (providesTo, fail-closed)
adapter -- wire method dispatch onto an AgentProvider
server -- Starlette JSON-RPC 2.0 + SSE transport
"""

from __future__ import annotations

__version__ = "1.0.0"
84 changes: 84 additions & 0 deletions agent-templates/a2a/_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Bridge to the FROZEN A2A contract client package.

The generated wire/card models and the typed error taxonomy live in
``agent-templates/contracts/a2a/v1/client/fuze_a2a_client`` and are the single
source of truth for the wire shapes. This module makes them importable whether or
not the client package has been ``pip install``ed, by putting its directory on
``sys.path`` on first import. Everything in this server imports the wire/card models
and errors THROUGH here so there is exactly one definition of the contract types.

We NEVER redefine the wire or card models — redefining a generated model is how a
server silently forks from its spec (see the client package docstring).
"""

from __future__ import annotations

import sys
from pathlib import Path

# .../agent-templates/a2a/_contract.py -> .../agent-templates
_AGENT_TEMPLATES = Path(__file__).resolve().parents[1]
_CONTRACT_ROOT = _AGENT_TEMPLATES / "contracts" / "a2a" / "v1"
_CLIENT_DIR = _CONTRACT_ROOT / "client"

if _CLIENT_DIR.exists() and str(_CLIENT_DIR) not in sys.path:
sys.path.insert(0, str(_CLIENT_DIR))

#: Absolute path to the frozen contract tree (schemas, examples, VERSION).
CONTRACT_ROOT = _CONTRACT_ROOT
SCHEMA_DIR = _CONTRACT_ROOT / "schema"
EXAMPLES_DIR = _CONTRACT_ROOT / "examples"

# Re-export the generated / frozen types. Imported lazily-safe: the client package
# only needs pydantic (always present here), never httpx, because we supply no
# transport (this is the server, not the client).
from fuze_a2a_client import errors as errors # noqa: E402
from fuze_a2a_client.card_models import ( # noqa: E402
AgentCapabilities,
AgentInterface,
AgentProvider,
AgentSkill,
FuzeA2AAgentCard,
SecurityRequirement,
)
from fuze_a2a_client.wire_models import ( # noqa: E402
Artifact,
JsonRpcError,
JsonRpcRequest,
JsonRpcResponse,
Message,
Method,
Part,
Role,
Task,
TaskArtifactUpdateEvent,
TaskState,
TaskStatus,
TaskStatusUpdateEvent,
)

__all__ = [
"CONTRACT_ROOT",
"SCHEMA_DIR",
"EXAMPLES_DIR",
"errors",
"FuzeA2AAgentCard",
"AgentInterface",
"AgentProvider",
"AgentSkill",
"AgentCapabilities",
"SecurityRequirement",
"Artifact",
"JsonRpcError",
"JsonRpcRequest",
"JsonRpcResponse",
"Message",
"Method",
"Part",
"Role",
"Task",
"TaskArtifactUpdateEvent",
"TaskState",
"TaskStatus",
"TaskStatusUpdateEvent",
]
Loading
Loading