Skip to content
Open
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
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Live A2A JSON-RPC peer listener (`ca2a start` / `ca2a_runtime.server`): serves
`message/send`, runs `parse_peer_request` → `handle_peer_request` end to end
(verify chain, intersect local/Cedar policy, open sealed payload with a
configured key, emit provenance). Ordinary A2A (no cA2A keys) returns
`ca2a: null`. Fail-closed on malformed metadata and denials. Optional
`local_policy` / `enclave_private_key_hex` config fields; install with
`pip install 'ca2a-runtime[serve]'`. Does **not** add attestation handshake
or seal-to-verified-measurement binding. See issue #47.
- A2A transport adapter (`ca2a_runtime.transport`): parse/attach cA2A extension
metadata on A2A `SendMessage`-shaped messages into `PeerRequest` (and the
reverse). Extension URI `https://agentrust.io/extensions/ca2a/v0.1`. Fail closed
on malformed cA2A metadata; absence of all cA2A keys returns `None` (ordinary
A2A). Does not add HTTP serving, `ca2a start`, or seal-to-verified-measurement
binding. New error `TRANSPORT_ERROR`. See issue #47.
A2A). New error `TRANSPORT_ERROR`. See issue #47.

## [0.1.0a1] - 2026-07-09

Expand Down
4 changes: 2 additions & 2 deletions LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ cA2A is a pre-release profile in active design. This document states plainly wha

## What is stubbed or not yet implemented

- **Live A2A transport serving.** An A2A metadata adapter (`ca2a_runtime.transport`) can parse/attach cA2A extension fields into a `PeerRequest`, and `handle_peer_request` enforces that request in-process. There is still no HTTP/JSON-RPC listener or `ca2a start`: nothing accepts a live inbound A2A socket call end to end. Parsing extension metadata is Tier 2 transport progress, not a claim that cA2A is attested across trust domains.
- **Sealed peer channel (live binding).** The channel is implemented: a payload is sealed to the peer's attested X25519 key (X25519 ECDH, HKDF-SHA256, ChaCha20-Poly1305), and only the holder of the peer's private key can open it. The remaining gap is the hardware property that the private key never leaves the peer's enclave (established by attestation), and wiring the seal to a verified report on a live inbound call. Until that end-to-end binding lands on hardware, do not assume a payload is confined to a specific attested measurement. Adapter-decoded `sealed_payload` bytes are opaque ciphertext only.
- **Live A2A transport serving.** An A2A metadata adapter (`ca2a_runtime.transport`) parses/attaches cA2A extension fields into a `PeerRequest`, and `ca2a start` serves JSON-RPC `message/send` end to end through `handle_peer_request` (verify chain → intersect policy → open sealed payload when a key is configured → emit a provenance record), fail-closed. There is still **no** attestation handshake on the live call, and the seal is **not** bound to a verified measurement: any enclave key is software-configured. Live serving is Tier 2 transport progress, not a claim that cA2A is attested across trust domains.
- **Sealed peer channel (live binding).** The channel is implemented: a payload is sealed to the peer's attested X25519 key (X25519 ECDH, HKDF-SHA256, ChaCha20-Poly1305), and only the holder of the peer's private key can open it. The live listener can open with a configured key. The remaining gap is the hardware property that the private key never leaves the peer's enclave (established by attestation), and wiring the seal to a verified report on a live inbound call. Until that end-to-end binding lands on hardware, do not assume a payload is confined to a specific attested measurement. Adapter-decoded `sealed_payload` bytes are opaque ciphertext only.
- **Real hardware attestation.** The **SEV-SNP verifier is implemented**: report parsing, VCEK certificate chain verification, ECDSA-P384 report-signature verification, and measurement/report-data binding, all fail-closed. The chain path is validated against the genuine AMD Milan root chain; the report-signature path is validated with synthetic vectors, since a real report plus VCEK pair needs SEV-SNP hardware. Report generation (`SevSnpProvider.attest`) still requires a real SEV-SNP guest. The **Intel TDX verifier** (DCAP Quote v4: PCK chain to the genuine Intel SGX Root CA, QE report, attestation-key binding, quote signature, MRTD binding) and the **TPM 2.0 verifier** (AK chain to a caller-supplied vendor root, AK signature, magic/type, qualifying-data and PCR-digest binding) are also implemented and synthetic-vector validated; quote generation needs the respective hardware. Until a backend verifies a real quote end to end against a golden measurement on hardware, cA2A must not be described as fully attested across trust domains.

## Out of scope
Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Already implemented and tested elsewhere; cA2A depends on it rather than reimple

## v0.2: Runtime enforcement and sealed channel

- Runtime peer-delegation enforcement: **decision core landed** (`ca2a_runtime.peer.enforce_peer_call`: verify chain, intersect delegated scope with local policy, enforce, emit provenance record; claim C3 validated), now with a **real Cedar policy engine** option (`ca2a_runtime.cedar.CedarPolicy`) alongside the allow-set `LocalPolicy`. Remaining: wire the decision core to a live A2A transport (Tier 2)
- Runtime peer-delegation enforcement: **decision core landed** (`ca2a_runtime.peer.enforce_peer_call`: verify chain, intersect delegated scope with local policy, enforce, emit provenance record; claim C3 validated), now with a **real Cedar policy engine** option (`ca2a_runtime.cedar.CedarPolicy`) alongside the allow-set `LocalPolicy`. **Live A2A serving landed** (`ca2a start` / `ca2a_runtime.server`: JSON-RPC `message/send` → parse → `handle_peer_request`). Remaining: attestation handshake on the live call, and seal bound to a verified measurement (Tier 2/3)
- Sealed peer channel: **landed** (`ca2a_runtime.channel`: HPKE-style X25519 -> HKDF-SHA256 -> ChaCha20-Poly1305 sealing to the peer's attested key; claim C4 validated). Remaining: bind the seal to a verified attestation report on a live call, and rely on the enclave to hold the private key (hardware property)
- Linked runtime evidence: each hop's TRACE record references the parent record hash and delegation credential id, producing a verifiable delegation DAG (Tier 2)

Expand All @@ -34,7 +34,7 @@ Real hardware attestation verification (SEV-SNP VCEK chain, Intel TDX quote via
- **TDX verifier: landed.** DCAP Quote v4 parsing, PCK chain to the genuine Intel SGX Root CA, QE report signature, attestation-key binding, quote signature, and MRTD binding, all fail-closed. Quote generation requires a real TDX guest. See `ca2a_verify.tdx`.
- **TPM 2.0 verifier: landed.** TPMS_ATTEST parsing, AK chain to a caller-supplied vendor root, AK signature (ECDSA or RSA), magic/type checks, and qualifying-data/PCR-digest binding, all fail-closed. Quote generation requires a real TPM. See `ca2a_verify.tpm`.
- **Cross-operator attestation (C6): validated in software.** A two-operator harness (SEV-SNP verifier + measurement pinning + sealed channel) shows independent keys, mutual attestation, confidential cross-operator delegation, and binary-swap detection. All six claims (C1-C6) are now validated experiments.
- **Pending:** end-to-end validation of the SEV-SNP, TDX, and TPM signature paths against real hardware quotes on a confidential VM; and a transport that parses real A2A wire messages into a `PeerRequest`.
- **Pending:** end-to-end validation of the SEV-SNP, TDX, and TPM signature paths against real hardware quotes on a confidential VM; live attestation handshake and seal-to-verified-measurement binding on the inbound call.

## v1.0: Stable profile

Expand Down
25 changes: 17 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Configuration

The cA2A runtime reads a YAML config. The peer runtime path is under construction; this page documents the surface it consumes, validated today by `ca2a validate-config`.
The cA2A runtime reads a YAML config. Offline commands validate it with
`ca2a validate-config`. `ca2a start` consumes the same file for the live
JSON-RPC listener.

## Reference

Expand All @@ -12,24 +14,31 @@ attestation:
max_delegation_depth: 8 # reject chains deeper than this
listen_addr: "0.0.0.0:8443"

policy_bundle_path: policy/ # optional: Cedar bundle for local scope intersection
local_policy: ["read", "write"] # allow-set for scope intersection (or use Cedar below)
# policy_bundle_path: policy.cedar
# enclave_private_key_hex: "<64 hex chars>" # optional; opens sealed_payload
```

## Fields

| Field | Default | Description |
|---|---|---|
| `attestation.provider` | `auto` | TEE provider for peer attestation. `auto` selects the strongest available hardware; `software-only` requires no hardware. `opaque` is explicit opt-in and never auto-selected. |
| `attestation.enforcement_mode` | `enforcing` | `enforcing` denies peer calls that fail verification; `advisory` logs and proceeds; `silent` evaluates without logging or blocking. |
| `attestation.provider` | `auto` | TEE provider for peer attestation. `auto` selects the strongest available hardware; `software-only` requires no hardware. `opaque` is explicit opt-in and never auto-selected. Live attestation on the call is not wired yet. |
| `attestation.enforcement_mode` | `enforcing` | Intended mode. The live listener always fails closed on cA2A denials today; advisory/silent are accepted in config but not applied on the wire. |
| `max_delegation_depth` | `8` | Chains deeper than this are rejected with `DELEGATION_DEPTH_EXCEEDED`. |
| `listen_addr` | `0.0.0.0:8443` | Address the peer runtime listens on. |
| `policy_bundle_path` | none | Directory of Cedar policy the runtime intersects with a delegated scope. |
| `listen_addr` | `0.0.0.0:8443` | Address `ca2a start` binds. |
| `local_policy` | none | Capability allow set for `LocalPolicy`. Required for `ca2a start` unless `policy_bundle_path` is set. |
| `policy_bundle_path` | none | Path to a Cedar policy file. When set, used instead of `local_policy`. |
| `enclave_private_key_hex` | none | Optional 32-byte X25519 private key (hex) for opening `sealed_payload`. Override with `--enclave-key-hex` or `CA2A_ENCLAVE_PRIVATE_KEY_HEX`. Software-configured only; not measurement-bound. |

## Validate
## Validate / start

```bash
ca2a validate-config --config examples/minimal/ca2a-config.yaml
# ok: provider=auto enforcement=advisory
# ok: provider=software-only enforcement=enforcing

pip install 'ca2a-runtime[serve]'
ca2a start --config examples/minimal/ca2a-config.yaml
```

Invalid values fail fast with a `CONFIG_ERROR` and a message naming the offending field.
38 changes: 28 additions & 10 deletions docs/spec/transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ cA2A is a profile on A2A, not a competing transport. A2A moves tasks and context
| Piece | Status |
|---|---|
| Extension URI + namespaced metadata keys (below) | Specified |
| `ca2a_runtime.transport` adapter: A2A metadata ↔ `PeerRequest` | Implemented (parse/attach only) |
| Hand-off into `handle_peer_request` once a `PeerRequest` exists | Implemented in-process; callers invoke it after parsing |
| HTTP/JSON-RPC serving / `ca2a start` | Not yet — separate Tier 2 checkbox |
| `ca2a_runtime.transport` adapter: A2A metadata ↔ `PeerRequest` | Implemented (parse/attach) |
| Hand-off into `handle_peer_request` once a `PeerRequest` exists | Implemented |
| HTTP/JSON-RPC serving / `ca2a start` | Implemented (live serving only) |
| Live attestation handshake on an inbound call | Not yet — separate Tier 2/3 checkbox |
| Seal bound to a **verified** attestation measurement on a live call | Not yet — separate Tier 2/3 checkbox |

Parsing real A2A extension metadata is progress on Tier 2 transport wiring. It is **not** evidence that cA2A is attested across trust domains. See [LIMITATIONS.md](../../LIMITATIONS.md) and [ROADMAP.md](../../ROADMAP.md).
Live serving wires parse → `handle_peer_request` end to end on `message/send`. It is **not** evidence that cA2A is attested across trust domains: there is still no attestation handshake on the call, and sealed open uses a configured software/enclave key rather than a key bound to a verified measurement. See [LIMITATIONS.md](../../LIMITATIONS.md) and [ROADMAP.md](../../ROADMAP.md).

## Overlay, not fork

Expand Down Expand Up @@ -88,26 +88,44 @@ Once a `PeerRequest` has been parsed, the intended inbound order is:
4. Open any sealed payload with the enclave-held key. Crypto implemented; live seal-to-verified-measurement binding is not. See [the sealed channel](sealed-channel.md).
5. Emit a linked provenance/TRACE record referencing the parent record hash and `credential_id`. See [the TRACE A2A profile](trace-a2a-profile.md).

Steps 1, 3, and 5 (and sealed open when a key is supplied) run today inside `handle_peer_request` after the adapter produces a `PeerRequest`. Steps 2 and the measurement-bound seal are remaining Tier 2/3 work.
Steps 1, 3, and 5 (and sealed open when a key is supplied) run inside `handle_peer_request` after the adapter produces a `PeerRequest`, including on the live `ca2a start` listener. Steps 2 and the measurement-bound seal are remaining Tier 2/3 work.

## Live listener (`ca2a start`)

```bash
pip install 'ca2a-runtime[serve]'
ca2a start --config examples/minimal/ca2a-config.yaml
```

The listener accepts JSON-RPC `message/send` (and `SendMessage`) on `POST /` and `POST /rpc`:

1. `parse_peer_request` on the envelope.
2. If no cA2A keys: return `{"ca2a": null, ...}` (ordinary A2A; no invented trust state).
3. If cA2A keys present: `handle_peer_request` with the configured policy and optional enclave key; map `CA2AError` to a JSON-RPC error with `data.ca2a_code`.

Config for the listener needs either `local_policy` (capability allow set) or `policy_bundle_path` (Cedar). Optional `enclave_private_key_hex` (or `--enclave-key-hex` / `CA2A_ENCLAVE_PRIVATE_KEY_HEX`) opens sealed payloads. That key is software-configured for this slice; it is not proven enclave-bound or measurement-linked.

## Enforcement is a peer decision

How strictly a cA2A peer acts on the extension fields is local configuration, not a transport-level flag. `Ca2aConfig.enforcement_mode` selects the behavior:
How strictly a cA2A peer acts on the extension fields is local configuration, not a transport-level flag. `Ca2aConfig.enforcement_mode` selects the intended behavior:

- `enforcing`: an unverifiable chain or a missing required credential denies the call. This is the default and the fail-closed posture the profile calls for.
- `enforcing`: an unverifiable chain or a missing required credential denies the call. This is the default and the fail-closed posture the profile calls for. The live listener always uses this posture today.
- `advisory`: the failure is recorded but the call proceeds.
- `silent`: the check runs without a visible signal.

```yaml
# ca2a config
attestation:
provider: auto
provider: software-only
enforcement_mode: enforcing
max_delegation_depth: 8
listen_addr: "0.0.0.0:8443"
listen_addr: "127.0.0.1:8443"
local_policy: ["read", "write"]
# policy_bundle_path: policy.cedar # alternative to local_policy
# enclave_private_key_hex: "..." # optional; for sealed_payload open
```

`max_delegation_depth` bounds the chain length a peer will accept and is passed through to `verify_chain`. `listen_addr` is reserved for the future HTTP listener (`ca2a start`); this adapter does not open a socket. See [failure modes](failure-modes.md).
`max_delegation_depth` bounds the chain length a peer will accept and is passed through to `verify_chain`. `listen_addr` is the HTTP bind address for `ca2a start`. See [failure modes](failure-modes.md).

## Transport stability

Expand Down
7 changes: 4 additions & 3 deletions examples/minimal/ca2a-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
attestation:
provider: auto # auto | tpm | sev-snp | tdx | opaque | software-only
enforcement_mode: advisory # enforcing | advisory | silent
provider: software-only # auto | tpm | sev-snp | tdx | opaque | software-only
enforcement_mode: enforcing # live listener always fails closed today

max_delegation_depth: 8
listen_addr: "0.0.0.0:8443"
listen_addr: "127.0.0.1:8443"
local_policy: ["read", "write"]
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ dependencies = [
]

[project.optional-dependencies]
serve = [
"starlette>=0.37",
"uvicorn>=0.29",
]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
Expand All @@ -39,6 +43,9 @@ dev = [
"types-pyyaml",
"bandit[toml]>=1.7",
"pip-audit>=2.6",
"starlette>=0.37",
"uvicorn>=0.29",
"httpx>=0.27",
]

[project.scripts]
Expand Down
67 changes: 66 additions & 1 deletion src/ca2a_runtime/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any

from ca2a_runtime import __version__
from ca2a_runtime.config import Ca2aConfig
from ca2a_runtime.delegation import DelegationCredential, verify_chain
from ca2a_runtime.errors import CA2AError, InvalidCredential, ProvenanceLinkBroken
from ca2a_runtime.errors import CA2AError, ConfigError, InvalidCredential, ProvenanceLinkBroken
from ca2a_runtime.provenance import DelegationRecord, cross_check_chain, verify_dag
from ca2a_verify import verify_chain_file

Expand Down Expand Up @@ -107,6 +108,58 @@ def _cmd_verify_dag(args: argparse.Namespace) -> int:
return 0


def _cmd_start(args: argparse.Namespace) -> int:
"""Start the live A2A JSON-RPC peer listener (``ca2a start``)."""
try:
import uvicorn
except ImportError:
print(
"ca2a start requires the 'serve' extra: pip install 'ca2a-runtime[serve]'",
file=sys.stderr,
)
return 1

from ca2a_runtime.policy_loader import load_policy
from ca2a_runtime.server import (
PeerRuntime,
create_app,
load_enclave_private_key,
parse_listen_addr,
)

try:
cfg = Ca2aConfig.load(args.config)
config_path = Path(args.config).resolve()
policy = load_policy(cfg, config_dir=config_path.parent)
key_hex = (
args.enclave_key_hex
or os.environ.get("CA2A_ENCLAVE_PRIVATE_KEY_HEX")
or cfg.enclave_private_key_hex
)
enclave_key = load_enclave_private_key(key_hex)
host, port = parse_listen_addr(cfg.listen_addr)
except ConfigError as exc:
print(f"invalid config: {exc}", file=sys.stderr)
return 1

if cfg.enforcement_mode != "enforcing":
print(
f"note: enforcement_mode={cfg.enforcement_mode!r} is accepted in config, "
"but the live listener always fails closed on cA2A denials",
file=sys.stderr,
)

runtime = PeerRuntime(config=cfg, policy=policy, enclave_private_key=enclave_key)
app = create_app(runtime)
print(
f"ca2a starting: listen={cfg.listen_addr} provider={cfg.provider} "
f"enforcement={cfg.enforcement_mode} "
f"(live serving only; no attestation handshake / seal-to-measurement binding)"
)
uvicorn.run(app, host=host, port=port)
return 0


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="ca2a", description="Confidential agent-to-agent")
parser.add_argument("--version", action="version", version=f"ca2a {__version__}")
Expand All @@ -130,6 +183,18 @@ def build_parser() -> argparse.ArgumentParser:
vd.add_argument("--max-depth", type=int, default=8)
vd.set_defaults(func=_cmd_verify_dag)

st = sub.add_parser(
"start",
help="Start the live A2A JSON-RPC peer listener",
)
st.add_argument("--config", required=True, help="Path to ca2a-config.yaml")
st.add_argument(
"--enclave-key-hex",
default=None,
help="X25519 private key as 32-byte hex (overrides config / CA2A_ENCLAVE_PRIVATE_KEY_HEX)",
)
st.set_defaults(func=_cmd_start)

return parser


Expand Down
Loading
Loading