The vault is a file on disk that holds secrets by name. An automation asks for a name, the dispatcher resolves it and the Robot receives the value in an environment variable. Every read surface returns names, field names, metadata and fingerprints. No command prints a value.
#What the vault stores
The vault models the asset types a UiPath operator expects. vault types prints the vocabulary as data:
credential secret username?:string, password:string, host?:string, user?:string, from?:string, port?:integer, security?:string
windowsCredential secret domain?:string, username:string, password:string
certificate secret certificate:string, password?:string
text displayable value:string
integer displayable value:integer
bool displayable value:boolean
A declared type is enforced at write time. An undeclared field, a missing required field or a wrong-typed value is refused before anything is written, so a mistake surfaces at authoring time. windowsCredential and certificate are declared but not implemented: there is no Windows-auth path and no X509 machinery here.
#Environments and the injection allow-list
Storage is always allowed. The default allow-list is every non-prod environment. prod requires an explicit include:
tsx orchestrator/cli.ts vault set mailSmtp --type credential --envs dev,staging --secret-stdin
Injection is the gated step. It is robot-facing only. The dispatcher injects a credential only when the job's effective environment is in its allow-list. It checks that before any queue claim or Robot spawn. A missing name, an environment that is not allowed and a prod job without an include are all business-class refusals. No run starts and no evidence is written. The control plane's own reads are not gated.
Names are resolved explicitly. An exact match wins. Otherwise a unique case-insensitive match is accepted, so an automation asking for acme still resolves an asset stored as ACME. Two names that differ only by case are refused as ambiguous.
#Encryption and key handling
A container carries a vaultVersion discriminator. Version 1 is plaintext and stays readable forever, so existing installs never break. A version 1 container is never written again once encryption is on. Version 2 is encrypted. Silent upgrade is forbidden.
Each record is sealed with AES-256-GCM with its own initialisation vector and authentication tag. One wrapping key covers the whole state root, not one key per credential. Each secret also has its own item key sealed under that key, as a rotation detail that is never an operator surface.
The wrapping key comes from an explicit unwrap secret, in order: TS_VAULT_MASTER_KEY (64 hex characters or base64 of 32 raw bytes), TS_VAULT_KEY_FILE, then a default key file outside the state root, namespaced by a hash of the root. A legacy <state root>/vault.key is read only. DPAPI and host-TPM binding are parked: a host that is not always connected cannot rely on a key bound to one machine.
vault encrypt --migrate moves a plaintext container to version 2 by stage-then-rename, with a best-effort overwrite of the source file. It is not complete until a verify-read unwraps every name and the source path is gone. It is not a forensic wipe. vault rotate <name> re-seals one credential under a fresh item key, which changes its fingerprint. Migration and rotation are operator actions, never steps inside the scheduler's tick.
#The custody audit
Every successful custody mutation appends one row to <state root>/vault-audit.ndjson, a sibling of the container. The row records the time, the action (create, update or rotate), the name, the declared type, the field names, the environment allow-list, the fingerprint, the previous fingerprint and via, the surface that asked. Values never appear. A refused write appends nothing, because history must never claim what did not happen.
This is not a governance row in the M7 union. That union is closed to approve, consume and delete and requires a verified principal. A vault write has no authenticated principal, so custody audits itself. via is provenance, not identity: it is bounded to 1 to 24 characters of [a-z0-9-], so a free-form string cannot land in the audit.
#Reading without seeing a value
vault list returns credential names and the container form. vault show <name> returns metadata, field names, the allow-list and the fingerprint, which is 12 hex characters of SHA-256 over the sealed item. vault doctor reports form, custody and hygiene. --strict exits non-zero when a plaintext container or a surviving source path exists, but that flag is hygiene only and has no dispatch effect. No scheduler, dispatcher or robot path reads it. None of these returns a value, in any environment. An automated test runs a dispatch with a fixture secret and searches every produced artifact, log and package for it. Any hit fails the test.