-
Notifications
You must be signed in to change notification settings - Fork 0
systems ipc and preload
Active contributors: Foundry core (ipc-contract.ts, ipc.ts, preload/bridge.ts)
The renderer is sandboxed: no Node, no direct disk, no git, no droid. Its entire capability surface is a named invoke list shared by both processes. There is no generic invoke(channel, ...) escape hatch and no remote module. If the UI needs something new, the channel is added to the contract first, then a handler, then the bridge.
Almost everything is request/response. A handful of broadcast events tell the UI to refresh; the trace itself is still polled by rowid, not pushed event-by-event.
src/shared/ipc-contract.ts # channel names + FoundryApi types
src/main/ipc.ts # ipcMain.handle for every channel
src/preload/bridge.ts # contextBridge → window.foundry
src/renderer/api.ts # plain() wrapper around window.foundry
Main registers handlers once at startup (registerIpc(ctx) from main.ts). Preload is bundled as CJS (bridge.cjs) because sandboxed preloads cannot be ESM.
Typed namespaces exposed to the renderer:
| Namespace | Capabilities (summary) |
|---|---|
settings |
get, patch |
projects |
list, add (folder dialog), save, remove, export, tryCommand, check, reveal |
roster |
list, save, remove, duplicate, reset |
pipelines |
list, save, remove, duplicate, validate, dryRun, reset |
catalog |
models, tools, gates, templateVariables |
runs |
start, list, detail, events (cursor), liveTail, promptFor, kill, archive, worktree merge/discard/open |
interrupts |
list, answer |
doctor |
run |
maintenance |
orphan worktrees, remove worktree, retention, compact |
app |
openExternal, assetUrl, version |
on |
subscribe to runs-changed, interrupts-changed, settings-changed
|
Payload helper types live beside the API: RunDetail, EventPage, SaveResult, WorktreeAction, and so on.
String channel names (settings:get, runs:events, event:runs-changed, …) are defined once so renaming cannot silently desync preload and main.
Each method is ipcRenderer.invoke(channel, ...args). Push subscriptions use ipcRenderer.on for the three event channels. A separate window.foundryMenu exposes one-way menu commands (menu:settings, menu:new-run, …) that the renderer maps to navigation.
Electron IPC uses the structured clone algorithm. Proxies, class instances, and accessor-only objects throw on send; the failure often lands in an un-awaited promise, so the only symptom is a button that appears to do nothing.
src/renderer/api.ts routes every argument through plain() (JSON round-trip) via a recursive guard on window.foundry. Call sites do not need to remember to clone.
- Declare the method and payload types on
FoundryApiinipc-contract.ts. - Add the channel string to
IPC. - Implement
ipcMain.handleinipc.tsusingAppContext. - Wire the same name on the preload
apiobject. - Call it from the renderer only through
apifromapi.ts.
- Handlers receive plain arguments and return plain data (or small result objects with
ok/issues). - Project-scoped trace access goes through
tracerOf(projectId)so missing projects return empty pages, not throws. - Mutating settings, roster, pipelines, or projects broadcasts
settings-changed(orruns-changedfor run mutations). - Run start validates the pipeline with the scoped roster and command names before starting, so blocking errors appear at the button, not mid-phase.
- Dry-run renders prompts with placeholder envelopes for earlier phases and spends no model tokens.
- External URLs and asset paths are resolved in main so the renderer never needs filesystem layout.
flowchart LR
subgraph Push["Few push channels"]
RC[runs-changed]
IC[interrupts-changed]
SC[settings-changed]
end
subgraph Poll["Polled"]
EV["runs.events after rowid"]
DET[runs.detail]
LIST[runs.list]
end
Main[Main process] --> Push
Main --> Poll
Push --> UI[Renderer refresh hooks]
Poll --> UI
Trace live tails (runs.liveTail) are a separate short text buffer for the open phase drawer, not a replacement for the event cursor.
| Side | Responsibility |
|---|---|
| Store | Settings, projects, roster, pipelines CRUD |
| Trace | Run list/detail/events/prompt/archive |
| Engine via registry | start, kill, interrupts, live tail |
| System services | doctor, maintenance, notifications triggered on run finish in context |
| Renderer | Sole consumer of window.foundry
|
Window bootstrap in main.ts loads the preload path, enables contextIsolation, disables nodeIntegration, and enables sandbox: true.
| Symbol | File |
|---|---|
FoundryApi, IPC
|
apps/desktop/src/shared/ipc-contract.ts |
registerIpc |
apps/desktop/src/main/ipc.ts |
contextBridge.exposeInMainWorld('foundry', api) |
apps/desktop/src/preload/bridge.ts |
export const api = guard(window.foundry) |
apps/desktop/src/renderer/api.ts |
AppContext.broadcast |
apps/desktop/src/main/context.ts |
| Path | Role |
|---|---|
apps/desktop/src/shared/ipc-contract.ts |
Contract: channels, API types, page shapes |
apps/desktop/src/shared/types.ts |
Domain types carried over IPC |
apps/desktop/src/main/ipc.ts |
All invoke handlers |
apps/desktop/src/main/context.ts |
Shared dependencies and broadcast |
apps/desktop/src/main/main.ts |
Window + preload path + registerIpc
|
apps/desktop/src/preload/bridge.ts |
Sandboxed bridge |
apps/desktop/src/renderer/api.ts |
plain() guard |
tests/ipc-clone.test.ts |
Payloads survive structured clone |
- Trace (event cursor)
- Store
- Renderer
- Architecture
- Patterns and conventions
Overview
Snapshot and history
How to contribute
Apps
Systems
Features
Primitives
Background and security
Reference