An automation never holds a secret. It asks the Robot for a credential by name and the Robot resolves it. The value arrives through an environment variable. The code only ever sees the name.
#Asking for a credential
The call is ctx.credentials.get(name). It returns a record of string fields. Nothing about the value appears in the automation, in the repository or in the package:
import { defineAutomation, SystemException } from '../framework';
export default defineAutomation({
id: 'sign-in',
capabilities: ['web'],
async init(ctx) {
const creds = await ctx.credentials.get('acme');
const username = String(creds.username ?? '').trim();
const password = String(creds.password ?? '');
if (!username || !password) {
throw new SystemException('credential "acme" is incomplete', {
code: 'CREDENTIAL_INCOMPLETE',
retryable: false,
});
}
// keep them for this run; never log them
},
});
The provider chain the Robot composes has two members. The environment provider reads the channel below. The demo provider answers only the name demoApp, from the demo app's well-known public login. It exists for local convenience. The first provider that resolves a name wins.
#The environment variable channel
The channel is an environment variable per name. The name is upper-cased and every character outside A-Z0-9_ is replaced with _, so a name like mail.smtp becomes TS_CRED_MAIL_SMTP. Two forms are read:
TS_CRED_<NAME>, a JSON object of the record's fields. This is the form the dispatcher injects.TS_<NAME>_USERNAMEandTS_<NAME>_PASSWORD, a simple pair for a credential that carries only those fields.
The dispatcher does not build the channel by hand. It resolves the requested names through the vault, applies the environment rule and spreads the result into the Robot child's environment:
tsx robot/cli.ts run-package <pkgDir> --runId <id> --artifacts <dir>
TS_ROBOT_ID=robot-01
TS_ENVIRONMENT=staging
TS_CRED_ACME=<the record's fields, as JSON>
The requested spelling is kept. An automation that asks for acme reads TS_CRED_ACME whether the vault stores the name as acme or ACME, so name resolution never changes the channel the automation reads.
Studio spawns the Robot with its own environment, so it cannot inherit the injected channel. It emits the channel through a helper that writes the value to a pipe and nowhere else. It does not write to argv, which other users on the host can read. It does not write to a file or a log. What the helper prints for a human is the lineage, which is credential names and fingerprints. The helper applies the same environment rule, but per name: an entry that is not allowed in the current environment is skipped and named rather than failing the whole run.
#When a credential is not allowed
On the governed path, the dispatcher checks every requested name before it claims a queue item or spawns a Robot. A name that is missing, a name that resolves to two candidates, an environment that is not in the allow-list and a prod job whose credential has no explicit prod include are all refusals. Each carries failureClass: 'deployment' and status code 409, so the failure is business-class and never retried. The job fails fast. Credentialed work does not start and no run evidence is produced.
A refusal names what it checked. An environment refusal reads like this:
deployment: credential "mailSmtp" is restricted to [dev,staging] and this job runs as "prod"
A different class covers an unreadable container. Corrupt ciphertext, a missing unwrap secret or an otherwise unreadable vault is a system-class failure at dispatch time: no Robot is spawned and the failure is audited. It is never a silent empty environment. The plaintext path is stricter here than a missing key: an absent name is skipped, but an unreadable record is an error.
#Things that catch people out
The environment allow-list defaults to every environment except prod. A credential with no --envs works in dev, test and staging and is refused in prod until it is listed there explicitly. That is the intended direction. It is the opposite of what an implicit default usually means.
Storage is never the refused step. Writing a prod credential succeeds in any environment. Only injection is gated. That gating is for the Robot alone. The control plane's own reads are not subject to the rule.
Case is not folded away. Two names that differ only by case are refused rather than resolved, so acme for test and ACME for prod can both exist and neither is guessed. A typo in a name is still an error, not a near miss.