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_DIRis a user-writable cache — all readable or reachable by any process running as the same OS user. This is consistent with the existing:9222stance: 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
atomAPI,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.execdies. Arbitrary shell becomes declared// @permissions run=…grants behind an explicit consent prompt, default-deny (--no-promptfails closed)1. [done]- Config writes are scoped. Scripts could write arbitrary
atom.configkeys; the newconfigcapability accepts onlytranquil-automations.scriptState.*. [done] - Capability partitioning by principal (audience tagging).
registerCapabilitygains anaudienceoption defaulting to["webview"];buildHostApifilters 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
:9222endpoint. [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
- CDP grant reaches host windows — potential app takeover. [accepted] The runner’s
--allow-netmust include the shared CDP endpoint, and that endpoint lists host windowpagetargets alongside webview guests (verified against the live app). A script can attach to a host window target andRuntime.evaluatewith 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/jsondiscovery and target sockets, exposing onlytype: "webview"targets on the per-run token model — which would close this and the open-:9222item together. Re-evaluation trigger: shipping automations to users who run scripts they did not author. - Module-graph fetching bypasses
--allow-net. [partial] Deno resolves remote imports (npm:,jsr:,https:) outside the net-permission model;--allow-importgovernshttps: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-importexplicitly 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. run=grants are escalators. [accepted]--allow-run=git(ornpm,sh, most developer tools) is effectively full user privilege — many binaries can be induced to execute arbitrary commands. The consent prompt rendersrun=grants honestly (“can run commands as you”), never as narrowly scoped.scriptDiris 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.- 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).
- Runner auth is new security-critical code. [open until review]
runner-host.jsjoinstrust.jsunder 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: onfor 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
remotemodule in the preload. - No CSP on trusted mockups;
allowfileaccessfromfiles; navigation/window-open gating; the bp-clienteval()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-importand 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.
- Deno security and permissions — per-resource
--allow-*scoping,--no-promptfail-closed behavior, and--allow-importgoverning remote module imports separately from--allow-net. https://docs.deno.com/runtime/fundamentals/security/↩