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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Packages without a separate changelog are covered by the cross-package notes below.

## [Unreleased - Minor]
## [Unreleased - Major]

### Added

- Added a pinned, multi-architecture self-host container and Compose runbook with persistent SQLite/files state, health checks, and mandatory HTTPS deployment-authority validation.
- A2A federation now preserves structured DM metadata end to end, defines a versioned `com.agentrelay.ratify` carrier for proof bundles and signed revocation lists, and supports reciprocal authenticated delivery between registered peers.

### Fixed

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ A2A (Agent-to-Agent) gateway endpoints:
```text
POST /v1/a2a/register Register an external A2A agent
GET /v1/a2a/agents List registered A2A agents
PATCH /v1/a2a/agents/:name Complete or rotate an A2A connection
DELETE /v1/a2a/agents/:name Remove an A2A agent
GET /v1/a2a/agents/:name/card Get agent card for a registered agent
GET /.well-known/agent-card.json A2A agent card (root-level; ?workspace= selects on multi-tenant)
Expand All @@ -655,6 +656,10 @@ POST /a2a/rpc A2A JSON-RPC gateway (root-level)
POST /a2a/webhook/:ws/:name Inbound webhook for relay agents
```

Single-workspace deployments serve the standard bare agent-card URL without a
workspace query parameter. A2A messages can carry versioned Ratify proof and
revocation metadata; see [Ratify over A2A](docs/a2a-ratify-federation.md).

Programmability, directory & observability:

```text
Expand Down
99 changes: 99 additions & 0 deletions docs/a2a-ratify-federation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Ratify over A2A

Relaycast carries Ratify Protocol proofs and revocations in the A2A message
metadata key `com.agentrelay.ratify`. The key lives at
`params.message.metadata["com.agentrelay.ratify"]`, not in the gateway's
top-level `params.metadata` routing fields.

The version 1 payload is a closed, discriminated shape. Wire field names are
snake_case. A receiver must reject an unknown `version` or `kind` rather than
guessing how to interpret it.

## Proof bundle

```json
{
"version": 1,
"kind": "proof_bundle",
"correlation_id": "challenge-or-invocation-id",
"bundle": "{canonical Ratify ProofBundle JSON}",
"grant": "{optional canonical Ratify DelegationCert JSON}",
"operation": {
"invocation_id": "task-42"
},
"task": {
"title": "Update the runbook",
"instructions": "Edit the deployment section",
"path": "docs/"
}
}
```

`bundle` is required in both presentation directions. `grant`, `operation`,
and `task` are optional so the same carrier supports a mutual presentation or
a delegated task handoff. When present, `grant` is the canonical Ratify
DelegationCert wire JSON. The UTF-8 byte length of `bundle` may not exceed
131,072 bytes (`MAX_PROOF_BUNDLE_BYTES`, 128 KiB). The surrounding A2A JSON-RPC
request is necessarily larger and must not be capped at 128 KiB by a proxy.

## Revocation list

```json
{
"version": 1,
"kind": "revocation_list",
"issuer_id": "human:northwind.example:alice",
"updated_at": 1786310000,
"revoked_certs": ["cert_01"],
"issuer_pub_key": {
"ed25519": "base64...",
"ml_dsa_65": "base64..."
},
"signature": {
"ed25519": "base64...",
"ml_dsa_65": "base64..."
}
}
```

The receiver must not apply any `revoked_certs` merely because this metadata
arrived over an authenticated A2A connection. It must:

1. resolve `issuer_id` to an already trusted issuer public key;
2. require `issuer_pub_key` to match that trusted key;
3. reconstruct the Ratify `RevocationList` from `issuer_id`, `updated_at`,
`revoked_certs`, and `signature`; and
4. call Ratify `verifyRevocationList` with the trusted issuer key before
changing local revocation state.

The public key beside a signature is not its own trust anchor. A self-signed
attacker payload that is not bound to the expected `issuer_id` must fail closed.

## Two-Relaycast handshake and delivery

For deployments A and B to exchange messages in both directions:

1. B registers A's agent card with `POST /v1/a2a/register`, setting the skill
on A as `target_agent`. B returns a relay proxy name and bearer token for A.
2. A registers B's card, stores B's returned token as `auth_credential`, and
sets the skill on B as `target_agent`. A returns its proxy token for B.
3. B completes the reciprocal connection with
`PATCH /v1/a2a/agents/{a-proxy-name}`, setting A's returned token as
`auth_credential`. The patch does not rotate either already-exchanged token.
A card with exactly one skill infers `target_agent`; multi-skill cards should
set it explicitly.
4. A sends a DM to B's local A2A proxy with the Ratify envelope in the DM
request's `data`. Relaycast places it in A2A message metadata unchanged.
5. B authenticates the bearer as the registered proxy for A, delivers the
message and metadata to the selected local agent, and emits the normal
`dm.received` delivery. B-to-A delivery follows the same path with the other
stored credential.

A workspace key can use `/a2a/rpc` as an outbound gateway, but it cannot inject
a message directly into a local agent. Only the agent token issued to a
registered A2A proxy can use the inbound path, and that token cannot relay to a
second external A2A agent.

Workspace agent cards advertise this extension under
`capabilities.extensions["com.agentrelay.ratify"]`, including supported
versions, kinds, and the proof-bundle byte maximum.
85 changes: 85 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ components:
$ref: '#/components/schemas/FileAttachment'
metadata:
type: object
additionalProperties: true
description: User message metadata. Internal `__relaycast_*` keys are stripped from public responses and caller metadata cannot override `agent_name`.
has_attachments:
type: boolean
Expand Down Expand Up @@ -350,6 +351,10 @@ components:
type: array
items:
$ref: '#/components/schemas/FileAttachment'
metadata:
type: object
additionalProperties: true
description: Public message metadata, including A2A extension payloads

DmSendResponse:
type: object
Expand Down Expand Up @@ -378,6 +383,10 @@ components:
type: array
items:
$ref: '#/components/schemas/FileAttachment'
metadata:
type: object
additionalProperties: true
description: Public message metadata, including A2A extension payloads

FlatDmMessage:
type: object
Expand All @@ -401,6 +410,10 @@ components:
type: array
items:
$ref: '#/components/schemas/FileAttachment'
metadata:
type: object
additionalProperties: true
description: Public message metadata, including A2A extension payloads
created_at:
type: string
format: date-time
Expand Down Expand Up @@ -2698,6 +2711,13 @@ paths:
items:
type: string
description: Optional file ids to attach to this DM
data:
type: object
nullable: true
additionalProperties: true
description: >-
Public structured message metadata. For Ratify over A2A,
place the versioned envelope at `com.agentrelay.ratify`.
mode:
type: string
enum: [wait, steer]
Expand Down Expand Up @@ -4878,6 +4898,28 @@ paths:
post:
summary: Register an external A2A agent
tags: [A2A]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
agent_card_url:
type: string
format: uri
agent_card:
type: object
auth_scheme:
type: string
enum: [bearer, api_key, none]
auth_credential:
type: string
target_agent:
type: string
description: >-
Remote card skill that DMs to the local proxy address. It
is inferred when the card advertises exactly one skill.
responses:
'201':
description: Agent registered
Expand All @@ -4899,6 +4941,43 @@ paths:
type: object

/a2a/agents/{name}:
patch:
summary: Complete or rotate an A2A connection
description: >-
Updates the outbound credential or selected remote skill without
rotating the inbound relay token. Use this after reciprocal
registration to complete authenticated delivery in both directions.
tags: [A2A]
parameters:
- name: name
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
minProperties: 1
properties:
auth_scheme:
type: string
enum: [bearer, api_key, none]
auth_credential:
type: string
nullable: true
target_agent:
type: string
nullable: true
responses:
'200':
description: Connection updated
content:
application/json:
schema:
type: object
delete:
summary: Remove an A2A agent
tags: [A2A]
Expand Down Expand Up @@ -4942,6 +5021,12 @@ paths:
description: Local development server
post:
summary: JSON-RPC gateway for A2A messages
description: >-
Sends to a registered external A2A agent or, when authenticated with
the bearer token issued by A2A registration, delivers to a local agent.
Ratify envelopes use
`params.message.metadata["com.agentrelay.ratify"]`; see
`docs/a2a-ratify-federation.md` for the version 1 shapes.
tags: [A2A]
responses:
'200':
Expand Down
Loading
Loading