What's New in ACP — Last 7 Days¶
Last updated: 2026-04-10 For the full history see CHANGELOG.md
v2.95.0 — Skill-Scoped Trust Scores (2026-04-10)¶
ACP v2.95 introduces per-skill trust scores derived from bilateral interaction record evidence — enabling callers to evaluate trust at the skill level rather than relying solely on aggregate peer scores. Aligned with A2A Issue #1717 community convergence.
New endpoint: GET /trust/skill-scores
{
"ok": true,
"trust_scores": {
"text.summarize": 0.525,
"code.review": 0.435
},
"method": "skill_scoped_v1",
"algorithm": {
"base": 0.3,
"caller_diversity": "min(unique_callers, 10) * 0.04",
"volume": "min(bilateral_count, 50) * 0.005",
"max": 1.0
},
"skill_count": 2,
"ir_count": 8,
"version": "2.95.0"
}
QuerySkill now returns skill_trust_score:
curl -X POST http://localhost:18900/skills/query \
-H "Content-Type: application/json" \
-d '{"skill_id": "text.summarize"}'
# → {"skill_trust_score": 0.525, "support_level": "supported", ...}
governance_metadata updated:
{
"trust_scores": {"text.summarize": 0.525},
"trust_score_method": "skill_scoped_v1",
"trust_score": 0.75
}
Global trust_score retained for full backward compatibility. When bilateral IR evidence exists, it is updated to the per-skill average. Empty {} = no evidence yet (not an error).
Test coverage: SS01–SS16 = 16/16 PASS | See CHANGELOG
v2.94.0 — Principal Diversity Defense (2026-04-10)¶
v2.79.0 — Protocol Binding Declaration / A2A §5.8 CPB (2026-04-07)¶
ACP v2.79 adds GET /protocol-binding and embeds protocol_binding in the AgentCard, aligned with A2A §5.8 (merged PR #1619, 2026-04-07).
Binding URI: urn:acp:binding:p2p-relay/v1
{
"ok": true,
"version": "2.79.0",
"binding_uri": "urn:acp:binding:p2p-relay/v1",
"binding_name": "ACP P2P Relay",
"transport": "p2p+relay",
"addressing": "acp://<relay_host>/<session_token>",
"nat_traversal": true,
"nat_levels": 3,
"supports_sse": true,
"supports_ws": true
}
The AgentCard (/.well-known/acp.json) now includes protocol_binding as a top-level field.
Test coverage: PB-01..PB-25 = 25/25 PASS; aligned with A2A PR #1619 §5.8
v2.78.0 — Active SINT Token Revocation (2026-04-07)¶
ACP v2.78 adds POST /trust/signals/capability-token/revoke and GET .../revocations — completing the SINT capability quad (A2A #1716).
SINT capability quad — full token lifecycle:
| Version | Endpoint | Role |
|---|---|---|
| v2.74 | GET /trust/signals/capability-token |
Declare: relay's token issuance config |
| v2.75 | GET /trust/signals/capability-token/fixtures |
Fixture: canonical 4-deny + 1-allow vectors |
| v2.77 | POST .../fixtures/validate |
Validate: runtime enforcement (6-check pipeline) |
| v2.78 | POST .../revoke |
Revoke: active JTI revocation |
Revoke endpoint:
curl -X POST http://localhost:18900/trust/signals/capability-token/revoke \
-H "Content-Type: application/json" \
-d '{"jti": "urn:acp:token:abc123", "reason": "compromised"}'
# → {"ok": true, "revoked": true, "jti": "...", "revocation_id": "rev-...", ...}
Revocation list:
curl http://localhost:18900/trust/signals/capability-token/revocations
# → {"ok": true, "total_revoked": 3, "revocations": [...]}
Validate now includes Check 6 (revocation — highest priority):
- Revoked JTI → authorized: false, deny_reason: "token_revoked"
- Clean JTI → revocation: {passed: true, reason: "token_not_revoked"}
Test coverage: RV-01..RV-30 = 30/30 PASS; full regression 157/157 PASS
v2.77.0 — Dynamic SINT Token Validation (2026-04-07)¶
ACP v2.77 adds POST /trust/signals/capability-token/fixtures/validate — the runtime enforcement endpoint that completes the SINT capability triad (A2A #1716 @pshkv).
SINT capability triad:
| Version | Endpoint | Role |
|---|---|---|
| v2.74 | GET /trust/signals/capability-token |
Declaration — what this relay issues |
| v2.75 | GET /trust/signals/capability-token/fixtures |
Static fixture vectors (4-deny+1-allow) |
| v2.77 | POST /trust/signals/capability-token/fixtures/validate |
Dynamic validation — runtime enforcement |
5-check validation pipeline:
| # | Check | Description |
|---|---|---|
| 1 | expiry |
Re-verifies exp at use_time (TOCTOU re-check) |
| 2 | scope |
resource URI tail must match target_skill_id |
| 3 | skill_id |
Resource path structural validation |
| 4 | subject |
token.sub must match invoking_agent_did |
| 5 | required_fields |
{jti, iss, sub, resource, scheme} all present |
Priority deny order: expiry > scope > skill_id > subject > required_fields
Tests: TV-01..TV-30 = 30/30 PASS | Full regression: 127/127 PASS | Commit: 7cb7f90
v2.76.0 — effective_tier Factor 5: bilateral_ir_adj (2026-04-07)¶
ACP v2.76 upgrades effective_tier to a 5-factor architecture, adding bilateral_ir_adj derived from the local bilateral IR log — a tamper-evident attestation without requiring an external chain (A2A #1716 @64R3N).
New computation functions:
| Function | Returns | Purpose |
|---|---|---|
_bilateral_ir_merkle_root(peer_id) |
str \| None |
SHA-256 Merkle root over bilateral IR records for peer |
_bilateral_ir_adj(peer_id) |
(adj, count, merkle_root) |
Factor 5 adjustment value |
Threshold logic:
| Bilateral records | bilateral_ir_adj |
Meaning |
|---|---|---|
| 0 (unknown peer) | +1 |
Raises tier floor — conservative |
| 1–4 | 0 |
Neutral — limited history |
| ≥5 | −1 |
Established peer — may lower floor |
5-factor combination rule:
- Any +1 overrides immediately (conservative wins)
- −1 requires ≥2 of 3 adjustment factors to agree (consensus required to lower floor)
AgentCard: capabilities.effective_tier_five_factors: true
Tests: ET-01..ET-30 = 30/30 PASS | Full regression: 153/153 PASS | Commit: a469555
v2.75.0 — Canonical Authorization Fixture Endpoint (2026-04-07)¶
ACP v2.75 adds GET /trust/signals/capability-token/fixtures — the minimal canonical
authorization test fixture set proposed by @pshkv in A2A #1716 (SINT PR#111).
Why it matters: Cross-project interoperability requires shared, executable test vectors.
@pshkv's proposal calls for a standard fixture contract at the AgentSkill boundary so that
any SINT-compatible implementation can validate its token verification logic against the same
scenarios. ACP is the first relay to ship this as a live queryable endpoint.
The 5 fixture vectors:
| ID | Verdict | Scenario |
|---|---|---|
allow_valid_subject_bound |
✅ allow | All fields nominal, signature valid, not expired, scope + subject match |
deny_scope_mismatch |
❌ deny | Token resource ≠ target skill — cross-skill token reuse blocked |
deny_expired_toctou |
❌ deny | TOCTOU: valid at check time, expired at use time |
deny_skill_id_mismatch |
❌ deny | Resource URI encodes different skill_id than invocation |
deny_subject_mismatch |
❌ deny | sub DID ≠ invoking agent DID — cross-agent token replay blocked |
Each fixture includes a token object with realistic SINT fields, invocation_context,
and expected_result (authorized, reason_code, http_status). Timestamps are computed
dynamically relative to now so fixtures are always temporally coherent.
AgentCard: capabilities.capability_token_fixtures: true +
endpoints.capability_token_fixtures: "/trust/signals/capability-token/fixtures"
Tests: CF-01..CF-20 — 20/20 PASS. Full regression 123/123 PASS.
v2.74.0 — SINT Capability Token Declaration Endpoint (2026-04-07)¶
ACP v2.74 adds GET /trust/signals/capability-token — a dedicated endpoint that exposes
full capability token issuance configuration and live stats in a single queryable resource.
Why it matters: A2A #1716 (@pshkv, SINT PR#111) landed today, formalising the contract for capability token checks at the AgentSkill boundary. ACP now provides a first-class endpoint so consumer agents can inspect token requirements, SINT field specs, active token counts, and issuer DID without parsing the full AgentCard or trust signals inventory.
Key fields returned:
- enabled / issuer_did — identity state for token issuance
- sint_fields.required / optional — canonical SINT Protocol field lists
- supported_tiers — T0 / T1 / T2 / T3
- token_required_skills — skills list requiring a capability token pre-invocation
- active_tokens / total_issued — live token cache stats
- a2a_ref — https://github.com/google-a2a/A2A/issues/1716
AgentCard: capabilities.capability_token_detail: true +
endpoints.capability_token_detail: "/trust/signals/capability-token"
Tests: CT-01..CT-25 — 25/25 PASS. Full regression 152/152 PASS.
v2.73.0 — Typed JSON Schema for agent_limitations (2026-04-07)¶
ACP v2.73 adds GET /agent-limitations/schema — a JSON Schema (draft/2020-12) endpoint that
makes the agent_limitations constraint dict machine-readable and validatable.
Why it matters: A2A #1694 proposed typed limitations for programmatic constraint discovery.
ACP v2.40 already shipped agent_limitations as a structured dict; v2.73 takes it further
by exposing a formal JSON Schema so consumers can validate values without relying on prose docs.
curl http://localhost:8000/agent-limitations/schema
# → { "ok": true, "schema": { "$schema": "...", "title": "AgentLimitations", "properties": {...} },
# "current_values": { "max_message_size_bytes": 65536, ... } }
Schema properties (6 fields, all optional):
| Field | Type | Description |
|---|---|---|
max_message_size_bytes |
integer | Max message payload size in bytes |
max_recv_queue_size |
integer | Max per-peer receive queue depth |
max_wait_seconds |
integer | Max long-poll wait seconds |
max_peers |
integer | Max concurrent peer connections |
supported_message_roles |
array[string] | Valid role field values |
supported_priorities |
array[enum] | Valid priority field values |
additionalProperties: false— strict schemacurrent_values— returns actual live values for this relay- AgentCard:
capabilities.agent_limitations_schema: true - Tests: AL-01..AL-22 = 22/22 PASS
v2.72.0 — Queryable Bilateral IR Log (2026-04-07)¶
ACP v2.72 adds GET /trust/bilateral-ir/log — a queryable log of bilateral interaction
records inspired by A2A #1718 (@viftode4's proposal for bilateral signed IR as unified trust primitive).
curl "http://localhost:8000/trust/bilateral-ir/log?bilateral=true&limit=10"
# → { "ok": true, "count": N, "bilateral_count": M, "records": [...] }
Filter params: caller_did / skill_id / bilateral / since / limit / offset
bilateral_count is a quick trust-depth indicator: high ratio = strong non-repudiable evidence.
v2.69.0 — Runtime Limitations Endpoint (2026-04-07)¶
ACP v2.69 adds GET /limitations/runtime — a live runtime metrics endpoint that complements
the static limitations[] declared in the AgentCard (v2.29). Aligns with A2A #1694 @citriac
Agent Exchange Hub v0.4.0's stable/runtime limitations split.
Metrics returned¶
{
"ok": true,
"runtime": {
"current_load": 2,
"queue_depth": 1,
"active_tasks": 3,
"total_tasks": 47,
"memory_usage_mb": 52.4,
"memory_source": "psutil",
"peer_count": 2
},
"version": "2.69.0",
"timestamp": 1744034400.0
}
| Field | Description |
|---|---|
current_load |
Active WebSocket peer connections |
queue_depth |
Tasks in submitted state |
active_tasks |
Non-terminal tasks (submitted/working/input_required) |
total_tasks |
All tasks ever created |
memory_usage_mb |
Process RSS (psutil → resource.getrusage fallback) |
peer_count |
Same as current_load |
AgentCard declares capabilities.runtime_limitations = true and endpoints.runtime_limitations = "/limitations/runtime".
v2.68.0 — trust.signals[] v2: 12 Signal Types + GET /trust/signals (2026-04-06)¶
ACP v2.68 extends the trust signal inventory to 12 types and exposes them via a queryable
endpoint, aligning with A2A #1628's trust.signals[] proposal.
New signal types (v2.68)¶
| Type | Description |
|---|---|
bilateral_ir |
Bilateral signed interaction records (v2.59+) |
capability_token |
SINT-format Ed25519 capability tokens (v2.57) |
wtrmrk |
WTRMRK sequence-root attestation as trust factor (v2.62) |
external_token |
Cross-protocol SINT token verification (v2.63) |
GET /trust/signals¶
# All 12 signals
curl http://localhost:8765/trust/signals
# Filter by type
curl "http://localhost:8765/trust/signals?type=bilateral_ir"
# Only enabled signals
curl "http://localhost:8765/trust/signals?enabled=true"
AgentCard declares capabilities.trust_signals_v268 = true and endpoints.trust_signals = "/trust/signals".
v2.67.0 — Direct Message Mode — A2A v1.0.0 SendMessageResponse Alignment (2026-04-06)¶
ACP v2.67 introduces Direct Message mode: a lightweight POST /message/send endpoint that
returns a Message object directly — no Task created, no state machine, no lifecycle management.
This aligns with A2A v1.0.0's SendMessageResponse.oneof { Task task; Message message; } pattern.
When to use Direct Message vs Tasks¶
| Use Case | Endpoint | Returns |
|---|---|---|
| Simple query, ping, calculation | POST /message/send |
Message (immediate) |
| Long-running, stateful work | POST /message:send (existing) |
Task (with lifecycle) |
POST /message/send¶
curl -X POST http://localhost:8765/message/send \
-H "Content-Type: application/json" \
-d '{"role": "user", "text": "What is 2+2?", "context_id": "ctx-001"}'
Response:
{
"ok": true,
"type": "message",
"message_id": "msg-a1b2c3d4",
"role": "user",
"parts": [{"type": "text", "text": "What is 2+2?"}],
"context_id": "ctx-001",
"timestamp": "2026-04-06T14:00:00Z"
}
Parts format (A2A aligned)¶
parts[] follows the A2A Part model with three types:
// Text part
{"type": "text", "text": "hello"}
// File part
{"type": "file", "file": {"name": "doc.txt", "mimeType": "text/plain", "bytes": "<base64>"}}
// Data part
{"type": "data", "data": {"key": "value"}}
Shorthand: "text": "..." is auto-converted to [{"type":"text","text":"..."}].
AgentCard¶
Size limit¶
Bodies >1MB return 413. 70KB and below are accepted normally (limit is MAX_MSG_BYTES = 1MB).
Tests¶
tests/test_direct_message.py — DM-1..14: 16 tests passed
- DM-1..3: happy path (text shorthand, parts[], context_id passthrough)
- DM-4..6: role validation, missing role, invalid role
- DM-7..9: parts validation, empty parts fallback, data part
- DM-10..11: message_id dedup / client-provided message_id
- DM-12: Content-Type guard
- DM-13: file part round-trip
- DM-14: size boundary (70KB=200, 1.1MB=413)
v2.66.0 — Task rejected Terminal State — A2A v1.0.0 Alignment (2026-04-06)¶
ACP v2.66 introduces rejected as a first-class terminal Task state, aligning with the
A2A v1.0.0 specification which distinguishes between a task
that errored (failed) and a task that an agent actively refuses to execute (rejected).
Why rejected ≠ failed¶
| State | Meaning | Triggered by |
|---|---|---|
failed |
Unexpected error, timeout, or system fault | Runtime exception |
rejected |
Agent explicitly declines the task | Agent decision / policy |
canceled |
Requester cancels an in-progress task | Caller (POST :cancel) |
rejected is a terminal state — once set, the task cannot be re-activated.
New: POST /tasks/{id}:agent-reject¶
Agent-initiated rejection for any non-terminal task.
curl -X POST http://localhost:8765/tasks/task-abc:agent-reject \
-H "Content-Type: application/json" \
-d '{"reason": "Skill not available for this input", "reject_code": "skill_unavailable"}'
Response:
{
"ok": true,
"task_id": "task-abc",
"status": "rejected",
"reason": "Skill not available for this input",
"reject_code": "skill_unavailable"
}
- Idempotent: calling on an already-terminal task returns
ok: true+note: "already in terminal state" - Unknown task →
404 - Accepts optional
reason(string) andreject_code(string) in request body
Updated: T3 POST /tasks/{id}:reject¶
The human-confirmation rejection endpoint now transitions confirmation_pending → rejected
(previously → failed). This better represents the semantics: a human reviewer actively
declined the task, not that it errored.
Updated: GET /tasks?status=rejected¶
The task list filter now accepts status=rejected.
AgentCard Updates¶
{
"capabilities": {
"rejected_state": true
},
"endpoints": {
"agent_reject": "/tasks/{id}:agent-reject"
}
}
Tests: RJ-1..10 — 9 passed, 1 skipped (T3 human-confirm scenario requires T3 skill)
v2.65.0 — POST /ir/import-evidence — APS-Compatible Reputation Update (2026-04-06)¶
ACP v2.65 closes the bilateral IR → reputation loop by providing a standardized
endpoint for importing external interaction records and generating APS-compatible
reputation_update payloads. Aligns with A2A Issue #1718 (importBilateralEvidence()).
New: POST /ir/import-evidence
- Accepts an external bilateral IR record from a peer relay
- Verifies relay_signature and caller_signature (Ed25519) independently
- Returns verify block: relay_sig_valid, caller_sig_valid, bilateral_verified, errors
- Returns APS-compatible reputation_update payload with trust_delta:
- +1 — bilateral verified (both signatures valid)
- 0 — relay-only verified (no caller signature)
- -1 — tampered or no signatures
New: GET /ir/imported-evidence
- Lists all records previously imported via POST /ir/import-evidence
- Supports ?agent_did= filter and ?limit= pagination
New helpers (internal)
- _verify_ir_signatures(ir) — dual Ed25519 verification with error collection
- _build_reputation_update(ir, verify_result) — APS reputation_update builder
with freshness_hint (seconds since interaction), aps_schema: "v1"
AgentCard: capabilities.import_evidence + endpoints.import_evidence: "/ir/import-evidence"
Tests: IE-1..20 (20 new tests) — all passing
Bug fix (BUG-052): test_t3c3 port contention fixed — _kill_port() pre-clean +
websockets.serve(reuse_address=True) + wait_http_ready timeout 20s
v2.64.0 — Bilateral IR Test Vectors + Governance live_endpoint (2026-04-06)¶
ACP v2.64 delivers two interoperability features driven by A2A community discussion:
a deterministic test vector suite for bilateral Interaction Record verification (A2A #1718, @aeoess),
and explicit live_endpoint alignment with the APS serviceEndpoint governance pattern (A2A #1717).
GET /ir/test-vectors — Cross-Implementation IR Verification¶
Requested by @aeoess (A2A Issue #1718): a canonical, deterministic set of test vectors that any ACP-compatible implementation can use to verify bilateral IR signature logic without running a live relay.
Returns 4 test vectors with SHA-256 seeded Ed25519 keys (fully reproducible):
| ID | Type | Scenario |
|---|---|---|
tv-ir-001 |
bilateral | Both relay + caller signatures valid |
tv-ir-002 |
unilateral | Relay-only signature (caller not enrolled) |
tv-ir-003 |
negative | Tampered payload → caller_signature_valid: false |
tv-ir-004 |
did:key | W3C did:key format in canonical payload, bilateral valid |
Chain integrity: tv-ir-002.previous_hash = sha256(tv-ir-001.canonical_payload) — same hash-chain
algorithm as live interaction records.
Determinism guarantee: Same seed bytes → same Ed25519 keys → same signatures on every call.
The canonical_bytes_hex field decodes exactly to json.dumps(canonical_payload, sort_keys=True).
# Fetch test vectors
curl http://localhost:7901/ir/test-vectors | jq .
# Verify a signature (Python)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
import base64, json, requests
data = requests.get("http://localhost:7901/ir/test-vectors").json()
v = next(v for v in data["vectors"] if v["id"] == "tv-ir-001")
pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(data["keys"]["relay"]["public_key_b64"] + "=="))
pub.verify(base64.b64decode(v["relay_signature"] + "=="), bytes.fromhex(v["canonical_bytes_hex"]))
print("✅ relay signature valid")
Capability gate: Requires --identity (Ed25519 key loaded). Returns 503 otherwise.
governance_metadata.live_endpoint — APS serviceEndpoint Alignment¶
Inspired by A2A #1717 (passportToAgentCard() APS live governance endpoint pattern):
GET /governance-metadata now includes a live_endpoint field pointing back to itself.
This matches the APS pattern where an AgentCard.serviceEndpoint URL allows the receiver to
query a live trust profile rather than relying on a static snapshot embedded in a token.
New AgentCard fields¶
{
"capabilities": {
"ir_test_vectors": true
},
"endpoints": {
"ir_test_vectors": "/ir/test-vectors"
}
}
v2.63.0 — Cross-Protocol Token Verification: GET /identity/did-key + POST /verify/external-token (2026-04-06)¶
ACP v2.63 closes the cross-protocol interoperability gap identified in A2A Issue #1713 (@pshkv, @viftode4). Any agent holding a SINT-format capability token — issued by APS v1.32.0 or SINT — can now present it to an ACP relay for cryptographic verification, without any code changes on either side.
The problem: ACP relays could issue their own capability tokens (v2.57), but had no way to verify tokens
from other systems (APS, SINT, future protocols). The did:key derivation algorithm was compatible, but the
verification endpoint didn't exist.
The solution: Two new endpoints complete the interoperability story.
GET /identity/did-key¶
Returns this relay's W3C did:key identifier and full public key material:
{
"ok": true,
"did_key": "did:key:z6MkpK7WQSpmbJUMxqa3EP2CeA7DTrD12H1Xhuwo5VaCPP9m",
"did_acp": "did:acp:kn6RtnQsQuntheAxlH1tCUV806wLfIC1ISEZ05M3btQ",
"public_key_b64": "kn6RtnQsQuntheAxlH1tCUV806wLfIC1ISEZ05M3btQ",
"public_key_hex": "92...7e",
"algorithm": "Ed25519",
"multicodec": "0xed01"
}
The did_key field uses multicodec [0xed, 0x01] + base58btc encoding — byte-for-byte identical
to APS v1.32.0 toDIDKey() and SINT keyToDid(). No adaptation layer required.
POST /verify/external-token¶
Accepts a SINT-format capability token and performs 7-step cryptographic verification:
curl -s http://localhost:18363/verify/external-token \
-H "Content-Type: application/json" \
-d '{
"token": {
"subject": "<64-hex-char-Ed25519-pubkey>",
"resource": "acp://relay.example/skills/invoke",
"actions": ["invoke"],
"tier": "T2_act",
"exp": 1775000000,
"signature":"<Ed25519-hex-sig>"
}
}'
Response (valid token):
{
"ok": true,
"valid": true,
"expired": false,
"subject_did": "did:key:z6Mkq3dfNXFN...",
"relay_did_key": "did:key:z6MkpK7WQ...",
"fields_verified": [
"required_fields",
"expiry_not_expired",
"subject_pubkey_decoded",
"did_key_derived",
"canonical_payload_built",
"signature_valid"
]
}
The 7-Step Verification Pipeline¶
| Step | Check | Failure Mode |
|---|---|---|
| 1 | Required fields present | ok=false, error="missing required fields" |
| 2 | Expiry (exp) check |
ok=false, expired=true |
| 3 | Subject pubkey decode (64 hex → 32 bytes) | ok=false, error="subject must be 64 hex chars" |
| 4 | did:key derivation (multicodec 0xed01 + base58btc) | — |
| 5 | Canonical payload: subject\|resource\|actions_csv\|tier\|exp_or_0 |
— |
| 6 | Ed25519 signature verification | ok=false, valid=false |
| 7 | Optional MoltTrust registry query (if configured) | — |
Cross-Protocol Compatibility¶
ACP's did:key derivation is validated against the published cross-verify benchmark from A2A #1713:
9/9 test vectors pass with zero code changes between APS v1.32.0, SINT, and ACP.
This means:
- A token issued by an APS relay can be verified by an ACP relay
- A token issued by an ACP relay carries a subject_did recognizable to SINT implementations
- The relay_did_key in every response lets the receiving party verify which relay attested the token
AgentCard¶
{
"capabilities": {
"external_token_verify": true
},
"endpoints": {
"did_key": "/identity/did-key",
"external_token_verify": "/verify/external-token"
}
}
external_token_verify is true only when the relay has an Ed25519 identity loaded (--identity).
v2.62.0 — wtrmrk_sequence_root: Factor 4 Attestation History in effective_tier (2026-04-06)¶
ACP v2.62 adds the fourth factor to the effective_tier formula: external attestation history
from the WTRMRK registry. Inspired by A2A Issue #1716 (@64R3N, @MoltyCel, @aeoess), who identified
that delegation_depth is a proxy variable — the real signal is whether the agent has a verifiable
on-chain attestation history.
Background: Why a Fourth Factor?¶
The v2.58 three-factor formula was:
reputation_adj is computed from local relay knowledge — messages seen, card verification, trust signals.
This is useful for known peers, but is blind to external reputation for first-time connections.
wtrmrk_sequence_root provides an external Merkle commitment anchored to a public registry, allowing
the relay to query an attestation history that is independent of local peer memory.
WTRMRK Grade → attestation_history_adjustment¶
| Grade | Meaning | wtrmrk_adj |
|---|---|---|
None |
Query failed (network error, unknown root) | 0 — fail-closed, neutral |
0 |
No on-chain record — completely unknown | +1 — raise floor |
1 |
Basic activity, low history depth | 0 — neutral |
2 |
Established agent, verified identity anchor | 0 — neutral |
3 |
High-reputation, hardware-attested, long track record | -1 — may lower floor |
Asymmetric Safety Rule¶
combined_adj = clamp(-1, +1, reputation_adj + wtrmrk_adj)
if reputation_adj == +1 or wtrmrk_adj == +1:
combined_adj = max(0, combined_adj) # either hostile signal → cannot lower floor
This means:
- -1 requires agreement: both reputation_adj=-1 AND wtrmrk_adj=-1 to lower the floor
- +1 wins alone: a single hostile signal (unknown peer OR unknown on-chain) raises the floor
- Defense in depth: two independent channels must both certify trust before floor is relaxed
Usage¶
# POST /tasks with wtrmrk_sequence_root in metadata
curl -X POST http://127.0.0.1:<http_port>/tasks \
-H 'Content-Type: application/json' \
-d '{
"skill_id": "transfer_funds",
"role": "agent",
"payload": {"amount": 100, "to": "0xabc..."},
"metadata": {
"wtrmrk_sequence_root": "<base64url_merkle_commitment>"
}
}'
# Inspect effective_tier factors with wtrmrk
curl "http://127.0.0.1:<http_port>/skills/transfer_funds/effective-tier?wtrmrk_sequence_root=<root>"
# Returns: factors.wtrmrk_queried=true, factors.wtrmrk_grade=2, factors.wtrmrk_adj=0, factors.combined_adj=0
Full Four-Factor Response Example¶
{
"skill_id": "transfer_funds",
"effective_tier": "T2",
"factors": {
"tier_rule": "T2",
"delegation_depth": 0,
"depth_floor": null,
"reputation_adj": 0,
"wtrmrk_sequence_root": "abc123xyz",
"wtrmrk_queried": true,
"wtrmrk_grade": 2,
"wtrmrk_adj": 0,
"combined_adj": 0,
"effective_tier": "T2"
}
}
AgentCard Capability¶
Peers can check wtrmrk_attestation to know whether WTRMRK-based Factor 4 is active
before including metadata.wtrmrk_sequence_root in task requests.
v2.61.0 — Complete Bilateral Signing: caller_signature in Interaction Records (2026-04-06)¶
ACP v2.61 closes the unilateral-attestation gap in interaction records. Inspired by A2A Issue #1718 (0 comments when ACP shipped); external community validation confirmed that relay-only signing is repudiable — the relay can forge or selectively reveal records.
The Problem¶
In v2.59, interaction records contained only a relay_signature — a signature by the relay itself.
This is useful, but unilateral: the relay signs what it wants. A dishonest relay could fabricate
records, change the task_id, or omit records entirely — and the caller has no cryptographic voice.
The Solution: caller_signature¶
{
"id": "ir_abc123",
"type": "interaction_record",
"relay_did": "did:acp:AbcXyz...",
"caller_did": "did:acp:DefGhi...",
"task_id": "task_abc",
"skill_id": "summarize",
"sequence_a": 7,
"previous_hash": "sha256:deadbeef...",
"timestamp": "2026-04-06T02:55:00Z",
"relay_signature": "base64url...",
"caller_token_hash": "sha256:...",
"caller_signature": "base64url...",
"caller_public_key": "base64url...",
"caller_signature_valid": true,
"bilateral": true
}
Canonical Payload (what the caller signs)¶
The caller creates an Ed25519 signature over this string, encoding result as base64url.
The relay verifies it using caller_public_key (raw Ed25519 bytes, base64url).
Usage¶
# Generate a keypair (one-time)
python3 -c "
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, PrivateFormat, NoEncryption
import base64
priv = Ed25519PrivateKey.generate()
pub = priv.public_key()
pub_b64 = base64.urlsafe_b64encode(pub.public_bytes(Encoding.Raw, PublicFormat.Raw)).rstrip(b'=').decode()
priv_b64 = base64.urlsafe_b64encode(priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())).rstrip(b'=').decode()
print('PUBLIC:', pub_b64)
print('PRIVATE:', priv_b64)
"
# POST /tasks with caller_signature
# (caller computes: relay_did|caller_did|task_id|sequence_a|timestamp, then signs)
curl -X POST http://127.0.0.1:<http_port>/tasks \
-H 'Content-Type: application/json' \
-d '{
"skill_id": "summarize",
"role": "agent",
"payload": {"text": "analyze this"},
"record": true,
"caller_signature": "<base64url_ed25519_sig>",
"caller_public_key": "<base64url_public_key>"
}'
bilateral Semantics¶
| relay_signature | caller_signature_valid | bilateral |
|---|---|---|
present (relay has --identity) |
true |
true ✅ |
| present | false (bad sig) |
false |
| present | null (no sig given) |
false |
absent (no --identity) |
true |
false |
AgentCard Capability¶
Peers can check bilateral_interaction_records to know whether their caller_signature
will be verified before relying on non-repudiation semantics.
v2.60.0 — Governance Metadata in AgentCard (2026-04-06)¶
ACP v2.60 adds governance metadata to the AgentCard — a structured block declaring an agent's trust posture, capability manifest, policy compliance, and audit trail reference. Inspired by A2A Issue #1717 (Microsoft agent-governance-toolkit, 0 comments); ACP ships the working implementation before A2A spec discussion concludes.
Governance Metadata Block¶
{
"governance_metadata": {
"schema_version": "1.0",
"generated_at": "2026-04-06T09:28:00Z",
"trust_score": 0.78,
"capability_manifest": {
"transfer-funds": { "tier": "T3", "status": "available", "deprecated": false },
"summarize": { "tier": "T1", "status": "available", "deprecated": false }
},
"policy_compliance": [
{ "policy": "acp-security-v1", "status": "compliant" },
{ "policy": "gdpr", "status": "compliant" }
],
"audit_trail_reference": "/interaction-records",
"interaction_record_count": 42,
"peer_count": 3,
"task_count": 157
}
}
How the trust_score is computed¶
When no explicit override is set, the relay computes a heuristic trust score based on its own runtime activity:
trust_score = 0.3
+ peer_count × 0.04
+ interaction_record_count × 0.005
+ task_count × 0.002
(clipped to [0.0, 1.0])
This reflects "earned" trust: a relay that has connected to many peers, completed many tasks, and generated many signed interaction records is treated as more trustworthy than a new relay with no history.
Capability Manifest¶
capability_manifest is auto-derived from the AgentCard's skills[] list:
"capability_manifest": {
"<skill_id>": {
"tier": "T0" | "T1" | "T2" | "T3",
"status": "available" | ...,
"deprecated": false
}
}
When --governance-metadata provides an explicit manifest, that overrides the auto-derived one.
New CLI: --governance-metadata¶
python3 acp_relay.py --port 7700 \
--governance-metadata '{
"trust_score": 0.9,
"policy_compliance": [
{"policy": "acp-security-v1", "status": "compliant"}
],
"audit_trail_reference": "https://audit.example.com/relay-1"
}'
Also accepts a path to a JSON file:
New Endpoints¶
| Method | Path | Description |
|---|---|---|
GET |
/governance-metadata |
Live governance metadata block (always fresh) |
PATCH |
/governance-metadata |
Update writable fields at runtime |
PATCH writable fields: trust_score (0.0–1.0), policy_compliance (array), audit_trail_reference (string), capability_manifest (object), schema_version (string).
Read-only (auto-computed, silently ignored on PATCH): generated_at, peer_count, task_count, interaction_record_count.
# Override trust_score at runtime
curl -X PATCH http://localhost:7800/governance-metadata \
-H 'Content-Type: application/json' \
-d '{"trust_score": 0.95}'
# → {"ok": true, "updated": ["trust_score"], "governance_metadata": {...}}
AgentCard Changes¶
"capabilities": {
...
"governance_metadata": true
},
"endpoints": {
...
"governance_metadata": "/governance-metadata"
}
capabilities.governance_metadata is false when --governance-metadata is not configured.
v2.59.0 — Bilateral Interaction Records: Signed Audit Trail per Task (2026-04-06)¶
ACP v2.59 introduces bilateral interaction records — a lightweight, relay-signed audit primitive that creates a tamper-evident chain of task invocations. Inspired by A2A Issue #1718 (proposed 2026-04-05, 0 comments); ACP ships the working implementation ahead of spec finalization.
How It Works¶
When a client submits POST /tasks with record: true, the relay generates an interaction_record:
{
"id": "ir-a3f7bc12",
"type": "interaction",
"relay_did": "did:acp:z6Mk...",
"caller_did": "did:acp:z6Mk...caller",
"task_id": "task-xyz",
"skill_id": "transfer-funds",
"sequence_a": 42,
"previous_hash": "sha256:e3b0c44298fc1c149...",
"timestamp": "2026-04-06T06:18:00Z",
"quality_hint": null,
"caller_token_hash": "sha256:abc123...",
"relay_signature": "base64url...",
"relay_public_key": "base64url..."
}
Chain Continuity¶
Each record's previous_hash is the sha256 of the prior record's canonical JSON, forming
an append-only audit chain. The first record has previous_hash: "genesis".
sequence_a is a monotonic counter — any gap signals tampering or missing records.
Caller Token Hash¶
If a capability_token (SINT format, v2.57) was provided in the task request, its jti
is hashed (sha256(jti)) and recorded in caller_token_hash — linking the token issuance
event to the invocation event without exposing the token itself.
New Endpoints¶
POST /tasks # Add "record": true to request body
GET /interaction-records # List all interaction records
GET /interaction-records?skill_id=X # Filter by skill
GET /interaction-records?peer_id=Y # Filter by caller DID substring
GET /interaction-records?limit=N # Limit results (default: 100)
AgentCard Capability¶
Design Philosophy¶
ACP's implementation is intentionally relay-anchored (only relay signs) rather than
bilateral (both sides sign). This means:
- No caller-side signing infrastructure required
- Works with any ACP client (including curl-only agents)
- Full non-repudiation for the relay side; caller identity anchored via DID + optional token hash
- Future v2.x can add optional caller_signature for full bilateral signing
v2.58.0 — effective_tier: Three-Factor Dynamic Authorization (2026-04-06)¶
ACP v2.58 implements dynamic effective tier computation — inspired by A2A Issue #1716 comment (@64R3N), shipped before the A2A spec reached consensus.
The Three-Factor Formula¶
| Factor | Source | Range |
|---|---|---|
tier_rule |
Skill's declared authorization_tier |
T0..T3 |
depth_floor |
min(len(principal_chain), 3) |
T0..T3 |
reputation_adj |
Peer trust history (-1/0/+1) | ±1 step |
Key design decisions:
- rep_adj is only applied when base_int ≥ T2 — T0/T1 skills remain auto-execute regardless of caller reputation
- T3 is always T3 (immune to any downgrade)
- Unknown peers get rep_adj = +1 (conservative); known+verified+active peers get -1 (fast-track)
New Endpoint¶
Returns full factor breakdown for transparency and debugging:
{
"skill_id": "my-skill",
"effective_tier": "T2",
"factors": {
"tier_rule": "T1",
"delegation_depth": 2,
"depth_floor": "T2",
"reputation_adj": 0,
"effective_tier": "T2"
}
}
Bug Fix¶
DELETE /principal-chain/<did>: DIDs containing colons (e.g.did:example:xxx) were being URL-encoded as%3Ain the path, causing 404 mismatches. Now correctly URL-decoded.
v2.57.0 — SINT-format Capability Tokens: Ed25519 signed skill authorization (2026-04-06)¶
ACP v2.57 introduces capability tokens — cryptographically signed, portable credentials that authorize an agent to invoke a specific skill. The design is fully compatible with the SINT Protocol proposed in A2A Issue #1716 (0 replies as of 2026-04-05). ACP ships the reference implementation first.
Core insight: Instead of trusting who's calling based on connection trust score (v2.49), you now issue a signed token that says: "this specific agent may invoke this specific skill at this tier, subject to these constraints, until this expiry." The relay verifies the Ed25519 signature inline — no external Authority Server, no OAuth, no key lookup.
Quick Start: Issue and Use a Capability Token¶
# Start relay with Ed25519 identity (required for issuance)
python3 acp_relay.py --port 7801 --name FinanceAgent \
--identity ~/.acp/identity.json \
--skills '[{"id":"transfer_funds","name":"Transfer","authorization_tier":"T3","capability_token_required":true}]'
# Issue a capability token for subject agent
curl -s -X POST http://localhost:7901/skills/transfer_funds/capability-token \
-H "Content-Type: application/json" \
-d '{"subject":"did:acp:CallerAgent","tier":"T3","ttl":300}'
{
"ok": true,
"token": {
"jti": "a3f8d2e1b4c9...",
"iss": "did:acp:FinanceAgentDID",
"sub": "did:acp:CallerAgent",
"resource": "acp://FinanceAgent/skills/transfer_funds",
"actions": ["invoke"],
"tier": "T3",
"constraints": {},
"iat": 1743954000,
"exp": 1743954300,
"signature": "a7b3c2...",
"scheme": "sint_ed25519",
"public_key": "3d8f4a..."
}
}
# Use the token to invoke the skill
curl -s -X POST http://localhost:7901/tasks \
-H "Content-Type: application/json" \
-d '{
"role": "agent",
"parts": [{"kind": "text", "text": "transfer 100 USD to account #7823"}],
"skill_id": "transfer_funds",
"capability_token": { ... token from above ... }
}'
Enforcement Model¶
POST /tasks
│
├─ skill.capability_token_required=True? ──(no token)──► 403 ERR_CAPABILITY_TOKEN_REQUIRED
│
├─ capability_token provided? ─────────────────────────► validate(sig + exp + skill)
│ └── invalid ──────────────────────────────────► 403 ERR_CAPABILITY_TOKEN_INVALID
│ └── valid ──────────────────────────────────── skip authorization_tier gate ✓
│
├─ no token: authorization_tier check (v2.49 trust_score path)
├─ param_constraints check (v2.50)
├─ rate_limit check (v2.53)
└─ human_confirmation gate (v2.51, T3 only)
Key design choice: A valid capability token bypasses the trust_score-based
authorization_tier check. The token is the credential. This enables cross-org invocation
without requiring the caller to have an established trust relationship.
List Issued Tokens¶
# All tokens
curl -s http://localhost:7901/capability-tokens
# Active (non-expired) tokens for a specific skill
curl -s "http://localhost:7901/capability-tokens?skill_id=transfer_funds&active=1"
AgentCard Capabilities¶
{
"capabilities": {
"capability_token_issuance": true
},
"endpoints": {
"capability_token_issuance": "/skills/{skill_id}/capability-token"
}
}
capability_token_issuance is true only when --identity is loaded (Ed25519 keypair required).
SINT Protocol Compatibility¶
ACP capability tokens use the standard SINT fields:
| Field | SINT standard | ACP v2.57 |
|---|---|---|
jti |
token id | ✅ random 16-byte hex |
iss |
issuer DID | ✅ _did_acp or _did_key |
sub |
subject DID | ✅ caller's DID |
resource |
capability resource | ✅ acp://{name}/skills/{id} |
actions |
permitted operations | ✅ default ["invoke"] |
tier |
authorization tier | ✅ T0/T1/T2/T3 |
constraints |
parameter bounds | ✅ dict, composable with v2.50 |
iat / exp |
validity window | ✅ unix timestamps |
signature |
Ed25519 sig | ✅ over canonical JSON |
scheme |
signature scheme | ✅ "sint_ed25519" |
v2.56.0 — principal_chain[] OBO Delegation (2026-04-05)¶
TL;DR: On-behalf-of delegation via a DID-identified principal chain embedded in AgentCard trust block and messages. No shared AS. Answers A2A Issue #1713.
python3 acp_relay.py --port 7801 --name WorkerAgent \
--principal did:acp:OrchestratorDID,role=orchestrator
v2.55.0 — Per-Peer AgentCard Re-verification (2026-04-05)¶
GET /peers/{peer_id}/verify-card — on-demand re-verification of a connected peer's AgentCard
with optional cache bypass (force=1), trust integration (trust=1), and custom TTL.
v2.54.0 — Batch Verify-Card + TTL Cache (2026-04-05)¶
POST /verify-card now supports three modes: single card, batch (cards: [...]),
and fetch-and-verify (url: "..."). TTL cache (300s default), ttl=0 force-fresh.
v2.53.0 — Per-Skill Rate Limiting (2026-04-05)¶
skill.rate_limit: {max_calls, window_seconds, scope} — sliding window rate limiting
per peer or globally per skill. 429 ERR_RATE_LIMIT on exceeded.
v2.52.0 — Skill Deprecation Notices (2026-04-05)¶
skill.deprecation_notice: {message, sunset_date, replacement_skill_id, severity} —
structured skill sunset metadata. Surfaced in GET /skills responses.
v2.51.0 — T3 Human Confirmation Gate (2026-04-05)¶
skill.human_confirmation_required: true — T3 skills can require human sign-off before
execution. POST /tasks/{id}:confirm / :reject two-phase protocol.