Skip to content

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

curl http://localhost:18900/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

curl http://localhost:18900/protocol-binding
{
  "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 schema
  • current_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

curl http://localhost:8765/limitations/runtime
{
  "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

{
  "capabilities": { "direct_message": true },
  "endpoints": { "message_send": "/message/send" }
}

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) and reject_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.

{
  "governance_metadata": {
    "live_endpoint": "/governance-metadata",
    "trust_score": 0.85,
    ...
  }
}

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:

effective_tier = max(tier_rule, delegation_depth_floor, base + reputation_adj)

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

{
  "capabilities": {
    "effective_tier_computation": true,
    "wtrmrk_attestation": true
  }
}

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)

<relay_did>|<caller_did>|<task_id>|<sequence_a>|<timestamp>

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

{
  "capabilities": {
    "interaction_records": true,
    "bilateral_interaction_records": true
  }
}

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:

python3 acp_relay.py --governance-metadata /etc/acp/governance.json

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

{ "capabilities": { "interaction_records": true } }

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

effective_tier = max(tier_rule, depth_floor(principal_chain.len), reputation_adj)
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

GET /skills/{skill_id}/effective-tier?peer_id=<did>

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 %3A in 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)

See v2.56 entry in CHANGELOG.

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.