Electron Stagewright docs

ADR-014: Security posture and threat model

Status: Accepted

Context

Electron Stagewright drives real desktop applications, and with an eval opt-in (--allow-eval or a target-specific variant) it runs arbitrary JavaScript inside the app under test (main and/or renderer). That power is the point — it is the escape hatch for flows no granular tool covers — but it means an operator deciding whether to point an agent at the server needs to understand exactly what the server can reach and what constrains it.

Those constraints already exist, but they were scattered across individual ADRs, code comments, and a placeholder "in progress" note in .github/SECURITY.md. No single document stated the trust model, enumerated the controls, or named the residual risks. This ADR records the overall security posture as a decision and anchors the published threat model; the detailed analysis lives in ../guides/security-model.md.

Decision

1. The server is a privileged local tool, not a sandbox

The server runs with the operator's OS privileges and can launch processes, read files within its launch surface, and (under an eval opt-in) execute arbitrary code in the target app. It does not sandbox the agent. The trust boundary is therefore the agent host: only a trusted agent host should be allowed to invoke the server. The default transport is stdio (a local child process), not a network listener, which keeps that boundary local by construction.

2. Eval is the central risk and is default-deny

3. The supporting controls

Capture/instrumentation is bounded by explicit channel allowlists (IPC) with opt-in redact for captured payloads; launches are confined (--app-root blocks .. escape; runtime-altering env vars like NODE_OPTIONS / LD_* / DYLD_* are refused); the protocol channel is kept clean (stdout is JSON-RPC only, all diagnostics to stderr); a per-operation timeout backstop (ADR-011) prevents a hung app from wedging the dispatch; untrusted-string lookups avoid prototype pollution.

4. The threat model is published and kept honest

The canonical threat model is docs/guides/security-model.md; .github/SECURITY.md summarises it and carries the reporting policy. A CI guard asserts the threat model names every --allow-eval-gated tool, so a future eval-gated tool cannot ship without a security-model entry.

5. Structural eval inspection is defence-in-depth, not a sandbox

AST inspection augments the substring blocklist but remains a deliberately limited defence-in-depth pass. It catches obvious structural variants that are cheap to identify, while the supported posture stays the safe default (opt-in + blocklist + AST preflight + audit + cap) plus an honest statement of the residual risk.

Rationale

Eval cannot be removed without gutting the escape-hatch use case, and it cannot be fully sandboxed without defeating its purpose (driving the real app). The proportionate posture for a pre-1.0 local tool is: make the dangerous surface opt-in and default-deny, add cheap defence-in-depth, keep diagnostics off the protocol channel, make captured-data risk explicit with redaction hooks, and state plainly that the operator owns the trust boundary. Publishing the model — including the parts that are only defence-in-depth — is more useful than implying a stronger guarantee than the code makes.

Alternatives considered

Consequences

References

Status Update — 2026-06-14

The first hardening increment from §5 has shipped: per-target eval authorization and a content-hash audit. AST inspection remains the single deferred item.

Status Update — 2026-06-15

The last deferred item from §5 — AST structural inspection — has shipped, so nothing on the eval-hardening list remains deferred.

Status Update — 2026-06-22: renderer-eval surfaced to plugins (ctx.allowEvalRenderer)

The per-target eval policy gains a plugin-facing renderer signal, consumed by the storage plugin's new per-key localStorage / sessionStorage tools (ADR-018 Status Update).