A lease-based registration and discovery service for logical AI agents that publish A2A Agent Cards. A logical agent can expose multiple independently leased runtime instances that share one Agent Card. The server also provides ownership tokens, TTL heartbeats, filtering, pagination, caching, metrics, tests, and an optional distributed etcd store.
This project is a registry for A2A agents. Its registry REST API is intentionally separate from the A2A task/message protocol. The A2A specification standardizes Agent Cards and describes registries/catalogs as a discovery mechanism, but it does not prescribe one universal registry API.
- Stores current A2A 1.0 Agent Cards without stripping unknown fields
- Accepts both v1
supportedInterfaces[].urland the legacy cardurl - Lease/heartbeat model inspired by etcd and Consul TTL checks
- Multiple runtime instances per logical agent, with a shared Agent Card
- Per-instance endpoint, metadata, TTL, lease token, heartbeat, and expiry
- Optional global write bearer token controls who may create registrations
- Discovery by skill, skill tag, capability, protocol binding, or agent name
- Cursor pagination, ETags, readiness/liveness probes, Prometheus text metrics
- Optional active HTTP/TCP health checks with passing, warning, and critical states
- Revision-resumable Server-Sent Events watch API for local resolver updates
- Optional A2A Agent Card JWS verification with trusted issuers, local JWK Sets, and restricted
jkuorigins - OpenTelemetry HTTP spans, bounded-label Prometheus metrics, rate limiting, active-instance quotas, and authenticated backups
- In-memory backend for development and an etcd v3 backend for replicated deployments
- Compatibility aliases for the routes and
ttlMsfield in the original PoC - No web framework; runtime dependencies are the official A2A TypeScript SDK, JOSE, OpenTelemetry API, and Pino structured logger
Requirements: Node.js 22 or newer.
Install the package globally via npm:
npm install -g @a2a-lib/registry-serverStart the registry server using the CLI:
a2a-registryAlternatively, run it directly without global installation using npx:
npx @a2a-lib/registry-serverThe server starts by default at http://localhost:3003 using the in-memory store.
The React dashboard is maintained as the ui Git submodule and can be served on the same port as the registry API:
git clone --recurse-submodules git@github.com:a2a-lib/a2a-registry-server.git
cd a2a-registry-server
npm ci
npm --prefix ui ci
npm run build:all
node dist/cli.js --uiWhen working from an existing clone, initialize the dashboard with git submodule update --init --recursive. The default build path is ui/dist; use --ui-dir <path> or REGISTRY_UI_DIR for a different static build. If the UI build is missing, dashboard requests return a clear 503 response while API and health endpoints remain available.
The CLI accepts configuration flags (which take precedence over environment variables) as well as dotenv-compatible files:
# Start with explicit host, port, and store
a2a-registry --host 127.0.0.1 --port 3003 --store memory
# Load configuration from a .env file
a2a-registry --env-file .env
# Serve the built web dashboard with the API
a2a-registry --ui
# Emit only warnings and errors
a2a-registry --log-level warn
# Inspect all available options
a2a-registry --helpUse --help for all options. SIGINT and SIGTERM trigger a graceful shutdown that stops accepting connections, waits for active requests, and closes the storage backend.
The registry server is available as the published Docker Hub image digicrafts/a2a-registry. The image includes the API, optional web UI, a non-root runtime user, and a readiness healthcheck on port 3003.
Pull a specific release tag for repeatable deployments:
docker pull digicrafts/a2a-registry:0.3.0Run the registry with the in-memory store:
docker run -d \
--name a2a-registry \
--restart unless-stopped \
-p 3003:3003 \
-e REGISTRY_PORT=3003 \
-e REGISTRY_STORE=memory \
digicrafts/a2a-registry:0.3.0The latest tag is also available, but version tags are recommended for production:
docker pull digicrafts/a2a-registry:latestTo enable a write bearer token, pass it at runtime rather than storing it in the image:
docker run -d \
--name a2a-registry \
--restart unless-stopped \
-p 3003:3003 \
-e REGISTRY_PORT=3003 \
-e REGISTRY_STORE=memory \
-e REGISTRY_WRITE_TOKEN=my-secret-token \
digicrafts/a2a-registry:0.3.0For a distributed deployment, use REGISTRY_STORE=etcd and configure ETCD_ENDPOINT, ETCD_PREFIX, and any required etcd credentials. See Distributed deployment with etcd.
You can also build and deploy the registry server as a lightweight container using the included multi-stage Dockerfile:
docker build -t digicrafts/a2a-registry:local .Run the local image:
docker run -d \
--name a2a-registry \
-p 3003:3003 \
-e REGISTRY_PORT=3003 \
-e REGISTRY_STORE=memory \
digicrafts/a2a-registry:localLog in to Docker Hub and publish both a release tag and latest. The following command creates a multi-platform image for Linux AMD64 and ARM64:
docker login
docker buildx create \
--name digicrafts-builder \
--driver docker-container \
--use
docker buildx inspect --bootstrap
docker buildx build \
--platform linux/amd64,linux/arm64 \
--pull \
-t digicrafts/a2a-registry:0.3.0 \
-t digicrafts/a2a-registry:latest \
--push \
.If you only need one architecture, use docker build followed by docker push instead. The Docker daemon must be running before building or running containers.
The container image includes a built-in healthcheck probing http://127.0.0.1:3003/health/ready. You can check container status and logs:
docker ps --filter "name=a2a-registry"
docker logs a2a-registryRegister an agent:
curl -i http://localhost:3003/v1/agents \
-H 'Content-Type: application/json' \
-d '{
"id": "weather-eu-1",
"ttlSeconds": 60,
"healthCheck": { "protocol": "http", "path": "/health", "intervalSeconds": 10 },
"metadata": { "region": "eu-west" },
"agentCard": {
"name": "Weather Agent",
"description": "Returns local forecasts",
"version": "1.0.0",
"supportedInterfaces": [{
"url": "https://weather.example/a2a",
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0"
}],
"capabilities": { "streaming": true },
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json"],
"skills": [{
"id": "forecast",
"name": "Weather forecast",
"description": "Forecast by location",
"tags": ["weather"]
}]
}
}'The create response contains the server-assigned instance.instanceId and a leaseToken. Save the lease token securely: it is returned once and is required to update, renew, or remove the registration.
export AGENT_LEASE_TOKEN='<value from registration response>'
curl -X POST http://localhost:3003/v1/agents/weather-eu-1/heartbeat \
-H "X-Registry-Lease-Token: $AGENT_LEASE_TOKEN"
curl 'http://localhost:3003/v1/agents?skill=forecast&capability=streaming&tag=weather'
curl -X DELETE http://localhost:3003/v1/agents/weather-eu-1 \
-H "X-Registry-Lease-Token: $AGENT_LEASE_TOKEN"Send a heartbeat well before ttlSeconds elapses—normally every one-third of the TTL, with jitter and retry backoff. Registrations that omit instanceId receive a unique UUID from the server. The returned instance.instanceId can be used with the instance-specific routes; the legacy agent-level heartbeat and unregister routes continue to work for a sole instance or when the lease token identifies the instance.
Register named instances with the same logical agent ID and exactly the same Agent Card. Put the instance-specific URL in endpoint; the shared card may advertise a stable load-balancer URL while discovery clients can select from agent.instances directly.
curl -i http://localhost:3003/v1/agents/weather/instances \
-H 'Content-Type: application/json' \
-d '{
"instanceId": "eu-west-1a",
"endpoint": "https://weather-a.example/a2a",
"ttlSeconds": 60,
"metadata": { "zone": "eu-west-1a", "weight": "100" },
"agentCard": { "name": "Weather Agent", "supportedInterfaces": [{ "url": "https://weather.example/a2a" }] }
}'
curl -i http://localhost:3003/v1/agents/weather/instances \
-H 'Content-Type: application/json' \
-d '{
"instanceId": "eu-west-1b",
"endpoint": "https://weather-b.example/a2a",
"ttlSeconds": 60,
"metadata": { "zone": "eu-west-1b", "weight": "100" },
"agentCard": { "name": "Weather Agent", "supportedInterfaces": [{ "url": "https://weather.example/a2a" }] }
}'Each response has a different leaseToken. Heartbeat an instance at /v1/agents/{id}/instances/{instanceId}/heartbeat. Discovery returns one logical agent containing both active records in instances; an instance disappears independently when its lease expires. While multiple instances are active, a registration with a different Agent Card is rejected with 409 agent_card_mismatch.
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/agents |
Register an instance (server-generated UUID when instanceId is omitted) |
GET |
/v1/agents |
Discover logical agents and active instances |
GET |
/v1/watch |
Stream revisioned registry snapshots as Server-Sent Events |
GET |
/v1/agents/{id} |
Fetch one logical agent and its active instances |
POST |
/v1/agents/{id}/instances |
Register a named instance |
GET |
/v1/agents/{id}/instances |
List active instances |
PUT |
/v1/agents/{id}/instances/{instanceId} |
Create or replace a named instance |
GET |
/v1/agents/{id}/instances/{instanceId} |
Fetch a named instance |
POST |
/v1/agents/{id}/instances/{instanceId}/heartbeat |
Renew a named instance lease |
DELETE |
/v1/agents/{id}/instances/{instanceId} |
Unregister a named instance |
PUT |
/v1/agents/{id} |
Register an instance, generating its ID when omitted |
DELETE |
/v1/agents/{id} |
Remove the compatibility instance, or the lease-token-owned sole instance |
POST |
/v1/agents/{id}/heartbeat |
Renew the compatibility instance, or the lease-token-owned sole instance |
GET |
/health/live |
Process liveness |
GET |
/health/ready |
Storage readiness |
GET |
/metrics |
Prometheus text metrics |
GET |
/admin/backup |
Authenticated public registry snapshot export |
GET |
/openapi.yaml |
OpenAPI 3.1 document |
Discovery accepts skill, tag, capability, protocolBinding, name, limit, and cursor. Pagination and total count logical agents, and only logical agents with at least one unexpired instance are returned. The top-level instance fields (endpoint, TTL, timestamps, and metadata) remain as a compatibility projection of the first active instance, preferring an explicitly named default instance; new clients should use instances.
An optional healthCheck on each registration enables server-side HTTP or TCP probes. HTTP checks use the registration endpoint unless path is supplied; TCP checks connect to the endpoint host and port. Health results are returned as instance.health and do not extend the agent-driven TTL lease. The SSE watch endpoint sends an initial snapshot unless after (or Last-Event-ID) is supplied, then emits a new snapshot whenever the registry revision changes.
When Agent Card trust is enabled, each logical agent includes an agentCardTrust status separate from the original agentCard. Verification uses the A2A SDK's JCS/JWS implementation and never adds or removes fields in the signed card. verified means a signature matched a configured trusted JWK and issuer policy; unverified means no signature was present; invalid records a rejected signature when enforcement is disabled. Set REGISTRY_TRUST_REQUIRED=true to reject unverified or invalid registrations. A jku is fetched only over HTTPS when its exact origin is listed in REGISTRY_TRUSTED_JKU_ORIGINS.
The /admin/backup endpoint exports active public registrations, trust status, health state, and revision metadata without lease tokens or backend credentials. It is disabled unless REGISTRY_BACKUP_TOKEN is set and requires that token as a bearer credential. The Prometheus endpoint exposes fixed route/method/status-class counters and duration buckets; request IDs, agent IDs, and arbitrary URLs are never metric labels. HTTP spans use the OpenTelemetry API and become exportable when the hosting process installs an OpenTelemetry SDK/provider.
PoC-compatible aliases remain available at /v1/registry, /v1/registry/register, /v1/registry/agents, and /v1/registry/heartbeat. They use the new ownership rules.
| Variable | Default | Meaning |
|---|---|---|
REGISTRY_HOST |
0.0.0.0 |
Listen address |
REGISTRY_PORT |
3003 |
Listen port |
REGISTRY_PUBLIC_URL |
local port URL | Base URL used in service metadata |
REGISTRY_STORE |
memory |
memory or etcd |
REGISTRY_LOG_LEVEL |
info |
Minimum Pino log level: fatal, error, warn, info, debug, trace, or silent |
REGISTRY_DEFAULT_TTL_SECONDS |
60 |
Lease TTL if omitted |
REGISTRY_MIN_TTL_SECONDS |
10 |
Lowest accepted TTL |
REGISTRY_MAX_TTL_SECONDS |
3600 |
Highest accepted TTL |
REGISTRY_WRITE_TOKEN |
unset | If set, registrations require Authorization: Bearer … |
REGISTRY_CORS_ORIGIN |
* |
CORS allow-origin value |
REGISTRY_MAX_BODY_BYTES |
1048576 |
Maximum JSON body size |
REGISTRY_HEALTH_CHECK_INTERVAL_MS |
1000 |
Scheduler tick for active health checks |
REGISTRY_TRUST_REQUIRED |
false |
Reject registrations without a trusted Agent Card signature |
REGISTRY_TRUSTED_ISSUERS |
unset | Comma-separated protected-header iss allowlist |
REGISTRY_TRUSTED_JKU_ORIGINS |
unset | Comma-separated HTTPS origins allowed for remote JWK Sets |
REGISTRY_TRUSTED_JWKS |
{} |
JSON JWK Set for locally trusted public keys |
REGISTRY_BACKUP_TOKEN |
unset | Enables GET /admin/backup with a bearer token |
REGISTRY_RATE_LIMIT_REQUESTS_PER_MINUTE |
600 |
Per-peer API request refill rate; 0 disables limiting |
REGISTRY_RATE_LIMIT_BURST |
60 |
Per-peer token bucket burst capacity |
REGISTRY_MAX_INSTANCES_PER_AGENT |
0 |
Maximum active instances per logical agent; 0 is unlimited |
REGISTRY_MAX_ACTIVE_INSTANCES |
0 |
Registry-wide active-instance quota; 0 is unlimited |
REGISTRY_UI / REGISTRY_ENABLE_UI |
false |
Serve the built web dashboard |
REGISTRY_UI_DIR |
package ui/dist |
Static dashboard build directory |
ETCD_ENDPOINT |
http://localhost:2379 |
etcd v3 JSON gateway |
ETCD_PREFIX |
/a2a-registry/agents/ |
etcd key prefix |
ETCD_USERNAME, ETCD_PASSWORD |
unset | etcd authentication credentials |
ETCD_BEARER_TOKEN |
unset | Pre-issued etcd auth token |
Operational logs are emitted as newline-delimited JSON through Pino. The --log-level
CLI option overrides REGISTRY_LOG_LEVEL; help and version output remain plain text.
docker compose up --buildThe etcd adapter grants a lease for each runtime instance and attaches that instance's registry key to it. A heartbeat atomically reattaches the key to a new lease and revokes the previous lease. When an instance stops renewing, etcd removes only that instance key even if the registry process that accepted it has failed. All registry replicas must use the same ETCD_PREFIX and cluster.
For production, enable etcd authentication and TLS, use a dedicated least-privilege role restricted to the registry prefix, and run an odd-sized etcd cluster. The current adapter accepts an HTTPS endpoint but does not yet expose custom CA/client-certificate file settings.
REGISTRY_WRITE_TOKENis an enrollment control; enable it outside trusted development networks.X-Registry-Lease-Tokenproves ownership of one runtime instance. Only its SHA-256 hash is stored.- Put TLS and an identity-aware proxy/API gateway in front of the server. A shared write token is not a replacement for OAuth2, workload identity, or mTLS.
- Active health checks intentionally fetch or connect to registered endpoints when configured; restrict registration access and network egress to trusted agents to manage SSRF risk.
- Agent Cards are public discovery metadata. Do not place credentials or internal secrets in them.
- Signed Agent Cards are preserved unchanged. Verification status is recorded separately using the configured issuer, JWK, and
jkutrust policy.
- Identity and policy: OIDC/mTLS identities, tenant namespaces, RBAC, admission policy, and audit events. Bind the authenticated identity to the registered agent ID.
Trust: verify A2A Agent Card JWS signatures, restrictjkuorigins, maintain trusted issuers/keys, and record verification status without modifying the signed card.- Active health checks: add gRPC health probes and richer check policies; HTTP/TCP probes and passing/warning/critical state reporting are available now and remain separate from TTL heartbeats.
- Watch API: add a native etcd watch/gRPC stream for lower-latency cross-replica delivery; the current SSE endpoint provides revisioned snapshots and works with both memory and etcd stores.
- Locality-aware resolution: add first-class zone/region/weight fields, health-aware selection, and optional client-side round-robin helpers. Until then these values can be carried in per-instance metadata.
- Consul adapter: use Consul sessions/TTL checks and KV/catalog metadata when an organization already operates Consul.
Operations: OpenTelemetry traces, labeled/rate metrics with bounded cardinality, rate limiting, quotas, backups, chaos tests, and SLO dashboards.- Governance: moderation/approval workflows, metadata schemas, retention, version compatibility policy, and a documented response to compromised registrations.
npm run check
npm test
# Include concurrent admission tests against a running etcd v3 gateway:
ETCD_TEST_ENDPOINT=http://127.0.0.1:2379 npm test
npm run buildThe memory store is used in unit/integration tests. Add an etcd container test before changing lease behavior.