v0.3.0 — A2A AgentCard + DNS TXT, four-language parity#5
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What's in this release
A2A AgentCard extension
A2aAgentCard,AgentpinExtensiontypesBuildAndSignAgentCard/buildAndSignAgentCard/build_and_sign_agent_card/A2aAgentCardBuilderbuilder APIVerifyAgentpinExtension/verifyAgentpinExtension/verify_agentpin_extensionverifierAllowedDomainstyped wrapper for cross-protocol allow-list intersection (consumed by SchemaPin v1.4A2aVerificationContext)a2a_endpointoptional field onDiscoveryDocumentResolvers
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 derivationDNS TXT cross-verification
_agentpin.{domain}IN TXT"v=agentpin1; kid=...; fp=sha256:<hex>"Go SDK
cmd/agentpinCLI withkeygen | issue | verify | bundle | versionTest plan
cargo test --workspace --all-features(179 tests) +cargo clippy -- -D warnings+cargo fmt --checknpm test(181 tests, node:test)pytest(174 tests)go test ./...(16 packages green) +go vet+gofmt -lkeygen → issue → verifyand Go CLI verifies Rust-issued credential (and vice versa)agentpin-serverdiscovery + revocation endpoints respond correctlyLocalAgentCardStoreregisters and resolves cards signed in foreign SDKs (cross-language)