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"]) andbuildHostApi(ctx)filtering byctx.kind, so runner and webview principals see disjoint capability sets.transport-ws.js(Cap’n Web string transport over a socket, with aCANCELcontrol frame) andrunner-host.js— a per-windowwsserver on127.0.0.1:0, 32-byte single-use run tokens delivered via env,AUTH <token>first-frame within 3 s, per-run capnweb session.wsadded as a dependency; the committeddist/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 packageserialize()), and cancel/timeout (protocol-levelCANCEL→ SIGTERM → SIGKILL).permissions.js/consent.js—// @permissions/// @timeoutheaders, parsed and consented once per script (approvals inatom.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.jsonimport map (tranquil/automation→ the shippedmod.ts).runner-capabilities.js— the runner-audiencetabs/ui/files/clipboard/config/workspacehost capabilities (nonce-tagged tab handles, background-webview fan-out capped at 4 with partition inheritance,configrestricted toscriptState.*).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(thetranquil/automationAPI),tabs.ts, the ownedcdp.tsclient (grown from the test-suite seed),rpc.ts,transport.ts,context.ts, andmain.ts(the bootstrap — see findings). Host-capability calls go through ahostCall()wrapper that re-throws on failure, so a rejected host error (tabs.active()with no tab, a badconfigkey) surfaces with the script’s ownfile:lineinstead of Cap’n Web’s read-loop internals.- A ▶ Run button on every
.tseditor’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 1sscript reachestimed-out;Deno.readTextFile("/etc/hosts")is denied). - Webview RPC is unregressed — the additive audience default (
["webview"]) leaves existing callers untouched, guarded now by the automatedrpc-webview-injectionsuite (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 satrunninguntil the timeout. Fix:deno runexecutes a tinydeno/main.tsbootstrap that pre-imports the runtime (keeping--allow-readscoped to<scriptDir>), dynamically imports the user script viaTRANQUIL_ENTRY, thenDeno.exit(0). A top-level throw stays uncaught so Deno prints its nativefile:lineerror. - CDP cannot screenshot
<webview>guests —Page.captureScreenshottimes out even for a visible tab (an OOPIF-family limit), sotab.screenshot()is host-mediated viawebview.capturePage(), the one exception to the direct-CDP data plane. --allow-importsemantics.https:/jsr:imports fail closed off the pinned allowlist under--no-prompt, butnpm:registry fetches are exempt — documented as a residual supply-chain surface (the runtime’s owncapnwebarrives vianpm:).- capnweb runs on Deno first try (no vendored fallback needed).
- The CDP
:9222grant 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-:9222item.
Follow-ups (not blocking)
- Filtered CDP proxy — the mitigation for the host-window CDP-escape risk and the template for
closing the open
:9222endpoint. - 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.