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
20 changes: 11 additions & 9 deletions docs/rfcs/a2a-delegation-profile.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# RFC Proposal: A2A delegation-link verification profile

When one AI agent hands a task to another, each can write a TRACE record, and the later record points back to the earlier one (a delegation link). This proposal sets out how a checker follows those links and decides whether each hand-off was allowed. It is for people building verifiers, or agents that pass work to each other over A2A (the agent-to-agent protocol). It is a draft: nothing in it binds an implementation yet, and it ships with 24 test vectors so independent implementations can be compared.
When one AI agent hands a task to another, each can write a TRACE record, and the later record points back to the earlier one (a delegation link). This proposal sets out how a checker follows those links and decides whether each hand-off was allowed. It is for people building verifiers, or agents that pass work to each other over A2A (the agent-to-agent protocol). It is a draft: nothing in it binds an implementation yet, and it ships with 26 test vectors so independent implementations can be compared.

**Status:** Draft proposal. Binds nothing.
**Scope:** Verification rules for the existing `delegation` block. No schema change.
**Target:** `spec/trace-v0.2.md` §3.1 surface, for the v0.3 A2A profile named in `ROADMAP.md`.
**Conformance material: ** `examples/delegation-link/`: 24 vectors, generator, published key.
**Conformance material: ** `examples/delegation-link/`: 26 vectors, generator, published key.

Requirement keywords are lowercase throughout this document, deliberately. `CONTRIBUTING.md`
draws the line that normative text lives in the specifications and informative text binds no
Expand Down Expand Up @@ -88,7 +88,7 @@ an answer that looks like verification and is not. Naming them is half the profi

## 3. The rules

Ten rules, each with a stable code, a classification, and two independent vectors.
Eleven rules, each with a stable code, a classification, and two independent vectors.
Classification is the three-way distinction the conformance corpus in `agentrust-io/ca2a`
already uses for its action cases: a structural failure, an authority failure, and a chain
that could not be read are three different findings, and a verifier that collapses any two
Expand All @@ -106,6 +106,7 @@ of them is reporting something other than what it found.
| D-8 | `credential_window` | authorization | This record's `iat` falls inside the credential's validity window. | 16, 17 |
| D-9 | `data_class_widened` | authorization | A hop's `data_class` is no more sensitive than its parent's, per the supplied lattice. | 18, 19 |
| D-10 | `digest_algorithm_unsupported` | unverifiable | A link naming an algorithm the verifier cannot compute yields an unverifiable chain. | 22, 23 |
| D-11 | `data_class_unknown` | unverifiable | A hop whose child or parent `data_class` is absent from `data_class_lattice` (the same string at both ends included) yields an unverifiable chain. | 25, 26 |

Provenance outranks authorization in the reported classification. A chain whose structure is
broken has no established parent for a credential to be judged against, so reporting an
Expand Down Expand Up @@ -188,11 +189,11 @@ freshness policy on top of this profile; no rule here reads it.

## 5. The conformance corpus

`examples/delegation-link/` holds 24 vectors. Each is one scenario: a complete record set,
`examples/delegation-link/` holds 26 vectors. Each is one scenario: a complete record set,
the verifier context to judge it under, and the classification and codes it expects, so a
third party can score an implementation without running anything from this repository.

Twenty of the twenty-four score the ten rules above, two per rule with a
Twenty-two of the twenty-six score the eleven rules above, two per rule with a
declared implementation defect that one vector catches and the other misses.
Three more (`01` to `03`) are clean chains, expected to verify with no rule
firing. The remaining vector, `24-parent-key-supplementary-plane.json`, is
Expand Down Expand Up @@ -222,10 +223,11 @@ Coverage is held to the discipline argued in #124 and executed in
`tests/test_vector_completeness.py`: two load-bearing vectors per rule, and: the part that
is not satisfied by writing the same vector twice: at least one declared implementation
defect that one vector catches and the other misses. `tests/test_delegation_completeness.py`
declares those defects, all ten of them modelling a real shortcut: verifying the leaf only,
anchoring on any trusted key found, an off-by-one bound, case-insensitive identifier lookup,
issuer and holder compared to the wrong ends, half a validity window, narrowing checked at
one hop.
declares those defects, all eleven of them modelling a real shortcut: verifying the leaf only,
anchoring on any trusted key found, an off-by-one bound, reading only the first link's
digest algorithm, resolving over the signed body, case-insensitive identifier lookup,
issuer and holder compared to the wrong ends, half a validity window, checking only the
child's data class against the lattice, and narrowing checked at one hop.

Vector identifiers are `TRACE-DELEG-NNN` and are never reused. They are deliberately not
`ACTION-*`: that namespace belongs to ca2a's conformance set, whose own rule is that its
Expand Down
134 changes: 134 additions & 0 deletions examples/delegation-link/25-data-class-unknown-at-leaf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"id": "TRACE-DELEG-025",
"name": "data-class-unknown-at-leaf",
"description": "The leaf declares `top-secret`, which is schema-valid and absent from the verifier's `data_class_lattice`. Because a class outside the supplied ordering cannot be compared against its parent, the hop's narrowing is `unverifiable` rather than silently accepted as `verified`.",
"spec": "docs/rfcs/a2a-delegation-profile.md",
"profile": "trace.a2a.delegation-link.v0",
"context": {
"leaf": "sha256:043e46131b38758706e4298e7e3d2d70bea5f44a23796ad56eb7ff62888a3e78",
"now": 1785000000,
"max_depth": 4,
"supported_digest_algorithms": [
"sha256"
],
"data_class_lattice": [
"public",
"internal",
"confidential",
"restricted"
],
"trusted_root_keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "pNTZUXlAITEWbtVbHk6zGRwVD73s0BEakqkKKaFQyZ4"
}
],
"credentials": {
"cred:orchestrator-to-planner": {
"issuer": "spiffe://acme.example/agent/orchestrator",
"holder": "spiffe://acme.example/agent/planner",
"not_before": 1784913600,
"not_after": 1785086400
},
"cred:planner-to-executor": {
"issuer": "spiffe://acme.example/agent/planner",
"holder": "spiffe://acme.example/agent/executor",
"not_before": 1784913600,
"not_after": 1785086400
},
"cred:executor-to-courier": {
"issuer": "spiffe://acme.example/agent/executor",
"holder": "spiffe://acme.example/agent/courier",
"not_before": 1784913600,
"not_after": 1785086400
},
"cred:courier-to-auditor": {
"issuer": "spiffe://acme.example/agent/courier",
"holder": "spiffe://acme.example/agent/auditor",
"not_before": 1784913600,
"not_after": 1785086400
}
}
},
"records": [
{
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000,
"subject": "spiffe://acme.example/agent/planner",
"model": {
"provider": "anthropic",
"model_id": "claude-sonnet-4-6"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "top-secret",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"delegation": {
"parent_record_hash": "sha256:b1d65c65d7191e1c6f57f8902548475671edad51035f1542f76a4e45337052b6",
"credential_id": "cred:orchestrator-to-planner"
},
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "W2BsRtHZW8BVdiCsCxrNxvv4LOfgBhuS_2v7TU7TyHU"
}
},
"signature": "lFd-i3GNlYdytslfkVq8Zm2RDPznm2BkEO-7opbCz8LEHugruH3i5NmhpnDbwFxcCdjpYm_cJFkkyPsqv6v7DA"
},
{
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000,
"subject": "spiffe://acme.example/agent/orchestrator",
"model": {
"provider": "anthropic",
"model_id": "claude-sonnet-4-6"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "restricted",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "pNTZUXlAITEWbtVbHk6zGRwVD73s0BEakqkKKaFQyZ4"
}
},
"signature": "m3TAE_Z_chc0gs8HMh1KAN4NECFLP09kiqyQPBDz9w9G0_iYoQ5-I94gpptC1a4xiRloSFLyTdprEmr9CFD_DA"
}
],
"expected": {
"classification": "unverifiable",
"codes": [
"data_class_unknown"
]
}
}
134 changes: 134 additions & 0 deletions examples/delegation-link/26-data-class-unknown-at-parent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"id": "TRACE-DELEG-026",
"name": "data-class-unknown-at-parent",
"description": "The parent declares `top-secret`, absent from `data_class_lattice`, while the leaf declares `confidential`, which is in the lattice. Narrowing compares both ends of the hop: a verifier that checks only whether the child's class is in the lattice passes this chain as `verified` while catching vector 25.",
"spec": "docs/rfcs/a2a-delegation-profile.md",
"profile": "trace.a2a.delegation-link.v0",
"context": {
"leaf": "sha256:7a1b046fe79faafc8e9515c90afc26da3a2e05d0eb0a0090b06599fba7127141",
"now": 1785000000,
"max_depth": 4,
"supported_digest_algorithms": [
"sha256"
],
"data_class_lattice": [
"public",
"internal",
"confidential",
"restricted"
],
"trusted_root_keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "pNTZUXlAITEWbtVbHk6zGRwVD73s0BEakqkKKaFQyZ4"
}
],
"credentials": {
"cred:orchestrator-to-planner": {
"issuer": "spiffe://acme.example/agent/orchestrator",
"holder": "spiffe://acme.example/agent/planner",
"not_before": 1784913600,
"not_after": 1785086400
},
"cred:planner-to-executor": {
"issuer": "spiffe://acme.example/agent/planner",
"holder": "spiffe://acme.example/agent/executor",
"not_before": 1784913600,
"not_after": 1785086400
},
"cred:executor-to-courier": {
"issuer": "spiffe://acme.example/agent/executor",
"holder": "spiffe://acme.example/agent/courier",
"not_before": 1784913600,
"not_after": 1785086400
},
"cred:courier-to-auditor": {
"issuer": "spiffe://acme.example/agent/courier",
"holder": "spiffe://acme.example/agent/auditor",
"not_before": 1784913600,
"not_after": 1785086400
}
}
},
"records": [
{
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000,
"subject": "spiffe://acme.example/agent/planner",
"model": {
"provider": "anthropic",
"model_id": "claude-sonnet-4-6"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "confidential",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"delegation": {
"parent_record_hash": "sha256:e075b86c9588fb5addd9380646ec063f6f85a453ecf0b49aeaaaca4cc55caabb",
"credential_id": "cred:orchestrator-to-planner"
},
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "W2BsRtHZW8BVdiCsCxrNxvv4LOfgBhuS_2v7TU7TyHU"
}
},
"signature": "PkPQBkfZzYGVDa9O6fj4bwmAUK6VfbZ5bg8qkdEt_WVONX6W5Tu2QO88m_LyxF6HKuxiZflJOZBiPdKHU5__DA"
},
{
"eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
"iat": 1785000000,
"subject": "spiffe://acme.example/agent/orchestrator",
"model": {
"provider": "anthropic",
"model_id": "claude-sonnet-4-6"
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"policy": {
"bundle_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"enforcement_mode": "enforce"
},
"data_class": "top-secret",
"build_provenance": {
"slsa_level": 0,
"digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"appraisal": {
"status": "affirming",
"verifier": "https://verifier.example/v1"
},
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "pNTZUXlAITEWbtVbHk6zGRwVD73s0BEakqkKKaFQyZ4"
}
},
"signature": "SovhFNqtpvIkmDdgLtW4Tsrb_KLzjiQ1gbiI3bxgUh5GwFNYr18ZQ7YZOwqM6rhSIocskYJNj6zVwuJjzgXZCg"
}
],
"expected": {
"classification": "unverifiable",
"codes": [
"data_class_unknown"
]
}
}
7 changes: 4 additions & 3 deletions examples/delegation-link/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,17 @@ and compares bytes on every run, with no entry in its `NOT_GENERATED` ledger.

## Coverage

Ten rules, two load-bearing vectors each, and for every rule at least one declared
Eleven rules, two load-bearing vectors each, and for every rule at least one declared
implementation defect that one of its vectors catches and the other misses.
`tests/test_delegation_completeness.py` enforces all three, and records the margins so they
cannot silently thin.

The pairs are not two views of the same mistake. They are built around a specific shortcut a
real implementation takes — verifying the leaf only, anchoring on any trusted key it finds,
an off-by-one bound, case-insensitive lookup of an opaque identifier, issuer and holder
compared to the wrong ends of the hop, half a validity window, narrowing checked at one hop,
the link algorithm read once and assumed uniform.
compared to the wrong ends of the hop, half a validity window, checking only the child's
data class against the lattice, narrowing checked at one hop, the link algorithm read once
and assumed uniform.

Vector 05 is the one to read first. It is a complete, correctly signed chain whose only
defect is which bytes its link was computed over, and it is the whole of the difference
Expand Down
33 changes: 33 additions & 0 deletions examples/delegation-link/gen_delegation_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,39 @@ def add(filename: str, doc: dict[str, Any]) -> None:
classification="verified", codes=[], credentials=WELL_FORMED,
))

# -- data_class_unknown -----------------------------------------------------

add("25-data-class-unknown-at-leaf.json", vector(
"TRACE-DELEG-025", "data-class-unknown-at-leaf",
"The leaf declares `top-secret`, which is schema-valid and absent from "
"the verifier's `data_class_lattice`. Because a class outside the "
"supplied ordering cannot be compared against its parent, the hop's "
"narrowing is `unverifiable` rather than silently accepted as `verified`.",
build_chain([
{"role": "orchestrator", "data_class": "restricted"},
{"role": "planner", "data_class": "top-secret",
"credential_id": CRED_PLANNER[0]},
]),
classification="unverifiable", codes=["data_class_unknown"],
credentials=WELL_FORMED,
))

add("26-data-class-unknown-at-parent.json", vector(
"TRACE-DELEG-026", "data-class-unknown-at-parent",
"The parent declares `top-secret`, absent from `data_class_lattice`, "
"while the leaf declares `confidential`, which is in the lattice. "
"Narrowing compares both ends of the hop: a verifier that checks only "
"whether the child's class is in the lattice passes this chain as "
"`verified` while catching vector 25.",
build_chain([
{"role": "orchestrator", "data_class": "top-secret"},
{"role": "planner", "data_class": "confidential",
"credential_id": CRED_PLANNER[0]},
]),
classification="unverifiable", codes=["data_class_unknown"],
credentials=WELL_FORMED,
))

for name, doc in sorted(out):
(OUT / name).write_text(
json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", newline="\n"
Expand Down
4 changes: 4 additions & 0 deletions src/agentrust_trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@
from agentrust_trace import intent_bridge as intent_bridge # noqa: E402

__all__ = [*__all__, "intent_bridge"]
from agentrust_trace import delegation as delegation # noqa: E402

__all__ = [*__all__, "delegation"]

Loading
Loading