Native library reference. Extended CLI examples use
jacs-compat. This source reference can include unreleased changes; see the package guide for installation and MCP profiles for available tools.
A2A Interoperability
The default discovery adapters retain the legacy A2A 0.4.0 card shape.
The separate typed jacs::a2a::v1 API uses the A2A 1.0 shape for a bounded
JSON-RPC profile; see the
current source quickstart
and the v1 example below.
Source and published packages: Version 0.16.1 has a recorded release inventory. Version 0.16.0 is the first release whose source includes the native PQ DID/JWK and Agent Card behavior. See the identity integration guide.
Use A2A when your agent needs to be discoverable and verifiable by another service, team, or organization. This is the cross-boundary story; MCP is the inside-the-app story.
What JACS Adds To A2A
- Agent Cards with JACS provenance metadata
- Signed artifacts such as
a2a-taskora2a-message - Trust policy for deciding whether another agent is acceptable
- Chain of custody via parent signatures
The Core Flow
1. Create An Unsigned Card Model
These convenience methods return unsigned models. For signed discovery use the serving APIs below or the A2A how-to.
Python:
from jacs.client import JacsClient
client = JacsClient.quickstart(name="my-agent", domain="my-agent.example.com")
card = client.export_agent_card()
Node.js:
import { JacsClient } from '@hai.ai/jacs/client';
const client = await JacsClient.quickstart({
name: 'my-agent',
domain: 'my-agent.example.com',
});
const card = client.exportAgentCard();
2. Serve Discovery Documents
Python and Node provide discovery-only server helpers. These mounts publish
well-known documents; they do not implement A2A message/task handling or the
legacy optional /jacs/sign and /jacs/verify host endpoint examples in the
extension descriptor. A host application must implement and authorize any
callable interface separately. Discovery does not grant remote signing access.
Quick demo server:
from jacs.a2a import JACSA2AIntegration
JACSA2AIntegration.quickstart(
name="my-agent",
domain="my-agent.example.com",
).serve(port=8080)
Production FastAPI mounting:
from jacs.a2a_server import create_a2a_app, jacs_a2a_routes
app = create_a2a_app(client, title="My A2A Agent")
# or:
# app.include_router(jacs_a2a_routes(client))
Node.js has two discovery helpers:
client.getA2A().listen(port)for a minimal demo serverjacsA2AMiddleware(client, options)for mounting discovery routes in an existing Express app
import express from 'express';
import { jacsA2AMiddleware } from '@hai.ai/jacs/a2a-server';
const app = express();
app.use(jacsA2AMiddleware(client));
app.listen(3000);
The local listening port does not rewrite the signed public interface URL. Configure the advertised domain before generating the signed card and provide the host service that interface describes. A reachable discovery route alone does not prove that message/task operations exist.
3. Sign And Verify Artifacts
Python:
signed = client.sign_artifact({"artifactId": "a-1", "operation": "classify"}, "artifact")
result = client.get_a2a().verify_wrapped_artifact(signed)
assert result["valid"]
Node.js:
const signed = await client.signArtifact(
{ artifactId: 'a-1', operation: 'classify' },
'artifact',
);
const result = await client.verifyArtifact(signed);
console.log(result.valid);
Both SDKs require the canonical native A2A verifier for an affirmative result.
The generic verify_response() / verifyResponse() document API is not an A2A
fallback: even a literal success boolean yields an invalid result with blank
signer, version, type, timestamp, and payload provenance, and cannot elevate
trust. This prevents parsed attacker-controlled wrapper fields from being
presented as authenticated A2A evidence.
The Node signing wrapper also validates the native return contract before releasing it: the document must carry complete portable-v2 signature metadata, its payload and type must match the request, and its ordered parent chain must exactly match the caller's input. If no parents were requested, only an absent or empty returned chain is accepted. This prevents a malformed native result from injecting, dropping, reordering, or changing chain-of-custody evidence.
Trust Policies
Trust policy answers a different question from cryptographic verification.
- Trust policy: should this remote agent be admitted?
- Artifact verification: is this specific signed payload valid?
The current policy meanings are:
| Policy | Behavior |
|---|---|
open | Accept all agents without verification |
verified | Require a valid Agent Card JWS against the same-origin JWKS and a durable jacsId:jacsVersion key pin (default). This is origin/key continuity, not proof of the claimed native JACS identity. |
strict | Require an explicitly trusted native JACS root. Direct ML-DSA-87 cards must use that exact key. ES256 cards must also carry the fixed-path compatibility binding, whose native signature, identity/version, binding hash, compatibility JWK/kid, scope, expiry, and signed issuance time all verify. Bindings are accepted for at most seven days after issuedAt (with five minutes of future clock skew), and the verifier durably pins the latest observed hash, kid, and issuedAt; a newer root-signed binding advances the pin and older replays fail closed. |
Lifecycle boundary: The absolute seven-day freshness check also applies on first contact, independently of
expiresAt. Local discovery generation refreshes an authentic binding after six days under the shared issuance lock, preserving its scopes and explicit expiry. Once a newer native-root-signed binding is observed, JACS also rejects rollback to the old hash/kid. Use an earlierexpiresAtwhen old compatibility keys must stop working sooner, and distribute the refreshed binding promptly after rotation or compromise.
verified means the card signature and a durable same-origin key pin verified.
It proves origin/key continuity, not that the self-asserted jacsId is a
real-world or explicitly trusted identity. strict is the identity-bound mode.
Direct-PQ verification checks the version/root-key metadata before TOFU pinning
and now uses raw-key SHA-256 fingerprints. Pins from an earlier unpublished
direct-PQ build may use a lossy fingerprint; they fail closed and are not
automatically reset. Independently verify the identity and exact root before
deliberately replacing such a pin. ES256 pins and explicit trusted-root checks
are unchanged. Native artifact-envelope key discovery
is a separate verification path, not an automatic A2A policy upgrade.
Python
a2a = client.get_a2a()
assessment = a2a.assess_remote_agent(remote_card_json, policy="strict")
if assessment["allowed"]:
result = a2a.verify_wrapped_artifact(artifact, assess_trust=True)
Node.js
const a2a = client.getA2A();
const assessment = a2a.assessRemoteAgent(remoteCardJson);
if (assessment.allowed) {
const result = await a2a.verifyWrappedArtifact(signedArtifact);
}
Bootstrap Patterns
Use the trust store when you want explicit admission:
- Export the agent document with
share_agent()/shareAgent() - Exchange the public key with
share_public_key()/getPublicKey() - Add the remote agent with
trust_agent_with_key()/trustAgentWithKey()
This is the cleanest path into strict policy.
Native post-quantum discovery
Current source defaults to the persisted native ML-DSA-87 root for
generate_well_known_documents. It publishes that root's RFC 9964 AKP JWK and
five discovery documents, with no delegated-key binding. The card's JWS signs
canonical card bytes and its metadata.jacsRootKid identifies the root's JWK
thumbprint. Loading or exporting does not create another signing key. The
legacy card wire shape remains unchanged.
Explicit ES256 compatibility
Agent cards can also be exported with an ES256 signature from the agent's
compatibility key (export_a2a_agent_card in the Rust core and bindings,
feature a2a). This is a card-typed exporter, not a generic JWS API:
- The JWS protected header is pinned to
{"alg": "ES256", "typ": "JOSE", "kid": "<compat-key kid>"}—typisJOSE, notJWT. - The export is gated by the
a2a-agent-cardscope of the native-root-signed compatibility key binding (an identity scope, granted by default). - The card's
metadatacarriesjacsCompatKid,jacsCompatBindingHash, and the fixed same-originjacsCompatBindingPath(/.well-known/jacs-compat-binding.json).
A stock JOSE verifier can check the ES256 signature against the agent's
compatibility JWKS (jacs-compat agent export-jwks), but that proves possession of the
compatibility key only. Tracing the card to the agent's native root requires
verifying the native-root-signed compatibility key binding
(jacs-compat agent export-compat-binding). Calling native
generate_well_known_documents with explicit ES256 publishes the binding at
the fixed path and reuses the same persisted ES256 key
across calls and restarts. An obsolete explicit ring-Ed25519 discovery-key
choice is rejected rather than silently ignored — see the
Security Model.
Under strict, the verifier checks the trusted native root signature, expected
jacsId and jacsVersion, binding content hash, compatibility kid and JWK,
the a2a-agent-card scope, expiry, and a signed issuedAt no more than seven
days old or five minutes in the future. This absolute freshness check applies
on first contact; local discovery generation refreshes an authentic binding at
six days while preserving its scopes and explicit expiry. A self-advertised
JWKS key by itself can never establish the claimed JACS identity.
Current Runtime Differences
- Python and Node.js: the FastAPI and Express routers serve five native
discovery documents by default, including the signed card and JWKS. Explicit
ES256 adds a compatibility binding. They are discovery-only;
serve()andlisten()are convenience hosts, not complete A2A task/message services. - The mounted routers refresh their complete snapshots lazily at six days from local generation (direct root) or signed binding issuance (ES256). HTTP freshness ends at renewal or earlier explicit expiry; separate fetches can still straddle replacement. Verify the related documents together and refetch on a mismatch. See Serve Your Agent Card.
Example Paths In This Repo
archive/native/jacspy/tests/test_a2a_server.pyarchive/native/jacsnpm/src/a2a-server.jsarchive/native/jacsnpm/examples/a2a-agent-example.jsarchive/native/jacs/tests/a2a_cross_language_tests.rs
Opt-in A2A 1.0 peer example
Current source's typed jacs::a2a::v1::export_agent_card signs directly with the
loaded ML-DSA-87 root. It uses the A2A 1.0 supportedInterfaces model and
standard detached signatures[].protected / signature fields, with typed
protobuf presence handling and RFC 8785 canonicalization. Its
verify_root_card verifies the signature, exact trusted root and expected
identity/version for the supported profile. Multiple signatures are accepted:
the helper selects the expected root kid regardless of order, rejects
duplicate kids or invalid entry/header shapes, and grants no authority to the
other signatures. It does not cryptographically verify those other entries or
fetch keys over the network. See the
source library guide.
export_agent_card_es256 is the explicit compatibility alternative. The
retained native-fixture/Python vector harness selects ES256. The current
JavaScript SDK smoke instead tests direct ML-DSA-87; the current Python SDK
smoke uses explicit ES256 plus a separately verified native-root binding. verify_card_binding remains the
compatibility binding check, and its caller must additionally verify the JWS.
Public Node and Python discovery APIs accept explicit A2A 1.0 options with
caller-provided skills, input/output modes and a messaging URL. Without those
options, language discovery remains the legacy 0.4.0 format. See the
current public API examples.
The current official SDK smokes exercise those public APIs with stock JavaScript SDK 1.3.0 (direct ML-DSA-87, Node 24.7+) and stock Python SDK 1.3.0 (explicit ES256 compatibility, Python 3.11+). Each uses actual SDK discovery, signature verification and a finite JSON-RPC report exchange. SDK version and wire version are different numbers. The Python backend does not provide ML-DSA verification.
The broader retained native-fixture/Python vector harness also uses the exact
a2a-sdk==1.3.0 registry package and hashed transitive dependencies. From the
repository root, with Python 3.11+ and uv installed:
uv venv --python python3 /tmp/jacs-a2a-peer-env
uv pip sync --python /tmp/jacs-a2a-peer-env/bin/python archive/native/examples/a2a_v1_peer_requirements.txt
cargo build --locked --manifest-path archive/native/Cargo.toml -p jacs \
--no-default-features --features a2a --example a2a_v1_peer --target-dir target/a2a-v1-example
/tmp/jacs-a2a-peer-env/bin/python archive/native/examples/a2a_v1_peer.py \
--native-bin target/a2a-v1-example/debug/examples/a2a_v1_peer
A disposable native helper creates an encrypted signing identity, a card,
its native-root-signed compatibility binding, and a pre-signed synthetic report.
It removes its private workspace before a separate recipient starts. A finite
loopback host serves the card and a real SDK JSON-RPC interface. The official
client follows that returned interface and receives the report as raw
application/json artifact bytes. The recipient checks exact bytes, uses the
native public-key-only verifier for the document, and separately validates the
native binding against an explicitly supplied trusted root before the official
SDK verifies the ES256 card signature. No email or external network is used.
The new profile puts native identity and binding references in
urn:jacs:provenance-v1 extension parameters, using the current card's standard
signature fields. It preserves explicitly supplied optional false booleans and
omits absent optional fields and ordinary proto defaults. Required strings and
arrays must be nonempty. It rejects caller-supplied binding parameters,
duplicate extension URIs, unsupported capability claims, non-object extension
parameter roots, and empty/null or unsafe numeric extension values instead of silently changing them. Production
interface URLs require HTTPS; explicit loopback HTTP is permitted for this
example. The caller must implement every declared skill; the example implements one report skill.
This deliberately bounded profile records a pinned-peer limitation: its canonicalizer removes explicit optional empty strings, although the protocol's presence rules preserve them. The peer is not patched. Unsupported values are rejected by the new typed boundary, and the example does not claim general A2A conformance. It tests independent SDK card/JWS handling and message transport; native document and root-binding verification still use JACS. The existing legacy Strict discovery parser is not a v1 parser, and this stateless bridge does not establish rollback history. Key attribution, human approval, current contact authority and Agreement-policy acceptance remain separate decisions.
Historical verification on September 15, 2026 used macOS arm64, Rust 1.98.1, Python
3.14.7, SDK 1.1.4 at commit 2d4d3048b245d2af854bad804f0e722ea9febc08,
and the reviewed Rustls 0.23.45 lock. The native profile
regression and existing legacy discovery/restart regression passed. The actual
peer verified three card vectors and received all 1,201 report bytes unchanged;
14 peer/native refusals and seven typed-profile refusals passed, along with
private-workspace and host cleanup. This is selected-feature (a2a, without
default features) example evidence, not an all-platform or general-conformance
certification.