stackbone.secrets
Typed reads of encrypted secrets. Operators register API keys, tokens and credentials in the Studio dashboard; your code reads them at runtime through the ambient
stackboneclient without ever seeing the ciphertext-at-rest.
Mental model
stackbone.secrets is the read side of your agent's credentials. It is
agent-local: the operator registers API keys, tokens and credentials
in the Studio dashboard, Stackbone stores them encrypted in your
agent's own database, and stackbone.secrets reads them straight out
of that database and decrypts them locally with the per-agent key the
runtime injects as STACKBONE_SECRET_KEY. The ciphertext-at-rest never
leaves your agent.
You read secrets wherever you need a credential — from inside an
agent tool, or from a
durable workflow 'use step'. There is no
createClient() and no per-invoke handle to wire up: the ambient
stackbone client resolves these credentials lazily from the
runtime-injected environment, so you just import it and call.
import { stackbone } from '@stackbone/sdk';
const result = await stackbone.secrets.get('STRIPE_API_KEY');A secret read is a direct query against the agent's own database
followed by a local decrypt — no control-plane round-trip and nothing to
negotiate, so it is available from the very first call in any tool or
workflow step. The facade does not cache the values, so every call
re-reads the database and secret rotations propagate on the next read.
Because the read goes through the agent database, your agent must have
its Postgres connection configured (the runtime injects
STACKBONE_POSTGRES_URL for you). The same surface is available to
every agent and workflow in your workspace and to your
Stackbone Connect calls — they all read from
the same install's database.
Read a single secret
Read one secret from inside an agent tool.
get returns a Result envelope — check .error
before touching .data, and never log a decrypted value.
import { tool } from '@langchain/core/tools';
import { stackbone, z } from '@stackbone/sdk';
const createCharge = tool(
async ({ amountCents }: { amountCents: number }) => {
const result = await stackbone.secrets.get('STRIPE_API_KEY');
if (result.error) throw new Error(result.error.code);
// Pass the value straight to the downstream client — don't log it.
const stripe = createStripeClient(result.data);
const charge = await stripe.charges.create({ amount: amountCents });
return JSON.stringify({ chargeId: charge.id });
},
{
name: 'create_charge',
description: 'Create a Stripe charge.',
schema: z.object({ amountCents: z.number() }),
},
);get rejects empty names with secrets_invalid_request. A name that
has not been registered for this agent surfaces secrets_not_found.
Read many at once
stackbone is the same ambient client everywhere — getMany works
identically from a tool's execute() and from a workflow step.
const result = await stackbone.secrets.getMany([
'STRIPE_API_KEY',
'STRIPE_WEBHOOK_SECRET',
'POSTMARK_TOKEN',
]);
if (result.error) throw new Error(result.error.code);
const stripeKey = result.data.STRIPE_API_KEY; // string | undefinedNames absent from the agent come back as omissions in the returned
map — getMany never fails just because one secret is missing. Callers
that need all-or-nothing semantics should diff the keys themselves:
const requested = ['A', 'B', 'C'];
const present = Object.keys(result.data);
const missing = requested.filter((k) => !present.includes(k));
if (missing.length > 0) throw new Error(`Missing secrets: ${missing.join(', ')}`);A secret read is also a fine thing to do inside a durable workflow. A
'use step'
function runs once, persists its result and retries on failure, so wrap
the credentialed work — fetching the secret and calling the third-party
API — in a step so a transient failure replays cleanly:
import { stackbone } from '@stackbone/sdk';
async function chargeCustomer(amountCents: number) {
'use step';
const result = await stackbone.secrets.get('STRIPE_API_KEY');
if (result.error) throw new Error(result.error.code);
const stripe = createStripeClient(result.data);
const charge = await stripe.charges.create({ amount: amountCents });
return { chargeId: charge.id };
}Errors
stackbone.secrets reads the agent database and decrypts locally, so its
failures are database- and crypto-shaped rather than HTTP-shaped.
| Code | When |
|---|---|
secrets_invalid_request |
Empty name, or an empty names array. |
secrets_not_found |
get(name) and the agent has no secret with that name. |
secrets_decrypt_failed |
The stored ciphertext could not be decrypted with the per-agent key. |
secrets_not_configured |
STACKBONE_SECRET_KEY is missing, or the per-agent cipher could not be built from it. |
secrets_unavailable |
The read against the agent database failed. |
database_not_configured |
The agent has no Postgres connection configured (STACKBONE_POSTGRES_URL unset / agent database missing). |
Where to go next
stackbone.config— for non-secret per-agent config (feature flags, toggles, JSON snippets).stackbone.approval— pair withstackbone.secretsto gate a paid API call on a human approval.- Agents — where agent tools live, the most common place to read a secret.