返回 DeepSeek-Reasonix
DESKTOP_HOST_PROTOCOL.md
根目录 / docs / DESKTOP_HOST_PROTOCOL.md
1 # Desktop host protocol
2
3 [简体中文](DESKTOP_HOST_PROTOCOL.zh-CN.md)
4
5 The Electron shell and the Go desktop service are two processes joined by one
6 private, versioned JSON-RPC 2.0 connection over the service's stdio. This
7 document is the contract both sides implement. The Go side owns every desktop
8 business command; the Electron side owns every native surface. Neither side
9 may reach around the contract: the React UI never touches Electron or Go
10 globals, and Go business code never links a shell toolkit.
11
12 ```text
13 React renderer ──typed IPC (preload)──▶ Electron main ──stdio JSON-RPC──▶ Go desktop service
14 ▲ │
15 └────── host/* reverse requests ────┘
16 ```
17
18 ## Historical session archive receipts
19
20 `ArchiveSessionTarget` accepts a retained historical source without first
21 publishing an active session. Canonical selectors keep their existing identity;
22 source selectors explicitly address the retained source version. A validated
23 unchanged conversion follows its adopted target's lifecycle. A changed source
24 is saved in full under an independent identity and committed directly archived.
25 The original target's deletion tombstone is never reused or removed.
26
27 `SessionMutationResult.outcome` is optional: `archived`, `archived_copy`,
28 `archived_partial` (the row is archived but `pendingSiblings` recovery snapshots of its
29 lineage are not; repeating the action finishes them), or `already_removed`. The last result means a proven residual source receipt was
30 registered for an already deleted target, not that content was restored. Missing
31 or unknown outcomes use the existing generic committed-result behavior. Clients
32 apply identity aliases and lifecycle fences only after `committed: true`.
33
34 Historical source archives use the existing `archive-import` child and `archive`
35 parent journal. Unpublished import reservations may transfer to this archive
36 flow under an observed-state check; orphan archive children never publish active
37 membership. Source errors `source_unavailable` and `source_ambiguous` retain the
38 source and return a sanitized explanation. Content, provider requests and the
39 session storage schema are unchanged by this RPC addition.
40
41 ## Transport
42
43 - Framing: newline-delimited JSON-RPC 2.0 (`rpcwire` strict mode). One frame
44 per line, UTF-8, no batch arrays.
45 - The Go service is started as `reasonix-desktop --host-rpc`. Its stdout carries
46 only protocol frames; stderr carries logs. The shell closes stdin only after
47 `desktop/shutdown` has reported `completed`; closing stdin without that result
48 is treated as `connection_lost` and runs bounded cleanup.
49 - Limits: 64 MiB per inbound frame on both sides, 512 concurrent inbound
50 handlers on the service, 30 s write-stall watchdog. Large binary payloads never
51 travel in frames; they use the resource origin below.
52 - Every request the shell makes runs on its own goroutine, exactly as the
53 retired in-process shell
54 bound calls did. Ordering is only guaranteed for `desktop/event` frames,
55 which the service writes from one queue.
56
57 ## Handshake
58
59 The first request on a fresh connection must be `desktop/hello`. Anything else
60 fails with `-32002 not_ready`.
61
62 ```jsonc
63 // shell → service
64 {"method":"desktop/hello","params":{
65 "protocolVersion": 11,
66 "contractDigest": "sha256:…", // digest embedded in the shell bundle
67 "build": {"version":"v1.30.0","channel":"stable","commit":"abc123"},
68 "host": {"name":"electron","version":"44.2.0","chrome":"152.0.0","platform":"darwin","arch":"arm64"},
69 "instance": {"home":"/Users/…/.reasonix","dev":false}
70 }}
71 // service → shell
72 {"result":{
73 "protocolVersion": 11,
74 "contractDigest": "sha256:…",
75 "service": {"version":"v1.30.0","channel":"stable","commit":"abc123","pid":4242},
76 "runtimeGeneration": "g-01J…", // new for every service process
77 "instance": {"identityVersion":3,"identityDigest":"sha256:…","legacyId":"com.reasonix.desktop.…"},
78 "runId": "…", "incidentId": "…", "diagnosticsEnabled": true,
79 "resources": {"origin":"http://127.0.0.1:51234","token":"…"},
80 "window": {"width":1280,"height":820,"minWidth":760,"minHeight":480,"frameless":false,"zoomFactor":1}
81 }}
82 ```
83
84 `instance` is optional for cross-version compatibility. New services publish a
85 versioned digest from the shared filesystem identity resolver plus the legacy
86 instance ID. The shell consumes these opaque values for diagnostics and never
87 uses the digest as a filesystem path. Older shells ignore the object and newer
88 shells accept its omission.
89
90 `window` is the initial main-window geometry Go derives from the saved state
91 and platform rules. Optional `position: {x, y}` carries the saved origin (zero
92 and negative coordinates are valid); omission requests centering. The shell
93 selects the matching display and fits the rectangle to its DIP work area before
94 creating the hidden window. Go later maximises and shows it from `domReady`,
95 without overriding the shell's corrected position. Persistence always captures
96 the normal-state rectangle, separately from the maximised flag; legacy oversized
97 rectangles are fitted rather than resetting every maximised entry to defaults.
98 While minimized, the shell retains its last non-minimized snapshot because
99 native normal-bounds queries can otherwise expose the maximized frame.
100
101 The persisted JSON shape is unchanged. Older shells ignore the optional hello
102 position; newer shells accept its omission. Ship shell and service together:
103 mixed development builds do not provide the complete restore fix. Downgrading
104 can reintroduce the old geometry bug, and older readers may reject negative
105 origins below their previous validation floor.
106
107 Failure codes are terminal: the shell shows the real error and offers
108 "open logs" and "quit". It never falls back to the browser mock.
109
110 | Code | Name | Meaning |
111 | --- | --- | --- |
112 | `-32001` | `protocol_mismatch` | `protocolVersion` differs |
113 | `-32003` | `contract_mismatch` | command/event digest differs (mixed install) |
114 | `-32004` | `build_mismatch` | shell and service versions differ and neither is `dev` |
115 | `-32005` | `instance_mismatch` | the shell's canonical data home differs from the service's |
116 | `-32002` | `not_ready` | request before a successful hello |
117
118 `runtimeGeneration` tags every event and every approval or browser grant
119 minted by this service process. A restarted service issues a new generation;
120 the shell discards anything tagged with an old one.
121
122 `runId` identifies this service run. `incidentId` links service and shell
123 lifecycle evidence for the same failure chain. Both are random diagnostic
124 identifiers; they do not contain a PID, local path, or user content. When
125 diagnostics are disabled (including a `dev` service build), `diagnosticsEnabled`
126 is false and both identifiers are empty strings; the keys are always present.
127
128 ## Lifecycle requests (shell → service)
129
130 | Method | Params | Result | Go owner |
131 | --- | --- | --- | --- |
132 | `desktop/start` | `{}` | `{}` | `App.startup` |
133 | `desktop/domReady` | `{}` | `{}` | `App.domReady` |
134 | `desktop/rendererAttached` | `{"rendererGeneration":n}` | `{}` | frontend heartbeat/readiness |
135 | `desktop/beforeClose` | `{"reason":"window"\|"quit"\|"tray"\|"updater"}` | `{"prevent":bool}` | `App.beforeClose` |
136 | `desktop/shutdown` | `{"requestId":string,"reason":string}` | shutdown phase/result | coordinated, retryable shutdown |
137 | `desktop/shutdownStatus` | `{"requestId":string}` | same shutdown phase/result | query after timeout/unknown result |
138 | `desktop/hostEvent` | `{"name":string,"payload":any}` | `{}` | second instance, tray open/quit, menu actions |
139 | `desktop/browserControl` | `{"enabled":bool}` | `{}` | built-in browser switch, read when a session is built |
140
141 Order: `hello` → `start` → window load → `domReady` → (`rendererAttached` after
142 each renderer mount) → … → `beforeClose` → (`shutdown` completed → stdin close
143 fallback → exit). A shutdown RPC timeout is an unknown result: the shell queries
144 `shutdownStatus` and keeps the window open on a retryable failure. An abrupt
145 stdin EOF enters the same coordinator with reason `connection_lost`; it does
146 not create a second cleanup flow after a completed shutdown.
147
148 The shell publishes a `stopping` service phase before the shutdown RPC. During
149 that phase readiness is false and new business calls are rejected, while the
150 shutdown and shutdown-status requests retain the existing service session.
151 Clean exit removes the current temporary file under
152 `diagnostics/lifecycle`; an empty lifecycle directory after exit is expected.
153 Rotating `logs/shell.log` is the durable post-exit record. See
154 [Windows close and transcript diagnostics validation](WINDOWS_CLOSE_TRANSCRIPT_VALIDATION.md).
155
156 ## Business commands
157
158 ```jsonc
159 {"method":"desktop/invoke","params":{"method":"OpenProjectTab","args":["/path", true]}}
160 {"result": {...}} // the method's JSON result, null for void
161 {"error":{"code":-32000,"message":"<error text>","data":{"method":"OpenProjectTab"}}}
162 ```
163
164 `method` must name an exported method of the Go `App` value that the contract
165 registry accepted. Signatures follow the rules the retired shell used: any
166 JSON-serialisable
167 parameters and a result of `()`, `(T)`, `(error)` or `(T, error)`. The
168 registry rejects anything else at build time, so the surface can never gain a
169 method the shell cannot call. The shell validates `method` against the
170 embedded command list before forwarding. Unknown names fail with `-32601`.
171
172 The generated contract (`cd desktop && go run . -emit-contract frontend/src/generated`)
173 is the single source of truth: it emits the JSON contract, its digest, the
174 TypeScript command table and the DTO type declarations consumed by the
175 renderer. A desktop Go test fails when the checked-in output drifts.
176
177 Each command also records its source-module `domain`, exact `owner` (for
178 example `App.OpenProjectTab`), repository-relative `sources`, `scope` and
179 `cancellation`; these fields are included in the digest. The generator scans
180 all platform declarations and writes `desktop/host_command_owners.generated.json`,
181 which the host embeds and validates against every reflected command. Scope
182 records the owner's named wire `inputs` (`argN` for unnamed legacy parameters)
183 and `resolver`; zero-input commands use `owner-state`, others `owner-inputs`.
184 These are provenance and dispatch boundaries. Input validation, tab/session
185 selection and access checks remain in the existing App method.
186
187 Current App commands declare `before-dispatch`: the host checks cancellation
188 before decoding and immediately before dispatch, then preserves the method's
189 result even if cancellation arrives during a synchronous write. They do not
190 promise interruption after dispatch. A host method may opt into
191 `cooperative-context` with a leading Go `context.Context`; the host injects
192 the request context and excludes it from JSON arguments and generated DTOs.
193 The method must cooperate with cancellation. Business Stop/Cancel commands
194 continue to use their existing owners and semantics.
195
196 ## Events (service → shell → renderer)
197
198 ```jsonc
199 {"method":"desktop/event","params":{"seq":1093,"generation":"g-01J…","name":"agent:event","args":[{...}]}}
200 ```
201
202 `args` preserves the variadic payload of the previous event bridge; most
203 events carry one element. The shell forwards the frame to the renderer on the
204 `reasonix:event` channel; the preload API `on(name, cb)` filters by `name` and
205 calls `cb(...args)`. Sequence numbers are strictly increasing per generation
206 so a renderer that re-attaches can detect a gap and re-snapshot instead of
207 trusting stale state.
208
209 Both the service supervisor and preload reject duplicate or out-of-order
210 frames; the preload also rejects old generations using the current service
211 state. It binds the transport before React subscribes. A generation change,
212 sequence gap or missed subscription raises the shell-local `desktop:resync`
213 event (`generation`, `reason`, `expectedSeq`, `actualSeq`), which is not a Go
214 business event. Runtime state is re-read through `SyncRuntimeState`; mounted
215 controllers re-read `ListTabs` and use the existing `TurnEventsForTab` ledger
216 and pending-prompt presentation to repair their projection. Reads are fenced
217 against newer recovery requests and session/navigation changes. No business
218 mutation is replayed, and a surviving application renderer is reattached
219 after a service restart without reloading its unsent drafts.
220
221 This recovery currently covers core runtime state, session metadata, durable
222 turn events and pending prompts. Terminal output has a bounded snapshot but
223 no atomic output cursor, so an affected terminal is visibly marked incomplete
224 instead of merging an ambiguous snapshot into live output. Extension output,
225 file-watch and other independent event streams still need capability-specific
226 resnapshot contracts; they are not covered by this core recovery guarantee.
227
228 ## Native host calls (service → shell)
229
230 These replace direct shell-toolkit calls in Go. Each maps to one method of the
231 Go `nativeHost` interface; the Wails implementation was retired when the Electron shell
232 landed.
233
234 | Method | Params | Result |
235 | --- | --- | --- |
236 | `host/window.show` | `{"reason":string}` | `{}` |
237 | `host/window.hide` | `{}` | `{}` |
238 | `host/app.hide` | `{}` | `{}` (macOS application hide) |
239 | `host/window.maximise` `unmaximise` `minimise` `unminimise` `toggleMaximise` `center` | `{}` | `{}` |
240 | `host/window.isMaximised` `isMinimised` | `{}` | `{"value":bool}` |
241 | `host/window.setPosition` | `{"x":n,"y":n}` | `{}` |
242 | `host/window.setTitle` | `{"title":string}` | `{}` |
243 | `host/screen.list` | `{}` | `{"screens":[{"x","y","width","height","scale","primary"}]}` |
244 | `host/dialog.openDirectory` | `{"title","defaultDirectory"}` | `{"path":string}` (`""` = cancelled) |
245 | `host/dialog.openFile` | `{"title","defaultDirectory","filters":[{"displayName","pattern"}],"multiple":bool}` | `{"paths":[]}` |
246 | `host/dialog.saveFile` | `{"title","defaultDirectory","defaultFilename","filters"}` | `{"path":string}` |
247 | `host/dialog.message` | `{"type":"info"\|"warning"\|"error"\|"question","title","message","buttons":[],"defaultButton","cancelButton"}` | `{"button":string}` |
248 | `host/shell.openExternal` | `{"url":string}` | `{}` |
249 | `host/app.quit` | `{}` | `{}` |
250 | `host/app.relaunch` | `{"args":[],"execPath"?:string}` | `{}` |
251 | `host/devtools.toggle` | `{}` | `{}` |
252 | `host/remoteWindow.open` | `{"hostKey","url","title"}` | `{"windowId":string}` |
253 | `host/remoteWindow.navigate` | `{"hostKey","url","title"}` | `{}` |
254 | `host/remoteWindow.focus` `close` | `{"hostKey"}` | `{}` |
255 | `host/tray.ensure` | `{"openTitle","openTooltip","quitTitle","quitTooltip","tooltip"}` | `{"ready":bool,"reason":string}` |
256
257 `host/shell.openExternal` accepts only `http:`, `https:`, and `mailto:` URLs.
258 Other schemes, including `file:`, `javascript:`, and `data:`, are rejected at
259 the Electron host boundary before the system opener is invoked.
260 | `host/tray.destroy` | `{}` | `{}` |
261 | `host/browser.grant` `revoke` | `{"grantId","tabId","sessionId"}` / `{"grantId"}` | `{}` |
262 | `host/browser.tabs.list` | `{"grantId"}` | `{"tabs":[{"id","url","title","loading","temporary"}]}` |
263 | `host/browser.tabs.open` | `{"grantId","url","temporary"}` | tab |
264 | `host/browser.tabs.navigate` | `{"grantId","tabId","url","action"}` | tab |
265 | `host/browser.tabs.close` | `{"grantId","tabId"}` | `{}` |
266 | `host/browser.snapshot` | `{"grantId","tabId","selector"}` | `{"documentToken","url","title","tree","refs"}` |
267 | `host/browser.act` | `{"grantId","operationId","tabId","documentToken","action","ref","text","keys","options","files","submit","deltaX","deltaY"}` | `{"executed","reason","documentToken"}` |
268 | `host/browser.screenshot` | `{"grantId","tabId","ref","fullPage","directory"}` | `{"path","mime","width","height"}` |
269 | `host/browser.downloads` | `{"grantId","tabId","waitForMs"}` | `{"downloads":[{"id","url","path","state","bytes"}]}` |
270
271 Browser calls fail with `-32010` (stale reference), `-32011` (the user took the
272 tab over) or `-32012` (no current grant); the Go executor maps them onto the
273 kernel sentinels and records the operation outcome in its ledger. Grant
274 `tabId` is the desktop tab (the task); browser tabs opened under that grant
275 belong to it.
276
277 Host events (`desktop/hostEvent`): `tray.open`, `tray.quit`, `secondInstance`
278 (raw argv in `payload`), `menu.showWindow`, `remoteWindow.closed`
279 (`{"hostKey"}`), `browser.takeover` (`{"tabId","epoch","reason"}`).
280
281 Dialog results never expose file contents; they return paths that Go then
282 authorises through the existing workspace and media checks.
283
284 ## Resource origin
285
286 The service listens on a loopback port for the existing authorised asset
287 handlers (`/__reasonix_workspace_media/…`, `/__reasonix_theme_asset/…`, the
288 remote markdown image proxy). The shell serves the packaged UI from the
289 privileged `reasonix://app/` scheme and forwards only those prefixes to the
290 resource origin, adding `Authorization: Bearer <token>` in the main process.
291 The token never reaches the renderer, a website view, a remote window or an
292 MCP App frame. Go keeps every file-identity and TTL check it has today.
293
294 ## Renderer preload API
295
296 The trusted preload exposes exactly one object, `window.reasonixDesktop`:
297
298 ```ts
299 interface ReasonixDesktopHost {
300 readonly kind: "electron";
301 readonly contract: { protocolVersion: number; digest: string; commands: readonly string[] };
302 readonly platform: { os: "darwin" | "windows" | "linux"; arch: string; versions: Record<string, string> };
303 invoke(method: string, args: unknown[]): Promise<unknown>;
304 // Optional: preserve structured RPC errors across Electron contextBridge.
305 invokeResult?(method: string, args: unknown[]): Promise<
306 { ok: true; value: unknown } | { ok: false; message: string; code?: number; data?: unknown }
307 >;
308 on(name: string, cb: (...args: unknown[]) => void): () => void;
309 native: {
310 openExternal(url: string): Promise<void>;
311 clipboard: { writeText(text: string): Promise<boolean>; readText(): Promise<string> };
312 window: {
313 setTheme(theme: "system" | "light" | "dark"): void;
314 setBackgroundColour(r: number, g: number, b: number, a: number): void;
315 getBounds(): Promise<{ x: number; y: number; width: number; height: number; maximised: boolean }>;
316 isMaximised(): Promise<boolean>;
317 minimise(): void; toggleMaximise(): void; close(): void;
318 };
319 getPathForFile(file: File): string; // native drop paths
320 onServiceState(cb: (state: ServiceState) => void): () => void;
321 browserControl: { // settings page for the built-in browser
322 get(): Promise<BrowserControlState | null>;
323 setEnabled(enabled: boolean): Promise<BrowserControlState>;
324 setIgnoreCertificateErrors(enabled: boolean): Promise<BrowserControlState>;
325 clearCache(): Promise<void>; // keeps cookies and site data
326 clearAllData(): Promise<void>; // cookies, site data and cache
327 importChromeLogin(): Promise<ChromeImportOutcome>;
328 };
329 };
330 browser: { // user-driven browser panel; agent calls go through Go
331 list(): Promise<BrowserTabView[]>;
332 open(url: string, opts?: { temporary?: boolean; taskId?: string }): Promise<BrowserTabView>;
333 close(tabId: string): Promise<void>;
334 activate(tabId: string | null): Promise<void>;
335 navigate(tabId: string, target: { url?: string; action?: "back" | "forward" | "reload" | "stop" }): Promise<void>;
336 setZoom(tabId: string, factor: number): Promise<void>;
337 toggleDevTools(tabId: string): Promise<void>;
338 resume(tabId: string): Promise<void>; // hand a taken-over tab back to the agent
339 setLayout(rect: { x: number; y: number; width: number; height: number } | null): void;
340 setOverlay(active: boolean): void; // app overlays hide every website view
341 onTabs(cb: (tabs: BrowserTabView[]) => void): () => void;
342 onDownload(cb: (download: BrowserDownloadView) => void): () => void;
343 };
344 }
345 ```
346
347 `BrowserTabView` is `{ id, taskId, url, title, loading, canGoBack, canGoForward,
348 temporary, mode: "agent" | "human", epoch, zoom, error }` and
349 `BrowserDownloadView` is `{ id, tabId, url, filename, path, state, received,
350 total }`. Website views live in `persist:browser` (shared logins) or
351 `temp:<id>` partitions and never receive the application preload.
352
353 `ServiceState` is `{ phase: "starting" | "ready" | "restarting" | "failed" | "exited"; generation: string; error?: string }`.
354 Business components import the typed SDK, never this object; only the bridge
355 adapter reads it.
356
357 `BrowserControlState` is `{ controlEnabled, ignoreCertificateErrors, writable,
358 warning: "invalid-config" | "unreadable-config" | "unsupported-version" | null }`
359 and `ChromeImportOutcome` is either `{ ok: true, profile, cookies, skipped }` or
360 `{ ok: false, reason }` with `reason` one of `chrome-missing`,
361 `profile-not-found`, `cookies-unreadable`, `safe-storage-denied`,
362 `safe-storage-unavailable`, `unsupported-platform`.
363
364 ## Performance diagnostics
365
366 The optional native calls below are restricted to the trusted app main frame.
367 Older shells may omit them. No persisted user-data format changes or migrations
368 are required.
369
370 - `processDiagnostics()` returns `{scope: "electron", samples, growth}`.
371 Samples contain age, nullable CPU interval, process PID/type/creation time,
372 nullable CPU percentage, working set and private memory in MiB, and a
373 truncation flag. Sampling is limited to once per 30 seconds in the foreground
374 and once per 60 seconds otherwise. Retention is at most 12 snapshots and five
375 minutes, with at most 128 processes per snapshot. No titles, URLs or process
376 names are collected. Electron-managed processes only; Go is excluded.
377 - `captureRendererProfile(requestId?)` records the current renderer through CDP for
378 five seconds at a requested 10 ms sample interval. It returns a status,
379 duration and at most eight app-script self-time summaries. Normal documents
380 do not enable JS self-profiling. Capture is single-flight, requires the
381 foreground window, observes a ten-minute cooldown, and allows at most three
382 attempts per shell lifetime. Existing debugger/DevTools sessions are not
383 taken over. Blur, hide, navigation, renderer loss or cancellation stops it.
384 - `cancelRendererProfile(requestId)` cancels only the matching capture; unscoped
385 renderer cancellation is ignored. This also fences delayed requests across
386 long suspension/resume gaps. Each CDP command has a
387 1.5 second deadline and the owned debugger is released on every terminal path.
388 Analysis runs in a disposable Worker with a 32 MiB old-generation limit,
389 1.5 second deadline, and input limits of 20,000 nodes / 100,000 samples.
390 Raw profiles never enter the UI report.
391 - `exportHeapSnapshot()` requires a user-confirmed native warning and save
392 dialog. It saves locally without uploading, and accepts no renderer-supplied
393 path. Snapshots may contain code, chats and secrets and can pause the renderer
394 or use substantial disk space. Electron cannot preempt a snapshot: its busy
395 lease remains held until the actual operation settles.
396
397 A memory growth signal requires a continuous PID plus creation-time identity,
398 at least five readings spanning two minutes, and three recent readings exceeding
399 the initial two-reading baseline by both 256 MiB and 50%. Private memory is used
400 when available throughout; otherwise working set is used. This is an observation
401 of sustained growth, not proof of a leak or exclusive physical RAM ownership.
402
403 Reports appear immediately. Process enrichment waits at most 750 ms; a bounded
404 CPU capture can update the same report later. The UI abandons capture enrichment
405 after 12 seconds and requests cancellation. These are asynchronous deadlines,
406 not preemptive limits on synchronous work. Dismissed reports never reappear.
407 The report distinguishes post-trigger samples from the already-ended long task.
408 User-requested heap capture suppresses pressure alerts during capture and for
409 the normal five-second settling grace afterward.
410
411 From `desktop/electron`, run `node scripts/performance-smoke.mjs` to verify the
412 production owner, Worker, report enrichment and local heap snapshot with an
413 isolated native fixture. `node scripts/performance-benchmark.mjs` compares off,
414 lightweight monitoring and short capture in three fresh-process trials each.
415 All modes use the same renderer bundle and runtime mode selection. Activity
416 signals are pinned and background throttling disabled for unattended native
417 measurement. Host event tests separately cover the production focus and
418 navigation cancellation policy; the smoke verifies actual CDP and ASAR paths.
419 It records CPU time where available, frame timings, working sets and metric
420 collection cost in `artifacts/performance/overhead.json`. This synthetic
421 benchmark is not a reproduction of the Windows user workload. Field comparison
422 must still cover startup, extended use, foreground return and closing tabs.
423
424 ## Security boundaries
425
426 - The application window: sandbox on, context isolation on, Node integration
427 off, `reasonix://app` only, preload above.
428 - Website views, remote Serve windows and MCP App frames: separate sessions,
429 no preload from the application, no `reasonix://` access, no `host/*` reach.
430 - IPC handlers accept requests only from the application window's
431 `webContents`. Any other sender is rejected and logged.
432 - `desktop/invoke` names outside the embedded contract fail before reaching Go.
433
433 lines MARKDOWN