Docs

Automations v2 — Security Review

Design-time review, 2026-08-11 — before Phase 1 implementation. The automations modernization (ADR-0022, ADR-0023) checked against the Security Considerations living threat model and the RPC Trust Gap findings. This page is the durable home for the assessment; the ADRs carry only the decision-level deltas, and the living tracker carries status-tagged items. Statuses here follow the same legend: [done] mitigated in the design · [partial] partially addressed · [open] tracked risk · [accepted] known risk, deliberately carried with a recorded mitigation path.

The runner threat model (the new principal)

ADR-0022 introduces a third RPC principal beside the host renderer and webview guests: a local Deno subprocess authenticated by possession of a one-time run token. Its security story is two-layer, default-deny: Deno permission flags bound direct system access (filesystem, network, env, subprocess)1; host capabilities bound app-side effects (tabs, notifications, config). Two boundaries are explicitly out of scope:

  • The same-user boundary is not defended. The run token travels in the child’s environment, the WS bridge listens on loopback, and DENO_DIR is a user-writable cache — all readable or reachable by any process running as the same OS user. This is consistent with the existing :9222 stance: a local same-user attacker already owns the session. Stated once, here, rather than implied piecemeal.
  • The script is chosen by the user. The sandbox bounds what a script can do without consent; it does not attempt to make a malicious consented script safe. Consent is the authority boundary for everything beyond the base grants.

How the effort improves the posture

  • Removes the app’s largest standing privilege grant. Today’s runner evaluates arbitrary script text in the host renderer with full Node, the full atom API, exec, and no kill switch — the inversion the living threat model complains about. Scripts move out of process into the sandbox above. [done] (by design; lands with Phase 1)
  • tranquil.exec dies. Arbitrary shell becomes declared // @permissions run=… grants behind an explicit consent prompt, default-deny (--no-prompt fails closed)1. [done]
  • Config writes are scoped. Scripts could write arbitrary atom.config keys; the new config capability accepts only tranquil-automations.scriptState.*. [done]
  • Capability partitioning by principal (audience tagging). registerCapability gains an audience option defaulting to ["webview"]; buildHostApi filters by the session’s principal kind. Webview guests can never see runner capabilities and vice versa — “untrusted = zero RPC surface” is preserved and strengthened for every future consumer. [done]
  • The token bridge is a hardened new gate — loopback-only, ephemeral port, 32-byte single-use token delivered via env (never in a URL), 3-second auth window, close-before-session on any failure. It is a working reference implementation of the token/handshake trust option sketched in RPC Trust Gap, and the named template for eventually closing the open :9222 endpoint. [done] for the runner path; the webview trust gaps themselves remain (below).
  • Availability. Runs are cancellable (protocol-level cancel, kill backstop) and time-limited (default 10 min); a hung script can no longer hang the editor thread. Token expiry, the background-webview cap with FIFO queueing, per-run output caps, and URL-trigger debouncing are built-in resource limits. [done]
  • Auditability. The Runs panel keeps per-run state, duration, and output for the last 100 runs — the first record of what automation code actually did. [partial] (observability, not a tamper-proof audit log)

New risks the effort introduces

  1. CDP grant reaches host windows — potential app takeover. [accepted] The runner’s --allow-net must include the shared CDP endpoint, and that endpoint lists host window page targets alongside webview guests (verified against the live app). A script can attach to a host window target and Runtime.evaluate with full renderer privileges — Node, atom, everything the sandbox denies. Deno flags cannot bound this; it is not worse than today (any local process can already attach to :9222), but ADR-0022’s two-layer claim carries this caveat explicitly. Recorded mitigation path: a host-side filtered CDP proxy — the runner bridge brokers /json discovery and target sockets, exposing only type: "webview" targets on the per-run token model — which would close this and the open-:9222 item together. Re-evaluation trigger: shipping automations to users who run scripts they did not author.
  2. Module-graph fetching bypasses --allow-net. [partial] Deno resolves remote imports (npm:, jsr:, https:) outside the net-permission model; --allow-import governs https: imports and registry fetches are permitted regardless1. Imports are therefore a supply-chain and covert-channel surface the consent prompt does not cover. Phase 1 pins --allow-import explicitly in the computed flags (fail-closed; exact allowlist fixed by the Phase 1 spike), and the writing-automations docs must state that a script’s import graph is trusted code.
  3. run= grants are escalators. [accepted] --allow-run=git (or npm, sh, most developer tools) is effectively full user privilege — many binaries can be induced to execute arbitrary commands. The consent prompt renders run= grants honestly (“can run commands as you”), never as narrowly scoped.
  4. scriptDir is a shared trust domain. [accepted] The default --allow-write=<scriptDir> lets a script rewrite sibling scripts that may hold broader approved grants. Scripts in one directory share fate; documented rather than partitioned in Phase 1.
  5. URL-triggered runs are drive-by-triggerable. [partial] A page matching a user-registered URL trigger causes a consented script to run against it. Registration is config-only (no script- or page-side API), runs are debounced, and the triggered script must treat page content as hostile — but the trigger itself fires on hostile navigation. Strictly narrower than the old auto-inject (which ran unsandboxed).
  6. Runner auth is new security-critical code. [open until review] runner-host.js joins trust.js under the same discipline: default-deny, reviewed on every change. Invariants: tokens are single-use, never appear in URLs, expire unclaimed after 60 s, the socket closes on any auth failure before a session exists, and no frame reaches the RPC layer pre-auth.

For the engine (ADR-0023, later phases), the same review adds: the webhook listener needs authentication beyond loopback binding (any local process can POST to a bare local port — same honesty as :9222); journal rows can hold sensitive step results (carried in ADR-0023’s consequences: encryption at rest settled before any sync ships); scheduled runs must never consent unattended — a changed @permissions declaration blocks the run pending re-approval, never auto-approves and never prompts at 3 a.m.; and the child↔engine token socket inherits the runner-bridge auth rules verbatim.

What this effort does not address

The browser-side items in Security Considerations are untouched — this effort neither worsens nor fixes them:

  • nodeIntegration: on for all pages including remote (the tracker’s #1 open risk).
  • The over-broad contextBridge (arbitrary IPC channels) and missing IPC-sender validation.
  • Auto-granted webview permission requests; the deprecated remote module in the preload.
  • No CSP on trusted mockups; allowfileaccessfromfiles; navigation/window-open gating; the bp-client eval() of context-menu strings.
  • RPC Trust Gap, both gaps. Gap 1 (no production trusted roots registered) remains — the new RPC smoke suite registers a root in its test fixture only. Gap 2 (the file://-only trust model vs. http-served app content) remains — though the runner bridge now provides the exact token/handshake mechanism a fix would reuse.

Sources

  • Deno security and permissions1 — the permission model both layers of the runner sandbox rest on, including --allow-import and the module-fetch exemption from --allow-net.
  • Security Considerations — threat model, status legend, and the browser-side open items restated above.
  • RPC Trust Gap — the trust-model gaps and the token/handshake option the runner bridge implements.

  1. Deno security and permissions — per-resource --allow-* scoping, --no-prompt fail-closed behavior, and --allow-import governing remote module imports separately from --allow-net. https://docs.deno.com/runtime/fundamentals/security/