Go SDK
The Directory Go SDK (github.com/agntcy/dir/client) provides a high-level client for
interacting with the Directory gRPC API from Go applications. It is embedded in the main dir
repository alongside the server.
Source: github.com/agntcy/dir/tree/main/client
Package reference: pkg.go.dev/github.com/agntcy/dir/client
Features
| API | Methods |
|---|---|
| Store | Push, PushBatch, PushStream, Pull, PullBatch, PullStream, Lookup, LookupBatch, LookupStream, Delete, DeleteBatch, DeleteStream, PushReferrer, PullReferrer, DeleteReferrer |
| Search | SearchRecords, SearchCIDs (streaming via StreamResult) |
| Routing | Publish, Unpublish, List, SearchRouting |
| Naming | Resolve, GetVerificationInfo, GetVerificationInfoByName |
| Sync | CreateSync, GetSync, ListSyncs, DeleteSync |
| Events | ListenStream (server-streaming via StreamResult) |
| Signing | Sign (local cosign — no dirctl required), Verify, PullSignatures, PullPublicKeys |
Note
The Go SDK performs signing natively via cosign
and does not require the dirctl binary.
Installation
go get github.com/agntcy/dir/client
Requires Go 1.21 or higher.
Configuration
Environment variables
| Variable | Description | Default |
|---|---|---|
DIRECTORY_CLIENT_SERVER_ADDRESS |
Directory server address | 0.0.0.0:8888 |
DIRECTORY_CLIENT_AUTH_MODE |
Auth mode: x509, jwt, tls, oidc, insecure, or empty |
"" (auto-detect) |
DIRECTORY_CLIENT_SPIFFE_SOCKET_PATH |
SPIFFE Workload API socket path | "" |
DIRECTORY_CLIENT_JWT_AUDIENCE |
JWT audience (JWT mode only) | "" |
DIRECTORY_CLIENT_TLS_CA_FILE |
CA certificate file (TLS mode) | "" |
DIRECTORY_CLIENT_TLS_CERT_FILE |
Client certificate file (TLS mode) | "" |
DIRECTORY_CLIENT_TLS_KEY_FILE |
Client key file (TLS mode) | "" |
DIRECTORY_CLIENT_AUTH_TOKEN |
Pre-issued Bearer token (OIDC mode) | "" |
DIRECTORY_CLIENT_OIDC_ISSUER |
OIDC issuer URL | "" |
DIRECTORY_CLIENT_OIDC_CLIENT_ID |
OIDC client ID | "" |
Load config from the environment with WithEnvConfig():
c, err := client.New(ctx, client.WithEnvConfig())
Direct instantiation
import (
"context"
"github.com/agntcy/dir/client"
)
ctx := context.Background()
config := &client.Config{
ServerAddress: "localhost:8888",
}
c, err := client.New(ctx, client.WithConfig(config))
if err != nil {
log.Fatal(err)
}
defer c.Close()
Always call c.Close() to release the gRPC connection and any SPIFFE credential sources.
Auto-detect mode
When AuthMode is empty (the default), the client auto-detects credentials in this order:
- A pre-issued
AuthTokenorOIDCIssuer/OIDCClientIDset inConfig - A valid cached OIDC token from the shared
dirctltoken cache - Falls back to an insecure connection if none of the above are found
Authentication modes
Insecure (local development only)
config := &client.Config{
ServerAddress: "localhost:8888",
AuthMode: "insecure",
}
X.509 / SPIFFE (recommended for production)
config := &client.Config{
ServerAddress: "localhost:8888",
AuthMode: "x509",
SpiffeSocketPath: "unix:///run/spire/agent-sockets/api.sock",
}
JWT / SPIFFE
In JWT mode the server authenticates via its X.509-SVID (TLS) while the client sends a JWT-SVID as a per-RPC credential.
config := &client.Config{
ServerAddress: "localhost:8888",
AuthMode: "jwt",
SpiffeSocketPath: "unix:///run/spire/agent-sockets/api.sock",
JWTAudience: "spiffe://example.org/dir-server",
}
TLS (custom CA / mTLS)
config := &client.Config{
ServerAddress: "directory.example.com:443",
AuthMode: "tls",
TlsCAFile: "/path/to/ca.crt",
TlsCertFile: "/path/to/client.crt",
TlsKeyFile: "/path/to/client.key",
}
OIDC / Bearer token
Pre-issued token (CI / scripts):
config := &client.Config{
ServerAddress: "gateway.example.com:443",
AuthMode: "oidc",
AuthToken: "eyJhbG...",
}
Interactive login — run once via dirctl, then reuse the cached token:
dirctl auth login --oidc-issuer=https://dex.example.com --oidc-client-id=dirctl
// Reuse the cached token automatically (AuthToken left empty)
config := &client.Config{
ServerAddress: "gateway.example.com:443",
AuthMode: "oidc",
OIDCIssuer: "https://dex.example.com",
OIDCClientID: "dirctl",
}
Usage
Store — push and pull records
The Go SDK exposes single-record, batch, and raw-streaming variants for every store operation.
import (
corev1 "github.com/agntcy/dir/api/core/v1"
)
// Push a single record
record := &corev1.Record{
Blob: []byte(`{"name":"my-agent","version":"1.0.0"}`),
}
ref, err := c.Push(ctx, record)
if err != nil {
log.Fatal(err)
}
fmt.Println("CID:", ref.Cid)
// Pull it back
pulled, err := c.Pull(ctx, ref)
// Metadata only (no content download)
meta, err := c.Lookup(ctx, ref)
// Delete
err = c.Delete(ctx, ref)
Batch operations for efficiency:
refs, err := c.PushBatch(ctx, []*corev1.Record{record1, record2})
records, err := c.PullBatch(ctx, refs)
metas, err := c.LookupBatch(ctx, refs)
err = c.DeleteBatch(ctx, refs)
Streaming operations using channels (for large or incremental datasets):
import "github.com/agntcy/dir/client/streaming"
// Build a channel of records to push as they become available
recordsCh := streaming.SliceToChan(ctx, records)
result, err := c.PushStream(ctx, recordsCh)
if err != nil {
log.Fatal(err)
}
for {
select {
case ref := <-result.ResCh():
fmt.Println("pushed:", ref.Cid)
case err := <-result.ErrCh():
log.Println("error:", err)
case <-result.DoneCh():
return
}
}
Routing — publish and discover
import routingv1 "github.com/agntcy/dir/api/routing/v1"
// Publish a CID to the routing layer
err := c.Publish(ctx, &routingv1.PublishRequest{Cid: ref.Cid})
// Stream listed records
resCh, err := c.List(ctx, &routingv1.ListRequest{})
for resp := range resCh {
fmt.Println(resp.Cid)
}
// Search the routing layer (streaming)
searchCh, err := c.SearchRouting(ctx, &routingv1.SearchRequest{Query: "my skill"})
for resp := range searchCh {
fmt.Println(resp.Cid)
}
// Unpublish
err = c.Unpublish(ctx, &routingv1.UnpublishRequest{Cid: ref.Cid})
Search
SearchCIDs and SearchRecords return a StreamResult that uses channels for non-blocking
consumption:
import searchv1 "github.com/agntcy/dir/api/search/v1"
result, err := c.SearchRecords(ctx, &searchv1.SearchRecordsRequest{Query: "my-agent"})
if err != nil {
log.Fatal(err)
}
for {
select {
case resp := <-result.ResCh():
fmt.Println(resp.Record)
case err := <-result.ErrCh():
log.Println("error:", err)
case <-result.DoneCh():
return
}
}
Naming — resolve and verify
// Resolve a name (optionally versioned) to record references
resp, err := c.Resolve(ctx, "my-agent", "1.0.0")
for _, ref := range resp.Refs {
fmt.Println(ref.Cid)
}
// Get verification info by CID
info, err := c.GetVerificationInfo(ctx, ref.Cid)
// Get verification info by name
info, err = c.GetVerificationInfoByName(ctx, "my-agent", "1.0.0")
Events — real-time streaming
import eventsv1 "github.com/agntcy/dir/api/events/v1"
// Listen to all events
result, err := c.ListenStream(ctx, &eventsv1.ListenRequest{})
if err != nil {
log.Fatal(err)
}
for {
select {
case resp := <-result.ResCh():
event := resp.GetEvent()
fmt.Printf("Event: %s - %s\n", event.Type, event.ResourceId)
case err := <-result.ErrCh():
log.Println("stream error:", err)
return
case <-result.DoneCh():
return
case <-ctx.Done():
return
}
}
Filter by event type or label:
result, err := c.ListenStream(ctx, &eventsv1.ListenRequest{
EventTypes: []eventsv1.EventType{
eventsv1.EventType_EVENT_TYPE_RECORD_PUSHED,
eventsv1.EventType_EVENT_TYPE_RECORD_PUBLISHED,
},
LabelFilters: []string{"/skills/AI"},
})
Signing and verification
The Go SDK signs records locally using cosign. No
dirctl binary is required. Signatures and public keys are stored as referrers on the record.
import (
corev1 "github.com/agntcy/dir/api/core/v1"
signv1 "github.com/agntcy/dir/api/sign/v1"
)
// Sign with a local key
resp, err := c.Sign(ctx, &signv1.SignRequest{
RecordRef: ref,
Provider: &signv1.SignRequestProvider{
Request: &signv1.SignRequestProvider_Key{
Key: &signv1.KeySignRequest{
KeyRef: "/path/to/cosign.key",
},
},
},
})
// Sign with OIDC (keyless via Sigstore Fulcio)
resp, err = c.Sign(ctx, &signv1.SignRequest{
RecordRef: ref,
Provider: &signv1.SignRequestProvider{
Request: &signv1.SignRequestProvider_Oidc{
Oidc: &signv1.OIDCSignRequest{},
},
},
})
// Verify locally (fetches referrers and verifies with cosign)
verifyResp, err := c.Verify(ctx, &signv1.VerifyRequest{
RecordRef: ref,
})
fmt.Println("valid:", verifyResp.Success)
// Verify via the server (uses the server's cached result)
verifyResp, err = c.Verify(ctx, &signv1.VerifyRequest{
RecordRef: ref,
FromServer: true,
})
// Fetch raw signatures and public keys
sigs, err := c.PullSignatures(ctx, ref)
keys, err := c.PullPublicKeys(ctx, ref)
Sync
import storev1 "github.com/agntcy/dir/api/store/v1"
// Create a sync policy between two Directory servers
syncID, err := c.CreateSync(ctx, "https://remote.example.com", []string{ref.Cid}, nil)
// List active syncs (streaming)
itemsCh, err := c.ListSyncs(ctx, &storev1.ListSyncsRequest{})
for item := range itemsCh {
fmt.Println(item.SyncId)
}
// Get a specific sync
syncInfo, err := c.GetSync(ctx, syncID)
// Delete a sync
err = c.DeleteSync(ctx, syncID)
Error handling
The client wraps gRPC status errors. Use google.golang.org/grpc/status and
google.golang.org/grpc/codes to inspect them:
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
_, err := c.Pull(ctx, ref)
if err != nil {
st, ok := status.FromError(err)
if !ok {
log.Fatal("non-gRPC error:", err)
}
switch st.Code() {
case codes.NotFound:
fmt.Println("record not found")
case codes.Unavailable:
fmt.Println("server unavailable")
case codes.PermissionDenied:
fmt.Println("authentication failure")
default:
fmt.Printf("gRPC error %s: %s\n", st.Code(), st.Message())
}
}
Prerequisites
- Go 1.21 or higher
- A running Directory server — see Quickstart or Kubernetes Deployment
- For SPIFFE auth modes: a running SPIRE agent