Docs

Automations v2 — Phase 1 Delivery Notes

The Deno subprocess runner (ADR-0022) shipped on 2026-08-13. This is the as-built record — what landed, how it was verified, the implementation findings that adjusted the design, and the follow-ups left open. The plan and threat model live in the design notes, ADR-0022, and the security review; this note is the delivery log.

What shipped

Automation scripts are now TypeScript modules run in a sandboxed deno run subprocess, not AsyncFunction text evaluated in the host renderer. The in-renderer runner, the injected require/atom globals, tranquil.exec, and the puppeteer-core dependency are removed — a clean break, no shim.

tranquil-rpc — the WS bridge and a new principal.

  • registerCapability(name, factory, { audience }) (default ["webview"]) and buildHostApi(ctx) filtering by ctx.kind, so runner and webview principals see disjoint capability sets.
  • transport-ws.js (Cap’n Web string transport over a socket, with a CANCEL control frame) and runner-host.js — a per-window ws server on 127.0.0.1:0, 32-byte single-use run tokens delivered via env, AUTH <token> first-frame within 3 s, per-run capnweb session. ws added as a dependency; the committed dist/ rebuilt.

tranquil-automations — the run manager, runtime, and UI.

  • run-manager.js — spawn per run with computed permission flags, output ring buffer (last 100 runs, 64 KB each, persisted through the package serialize()), and cancel/timeout (protocol-level CANCEL → SIGTERM → SIGKILL).
  • permissions.js / consent.js// @permissions / // @timeout headers, parsed and consented once per script (approvals in atom.config, re-prompt on any change); run= grants rendered honestly (“can run commands as you”).
  • deno.js / deno-config-seed.js — Deno resolution (config override → PATH) and the seeded ~/.tranquil/automations/deno.json import map (tranquil/automation → the shipped mod.ts).
  • runner-capabilities.js — the runner-audience tabs / ui / files / clipboard / config / workspace host capabilities (nonce-tagged tab handles, background-webview fan-out capped at 4 with partition inheritance, config restricted to scriptState.*).
  • runs-panel.js — the Automation Runs bottom-dock panel (state, duration, streaming output, Cancel, click-through from failure notifications; a draggable list/detail divider).
  • deno/ — the runtime library: mod.ts (the tranquil/automation API), tabs.ts, the owned cdp.ts client (grown from the test-suite seed), rpc.ts, transport.ts, context.ts, and main.ts (the bootstrap — see findings). Host-capability calls go through a hostCall() wrapper that re-throws on failure, so a rejected host error (tabs.active() with no tab, a bad config key) surfaces with the script’s own file:line instead of Cap’n Web’s read-loop internals.
  • A ▶ Run button on every .ts editor’s tab bar; cmd-shift-R, palette commands, and config-driven URL triggers all route through the run manager.

tranquil-examples — the five seeded scripts ported to .ts plus the new fetch-titles.ts multi-URL fan-out example; lessons and READMEs rewritten for the sandboxed TypeScript model.

tranquil-test-suite — a new rpc-webview-injection suite (trusted file:// gets window.tranquilHost.ping()"pong"; untrusted gets nothing), and port isolation so the suite runs on :9223 alongside a dev instance on :9222.

Gate — passed

The Phase 1 gate was all seeded examples pass · cancel/kill works · zero regression in webview RPC:

  • Examples run as sandboxed subprocesses, including the fan-out (produces titles.tsv); .js-era scripts warn and point at the migration.
  • Cancel/kill verified end-to-end (run-manager suite: a 60 s sleeper dies in ~2 s on cancel; a @timeout 1s script reaches timed-out; Deno.readTextFile("/etc/hosts") is denied).
  • Webview RPC is unregressed — the additive audience default (["webview"]) leaves existing callers untouched, guarded now by the automated rpc-webview-injection suite (full suite 9/9 green).

Findings that changed the design

Recorded here and folded back into ADR-0022:

  • Runs never ended without a bootstrap. The runtime holds the RPC and CDP sockets open, which keep Deno’s event loop alive past the script’s top-level await — so a script run directly sat running until the timeout. Fix: deno run executes a tiny deno/main.ts bootstrap that pre-imports the runtime (keeping --allow-read scoped to <scriptDir>), dynamically imports the user script via TRANQUIL_ENTRY, then Deno.exit(0). A top-level throw stays uncaught so Deno prints its native file:line error.
  • CDP cannot screenshot <webview> guestsPage.captureScreenshot times out even for a visible tab (an OOPIF-family limit), so tab.screenshot() is host-mediated via webview.capturePage(), the one exception to the direct-CDP data plane.
  • --allow-import semantics. https:/jsr: imports fail closed off the pinned allowlist under --no-prompt, but npm: registry fetches are exempt — documented as a residual supply-chain surface (the runtime’s own capnweb arrives via npm:).
  • capnweb runs on Deno first try (no vendored fallback needed).
  • The CDP :9222 grant reaches host-window targets — an accepted risk (no worse than today); the recorded mitigation is a host-side filtered CDP proxy that also closes the open-:9222 item.

Follow-ups (not blocking)

  • Filtered CDP proxy — the mitigation for the host-window CDP-escape risk and the template for closing the open :9222 endpoint.
  • Deferred by design: paneControls-from-scripts (the API is a throwing stub reserving the shape), Windows/Linux, bundled Deno / packaging (an ADR-0023 concern), and the automations security review’s unchanged browser-side items.

Next

Phase 2 is the in-app engine spike (ADR-0023, still proposed) — its 10-item checklist is the gate. The step-primitives module ships with local scripts as an ephemeral journal, so Phase 3’s step.* surface is authored against the same runtime this phase delivered.