Mandatory for any change to: key handling or the keychain bridge, IPC commands, provider
base-URL handling, the run_command sandbox, prompt/tool-call construction, the DB
encryption (SQLCipher) path, or a new third-party dependency. When in doubt, run it.
-
Scope the surface. From the diff, identify which of the surfaces below it touches.
Read security-review.md and the keychain
reference before judging.
-
STRIDE-lite framing. Walk the change against the threats that actually apply to a
local-first agent runner:
- Spoofing / Tampering — can an agent-config value (a base URL, a tool arg) redirect
a call or alter what executes?
- Information disclosure — can a key, prompt, or raw provider payload escape to the
frontend, a log, the DB, or an error/event?
- Elevation of privilege — can model output or a tool result act as a trusted
instruction, or can sandboxed code reach ambient authority?
- Denial of service — can a hung provider or runaway sandbox pin a worker open?
-
Keys and secrets. Confirm keys live only in the OS keychain and are resolved at call
time, host-aware per ADR-0018:
on the desktop, the WebView adapter passes only a key reference to the Rust
llm_stream command, which reads the actual key from the keychain and attaches the
Authorization header inside Rust — the raw key never enters the WebView's JS runtime; on
the Node-style surfaces (CLI, VS Code host, Phase-2 Bun API) the adapter resolves the
key and attaches it just before the HTTPS request inside the one trusted process.
- No key in a Tauri IPC payload to the WebView, a Zustand store, a React prop,
localStorage, or an IPC return value — the frontend learns only that a provider is
configured, and the WebView adapter holds only a key reference, never the raw key.
- No plaintext at rest: no key in a config file, a committed
.env, a .relavium.yaml,
a log, or an unencrypted DB column (the DB is SQLCipher; secrets still belong in the
keychain).
- No key interpolated into an error message or a
node:failed/run:failed event.
- Desktop Rust egress (ADR-0018). If the diff touches the
llm_stream Tauri command
or the WebView adapter's transport, confirm: the WebView passes a { providerId, keyId }
key reference (never the raw key); Rust resolves the key from the keychain, attaches the
header, and never logs or persists it; and only Channel<StreamChunk> frames cross back —
no raw key and no RunEvent cross IPC (the RunEventBus is WebView-side). See
ipc-contract.md.
- Grep:
grep -rni "apikey\|api_key\|secret\|token" $changed_files then trace each hit
to confirm it never crosses to the frontend (as a raw key), a log, or a payload.
-
SSRF on custom base URLs. For any code reaching a user-supplied baseURL (DeepSeek /
any OpenAI-compatible provider): HTTPS only; reject non-HTTP(S) schemes and
credentials-in-URL; block private/loopback/link-local/metadata ranges (127.0.0.0/8,
::1, 10/8, 172.16/12, 192.168/16, 169.254/16 incl. 169.254.169.254) unless the
user explicitly opted into a local endpoint. An agent-config URL must never make the
engine call an internal address with a real key attached. Confirm TLS verification is not
disabled and every outbound call carries an AbortSignal + timeout.
-
The run_command sandbox. run_command spawns model-driven shell execution, so it
runs sandboxed: only commands on the workflow's allowedCommands allowlist execute (never
an unlisted command), under the workflow's filesystem scope tier (restricted fs — no
reach outside the granted paths), with no network authority beyond what an allowed
command itself performs, and under a CPU/memory/time budget that terminates a runaway. It
never receives a provider key or any secret, and its output (stdout/stderr/exit code) is
untrusted input. See built-in-tools.md.
-
Prompt-injection posture. Model output and tool results are untrusted data, never
trusted instructions. Confirm: requested tool calls are validated against the declared
tool schema and the user's tool allowlist before execution (a model cannot invoke an
ungranted tool; args are schema-checked, not eval'd); high-impact effects (filesystem
writes, shell, external calls) route through the human-gate/approval path; the system
field is set by Relavium, never by tool output, and untrusted content is never
concatenated where it can override the system prompt.
-
Dependency provenance and crypto. A new third-party dependency is a new attack
surface — it requires an ADR and sign-off; check the
package.json and lockfile diff and that no known-CVE dependency is knowingly added.
Confirm no hand-rolled crypto/TLS/keychain primitive — vetted platform
implementations only, wrapped behind a Relavium interface. A PR that rolls its own crypto
is rejected.
-
Logging. No secrets, no full prompts/responses, no raw keys in logs — ever
(redaction per logging-and-observability.md).
-
Report findings as file:line — severity — issue — impact — fix, and state the
verdict: any unresolved finding on the surfaces above blocks the merge.