Adding Secrets to an Effect Golem Agent
Effect agents declare secrets as redacted fields in a defineConfig service. The host supplies a
secret field on demand as an Effect Redacted.Redacted<T> value. Keep that wrapper intact through
Effect composition and logging, and call Redacted.value(...) only at the immediate boundary that
must consume the plaintext.
Steps
- Create a config service class with
defineConfig(name, fields). - Mark each sensitive field with
Schema.Redacted(innerSchema). - Attach the config class to
defineAgentwithconfig: MyAgentConfig. - Yield the config service inside the method handler.
- Evaluate a regular field directly; evaluate a secret field's
.getEffect. - Keep the resulting
Redacted.Redacted<T>wrapped until the narrowest possible use site. - Provision production values with
golem secret; usesecretDefaultsonly for development.
Agent with Regular and Secret Config
import { Effect, Redacted, Schema } from "effect";
import { defineAgent, defineConfig, method } from "@golemcloud/effect-golem";
export class SecureAgentConfig extends defineConfig("SecureAgent.Config", {
label: Schema.String,
apiKey: Schema.Redacted(Schema.String),
}) {}
const Info = Schema.Struct({
label: Schema.String,
apiKeyPrefix: Schema.String,
});
export const SecureAgent = defineAgent({
name: "SecureAgent",
mode: "durable",
config: SecureAgentConfig,
id: {
name: Schema.String,
},
methods: {
getInfo: method({
input: {},
success: Info,
}),
},
}).implement({
init: () => Effect.void,
methods: () => ({
getInfo: () =>
Effect.gen(function* () {
const config = yield* SecureAgentConfig;
const label = yield* config.label;
const apiKey = yield* config.apiKey.get;
return {
label,
// Reveal only long enough to derive the non-secret result.
apiKeyPrefix: Redacted.value(apiKey).slice(0, 4),
};
}),
}),
});
Register the implementation from the component entry point:
// src/main.ts
import "./secure-agent.js";
The config service has different access shapes for regular and secret leaves:
const readConfig = Effect.gen(function* () {
const config = yield* SecureAgentConfig;
const label = yield* config.label; // string
const apiKey = yield* config.apiKey.get; // Redacted.Redacted<string>
return { label, apiKey };
});
.get is an Effect property, not a method: write yield* config.apiKey.get, not
config.apiKey.get().
Nested and Structured Secrets
A Schema.Struct inside defineConfig creates nested paths. Mark only the sensitive leaves when
the surrounding values are ordinary config:
export class ServiceConfig extends defineConfig("Service.Config", {
database: Schema.Struct({
host: Schema.String,
password: Schema.Redacted(Schema.String),
}),
}) {}
const readDatabaseConfig = Effect.gen(function* () {
const config = yield* ServiceConfig;
const host = yield* config.database.host;
const password = yield* config.database.password.get;
return { host, password };
});
This produces the regular path database.host and secret path database.password. To make an
entire object one secret value instead, wrap its struct:
credentials: Schema.Redacted(
Schema.Struct({
username: Schema.String,
password: Schema.String,
}),
),
That produces one secret path named credentials whose .get Effect yields a redacted object.
Preserve Redaction
Schema.Redacted is both the secret declaration marker and the guest-side protection around the
decoded value. Ordinary inspection, string conversion, JSON conversion, and logging render the
wrapper as redacted. That protection ends as soon as Redacted.value(secret) returns plaintext.
- Keep secret variables typed as
Redacted.Redacted<T>while composing Effects. - Reveal directly at the external API or minimal transformation that needs plaintext.
- Never put revealed plaintext in log messages, log annotations, errors, snapshots,
Refstate, method results, or long-lived implementation closures. - If a secret must be associated with a log event, retain the redacted wrapper; do not log
Redacted.value(secret). - Returning a deliberately non-secret derivative, such as a four-character prefix, is safe only when the application explicitly requires that disclosure.
Runtime Reads and Rotation
Yield the config service and evaluate the secret .get Effect inside the method handler when the
method should observe updates:
const readCurrentPrefix = Effect.gen(function* () {
const config = yield* SecureAgentConfig;
const currentApiKey = yield* config.apiKey.get;
return Redacted.value(currentApiKey).slice(0, 4);
});
The SDK does not cache secret .get Effects: each evaluation asks the host for the value. A fresh
config service is also supplied for each invocation. Do not reveal and capture a secret during
agent initialization if later invocations should be able to observe rotation. Actual propagation
and consistency of an external secret update remain host concerns.
Managing Secrets with the CLI
Effect components use TypeScript casing and value syntax. Config paths retain camelCase, and string values passed through the shell include the TypeScript string literal quotes:
# Create secret values in the current environment
golem secret create apiKey --secret-type string --secret-value '"sk-abc123"'
golem secret create database.password --secret-type string --secret-value '"s3cret"'
# List, update, and delete
golem secret list
golem secret update-value apiKey --secret-value '"new-value"'
golem secret delete apiKey
For update-value and delete, --id <uuid> can be used instead of the positional path.
Defaults in golem.yaml
Put ordinary defaults under the agent's config, but keep secret defaults in the environment's
secretDefaults map:
agents:
SecureAgent:
config:
label: "production"
secretDefaults:
local:
apiKey: "dev-key-123"
database:
password: "dev-password"
Use secretDefaults for local development only. Manage deployed secret values separately with
the CLI rather than checking them into the manifest.
Key Constraints
- Pass the class returned by
defineConfigtodefineAgent.config. A raw schema record or aSchema.Structis not a validconfigvalue. - Do not use
Config<T>orSecret<T>from the plain TypeScript SDK; Effect agents usedefineConfig,Schema.Redacted, and EffectRedacted. - Config is not an
initargument. Yield the config service in an Effect. - Regular fields are field Effects; secret fields provide a
.getEffect that yields a redacted value. - Secret paths retain TypeScript camelCase and are scoped per environment, not per agent instance.
- A missing required secret prevents the agent from being created successfully.
- Keep versions of
effectand@golemcloud/effect-golemaligned with the generated project. - If the agent also needs detailed non-secret config guidance, use
golem-add-config-effect. - Run
golem buildafter changing config schemas or agent definitions.