CHANGELOG¶
All notable changes to ACP (Agent Communication Protocol) are documented here.
Format: Semantic Versioning — MAJOR.MINOR.PATCH-status
Dates: Asia/Shanghai (UTC+8)
v3.17.2 — Public Launch Polish (2026-06-12)¶
Added¶
- CodeQL code scanning for Python, JavaScript/TypeScript, and Go.
- OpenSSF Scorecard workflow with SARIF upload and published scorecard results.
SUPPORT.md,CITATION.cff, and expanded English contributor guidance for public collaboration.
Changed¶
- README badges now show live release, CI, CodeQL, docs, Docker, license, and OpenSSF Scorecard status.
- Development extras now include MkDocs dependencies so
make docsworks afterpip install -e ".[dev]". - Root and Node SDK license files now use the full Apache License 2.0 text, enabling GitHub SPDX detection.
- Security policy links now point directly to private vulnerability reporting and the published security model.
Fixed¶
- Runtime UTC timestamp generation now uses timezone-aware
datetime.now(timezone.utc). - Identity documentation examples now avoid deprecated
datetime.utcnow().
v3.17.1 — Public Release Readiness (2026-06-12)¶
Added¶
- GitHub Actions CI for Python, Node, Go, Rust, and MkDocs.
- Dependabot configuration, issue templates, pull request template, security policy, code of conduct, editor config, and a top-level Makefile.
Changed¶
- Python package metadata now matches the current relay version and builds clean sdist/wheel artifacts.
- CI now runs a stable release smoke suite covering certification, integration, reliable messaging, hybrid identity, and Python SDK tests.
Fixed¶
- Relay HTTP startup no longer performs reverse-DNS lookup during bind, avoiding local startup hangs.
- Docker images now keep the HTTP API bound to
0.0.0.0even when custom relay flags replaceCMD. - Push webhook delivery now executes from the SSE broadcast path instead of unreachable code.
- Reliable messaging tests use the active Python interpreter and explicit HTTP ports for deterministic CI runs.
- MkDocs strict builds now pass after fixing stale relative links.
v2.95.0 — Skill-Scoped Trust Scores (2026-04-10)¶
Added¶
_compute_skill_trust_scores()— computes per-skill trust scores from bilateral IR evidence- Algorithm:
base=0.3 + min(unique_callers,10)*0.04 + min(bilateral_count,50)*0.005; clamped to [0.0, 1.0] - Returns
{}when no bilateral IR records exist (no evidence → no score) - A2A #1717 community convergence: skill-scoped trust for granular authorization
- aeoess APS v1.37.0
importBilateralEvidence()per-skill accumulation pattern aligned GET /trust/skill-scores— new endpoint returning per-skill trust scores dicttrust_scores: {"<skill_id>": float, ...},method: "skill_scoped_v1",algorithmblock- Returns
{}trust_scores when no IR evidence governance_metadata.trust_scores— dict (skill_id → float) embedded in GM blocktrust_score_method: "skill_scoped_v1"declared alongside- Backward compat: global
trust_scoreretained; updated to per-skill average when IR evidence present - QuerySkill
skill_trust_scorefield —POST /skills/queryresponse includes per-skill score nullwhen no bilateral IR evidence for that skill_idfloat [0.0, 1.0]when IR evidence existscapabilities.skill_scoped_trust_scores: truein AgentCardendpoints.skill_trust_scores: /trust/skill-scoresin AgentCard- Tests SS01–SS16 (
tests/test_skill_scoped_trust_v295.py) — 16/16 PASS - SS01–SS04: VERSION + capability/endpoint flags + backward compat
- SS05–SS07: empty no-IR response, schema, algorithm fields
- SS08–SS11: score computation (single skill, multi-skill, clamping, skill_count)
- SS12–SS14: QuerySkill skill_trust_score field (null/populated)
- SS15–SS16: governance_metadata integration + global trust_score backward compat
Changed¶
- VERSION: 2.94.0 → 2.95.0
test_principal_diversity_v294.py: PD01/PD15 version assertions relaxed to>= 2.94.0(version-agnostic)_build_governance_metadata(): always populatestrust_scoresandtrust_score_method
Notes¶
- Backward compatibility preserved: global
trust_scorescalar still present in GM block - Empty dict semantics:
{}trust_scores = "no bilateral IR evidence yet" (not a 404) - No breaking changes: all existing /trust/ endpoints unchanged
v2.94.0 — Principal Diversity Defense for Bilateral IR (2026-04-10)¶
Added¶
_principal_diversity_score(peer_id)— new function implementing the colluding-pair inflation defense from aeoess adversarial-trust-fixture.json (A2A #1718 gist:bdcd1dd0512661138ff7a71bf1e946c7)- Parameters aligned with aeoess model:
concentration_threshold=0.60,penalty_weight=0.10,min_records_for_analysis=3 - Returns:
concentration_ratio,penalty_applied,diversity_weight,effective_bilateral_count,top_counterparty,unique_counterparties - Formula:
effective = normal_count + excess_count * 0.10whentop_counterparty_ratio > 0.60 GET /trust/bilateral-ir/diversity— new endpoint exposing principal diversity analysis per peer?peer_id=<did>— required query param- 400 on missing peer_id, 404 on unknown peer
- Response includes full
defense_paramsblock with reference to aeoess gist _bilateral_ir_adj()enhanced — now returns 4-tuple(adj, count, merkle_root, diversity_dict);effective_bilateral_count(penalty-adjusted) used in threshold calculation instead of raw countcapabilities.principal_diversity_defense: truein AgentCardendpoints.bilateral_ir_diversity: /trust/bilateral-ir/diversityin AgentCardprincipal_diversitysub-field in/trust/signals/capability-tokenresponse factorsPOST /trust/bilateral-ir/inject— test helper endpoint for seeding bilateral IR records in tests- Tests PD01–PD16 (
tests/test_principal_diversity_v294.py) — 16/16 PASS - PD01–PD04: VERSION + capability/endpoint flags
- PD05–PD07: 400/404 error handling
- PD08–PD10: defense params values and response schema
- PD11–PD14: penalty logic (no-penalty below threshold, penalty above, effective count formula, insufficient records)
- PD15–PD16: version in response, backward compat
Changed¶
_bilateral_ir_adj()return type: 3-tuple → 4-tuple (backward compat: callers updated)- VERSION: 2.93.0 → 2.94.0
Security¶
- Colluding-pair inflation attack (AF-002 adversarial scenario) now has a concrete defense
- Peer concentrating >60% of bilateral interactions with a single counterparty receives
diversity_weight < 1.0 effective_bilateral_countreplaces raw count in tier threshold calculation — prevents artificial T3 inflation from exclusively mutual interactions
v2.93.0 — ACP-RFC-004: Decentralized Agent Identity Without CA (2026-04-09)¶
Added¶
- ACP-RFC-004:
docs/rfc/identity-without-ca.md— Full specification for self-sovereign Ed25519 agent identity - Problem analysis: 6 failure modes of the central CA approach (A2A #1672)
- Three-layer identity model: who (Ed25519) → what they can do (capability_token) → what happened (bilateral IR)
- Self-signed AgentCard:
card_signature= Ed25519 over canonical card JSON POST /identity/verify-cardcross-instance verification (no CA, no network lookup, no prior relationship)- Multi-provider DID support:
did:key,did:web,did:acp— independently verifiable - Integration with
credential_lifecycle(RFC-003) as CRL/OCSP replacement - Security analysis: 5 threats + mitigations (spoofing/key compromise/replay/MITM/Sybil)
- Python reference implementation: ~50 lines for full key generation + sign + verify
- Comparison table: 9 dimensions vs A2A #1672 CA approach
- Relation to IETF RFC 8032/8785, W3C DID Core, draft-prakash-aip
- A2A community engagement:
docs/community/a2a-1712-comment.md— Ready-to-post comment for A2A #1712 - Addresses WTRMRK proposal by 64R3N
- Responds to aeoess's three-layer framework (who/what/what happened)
- Concrete ACP implementation references for all three layers
- CA vs self-signed comparison table
Changed¶
- VERSION: 2.92.0 → 2.93.0
ACP vs A2A Competitive Position Update¶
- ACP Ed25519 self-signed identity (v2.85, default-on) vs A2A #1672 central CA (still proposed, no implementation) — 3.5 month lead
- ACP three-layer model (identity + capability_token + bilateral IR) — complete, no A2A equivalent
- RFC-004 published; A2A discussion in #1672 (414+ comments) and #1712 still open
v2.85.0 — Ed25519 Identity Default-On + GET /protocol-binding/compatibility (2026-04-08)¶
Changed (Breaking-safe)¶
- Ed25519 identity is now auto-generated by default — no
--identityflag required - On first run,
~/.acp/identity.jsonis created automatically (persisted across restarts) --no-identitynew flag: disables auto-generation for embedded/testing scenarios--identity <path>still works for custom keypair path (backward compatible)capabilities.identity_default=Truein AgentCard when identity loaded
Added¶
GET /protocol-binding/compatibility— multi-protocol compatibility matrix- 6 entries:
websocket(native),http/sse(native),a2a(partial),anp(partial),mcp(none),grpc(none) aligned_sections[]for partial entries (e.g. A2A §2/§3/§5.8)acp_bindingURI +versionfields- POST returns 405
endpoints.protocol_binding_compatibilityin AgentCard- Aligns with A2A #1723 (SLIMRPC protocol declaration discussion)
- Tests: ID-01..ID-10 (identity default) + PBC-01..PBC-10 (compat matrix) = 20/20 PASS
v2.84.0 — protocol_bindings[] AgentCard Array + client_msg_id Idempotency (2026-04-08)¶
Added¶
protocol_bindings[]— A2A §5.8 aligned plural array on AgentCard top-level; backward-compatible singularprotocol_bindingretained;capabilities.protocol_bindings_array=Trueclient_msg_id— accepted as alias formessage_idon/message:sendand/peer/{id}/send; echoed in all send responses; dedup cache covers both forms (ANP §3.2 borrow)tests/test_protocol_binding_v279.pyextended;tests/test_client_msg_id_v284.pyadded (CM1–CM4)
Fixed (BUG-056)¶
/peer/{id}/sendwas silently ignoringclient_msg_idfield — not echoed, not deduped. Fixed to match/message:sendbehaviour.
v2.82.0 — evidence_stream: SSE Task Lifecycle Subscription (2026-04-08)¶
Added¶
GET /tasks/{id}/evidence-stream— SSE subscription to task lifecycle events (evidence anchoring, status transitions, artifact appends)- Events:
evidence.anchored,task.status,task.artifact capabilities.evidence_stream=True;endpoints.evidence_streamin AgentCard
v2.81.0 — task_evidence: Lifecycle Evidence Anchoring (2026-04-08)¶
Added¶
POST /tasks/{id}/evidence— anchor arbitrary evidence payloads to a taskGET /tasks/{id}/evidence— retrieve all anchored evidence records- Evidence fields:
evidence_type,payload,anchored_at,anchor_id capabilities.task_evidence=True
v2.80.0 — heartbeat_period_ms: AgentCard Heartbeat Interval Declaration (2026-04-08)¶
Added¶
heartbeat_period_msfield on AgentCard top-level — declares relay's preferred heartbeat polling interval (default 30 000 ms)capabilities.heartbeat_period_ms=True
v2.79.0 — GET /protocol-binding + AgentCard protocol_binding Declaration (2026-04-07)¶
Added¶
_PROTOCOL_BINDING = "urn:acp:binding:p2p-relay/v1"global constantGET /protocol-binding— returns{binding_uri, transport, addressing, nat_traversal, sse, ws, spec_url}- AgentCard top-level
protocol_bindingfield;capabilities.protocol_binding=True - PB-01..PB-25 = 25/25 PASS
- Aligns with A2A PR #1619 (merged 2026-04-07) §5.8 URI-based CPB identification
v2.78.0 — SINT Token Revocation: POST /trust/signals/capability-token/revoke (2026-04-07)¶
Added¶
POST /trust/signals/capability-token/revoke— revoke a SINT token by JTI with reason + revoked_byGET /revocations— list all revoked tokens/fixtures/validateCheck 6: revocation status (highest priority check)409 ERR_ALREADY_REVOKEDon duplicate revoke; forward revocation (unknown JTI) supported- Completes SINT capability quad: v2.74 declare + v2.75 fixture + v2.77 validate + v2.78 revoke
- RV-01..RV-30 = 30/30 PASS; full regression 157/157 PASS
v2.77.0 — SINT Dynamic Token Validation: POST /trust/signals/capability-token/fixtures/validate (2026-04-07)¶
Added¶
POST /trust/signals/capability-token/fixtures/validate— 5-check validation pipeline: expiry (TOCTOU) → scope → skill_id → subject → required_fields- Priority deny ordering with
deny_detailsaudit trail 405on GET (must use POST)- TV-01..TV-30 = 30/30 PASS
- Aligns with A2A #1716 @pshkv SINT PR#111 runtime enforcement
v2.76.0 — effective_tier Factor 5: bilateral_ir_adj from Local IR Log (2026-04-07)¶
Added¶
- Factor 5 in
_compute_effective_tier():bilateral_ir_adjderived from local bilateral IR record log _bilateral_ir_adj(peer_id) → int— counts recent verified bilateral IRs; +1 for ≥2 clean, -1 for ≥1 flagged- Five-factor formula:
max(tier_rule, depth_floor, base + combined_adj5) factors{}extended withbilateral_ir_count,bilateral_ir_adj
v2.75.0 — SINT Canonical Authorization Fixture: GET /trust/signals/capability-token/fixtures (2026-04-07)¶
Added¶
GET /trust/signals/capability-token/fixtures— canonical authorization fixture for SINT token generation- Fixture fields:
issuer_did,subject_did,skill_id,scope,exp_ttl_s,required_fields[] capabilities.capability_token_fixtures=True
v2.74.0 — SINT Capability Token Declaration: GET /trust/signals/capability-token (2026-04-07)¶
Added¶
GET /trust/signals/capability-token— relay's SINT capability token declaration- Returns:
token_type,algorithm,issuer_did,supported_scopes[],max_ttl_s capabilities.capability_token=True;endpoints.capability_tokenin AgentCard- Aligns with A2A #1716 SINT per-invocation token design (@pshkv)
v2.73.0 — GET /agent-limitations/schema: Typed JSON Schema for agent_limitations (2026-04-07)¶
Added¶
GET /agent-limitations/schema— returns canonical JSON Schema foragent_limitationsobject- Schema covers all limitation fields with type, description, and default values
capabilities.agent_limitations_schema=True
v2.72.0 — GET /trust/bilateral-ir/log: Queryable Bilateral IR Record Log (2026-04-07)¶
Added¶
GET /trust/bilateral-ir/log— queryable log of all bilateral interaction records- Filters:
?agent_did=,?since=,?limit=,?verified_only=true - Returns:
{records[], count, total} - Aligns with A2A #1718
importBilateralEvidence()interface
v2.71.0 — security_posture as 13th Trust Signal Type (2026-04-07)¶
Added¶
security_postureas signal type #13 in_build_trust_signals()- Fields:
tls_enforced,auth_required,rate_limit_active,max_payload_enforced capabilities.security_posture_signal=True
v2.70.0 — trust.signals Severity + Category Metadata + GET /trust/signals/schema (2026-04-07)¶
Added¶
TRUST_SIGNAL_SCHEMAconstant: canonicalseverity(critical/high/medium/low) andcategory(identity/integrity/authorization/discovery/attestation) for all 12 signal types- Each signal in
_build_trust_signals()now includesseverity+categoryfields GET /trust/signals?category=<cat>&severity=<sev>filter paramsGET /trust/signals/schema— returns static canonical schema for all signal types- SC-1..15 = 15/15 PASS; TS-1..14 regression = 14/14 PASS
v2.68.0 — trust.signals[] v2: 4 New Signal Types + GET /trust/signals (2026-04-06)¶
Added¶
- Signal types #9–#12 added to
_build_trust_signals()(total: 12): bilateral_ir— bilateral signed IR (v2.59)capability_token— SINT-format per-invocation token (v2.57)wtrmrk— WTRMRK sequence-root trust factor (v2.62)external_token— cross-protocol SINT verification (v2.63)GET /trust/signals— full trust signal inventory, filterable by?type=and?enabled=
Fixed (BUG-054)¶
NameErrorin_build_trust_signals():_skillsglobal was referenced before definition
v2.67.0 — Direct Message Mode: POST /message/send (2026-04-06)¶
Added¶
POST /message/send— returnsMessagedirectly (no Task created); A2A v1.0.0SendMessageResponse { oneof { Task task; Message message; } }alignment- Response:
{ok, type:'message', message_id, role, parts[], timestamp} - Optional:
context_id,task_id,metadataechoed back rolerequired (user|agent);partsortextrequired;415on non-JSON- DM-1..14 = 14/14 PASS
v2.66.0 — Task rejected Terminal State — A2A v1.0.0 Alignment (2026-04-06)¶
Added¶
TASK_REJECTED = 'rejected'constant; added toTERMINAL_STATESPOST /tasks/{id}:agent-reject— agent-initiated rejection for any taskGET /tasks?status=rejectedfilter support- T3
:rejectendpoint:confirmation_pending→rejected(wasfailed) capabilities.rejected_state=True;endpoints.agent_rejectin AgentCard- RJ-1..10 = 9/10 PASS (1 skip: T3 skill not in test-mode)
v2.65.0 — POST /ir/import-evidence: APS-Compatible Reputation Update Payload (2026-04-06)¶
Added¶
POST /ir/import-evidence— accept external bilateral IR, verifyrelay_signature+caller_signature(Ed25519), returntrust_delta(-1/0/+1)+freshness_hintGET /ir/imported-evidence— list imported evidence, filter byagent_did+limit_verify_ir_signatures()— dual Ed25519 verification with error collection_build_reputation_update()— APS v1reputation_updatebuildercapabilities.import_evidence=True- IE-1..20 = 20/20 PASS
- Aligns with A2A #1718
importBilateralEvidence()interface (@aeoess)
v2.64.0 — Bilateral IR Test Vectors + Governance Live Endpoint (2026-04-06)¶
Added¶
GET /ir/test-vectors— 4 canonical deterministic test vectors for cross-implementation IR verification (A2A #1718 @aeoess)- tv-ir-001: bilateral IR, both Ed25519 signatures valid
- tv-ir-002: unilateral IR, relay-only signature
- tv-ir-003: tampered payload,
caller_signature_valid=false(negative test) - tv-ir-004: did:key format in canonical payload
GET /governance/live-endpoint— APS live governance endpoint declaration- ITV-1..4 = 4/4 PASS
v2.63.0 — Cross-Protocol Token Verify: GET /identity/did-key + POST /verify/external-token (2026-04-06)¶
Added¶
GET /identity/did-key— relay'sdid:key+ public key material (algorithm, multicodec, hex, base64)- Multicodec
[0xed, 0x01]+ base58btc — W3C spec, APS v1.32.0toDIDKey()and SINTkeyToDid()compatible POST /verify/external-token— SINT-format token verify (7-step: fields → expiry → decode → did:key → canonical → sig → optional MoltTrust)capabilities.external_token_verify = bool(_ed25519_private)(requires--identity)- ETV-1..16 = 16/16 PASS
v2.62.0 — wtrmrk_sequence_root Factor 4: Attestation History Adjustment in effective_tier (2026-04-06)¶
Added¶
_query_wtrmrk(sequence_root: str) → int | None— queries WTRMRK registry (api.moltrust.ch/capability-token/validate), 300s TTL cache, fail-closed (returnsNoneon any exception)_wtrmrk_to_adj(grade: int | None) → int— maps WTRMRK grade toattestation_history_adjustment:- Grade
None(query failure) →0(neutral, fail-closed) - Grade
0(unknown on-chain) →+1(raise floor) - Grade
1–2(basic/established) →0(neutral) - Grade
3(hardware-attested, long track record) →-1(may lower floor) POST /tasksbody:metadata.wtrmrk_sequence_root— optional Merkle commitment; if present, triggers Factor 4 computation during tier checkGET /skills/{id}/effective-tier: new query paramwtrmrk_sequence_root=<base64url>— activates Factor 4 in the responseAgentCard capabilities.wtrmrk_attestation: Truetests/test_wtrmrk_attestation.py— WA-1..14 (14/14 PASS)
Changed¶
_compute_effective_tier(skill_obj, peer_id, wtrmrk_sequence_root=None)— fourth factor added- New factors{} fields:
wtrmrk_sequence_root,wtrmrk_queried(bool),wtrmrk_grade(int|null),wtrmrk_adj(int|null),combined_adj(int) combined_adj = clamp(-1, +1, reputation_adj + wtrmrk_adj)with asymmetric safety rule: if either factor is+1, combined cannot be-1combined_adjreplacesreputation_adjas the applied adjustment (whenbase_int >= T2)- T3 skills remain immune to any
combined_adjdowngrade _check_authorization_tier(skill_id, peer_id, wtrmrk_sequence_root=None)— passeswtrmrk_sequence_rootthrough to_compute_effective_tier
Asymmetric Safety Rule¶
combined_adj = clamp(-1, +1, rep_adj + wtrmrk_adj)
if rep_adj == +1 OR wtrmrk_adj == +1:
combined_adj = max(0, combined_adj) # cannot be -1 if either signal is hostile
This means: - Both signals must agree to lower the floor (combined=-1): requires Grade 3 WTRMRK AND established peer - Either signal alone can raise the floor (combined=+1): defense in depth - Grade 3 + Grade 0 signals cancel to neutral (combined=0), not hostile
Factor 4 Cache Behaviour¶
- TTL: 300 seconds (configurable via
_WTRMRK_CACHE_TTL) - Cache key:
sequence_rootstring - On cache hit: returns cached grade without network call
- On failure: caches
(None, timestamp)to avoid hammering failed endpoint
Four-Factor Formula (v2.62)¶
effective_tier = max(
tier_rule, # Factor 1: declared authorization_tier
depth_floor, # Factor 2: delegation chain depth → conservative floor
base + combined_adj # Factor 3+4: rep + wtrmrk combined (only when base >= T2)
)
base = max(tier_rule_int, depth_floor_int)
Tests¶
tests/test_wtrmrk_attestation.py— WA-1..14 (14/14 PASS, 29s)- WA-1/2: No wtrmrk_sequence_root →
wtrmrk_queried=False, adj fields null - WA-3: wtrmrk_sequence_root query param →
wtrmrk_queried=True - WA-4: Query failure → fail-closed,
wtrmrk_grade=None, no server crash - WA-5:
_wtrmrk_to_adjmapping: None→0, 0→+1, 1→0, 2→0, 3→-1 ✅ - WA-6: Without high-rep peer,
combined_adj >= 0confirmed - WA-7/8: Asymmetric safety rule: single +1 prevents combined=-1 ✅
- WA-9: T3 tier immune to downgrade even with Grade-3 wtrmrk ✅
- WA-10/11: POST /tasks metadata.wtrmrk_sequence_root accepted ✅
- WA-12:
AgentCard.capabilities.wtrmrk_attestation = True✅ - WA-13/14: T2+combined=-1→T1; T1 unaffected by combined=+1 (pure logic) ✅
- Full regression: 525+ passed, 4 skipped, 0 failed ✅
v2.61.0 — caller_signature: Complete Bilateral Signing in Interaction Records (2026-04-06)¶
Added¶
caller_signature(str, optional) — base64url-encoded Ed25519 signature from the caller over the canonical IR payloadcaller_public_key(str, optional) — base64url-encoded raw Ed25519 public key of the caller; required for verificationcaller_signature_valid(bool|null) —Truewhen signature verifies;Falsewhen signature present but invalid;nullwhen no signature providedbilateral(bool) —Trueonly when bothrelay_signatureandcaller_signatureare cryptographically valid;FalseotherwiseAgentCard capabilities.bilateral_interaction_records: True— signals full bilateral signing support to peers
Changed¶
_create_interaction_record()— expanded to acceptcaller_signatureandcaller_public_key; verifies caller's Ed25519 signature against canonical IR payload (relay_did+caller_did+task_id+sequence_a+timestamp); new return fields:caller_signature,caller_public_key,caller_signature_valid,bilateralPOST /taskshandler — extractscaller_signatureandcaller_public_keyfrom the request body and passes them to_create_interaction_record()POST /tasksrole validation — BUG FIX:rolefield is now correctly looked up from the top-level request body first (body.get("role")), withpayloadas fallback; previously only checked nestedpayload, causing silent400whenrolewas in the top level (correct position)
Canonical Payload for Caller Signature Verification¶
The caller must sign the following canonical string (concatenated with |):
timestamp is the ISO 8601 UTC timestamp of the interaction record.
The relay verifies this signature using the provided caller_public_key (Ed25519 raw bytes).
Behaviour¶
caller_signatureprovided +caller_public_keyabsent →caller_signature_valid: false,bilateral: falsecaller_public_keyprovided +caller_signatureabsent →caller_signature_valid: false,bilateral: false- Neither provided →
caller_signature: null,caller_public_key: null,caller_signature_valid: null,bilateral: false - Invalid signature (e.g. wrong bytes) →
caller_signature_valid: false,bilateral: false; record is stored, not rejected bilateral: trueis achieved only with a relay identity loaded (--identity) AND a valid caller signature
Design Rationale (A2A #1718)¶
A2A Issue #1718 (bilateral signed interaction records) identified the core weakness of relay-only signing:
the relay can forge or selectively reveal interaction records — making them repudiable.
ACP v2.61 implements the caller-side signature in a fully backward-compatible manner:
- Unilateral records (relay-only) continue to work as before — bilateral: false
- Callers that supply caller_signature upgrade the record to cryptographically non-repudiable bilateral evidence
- No new endpoints required; just add two fields to the existing POST /tasks body
Tests¶
tests/test_caller_signature.py— CS-1..12 (12/12 PASS, 3.98s)- Full regression (excluding known flaky/long-running files): 511+ passed, 4 skipped, 0 failed ✅
- CS-1: no caller_signature → bilateral=false, caller_signature_valid=null
- CS-2: invalid sig (bad bytes) → caller_signature_valid=false, bilateral=false
- CS-3: sig without pubkey → caller_signature_valid=false
- CS-4: pubkey without sig → caller_signature_valid=false
- CS-5: AgentCard.capabilities.bilateral_interaction_records=true
- CS-6..12: GET /interaction-records bilateral field, chain linkage, field presence, None vs False semantics
v2.56.0 — principal_chain[] OBO Delegation Chain — Trust-Block Propagation + Runtime Management (2026-04-05)¶
Added¶
_principal_chain— module-level list of{did, role, added_at}OBO delegation entriesGET /principal-chain— list current chain; response includesself_did,count,principal_chain[]POST /principal-chain— add or upsert a principal by DID; body:{"did": "...", "role": "orchestrator"|"delegator"|"owner"|<str>}DELETE /principal-chain/<did>— remove a specific principal; 404 when DID not foundGET /peers/{peer_id}/principal-chain— return theprincipal_chainembedded in a connected peer's AgentCard trust block- 404: peer not found in registry
- 422: peer registered but has not yet shared an AgentCard
AgentCard trust.principal_chain[]— emitted when_principal_chainis non-empty; absent when chain is emptycapabilities.principal_chain = bool(_principal_chain)— discoverable via/.well-known/acp.jsonendpoints.principal_chain = "/principal-chain"andendpoints.peer_principal_chain = "/peers/{peer_id}/principal-chain"in AgentCard--principal DID[,role=ROLE]CLI flag — populate chain at startup; repeatable; upsert semantics; role defaults todelegatorPOST /message:send— new optional fieldon_behalf_of(str DID or list[str] DIDs)- If provided: outgoing message carries
principal_chain: [self_did, ...on_behalf_of] - If omitted but
_principal_chainis non-empty: auto-attach the standing chain to all outbound messages
Behaviour¶
POST /principal-chainwith a DID that already exists → upserts (replaces) the entry; no duplicatesDELETE /principal-chain/<did>on unknown DID →404with{"removed": false, "count": <int>}trust.principal_chainkey is absent from AgentCard when chain is empty (clean card for non-OBO agents)on_behalf_of: "did:acp:X"is equivalent toon_behalf_of: ["did:acp:X"]— both produce a list- Message
principal_chainformat:[<self_did>, <principal1>, <principal2>, ...]— sender always first
Design Rationale (A2A #1713)¶
A2A Issue #1713 (OBO — "On Behalf Of", 15 comments, still open) discusses cross-org accountability when Agent A acts on behalf of Agent B without a shared Authorization Server. ACP v2.56 provides a lightweight, zero-infrastructure alternative: - No shared AS required — principal DIDs are self-sovereign (Ed25519 / did:acp / did:key) - No OAuth token exchange — chains are plain JSON arrays, verifiable via existing DID infrastructure - Runtime-mutable via REST — delegates can be added/removed without restart - Composable with v2.54 trust_integration and v2.55 per-peer verification
Tests¶
tests/test_principal_chain.py— PC-1..10 (10/10 PASS)- Full regression: 239/239 PASS (pending test round; prior baseline 238/238)
v2.55.0 — GET /peers/{peer_id}/verify-card — On-Demand Per-Peer AgentCard Re-Verification (2026-04-05)¶
Added¶
GET /peers/{peer_id}/verify-card— on-demand AgentCard re-verification for a known connected peer- Reuses the v2.54 TTL cache infrastructure (
_verify_card_cached) for efficient repeated queries - Query params:
force=1— bypass TTL cache; always re-verify (also triggered byttl=0)trust=1— if verification succeeds, upsert acard_verifiedsignal into peer'strust.signalsttl=<seconds>— custom cache TTL override (default: 300;0treated asforce=1)
- Response 200 fields:
ok / peer_id / name / connected / card_available / valid / did / did_consistent / public_key / scheme / error / cached / cache_expires_in / trust_signal_written / last_connected / card_received_at - Response 404 (
ERR_PEER_NOT_FOUND): peer not in registry - Response 422 (
ERR_CARD_UNAVAILABLE): peer registered but has not yet shared an AgentCard capabilities.peer_verify_card = Truein AgentCardendpoints.peer_verify_card = "/peers/{peer_id}/verify-card"in AgentCardcard_received_attimestamp now written to_peers[peer_id]when a peer shares its AgentCard/debug/injectenhanced: accepts optionalagent_cardfield to inject a peer with an AgentCard (test fixture support)
Behaviour¶
trust_signal_written = falsewhenvalid=falseortrust=0(no spurious signals)cached=trueon second call within TTL;cache_expires_inreports remaining secondsforce=1andttl=0both bypass cache read and write paths (consistent with v2.54 behaviour)- Peer with no AgentCard returns 422, not 404 — distinguishes "unknown peer" from "card not yet received"
Bug Fixes¶
- Removed duplicate
from urllib.parse import urlparse, parse_qsinsidedo_GEThandler — Python scoping rule causedUnboundLocalErrorfor all GET routes when the local import shadowed the module-level import - Replaced 3 call sites of non-existent
_iso_now()with the correct_now()
Tests¶
tests/test_peer_verify_card.py— PVC-1..10 (10/10 PASS)- Full regression: 238/238 PASS
v2.54.0 — POST /verify-card (v2) — Batch + Fetch + TTL Cache + Trust Integration (2026-04-05)¶
Added¶
POST /verify-card— enhanced AgentCard verification endpoint (v2), three modes:mode=single(default): verify one card with TTL cache (300 s default,ttl=0bypasses cache)mode=batch: verify up to 100 AgentCards in one request; returnsvalid_count / invalid_count / unknown_count / results[]mode=fetch: fetch AgentCard from URL (wrapped or raw) then verify; returnsfetched_from + card_name- Optional params on all modes:
ttl(int),trust_integration(bool),peer_id(str) _verify_card_cachedict +_VERIFY_CARD_CACHE_TTL = 300— module-level TTL cache_verify_card_cache_key(card)— stable(public_key, card_sig)cache key_verify_card_cached(card, ttl)— cached wrapper around_verify_agent_card()_fetch_agent_card_from_url(url, timeout)— URL fetcher with wrapped/raw support_verify_card_batch(cards, ttl)— batch verifier with per-resultindexfield_apply_trust_integration(vr, peer_id)— upsertscard_verifiedsignal intotrust.signalscapabilities.verify_card_v2 = Truein AgentCardendpoints.verify_card_v2 = "/verify-card"in AgentCard
Behaviour¶
ttl=0now correctly bypasses cache on both read and write paths (bug fix from initial design)mode=batchnon-dict items in list →valid=False, error="not a JSON object"(no crash)mode=fetchfailures →422 Unprocessable Entitywith{ok: false, error: "..."}trust_integration=true+valid=false→trust_signal_written=false(no spurious signals)- Result cache is keyed by card signature, not card content — ordering-independent
Tests¶
tests/test_verify_card_v2.py— VC2-1..16 (16/16 PASS)- Full regression: 237/237 PASS
v2.53.0 — skill.rate_limit — Per-Skill / Per-Peer Invocation Frequency Limiting (2026-04-05)¶
Added¶
skill.rate_limitfield in AgentCardskills[]:{requests_per_minute?, requests_per_day?, burst?}_parse_rate_limit(): normalises and validates rate_limit config (non-int / ≤0 values silently dropped)_rl_buckets: in-memory(skill_id, peer_id)→ bucket state{min_count, day_count, burst_used, min_start, day_start}_check_rate_limit(skill_id, peer_id): checks + increments counters; resets expired windows (60s minute, 86 400s day)POST /tasks:_check_rate_limit()inserted after_check_param_constraints, before_needs_human_confirmationERR_RATE_LIMITerror constant; HTTP 429 response includeslimit_type / limit / burst / effective_limit / current_count / reset_in_seconds / skill_id / peer_idcapabilities.skill_rate_limit = truein AgentCard
Behaviour¶
burstextendsrequests_per_minuteonly (effective_limit = rpm + burst); does not extendrequests_per_day- Counters are isolated per
(skill_id, peer_id)— one peer's throttle never affects another - Skills without
rate_limit→ no change (fully backward-compatible) - Enforcement order (complete):
authorization_tier → param_constraints → rate_limit → human_confirmation
Tests¶
- SRL1–SRL12: 12/12 PASS
- Full regression: 236/236 PASS
v2.52.0 — Task Audit Log + Skill Deprecation Notice (2026-04-05)¶
Two compliance-focused features completing the ACP T3 accountability story.
Task Audit Log:
- task.audit_log[] — append-only, seq-monotonic audit trail on every task object
- _append_audit(task, event_type, detail) helper — never removes or mutates entries
- Events: created (seed), skill_invoked (skill_id/peer_id/tier), status_changed (from/to), confirmed (by human), rejected (by human + reason)
- GET /tasks/{id}/audit-log endpoint — returns {task_id, status, audit_log, total}; supports ?since_seq=N + ?limit=N for incremental polling
- audit_log also embedded in task objects from GET /tasks/{id} and POST /tasks
- capabilities.task_audit_log = True
Skill Deprecation Notice:
- skill.deprecation_notice field in AgentCard skill objects — fields: deprecated, deprecated_since, sunset_at, replacement_skill, message
- POST /tasks on deprecated skill → 201 + deprecation_warning top-level field (non-blocking)
- deprecated: false or absent → no warning injected (backward-compatible)
- Visible in GET /skills skill objects
- ERR_SKILL_DEPRECATED error constant added (informational)
- capabilities.skill_deprecation_notice = True
- Works alongside T3 human_confirmation: deprecated T3 skills return both deprecation_warning AND enter confirmation_pending
Bug fix (development): _skills NameError in POST /tasks — replaced with (_status["agent_card"] or {}).get("skills", []) lookup pattern, consistent with existing _check_authorization_tier / _check_param_constraints.
AUD1–AUD10 + DEP1–DEP6: 16/16 PASS | full regression: 217/217 PASS | commit: ac55111
v2.51.0 — T3 Human Confirmation Gate (2026-04-05)¶
Per-skill authorization tier enforcement at POST /tasks. Inspired by A2A #1716 (SINT Protocol RFC); implemented without OAuth by reusing trust.signals (v2.14) + per-peer trust scores (v2.34).
skill.authorization_tierfield in AgentCard skill objects (T0/T1/T2/T3/null)T2requirestrust_score >= 0.7;T3requirestrust_score >= 0.9+verified_identitysignalERR_AUTHORIZATION_TIERerror code, 403 response withskill_id+peer_idcapabilities.skill_authorization_tiers = Truein AgentCard- SAT1–SAT12: 12/12 PASS | core regression: 172/172 PASS
v2.48.0 — GET /peers/\<id>/messages — Per-Peer Message History (2026-04-05)¶
- New endpoint
GET /peers/<peer_id>/messageswithdirection/since_seq/sort/limit/offsetfiltering - New
--test-modeflag +POST /debug/injectendpoint for integration tests without real P2P connection capabilities.peer_message_history = True+endpoints.peer_messagesin AgentCard- PMH1–PMH10: 10/10 PASS
v2.31.0 — PATCH /skills//limitations (2026-04-02)¶
- feat:
PATCH /skills/<id>/limitations— 运行时 per-skill limitations 动态更新,无需重启 - feat:
limitations_merge: true支持追加模式(by kind+code de-dup) - feat: 空数组
[]清除 runtime override,恢复声明默认值 - feat:
GET /skills/<id>/status和GET /skills自动反映 override - feat:
capabilities.skill_limitations_patch: true - test: SU1–SU8 = 8/8 PASS;回归 189/189 PASS
v2.30.0 — error_failed_msg_id 能力声明 (2026-04-01)¶
- feat:
capabilities.error_failed_msg_id: true正式声明(功能自 v0.6 起实现,ref ANP) - test: FM1–FM8 = 8/8 PASS
v2.29.0 — GET /skills//status Per-Skill 可用性探测 (2026-04-01)¶
- feat:
GET /skills/<id>/status— 轻量 per-skill 可用性探测 - feat: runtime (
permanent:false) capability/access limitation →available: false - feat: 响应含
limitations[](runtime override 合并后的完整列表) - feat:
capabilities.skill_status_probe: true - test: SS1–SS12 = 12/12 PASS
v2.11.0 — Skills 字段增强 (2026-03-28)¶
- feat:
GET /skills响应中每个 skill 新增input_modes、output_modes、examples字段 - feat:
GET /.well-known/acp.jsonAgentCard skills[] 包含完整新字段 - feat:
/skills/query新增constraints.input_mode过滤 — 无 skill_id 时按 input_mode 筛选 - security:
/webhooks/register和/webhooks/deregister现在限制仅 localhost 调用,防止消息泄露(BUG-039)
[Unreleased] — post-v2.0-offline¶
[2.8.0] — 2026-03-28 (Extension Mechanism — URI-Identified Extensions in AgentCard)¶
Added¶
- Extension mechanism (
relay/acp_relay.py): _make_builtin_extensions()— auto-registers built-in extensions based on runtime config:acp:ext:hmac-v1when--secretis set (HMAC-SHA256 signing)acp:ext:mdns-v1when--advertise-mdnsis set (mDNS LAN discovery)acp:ext:h2c-v1when--http2is set (HTTP/2 cleartext transport)
_make_agent_card()now always emitsextensions: [](empty list when none declared) — was opt-in before v2.8- Deduplication by URI: if same URI appears in built-in and user-declared, kept once (first occurrence)
--extensions URI[,URI,...]new CLI flag — shorthand for declaring multiple extensions by URI- Built-in + user-declared extensions merged in card; built-ins first, then user-declared
- Python SDK (
sdk/python/acp_client/models.py): Extensiondataclass —uri(str, required),required(bool, defaultFalse),params(dict, default{})Extension.to_dict()— serialises to dict; omitsparamswhen emptyExtension.from_dict(d)— parses dict; validatesurirequired; forward-compat (skips malformed entries)__repr__— human-readable withrequiredindicator
AgentCard.extensions: List[Extension]field (default[])AgentCard.has_extension(uri)— bool check by URIAgentCard.get_extension(uri)→Extension | NoneAgentCard.required_extensions()→List[Extension]AgentCard.from_dict()— handles missing/nullextensionsfield (backward compat)AgentCard.to_dict()— always emitsextensionskey- Spec (
spec/core-v1.0.md): - New §5.5 "Extension Mechanism (v2.8+)" with full schema, URI naming convention, well-known built-in URIs table, semantics/compat rules, discovery, CLI flags
- AgentCard schema example updated to show
extensionsarray - Top-level fields table updated:
extensions→ stable - Tests (
tests/test_extensions.py): 39 test cases (all passing): - Extension dataclass defaults, serialisation, round-trip
- AgentCard
extensionsfield: default empty, to_dict/from_dict - Backward compat: old responses without
extensionsfield - Convenience methods:
has_extension,get_extension,required_extensions - Relay:
_make_builtin_extensionsfor all 3 built-ins - Relay:
_make_agent_cardalways emits extensions key - Relay: user-declared merge, deduplication
--extensionsCLI bulk URI parsing
Changed¶
relay/acp_relay.pyVERSION:2.7.0→2.8.0tests/unit/test_relay_core.py: updatedtest_extensions_absent_when_emptyto assert extensions key always present (v2.8 semantics)
Design¶
- Inspired by A2A extension model; designed to remain minimal and registry-free
- URI naming:
acp:ext:<name>-v<version>for built-ins; full HTTPS URL for external/vendor extensions - Non-required default:
required: false— clients that don't recognise an extension MUST ignore it - No registry, no central authority — URI uniqueness is the extension definer's responsibility
[1.8.0] — 2026-03-28 (acp-client LangChain Tool Adapter)¶
Added¶
sdk/python/acp_client/integrations/— new optional integrations sub-packagelangchain.py— LangChain Tool adapter (ACPTool,ACPCallbackHandler,create_acp_tool)ACPTool—BaseToolsubclass (lazy import; langchain is optional dep, not required for core SDK)name = "acp_send", LLM-readable description_run(message) -> str— synchronous send + receive viaRelayClient_arun(message) -> str— async wrapper (thread-pool executor, non-blocking)- Graceful error handling: returns descriptive error strings, never raises, so LLM can recover
ACPCallbackHandler—BaseCallbackHandlersubclass (lazy import)on_tool_start/on_tool_end/on_tool_error— structured log entries vialogging_callslist accumulates all events for post-run inspectioncreate_acp_tool(relay_url, peer_id, timeout=30)— factory helper
__init__.py— package docstring (zero required imports)__init__.py— conditional top-level re-export ofcreate_acp_tool(available when langchain installed)pyproject.toml— new optional extra:[langchain]=langchain>=0.1.0tests/test_langchain_integration.py— 38 test cases (all passing, mock-only, no real langchain required)- TC-01: init (name, description, relay_url, peer_id, timeout)
- TC-02: _run success paths (send_and_recv, specific peer_id, instance method)
- TC-03: _run timeout (None reply → error string, no raise)
- TC-04: _run ACPError handling
- TC-05: _arun async wrapper
- TC-06: missing langchain ImportError with install hint
- TC-07: create_acp_tool factory
- TC-08: ACPCallbackHandler events
- TC-09: repr
- TC-10: integration smoke tests
- TC-11: public API (top-level re-export)
- TC-12: pyproject.toml optional dep declared
sdk/python/README-sdk.md— new "LangChain Integration" chapter
Design¶
- Lazy import pattern: LangChain never imported at module load time;
ImportErrorwith pip hint raised only at first instantiation if langchain absent - Dynamic subclassing via
__new__: builds a realBaseTool/BaseCallbackHandlersubclass at instantiation, compatible with all LangChain versions - Zero new mandatory dependencies; core
acp_clientremains stdlib-only - Python 3.9–3.13 compatible
Bump¶
__version__:1.7.0→1.8.0
[1.7.0] — 2026-03-28 (acp-client Python pip Package)¶
Added¶
sdk/python/acp_client/— new pip-installableacp-clientpackage (v1.7.0)client.py—RelayClient(sync, stdlib urllib, zero external deps)async_client.py—AsyncRelayClient(async via run_in_executor bridge)models.py— typed dataclasses:AgentCard,Message,Task,TaskStatus,Part,PartTypeexceptions.py—ACPErrorhierarchy:PeerNotFoundError,TaskNotFoundError,TaskNotCancelableError,SendError,AuthError,TimeoutError__init__.py— clean public API surface_cli.py—acp-clientCLI entry-point (status / card / link / peers / send / recv / tasks / stream)sdk/python/pyproject.toml— PEP 517 build config (Python ≥ 3.9, zero mandatory deps, optional:[async],[http2],[dev])sdk/python/README-sdk.md— complete SDK documentation (install + 30s quick-start + full API reference + relay integration guide)sdk/python/tests/test_sdk_package.py— 60 test cases (all passing, no live relay required — uses in-process mock HTTP server)
Design¶
- Zero mandatory external dependencies (stdlib urllib only for core HTTP)
- Optional extras:
httpxfor native async,h2for HTTP/2 - Backward-compatible:
sdk/python/acp_sdk/unchanged; existingfrom acp_sdk import RelayClientcontinues to work - Fully typed public API with rich exception hierarchy
acp-clientCLI covers all major relay operations
[3.0.0] — 2026-03-28 (Automatic NAT Traversal — Three-Level P2P Integration)¶
主题:v1.4 NAT 穿透与 /peers/connect 完整集成,实现零感知自动三级降级
Added¶
_connect_with_nat_traversal(link, name, role)— 三级连接策略,替换/peers/connect直连逻辑- Level 1(直连):ws://IP:PORT/TOKEN,3s 超时
- Level 2(DCUtR 打洞):交换 signaling → TCP/UDP SYN 打洞,12s 超时
- Level 3(Relay 降级):HTTP Relay(Cloudflare Worker)兜底
/peers/connect端点现在自动调用_connect_with_nat_traversal()(不再硬编码 Level 1 直连)_peers[peer_id]["transport_level"]字段:记录实际使用的连接级别("direct" | "dcutr" | "relay")--relay语义变更:v1.4 起从「用户主动选择 relay」→「强制 Level 3 跳过 L1+L2」- SSE 事件
dcutr_started/dcutr_connected/relay_fallback:连接过程可观测 - http_relay scheme 链接直接进入 Level 3(跳过 L1+L2)
Changed¶
- VERSION:
2.9.0→3.0.0(里程碑:P2P 无中间人核心设计完整落地) /peers/connect路由逻辑:原始await guest_mode(...)替换为await _connect_with_nat_traversal(...)
Fixed¶
- BUG-037:
messages_received多 peer 场景计数始终为 0(懒绑定修复,commitced26b3) - 根因:HTTP relay 通道先于 P2P 注册处理
acp.agent_card,agent_name 未及时绑定 - 修复:
_on_message中增加懒绑定路径,将_from绑定到最新未命名 peer 再计数 - 测试:12/12 PASS(
tests/test_bug037_messages_received.py)
Design¶
- 里程碑意义:ACP 核心设计「P2P 无中间人」完整落地
- 连接流程对用户完全透明,不再需要手动指定
--relay - 最优路径(Level 1 直连)成功率 ≥70%(同网段或公网 IP 场景)
- NAT 穿透成功率预期 ≥55%(Full Cone / Restricted Cone NAT)
- 对称 NAT 自动降级 Level 3,零用户感知
[2.9.0] — 2026-03-28 (Message History List — GET /messages with Filtering + Pagination)¶
Added¶
GET /messages— 历史消息列表端点(分页 + 过滤)- 参数:
?limit=20&offset=0&from=<agent_name>&since=<epoch>&message_type=<type> - 响应:
{"messages":[...], "total": N, "has_more": bool, "next_offset": N} - 从
_recv_queue中读取,支持 sender 过滤和时间窗口过滤 [stable]端点声明(spec/core-v1.3.md端点列表)- 文档更新:
docs/whats-new.mdv2.9 节
Design¶
- 参考 A2A v1.0
tasks/list分页模式,ACP 风格化简化(无游标,仅 offset) - 与
GET /recv(弹出队列)区分:GET /messages只读不消费
[2.8.0] — 2026-03-28 (Extension Mechanism + LangChain Adapter + Node SDK v2.1.0)¶
Added — Extension Mechanism(URI-identified extensions in AgentCard)¶
_extensions全局变量:[{uri, required, params}]扩展列表--extension <URI>/--extensions <URI1,URI2,...>CLI flagsPOST /extensions/register/DELETE /extensions/{uri}运行时扩展管理GET /.well-known/acp.json始终包含extensions字段(空列表[]时不省略)- 自动内建扩展推导:
--identity启用时自动添加acp:ext:did_identity,--secret启用时添加acp:ext:hmac_signing spec/extensions.md:Extension URI 命名规范 + well-known 扩展表
Added — LangChain Adapter¶
sdk/python/acp_client/integrations/langchain.pyACPTool:LangChainBaseTool子类,将 ACP relay 封装为 LangChain 工具ACPCallbackHandler:LangChain callback handler,拦截 tool call 并转发至 ACP relay- 可与 LangChain Agent、LCEL chain、AgentExecutor 直接集成
Added — Node.js SDK v2.1.0(Extension 支持)¶
Extension类:{uri, required, params}—toDict()/fromDict()/toString()RelayClient.agentCard()升级为 async,自动解析extensions[]为Extension实例RelayClient.hasExtension(uri)— 快捷检查 AgentCard 是否含指定扩展RelayClient.requiredExtensions()— 过滤必需扩展列表- 向后兼容:
extensions字段缺失时返回[] sdk/python/acp_client/__init__.py:顶层导出Extension类(sdkv1.9.0)- 测试:14 个新增测试用例,
32/32 PASS
Added — GitHub Pages 文档站¶
docs/MkDocs Material 配置(mkdocs.yml)docs/index.md、docs/getting-started/、docs/guides/、docs/spec/- GitHub Pages CI workflow:
docs推送自动部署
Fixed¶
- BUG-036:
/peer/{id}/send响应缺少server_seq字段(commitda09a6f)
[2.7.0] — 2026-03-28 (AgentCard limitations Field — Three-Part Capability Boundary)¶
Added¶
limitations: string[]top-level AgentCard field: declares what this agent CANNOT do- Completes three-part capability boundary triad:
capabilities(can-do) +availability(scheduling) +limitations(cannot-do) --limitationsCLI flag: comma-separated string (e.g.--limitations "no_file_access,no_internet")_status["limitations"]in/statusendpoint response_limitationsglobal variable initialized to[](backward-compatible default)- spec/core-v1.3.md §11:
limitationsfield schema, well-known values table, 3-part boundary explanation - docs/whats-new.md: v2.7 section with usage examples and A2A #1694 comparison
- README: new row in vs-A2A comparison table + callout paragraph for #1694
- tests/test_limitations.py: 20 tests across LM1–LM5 (all pass)
Design¶
- ACP-exclusive: A2A #1694 (2026-03-27) proposes the same concept — ACP ships working code same day
- Fully backward-compatible: old clients ignore the optional
limitationsfield - Limitation strings are free-form
snake_case; well-known values documented in spec §11.3
[2.6.0] — 2026-03-27¶
Added¶
- Task
cancelling中间状态(两阶段取消协议) - AgentCard
capabilities.task_cancelling: true能力声明 - spec §3.3.1 两阶段取消时序图
- spec Appendix B A2A 对比(Issue #1684/#1680 差异化说明)
tests/test_task_cancel.py(10 个测试用例)
[v2.5.0] - 2026-03-27¶
Added¶
- spec §8: Task 事件序列规范(7 MUST + 2 SHOULD 合规要求)
- SSE 事件 Envelope 必填字段:type/ts/seq/task_id
- Task 完整生命周期 SSE Wire Format 示例
- relay/acp_relay.py: Named event 行(acp.task.status / acp.task.artifact)
- AgentCard: supported_interfaces 字段
- tests/test_task_event_sequence.py: 10 个 Task 事件序列测试
Fixed¶
- BUG-031: test_dcutr_t6_scenario_a.py T6.7 缺少 role 字段
- BUG-032: test_scenario_bc.py relay 启动等待不足
- BUG-033: cert teardown TimeoutExpired
[2.4.0] — 2026-03-27 (AgentCard transport_modes Top-Level Field)¶
Added — transport_modes Routing Topology Declaration (v2.4 milestone)¶
transport_modes— new top-level AgentCard field (v2.4+)- Declared at
/.well-known/acp.jsonas a top-level key (not nested undercapabilities) - Declares the routing topologies supported by this node (distinct from
capabilities.supported_transportswhich declares protocol bindings) - Valid values:
"p2p"(direct peer-to-peer WebSocket) and/or"relay"(HTTP relay-mediated) - Default:
["p2p", "relay"]— both topologies supported; peer may choose - Examples:
["p2p", "relay"]— standard node, both modes available (default)["relay"]— sandbox/NAT-only node; P2P not possible["p2p"]— edge agent with public IP; no relay dependency
- Absent means
["p2p", "relay"](backwards-compatible) -
Receivers MUST treat as advisory; unknown values MUST be ignored
-
--transport-modesCLI flag (v2.4+) - Comma-separated routing modes:
--transport-modes p2p,relay(default),--transport-modes p2p,--transport-modes relay -
Invalid values are warned and silently ignored; empty result falls back to default
-
Spec update —
spec/core-v1.0.md §5.2–§5.5 - §5.2: New "Top-Level AgentCard Fields" table (formally documents all top-level keys)
- §5.3: Capability Flags table updated with note distinguishing
supported_transportsvstransport_modes - §5.4: New dedicated section —
transport_modessemantics, valid values, CLI, examples -
§5.5: Forward Compatibility (renumbered from §5.3)
-
Tests —
tests/unit/test_transport_modes_v24.py— 15 new unit tests transport_modespresent in AgentCard, is a list, top-level (not under capabilities)- Default
["p2p", "relay"], p2p-only, relay-only variants - Snapshot semantics (mutation does not affect global)
- Version check (>= 2.4.0)
- Global default and valid values
Changed¶
relay/acp_relay.py: VERSION bumped2.2.0→2.4.0_make_agent_card(): returnstransport_modesas a snapshot list (not reference)
[2.2.0] — 2026-03-27 (GET /tasks List Endpoint with Filtering + Pagination)¶
Added — GET /tasks List Queries (v2.2 milestone)¶
GET /tasks— full list + filtering + dual pagination?status=<s>— filter by task status (submitted/working/completed/failed/canceled/input_required)- Returns
400 ERR_INVALID_REQUESTfor unknown status values - Backwards-compatible: legacy
?state=parameter still accepted (statustakes precedence)
- Returns
?peer_id=<id>— filter by peer; checks bothtask.peer_id(top-level) andtask.payload.peer_id(BUG-014 dual-layer lookup)?created_after=<ISO 8601>— return only tasks created after given timestamp?updated_after=<ISO 8601>— return only tasks updated after given timestamp?sort=asc|desc— sort bycreated_at; defaultdesc(newest first)- Legacy
created_asc/created_descvalues also accepted
- Legacy
?limit=<n>— page size; default 20, max 100 in offset mode; legacy default 50, max 200?offset=<n>— offset-based pagination (v2.2 new); triggers offset mode- Response shape (offset mode):
totalreflects filtered count (not rawlen(_tasks))next_offsetonly present whenhas_more=true- Legacy keyset cursor mode (
?cursor=<task_id>) preserved whenoffsetparam absent
Tests (TL1–TL10, tests/test_tasks_list.py)¶
- TL1: No params → returns all tasks with required fields
- TL2:
?status=workingfilters correctly; only matching tasks returned - TL3:
?peer_id=matches both top-level andpayload.peer_id(BUG-014) - TL4:
?limit=2&offset=0— first page - TL5:
?limit=2&offset=2— second page; no overlap with first - TL6:
has_more=truewhen items remain;next_offsetpresent only whenhas_more=true - TL7:
?sort=ascreturns oldest task first - TL8:
?created_after=<ISO>filters out older tasks - TL9: Impossible filter →
{"tasks": [], "total": 0, "has_more": false} - TL10:
?status=bogus→400 ERR_INVALID_REQUEST
Results: 10/10 passed — full regression: 256 passed, 4 skipped, 0 failed
[2.0.0-alpha.1] — 2026-03-26 10:17 (Offline Delivery Queue)¶
Added — Offline Message Delivery Queue (v2.0 milestone)¶
_offline_enqueue(msg, peer_id)— buffers messages when peer is disconnected (v2.0)- Called automatically from
_ws_send()onConnectionError - Per-peer keyed queue (
peer_idor"default"for legacy single-peer sends) deque(maxlen=100)per bucket — oldest messages dropped when full (never blocks)-
Stores metadata:
_queued_at,_offline_for_peer -
_offline_flush(ws, peer_id)— delivers buffered messages on reconnect (v2.0) - Called automatically in
host_modeandguest_modeafter peer connects / reconnects - Flushes in FIFO order; strips internal bookkeeping fields; adds
_was_queued: Truemarker - Tries peer-specific bucket first, then falls back to
"default"bucket -
Logs delivery count:
📤 Flushed N offline message(s) to peer '<id>' on connect -
_offline_queue_snapshot()— serializable view of all queue buckets -
GET /offline-queue— inspect offline delivery buffer -
Returns
{total_queued, max_per_peer, queue: {peer_id: {depth, messages: [{type, queued_at}]}}} -
capabilities.offline_queue: true— advertised in AgentCard endpoints.offline_queue: "/offline-queue"— advertised in AgentCard endpoints block
Behaviour change¶
POST /message:sendandPOST /sendno longer immediately fail with503and drop the message. They still return503 ERR_NOT_CONNECTED(API contract unchanged), but the message is now silently buffered for delivery the moment a peer reconnects.- Callers who want guaranteed delivery can poll
GET /offline-queueto confirm the message is buffered.
Tests (OQ1–OQ10, tests/test_offline_queue.py)¶
- OQ1: capabilities.offline_queue=True advertised
- OQ2: endpoints.offline_queue="/offline-queue" in AgentCard
- OQ3: GET /offline-queue → empty queue on fresh relay
- OQ4: Required structure fields (total_queued, max_per_peer, queue)
- OQ5: POST /message:send → 503 + message buffered
- OQ6: Queue depth increments with each failed send
- OQ7: Queue snapshot metadata has type, queued_at per message
- OQ8: Legacy POST /send also buffers to offline queue
- OQ9: Queue bounded by OFFLINE_QUEUE_MAXLEN=100 (oldest dropped)
- OQ10: Relay /status healthy after offline queue activity
Results: 10/10 passed — full regression: 236 passed, 4 skipped, 0 failed
Motivation¶
- A2A has no offline delivery mechanism — if a task message is sent while the receiving agent is offline, the message is simply lost.
- ACP v2.0 offline queue: "send and forget safely" — messages survive short disconnects, auto-delivered on reconnect without any extra code by the caller.
- Show HN talking point: "If your peer is offline when you send, ACP queues it and delivers it the moment they reconnect. A2A drops it silently."
[1.9.0] — 2026-03-26 07:45¶
Added — Peer AgentCard Auto-Verification (v1.9)¶
acp.agent_cardhandler now auto-verifies peer card on receipt- When peer sends AgentCard with
identity.card_sig, immediately calls_verify_agent_card() - Result stored in
_status["peer_card_verification"] - Logs
✅ AgentCard verified: <name> | did=<did>...on success - Logs
⚠️ AgentCard sig INVALID: <name> | <reason>on failure -
Gracefully handles unsigned peers (valid=None, descriptive error)
-
_send_agent_card()now sends signed card (v1.9 integration with v1.8) - Calls
_sign_agent_card(card)before sending during handshake -
Peer receives a verifiable card from the first message
-
GET /peer/verify— peer card verification result endpoint - Returns
{peer_name, peer_did, verified, valid, did_consistent, public_key, scheme, error} verified: convenience boolean (True iff valid is True)- 404 when no peer is connected
-
Cleared automatically on disconnect
-
_status["peer_card_verification"]initialized toNone; cleared on disconnect (both host-mode and guest-mode disconnect paths) -
capabilities.auto_card_verify: true— always advertised (all relays) endpoints.peer_verify: "/peer/verify"— advertised in AgentCard endpoints block
Tests (PV1–PV8, tests/test_peer_card_verify.py)¶
- PV1: capabilities.auto_card_verify=True on both relays
- PV2: GET /peer/verify → 404 when no peer connected
- PV3: endpoints.peer_verify = "/peer/verify" in AgentCard
- PV4: /.well-known/acp.json returns signed card when --identity enabled
- PV5: auto-verify after peer connect → verified=True (skipped: sandbox no public IP)
- PV6: unsigned peer card → valid=False + descriptive error
- PV7: /peer/verify response has all required fields (valid, did, public_key, scheme, error)
- PV8: peer_card_verification=None when no peer connected
Results: 7 passed, 1 skipped — full regression: 226 passed, 4 skipped, 0 failed
Motivation¶
- Completes the identity story: v1.8 lets you sign your card; v1.9 auto-verifies the peer's card
- Together: when two ACP agents connect, both sides automatically know if the other's identity is cryptographically verified — zero extra API calls needed
- Show HN talking point: "Connect two agents → identity mutual verification happens at handshake"
[1.8.0] — 2026-03-26 05:15¶
Added — AgentCard Self-Signature (card_sig)¶
_sign_agent_card(card)(commit TBD, v1.8)- Signs AgentCard with Ed25519 private key at serve time
- Signature covers canonical JSON (sorted keys, separators
','/':') withidentity.card_sigexcluded to avoid circular reference - Result stored at
card.identity.card_sig(base64url, no padding) -
No-op when
--identitynot enabled (zero-breaking backward compat) -
_verify_agent_card(card) - Verifies any ACP AgentCard's Ed25519 self-signature
- Returns
{valid, did, did_consistent, public_key, scheme, error} did_consistent: cross-checksdid:acp:matchesidentity.public_key-
Works for any relay's card — not just the local agent's
-
GET /.well-known/acp.jsonnow returns signed card when--identityenabled -
identity.card_sigfield added to response -
GET /verify/card— self-verification endpoint -
Returns
{self_verification, card_signed}for the local agent's own card -
POST /verify/card— arbitrary card verification endpoint - Body: raw AgentCard JSON or wrapped
{self: card}form - Returns full verification result
-
Invalid JSON body → 400
-
capabilities.card_sig:truewhen--identityenabled,falseotherwise -
endpoints.verify_card:"/verify/card"advertised in AgentCard endpoints block
Tests (CS1–CS10, tests/test_card_signature.py)¶
- CS1: card_sig present in GET /.well-known/acp.json when --identity enabled
- CS2: GET /verify/card self-verification → valid=True
- CS3: POST /verify/card valid signed card → valid=True
- CS4: POST /verify/card tampered card → valid=False
- CS5: POST /verify/card unsigned card → valid=False + "card_sig missing"
- CS6: capabilities.card_sig=True with --identity
- CS7: POST /verify/card accepts wrapped {self: card} form
- CS8: POST /verify/card invalid JSON → 400
- CS9: did_consistent=True when did:acp: matches public_key
- CS10: card_sig absent without --identity; capabilities.card_sig=False
Results: 11/11 PASS — full regression: 219 passed, 3 skipped, 0 failed
Motivation¶
- Directly addresses A2A issue #1672 (Agent Identity Verification — no protocol-level mechanism)
- ACP ships cryptographic AgentCard verification today; A2A has no timeline
- Any ACP peer can now verify "this card was signed by the owner of this did:acp:" identity without any external CA or registration service
[1.7.0] — 2026-03-25 20:30¶
Updated (spec + README — post-release patch)¶
- spec/error-codes.md: explicitly documents
Content-Type: application/json; charset=utf-8for all responses including errors; rejectsapplication/problem+json(RFC 9457) by design; references A2A #1685 as motivation (commit81ffd30) - README vs-A2A table (commit
81ffd30): - New row: "Error response Content-Type" — ACP uniform vs A2A #1685 ambiguous
- New row: "Webhook security" — ACP URL-only vs A2A #1681 credentials leaked in plaintext
- New callout paragraph referencing A2A #1681 + #1685
Added (Python SDK)¶
RelayClient.tasks()v1.4 time-window filters (commit00e4a09)- New params:
created_after,updated_after,peer_id,sort,cursor,limit -
Aligns sync and async clients with full relay
/tasksendpoint query surface -
RelayClient.cancel_task()v1.5.2 §10 idempotent semantics - Default: returns error dict on 409
ERR_TASK_NOT_CANCELABLE(no exception) raise_on_terminal=True: raisesValueErrorfor terminal-state tasks-
Async client (
AsyncRelayClient.cancel_task()) upgraded identically -
RelayClient.capabilities()— new method - Extracts
capabilitiesblock from AgentCard (http2 / did_identity / hmac_signing / mdns) -
Returns
{}gracefully when relay unreachable -
RelayClient.identity()— new method -
Returns
identityblock withdid:acp:DID field (v1.3+) -
RelayClient.did_document()— new method -
Fetches
/.well-known/did.jsonW3C DID Document (v1.3+) -
AsyncRelayClient: all above methods added to async client as well
Added (relay server)¶
- SSE
context_idpropagation (commitb91f642) _create_task(): storescontext_idon task object; includes it in initialstatusSSE event_update_task(): propagatestask.context_idto all subsequentstatusandartifactSSE events/tasks/createendpoint and/sendinline task creation both passcontext_idthrough- Tasks without
context_id: events cleanly omit the field (no null pollution) - Closes parity gap with A2A Issue #1683 (contextId missing from SSE events)
Updated (README)¶
- vs-A2A comparison table: new row "Cancel task semantics"
- ACP v1.5.2 §10: synchronous + idempotent (200 / 409
ERR_TASK_NOT_CANCELABLE) - A2A:
CancelTaskRequestschema missing (#1684), async cancel state disputed (#1680) - New callout referencing A2A issues #1680 and #1684
Tests¶
sdk/python/tests/test_relay_client_v17.py: 10 tests, 10/10 PASS- T1–T3:
tasks()time-window + combined filter query string construction - T4–T6:
cancel_task()success / 409 no-raise / 409 raise - T7:
capabilities()http2 + did_identity flags - T8:
identity()did:acp: field - T9:
did_document()W3C DID Document structure - T10:
capabilities()fallback on unreachable server tests/test_context_id_sse.py: 17/17 PASS (C1–C8, context_id SSE propagation)
Full suite: 140 passed, 0 failed ✅
[1.4.1-dev] — 2026-03-25 14:40¶
Added¶
- DCUtR HTTP reflection fallback (
relay/acp_relay.py, commitb3da914) DCUtRPuncher.attempt(): when STUN fails (UDP blocked by corporate firewall), falls back to HTTP reflection via_relay_get_public_ip()to discover public IP- Appends
{http_ip}:{local_port}to candidate address list; Level 2 hole punch continues _status["relay_base_url"]now populated at both relay startup paths (--relayCLI flag and P2Pguest_modefallback)- SSE event
dcutr_http_reflectemitted for observability - Graceful no-op when
relay_base_urlis unset
Tests¶
tests/test_nat_http_reflect.py: 12 unit tests, 12/12 PASS (mock-based, no network required)- R1–R3:
_relay_get_public_ipsuccess / timeout / invalid JSON - R4:
_status["relay_base_url"]round-trip - R5: DCUtR triggers HTTP reflection when STUN fails + relay_base set
- R6: DCUtR skips HTTP reflection when
relay_base_urlis None
Fixed¶
- BUGS.md: BUG-012 status label corrected to ✅ (code fix was already present in prior commits; status record was missed)
[1.6.0] — 2026-03-25 13:50¶
Added¶
- HTTP/2 cleartext (h2c) transport binding (
relay/acp_relay.py) - Optional dependency:
hypercorn+h2(graceful fallback to HTTP/1.1 if unavailable) - Implementation: raw
h2state machine oversocketserver.ThreadingTCPServer --http2CLI flag;capabilities.http2: truein AgentCard_H2Handler._dispatch(): bridges h2c frames to existingLocalHTTPhandler via fake socket- Supports all endpoints:
/status,/.well-known/acp.json,/tasks, SSE streams
Tests¶
tests/test_http2_transport.py: 6 scenarios (H1–H6) all PASS- H1 server startup, H2 AgentCard, H3 SSE, H4 POST /tasks, H5 /status, H6 discovery
- Test infrastructure overhaul (commit
21e3e7d) tests/conftest.py: global http_proxy strip +clean_subprocess_env()for relay subprocessespytest.mark.p2p: skip P2P-dependent tests in sandbox (--with-p2pto enable)test_scenario_h: rewritten as HTTP-only concurrent isolation test- Full suite: 15 passed, 3 skipped (P2P), 0 failed, 0 errors
Key commits: 3f06b24, e8974b2, cf578e3, 394b71c (HTTP/2), 21e3e7d (test infra), 0ac2215 (BUG-019 docs)
[1.5.2-dev] — 2026-03-25 05:55¶
Added¶
- spec §10 — Task Cancel Semantics (
spec/core-v1.3.md): explicit synchronous cancel contract - Cancel is synchronous and immediate:
:cancelreturns finalcanceledstate in the same HTTP response, no async/deferred mechanism - Cancel is idempotent: calling
:cancelon an already-canceled task returns 200 with existing state - New error code:
ERR_TASK_NOT_CANCELABLE(409) for tasks in terminal states (completed,failed) - Design rationale documented: deliberate contrast with A2A issue #1680 (async cancel, unresolved)
- Agent-side cancel behavior guidance (best-effort signal, not a transaction rollback)
- Show HN draft updated (
docs/show-hn-draft.md): added A2A competitive comparison points - A2A #1681 security bug:
PushNotificationConfigleaks credentials by default; ACP has no Push Notification mechanism - A2A #1680 cancel design gap: async cancel unresolved; ACP cancel is synchronous and unambiguous
- Updated anti-trolling prep with cancel and security talking points
- spec Appendix A: version history updated to v1.5.2
Research (2026-03-25 05:25 — Competitive scan #7, post-1.5.1-dev update)¶
- A2A 9-day code freeze continues (last merge 2026-03-16, TSC governance mode)
- A2A #1681 (security bug):
GetTaskPushNotificationConfigleaks full credentials in response — ACP has no PushNotification mechanism, zero exposure to this class of vulnerability; strong differentiation point for Show HN - A2A #1680 (design gap): async cancel semantics unresolved — community debating two approaches for cancel-in-progress tasks; ACP cancel is simple synchronous (
canceledstate returned immediately), no async webhook complexity - A2A #1679: Python tutorial docs require full rewrite for
v1.0-alpha.0breaking changes; ACP API stable, low doc maintenance burden - ANP: confirmed archived (last update 2026-03-05), no new activity
[1.5.0-dev] — 2026-03-24 (pre-1.5.1, NAT signaling layer)¶
Added (22:47 — v1.4 NAT traversal signaling layer)¶
- Cloudflare Worker v2.1: NAT traversal signaling endpoints (commit
8c162d4) GET /acp/myip— reflect caller's public IP viaCF-Connecting-IPheader; used by agents to discover their public address when STUN UDP is blockedPOST /acp/announce— register{token, ip, port, nat_type}with 30s TTL; auto-expires, no message content storedGET /acp/peer?token=— one-time fetch + delete of peer announce record (prevents address harvesting)- Privacy design: signaling records are ephemeral (30s) and one-time-read, no persistent storage of agent addresses
- Python signaling helpers (
acp_relay.py) — stdlib-only (urllib), no new deps _relay_get_public_ip(relay_base_url)— HTTP reflection fallback for when STUN UDP is firewalled_relay_announce(relay_base_url, token, ip, port, nat_type)— register address via Worker_relay_get_peer_addr(relay_base_url, token)— fetch peer address (one-time, auto-deletes)- These complement
STUNClient: STUN → primary; HTTP reflection → corporate firewall fallback tests/test_nat_signaling.py: 22/22 PASS — covers all helpers, error paths, edge cases, full roundtrip; uses local mock server, no network required
Fixed (20:33)¶
- BUG-016 (P1):
/peer/{id}/sendconnection race —ERR_PEER_CONNECTINGguard (commit665f767) - Root cause:
_register_peer()setsconnected=True, ws=Noneimmediately on/peers/connect; send handler only checkedconnected, notws, causing a spurious "not connected" 503 during WS handshake - Fix: added
ws is Noneguard returning 503ERR_PEER_CONNECTINGwith retry hint - Test fix:
wait_peer_ready()now uses probe-send success as readiness signal instead of peer list polling - Verified:
test_scenario_fg.py19/19 ✅ (was 16/19 before fix)
Fixed (20:00)¶
- BUG-015 (P3):
test_scenario_fg.pypytest incompatibility (commit58dbb66) - Root cause: module-level
sys.exit()triggeredINTERNALERROR: SystemExitwhen mixed with other pytest suites - Fix: refactored to
run_fg_tests()+test_scenario_fg()pytest entry;sys.exit()moved toif __name__ == "__main__":guard - Verified: 7 tests collected cleanly in mixed-suite run; standalone
python3execution unchanged
Research (scan #6 — 2026-03-24 21:37)¶
- A2A PR #1678 (NEW ⭐): Python SDK tutorial updated to
v1.0.0-alpha.0 AgentCard.urlrenamed toicon_url(breaking); newsupported_interfaces+extended_agent_cardfields- Signal: A2A AgentCard still churning; ACP's minimal, stable AgentCard format is a differentiation point
supported_interfacesadds protocol negotiation complexity — ACP's "one link, zero config" narrative strengthened- A2A code layer: 9 consecutive days without spec/code merge (last: 2026-03-16)
- Window remains open for ACP v1.4 + v2.0 launch before A2A stabilizes
- A2A #1676:
PushNotificationConfigmissing (still unresolved) — ACP/recvpolling unaffected - A2A #1672:
getagentid.devidentity CA discussion still open, no resolution - ANP: archived, no new activity (dropped from tracking)
- Full report:
acp-research/reports/2026-03-24-scan-2.md
Research (scan #5 — 2026-03-24 18:00)¶
- A2A #1676 (NEW):
PushNotificationConfigdefinition missing from A2A spec (bug) - ACP is unaffected;
/recvpolling design avoids push config complexity entirely - A2A #1672 (47 comments):
getagentid.devemerging as de-facto A2A identity CA - Centralized registration service; external dependency; single point of failure
- ACP
did:acp:advantage: self-sovereign, derived from Ed25519 pubkey, zero external resolver, zero registration, works fully offline — already shipping in v1.5 - A2A code layer: 8 consecutive days with no merges (last: 2026-03-16, CODEOWNERS update)
- TSC governance mode confirmed; fast-iteration window remains open for ACP
- ANP: confirmed archived (last update 2026-03-05), dropped from active tracking
- Show HN draft updated with
getagentid.devvsdid:acp:talking points (commite39ac4f) - Full report:
acp-research/reports/2026-03-24-scan.md
[1.5.1-dev] — 2026-03-24¶
Added¶
GET /taskstime-window filters —created_afterandupdated_after(commita187471)created_after=<ISO-8601>— return only tasks created after this timestampupdated_after=<ISO-8601>— return only tasks updated after this timestamp- Combinable with existing
state/peer_id/cursor/sortparams - Future timestamps → empty list (correct behavior, TF4)
- Invalid timestamp strings → 200/400, no 500 crash (TF5)
- Tests: 6/6 PASS (
tests/test_tasks_filtering.py— TF1–TF6) - Inspired by A2A v1.0.0
tasks/list+last_updated_after(research scan #4)
Fixed¶
- BUG-014 (P2):
GET /tasks?peer_id=filter was always returning empty list - Root cause:
peer_idis stored inpayload.peer_id, not top-levelt["peer_id"] - Fix: filter now checks both
t.get("peer_id")andt.get("payload", {}).get("peer_id") - Previously silently broken with zero test coverage; discovered during TF6 regression test
Research¶
- A2A v1.0.0 released 2026-03-12 — competitive analysis scan #4 (commit
8f0c9b5) - A2A v0.3.0 → v1.0.0 with multiple BREAKING CHANGES (OAuth modernization, gRPC multi-tenancy,
extendedAgentCardrestructure,canceledspelling standardization) - ACP's P2P/zero-server positioning MORE differentiated vs. A2A enterprise trajectory
- A2A #1667 (heartbeat agent): ACP
availabilityblock already ships this natively - A2A #1672 (agent identity): reference impl submitted (getagentid.dev, centralized CA); ACP ed25519 self-sovereign model is superior (no third-party CA dependency)
- Action items: P2 — SDK compat version docs; P3 — highlight self-sovereign identity in README
- Full report:
acp-research/reports/2026-03-24-scan4.md
[1.5.0-dev] — 2026-03-24 (hybrid identity)¶
Added¶
- Hybrid Identity Model (
--ca-cert) — v1.5 (commit7aaa2cb) - New CLI flag:
--ca-cert <PATH_OR_PEM> - When used alongside
--identity: AgentCard gainsidentity.ca_cert(PEM string) identity.schemeupgraded from"ed25519"→"ed25519+ca"in hybrid modecapabilities.identity:"none"|"ed25519"|"ed25519+ca"(new enum)- All
did:acp:/public_keyfields preserved — fully backward compatible - New spec:
spec/identity-v1.5.md(hybrid trust model, 4 verification strategies) - Tests: 6/6 PASS (
tests/test_v15_hybrid_identity.py) - Motivation: A2A #1672 (43 comments) converging toward same "hybrid" conclusion; ACP ships this today vs. A2A still in discussion
Research¶
- A2A code layer: 8 consecutive days without a merge (last commit 2026-03-16)
- A2A #1672 hybrid identity: self-sovereign + CA model — ACP v1.5 preemptively ships this
- A2A #1628 trust.signals[]: enterprise blockchain-level trust, out of ACP scope
- A2A #1606 data handling declarations: compliance metadata, v2.0 extensions candidate
- Reports:
acp-research/reports/2026-03-24-scan.md,2026-03-24-scan2.md
[1.4.0-dev] — 2026-03-24¶
Added¶
- Java SDK (
sdk/java/) — zero external dependencies, JDK 11+ (commit28813ed) RelayClient.of(url)— ping, send, recv, connectPeer, sendToPeer, stream (SSE), patchAvailability- Full model classes:
Part,Message,Task,SendRequest,SendResponse,SseEvent - Zero-dependency JSON serializer/parser (
Json.java, hand-written recursive descent) - Maven
pom.xml; zero runtime dependencies (JDK 11java.net.httponly) - Spring Boot
@Beanintegration example in README - Tests: 41/41 ✅ (21
JsonTestunit + 10RelayClientTestunit + 10 integration) - Scenario H test — multi-agent concurrent routing validation (commit
06f6fac) - H1: Hub simultaneous dual-peer connect (2/2 peers)
- H2: Hub→WA + Hub→WB parallel 10-msg each; zero cross-routing errors ✅
- H3: WA↔WB bidirectional concurrent exchange ✅
- H4: Idempotency ID isolation across peers ✅
- 6/6 PASS — completes all 8 scenario coverage (A–H)
- README: new
## Heartbeat / Cron Agentssection with Python template (commit06f6fac) - Research: ANP downgraded to archived in ROADMAP (last updated 2026-03-05)
Test Coverage (cumulative)¶
| Scenario | Status | File |
|---|---|---|
| A — P2P dual agent | ✅ | test_three_level_connection.py |
| B — Orchestrator→Workers | ✅ | test_scenario_bc.py |
| C — Pipeline A→B→C→A | ✅ | test_scenario_bc.py |
| D — Stress (100 msgs, concurrent) | ✅ | test_scenario_d_stress.py |
| E — NAT 3-level fallback (real) | ⏳ needs real NAT environment | — |
| F — Error handling | ✅ | test_scenario_fg.py |
| G — Disconnect/reconnect | ✅ | test_scenario_fg.py |
| H — Multi-agent concurrent routing | ✅ | (ad-hoc, 2026-03-24) |
[1.3.0-dev] — 2026-03-22/23¶
Added (v1.4-dev)¶
- Three-level connection strategy fully integrated in
guest_mode: - Level 1: Direct WebSocket (unchanged)
- Level 2: DCUtR UDP hole punch via relay signaling (NEW — wired into main connect flow)
- Signaling-only relay WS for address exchange
- STUNClient public address discovery
- Simultaneous UDP probes via DCUtRPuncher
- SSE events:
dcutr_started,dcutr_connected,relay_fallback status.connection_type:p2p_direct|dcutr_direct|relay
- Level 3: Relay permanent fallback (unchanged)
- tests/test_three_level_connection.py: 20/20 PASS
Added (v1.1)¶
GET /taskspagination — keyset cursor pagination, state/peer_id filter, sort order- New params:
limit(max 200),cursor(exclusive keyset),state,peer_id,sort - Response:
has_more,next_cursor,totalfields - Addresses the gap noted in A2A issue #1667 discussion
Added (2026-03-23 — DCUtR NAT 穿透初版实现)¶
- DCUtR 风格 UDP 打洞 NAT 穿透 — Level 2 连接策略(v1.4 特性,初版实装)
- 新增
STUNClient类 (~120 行):stdlib-only STUN Binding Request 客户端- 支持 RFC 5389 / RFC 8489(XOR-MAPPED-ADDRESS 优先,MAPPED-ADDRESS 兜底)
- 使用公共 STUN 服务器
stun.l.google.com:19302 - 3s 超时,失败静默返回 None(不抛异常)
- 运行在 executor 中,不阻塞 asyncio event loop
- 新增
DCUtRPuncher类 (~200 行):UDP 打洞状态机attempt(relay_ws, local_port)— 发起方:发 dcutr_connect → 等 dcutr_sync → 双方同时发 UDP 包 → 等回包listen_for_dcutr(relay_ws, local_port)— 响应方:等 dcutr_connect → 回 dcutr_sync → 执行打洞- 打洞成功后自动关闭 Relay 连接(后续通信完全直连)
- 所有超时/失败均静默降级,不抛异常到上层
- 新增
connect_with_holepunch()函数 (~60 行):对外公开 API- 返回
(websocket, is_direct: bool) - Level 1: 直连(3s timeout)→ Level 2: UDP 打洞(5s 信令 + 3s 探测)→ Level 3: Relay 永久中转
- 返回
- 新增 3 种 ACP 控制消息类型:
dcutr_connect/dcutr_sync/dcutr_result- 在 Relay WebSocket 上传输,不影响业务消息
- stdlib only:
asyncio,socket,struct,os,time,uuid— 无新增第三方依赖 - 向后兼容:
acp://链接格式不变,NAT 穿透对上层完全透明 - 文档:新建
docs/nat-traversal.md(用户指南),更新spec/nat-traversal-v1.4.md(完整规范)
Fixed (commit 638f778 — 2026-03-23, scenario-C ring pipeline testing)¶
- BUG-007 part 2 (P1) —
/message:sendwithpeer_idstill routed to wrong peer - Root cause: BUG-007 part 1 (commit
3a1c499) added the ambiguity guard but did not update the actual send dispatch —_ws_send_sync(msg)continued to use_peer_ws(the last-connected peer) even whenpeer_idwas explicitly provided in the body. - Fix:
_ws_send(msg, peer_id=None)and_ws_send_sync(msg, peer_id=None)now accept an optionalpeer_idparameter. When supplied, they look up_peers[peer_id]["ws"]and route directly to that WebSocket, also updating the per-peermessages_sentcounter. Both the sync and async paths of/message:sendnow pass_req_peer_id. - Legacy behavior (no
peer_id→ use_peer_ws) preserved for backward compatibility. - Verified with Scenario C (A→B→C→A ring pipeline): 8/8 checks pass ✅.
Tested — Scenario C: A→B→C→A Ring Pipeline (2026-03-23)¶
Full end-to-end 3-agent ring pipeline validated:
- Ring topology established: A→B, B→C, C→A (6 peer connections total, 2 per agent) ✅
- A injects payload (raw=[1,2,3,4,5]) → B via peer_id-directed /message:send ✅
- B receives, processes (doubled=[2,4,6,8,10]), forwards to C ✅
- C receives, finalizes (sum=30), sends result back to A ✅
- A receives complete pipeline result ✅
- Task state machine (pipeline_001 → completed) ✅
- Per-agent send/recv stats correct (A:2/1, B:1/1, C:1/1) ✅
- Result: 8/8 PASS 🎉
Fixed (commit 3a1c499 — 2026-03-23, 3-agent scenario-B testing)¶
Two bugs discovered during Orchestrator → Worker1 + Worker2 multi-peer test:
- BUG-007 (P1) —
/message:sendsilently routed to wrong peer when multiple peers connected - When ≥2 peers are connected and no
peer_idis supplied,/message:sendpreviously sent to_peer_ws(the most recently connected peer) with no indication of ambiguity. - Fix: if
len(connected_peers) > 1andpeer_idis absent in the request body, return HTTP 400ERR_AMBIGUOUS_PEERwith aconnected_peerslist guiding the caller to usePOST /peer/{id}/sendfor directed delivery. Ifpeer_idIS supplied in the body, the message is routed to that specific peer (single-peer path unchanged). -
Verified:
ERR_AMBIGUOUS_PEERreturned with peer list ✅;peer_idrouting ✅; single-peer agents unaffected ✅. -
BUG-008 (P2) — Task action endpoints had inconsistent naming convention
:cancelused A2A-aligned colon style;/update,/wait,/continueused slash style.- Fix: router now accepts both colon and slash variants for all three endpoints:
POST /tasks/{id}:update//tasks/{id}/update,GET /tasks/{id}:wait//tasks/{id}/wait,POST /tasks/{id}:continue//tasks/{id}/continue. Old slash-style paths remain fully supported (backward-compatible). - Spec will be updated to recommend colon style; both accepted indefinitely.
- Verified:
/updateslash ✅,:updatecolon ✅,:waitcolon ✅.
Known Issues (discovered 2026-03-23, not yet fixed)¶
- BUG-009 (P1) — SSE
/streamevent delivery latency ~950 ms - Root cause: the
/streamand/tasks/{id}:subscribehandlers poll the event queue usingtime.sleep(1)in a busy-wait loop. On average, an event arriving mid-sleep waits ~500 ms; worst case 1 s. Measured avg 950 ms across 8 trials. - Impact: SSE push is unsuitable for latency-sensitive use cases until fixed.
- Planned fix: replace
time.sleep(1)withthreading.Event.wait(timeout=0.05);_broadcast_sse_eventcallsevent.set()to wake subscribers immediately. Expected result: SSE delivery latency < 10 ms. - Priority: P1 — fix in next development round.
Fixed (commit 643450c — 2026-03-23, real dual-agent testing)¶
Six bugs discovered during first live AlphaAgent↔BetaAgent P2P communication session:
- BUG-001 (P0) — SSE
/streamnever delivered message events (only keepalive) - Root cause 1:
HTTPServeris single-threaded; the/streamblocking loop blocked all subsequent HTTP requests including/message:send. Fix: useThreadingHTTPServer. - Root cause 2: BaseHTTP defaults to HTTP/1.0 and sets
close_connection = Trueafterhandle_one_request()returns, silently closing the SSE connection before any events are sent. Fix:self.close_connection = False+X-Accel-Buffering: noheader. - Root cause 3:
/message:sendoutbound path never called_broadcast_sse_event. Fix: add broadcast withdirection: "outbound"after_ws_send_sync. -
Test fix:
tests/compat/test_stream.pyraw-socket reader returns 0 bytes against HTTP/1.0 keep-alive connections; replaced withhttp.clientstreaming reader. -
BUG-002 (P0) — Task
:cancelendpoint returnedstatus: "failed"instead of"canceled" -
Added
TASK_CANCELED = "canceled"constant; added toTERMINAL_STATES; cancel handler now uses the constant. -
BUG-003 (P1) —
/peers/connectfor the same link created duplicate peer entries -
Two-layer fix: (1)
/peers/connectchecks existing connected peers before registering; returnsalready_connected: trueon match. (2)guest_mode()WS connect reuses pre-registered peer entry (matched by token link) instead of calling_register_peer()again, which had created a second entry. -
BUG-004 (P1) —
/message:sendresponse body missingserver_seqfield -
Captured
seq = msg["server_seq"]before_ws_send_sync; included in both sync (reply) and async (fire-and-forget) response paths. -
BUG-005 (P1) —
peer.messages_receivedcounter never incremented -
_on_message()now looks up sender peer bymsg.get("from")name; falls back to single connected peer whenfromfield absent; incrementsmessages_received. -
BUG-006 (P2) — Client-supplied
task_idin POST/tasksbody was ignored _create_task()now accepts optionaltask_idparameter; if the ID already exists, returns the existing task (idempotent)./taskshandler passesbody.get("task_id").
Added¶
- Extension mechanism — URI-identified AgentCard extensions (commit
88d00fc) - New optional
extensionsarray in AgentCard:[{uri, required, params?}] capabilities.extensions: trueflag when at least one extension declared- Runtime APIs:
GET /extensions— list all declared extensions with countPOST /extensions/register— register new extension at runtime (no restart)POST /extensions/unregister— remove extension by URI at runtime
- Merge semantics: URI-keyed; re-registering the same URI updates in-place
- Extensions omitted from AgentCard when none declared (clean opt-in)
tests/unit: +5TestExtensionstests (card absent/present, capabilities flag, register/unregister)docs/integration-guide.md: full Extension mechanism section with curl examplesdocs/comparison.md: ACP Extensions vs A2Aextensions[]comparison row-
Design: aligned with A2A extension model (URI-identified,
requiredflag), zero-config when unused -
did:acp:DID Identity — stable, self-sovereign Agent identifier (commit6595e39) - Derives
did:acp:<base64url(ed25519-pubkey)>from existing--identitykeypair - No external registry; the DID is the key (key-based method)
- AgentCard gains
didfield when identity enabled; omitted otherwise - New endpoint
GET /.well-known/did.json— W3C-compatible DID Document:verificationMethod[]withpublicKeyMultibase(Ed25519VerificationKey2020)authentication,assertionMethodrelationships- Returns 404 when
--identitynot configured
capabilities.did_identity: trueflag when--identityprovided- Outbound AgentCard includes
didfield for peer verification tests/unit: +5TestDidAcptests (derivation, AgentCard embed, DID Document structure)docs/integration-guide.md: full DID Identity section (format, AgentCard sample,/.well-known/did.jsonsample, Python peer-verification snippet, design notes)docs/comparison.md: DID identifier + DID Document rows —did:acp:(key-based, no DNS) vs ANPdid:wba:(domain-based, requires DNS)-
docs/README.zh-CN.md: v1.3 status规划中→ 🚧 进行中, all three items ✅ -
Official Docker image v1.3 + GHCR CI publish pipeline (commit
1f0b7e5) Dockerfileversion label bumped1.2.0→1.3.0- New run examples in
Dockerfileheader: v1.3 Extension + DID identity flags - GHCR pull instructions:
docker pull ghcr.io/kickflip73/agent-communication-protocol/acp-relay:latest .github/workflows/docker-publish.yml— automated multi-arch build & push:- Triggers: push to
main, semver tags (v*.*.*), manualworkflow_dispatch - Matrix:
base(no extra deps) +full(websockets+cryptography) - Registry: GitHub Container Registry (
ghcr.io) - Tags:
:latest,:vX.Y.Z,:sha-<short>,-fullvariant suffix - Platforms:
linux/amd64+linux/arm64(multi-arch) - GHA layer cache (
cache-from/to: type=gha) for fast incremental rebuilds - Smoke-test job: pull
:latest, start container, verify/.well-known/acp.jsonreturns valid AgentCard
- Triggers: push to
docker-compose.ymlv1.3 additions:- Commented DID Identity pair example (requires
acp-relay:full, persistentacp-identityvolume) - Commented Extension registration demo example
volumes.acp-identitydeclaration for stable Ed25519 keypair across container restarts
- Commented DID Identity pair example (requires
Notes¶
- v1.3 introduces two orthogonal extensibility layers: Extensions (capability advertisement) + DID (identity layer)
- Both are fully opt-in: no breaking changes to v1.0/v1.2 deployments
- Unit test total: 92 (v1.2) + 10 (v1.3 TestExtensions + TestDidAcp) = 102 PASS
tests/unit/test_relay_core.py: 121def test_entries (includes v1.3 classes)- ACP now has 4 extensibility dimensions: HMAC security · Ed25519 identity · availability scheduling · URI-identified Extensions — all opt-in, zero-config default
- v1.1 Backlog fully closed:
failed_message_id✅ · replay-window ✅ · Rust SDK ✅ · DID ✅ · Docker CI ✅ (only HTTP/2 transport binding remains open as optional long-term item)
[1.2.0-dev] — 2026-03-22¶
Added¶
- AgentCard
availabilityblock — heartbeat/cron agent scheduling metadata (commitc10c230) - New optional
availabilityobject in AgentCard; omitted when not configured (opt-in) - Fields:
mode(persistent|heartbeat|cron|manual),interval_seconds,next_active_at,last_active_at(auto-stamped from startup time),task_latency_max_seconds capabilities.availability: trueflag when block is present- CLI flags:
--availability-mode,--heartbeat-interval,--next-active-at - Config-file keys:
availability-mode,heartbeat-interval,next-active-at - ACP is the first Agent communication protocol to support scheduling metadata natively (A2A issue #1667, 2026-03-21: A2A AgentCard has no scheduling fields)
tests/unit: +10TestAgentCardAvailabilitytests; total 83 PASSPATCH /.well-known/acp.json— live availability update API (commitcd67181)- Heartbeat agents can stamp
next_active_at/last_active_aton each wake without restarting the relay - Merge semantics: only patched fields are updated; others preserved
- Whitelist validation: allowed fields enforced; unknown fields → 400
- Mode enum validation; missing
availabilitykey → 400 - Supports both
/cardand/.well-known/acp.jsonpaths tests/unit: +9TestPatchAvailabilitytests; total 92 PASSdocs/cli-reference.mdupdated to v1.2-
New section: "Live availability update (PATCH)" with curl examples, response schema, PATCH rules summary, macOS/Linux
datecommand portability note -
Rust SDK —
sdk/rust/—acp-relay-sdkv1.2 (commitbed7884) - Thin blocking HTTP client (
reqwest 0.12+serde+thiserror) RelayClient::new(base_url)— validates URL scheme; strips trailing slashsend_message(MessageRequest)→MessageResponseMessageRequest::user/agent(text)helpers;.with_message_id(id);.sync_timeout(secs)for blocking request-response
agent_card()→AgentCardResponse(self + optional peer, withAvailability)patch_availability(AvailabilityPatch)→ live update scheduling metadata (v1.2)status(),link(),ping()utility methodsAcpErrorenum:Http/Relay { code, message }/InvalidUrl/Json- 8 unit tests (helpers, URL validation, skip_serializing_if behaviour)
sdk/rust/README.md: quick-start, heartbeat example, API tabledocs/integration-guide.md— new full Rust SDK section (send, card, PATCH, error handling)- Added Go SDK section header to match Python/Node/Rust consistency
Notes¶
- Inspired by A2A issue #1667: A2A protocol has no mechanism for heartbeat/cron agents to advertise scheduling intent. ACP v1.2 fills this gap with a clean, opt-in design.
- Multi-language SDK matrix now complete: Python ✅ · Go ✅ · Node.js ✅ · Rust ✅
[1.1.0-dev] — 2026-03-22¶
Added¶
- HMAC replay-window (
--hmac-window <seconds>, default 300 s) (commite263f52) - New
_hmac_check_replay_window(ts_str)helper: parses ISO-8601 UTC timestamp, checks|server_now − msg_ts| ≤ window; returns(ok, reason)for clean logging - Inbound WS handler: when
--secretis set, out-of-window messages are hard-rejected (dropped) before any processing — prevents replay attacks - Signature mismatch remains warn-only for graceful interop with legacy agents
- Configurable via
--hmac-window <seconds>CLI flag orhmac-windowconfig-file key - Graceful degradation: when
--secretis not set, replay-window check is a no-op docs/security.md: HMAC audit result PARTIAL → ✅ PASS; new §1.3 replay-window docs; audit history v1.1.0 = 9 PASS, 0 PARTIALtests/unit: +10TestHMACReplayWindowtests; unit test total 63 → 73 PASS
Security¶
- HMAC-SHA256 audit now fully PASS (9/9, 0 PARTIAL)
- Previous PARTIAL item: "no server-side timestamp window check" — now resolved
[1.0.0] — 2026-03-21¶
Added (P0 — Specification & Versioning)¶
spec/core-v1.0.md: authoritative v1.0 specification (631 lines) (commit20aa1ed)- Supersedes
spec/core-v0.8.md - Stability annotations:
stable/experimentalper endpoint and field - §1.1: role MUST-level validation rules (v0.9 breaking change formally recorded)
- §4: complete HTTP API stability matrix (17 endpoints)
- §6:
ERR_INVALID_REQUESTformal definition (incl. role trigger) - §11: CLI reference (12 flags, stability annotations)
- §12: package distribution (
pip install acp-relay,npm install acp-relay-client) - §13: v1.0 compatibility guarantees (4 MUST requirements)
- Appendix A: version history through v0.9 + v1.0
- Appendix B: ACP vs A2A comparison table (refs #876, #883)
- API stability annotations in
acp_relay.py(commit19b3627) [stable](13 endpoints):/.well-known/acp.json,/status,/peers,/recv,/tasks,/stream,/message:send,/send(legacy),/peers/connect,/tasks/{id}/continue,/tasks/{id}:cancel,/skills/query[experimental](1 endpoint):/discover(mDNS, platform-dependent)docs/security.md: complete security model documentation (commita3ee229)- §1 HMAC-SHA256: mechanism, audit findings table (replay-window later resolved in v1.1)
- §2 Ed25519: mechanism, audit findings table, HMAC coexistence
- §3 HMAC vs Ed25519 side-by-side comparison
- §4 Transport security recommendations (nginx/Caddy/Cloudflare Tunnel)
- §5 Known limitations summary (severity + roadmap)
- §6 Audit history
- Go SDK stub (
sdk/go/) (commitbcf6b75) - Package
acprelay— stdlib-only, zero external dependencies (Go 1.21+) Clientstruct with 6 stable methods:Send,Recv,GetStatus,GetTasks,CancelTask,QuerySkills- 16 tests via
net/http/httptest.Server sdk/go/README.mdwith install + quick start + API reference table
Changed (P0)¶
- Version bumped to
1.0.0across all package files (commitddfaf07) relay/acp_relay.py:VERSION = "0.8-dev"→"1.0.0"pyproject.toml:0.9.0.dev0→1.0.0sdk/python/setup.py:0.9.0.dev0→1.0.0sdk/node/package.json:0.9.0-dev.0→1.0.0
Security (P1 — Audit)¶
- HMAC-SHA256 audit (commit
a3ee229) - ✅ PASS:
hmac.compare_digestconstant-time comparison - ✅ PASS: no timing oracle in error path
- ✅ PASS:
message_idunpredictability (secrets.token_hex(8)) - ✅ PASS: secret never written to disk
- ⚠️ PARTIAL: no server-side replay-window timestamp check (resolved in v1.1
--hmac-window) - Ed25519 identity audit (commit
a3ee229) - ✅ PASS: key file permissions enforced (
chmod 0600) - ✅ PASS: canonical form deterministic (
sort_keys=True+ compact separators) - ✅ PASS:
identity.sigexcluded from signing payload correctly - ✅ PASS:
InvalidSignatureexception handling (no exception leaks) - ✅ PASS: graceful fallback when
cryptographynot installed - ✅ PASS: key generation from OS CSPRNG (
Ed25519PrivateKey.generate())
Release Tag¶
v1.0.0-rc.1pushed (commitddfaf07)
[0.9.0] — 2026-03-21¶
Added (P0 — Developer UX)¶
- CLI
--version: printsacp_relay.py <version>and exits (commite74afdf) - CLI
--verbose/-v: switch root logger from INFO → DEBUG at startup - CLI
--config <FILE>: load defaults from a JSON or YAML config file - JSON: stdlib
json.loads - YAML: stdlib-only flat key-value parser (no PyYAML required); bool/int coercion
- Precedence:
CLI flags > config file > hardcoded defaults - All 12 flags supported; clear error + exit(1) on missing file
- Example config files:
relay/examples/config.json,config-relay.json,config-secure.yaml docs/cli-reference.md: comprehensive CLI reference (all flags, port layout, 8 usage patterns, config file section)spec/core-v0.8.md: single authoritative specification (515 lines, supersedes core-v0.5.md) (commit4728b0e)- 11 chapters: principles, message envelope, Part model, Task FSM, AgentCard, error codes, extensions, transport, peer registration, skill query, versioning
- Appendix A: full version history v0.1–v0.8
- Appendix B: A2A v1.0 comparison table
Changed (P0)¶
AsyncRelayClientrewritten — stdlib-only, zero external dependencies (removedaiohttp) (commit7bcb907)- Implementation:
asyncio.get_event_loop().run_in_executor()offloads urllib calls to thread pool - New methods:
connect_peer,discover,card,link,get_task,continue_task,cancel_task,wait_for_task, asyncstreamgenerator send(): addscontext_id(v0.7),task_id,create_task,syncmodeupdate_task(): newartifactparameterquery_skills(): addsqueryfree-text +limitparamswait_for_peer(): converted to async- 35 new tests in
sdk/python/tests/test_async_relay_client.py— all passing - Python SDK
__version__:0.6.0→0.8.0 acp-research/ROADMAP.md: full rewrite — all v0.1–v0.8 milestones marked complete
Added (P1 — Quality & Docs)¶
/message:sendserver-side required field validation (commitbb1c80e)- Missing
role→400 ERR_INVALID_REQUESTwith descriptive error message - Invalid
rolevalue (notuser/agent) →400 ERR_INVALID_REQUEST - Replaces silent default
"user"fallback; addresses A2A issue #876 gap - 7 new MUST-level test cases in
tests/compat/test_message_send.py CHANGELOG.md(this file): complete version history v0.1.0–v0.9.0-dev (commitb48e9d5)docs/integration-guide.mdcomprehensive rewrite (commit2a74d3e)- Covers P2P / Relay / mDNS transport options; port layout (WS :7801 + HTTP :7901)
- Task CRUD, multi-peer sessions, HMAC signing, Ed25519 identity
- Python sync + async SDK examples; Node.js SDK examples
- Multi-language quick-start (curl / Go / Java / Rust)
- Troubleshooting table (503 / 400 / 413 + solutions)
tests/unit/test_relay_core.py: 63 unit tests covering all internal helpers (commitac9846c)- TestErrHelper, TestIdGenerators, TestPartConstructors, TestValidatePart/Parts, TestHMACHelpers, TestTaskStateConstants, TestLoadConfigFile, TestParseLink, TestVersion
Added (P2 — Package Distribution)¶
pyproject.toml:pip install acp-relaysupport (commit0fb0c9e)- Package name:
acp-relay; version:0.9.0.dev0 - Required dep:
websockets>=12.0only - Optional
[identity]:cryptography>=42.0; Optional[dev]: pytest + httpx - CLI entry-point:
acp-relay = 'acp_relay:main' relay/py.typedPEP 561 marker- Node.js SDK renamed to
acp-relay-client(commit9c1b0d9) - ESM entry-point
src/index.mjs(createRequire bridge,export default RelayClient) package.json: full npm metadata,exportsfield (ESM + CJS + types), files whitelist.npmignore: excludestests/from published packageLICENSE: Apache-2.0 (aligned with repo root)- 19 tests passing
[0.8.0] — 2026-03-21¶
Added¶
- Ed25519 optional identity extension (
--identity [path]) (commit1a13dec) - Self-sovereign keypair: auto-generated at
~/.acp/identity.json(chmod 0600) - Every outbound message includes
identity.sig(base64url-encoded Ed25519 signature) - AgentCard publishes
identity.public_keyfor peer verification - Graceful fallback: identity block omitted when
cryptographynot installed - Requires:
pip install cryptography - Node.js SDK (
sdk/node/) (commitfd8c02a) RelayClientclass — zero external dependencies, TypeScript types- All v0.8 endpoints: send, recv, tasks, peers, skills, stream (SSE)
- 19 tests passing
- Compatibility test suite (
tests/compat/) (commit98197cf) - Black-box spec compliance runner: parameterized by
ACP_BASE_URL - Covers: AgentCard structure,
/message:sendresponse shape, SSE events, Task lifecycle, error code format, idempotency spec/core-v0.8.md: consolidated authoritative specification (515 lines) supersedesspec/core-v0.5.mdandspec/transports.md
Changed¶
- README overhauled for v0.8: dependency table, full feature matrix, updated quickstart
[0.7.0] — 2026-03-20¶
Added¶
- HMAC-SHA256 optional message signing (
--secret <key>) (commit87dad51) sig = HMAC-SHA256(secret, message_id + ":" + timestamp)- Verification is warn-only (never drops messages) for graceful interop
- AgentCard
trust.scheme:"hmac-sha256"|"none" - mDNS LAN peer discovery (
--advertise-mdns) (commitaabfae5) - Pure stdlib UDP multicast
224.0.0.251:5354— no zeroconf library required GET /discover: returns list of LAN peers withacp://links- SSE event
type=mdnsfor real-time new-peer notifications context_idmulti-turn conversation grouping (commitaabfae5)- Optional field on
/message:send— client-generated, server-echoed - Groups related messages across multiple Task cycles
- AgentCard capability:
context_id: true spec/transports.mdv0.3: Protocol Bindings vs Extensions separation (commit68db641)
Changed¶
- AgentCard
capabilitiesblock:hmac_signing,lan_discovery,context_idfields
[0.6.0] — 2026-03-20¶
Added¶
- Multi-session peer registry (commit
ad7e1c4) GET /peers: list all connected peersGET /peer/{id}: get a specific peer's infoPOST /peer/{id}/send: send a message to a specific peerPOST /peers/connect: connect to a new peer viaacp://link- AgentCard capability:
multi_session: true - Standardized error codes (commit
c816cb5) - 6 codes:
ERR_NOT_CONNECTED/ERR_MSG_TOO_LARGE/ERR_NOT_FOUND/ERR_INVALID_REQUEST/ERR_TIMEOUT/ERR_INTERNAL - Unified response:
{ok, error_code, error, failed_message_id} failed_message_id: enables precise client-side retries (inspired by ANP)- Reference:
spec/error-codes.md - Minimal agent spec (
spec/v0.6-minimal-agent.md): 3-endpoint minimum to join ACP network GET /.well-known/acp.json(AgentCard)POST /message:send(receive inbound)GET /stream(SSE outbound, optional)- Python SDK v0.6 (
sdk/python/) (commit430a97f) RelayClient: sync HTTP client, all v0.6 endpoints, stdlib-onlyRelayClient.stream(): SSE generator usingurllib- Cloudflare Worker v2.0 (commit
8e8b771) - Multi-room concurrent sessions
- Sliding TTL (30 min inactivity expiry)
- Cursor-based poll (no duplicate messages)
DELETE /acp/{token}cleanup endpoint- Transport C: HTTP polling relay (
acp+wss://scheme) (commit907c729) - Fallback for K8s/firewall environments with no inbound TCP
- Auto-fallback: P2P timeout (10 s) → relay (commit
fd74394) - Composite link: single
acp://token pre-registered on relay; transparent upgrade/fallback - Proxy-aware WebSocket connector (commit
4f392b8) - Reads
http_proxy/HTTPS_PROXYenv vars; routes WS through HTTP CONNECT tunnel
Removed¶
- GitHub Issues relay transport (
acp+gh://) permanently deleted (commitbc25ab7) - Reason: required both-side GitHub tokens; violated zero-registration principle
[0.5.0] — 2026-03-19¶
Added¶
- Task state machine — 5 states (commit
cd9545e,bb6aba3)
New endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| /tasks | GET | List tasks; ?status= filter |
| /tasks/{id} | GET | Get single task |
| /tasks/{id}/wait | GET | Long-poll until terminal state (?timeout=N) |
| /tasks/{id}/update | POST | Update state + optional artifact |
| /tasks/{id}/continue | POST | Resume from input_required |
| /tasks/{id}:cancel | POST | Cancel → failed |
| /tasks/{id}:subscribe | GET | Per-task SSE stream |
- Bilateral task synchronization:
create_task: trueon/message:sendauto-registers same-id task on the receiving peer; state updates propagate back viatask.updatedmessages - Structured Part model — three types:
- Message idempotency
message_id: client-generated UUID, server deduplicates per sessionserver_seq: monotonically increasing counter; clients can detect gaps/reordering- QuerySkill API (commit
710aade) POST /skills/query: runtime capability query (skill_id,capabilityfilter)GET /.well-known/acp.json: standard AgentCard discovery endpoint- Structured SSE event types:
status|artifact|message|peer /message:sendendpoint (A2A-aligned) alongside legacy/sendspec/core-v0.5.md: initial formal specification
[0.4.0] — 2026-03-18¶
Added¶
- A2A-aligned AgentCard (commit
83ca11b) /.well-known/acp.json:name,description,version,capabilities,skillssession_idfield on all messages- Safety limits:
--max-msg-sizeflag (default 1 MiB);ERR_MSG_TOO_LARGEon violation --relayflag for host mode: one-command relay session start (commit07f38ff)- SKILL.md v2: full SOP runbook with InStreet-style observable verification
Fixed¶
- Unbounded consumption risk: max message size enforcement
- Critical
NameErrorin peer-equal architecture refactor (commitaf73415)
[0.3.0] — 2026-03-18¶
Added¶
- Four communication modes (commit
4f7e242) - Standard (request-response)
- Streaming (SSE events)
- Task delegation (fire-and-forget with status polling)
- Broadcast (one-to-many)
- Explicit connection lifecycle:
connect/disconnectevents; clean teardown - Lightweight explicit session management: session tokens in AgentCard
[0.2.0] — 2026-03-05¶
Added¶
- ACP P2P v0.2: decentralized group chat support
- Skill guide: how to expose and invoke agent capabilities
acp_relay.py: local daemon replacing central relay server architecture- Zero-code-change design: Agents connect by passing a single link
- Human-as-messenger pattern:
acp://IP:PORT/TOKENlink shared by human
Changed¶
- Architecture shift: from centralized relay → true P2P direct connect (commit
183c425)
[0.1.0] — 2026-03-05¶
Added¶
- Initial ACP v0.1 specification (
spec/) - Python SDK skeleton (
sdk/python/) - Gateway server reference implementation
- Framework integration examples (LangChain, AutoGen, CrewAI stubs)
- Bilingual README (EN + ZH)
- Design principles established:
- Lightweight & zero-config
- True P2P — no middleman
- Practical — curl-compatible
- Personal/team focus
- Standardization (Agent↔Agent, like MCP for Agent↔Tool)
Version Summary¶
| Version | Date | Theme | Key Feature |
|---|---|---|---|
| 0.9.0-dev | 2026-03-21 | Developer UX + Distribution | CLI flags, async SDK stdlib-only, unit tests, pip install acp-relay, acp-relay-client npm |
| 0.8.0 | 2026-03-21 | Ecosystem | Ed25519 identity, Node.js SDK, compat test suite |
| 0.7.0 | 2026-03-20 | Trust + Discovery | HMAC signing, mDNS LAN discovery, context_id |
| 0.6.0 | 2026-03-20 | Multi-peer + Reliability | Peer registry, error codes, HTTP relay, Python SDK |
| 0.5.0 | 2026-03-19 | Structure | Task state machine, Part model, idempotency, QuerySkill |
| 0.4.0 | 2026-03-18 | Safety | AgentCard v2, max-msg-size, SKILL.md SOP |
| 0.3.0 | 2026-03-18 | Modes | 4 communication modes, explicit lifecycle |
| 0.2.0 | 2026-03-05 | P2P | True P2P relay, Skill guide, zero-code-change |
| 0.1.0 | 2026-03-05 | Foundation | Initial spec, Python SDK, design principles |