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 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, and Stackbone stores them encrypted in your
agent's own database. 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'. The ambient
stackbone client resolves these credentials lazily from the
runtime-injected environment, so you import it and call: there is no
createClient() and no per-invoke handle to wire up.
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 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). Every agent and workflow in your
workspace reads this same surface, and so do 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 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 works inside a durable workflow too. A
'use step'
function runs once, persists its result and retries on failure. 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). |
If the container your agent runs in restarts with a different encryption key, every secret stored under the previous key reads back as
secrets_decrypt_failed. Those values cannot be recovered, so register them again in the dashboard. A row that exists but fails to decrypt makes a run report the credential as 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.