review-ipc
Audit the IPC layer across the SDK, permission gate, and host listener for correctness and completeness.
Context: the worker/view bridge
Every Tier 2 extension runs in two iframes — a worker (always-on,
hidden, worker.html) and a view (on-demand, view.html). Each iframe
imports from a different SDK entry (asyar-sdk/worker vs
asyar-sdk/view) and owns its own MessageBroker and
ExtensionContext. The IPC bridge audited here therefore covers
both iframes' calls into the host. ExtensionIpcRouter maps
event.source back to a role via findIframeRoleForSource(...) —
services that branch on role read it off the dispatch context.
For the runtime details (state machine, mailbox, RPC), see
docs/explanation/extension-runtime.md;
for the per-message protocol, see
docs/explanation/ipc-bridge.md.
What to check
1. Type string format consistency
All SDK proxy methods must call broker.invoke('{service}:{action}', ...). The MessageBroker prepends asyar:api: automatically, producing asyar:api:{service}:{action}. Verify:
- Every
this.broker.invoke(...)call inasyar-sdk/src/services/*Proxy.tsuses the{service}:{action}format (noasyar:api:prefix — that's added by MessageBroker) - The resulting full type (
asyar:api:{service}:{action}) is classified in the Rust gate —asyar-launcher/src-tauri/src/permissions.rs: eitherget_required_permission(needs a manifest permission) oris_public_call(deliberately permission-free). That is the ONLY enforcement point — there is no JS-side gate to update - The gate fails closed (since 2026-07-24): a call type in neither list is DENIED, so a new proxy method nobody classified breaks loudly instead of becoming default-allowed
- New proxies are added to the right entry's bag: the worker entry
(
asyar-sdk/src/worker.ts) only exposes proxies that don't depend on the DOM or user interaction; the view entry (asyar-sdk/src/view.ts) exposes the full bag — it re-exportscontracts.ts, which is where the shared proxies andExtensionContextlive. A misclassified proxy is a Phase-6 regression.
2. Permission gate coverage
Coverage is auto-enforced by asyar-launcher/scripts/permission-coverage.test.mjs (runs in CI via vitest): it extracts every .invoke('service:action') call type in the SDK and fails if any is not classified in the Rust gate (get_required_permission ∪ is_public_call). A forgotten mapping turns CI red — you no longer hand-audit this.
When adding/auditing a gated API, edit asyar-launcher/src-tauri/src/permissions.rs:
- Add gated calls to
get_required_permission— the permission string must be a valid manifest permission (e.g.clipboard:read,network,fs:read,runs:track,tools:register). The generatedgatedPermissions.ts(launcher + SDK CLI copies) is derived from it byscripts/generate-permission-catalog.mjs— runpnpm gen:permission-catalog - Add deliberately permission-free calls to
is_public_call(with a one-line rationale) — do NOT rely on fall-through, which now denies - Verify with
pnpm exec vitest run scripts/permission-coverage.test.mjsandcargo test permissions::tests
3. Tier 1 action registration
Tier 1 (built-in) extensions must not use ActionServiceProxy for registering actions. Callbacks are stripped by JSON.stringify in postMessage serialization.
Check every file in asyar-launcher/src/built-in-features/*/index.ts:
- Actions must be registered via
actionService.registerAction(...)(the host singleton imported from../../services/action/actionService.svelte) - Any use of
context.getService<IActionService>('actions')for registering callbacks should be flagged
4. IPC response path
Verify that for any new service method added to a proxy:
- The host-side handler exists in
asyar-launcher/src/services/extension/ExtensionIpcRouter.tsor a dedicated service file - Replies are posted with
event.source.postMessage({ type: 'asyar:response', messageId, result }, '*')on success — usingevent.sourceensures the reply lands in the same iframe (worker or view) that issued the request - Error path sends
{ type: 'asyar:response', messageId, error: string } - Host → iframe pushes that target a specific role go through
pickExtensionIframe(extensionId, prefer)inasyar-launcher/src/services/extension/extensionIframeSelector.ts— never an unscopediframe[data-extension-id="..."]selector
5. Role-aware dispatch checks (Phase 6+)
When auditing services that branch on iframe role:
actions.registerActionHandleris role-neutral — registering from either role is supported and the launcher routes the matchingasyar:action:executeenvelope back to whichever role registeredcommands.onCommandfor amode: "background"manifest command must register from the worker; the launcher dispatches background-mode commands to the worker iframestate:rpcRequest/state:rpcReplyenvelopes carry the worker↔view RPC protocol; the worker-side interceptor inasyar-sdk/src/worker.tsis the only place that callsextensionRpc.deliverActionPayload(...). View-side calls go throughcontext.request(...)only.
Files to read
asyar-sdk/src/ipc/MessageBroker.ts— confirmsbroker.invoke()prependsasyar:api:asyar-sdk/src/services/*Proxy.ts— all proxy service filesasyar-sdk/src/worker.ts,asyar-sdk/src/view.ts,asyar-sdk/src/contracts.ts— entry-point splitasyar-launcher/src-tauri/src/permissions.rs— the real gate (get_required_permission+is_public_call+gate_decision, fail-closed)asyar-launcher/scripts/permission-coverage.test.mjs— the CI coverage gate (SDK call types ⊆ classified)asyar-launcher/src/services/extension/ExtensionIpcRouter.ts— host IPC handler + role-source mappingasyar-launcher/src/services/extension/extensionIframeSelector.ts—pickExtensionIframerole-prefer/fallback selectorasyar-launcher/src/built-in-features/*/index.ts— Tier 1 action registration
Host → Extension Push Messages (out of scope for this audit)
The host sends two push message types directly into extension iframes. These are not extension API calls — they originate from the host and require no permission gate:
| Message type | Direction | Purpose |
|---|---|---|
asyar:theme:variables |
host → iframe | Injects all CSS custom properties on load and on theme change |
asyar:theme:fonts |
host → iframe | Sends Satoshi + JetBrains Mono as base64 @font-face CSS on load |
Sender: ExtensionIframe.svelte (handleIframeLoad, MutationObserver)
Receiver: ExtensionContext.ts setupThemeInjection() in the SDK
These are host→iframe pushes, not extension calls, so they never reach the gate — do not flag them as missing.
Output format
Report findings grouped by category:
- Missing permission gate entries — type string + which proxy method
- Tier 1 proxy misuse — file + line where ActionServiceProxy is misused
- Orphaned gate entries — call types in
get_required_permission/is_public_callwith no matching SDK.invoke()(dead classification) - IPC response path gaps — proxy methods with no corresponding host handler
- Clean — explicitly state if a category has no issues
If the request names a single service (e.g. "review-ipc for notifications"), scope the review to that service only.