Files
fusion/docs/secrets.md
Fusion (runfusion.ai) a3e8c8f8b7 feat(FN-4882): add cross-node secrets sync reliability backstop with integr
Documents the secrets sync cross-node protocol in architecture.md and secrets.md, with a reliability backstop test covering the cross-node sync behavior.

Fusion-Task-Id: FN-4882
2026-05-18 02:25:21 -07:00

8.5 KiB

Secrets

← Docs index

Overview

Fusion's secrets subsystem provides encrypted-at-rest secret storage with project scope (.fusion/fusion.db) and global scope (~/.fusion/fusion-central.db).

Current shipped behavior in this branch includes:

  • AES-256-GCM encryption primitives (packages/core/src/secrets-crypto.ts)
  • CRUD + reveal APIs via SecretsStore (packages/core/src/secrets-store.ts)
  • Per-secret access policy metadata (auto / prompt / deny)
  • Schema-backed read metadata (last_read_at, last_read_by)

Threat-model baseline:

  • Secret plaintext is not stored in SQLite.
  • Ciphertext + nonce are persisted; plaintext exists only in process memory during create/reveal.
  • Secret values must never be logged.

See also: Storage, Multi-project, Architecture, Settings reference.

Architecture

Fusion stores secrets in two SQLite tables:

  • Project scope: secrets in .fusion/fusion.db
  • Global scope: secrets_global in ~/.fusion/fusion-central.db

Both tables share the same column contract:

Column Type Notes
id TEXT Primary key UUID.
key TEXT Unique secret key (idxSecretsKey / idxSecretsGlobalKey).
value_ciphertext BLOB AES-GCM ciphertext payload (includes auth tag).
nonce BLOB Per-row random nonce.
description TEXT Optional metadata.
access_policy TEXT CHECK constrained to auto, prompt, deny.
env_exportable INTEGER 0/1 flag for env-materialization intent metadata.
env_export_key TEXT Optional env variable key metadata.
created_at TEXT ISO timestamp.
updated_at TEXT ISO timestamp.
last_read_at TEXT Last reveal timestamp.
last_read_by TEXT Agent/user identifier recorded on reveal.

For broader database inventory, see docs/storage.md.

Encryption

Secret crypto uses AES-256-GCM with:

  • 32-byte master key
  • 12-byte random nonce per encrypt operation
  • 16-byte auth tag appended to ciphertext

Implementation reference: packages/core/src/secrets-crypto.ts.

Master Key Resolution

The current implementation exposes a MasterKeyProvider abstraction consumed by createSecretCipher / SecretsStore.

  • Required contract: async provider that returns a 32-byte key.
  • Validation failures return non-sensitive SecretCryptoError codes.

Runtime keychain/filesystem resolution is wired via MasterKeyProvider and consumed by SecretsStore; rotation UX remains follow-up work.

Access Policies

Per-secret policy values are:

  • auto
  • prompt
  • deny

Resolution helper (resolveSecretAccessPolicy) uses:

  1. Row-level secret policy (if set)
  2. Global settings default secretsAccessPolicy (if set)
  3. Fallback: prompt

Implementation references:

  • packages/core/src/secret-access-policy.ts
  • packages/core/src/types.ts (GlobalSettings.secretsAccessPolicy)

Approval integration is active through fn_secret_get policy handling (packages/cli/src/extension.ts:1581-1611) and approvals lifecycle APIs.

Dashboard CRUD

Secret persistence primitives are shipped at the store/API layer; a dedicated end-user SecretsView remains separate product work.

Agent Access (fn_secret_get)

fn_secret_get is shipped in packages/cli/src/extension.ts:1542-1629.

Tool contract:

  • Params: key (required), scope (project or global, optional).
  • Resolution: key lookup in requested scope; missing key returns not-found result without plaintext.
  • Policy outcomes:
    • auto → reveals and returns plaintext value (secret:read audit at extension.ts:1615).
    • prompt → creates approval request and returns details.outcome: "pending_approval" (extension.ts:1607-1611).
    • deny → immediate refusal and secret:approval-denied audit (extension.ts:1581-1583).

.env Auto-write into Worktrees

Fusion can materialize env-exportable secrets into each acquired task worktree when project settings enable it (secretsEnv.enabled=true).

  • Supported settings: enabled, filename (default .env, validated as local filename only), overwritePolicy (skip/merge/replace), keyPrefix, requireGitignored (default true).
  • Safety guard: when requireGitignored is enabled, Fusion runs git check-ignore -- <filename> and refuses writes unless the file is ignored.
  • Write contract: managed content is canonicalized and written atomically with mode 0o600; audit metadata includes keys and counts, never values.
  • Fingerprint sidecar: successful writes persist .fusion-secrets-env.fingerprint containing <sha256>\n<filename>\n (mode 0o600) so teardown can verify file integrity before deletion.
  • Teardown cleanup: when a worktree is removed, Fusion deletes the managed env file only when the on-disk fingerprint still matches; edited files are preserved and only the sidecar is removed.

Settings shape is project-scoped in ProjectSettings (packages/core/src/types.ts:2599-2609): secretsEnv (env materialization config) and secretsSyncPassphrase (ciphertext already wrapped under local master key by caller; see types.ts:2602-2604).

Cross-node Sync

Fusion now exposes four secrets sync endpoints:

  • POST /api/nodes/:id/secrets/push — wraps local secrets into a passphrase-protected envelope and sends it to a remote node.
  • POST /api/nodes/:id/secrets/pull — fetches a remote envelope from GET /api/secrets/sync-export and applies it locally.
  • POST /api/secrets/sync-receive — inbound apply endpoint (Bearer apiKey required).
  • GET /api/secrets/sync-export — inbound export endpoint (Bearer apiKey required).

Envelope format is WrappedSecretsBundle from packages/core/src/secrets-sync.ts:33-38: { version, ciphertext, salt, nonce, kdf, kdfParams } plus transport metadata (sourceNodeId, exportedAt). Wrapping uses scrypt (N=32768, r=8, p=1, keyLen=32, secrets-sync.ts:17-22) and AES-256-GCM with base64 ciphertext/salt/nonce (secrets-sync.ts:68-78).

Sync passphrase storage is local-only: reserved key __sync_passphrase__ in secrets_global with access_policy="deny" and env_exportable=false, encrypted under the local master key. The passphrase is never transmitted and never returned by HTTP endpoints.

Error mapping:

  • SecretsSyncError codes (wrong-passphrase, version-mismatch, malformed) return HTTP 400 with { "error": <code> }.
  • Missing passphrase returns HTTP 400 with { "error": "passphrase-not-configured" }.
  • Bearer auth failures return HTTP 401.

Inbound auth contract is enforced in route code (packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts:99-114, :181-196): missing/invalid Bearer Authorization or mismatched local apiKey returns 401.

Audit payloads exclude plaintext values, passphrases, and envelope crypto material (ciphertext, salt, nonce).

Audit Events

Filesystem-domain secret audit taxonomy:

  • secret:read
  • secret:create
  • secret:update
  • secret:delete
  • secret:approval-requested
  • secret:approval-granted
  • secret:approval-denied
  • secret:sync-push
  • secret:sync-pull
  • secret:env-write
  • secret:env-write-skipped
  • secret:env-cleanup
  • secret:env-cleanup-skipped

All listed events are enumerated in packages/engine/src/run-audit.ts:261-274 (union at run-audit.ts:325). Route/tool emitters include: secret:sync-push (packages/dashboard/src/routes/register-secrets-sync-routes.ts:92), secret:sync-pull (register-secrets-sync-routes.ts:180, register-secrets-sync-inbound-routes.ts:158-164), secret:read + approval events (packages/cli/src/extension.ts:1581-1615), env materialization/cleanup (packages/engine/src/secrets-env-writer.ts:99-217).

Track follow-up: FN-5031 (missing packages/core/src/__tests__/secrets-env.test.ts contract file). Track follow-up: FN-5032 (docs/settings-reference.md still marks shipped secrets settings as planned in some rows).

Plaintext prohibition: audit payload metadata must never include plaintext, decrypted values, ciphertext, or nonce fields. Use assertNoSecretPlaintext(...) as the canonical enforcement helper before emitting secret audit events.

Operational Notes

  • Backups: preserve both SQLite data and master-key material/provider source used by deployment.
  • If master key material is lost, encrypted secret values become unrecoverable.
  • Pending advanced capabilities:
    • Full rotation UX and key lifecycle tooling
    • TTL/rotation automation, env-set profiles, KMS/Vault backends, per-node asymmetric sync