Runtime Discovery
Runtime Discovery is a service that watches container runtimes (Docker, Kubernetes) for workloads and provides a gRPC API for querying them. In addition, it also resolves workloads using various resolvers (A2A, OASF) to extract and provide details about the workload's capabilities.
Architecture
The system is split into two independent components:
flowchart LR
subgraph Discovery["Discovery (discovery/)"]
RA["Runtime Adapters<br>• Docker<br>• Kubernetes<br>• Process"]
RES["Resolvers<br>• A2A<br>• OASF"]
SW["Store Writer"]
end
subgraph Server["Server (server/)"]
SPACE[" "]
API["gRPC API<br>/ListWorkloads"]
end
RA --> RES
RES --> SW
SPACE ~~~ API
Discovery ~~~ Server
SW -- write/manage --> SB["Storage Backend<br>• etcd (distributed)<br>• Kubernetes CRDs (native)"]
SB -- read/watch --> API
style SPACE fill:none,stroke:none
Components
Discovery (discovery/)
The discovery component is responsible for:
-
Watching runtimes for workloads with the
org.agntcy/discover=truelabel. Supported runtimes:- Docker: Watches Docker daemon for labeled containers.
- Kubernetes: Watches Kubernetes API for labeled pods/services.
- Process: Watches a directory of workload descriptor files written by processes running directly on the host.
- Extensible architecture allows adding more runtimes in the future.
-
Resolving workload metadata using configurable resolvers:
- A2A resolver: Extracts A2A agent card from workloads with
org.agntcy/agent-type=a2alabel. - OASF resolver: Resolves OASF records from Directory for workloads with
org.agntcy/agent-record=<fqdn>label. - Extensible architecture allows adding more resolvers in the future.
- A2A resolver: Extracts A2A agent card from workloads with
-
Writing workloads to the storage backend (etcd or CRDs).
The storage backend can be used to expose discovered workloads to other components (e.g., clients/servers) without coupling them directly and to reduce attack surface.
Choose the storage backend based on where the components run:
- etcd is recommended in non-Kubernetes environments where discovery and server run as separate services (e.g. Docker Compose or across hosts), since both only need network access to etcd.
- CRDs can be used in Kubernetes environments for a more native experience, so clients can query workloads via both gRPC and the Kubernetes API.
- SQLite (
sqlite) suits a single host where discovery and server run as local binaries, e.g. a developer machine. They share a database file instead of requiring an etcd instance, so both must be configured with the same path. The file is opened in WAL mode, so several processes can use it at once: readers don't block the writer, and writes are serialized (one writer at a time, others wait up to 5s). This only works for processes on the same host using a local filesystem; don't put the file on a network filesystem (e.g. NFS) or share it between containers through Docker Desktop bind mounts, where file locking is unreliable. Use etcd in those cases.
flowchart LR
n1["Runtime Discovery"] --> n3["Runtime Server (for querying workloads via gRPC)"] & n4["CRD (for querying workloads via Kubernetes API)"]
n1@{ shape: rect}
Server (server/)
The server component provides a gRPC API for querying discovered workloads.
Example Setup
Build Container Images
IMAGE_TAG=latest task build
Docker Compose
# Deploy the stack
docker compose -f install/docker/docker-compose.yml up -d
# Deploy example workloads
docker compose -f install/examples/docker-compose.yml up -d
# Add discovery to all networks (required for resolvers to work)
docker network connect examples_team-a runtime-discovery
docker network connect examples_team-b runtime-discovery
docker restart runtime-discovery
# Query the API
grpcurl -plaintext localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads
# Cleanup
docker compose -f install/docker/docker-compose.yml down
docker compose -f install/examples/docker-compose.yml down
Kubernetes
The Helm chart supports both CRD and etcd storage backends.
Setup KIND Cluster
# Create cluster
kind create cluster --name runtime
# Load images into KIND
kind load docker-image ghcr.io/agntcy/dir-runtime-discovery:latest --name runtime
kind load docker-image ghcr.io/agntcy/dir-runtime-server:latest --name runtime
Deploy Example Workloads
kubectl apply -f install/examples/k8s.workloads.yaml
Deploy with CRD Storage
# Install the chart with CRD storage (default)
helm install runtime install/chart/
# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=discovery --timeout=60s
kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=server --timeout=60s
# Query the gRPC API
kubectl port-forward svc/runtime-server 8080:8080 &
grpcurl -plaintext localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads
# Query the Kubernetes API to see discovered workloads
kubectl get dw
Deploy with etcd Storage
# Install the chart with etcd storage
helm install runtime install/chart/ \
--set etcd.enabled=true \
--set discovery.config.store.type=etcd \
--set server.config.store.type=etcd
# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=etcd --timeout=60s
kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=discovery --timeout=60s
kubectl wait --for=condition=ready pod -l app.kubernetes.io/component=server --timeout=60s
# Query the gRPC API
kubectl port-forward svc/runtime-server 8080:8080 &
grpcurl -plaintext localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads
Cleanup
kind delete cluster --name runtime
Process (Host)
The process runtime discovers agents running directly on a host. Each process announces itself by writing a JSON descriptor file to a watched directory, and is removed when the file is deleted or the process exits. Discovery runs as a host binary, since it must see host PIDs (this does not work from a container on Docker Desktop).
Descriptor Format
One file per process at <dir>/<id>.json; the file name without .json is the workload ID. Descriptors must be regular files of at most 64 KiB; symlinks and other file types are ignored. See install/examples/process/hello-agent.json.
| Field | Required | Description |
|---|---|---|
name |
yes | Workload name |
pid |
yes | Process ID; the workload is listed only while this process is alive |
locators |
no | Endpoints the process can be reached at, each {"protocol": ..., "url": ...} with a full URL, e.g. {"protocol": "slim", "url": "slim://org/namespace/agent"} |
ports |
no | Ports the process listens on (strings or numbers); turned into tcp://<address>:<port> locators |
addresses |
no | Addresses for ports; defaults to ["127.0.0.1"] |
labels |
no | Must include org.agntcy/discover=true; resolver labels work as for containers |
annotations |
no | E.g. org.agntcy/agent-record |
Discovery sets runtime and type to process, hostname to the host name, and isolationGroups to ["host"].
Locators
Every workload lists the endpoints it can be reached at in locators, each with a protocol and a full URL. For a process, they come from the descriptor's locators, plus one tcp locator per address and port. An agent that is not reachable over an address and port (for example one served over SLIM) only lists its locator. The A2A resolver probes tcp, http and https locators for an agent card and skips other protocols, while the OASF resolver still resolves org.agntcy/agent-record.
Trust
A descriptor is an unauthenticated claim: discovery checks that the PID is alive, not that the process is the agent its descriptor or record names. Anyone who can write to the directory can register a workload, so keep it private to the user (discovery creates it with mode 0700). Consumers that need to know an instance is genuine must verify it themselves, e.g. by probing the agent and checking a reply signed with the key its record names (see #94).
Write descriptors atomically (write a dotfile or <id>.json.tmp, then rename it to <id>.json) and delete them on clean shutdown. Discovery never modifies or removes descriptor files.
Run Locally
# Start a toy A2A agent serving an agent card on port 9999
mkdir -p /tmp/hello-agent/.well-known
echo '{"name": "Hello Agent", "description": "Example agent"}' > /tmp/hello-agent/.well-known/agent-card.json
python3 -m http.server 9999 --directory /tmp/hello-agent &
AGENT_PID=$!
# Create the descriptor directory, private to the user (discovery also creates it on start)
mkdir -p -m 700 ~/.agntcy/dir-runtime/workloads.d
# Start discovery and server, sharing a SQLite store
DISCOVERY_RUNTIME_TYPE=process DISCOVERY_STORE_TYPE=sqlite DISCOVERY_RESOLVER_OASF_ENABLED=false \
go -C discovery run ./cmd &
SERVER_HOST=127.0.0.1 SERVER_STORE_TYPE=sqlite go -C server run ./cmd &
# Announce the agent
cat > ~/.agntcy/dir-runtime/workloads.d/hello-agent.json <<EOF
{
"name": "hello-agent",
"pid": $AGENT_PID,
"labels": {"org.agntcy/discover": "true", "org.agntcy/agent-type": "a2a"},
"ports": ["9999"]
}
EOF
# Query the API (services.a2a holds the agent card)
grpcurl -plaintext localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads
# Stop the agent; the workload disappears within the poll interval
kill $AGENT_PID
To resolve OASF records from a local Directory (dirctl daemon start), drop DISCOVERY_RESOLVER_OASF_ENABLED=false, set DIRECTORY_CLIENT_SERVER_ADDRESS=localhost:8888 and DIRECTORY_CLIENT_AUTH_MODE=insecure, and add "org.agntcy/agent-record": "<cid or name:version>" to the descriptor's labels or annotations.
The server is bound to 127.0.0.1 here because it has no authentication: with the default SERVER_HOST=0.0.0.0 anyone who can reach the machine can list its workloads. To share workloads with other machines, put transport security and access control in front of the server, and list locators (or addresses and ports) in the descriptors that those machines can reach (the 127.0.0.1 default is only reachable locally).
The walkthrough uses SQLite so no extra service is needed; etcd works the same way by running etcd and setting DISCOVERY_STORE_TYPE=etcd and SERVER_STORE_TYPE=etcd.
Docker and process discovery on the same machine can share one store and one server: on startup each instance only removes stale workloads of its own runtime. See Sharing a Store for several instances of the same runtime.
Sharing a Store
Several discovery instances can write to the same store, and one server then lists all their workloads. On startup, discovery removes stored workloads its runtime no longer reports, but only those it owns: workloads of the same runtime type and the same instance ID.
- Different runtimes (e.g. Docker and process discovery on one machine) need no configuration.
- Several instances of the same runtime (e.g. Docker discovery on two hosts writing to one etcd) must each set a unique, stable
DISCOVERY_INSTANCE_ID. Discovery records it on every workload it writes in theorg.agntcy/discovery-instanceannotation. The annotation is reserved: a value supplied by the workload itself (e.g. in a process descriptor or pod annotations) is replaced, or removed when no instance ID is set. Do not derive it from a container or pod hostname, since those change when the container is recreated and the new instance would no longer clean up the old one's workloads.
Workload IDs must be unique across all instances sharing a store; an instance that reports an existing ID overwrites that workload. Container IDs and pod UIDs are unique in practice; for the process runtime, use descriptor file names that are unique across hosts (e.g. a UUID or <host>-<agent>).
Workload Labels
Workloads are discovered based on labels. The discovery component watches for workloads with specific labels and processes their metadata.
Discovery Labels
| Label | Runtime | Description |
|---|---|---|
org.agntcy/discover=true |
Kubernetes | Marks a pod/service for discovery |
org.agntcy/discover=true |
Docker | Marks a container for discovery |
org.agntcy/discover=true |
Process | Marks a process descriptor for discovery |
Resolver Labels
Resolvers extract metadata from discovered workloads based on their labels. They provide additional information about the workload's capabilities.
| Label/Annotation | Description |
|---|---|
org.agntcy/agent-type=a2a |
Enables A2A resolver - fetches A2A agent card from workload |
org.agntcy/agent-record=<fqdn> |
Enables OASF resolver - resolves record from Directory (e.g., my-agent:v1.0.0) |
To configure OASF resolver, the Directory client must be set up using environment variables (e.g., DIRECTORY_CLIENT_SERVER_ADDRESS, DIRECTORY_CLIENT_AUTH_MODE).
Workload Services
Discovered workloads have a services field that holds metadata extracted by resolvers:
{
"id": "4467371c-84fd-4683-ab30-93895d78bab7",
"name": "service-a2a",
"hostname": "service-a2a",
"runtime": "kubernetes",
"type": "pod",
"labels": {
"app": "service-a2a",
"org.agntcy/discover": "true",
"org.agntcy/agent-type": "a2a",
"org.agntcy/agent-record": "my-agent:1.0.0"
},
"locators": [
{"protocol": "tcp", "url": "tcp://10-244-0-9.team-a.pod:8080"},
{"protocol": "tcp", "url": "tcp://10-244-0-9.team-a.pod:9999"}
],
"isolationGroups": [
"team-a"
],
"services": {
"a2a": {
"name": "My Agent",
"description": "...",
"capabilities": [...]
},
"oasf": {
"cid": "baf123",
"name": "my-agent:1.0.0",
"record": {
"name": "my-agent",
"version": "1.0.0",
"skills": [...]
}
}
}
}
Configuration
Discovery Component
| Environment Variable | Description | Default |
|---|---|---|
DISCOVERY_WORKERS |
Number of resolver workers | 16 |
DISCOVERY_INSTANCE_ID |
Instance ID that scopes workload cleanup when several instances of the same runtime share a store (see Sharing a Store) | `` |
DISCOVERY_STORE_TYPE |
Storage type (etcd, crd, sqlite) |
etcd |
DISCOVERY_STORE_ETCD_HOST |
etcd server hostname | localhost |
DISCOVERY_STORE_ETCD_PORT |
etcd server port | 2379 |
DISCOVERY_STORE_ETCD_USERNAME |
etcd username for authentication | `` |
DISCOVERY_STORE_ETCD_PASSWORD |
etcd password for authentication | `` |
DISCOVERY_STORE_ETCD_DIAL_TIMEOUT |
Timeout for connecting to etcd | 5s |
DISCOVERY_STORE_ETCD_WORKLOADS_PREFIX |
etcd key prefix for workloads | /discovery/workloads/ |
DISCOVERY_STORE_CRD_NAMESPACE |
Namespace to store workloads in | default |
DISCOVERY_STORE_CRD_KUBECONFIG |
Path to kubeconfig file (empty for in-cluster) | `` |
DISCOVERY_STORE_CRD_RESYNC_PERIOD |
How often to resync the cache from the API server | 30s |
DISCOVERY_STORE_SQLITE_PATH |
SQLite database file shared with the server (~ expands to home) |
~/.agntcy/dir-runtime/workloads.db |
DISCOVERY_RUNTIME_TYPE |
Runtime type (docker, kubernetes, process) |
docker |
DISCOVERY_RUNTIME_DOCKER_HOST |
Docker daemon socket path | unix:///var/run/docker.sock |
DISCOVERY_RUNTIME_DOCKER_LABEL_KEY |
Label key to filter containers | org.agntcy/discover |
DISCOVERY_RUNTIME_DOCKER_LABEL_VALUE |
Label value to filter containers | true |
DISCOVERY_RUNTIME_KUBERNETES_KUBECONFIG |
Path to kubeconfig file (empty for in-cluster) | `` |
DISCOVERY_RUNTIME_KUBERNETES_NAMESPACE |
Namespace to watch (empty for all namespaces) | `` |
DISCOVERY_RUNTIME_KUBERNETES_LABEL_KEY |
Label key to filter pods | org.agntcy/discover |
DISCOVERY_RUNTIME_KUBERNETES_LABEL_VALUE |
Label value to filter pods | true |
DISCOVERY_RUNTIME_PROCESS_DIR |
Directory containing workload descriptor files (~ expands to home) |
~/.agntcy/dir-runtime/workloads.d |
DISCOVERY_RUNTIME_PROCESS_POLL_INTERVAL |
How often to rescan the descriptor directory | 2s |
DISCOVERY_RUNTIME_PROCESS_LABEL_KEY |
Label key to filter process descriptors | org.agntcy/discover |
DISCOVERY_RUNTIME_PROCESS_LABEL_VALUE |
Label value to filter process descriptors | true |
DISCOVERY_RESOLVER_A2A_ENABLED |
Enable A2A resolver | true |
DISCOVERY_RESOLVER_A2A_TIMEOUT |
Timeout for A2A discovery | 5s |
DISCOVERY_RESOLVER_A2A_PATHS |
Comma-separated list of paths to probe for A2A discovery | /.well-known/agent-card.json,/.well-known/card.json |
DISCOVERY_RESOLVER_A2A_LABEL_KEY |
Label key to identify A2A workloads | org.agntcy/agent-type |
DISCOVERY_RESOLVER_A2A_LABEL_VALUE |
Label value to identify A2A workloads | a2a |
DISCOVERY_RESOLVER_OASF_ENABLED |
Enable OASF resolver | true |
DISCOVERY_RESOLVER_OASF_TIMEOUT |
Timeout for OASF resolution | 5s |
DISCOVERY_RESOLVER_OASF_LABEL_KEY |
Label key to identify OASF workloads | org.agntcy/agent-record |
Discovery OASF Resolver
The OASF resolver requires Directory client configuration via environment variables.
These are the same as those used by the Directory client library, e.g. DIRECTORY_CLIENT_SERVER_ADDRESS.
Refer to the Directory Client documentation for all available options.
When a workload has the configured OASF resolver label, the resolver attempts to fetch the corresponding record from Directory and validate its signature before attaching it to the workload.
Server Component
| Environment Variable | Description | Default |
|---|---|---|
SERVER_HOST |
Server bind address | 0.0.0.0 |
SERVER_PORT |
Server listen port | 8080 |
SERVER_STORE_TYPE |
Storage type (etcd, crd, sqlite) |
etcd |
SERVER_STORE_ETCD_HOST |
etcd server hostname | localhost |
SERVER_STORE_ETCD_PORT |
etcd server port | 2379 |
SERVER_STORE_ETCD_USERNAME |
etcd username for authentication | `` |
SERVER_STORE_ETCD_PASSWORD |
etcd password for authentication | `` |
SERVER_STORE_ETCD_DIAL_TIMEOUT |
Timeout for connecting to etcd | 5s |
SERVER_STORE_ETCD_WORKLOADS_PREFIX |
etcd key prefix for workloads | /discovery/workloads/ |
SERVER_STORE_CRD_NAMESPACE |
Namespace to read workloads from | default |
SERVER_STORE_CRD_KUBECONFIG |
Path to kubeconfig file (empty for in-cluster) | `` |
SERVER_STORE_CRD_RESYNC_PERIOD |
How often to resync the cache from the API server | 30s |
SERVER_STORE_SQLITE_PATH |
SQLite database file shared with discovery (~ expands to home) |
~/.agntcy/dir-runtime/workloads.db |
SERVER_OASF_SCHEMA_URLS |
Comma-separated OASF schema servers whose A2A card extensions are read when filtering by agent; https, or plain http on a loopback host (e.g. a local schema server) |
https://schema.oasf.outshift.com |
gRPC API
The server exposes a gRPC API defined in proto/agntcy/dir/runtime/v1/discovery_service.proto.
GetWorkload
Get a specific workload by ID, name, or hostname.
grpcurl -plaintext -d '{"id": "my-service"}' \
localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/GetWorkload
ListWorkloads
Stream workloads, optionally filtered. Filters of different kinds combine with AND; a filter with several values matches any of them.
| Field | Matches |
|---|---|
labels |
Workload labels; values are regular expressions matched anywhere in the label value |
runtimeTypes |
runtime (docker, kubernetes, process) |
reachableFromWorkloadId |
Workloads sharing an isolation group with that workload, within the same runtime and discovery instance, excluding it |
locatorProtocols |
A locator's protocol (slim, grpc, http, tcp, …) |
names |
The agent's name: the A2A card's name, else the OASF record's |
skills, domains |
The agent's OASF skills and domains |
authors |
The DID of the human vouching for the agent (A2A card), else the OASF record's authors |
schemaVersions |
The OASF schema version of the skills and domains |
Names, skills, domains, authors and schema versions match as in Directory record search: exactly (case-insensitive), or with * and ? wildcards, e.g. natural_language_processing/*.
The agent filters read what the workload says about the agent it runs:
- A2A card (
services.a2a): thediscoveryextension (skills,domains,schema_version) and theidentityextension (human_did) incapabilities.extensions, recognised by auriof the form<schema URL>[/<version>]/objects/discovery(or/objects/identity) on a schema server listed inSERVER_OASF_SCHEMA_URLS. Extensions from other servers are ignored: dir-runtime doesn't validate the payload, so the list keeps workloads on one vocabulary. - OASF record (
services.oasf), resolved fromorg.agntcy/agent-record, for whatever the card doesn't provide. A record the Directory doesn't know (for example one kept on a private node) is not an error: the workload is listed without it, and the resolver doesn't retry.
When both are present, the card wins. Values are claims made by whoever deployed the workload; nothing is verified (see #94).
# List all workloads
grpcurl -plaintext -d '{}' \
localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads
# Filter by labels (supports regex)
grpcurl -plaintext -d '{"labels": {"org.agntcy/agent-type": "a2a"}}' \
localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads
# Agents with a debugging skill reachable over SLIM
grpcurl -plaintext -d '{"skills": ["software_engineering/code_debugging/*"], "locatorProtocols": ["slim"]}' \
localhost:8080 agntcy.dir.runtime.v1.DiscoveryService/ListWorkloads