Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6719cbf
feat(secrets): vault-aware UI — config diagnostics + Refresh-from-vault
0xr00tf3rr3t Jun 11, 2026
a4355fd
feat(secrets): Security Providers section in Settings (choose + test …
0xr00tf3rr3t Jun 11, 2026
a36d8a4
feat(setup): add secrets-provider onboarding step to first-run setup
0xr00tf3rr3t Jun 11, 2026
46253b3
docs: add KeePassXC vault setup guide for the Security Providers feature
0xr00tf3rr3t Jun 12, 2026
e007e90
test(secrets): main-process no-values IPC contract for secretsProvide…
0xr00tf3rr3t Jun 12, 2026
ca1e2ec
test(secrets): pin AIR-005 floor boundaries + AIR-006 deletion window
0xr00tf3rr3t Jun 14, 2026
0b53fbe
fix(secrets): reap helper process-group + gate command provider on Wi…
0xr00tf3rr3t Jun 14, 2026
cf258dc
merge(secrets/04): layer vault-bootstrap onboarding onto secrets/03 h…
0xr00tf3rr3t Jun 15, 2026
b760a03
fix(secrets): run TPM seal off the Electron main thread (AIR-016)
0xr00tf3rr3t Jun 15, 2026
b13db25
fix(secrets): run vault db-create off the main thread too (AIR-016 si…
0xr00tf3rr3t Jun 15, 2026
563f8ee
feat(secrets): label each vault-resolved key 'Vault Provided' on the …
0xr00tf3rr3t Jun 15, 2026
e2089e9
fix(secrets): secretsProviderStatus lists vault-only keys, not the pr…
0xr00tf3rr3t Jun 15, 2026
df62464
fix(setup): detect Anthropic OAuth token from vault + offer vault/man…
0xr00tf3rr3t Jun 15, 2026
9ff7525
fix(setup): existing-vault detection on model step (secretsChoice env…
0xr00tf3rr3t Jun 15, 2026
c878bbb
fix(config): never persist empty model.default + stop credential-blee…
0xr00tf3rr3t Jun 15, 2026
4a4da60
feat(setup): blank model field falls back to a DISCOVERED model via /…
0xr00tf3rr3t Jun 15, 2026
fec64f3
fix(config-health): OAuth-token alias is SATISFIED, not a mismatch — …
0xr00tf3rr3t Jun 15, 2026
5cebe79
fix(validation): chat-readiness gate accepts OAuth-token alias — unbl…
0xr00tf3rr3t Jun 15, 2026
d465836
fix(secrets): forward ALL provider credential names from the security…
0xr00tf3rr3t Jun 15, 2026
21a0fe2
refactor(secrets): single-source KEY_ALIASES in shared/url-key-map (G…
0xr00tf3rr3t Jun 15, 2026
fb4885a
fix(secrets): snap-aware CLI in detectExistingVault + shell-quote bin…
0xr00tf3rr3t Jun 15, 2026
efc252b
fix(preload): drop duplicate invalidateSecretsCache property (CI type…
0xr00tf3rr3t Jun 15, 2026
1164c69
fix(test): port secrets/04 config-health tests onto upstream's robust…
0xr00tf3rr3t Jun 15, 2026
77a34c1
feat(updater): add desktop.auto_update opt-out (default enabled)
0xr00tf3rr3t Jun 16, 2026
b5eb416
refactor(updater): single-source the auto-update gate + drift guard
0xr00tf3rr3t Jun 16, 2026
41f4d3d
test(updater): extract + prove the updater WIRING gate (not just the …
0xr00tf3rr3t Jun 16, 2026
3d180bf
test(ssh): prove SSH command args are inert data, not code (injection…
0xr00tf3rr3t Jun 16, 2026
01f9a39
fix(installer): install gate must honor credential-name aliases (Setu…
0xr00tf3rr3t Jun 16, 2026
1da7870
fix(secrets): make command-provider vault write/delete non-blocking (…
0xr00tf3rr3t Jun 16, 2026
be3955e
fix(installer): constrain vault install gate to credential aliases (A…
0xr00tf3rr3t Jun 17, 2026
56b8e73
fix(test): resolve rebase artefacts in config-health.test.ts
0xr00tf3rr3t Jun 19, 2026
66cba60
fix(test): make ssh-remote CLI-resolution test pass in a clean CI con…
0xr00tf3rr3t Jun 16, 2026
3a7cfe5
refactor(config-health): remove dead checkRuntimeEnvKeyMismatch no-op…
0xr00tf3rr3t Jun 20, 2026
48c3e63
fix(lint): satisfy CI lint gate (4 pre-existing errors)
0xr00tf3rr3t Jun 20, 2026
481d279
ci: re-trigger Forgejo run on new act_runner
0xr00tf3rr3t Jun 20, 2026
811cd34
ci: re-trigger on live act_runner (labels declared)
0xr00tf3rr3t Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,65 @@ By default, API keys live in `~/.hermes/.env` (the **env** provider). No
configuration is needed — this is byte-for-byte the historical behavior, and
nothing changes for you.

### First-run vault setup & diagrams

The setup wizard can detect an existing vault or **create a new encrypted
KeePassXC vault** for you (no dead-end picker). The full set of diagrams —
logical component flow, the onboarding state machine, and the **secret
workflow** — lives in
[docs/diagrams/vault-bootstrap-diagrams.md](docs/diagrams/vault-bootstrap-diagrams.md).
The two most useful are embedded below.

**First-run onboarding (assume nothing exists):**

```mermaid
stateDiagram-v2
[*] --> Detect: first run
Detect --> Found: tmpfs dump OR vault file on disk
Detect --> NotFound: nothing resolvable
Found --> Prefill: suggest read command (UID-safe)
Prefill --> ModelStep: provider resolves the model key -> hide key field
NotFound --> ToolCheck: checkToolAvailability()
ToolCheck --> CanCreate: keepassxc-cli present
ToolCheck --> InstallHint: CLI missing
InstallHint --> Detect: user installs, retry
CanCreate --> Create: createVault()
Create --> CreateOk: kdbx + key 0600, command returned
Create --> CreateFail: vault-already-exists / db-create-failed
CreateFail --> ToolCheck: surface honest error
CreateOk --> SealChoice: offer opt-in TPM seal
SealChoice --> Sealed: systemd-creds ok -> sealed=true
SealChoice --> Fallback: polkit/no-tpm -> sealed=false, 0600 stands
Sealed --> ModelStep
Fallback --> ModelStep
ModelStep --> [*]: provider configured, setup complete
```

**Secret workflow (where the value & key name are gated):**

```mermaid
flowchart TD
Start([User edits/reads a secret in the UI]) --> Which{Read or Write?}
Which -->|Detect/Read NAMES| RIPC["IPC: vault-detect-existing"]
RIPC --> RParse["envKeyNames(): KEEP name, DROP value"]
RParse --> RNames["return NAMES + paths only"]
RNames -.->|NEVER a value| UIback([Renderer shows key names])
Which -->|Write/Delete| WGate{"canWrite gate:\nprovider==command AND\nunlocked (count>0) AND helper set"}
WGate -->|fail-closed| Deny[/"write-not-permitted"/]
WGate -->|permitted| KeyVal{"VALID_KEY_NAME\n^[A-Za-z_][A-Za-z0-9_]*$"}
KeyVal -->|bad name| BadKey[/"bad-key (blocks KEY=VALUE / newline)"/]
KeyVal -->|valid| Spawn["execFileSync /bin/sh -c <user helper>"]
Spawn --> EnvName["KEY NAME -> HERMES_SECRET_KEY env (inert)"]
Spawn --> Stdin["VALUE -> helper STDIN ONLY\nnot argv/shell/env"]
EnvName --> Vault[("vault .kdbx")]
Stdin --> Vault
Spawn --> Result{exit code?}
Result -->|ok| OkR["ok:true, invalidate caches"]
Result -->|fail| FailR["coarse reason; VALUE never logged"]
OkR -.->|booleans only| UIback
FailR -.->|coarse reason only| UIback
```

If you'd rather not keep keys in a plaintext `.env`, the opt-in **command**
provider resolves them by running a helper command you configure. Resolution
order everywhere is: `process.env` → `.env` → provider → unset.
Expand Down
23 changes: 23 additions & 0 deletions changelogs/0.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
- **Agnes context window** — correct context-length mapping for Agnes and DeepSeek models
- **Reasoning effort** — reasoning effort exposed per-message for models that support it

### Secrets & Vault Onboarding
- **First-run vault creation** — the setup wizard can now create a new encrypted KeePassXC vault (generated key-file, 0600) for first-time users instead of assuming one already exists; no dead-end picker
- **Auto-detect existing vault** — on first run the wizard detects an existing tmpfs secrets dump or on-disk vault and auto-fills the helper command, showing the resolved key **names** (never values)
- **Opt-in TPM sealing** — after creating a vault, optionally seal its key-file to the TPM (via `systemd-creds`) for auto-unlock at boot; honestly falls back to 0600 file permissions when no TPM is present
- **Dependency-honest** — when `keepassxc-cli` is missing, the wizard shows an actionable install hint rather than failing silently
- **UID-safe paths** — all runtime/vault paths are derived from `XDG_RUNTIME_DIR` / the current uid / `XDG_DATA_HOME`; no hardcoded `/run/user/1000`
- **Snap-installed KeePassXC supported** — the CLI is resolved under both the native `keepassxc-cli` and Snap's `keepassxc.cli` names; a Snap-confined install (which can't write hidden `$HOME` dirs) gets a non-hidden `~/hermes/` vault location automatically, while native installs keep the XDG-correct hidden path. `--set-key-file` lets the CLI own key-file generation (verified against keepassxc-cli 2.7.9). TPM sealing honestly reports that `systemd-creds --with-key=tpm2` needs a one-time privileged step rather than failing silently
- **`ANTHROPIC_TOKEN` recognized as the anthropic key** — the install gate now treats `ANTHROPIC_TOKEN` (the gateway/Bearer credential name many vaults inject) as an accepted alias of `ANTHROPIC_API_KEY`, so a vault-only user no longer sees a false "ANTHROPIC_API_KEY is not set" warning when the gateway authenticates fine. Alias map is one-directional and scoped to anthropic only
- **Hardened against adversarial input (security)** — the tmpfs key-name parser and the generated shell commands are covered by an adversarial test suite (hostile key names with `=`/spaces/metachars are dropped; a `__proto__` key cannot pollute the prototype; vault paths with `$(...)`/backticks/`;`/quotes are kept inert — proven by running the produced command through `/bin/sh` and asserting an injection canary file is never created). The vault write/delete gate fails **closed** against a locked vault and is re-checked in the main process so a compromised renderer cannot bypass it. Secret **values** never cross to the renderer, never enter argv/the shell string/the process env, and are never logged. See **[docs/diagrams/vault-bootstrap-diagrams.md](../docs/diagrams/vault-bootstrap-diagrams.md)** for the logical, onboarding-state, and **secret-workflow** diagrams.

### Discover & Skills
- **Discover page** — new Discover screen for browsing and installing skills and MCP servers
- **MCP server management UI** — add, remove, and configure MCP servers from a dedicated UI
Expand Down Expand Up @@ -84,6 +94,19 @@
- Fixed network proxy settings not persisting across restarts
- Fixed local provider base URLs not persisting
- Fixed import backup file path resolution
- **Fixed false "missing API key" warnings and a blocked Send button in remote / SSH connection mode.** `validateChatReadiness` and the config-health key checks (`EMPTY_API_SERVER_KEY`, `MODEL_KEY_MISSING`) inspected the *local* `.env` for the model/server key even when the desktop was pointed at a remote (or SSH-tunnelled) hermes-agent gateway, where those keys live on the remote and the desktop only needs its connection credential. They now short-circuit on `remote`/`ssh` mode — mirroring the precedent `checkInstallStatus` already set — so remote / vault-only users are no longer falsely warned or blocked. A misconfigured `remote` mode with no `remoteUrl` still warns, and unrelated (non-key) audit checks still run in remote mode

```mermaid
flowchart TD
S["key-presence check<br/>(validateChatReadiness /<br/>config-health)"] --> M{"connection mode?"}
M -->|"remote + remoteUrl"| OK["return OK / skip check<br/>(keys live on the gateway)"]
M -->|ssh| OK
M -->|"remote, NO remoteUrl"| LOCALCHK
M -->|local| LOCALCHK["inspect local .env<br/>for expected key"]
LOCALCHK -->|present| OK
LOCALCHK -->|absent| WARN["MISSING_API_KEY /<br/>EMPTY_API_SERVER_KEY /<br/>MODEL_KEY_MISSING"]
M -.->|"getConnectionConfig() throws"| LOCALCHK
```

### SSH & Cron
- Fixed SSH API port fallback validation
Expand Down
80 changes: 80 additions & 0 deletions docs/diagrams/auto-update-gate-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Auto-update opt-out gate — diagrams

Diagrams for the `desktop.auto_update` opt-out feature (branch `secrets/04`).
Auto-update is **ENABLED BY DEFAULT**; only an explicit `desktop.auto_update: false`
(or `0`) in `config.yaml` disables it. The opt-out exists so a user running a
locally-built or patched `/opt` artifact can stop electron-updater from
re-downloading the public release and overwriting their build on quit
(`autoInstallOnAppQuit`).

## 1. Logical flow — the opt-out decision and the updater gate

```mermaid
flowchart TD
A["App launch → setupUpdater()"] --> B{"app.isPackaged<br/>AND not portable build?"}
B -->|"No (dev / portable)"| Z["Register no-op IPC handlers<br/>return — no autoDownload wiring"]
B -->|"Yes (packaged install)"| C["getConfigValue('desktop.auto_update')<br/>→ string | null"]
C --> D["isAutoUpdateDisabled(raw)<br/>shared single source of truth"]
D --> E{"normalized value<br/>=== 'false' or '0' ?"}
E -->|"Yes (explicit opt-out)"| Z
E -->|"No — null / unset / empty / garbage<br/>(fail-safe to upstream default)"| Y["Wire electron-updater:<br/>autoDownload + autoInstallOnAppQuit"]
Z --> ZZ["Updates never auto-installed<br/>local/patched build preserved"]
Y --> YY["Auto-update ON (community default)"]

classDef safe fill:#0b3d0b,stroke:#3fae3f,color:#d6ffd6;
classDef on fill:#0b2d4d,stroke:#3f8fd0,color:#d6ecff;
class Z,ZZ safe;
class Y,YY on;
```

## 2. SECRET / overwrite-gate workflow — what crosses each boundary

The "secret" being protected here is the user's **local build integrity** (their
patched `/opt` artifact). The gate decides whether the auto-updater is allowed to
overwrite it. Only NAMES/booleans cross the IPC boundary to the renderer — never
the artifact or any credential.

```mermaid
flowchart TD
subgraph CFG["config.yaml (operator-controlled, local FS)"]
K["key: desktop.auto_update<br/>value: false | 0 | (unset)"]
end

subgraph MAIN["Electron main process"]
G["isAutoUpdateDisabled()<br/>(../shared/auto-update-gate)"]
GATE{"fail-CLOSED to<br/>upstream default?"}
UPD["electron-updater<br/>autoDownload / autoInstallOnAppQuit"]
end

subgraph RND["Renderer (Settings toggle)"]
T["'Automatic updates' toggle<br/>shows ENABLED / DISABLED"]
end

ART["Local /opt build artifact<br/>(the asset being protected)"]

K -->|"raw string value"| G
G --> GATE
GATE -->|"explicit false/0 → DISABLED"| BLOCK["updater short-circuited<br/>artifact NOT overwritten"]
GATE -->|"anything else → ENABLED (safe default)"| UPD
UPD -.->|"may overwrite on quit"| ART
BLOCK -. protects .-> ART

G -. "boolean only (no secret)" .-> T
T -->|"writes 'true'/'false' via setConfig"| K

classDef boundary fill:#1a1a2e,stroke:#888,color:#eee;
classDef danger fill:#4d0b0b,stroke:#d05f5f,color:#ffd6d6;
classDef safe fill:#0b3d0b,stroke:#3fae3f,color:#d6ffd6;
class CFG,MAIN,RND boundary;
class UPD danger;
class BLOCK safe;
```

**Controls depicted:**
- The decision is computed in ONE place (`isAutoUpdateDisabled`, shared) and
consumed identically by the main-process gate and the renderer toggle — they
cannot drift (pinned by `autoUpdateGateParity.test.ts`).
- The gate **fails CLOSED to the upstream default (ENABLED)**: a typo / empty /
garbage config value can never silently disable security updates.
- Only a boolean crosses the IPC boundary to the renderer; no artifact bytes and
no secret values traverse it.
81 changes: 81 additions & 0 deletions docs/diagrams/install-gate-vault-alias-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Install gate — vault alias constraint (P1 fix) — diagrams

Diagrams for the install-gate vault-awareness fix (branch `secrets/04`, PR #673).

**The bug (Greptile P1):** when the catalogued provider's expected LLM key (e.g.
`ANTHROPIC_API_KEY`) was not resolved directly from the secrets provider, the gate
fell through to a broad `/(_API_KEY|_TOKEN)$/` scan that accepted **any**
token-shaped vault credential. A user whose vault held only `GITHUB_TOKEN` /
`SLACK_BOT_TOKEN` (and no LLM key) falsely cleared the gate and was shown the chat
screen instead of being routed back through Setup.

**The fix:** when `expectedKey` is known, accept **only** that key or one of its
accepted aliases (`aliasesForEnvKey()` over the single-source-of-truth
`KEY_ALIASES` in `src/shared/url-key-map.ts`). The broad fallback now fires **only**
when `expectedKey` is `null` (uncatalogued provider — no canonical name to match).
This brings `installer.ts` into agreement with `config-health.ts` `resolvedHasKey()`
(same alias-constrained logic). Member of the AIR-026 credential-name-alias class.

## 1. Logical flow — the install-gate vault decision

```mermaid
flowchart TD
A["checkInstallStatus()<br/>hasApiKey still false, non-env provider"] --> B["resolvedSecrets(profile)<br/>→ resolved map"]
B --> C["expectedKey = expectedEnvKeyForModel(provider, baseUrl)"]
C --> D{"expectedKey known?<br/>(catalogued provider)"}
D -->|"Yes"| E{"resolved[expectedKey] usable<br/>OR any aliasesForEnvKey() usable?"}
E -->|"Yes"| P["hasApiKey = true → chat"]
E -->|"No"| F["hasApiKey stays false → Setup"]
D -->|"No (uncatalogued)"| G{"any /(_API_KEY|_TOKEN)$/ usable?"}
G -->|"Yes"| P
G -->|"No"| F

classDef pass fill:#0b3d0b,stroke:#3fae3f,color:#d6ffd6;
classDef block fill:#4d0b0b,stroke:#d04f4f,color:#ffd6d6;
class P pass;
class F block;
```

The closed hole: a vault holding only `GITHUB_TOKEN` with `expectedKey =
ANTHROPIC_API_KEY` now lands on **F (Setup)**, not **P (chat)** — the broad-scan
branch (G) is unreachable for a known provider.

## 2. SECRET / credential-name workflow — what is matched, what crosses

The "secret" here is the user's LLM credential. The gate never sees or moves the
value across a boundary — it only asks "does a usable value exist under the
expected NAME (or an accepted alias of it)?" Key NAMES are matched; the value is
read only for a non-empty/`usable()` check and never logged or returned.

```mermaid
flowchart TD
subgraph VAULT["secrets provider (resolvedSecrets map — names + values, in-process)"]
V1["ANTHROPIC_API_KEY=… (canonical)"]
V2["ANTHROPIC_TOKEN=… (alias)"]
V3["CLAUDE_CODE_OAUTH_TOKEN=… (alias)"]
V4["GITHUB_TOKEN=… (UNRELATED — must NOT satisfy)"]
end
subgraph MAP["src/shared/url-key-map.ts (single source of truth)"]
M["KEY_ALIASES[ANTHROPIC_API_KEY]<br/>= [ANTHROPIC_TOKEN, CLAUDE_CODE_OAUTH_TOKEN]"]
end
GATE["vaultResolvedHasKey(resolved, expectedKey)<br/>usable() = string & non-blank"]
M --> GATE
V1 -->|"name matches expectedKey"| GATE
V2 -->|"name matches alias"| GATE
V3 -->|"name matches alias"| GATE
V4 -.->|"name NOT in {expectedKey ∪ aliases} → ignored"| GATE
GATE --> OUT["boolean only → hasApiKey<br/>(no value crosses to renderer)"]

classDef ok fill:#0b3d0b,stroke:#3fae3f,color:#d6ffd6;
classDef no fill:#4d0b0b,stroke:#d04f4f,color:#ffd6d6;
class V1,V2,V3 ok;
class V4 no;
```

## Verification

- 8/8 regression tests (`tests/installer-vault-gate.test.ts`); bug-repro reds
against pre-fix code (broad scan returned `true` for `{GITHUB_TOKEN}`).
- Typecheck clean (node + web); semgrep TS rules clean on `installer.ts`.
- AppSec verdict: SHIP (fail-closed on resolver error; no proto-pollution/ReDoS;
sibling-asymmetry with `config-health.ts` resolved).
130 changes: 130 additions & 0 deletions docs/diagrams/vault-bootstrap-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Vault Bootstrap — Diagrams

Diagrams for the first-run vault-bootstrap / secrets-provider onboarding feature.
All three are Mermaid (render natively on GitHub) and were validated to parse via
`mermaid.parse()` before commit.

---

## 1. Logical component / data flow

How the renderer, the main-process IPC layer, the bootstrap module, and the
external tools relate. Note the trust boundary: the renderer only ever receives
NAMES / paths / booleans / counts — never a secret value.

```mermaid
flowchart TD
subgraph Renderer["Renderer (untrusted)"]
SetupUI["Setup wizard / Settings - SecretsProviders"]
end

subgraph Preload["Preload bridge"]
API["window.api: vault-detect-existing, vault-create,\nsecrets-provider-can-write, -write, -delete"]
end

subgraph Main["Main process (trusted)"]
IPC["ipcMain handlers (re-check gates server-side)"]
Boot["vaultBootstrap.ts\ndetect / create / seal / tool-check"]
Write["commandProviderWrite.ts\nwrite / delete via sh helper"]
Gate["config.ts: secretsProviderCanWrite\n-> decideCanWrite (fail-closed)"]
Resolve["secrets/index.ts\nproviderListSafe / resolvedSecretMap"]
end

subgraph External["External (OS)"]
KP["keepassxc-cli / keepassxc.cli"]
TPM["systemd-creds --with-key=tpm2"]
FS["vault .kdbx + key-file (0600)"]
TMPFS["tmpfs dump\n$XDG_RUNTIME_DIR/hermes-secrets.env"]
end

SetupUI -->|invoke| API --> IPC
IPC --> Boot
IPC --> Write
IPC --> Gate
Gate --> Resolve
Boot -->|spawn, timeout-bounded| KP
Boot -->|opt-in seal| TPM
Boot -->|chmod 0600 / 0700| FS
Boot -->|read NAMES only| TMPFS
Resolve -->|raw vault list| KP
IPC -.->|NAMES / paths / booleans only\nNEVER a value| API
```

---

## 2. First-run onboarding state machine

The "assume nothing exists" flow — every detect path has a matching create path,
and a missing dependency surfaces an install hint rather than a dead end.

```mermaid
stateDiagram-v2
[*] --> Detect: first run
Detect --> Found: tmpfs dump OR vault file on disk
Detect --> NotFound: nothing resolvable

Found --> Prefill: suggest read command (UID-safe)
Prefill --> ModelStep: provider resolves the model key -> hide key field

NotFound --> ToolCheck: checkToolAvailability()
ToolCheck --> CanCreate: keepassxc-cli present
ToolCheck --> InstallHint: CLI missing
InstallHint --> Detect: user installs, retry

CanCreate --> Create: createVault()
Create --> CreateOk: kdbx + key 0600, command returned
Create --> CreateFail: vault-already-exists / db-create-failed
CreateFail --> ToolCheck: surface honest error

CreateOk --> SealChoice: offer opt-in TPM seal
SealChoice --> Sealed: systemd-creds ok -> sealed=true
SealChoice --> Fallback: polkit/no-tpm -> sealed=false, 0600 stands
Sealed --> ModelStep
Fallback --> ModelStep

ModelStep --> [*]: provider configured, setup complete
```

---

## 3. SECRET workflow (security-critical)

How a secret VALUE and a KEY NAME move through the system, and exactly where each
is gated. This is the diagram that encodes the threat-model controls: the VALUE
never crosses to the renderer and never enters argv / the shell string / the
process env; the KEY NAME is validated before it touches a helper; writes are
fail-closed against a locked vault.

```mermaid
flowchart TD
Start([User edits/reads a secret in the UI]) --> Which{Read or Write?}

%% READ / DETECT path
Which -->|Detect/Read NAMES| RIPC["IPC: vault-detect-existing"]
RIPC --> RParse["envKeyNames(): regex ^[A-Za-z_][A-Za-z0-9_]*=\nKEEP name, DROP value"]
RParse --> RNames["return NAMES + paths only"]
RNames -.->|NEVER a value| UIback([Renderer shows key names])

%% WRITE path
Which -->|Write/Delete| WGate{"secretsProviderCanWrite()\ndecideCanWrite: provider==command\nAND providerListSafe count > 0 (unlocked)\nAND helper configured"}
WGate -->|fail-closed| Deny[/"return write-not-permitted\n(locked vault / no helper)"/]
WGate -->|permitted| KeyVal{"VALID_KEY_NAME test\n^[A-Za-z_][A-Za-z0-9_]*$"}
KeyVal -->|bad name| BadKey[/"return bad-key\n(blocks KEY=VALUE / newline injection)"/]
KeyVal -->|valid| Spawn["execFileSync /bin/sh -c <user helper>"]

subgraph Spawnenv["how the secret crosses to the helper"]
direction TB
EnvName["KEY NAME -> HERMES_SECRET_KEY env (inert data)"]
Stdin["VALUE -> helper STDIN ONLY\nnot argv, not shell string, not env"]
end
Spawn --> EnvName
Spawn --> Stdin
EnvName --> Vault[("vault .kdbx")]
Stdin --> Vault

Spawn --> Result{exit code?}
Result -->|ok| OkR["return ok:true\ninvalidate caches"]
Result -->|fail| FailR["return coarse reason\nexit-N / timeout\nstderr piped+discarded\nVALUE never logged"]
OkR -.->|booleans only| UIback
FailR -.->|coarse reason only| UIback
```
Binary file added docs/images/keepassxc-vault/01-provider-setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/keepassxc-vault/01b-key-entered.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/keepassxc-vault/02-secrets-step.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/keepassxc-vault/04-helper-filled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading