Skip to content

Add Go SDK (P1.1) — v0.2.0 surface parity - #4

Merged
jaschadub merged 5 commits into
devfrom
feature/v0.3.0-go-sdk
May 1, 2026
Merged

Add Go SDK (P1.1) — v0.2.0 surface parity#4
jaschadub merged 5 commits into
devfrom
feature/v0.3.0-go-sdk

Conversation

@jaschadub

Copy link
Copy Markdown
Contributor

Summary

Brings AgentPin to four-language parity by adding a Go SDK at
github.com/ThirdKeyAi/agentpin/go, mirroring the v0.2.0 stable surface
of the Rust crate. This addresses the P1.1 priority from the cross-stack
gap analysis: a Go SDK is the missing fourth language alongside Rust,
JavaScript, and Python.

Scope

v0.2.0 surface only. The two in-flight v0.3.0-alpha.1 features
(A2A AgentCard types and DNS TXT cross-verification, currently in PRs
#2 and #3) are intentionally NOT included in this PR. They will
follow in a separate Go alpha PR after the corresponding Rust PRs
merge — keeping this change focused on bringing Go to v0.2.0 parity.

Package layout

Mirrors SchemaPin's Go SDK conventions:

go/
├── go.mod module github.com/ThirdKeyAi/agentpin/go
├── README.md install, quickstart, API map
├── cmd/agentpin/ CLI: keygen | issue | verify | bundle
├── internal/version/version.go declares 0.2.0 (matches Rust/JS/Python)
└── pkg/
├── crypto/ ECDSA P-256 keypair + DER signature helpers
├── jwk/ PEM ↔ JWK + RFC 7638 thumbprint
├── jwt/ ES256-only JWT (rejects everything else)
├── types/ Shared types (capability, constraint, discovery, etc.)
├── discovery/ Build / validate / fetch discovery documents
├── credential/ IssueCredential + capability subset validation
├── verification/ 12-step VerifyCredentialOffline + resolver variant
├── revocation/ Build / check / fetch revocation documents
├── pinning/ TOFU KeyPinStore with JSON persistence
├── delegation/ Attestation create / verify + chain depth check
├── mutual/ 128-bit nonce challenge / response
├── nonce/ Replay-protection store interface + in-memory impl
├── bundle/ TrustBundle helpers
└── resolver/ WellKnown / LocalFile / TrustBundle / Chain resolvers

Critical security requirements (all met)

  • ES256-only enforcement.jwt.DecodeJWTUnverified rejects every
    algorithm except ES256 and every typ except agentpin-credential+jwt
    before any signature work. We use crypto/ecdsa directly — no
    third-party JWT dependency with permissive alg defaults. Tests
    cover none, HS256, RS256, ES384, and empty-alg cases.
  • Wire format compatibility. RFC 7638 JWK thumbprint, discovery
    documents, credentials, revocation lists, and trust bundles all
    round-trip byte-identically with the Rust SDK. Asserted by
    cross-language interop tests (see below).
  • 12-step verification flow preserved verbatim from the Rust crate
    (documented in the package doc comment).
  • No eval/dynamic code. Rate-limit and domain-pattern matchers
    use closed-vocabulary parsers.

Cross-language interop test results

go/pkg/verification/cross_language_test.go exercises every wire
format boundary using fixtures generated by the Rust CLI:

TestResult
TestCrossLang_PEMtoJWK_MatchesRustPASS
TestCrossLang_JWKThumbprint_MatchesRustPortPASS
TestCrossLang_DiscoveryDocument_RustSerialization_RoundTripPASS
TestCrossLang_RustKey_GoSignVerifyPASS
TestCrossLang_JWT_RustGenerated_GoDecodeAndSignatureVerifyPASS

End-to-end manual interop test also confirmed that the Rust CLI
verifies a Go-issued credential
(and vice versa), proving the JWT
DER-signature wire format is byte-compatible across SDKs.

QA matrix

CheckStatus
go test ./...PASS — 106 tests across 13 packages, 0 failures
go vet ./...clean
gofmt -l .empty (every file gofmt'd)
cargo test --workspacePASS — 135 Rust tests still pass
cargo clippy --workspaceclean (no warnings)
Bidirectional CLI interopRust verifies Go-issued cred and vice versa

Test count by package

PackageTests
pkg/types23
pkg/crypto5
pkg/jwk5
pkg/jwt11
pkg/discovery7
pkg/credential2
pkg/verification18 (incl. 5 cross-language)
pkg/revocation5
pkg/pinning6
pkg/delegation4
pkg/mutual6
pkg/nonce3
pkg/bundle3
pkg/resolver8
Total106

CI

  • New .github/workflows/go.yml runs gofmt -l, go vet, and
    go test across Go 1.21 and 1.22 on every PR touching go/**.
  • The version-consistency job in .github/workflows/release.yml is
    extended to also validate go/internal/version/version.go so the
    four manifests stay in lockstep.

Documentation

  • go/README.md — install (go install …@latest), 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 the 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.mdUnreleased section documenting the new SDK above
    the existing 0.2.0 entry. No existing entries were changed.

Deviations from the Rust API

Minor, idiomatic-Go surface-only changes:

  • Verification result fields use Go-style PascalCase (AgentID vs
    Rust agent_id) but JSON tags preserve the snake_case wire format.
  • KeyPinStore is goroutine-safe via an internal sync.Mutex
    (Rust's struct is !Sync; callers wrap with Mutex themselves).
  • The WellKnownResolver exposes an injectable *http.Client field
    so callers can plug in their own transport; Rust uses a private
    reqwest::Client built per-call.
  • All other functions, types, JSON field names, and error codes match
    the Rust API symbol-for-symbol.

Follow-ups (not in this PR)

Pre-existing CI infrastructure issues

These are not introduced by this PR but may show up red on the CI
run; flagging for transparency:

  • The Rust CI matrix pins MSRV to 1.70, which currently fails with a
    clap_builder edition-2024 transitive dep error on dependency
    resolution. This is independent of the Go SDK and would be in scope
    for a separate dependency-pin PR.
  • The JavaScript CI workflow runs npm ci, but package-lock.json
    is gitignored at the repo root, so npm ci cannot succeed without
    generating a lockfile. Also out of scope for this PR.

Test plan

  • cd go && go test ./... — 106 tests pass
  • cd go && go vet ./... — clean
  • cd go && gofmt -l . | grep -v '^$' — empty
  • Cross-language interop tests pass (5 tests)
  • cargo test --workspace -j2 — Rust 135 tests still pass
  • cargo clippy --workspace -j2 -- -D warnings — clean
  • Bidirectional CLI interop verified manually (Rust verifies Go
    credential; Go verifies Rust credential)

jaschaduband others added 5 commits May 1, 2026 10:32
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.
@jaschadub
jaschadubforce-pushed the feature/v0.3.0-go-sdk branch from c97c0f4 to e77fca8CompareMay 1, 2026 20:36
@jaschadub
jaschadub merged commit 8c58867 into devMay 1, 2026
3 checks passed
@jaschadub
jaschadub deleted the feature/v0.3.0-go-sdk branch May 16, 2026 04:37
Sign up for freeto 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

@jaschadub