Skip to content

Credential Storage

Wayland Core stores credentials through a CredentialsStore trait. The backend is selected in config.toml and the default is Auto, an ordered ladder that will not write a secret in cleartext:

1. OS keyring used only when a live WRITE probe succeeded
2. Encrypted vault used only when unlock material was supplied
3. legacy plaintext READ AND DELETE ONLY, never written
4. otherwise the write FAILS with `no_secure_backend_for_write`

The legacy plaintext tier exists so a machine that already holds a credentials.toml keeps resolving its keys, and those keys are promoted up the ladder on the next read. It is not a write target, and no put reaches it. Writing cleartext requires an operator to name backend = "plaintext" explicitly (crates/wcore-config/src/credentials.rs, open_secure_ladder_store and build_ladder).

The encrypted-file backend is for headless or server deployments where the OS keychain is unavailable.

All three backends use flat string keys with dotted prefixes for namespacing, for example providers.anthropic.api_key or bedrock.secret_access_key.


Set the backend in ~/.wayland-core/config.toml (or your project-local .wayland-core.toml):

[storage.credentials]
backend = "keyring" # or "plaintext" or "encrypted_file"
service_name = "wayland-core" # optional; scopes keyring entries (default: "wayland-core")

For the encrypted-file backend, two file paths must also be specified:

[storage.credentials]
backend = { encrypted_file = { cipher_path = "~/.wayland/credentials.enc", key_params_path = "~/.wayland/credentials.kdf.json" } }

Set WAYLAND_VAULT=plaintext before startup to skip any auto-migration prompt and keep the plaintext backend regardless of what the config file says.


Plaintext backend (opt-in, not the default)

Section titled “Plaintext backend (opt-in, not the default)”

PlaintextCredentialsStore stores a [secrets] table in a TOML file. On every write it enforces 0o600 permissions on Unix and attempts a deny-all ACL on Windows via atomic_write - a sibling tempfile is written and then renamed so a crash or interruption cannot leave a half-written file. Reads warn via tracing if the file is world-readable but still load, to avoid stranding users on a freshly created file that the kernel briefly held at the umask default.

Default path: wherever wayland-core --config-path reports on your OS (usually ~/.config/wayland-core/config.toml on Linux, ~/Library/Application Support/wayland-core/config.toml on macOS).

You have to select this backend deliberately. It is appropriate for development machines where the OS keychain is inconvenient, and it offers no encryption at rest: anyone with read access to the file can read the secrets. Confidential material that must never be written in cleartext, such as an OAuth token set whose refresh token is a long-lived bearer credential, opens the secure ladder directly, so naming this backend cannot downgrade it.


KeyringCredentialsStore uses the keyring crate to store secrets in the platform’s native credential store:

  • macOS: Keychain
  • Windows: Credential Manager
  • Linux: Secret Service (e.g. GNOME Keyring, KWallet)

Each key is stored as a (service, user) pair where service is the configured service_name (default: "wayland-core") and user is the key string. Lookup is O(1).

To switch to the keyring backend:

[storage.credentials]
backend = "keyring"

Then set your API key once:

Terminal window
wayland-core setup # interactive onboarding stores keys in the selected backend

Or set it directly via the environment - the engine’s credential lookup order is environment variable first, then the credential store, then the config file. Keys read from the environment are never written to the store.


EncryptedFileCredentialsStore is for server or CI deployments where no OS keychain is available. It uses:

  • KDF: Argon2id (v0x13), parameters m=65536 KiB (64 MiB), t=3, p=1, output 32 bytes
  • AEAD: XChaCha20-Poly1305, 24-byte nonce, 16-byte Poly1305 MAC tag
  • Blob layout: nonce(24) || ciphertext || tag(16) in the cipher file

The KDF parameters are confirmed in crates/wcore-config/src/credentials.rs:

const DEFAULT_M_COST_KIB: u32 = 64 * 1024; // 64 MiB
const DEFAULT_T_COST: u32 = 3;
const DEFAULT_P_COST: u32 = 1;
pub const NONCE_LEN: usize = 24;
pub const TAG_LEN: usize = 16;
pub const KEY_LEN: usize = 32;

The backend writes two files:

FileContentsSecret?
cipher_path (e.g. ~/.wayland/credentials.enc)Raw bytes: nonce(24) || ciphertext || tag(16)Yes - protect with 0o600
key_params_path (e.g. ~/.wayland/credentials.kdf.json)JSON: { salt_b64, m_cost, t_cost, p_cost, version }No - contains only public KDF tuning params

The salt is 16 random bytes generated from rand::thread_rng() (seeded from the OS) on vault creation and stored in key_params_path. Storing the KDF parameters separately means you can re-derive the key from your passphrase at any time without knowing the original tuning values. The AEAD nonce is re-randomized on every put() write.

Both files are written with atomic_write (tempfile + rename) and 0o600 permissions are enforced immediately after.

The engine resolves the vault passphrase in this order:

  1. WAYLAND_VAULT_PASSPHRASE_FD: a Unix file descriptor number (e.g. 3). The engine validates the fd is open and readable before consuming it. This path does not appear in /proc/<pid>/environ.
  2. WAYLAND_VAULT_PASSPHRASE: env var (legacy). Emits a tracing::warn! about the /proc/<pid>/environ visibility risk on Linux.
  3. Interactive prompt: rpassword::prompt_password("vault passphrase: ") on a TTY.

The passphrase is held in a zeroize::Zeroizing<String> in memory. Argon2id derivation runs once per process; subsequent get()/put() calls reuse the cached derived key under a parking_lot::Mutex. Cross-process write concurrency is not modeled - if you run multiple writers against the same vault files, serialize at the application layer.

On first access the engine:

  1. Reads the passphrase via the resolution order above.
  2. Loads key_params_path if it exists; otherwise uses KdfParams::default() (fresh random salt).
  3. If cipher_path already exists, decrypts it immediately to verify the passphrase is correct. A wrong passphrase fails here rather than silently rotating the vault key on the next write.

Pass the passphrase via a file descriptor to avoid environment variable exposure:

Terminal window
# Open a pipe, write the passphrase to fd 3, run wayland-core
exec 3< <(echo -n "my-vault-passphrase")
WAYLAND_VAULT_PASSPHRASE_FD=3 wayland-core "run my task"
exec 3<&-

For CI/CD, inject the passphrase via a secret manager into the fd at process start rather than an environment variable.


Keys follow a dotted convention. Some examples used by the engine:

KeyWhat it holds
providers.anthropic.api_keyAnthropic API key
providers.openai.api_keyOpenAI API key
bedrock.access_key_idAWS access key ID for Bedrock
bedrock.secret_access_keyAWS secret access key for Bedrock
providers.<name>.api_keyAny custom provider’s key

The wayland-core setup interactive onboarding stores keys under these namespaced paths. Channel adapter keys (Slack, Discord, etc.) use channels.<name>.<field> prefixes managed by their respective crates.


ScenarioRecommended backend
Developer workstation, interactive usekeyring
Developer workstation, no keychain daemonplaintext (ensure 0o600; consider full-disk encryption)
Server or CI, no GUI/keychainencrypted_file with passphrase via fd
Testing or short-lived containerplaintext or environment variables only