From 4ac7656afaa887991a05b608e6f92db5b9d24c70 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Mon, 20 Jul 2026 12:41:22 +0200 Subject: [PATCH 01/18] docs: record extension-platform investigation evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First artifact on this branch. This is deliberately NOT the plan — the architecture decision (compiled-in vs runtime-loaded vs iframe) is still open at §8 and four consultants are being run against it. Committing the evidence first because the expensive mistake here is planning against an unverified substrate: three of the four constraints that decide this design (the CSP, the dev/prod origin split, and the fact that interaction-ownership and paint-order are DOM-level contracts) are non-obvious and were each found by reading, not by assumption. Also records that workflow-mcp is already a complete extension host — sandbox, approval store, utilityProcess, SHA-256 pinning — which reframes the whole problem as UI-only. Co-Authored-By: Claude Opus 4.8 --- ...-07-20-extension-platform-investigation.md | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-20-extension-platform-investigation.md diff --git a/docs/superpowers/specs/2026-07-20-extension-platform-investigation.md b/docs/superpowers/specs/2026-07-20-extension-platform-investigation.md new file mode 100644 index 00000000..ebf67c6e --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-extension-platform-investigation.md @@ -0,0 +1,305 @@ +# Extension Platform — Investigation Findings + +> **Status:** live. This is the *evidence* the design will be argued from, not the design. +> The architecture decision is deliberately **still open** at the bottom of this file — +> four consultants are being run against it. The plan file lands after they report. + +**Date:** 2026-07-20 +**Branch:** `feat/extension-platform` + +--- + +## 1. What is being built, in the user's words + +Personal workflow tools that live inside Agent Code — *not* product features, *not* MCP +tools for the agent to call. + +- Installed and managed **from Settings**, sourced from GitHub. +- Invoked by a palette command (`Open Timer`). +- Opens **"a VS Code like custom page"** for that extension. +- Backed by an API with **access to a lot of app features** — the reference example (a + timer) needs almost none of it, but "app-integrated extensions" are the point. + +The timer is the forcing function, not the goal. The goal is a platform where the user +(and later others) can add workflow tools without touching core app source. + +--- + +## 2. The single most important finding + +**Agent Code already ships an extension host.** `packages/workflow-mcp` executes +user-authored JavaScript in production today, with every hard problem already solved: + +| Extension-host problem | Where it is already solved | +|---|---| +| Discover user code on disk | user-level + project-level workflow dirs | +| Read metadata without executing it | `acorn` parses the `meta` export as a pure AST literal | +| Integrity | SHA-256 of source bytes; 512 KB size cap | +| Consent | Electron dialog keyed to `canonicalIdentity + sourceHash` (`WorkflowSourceApprovalStore`) | +| Sandbox | `node:vm`, null-prototype global, `codeGeneration: { strings: false, wasm: false }` | +| Determinism | `Date.now()` / `Math.random()` removed from the realm | +| Process isolation | separate Electron `utilityProcess` — *"Agent Code Workflow Evaluator"* | +| Lifecycle | heartbeats, timeouts, cancel, kill | + +**Implication:** the question was never "how do we host user code." That is done. The +only open question is **how user-authored UI reaches the screen.** + +What workflow-mcp does *not* have: any UI contribution story at all. + +--- + +## 3. Renderer constraints (verified, with evidence) + +### 3.1 Content-Security-Policy + +`src/renderer/index.html:15-18` — the app's only CSP: + +``` +default-src 'self'; +img-src 'self' data: blob:; +style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; +font-src 'self' data: https://fonts.gstatic.com; +script-src 'self'; +worker-src 'self' blob: +``` + +- No `'unsafe-eval'`, no `blob:` for scripts → the "read source text and evaluate it" + family of workarounds is closed. +- `worker-src 'self' blob:` was tuned **deliberately** (the inline comment cites #513 — + Monaco's blob workers were dying silently). This policy is load-bearing, not incidental. +- `src/remote-client/index.html` has **no** CSP at all — a second renderer surface that + any UI contribution model has to account for. + +**Correction worth recording:** one investigation agent reported "no CSP is set anywhere" +— a grep miss that did not cover `.html`. That conclusion was wrong and its downstream +reasoning ("no browser-level containment to build on") must not be reused. + +### 3.2 Document origin differs between dev and prod + +`src/main/window/mainWindow.ts:424-428` + +```ts +if (process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) // dev: http://localhost +} else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) // prod: file:// +} +``` + +A design that works in one mode and not the other is the trap here. A `file://` import of +a home-directory bundle plausibly passes in prod and is double-blocked in dev. **Any +loading scheme must be identical in both modes** — which a custom protocol satisfies and +a raw filesystem path does not. + +### 3.3 Window hardening + +`src/main/window/mainWindow.ts:334-336` — `sandbox: false`, `contextIsolation: true`, +`nodeIntegration: false`. Renderer-hosted code gets no Node, no `require`, no fs. This is +correct and should not change. + +### 3.4 Everything in the UI layer is statically registered + +| Registry | Shape | Evidence | +|---|---|---| +| Surfaces | 3 module-scope `const` arrays, 25 static imports | `app/surfaces/registry.tsx:41,:87,:92` | +| Commands | one `const commandDefs` from 14 imported arrays (~95 defs) | `features/command-palette/registry.ts:26` | +| Pane kinds | exhaustive `Record` that `throw`s on unknown | `providers/registry.renderer.ts:53` | +| Keybinds | one hardcoded capture-phase `document` listener | `workspace/tile-tree/useKeybinds.ts:897` | + +There is **no add/remove/register function anywhere** in the renderer UI layer. + +The *consumption* side is already generic, though — `GlobalModals` / `GlobalOverlays` / +`SidePanels` just `.map()` an array, and `buildCommandRegistry` is recomputed per context +change. A second array concatenated into each map is a small change. + +### 3.5 A "page" cannot be a split pane + +`workspace/types.ts:38-39` — `TileNode` leaves are `{ type: 'leaf'; sessionId }`. Content +dispatch runs through a closed `SessionKind` union that throws on unknown ids. + +Custom UI therefore mounts as **modal / overlay / side-panel**, not as a tile, unless a new +non-session `TileNode` variant is added — which touches treeOps, geometry, persistence and +keybinds. Out of scope for v1. + +### 3.6 Two integration contracts that do not cross a frame boundary + +This is the finding that most threatens the iframe design: + +- **Interaction ownership is a DOM attribute query** — + `lib/interaction-ownership.ts:22-36` reads `data-agent-code-interaction-owner="app"` + from the DOM. +- **Paint order is DOM sibling index** — `app/surfaces/registry.tsx:33-38,:44-59` + documents that array position *is* z-order, and that a prior PR (#505) shipped a + stacking regression by grouping semantically instead of by paint order. + +An iframed surface participates in neither. It would need theme tokens piped over +`postMessage` to avoid looking foreign, and would fight the keyboard system rather than +join it. + +Also relevant: there is currently **zero** `