Why We Killed the Generic capability.run Channel from the Renderer
ADR-0021: read-only capabilities are the only ones the renderer can invoke directly. Everything else goes through a typed IPC handler with a Zod-validated payload. Here's the threat model and the migration path.
For a long time, the renderer in Fabriq Desktop could call any engine capability through a single escape hatch: dapei:capability:run. The convenience was real — but so was the risk.
The threat model
Any code path in the renderer that touches user input (a markdown preview, a search box, a feature name field) becomes a potential XSS surface. If that surface can also fire repos.remove or feature.close through a generic channel, a single injection becomes a destructive action.
We chose to make the renderer honest: it can ask questions, but it cannot change things without going through a typed, schema-validated handler.
The decision (ADR-0021)
The full text lives in the ADR. The shape:
dapei:capability:runbecomes read-only. The main process consults a renderer-specific allowlist (RENDERER_CAPABILITY_ALLOWLIST) before forwarding.- All writes route through typed channels. Examples:
dapei:repos:syncdapei:feature:runStagedapei:advisor:executeAction
- Every typed channel carries a Zod schema.
registerIpcHandlerattaches the schema fromREQUEST_SCHEMASin@dapei/desktop-contractsand validates in main before the handler runs.
What this costs
Some duplication. Each new write capability needs a typed channel and a schema. We accept that cost — the alternative is one renderer-side vulnerability compounding into irreversible damage.
What this buys
- A misbehaving renderer (or XSS) cannot execute write capabilities.
- New capabilities land in the typed surface, which makes the IPC layer self-documenting.
- Engine allowlist becomes the single source of truth for "what can the renderer see."
Migration in practice
If you're adding a new write capability:
- Define the channel in
@dapei/desktop-contracts(request/response types). - Add a Zod schema to
REQUEST_SCHEMAS. - Register the handler in main using
registerIpcHandler— schema validation is automatic. - Add a typed wrapper in the renderer.
That's the price of admission. It's small. It's worth it.
Related articles
Typed IPC: Why Your Electron App Needs a Type-Safe Customs Office
We killed the generic 'run any capability' channel from the renderer after a threat model review. Every IPC call in Fabriq now goes through a typed, Zod-validated handler. Here's the threat model, the migration, and how it makes the app auditable by default.
Local-First vs Cloud-First: Why AI Dev Tools Should Run on Your Machine
The AI coding industry trends toward cloud-hosted agents. Fabriq goes the other way: local-first, offline-capable, putting data sovereignty and team control above convenience. Here's the reasoning behind that bet.
