Adding Secrets to a Go Agent
Overview
Secrets are sensitive configuration values (API keys, passwords, tokens) read at runtime through golem.Secret[T]. A secret uses the same config mechanism as regular typed config — it is just a field whose type is golem.Secret[T] inside the agent’s config struct (see golem-add-config-go). The config value carries an opaque secret handle; the plaintext is revealed only when your code calls .Get() or .MustGet().
Manage secrets per environment via the CLI. Use secretDefaults in golem.yaml only for development placeholders; never commit real credentials.
Steps
- Add a
golem.Secret[T]field to the agent’s config struct. - Define / implement the agent as a configured agent (
DefineConfiguredAgent+ImplementConfigured). - Reveal the value at runtime with
.MustGet(), or.Get()to handle a read failure. - Provide values via
secretDefaults(local) or the CLI (per environment).
Declaring a Secret Field
A secret is any field of type golem.Secret[T], at any depth of the config struct:
// Package client is the DEFINITION of an API-client agent.
package client
import "github.com/golemcloud/golem/sdks/go/golem"
type ID struct{ Name string }
type Config struct {
Endpoint string
APIKey golem.Secret[string] // a secret leaf
DB DBConfig
}
type DBConfig struct {
Host string
Password golem.Secret[string] // secret at path db.password
}
var Agent = golem.DefineConfiguredAgent[ID, Config](golem.Spec{
Name: "ClientAgent",
Description: "Calls an external API using a secret key",
})
var Connect = Agent.Method[golem.Unit, string]("connect", golem.Description("Describe the connection using secret values"))Reading a Secret at Runtime
Read the config, then call .MustGet() on the config-backed secret field to reveal the current plaintext. Each config-backed read resolves a fresh handle, so live reads observe rotation; retry/replay uses the recorded revision. A received RPC handle instead stays pinned to its transferred revision. .Get() returns (T, error) instead, for a secret that may be unavailable:
// Package impl is the IMPLEMENTATION of the API-client agent.
package impl
import (
"fmt"
"myapp/agents/client"
"github.com/golemcloud/golem/sdks/go/golem"
)
type state struct{}
var agent = client.Agent.ImplementConfigured(func(_ *golem.InitContext[client.ID, state, client.Config]) *state { return &state{} })
func init() {
agent.Handle(client.Connect, func(ctx *golem.Context[state], _ golem.Unit) string {
cfg := ctx.Config(client.Agent)
apiKey := cfg.APIKey.MustGet() // reveals the current plaintext
dbPass := cfg.DB.Password.MustGet() // secret at any depth
return fmt.Sprintf("connecting to %s (key len=%d, db host=%s, pw len=%d)",
cfg.Endpoint, len(apiKey), cfg.DB.Host, len(dbPass))
})
}A golem.Secret[T] cannot be constructed from a plaintext: it comes from the agent’s config, or from another agent. Its String()/GoString() render as golem.Secret(redacted), so it stays out of logs formatted with %v/%s.
Passing a Secret to Another Agent
A golem.Secret[T] can be a field of a method’s input or its result. It travels as a secret handle, never as plaintext, and the receiver reveals it with .Get() / .MustGet():
var Share = Agent.Method[golem.Unit, golem.Secret[string]]("share")
agent.Handle(Share, func(ctx *golem.Context[state], _ golem.Unit) golem.Secret[string] {
return ctx.Config(Agent).APIKey
})
// in another agent
key := client.Share.MustCall(client.Agent.Get(client.ID{Name: "keeper"}), golem.Unit{}).MustGet()A config secret can be sent again; a secret you received moves when you send it on, and the copy that was sent is unusable afterwards (golem.ErrSecretMoved). External clients cannot carry secrets.
A transferred handle is pinned to one secret revision. Repeated .Get() calls on that received handle do not observe rotation. To give a recipient the rotated value, send a newly resolved handle from the config-backed secret; the recipient must use the new handle.
Secret Defaults in golem.yaml (development only recommended)
For local development, set placeholder defaults under secretDefaults.<environment>. Keys are camelCase and follow the same path flattening as config; nested structs are nested maps. The CLI selects defaults by environment name, with no production exclusion. Keep defaults development-only and never commit real credentials:
secretDefaults:
local:
apiKey: "dev-key-123"
db:
password: "dev-password"Managing Secrets via CLI (per environment)
Outside local defaults, secrets are environment-scoped — each deployment environment has its own values, set through the platform CLI (paths use the same camelCase as the config keys):
golem secret create apiKey --type string --value "sk-abc123"
golem secret create db.password --type string --value "s3cret"
# Without a value option the value is prompted for (hidden input); or pipe it in
printenv DB_PASSWORD | golem secret create db.password --type string --value-stdin
# Create, or update the value if the secret already exists
golem secret create apiKey --type string --value "sk-abc123" --update-existing
# List, update, clear, and delete (delete asks for confirmation; --yes skips it)
golem secret list
golem secret update apiKey --value "new-value"
golem secret update apiKey --unset
golem secret delete apiKeyKey Constraints
- A secret is a config field of type
golem.Secret[T]— the agent must be a configured agent (DefineConfiguredAgent/ImplementConfigured); seegolem-add-config-go. - Only
.Get()/.MustGet()reveal the plaintext. Config-backed live reads resolve fresh handles and observe rotation without restarting the agent; retry/replay uses the recorded revision. Received handles stay pinned to their transferred revision and require a new handle to observe rotation. - Use the CLI for production secret values.
secretDefaultsapplies by environment name, even in production; restrict it to development placeholders and never commit real credentials. - Deployment can succeed with an unset secret; a missing required secret fails agent creation, not deployment. Read a secret from within an invocation only —
.Get()calls the host. It returns an error when the secret is unavailable, its version is gone, or it was moved;.MustGet()panics instead, which traps the component (fails the agent), as in Rust. - Over RPC, secret fields are always platform-provisioned; a definition’s
WithConfigoverrides only local config, never secrets.
Related Guides
| Guide | Description |
|---|---|
golem-add-config-go | The non-secret typed-config mechanism secrets build on |
golem-add-agent-go | Define the base agent the config attaches to |
golem-call-another-agent-go | Understand why secrets are not overridable over RPC |