Menu da documentação

Documentação

Secret vault

Where credentials live and how they are handed out.

Nesta página

One file, and it stays here

Every credential TDE holds — API keys, connector tokens, database connection strings, generated service passwords — lives in a single encrypted SQLite file:

Vault location
~/.tde/secrets.db

The file never leaves the machine, and neither does the key that opens it. Not to TDE's servers, not to another one of your devices, not into a backup TDE makes. What Sync replicates is a small allowlist of values, re-encrypted under a separate license-derived key — never this file and never its key.

What is inside

One table, one row per secret:

ColumnHolds
nameThe lookup name, unique — GITHUB_TOKEN, LINEAR_API_KEY:my-project
ciphertextThe encrypted value, base64
nonceA fresh random 96-bit nonce, base64 — one per write
scope_kind, scope_valueglobal, project:<name> or connector:<name>
created_at, updated_atTimestamps; the update clock is the conflict resolver

The cipher is AES-256-GCM-SIV with a 256-bit key. Every write generates a new nonce, so storing the same value twice produces different ciphertext. The nonce length is validated on read rather than assumed.

The key

The vault key is 32 random bytes. It is never derived from a constant, never stored in the database, and never transmitted. Where it is kept depends on what kind of machine you are on, and TDE decides that by probing rather than guessing.

MachineBackendWhere the key lives
Desktop with a working credential storeOsKeyringThe OS keychain, under service dev.tde.secret-vault
Headless VPS, container, no session busKeyFile~/.tde/vault.key, 32 bytes, mode 0600

On Linux the keyring is the Secret Service over D-Bus; on macOS it is the system keychain. If neither answers — no D-Bus, no unlocked collection, a bare container — TDE falls back to the key file. The file is created with create_new and mode 0600 in one step, so it is never world-readable even momentarily, and two racing processes cannot clobber each other's key. If it is later found with a looser mode, TDE narrows it back to 0600 rather than refusing to start.

Resolution order is strict, and the backend is pinned by the presence of the key file so it cannot drift between boots:

  1. vault.key exists → that is the key. The keyring is not even probed.
  2. The keyring returns a key → use it.
  3. The keyring is empty but the vault has rows → hard error. Those rows are not recoverable and TDE will not pretend otherwise by minting a new key.
  4. The keyring is empty and the vault is empty → mint a key into the keyring.
  5. The keyring is unavailable and the vault has rows → recoverable error. TDE never silently re-keys.

Connectors store names, not values

~/.tde/connectors.json records which connectors exist, their endpoints, org slugs and chat ids — and for credentials, only the name of a vault entry. The in-memory field that holds resolved secrets is marked non-serializable, which is the load-bearing detail: there is no code path that could write a token into that file, because the serializer does not know the field exists.

Where secrets are not

The vault would be pointless if values leaked out the side. Four boundaries are enforced in code, each with tests pinning them:

Debug output. SecretEntry, SecretRecord, ConnectorConfig, DatabaseProfile, the Slack broker authorization and the service secret material all have hand-written Debug implementations that print [REDACTED]. A connector's debug output lists the role keys it holds — token, bot_token — and never their values.

Job payloads. A pipeline run snapshot is validated before it is persisted, and a snapshot carrying an agent API key is rejected outright with snapshot must never contain an agent API key. The field itself is marked skip-serialize and skip-deserialize, so the rejection is a belt over a brace.

Stream events and process output. Agent runner output is scrubbed: every value in the project environment is replaced with [REDACTED] in thinking text, message text, tool names and details, usage records, permission requests and raw stdout. Longest values are replaced first, so one credential that is a prefix of another cannot leave a suffix behind.

Process arguments. Credentials that a third-party CLI needs are injected as environment variables, never as argv, so they do not show up in a process list.

Reloaded at execution time

Keys are not baked into work when it is scheduled. A durable job freezes its pipeline, model, prompt, budget and agent selection into an immutable payload — and then only the secrets are rehydrated from the freshly loaded scheduler at execution time. The same pattern holds elsewhere: each bot run opens the vault inside its worker, and each sync phase opens it per phase.

The practical consequence is the useful one: rotating a key takes effect on the next job without re-creating anything, and a job that has been queued for a day does not run with yesterday's revoked credential.

Replication is opt-in per name

Sync does not replicate the vault. It replicates an allowlist of exact names plus two prefixes, re-encrypted under a license-derived key:

Replicated vault entries
ANTHROPIC_API_KEY      GITHUB_TOKEN         OPENAI_API_KEY
CURSOR_API_KEY         GROQ_API_KEY         OPENCODE_GO_API_KEY
DEEPSEEK_API_KEY       LINEAR_API_KEY       XAI_API_KEY
GEMINI_API_KEY         MISTRAL_API_KEY

LINEAR_API_KEY:*       STRIPE_API_KEY:*     SLACK_TOKEN:triage:*

Everything else stays local, and the exclusions are reasoned rather than accidental: TELEGRAM_BOT_TOKEN because a bot's long-poll is single-consumer and two machines would fight over it, POSTGRES_DSN:* and service credentials because they point at loopback ports on one host, EMAIL_PASSWORD:* and SENTRY_TOKEN because they need machine-local configuration to be usable at all. The governing rule is that a replicated secret must be usable alone on the receiving machine — otherwise the new machine looks configured and is not.

Reading and writing it

There is no tde secrets command. The vault is written through the surfaces that own the credentials — Connectors, the Database profile form, the Store's service installer, the agent configuration screen — and read by the code that needs them. The standalone tde-control TUI has a Secrets tab for inspection.

On a headless machine where the vault cannot be opened at all, TDE degrades to an in-memory vault for the session rather than refusing to boot, so a broken keychain does not take the desktop down with it.