Skip to content

Secrets vault: unified, encrypted secrets backend with per-persona access #19

Description

@mattmezza

Summary

Add an encrypted secrets store that (a) consolidates the secrets currently spread across .env and
the SQLite config store, and (b) lets personas/subagents use and store secrets under access control
— without secret values ever entering the model's context.

Two kinds of secret, one store

  • Infrastructure secrets (provider keys, bot token, email / CalDAV credentials): read by the
    app's own code at boot or at the tool boundary; not persona-scoped.
  • Agent secrets (e.g. a payment-provider key a finance persona uses, a stored session/token):
    persona-scoped and accessed by reference.

Same encrypted table and master key; two access paths.

Core design — references, never plaintext

This generalises the existing tool_env pattern (auth injected at the executor; the model never sees
the value). Secrets are stored by name; a persona's prompt lists only the names it may use. When
the agent emits e.g. run_command("curl -H 'Authorization: Bearer {{secret:NAME}}' ..."), the
executor resolves the placeholder from the vault after checking the active persona's ACL, then
runs. Critical boundary: substitution happens only in the run_command path, never in message /
email bodies
, so a prompt-injected agent cannot exfiltrate a secret through the messaging tools.
Plaintext never enters context, history, logs, or memory extraction.

Consolidation lever (incremental, reversible)

The config loader already does ${ENV_VAR} interpolation. Add a sibling resolver ${vault:NAME}
resolved from the unsealed vault at config-load time. Secrets can then be migrated one at a time — a
field can read from .env, the config store, or the vault, with .env still a fallback throughout.
No flag day.

Storage & master key

  • One SQLite table of ciphertext; unsealed into process memory at boot and resolved from there.
  • A single master key held in env / keyfile; everything else moves into the vault, collapsing the
    on-disk plaintext surface to one value whose only job is to unseal the rest.
  • Bootstrap note: the master key (and only it) must be available before unseal — avoids a
    chicken-and-egg with the provider key / bot token needed to start.

Access control

  • ACL is the persona's secret scope (the fourth Profile facet). Subagents inherit-never-widen.
  • Each persona owns a private namespace persona:<name>:* it may write; writing shared/global is an
    ASK action. Where a secret originates in code (a browser session, an OAuth token), the sidecar
    writes it to the vault directly so it never round-trips through the model.

UX & product

  • Admin UI: a Secrets page listing secret names only (never values) with add / rotate /
    revoke, and a persona × secret access matrix so it's obvious which persona can use what. Value
    entry (add/rotate) is the only place a value is typed — write-only and masked. Responsive/touch-
    friendly
    at phone width for quick checks, reusing consistent masked-field + table components.
  • Setup wizard: generate-or-enter the master key, and a guided "import from .env" step that
    detects existing secrets and offers to migrate them — consolidation as a one-screen action.
  • On the go (Telegram): read-only / safe actions only — list secret names + which persona
    holds access, and see recent-use audit; values are never entered or shown over Telegram
    (error-prone on a phone, and the channel must stay clean). Sensitive edits are deep-linked to the
    web UI.
  • Mobile-first: the web Secrets page is responsive for quick on-the-go checks; value entry stays
    web-only by design.

Phasing

  1. Store + master key + ${vault:...} resolver (consolidate existing infra secrets). Low risk.
  2. Persona-scoped reference / ACL layer on top (the security-sensitive part), once personas exist.

Threat-model note

Combining a secret store, outbound messaging, and untrusted web content reproduces the classic
prompt-injection trifecta. Primary mitigations: reference-only reads; substitution confined to
run_command; per-persona scoping; inherit-never-widen; ASK on shared-secret writes and new outbound
destinations; optional browser egress allowlist.

Acceptance criteria

  • An existing infra secret (e.g. the email password) can be served from the vault via ${vault:...}
    with .env fallback intact.
  • A new user can set the master key and import existing .env secrets from the setup wizard in one
    pass
    .
  • A persona can use a secret by reference in run_command without the value appearing in context,
    history, or logs.
  • Secret names and access are viewable on mobile, but values are never entered or shown over
    Telegram
    .
  • A persona cannot read secrets outside its scope; a subagent cannot widen scope.

Related

  • Backs: browser sessions, pi provider keys.
  • ACL facet of: persona profiles.

Metadata

Metadata

Assignees

No one assigned

    Labels

    newNew additiontodoPlanned / not yet started

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions