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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`ca2a start`**: run the reference transport from a config file. It resolves
the policy (`local_policy` or a Cedar `policy_bundle_path`, the latter relative
to the config file) and the attestation provider, builds a
`ca2a_runtime.node.PeerNode`, and serves it with
`ca2a_runtime.transport.server`. The wiring lives in `ca2a_runtime.bootstrap`
(`load_policy`, `select_provider`, `build_peer_node`) so a program that already
has a `Policy` and a provider can keep constructing a `PeerNode` directly; the
CLI adds no capability the library did not have. No new dependencies: the
reference transport is standard library only. Provider selection fails closed.
`software-only` has no hardware guarantee, so `auto` refuses to start when no
confidential-computing platform is detected rather than silently choosing it,
and a named hardware provider whose device node is absent is a startup error.
Software mode prints what a caller will appraise the channel key as
(`assurance="none"`) instead of letting the operator assume otherwise. This
closes the `ca2a start` gap the transport spec listed. See issue #47.

### Changed

- `Ca2aConfig.listen_addr` now defaults to `127.0.0.1:8443` and must name a host
explicitly. It was `0.0.0.0:8443` while nothing read it; now that `ca2a start`
binds it, defaulting to every interface would put a peer on the network by
omission. `transport.server.serve` already defaulted to loopback, so the two
agree. Bracketed IPv6 (`[::1]:8443`) parses, and a malformed address fails
`ca2a validate-config` rather than at bind time.
- Dropped `enclave_private_key_hex` from the config. A `PeerNode` generates its
own X25519 channel keypair and publishes the public half through the
attestation handshake, so the caller seals to a key the node attested. A
pre-shared key in a config file would be sealing to something nobody appraised.

### Changed

- **BREAKING: TRACE records now carry the v0.2 profile** `tag:agentrust-io.com,2026:trace-v0.2`. Pins move to `agentrust-trace>=0.5` and `agentrust-trace-tests>=0.4,<0.5`, which have to move together: the conformance suite cut over rather than dual-accepting, so 0.4.0 of the suite fails a v0.1 record and 0.3.x fails a v0.2 one. The v0.1 URI named `agentrust.io`, a domain this project never controlled, which RFC 4151 does not permit for a tag URI (agentrust-io/trace-spec#107). Nothing else about the record format changed.
Expand Down
4 changes: 2 additions & 2 deletions LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ cA2A is a pre-release profile in active design. This document states plainly wha
## What is built

- The delegation credential model and the offline chain verifier skeleton: signature checks, scope attenuation (a child grant must be a provable subset of its parent), depth limits, and cross-chain replay rejection. The hardest of these semantics is reused from [agent-manifest](https://github.com/agentrust-io/agent-manifest), where it is implemented and tested.
- Configuration, error registry, and the CLI surface.
- Configuration, error registry, and the CLI surface, including `ca2a start`, which builds a `PeerNode` from a config file and serves it over the reference transport.
- A reference HTTP transport and the attestation handshake, in software mode. `ca2a_runtime.transport.server` and `ca2a_runtime.transport.client` (standard library only) run a live inbound A2A-profile call end to end: the caller fetches the callee's attested channel key, seals a payload to it, and sends a delegated task; the callee parses the A2A metadata with the adapter, runs verify + policy + enforce + open-sealed + provenance, and replies. `ca2a_runtime.attestation` gates the seal on a verified channel key. This is a **reference** transport, not part of the profile: the profile mandates no wire protocol (see Out of scope), and in software mode the peer key is accepted at `assurance="none"`.

## What is stubbed or not yet implemented

- **Hardware-attested live binding.** The `verifier` seam in `ca2a_runtime.attestation` has now been driven off a real SEV-SNP quote on an Azure confidential VM: `verify_offer` returned `assurance="hardware"`, a payload was sealed to a channel key a hardware-verified measurement vouches for, and both a measurement mismatch and a stale nonce were rejected. See [docs/hardware-validation.md](docs/hardware-validation.md). Two gaps remain. First, the reference server/client still run in **software mode** by default (`assurance="none"`); the hardware path is a validated capability, not the default configuration, and there is no CLI listener (`ca2a start`) since serving is via `ca2a_runtime.transport.server.serve`. Second, attestation on that run was one-directional: a follow-on cross-operator run (an Azure SEV-SNP peer calling a GCP Intel TDX peer, recorded in the same document) had the caller appraise the callee's real TDX quote before sealing, but the callee did not appraise the caller in return. Mutual simultaneous attestation is the remaining step, and both peers were driven by one operator's harness.
- **Hardware-attested live binding.** The `verifier` seam in `ca2a_runtime.attestation` has now been driven off a real SEV-SNP quote on an Azure confidential VM: `verify_offer` returned `assurance="hardware"`, a payload was sealed to a channel key a hardware-verified measurement vouches for, and both a measurement mismatch and a stale nonce were rejected. See [docs/hardware-validation.md](docs/hardware-validation.md). Two gaps remain. First, the reference server/client still run in **software mode** by default (`assurance="none"`); the hardware path is a validated capability, not the default configuration. `ca2a start` inherits that: it refuses to start under `provider: auto` when no confidential-computing platform is detected, so a software-mode listener is always a config that names `software-only`, never a downgrade. Second, attestation on that run was one-directional: a follow-on cross-operator run (an Azure SEV-SNP peer calling a GCP Intel TDX peer, recorded in the same document) had the caller appraise the callee's real TDX quote before sealing, but the callee did not appraise the caller in return. Mutual simultaneous attestation is the remaining step, and both peers were driven by one operator's harness.
- **Sealed peer channel (hardware property).** 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. On a live call the handshake now gates the seal on a channel key the caller has appraised, but in software mode that appraisal is `assurance="none"`. Until the seal is bound to a hardware-verified measurement (above), 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 and Intel TDX verifiers now appraise genuine hardware evidence end to end**: a real Azure CVM SEV-SNP report (VCEK chain to the AMD ARK-Milan root, ECDSA-P384 report signature, measurement binding) and a real GCP C3 DCAP v4 TDX quote (PCK chain to the Intel SGX Root CA, QE binding, quote signature, MRTD binding), both fail-closed and both rejecting a tampered copy. Runs are recorded in [docs/hardware-validation.md](docs/hardware-validation.md). The **TPM 2.0 verifier** (AK chain to a caller-supplied vendor root, AK signature, magic/type, qualifying-data and PCR-digest binding) is implemented but synthetic-vector validated only. Quote *generation* still requires the respective hardware for all three. This validates the verifier, not a running attested peer: until the `verifier` seam in `ca2a_runtime.attestation` is driven off a live quote on a confidential VM, cA2A must not be described as attested across trust domains.

Expand Down
2 changes: 1 addition & 1 deletion 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`. Live transport **landed** (`ca2a_runtime.transport`: A2A wire binding in `transport.a2a`, a reference standard-library HTTP server and client, and `ca2a_runtime.node.PeerNode`), exercised end to end in software mode by `tests/unit/test_live_call.py`
- 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 transport **landed** (`ca2a_runtime.transport`: A2A wire binding in `transport.a2a`, a reference standard-library HTTP server and client, and `ca2a_runtime.node.PeerNode`), exercised end to end in software mode by `tests/unit/test_live_call.py` and runnable from a config file with `ca2a start`
- Sealed peer channel: **landed** (`ca2a_runtime.channel`: HPKE-style X25519 -> HKDF-SHA256 -> ChaCha20-Poly1305 sealing to the peer's attested key; claim C4 validated). The seal is now **gated on a verified channel key** by the attestation handshake (`ca2a_runtime.attestation`: offer, verify, seal), so a payload is sealed only to an attested peer key; software mode records `assurance="none"` and hardware plugs in via a `verifier` callable. Remaining hardware property: the enclave holding the private key, established on a confidential VM
- Linked runtime evidence: **landed** (`ca2a_runtime.trace_binding` emits a signed TRACE record per hop with the A2A `delegation` block; `ca2a_verify.verify_trace_dag` verifies the DAG offline, each link committing to the parent's full signed record). Built on `agentrust-trace` (Ed25519 + RFC 8785), reused not reimplemented. Software-mode records are Level 0; a hardware TEE run lifts them to Level 1. See `examples/trace-dag/`.

Expand Down
37 changes: 28 additions & 9 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# 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 to build a
[`PeerNode`](spec/component-model.md) and serve it over the reference HTTP
transport.

## Reference

Expand All @@ -10,26 +13,42 @@ attestation:
enforcement_mode: enforcing # enforcing | advisory | silent

max_delegation_depth: 8 # reject chains deeper than this
listen_addr: "0.0.0.0:8443"
listen_addr: "127.0.0.1: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
```

## 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 a detected hardware provider and fails if there is none; it never falls back to `software-only`, which has to be named explicitly. `opaque` is not implemented. |
| `attestation.enforcement_mode` | `enforcing` | Intended mode. The peer path always fails closed on cA2A denials today; advisory and 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` | `127.0.0.1:8443` | Address `ca2a start` binds. The host is never defaulted, so serving on every interface has to be written out. |
| `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, resolved relative to the config file. When set, used instead of `local_policy`. |

## Validate
There is no key field: a `PeerNode` generates its own X25519 channel keypair at
startup and publishes the public half through the attestation handshake, so a
caller seals to a key the node attested rather than one written into a file.

## Validate and start

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

ca2a start --config examples/minimal/ca2a-config.yaml
# note: software-only provider, callers appraise this channel key as
# assurance="none" and the seal carries no hardware guarantee
# ca2a listening on 127.0.0.1:8443 (provider=software-only)
```

`ca2a start` needs no extra install: the reference transport is standard library
only. It is one way to run the peer path, not part of the profile. A program that
already has a `Policy` and a provider can build a `PeerNode` and serve it from
its own A2A server instead.

Invalid values fail fast with a `CONFIG_ERROR` and a message naming the offending field.
9 changes: 5 additions & 4 deletions docs/spec/component-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ The cA2A runtime is a set of small, composable modules under `src/`. Each maps t

### config

`ca2a_runtime.config` holds `Ca2aConfig`, a frozen dataclass validated by `from_dict()` / `load()`. It defines the surface the runtime peer path will consume: `provider` (from `VALID_PROVIDERS`), `enforcement_mode` (from `VALID_ENFORCEMENT`), `max_delegation_depth`, `policy_bundle_path`, and `listen_addr`. Invalid values raise `CONFIG_ERROR`. The config surface is implemented and validated; the peer path that consumes `enforcement_mode`, `policy_bundle_path`, and `listen_addr` is Tier 2 and not yet built.
`ca2a_runtime.config` holds `Ca2aConfig`, a frozen dataclass validated by `from_dict()` / `load()`: `provider` (from `VALID_PROVIDERS`), `enforcement_mode` (from `VALID_ENFORCEMENT`), `max_delegation_depth`, `policy_bundle_path`, `local_policy`, and `listen_addr`. Invalid values raise `CONFIG_ERROR`. `ca2a_runtime.bootstrap` turns a validated config into a running `PeerNode`: it resolves the policy from `local_policy` or `policy_bundle_path` and the provider from `provider`, both fail-closed. `enforcement_mode` is still only recorded; the peer path always fails closed on a denial.

### errors

`ca2a_runtime.errors` is the central registry. Every error is a `CA2AError` subclass carrying a stable `code` and an `http_status`: `CONFIG_ERROR`, `INVALID_CREDENTIAL`, `SCOPE_ESCALATION`, `BROKEN_DELEGATION_LINK`, `DELEGATION_DEPTH_EXCEEDED`, `CREDENTIAL_REPLAY`, `ATTESTATION_UNSUPPORTED`, `ATTESTATION_FAILED`, `SEALED_CHANNEL_ERROR`, `PROVENANCE_LINK_BROKEN`. See [error codes](error-codes.md).

### cli

`ca2a_runtime.cli` exposes the `ca2a` command with two subcommands: `validate-config --config` (loads and validates a `Ca2aConfig`) and `verify-chain --chain [--max-depth]` (calls `verify_chain_file` and prints a JSON result). Both are implemented and operate offline.
`ca2a_runtime.cli` exposes the `ca2a` command. `validate-config --config` loads and validates a `Ca2aConfig`, `verify-chain --chain [--max-depth]` calls `verify_chain_file`, and `verify-dag --dag [--chain]` verifies a provenance DAG; all three operate offline. `start --config` is the one online command: it builds a `PeerNode` through `ca2a_runtime.bootstrap` and serves it with `ca2a_runtime.transport.server`.

## Component map

Expand All @@ -43,9 +43,10 @@ The cA2A runtime is a set of small, composable modules under `src/`. Each maps t
| delegation | `ca2a_runtime.delegation.credential` | `DelegationCredential`, `new_keypair`, `verify_chain` | Implemented |
| provenance | `ca2a_runtime.provenance` | `DelegationRecord`, `record_for`, `verify_dag`, `cross_check_chain` | Implemented |
| verify | `ca2a_verify.verify` | `verify_delegation_chain`, `verify_chain_file`, `ChainResult` | Implemented |
| config | `ca2a_runtime.config` | `Ca2aConfig` | Surface implemented; peer path pending (Tier 2) |
| config | `ca2a_runtime.config` | `Ca2aConfig` | Implemented |
| bootstrap | `ca2a_runtime.bootstrap` | `load_policy`, `select_provider`, `build_peer_node` | Implemented |
| errors | `ca2a_runtime.errors` | `CA2AError` and subclasses | Implemented |
| cli | `ca2a_runtime.cli` | `ca2a validate-config`, `ca2a verify-chain` | Implemented |
| cli | `ca2a_runtime.cli` | `ca2a validate-config`, `ca2a verify-chain`, `ca2a verify-dag`, `ca2a start` | Implemented |
| channel | `ca2a_runtime.channel.sealed` | `SealedChannel` | Placeholder, fails closed (Tier 2) |
| tee | `ca2a_runtime.tee.base` | `BaseProvider`, `AttestationReport` | Interface only; hardware backends pending (Tier 3) |

Expand Down
Loading
Loading