Skip to content

v0.3.0 — A2A AgentCard + DNS TXT, four-language parity#5

Merged
jaschadub merged 14 commits into
mainfrom
release/v0.3.0
May 14, 2026
Merged

v0.3.0 — A2A AgentCard + DNS TXT, four-language parity#5
jaschadub merged 14 commits into
mainfrom
release/v0.3.0

Conversation

@jaschadub
Copy link
Copy Markdown
Contributor

Summary

  • Ships v0.3.0 across Rust, JavaScript, Python, and Go — first release with full four-language parity for the A2A AgentCard extension surface and DNS TXT cross-verification.
  • Cards signed in any SDK verify cleanly in the other three; canonical signing input is byte-identical across all four implementations.
  • Closes the Symbiont v1.8.0 / SchemaPin v1.4.0 dependency on a stable AgentPin A2A surface.

What's in this release

A2A AgentCard extension

  • A2aAgentCard, AgentpinExtension types
  • BuildAndSignAgentCard / buildAndSignAgentCard / build_and_sign_agent_card / A2aAgentCardBuilder builder API
  • VerifyAgentpinExtension / verifyAgentpinExtension / verify_agentpin_extension verifier
  • AllowedDomains typed wrapper for cross-protocol allow-list intersection (consumed by SchemaPin v1.4 A2aVerificationContext)
  • a2a_endpoint optional field on DiscoveryDocument

Resolvers

  • LocalAgentCardStore — in-memory pre-registered AgentCards (backs Symbiont's push-based external-agent registration flow)
  • A2aAgentCardResolver — HTTPS fetch + extension verification + endpoint-host cross-check + DiscoveryDocument derivation

DNS TXT cross-verification

  • _agentpin.{domain} IN TXT "v=agentpin1; kid=...; fp=sha256:<hex>"
  • Parse / match / fail-closed on mismatch
  • Async lookup helpers per SDK (hickory-resolver / dns/promises / dnspython / net.Resolver)

Go SDK

  • Initial fourth-language port at the full v0.3.0 surface (the v0.2.0-only initial Go port is superseded)
  • cmd/agentpin CLI with keygen | issue | verify | bundle | version

Test plan

  • Rust workspace: cargo test --workspace --all-features (179 tests) + cargo clippy -- -D warnings + cargo fmt --check
  • JavaScript: npm test (181 tests, node:test)
  • Python: pytest (174 tests)
  • Go: go test ./... (16 packages green) + go vet + gofmt -l
  • CI version-consistency check passes (all four SDKs at 0.3.0)
  • CLI smoke test: Rust keygen → issue → verify and Go CLI verifies Rust-issued credential (and vice versa)
  • agentpin-server discovery + revocation endpoints respond correctly
  • 12-way JWT credential interop matrix — all directions pass (Rust↔Go↔Python↔JS, 12/12)
  • 12-way A2A AgentCard interop matrix — all directions pass (12/12)
  • DNS TXT parse + verify + mismatch-rejection in Python/JS/Go
  • LocalAgentCardStore registers and resolves cards signed in foreign SDKs (cross-language)
  • Stub/placeholder/mock audit: clean (no TODO/FIXME/unimplemented! in shipping source)

jaschadub and others added 14 commits April 3, 2026 21:41
…dup, CI/CD

Transport bindings (spec Section 13):
- HTTP, MCP, WebSocket, gRPC extract/format helpers (Rust, Python, JS)
- Reference middleware examples (Axum, FastAPI, Express)

Capability taxonomy validation:
- validate_capability() with core action passthrough, admin:* rejection,
  reverse-domain enforcement for custom actions
- strict_capabilities flag on VerifierConfig (default: false)

Key rotation helpers:
- prepare_rotation / apply_rotation / complete_rotation lifecycle
- Integrates with discovery documents and revocation system

Nonce deduplication:
- NonceStore trait + InMemoryNonceStore with lazy TTL expiry
- verify_response_with_nonce_store() for replay attack prevention

CI/CD:
- GitHub Actions for Rust (stable + MSRV 1.70), Python (3.9 + 3.12),
  JavaScript (Node 18 + 20), and cross-language version consistency

Integration tests:
- 6 end-to-end scenarios per language (maker-deployer flow, revocation,
  mutual auth, transport roundtrip, key rotation, pinning)

Test results: Rust 135, Python 117, JavaScript 127 -- all passing
Implements the v0.3.0 surface that **Symbiont v1.8.0 Phase 3 and SchemaPin
v1.4.0's A2aVerificationContext both depend on**. Rust-only in this PR;
JavaScript and Python ports follow in alpha.2.

New surface (purely additive — v0.2.0 callers unaffected):

- AllowedDomains type (types::discovery): typed wrapper over the list of
  domains an agent is permitted to interact with. Extracted from
  Constraints::allowed_domains via the new
  Constraints::allowed_domains_typed() helper. Empty list = no restriction
  (all domains trusted). Includes intersect() for composing with
  cross-protocol callers — most importantly SchemaPin v1.4's
  A2aVerificationContext, which scopes tool verification to the
  intersection of caller and provider domains. Implements FromIterator.

- Minimal A2A AgentCard subset (types::a2a): A2aAgentCard,
  A2aAgentCapabilities, A2aAgentSkill plus the AgentPin-specific
  AgentpinExtension (agentpin_endpoint, public_key_jwk, signature).
  Inline definition rather than a dependency on the upstream a2a-types
  crate while the A2A spec is still draft — the public surface lets
  us re-export from upstream once it stabilises without breaking
  callers.

- A2aAgentCardBuilder (a2a module): turns an AgentDeclaration into a
  signed A2aAgentCard. Maps capabilities to skills via
  capability_to_skill, propagates Constraints::allowed_domains into
  A2aAgentCapabilities::allowed_domains. Detached ECDSA P-256 signature
  covers the canonical (sorted-key) bytes of the AgentCard with the
  extension cleared. with_skill_overrides() lets callers supply richer
  skill names/descriptions than the raw capability strings.

- verify_agentpin_extension(card): verifies the extension signature
  against the JWK embedded in the extension. Defends against tampering
  with name / url / capabilities / skills / agentpin_endpoint.

- LocalAgentCardStore (resolver_local module): in-memory store of
  pre-registered AgentCards keyed by their AgentPin discovery domain.
  Implements DiscoveryResolver (always available, no `fetch` feature).
  Verifies the signature at register-time and pre-derives a
  DiscoveryDocument so the rest of the AgentPin verification stack
  runs unchanged. Supports Symbiont v1.7.0's push-based external-agent
  registration flow.

- A2aAgentCardResolver (resolver_a2a module, gated on `fetch`):
  fetches https://{domain}/.well-known/agent-card.json, verifies the
  AgentPin extension, cross-checks that the embedded agentpin_endpoint
  host matches the fetched domain (defends against a card pointing at
  another domain's AgentPin discovery), and derives a
  DiscoveryDocument. last_card() exposes the original A2A
  representation for callers that want both views.

- a2a_endpoint field on DiscoveryDocument — optional URL of the
  entity's A2A AgentCard endpoint, enabling cross-protocol discovery.

QA:

- 153 lib tests pass (was 145; +8 AllowedDomains, +4 types::a2a, +7
  a2a builder, +8 resolver_local — net new tests are folded into
  the lib total).
- cargo build / cargo test --all-features green.
- cargo clippy --all-features -j2 -- -D warnings clean.
- cargo fmt --check clean.

Spec / docs:

- CHANGELOG: new 0.3.0-alpha.1 entry.
- ROADMAP: v0.3.0 marked Rust shipped (alpha.1); release-timeline row
  added.
- SKILL.md: frontmatter version bumped to 0.3.0-alpha.1, stable_version
  field added (0.2.0).
- context7.json: description expanded with v0.3.0-alpha A2A surface +
  trust-stack position.

Backward compatibility:

- DiscoveryDocument without `a2a_endpoint` and AgentCards without
  `agentpin` extension behave exactly as v0.2.0.
- All existing 145 tests still pass unchanged.

Roadmap impact:

- Item v0.3.0 — Rust complete. JS/Python ports follow in alpha.2.
- Unblocks SchemaPin v1.4.0 A2aVerificationContext (item 5 of v1.4
  roadmap, was waiting on AllowedDomains).
- Unblocks Symbiont v1.8.0 Phase 3 (AgentPin-verified AgentCards).
v0.3.0-alpha.1 (P0): A2A AgentCard types + AllowedDomains (Rust)
Adds an OPTIONAL second-channel verification mechanism mirroring SchemaPin
v1.4-alpha.1's _schemapin.{domain} record exactly. The wire format is the
same parser shape with the version tag changed; AgentPin spec § 4.8.3 had
already reserved this slot in v0.1, this PR ships the implementation.

Wire format:

    _agentpin.example.com.  3600  IN  TXT  "v=agentpin1; kid=...; fp=sha256:..."

New `dns` module (always available; no DNS dependencies):

- DnsTxtRecord struct (version, kid, fingerprint).
- parse_txt_record(value) — whitespace-tolerant, case-insensitive on `fp`,
  ignores unknown fields for forward compatibility, requires `v=agentpin1`
  and `fp=sha256:<hex>`. Cleanly rejects SchemaPin's v=schemapin1 records.
- verify_dns_match(discovery, txt) — returns Ok(()) when the TXT `fp`
  matches the JWK thumbprint of *any* key in discovery.public_keys.
  AgentPin discovery docs may carry several keys for rotation; a published
  TXT record need only match one. When the TXT carries an explicit `kid`,
  the matching key MUST also carry the same `kid` (defends against the
  vanishingly-unlikely case of two keys sharing a SHA-256 fingerprint).
- txt_record_name(domain) — `_agentpin.{domain}` with trailing-dot trim.
- fetch_dns_txt(domain) — async lookup behind the new `dns` Cargo feature
  (uses hickory-resolver). Returns Ok(None) when no _agentpin record
  exists; mismatching/malformed records return Err.

Verifier semantics:

- Absent record   → no effect (purely additive)
- Present + match → verification succeeds (absence of mismatch = signal)
- Present + miss  → hard fail (Error::Discovery) — fail-closed because a
  publisher who *intentionally* published a TXT record signaled DNS is
  part of their trust chain. Divergence between DNS and .well-known
  indicates compromise of one channel; better to refuse than to guess.

Cargo features:

- `fetch` (existing) → reqwest + tokio + async-trait
- `dns`   (NEW)      → hickory-resolver + tokio + async-trait

QA:

- 141 lib tests pass (was 130; +11 new DNS tests covering parse paths,
  multi-key match, kid-disambiguation, mismatch fail, txt_record_name).
- cargo build / test --all-features green.
- cargo clippy --all-features -- -D warnings clean.
- cargo fmt --check clean.

Versions bumped to 0.3.0-alpha.1 across Rust, JS, Python (CI requires
all three SDK manifests to match). JS/Python ports of the dns module
follow in subsequent alphas; the version tag aligns the alpha cycle.

Spec / docs:

- CHANGELOG: new 0.3.0-alpha.1 entry.
- SKILL.md: frontmatter version + stable_version, description expanded.
- context7.json: description expanded with DNS TXT surface.

Backward compatibility:

- v0.2.0 verifiers ignore TXT records entirely.
- v0.3.0 publishers can adopt at their own pace without breaking older
  verifiers.
- All existing 130 tests still pass unchanged.

Threat model — defends against:

- HTTPS-origin compromise (compromised hosting account, expired domain
  not removed from CDN, ACME ownership-validation bypass).
- TLS cert mis-issuance (rogue or coerced CA issues a cert for the
  publisher's domain to an attacker).
- CDN cache-poisoning of the static .well-known asset.

Does NOT defend against joint compromise of HTTPS + DNS or targeted
DNS hijack at the verifier (use DNSSEC, DoH/DoT, or pinned recursive
resolvers in high-stakes deployments).
v0.3.0-alpha.1 (P1.2): DNS TXT cross-verification at _agentpin.{domain} (Rust)
Brings AgentPin to four-language parity (Rust, JavaScript, Python, Go).
The Go SDK lives at github.com/ThirdKeyAi/agentpin/go and mirrors the
v0.2.0 stable surface of the Rust crate. v0.3.0-alpha.1 features (A2A
AgentCard types, DNS TXT cross-verification) are intentionally NOT
included — they will follow in a separate alpha PR after the Rust PRs
land.

Package layout (mirrors SchemaPin's Go SDK conventions):

  go/pkg/{crypto,jwk,jwt,types,discovery,credential,verification,
          revocation,pinning,delegation,mutual,nonce,bundle,resolver}/
  go/cmd/agentpin/   keygen | issue | verify | bundle subcommands
  go/internal/version/version.go   declares 0.2.0 (matches Rust/JS/Python)

Security guarantees:

* ES256 only. The JWT verifier rejects every algorithm except ES256
  and every typ except agentpin-credential+jwt before any signature
  work happens. crypto/ecdsa is used directly — no third-party JWT
  dependency with permissive alg defaults. Algorithm-rejection tests
  cover none, HS256, RS256, ES384, and empty alg.

* Wire format compatibility. JWK thumbprint (RFC 7638), discovery
  documents, credentials, revocation lists, and trust bundles all
  round-trip byte-identically with the Rust SDK. The cross-language
  interop tests under go/pkg/verification/cross_language_test.go load
  Rust-generated PEM / JWK / discovery / JWT fixtures and prove the
  Go SDK can verify them; the test suite also includes a Go-issued
  credential cycle that re-verifies the bidirectional path.

* 12-step verification flow preserved verbatim from the Rust crate.
  The package doc comment in go/pkg/verification/verification.go
  enumerates all twelve steps and notes that any drift here must be
  mirrored in Rust / JS / Python.

Tests: 106 Go tests pass, all packages green, go vet clean, gofmt -l
empty. Rust workspace (135 tests) still passes. End-to-end CLI test
confirmed bidirectional interop: Rust CLI verifies a Go-issued
credential and vice versa.

CI:

* New .github/workflows/go.yml runs gofmt / go vet / go test on every
  PR touching go/** across Go 1.21 and 1.22.
* The version-consistency check in .github/workflows/release.yml is
  extended to also validate go/internal/version/version.go.

Documentation:

* go/README.md — install, quickstart, API reference table mapping Go
  symbols to the Rust equivalents, security guarantees, and a manual
  fixture-regeneration recipe.
* Top-level README.md — Go added to SDK list and project structure.
* SKILL.md — Go quickstart section in the same shape as the existing
  JS/Python sections; language API reference table extended.
* context7.json — description and folders updated; *.go added to
  excludeFiles.
* CHANGELOG.md — Unreleased section documenting the new SDK above
  the existing 0.2.0 entry. No existing entries changed.
JavaScript and Python SDK ports of the v0.3.0-alpha.1 Rust additions, ready
to land alongside the Rust feature branches. All three SDKs interop on the
wire: cards signed in any of Rust/JS/Python verify cleanly in the other two.

Modules added in each SDK:
- a2a   - A2A AgentCard builder + signer + verifier, sorted-key canonical
          JSON that produces byte-identical signing input across languages.
- dns   - _agentpin.{domain} TXT parser + fingerprint matcher; fail-closed
          on mismatch, inert on absent records. Async lookup via Node's
          built-in dns/promises (JS) or optional dnspython (Python).
- resolverLocal / resolver_local - LocalAgentCardStore: in-memory store of
          pre-registered AgentCards keyed by domain. Verifies the extension
          signature at registration and pre-derives a DiscoveryDocument so
          the rest of the verification stack runs unchanged.
- resolverA2a   / resolver_a2a   - A2aAgentCardResolver: fetches
          .well-known/agent-card.json, verifies the extension, cross-checks
          the embedded agentpin endpoint host against the fetched domain.

Discovery additions:
- AllowedDomains helpers (unrestricted / fromDomains / isUnrestricted /
  allows / intersect / fromConstraints). Same semantics as the Rust type:
  empty list = no restriction, unrestricted intersect X = X.
- a2a_endpoint field on discovery documents (optional, omitted when absent
  so v0.2.0 documents round-trip unchanged).

Test results:
- Rust: 135 (unchanged on this branch; the Rust alpha.1 work lives on the
  feature/v0.3.0-{a2a-types,dns-txt,go-sdk} branches and merges into dev
  separately).
- JavaScript: 181 (was 127, +54 new) - cargo fmt + clippy clean.
- Python: 174 (was 123, +51 new).
- Cross-language interop: Python -> JS, JS -> Python, Rust -> JS, Rust ->
  Python all verified end-to-end.

SDK versions remain at 0.2.0 on this branch so the workspace
version-consistency CI check stays green. A coordinated bump can happen
when the Rust alpha.1 PRs and this branch all land in dev.
This release brings four-language parity across Rust, JavaScript, Python,
and Go. Cards signed in any of the four SDKs verify cleanly in the other
three; signature canonicalisation is byte-identical across all
implementations.

Go SDK additions (new at the 0.3.0 surface):

- pkg/types/a2a.go - A2aAgentCard, A2aAgentCapabilities, A2aAgentSkill,
  AgentpinExtension, plus CapabilityToSkill.
- pkg/types/allowed_domains.go - AllowedDomains helper namespace mirroring
  the Rust typed wrapper (Unrestricted, FromDomains, IsUnrestricted,
  Allows, Intersect, FromConstraints).
- pkg/types/discovery.go - new A2aEndpoint field on DiscoveryDocument.
- pkg/a2a - signed AgentCard builder + verifier with sorted-key canonical
  JSON (re-marshal through map[string]interface{} so encoding/json sorts
  keys recursively, producing bytes identical to the Rust SDK's
  BTreeMap-based canonicalisation).
- pkg/dns - _agentpin.{domain} TXT parser + fingerprint matcher + lookup
  via net.Resolver. Same wire format and fail-closed semantics as the
  other SDKs.
- pkg/resolver/local_card.go - LocalAgentCardStore implementing
  DiscoveryResolver. Verifies the extension signature at registration time
  and pre-derives a DiscoveryDocument.
- pkg/resolver/a2a_card.go - A2aAgentCardResolver: HTTPS fetch ->
  extension verification -> endpoint-host cross-check -> derive
  DiscoveryDocument.

Version bumps (0.3.0-alpha.1 -> 0.3.0):

- crates/agentpin/Cargo.toml
- javascript/package.json + javascript/src/index.js
- python/pyproject.toml + python/setup.cfg + python/agentpin/__init__.py
- go/internal/version/version.go

Docs:

- CHANGELOG.md: consolidated 0.3.0 entry covering all four SDKs.
- ROADMAP.md: v0.3.0 marked shipped (2026-05-14); v0.4.0 reframed around
  mutual-auth handshake and hardware-backed keys.
- SKILL.md and context7.json: descriptions updated to reflect four-
  language parity instead of "Rust alpha".

Test results:

- Rust: 179 tests pass (cargo clippy -- -D warnings clean, cargo fmt
  clean).
- JavaScript: 181 tests pass.
- Python: 174 tests pass.
- Go: all packages green (go vet clean, gofmt -l empty).

Cross-language interop verified end-to-end: cards generated in any of
Rust/JS/Python/Go verify in the other three. Version-consistency CI
check passes (all four SDKs report 0.3.0).
The README's Cargo dep example still pointed at "0.2"; this aligns it with
the v0.3.0 release so the rendered crates.io / GitHub page advertises the
correct version on launch.
The repository .gitignore explicitly excludes package-lock.json (the JS
SDK is a library with no runtime deps; only devDependencies are used for
tests/lint), but the existing JS CI workflow used `npm ci` which
requires a committed lockfile and fails before tests even start.

Switch to `npm install --no-audit --no-fund`. Resolves the test (18)
and test (20) PR-check failures.
clap_builder 4.6.0 declared edition = "2024" which only resolves on
Rust 1.85+, breaking the agentpin-cli crate's declared MSRV of 1.70.
Pin to clap ~4.5 (4.5.x line) until either the project bumps MSRV or
clap_builder drops the edition 2024 requirement.

This is the v0.3.0 release-blocker fix for the test (1.70) CI step.
Downstream ecosystem crates have moved to edition 2024 — observed in CI:
`getrandom 0.4.2` and `clap_builder 4.6.0` both declare
`edition = "2024"`, which only resolves on Rust 1.85+. The previously
declared 1.70 floor was therefore unbuildable from a fresh dep graph,
making the test (1.70) row of the Rust CI matrix unreachably red.

Bump MSRV across all three workspace crates (`agentpin`,
`agentpin-cli`, `agentpin-server`), update the CI matrix from `1.70`
to `1.85`, and add a CHANGELOG note under 0.3.0 "Changed".

Also revert the temporary `clap = "~4.5"` pin from the previous commit —
the unpinned clap 4 family now resolves cleanly on MSRV 1.85.
Auto-fix from `eslint --fix`. Four error-level violations of the
`quotes` rule (require single quotes) in dns.js and transport.js were
blocking `npm publish` (the JS `prepublishOnly` hook runs `npm test &&
npm run lint`). Tests still pass — only the literal quote style
changed.
The previous 1.85 bump unblocked edition-2024 deps but the next batch of
ecosystem crates (icu_collections, icu_locale_core, icu_normalizer,
icu_normalizer_data, icu_properties, icu_properties_data, icu_provider,
idna_adapter — all transitive deps of hickory-resolver / url parsing)
declare rust-version = "1.86". Bump MSRV one more notch so the
test (1.86) CI row resolves.
@jaschadub jaschadub merged commit 55821c8 into main May 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant