Secrets Overview

The Sealed Credential Store manages sensitive values (API keys, passwords, tokens) across the full lifecycle: encrypted on disk, decrypted on demand, and scrubbed before they re-enter agent context.

Architecture

The store is composed of four layers:

ComponentRole
SecretStoreHeap-only runtime store. Values held as Zeroizing<String>. All resolve/substitute calls return StoreLocked while locked.
EncryptedSecretStoreSQLite-backed persistence. Secret values encrypted with AES-256-GCM; master key derived from a passphrase via Argon2id. KDF salt stored in a kdf_params table.
SecretPolicyStoreSQLite-backed pre-approval policy store. Policies survive daemon restarts.
SecretGrantStoreEphemeral in-memory per-agent delegation grants. Grants expire on daemon restart by design.

Design Goals

  • Encrypted at rest: Secret values are encrypted with AES-256-GCM before being written to SQLite. The master key is derived from a passphrase (Argon2id, 64 MiB memory cost) and held in Zeroizing<[u8; 32]> - zeroed on drop.
  • Opaque handles: Agents reference secrets as {{secret:<name>}} in tool arguments; the daemon substitutes the plaintext only inside the tool dispatch layer.
  • Output scrubbing: Tool results are scanned for secret values before they are returned to the agent; matches are replaced with [REDACTED:<name>].
  • Pre-approval policies: Secret resolution requires an active policy matching the (secret, tool, host) triple - no open-ended blanket access.
  • Audit trail: Every secret use is recorded with the matching policy ID.
  • Canary tokens: Fake secrets embedded in the store detect exfiltration attempts.

Security Properties

PropertyGuarantee
Disk persistenceValues encrypted with AES-256-GCM; master key never written to disk
In-memoryValues held as Zeroizing<String>; zeroed by the allocator on drop
IPC responsesValues never appear in ash output or IPC responses
LLM contextValues scrubbed before re-entering LLM context
Agent memoryAgents never see plaintext; only {{secret:<name>}} handles are visible
KernelSecrets are not in environment variables (not in /proc/<pid>/environ)
Locked stateAll resolve/substitute calls return StoreLocked until ash secrets unlock succeeds

Lock / Unlock Lifecycle

First run:
  ash secrets unlock          # prompts for passphrase → init_passphrase() → store ready

Daemon restart:
  ash secrets unlock          # prompts for passphrase → derive_key() + load_all() → store ready

While running:
  ash secrets lock            # zeroize all in-memory plaintext; subsequent uses return StoreLocked
  ash secrets unlock          # re-derive key and reload from encrypted store

Key rotation:
  ash secrets rekey           # prompts for old + new passphrase; re-encrypts all blobs atomically

On first run (no KDF params in the database yet), ash secrets unlock automatically calls init_passphrase() to create a fresh encrypted store with the supplied passphrase. On subsequent starts it calls derive_key() against the stored salt.

Workflow

1. Operator unlocks / initialises the store:
   ash secrets unlock
   (prompted for passphrase; initialises on first run, reloads on restart)

2. Operator registers a secret:
   ash secrets add my-api-key
   (value read via echo-off prompt; persisted encrypted to SQLite if store is unlocked)

3. Operator creates a pre-approval policy:
   ash secrets policy add \
     --label "API access" \
     --secret "my-api-key" \
     --tool "web.fetch" \
     --host "api.example.com"

4. Agent uses secret in a tool call:
   {"url": "https://api.example.com/v1", "headers": {"Authorization": "Bearer {{secret:my-api-key}}"}}

5. agentd:
   a. Finds active policy matching (my-api-key, web.fetch, api.example.com)
   b. Substitutes plaintext in the tool input (never logged)
   c. Calls the tool handler
   d. Scrubs the tool output before returning to agent
   e. Writes audit entry: "secret_used: my-api-key via web.fetch (policy: <uuid>)"

Declaring Secret Access in a Manifest

spec:
  capabilities:
    - secret.use:my-api-key
    - secret.use:db-*           # glob: all secrets starting with "db-"
  secret_policy:
    - label: "API access"
      secret_pattern: "my-api-key"
      tool_pattern: "web.fetch"
      host_pattern: "api.example.com"

The secret_policy section in the manifest creates pre-approval policies automatically at spawn time. This enables unattended operation without requiring a separate ash secrets policy add step.