feat(presence): PRES-2b — presence IPC route + runtime wiring (sandbox surface)#138
Conversation
…ker service The main-side sandbox half of the presence transport (design 74): - `presence-router.ts` — wraps the PRES-2a PresenceManager with a per-entity subscriber set + an outbound peer fan-out (`deliverPresencePeersToApps`, mirroring `ydoc-remote-broadcast`). A peer push only ever reaches an app already in the entity's subscriber set, and an app enters that set only via the cap-gated `publish` — so the outbound push carries no new authorization surface, security held at the source. - `presence-service.ts` — the `presence` broker ServiceHandler (publish/untrack), re-checking `entities.read:<type>` against the vault ledger (OQ-205: presence piggybacks on the read grant, no new default cap), fail-closed like sharing. 16 tests: two routers converge over a loopback relay and each fans the other's presence to exactly its subscribed app window; the cap gate denies/unavailables fail-closed and publish/untrack delegate correctly. Remaining for PRES-2b: app-side transport (preload + SDK createPresenceTransport) + runtime wiring (per-session manager/router, register service, route inbound WireKind.Awareness). Gate: PRES-4 /security-review + /pentester. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ presence service proxy The sandbox-app half of the presence wire (design 74), pure + testable: - `@brainstorm/react-yjs` `createPresenceTransport(host)` — adapts a `PresenceHost` (publish / untrack / onPeers, injected so the module stays window-free) to the PRES-1 `PresenceTransport` that `createSyncedAwareness` consumes. `send` publishes the local state (clientID dropped — the main proxy owns the wire identity); `subscribe` diffs each peer snapshot so a client leaving the set becomes `applyRemoteState(id, null)`; teardown clears our presence. + PresenceHost types. - `@brainstorm/sdk-types` `PresenceService` (publish/untrack) + services-map entry. - `@brainstorm/sdk` `presenceProxy(bridge)` — `publish` sends the `entities.read:<type>` cap hint; `untrack` needs none. Wired into both runtimes. 10 react-yjs transport tests (send→publish, peer diff→applyRemoteState, departures →null, teardown→untrack, end-to-end over a fake host). typecheck:packages + lint green. Remaining PRES-2b: app-preload `app:presence-peers` sink + `main/index.ts` per-session wiring (register service, route inbound WireKind.Awareness) — the integration slice that pairs with the PRES-4 security gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The integration slice that brings the sandbox presence surface online: - `presence-wiring.ts` — per-session `installPresenceRouter`/`getPresenceRouter`/ `disposePresenceRouter` (mirrors live-sync-wiring). The router's manager emits outbound awareness through `engine.emitLocalAwareness` (DEK-sealed, tracked- shared-entities only); both sides reach the current session via module getters, so engine↔router need no construction-order cycle. - `main/index.ts` — register the `presence` broker service (getRouter + ledger, fail-closed); in `wireSyncEnginesForSession` install/dispose the router with the engine; add the engine ctx `applyRemoteAwareness` hook → `router.applyInbound` (inbound relay awareness frames now reach the peer fan-out — the 10.6 path was dormant, "absent ⇒ presence off"). - `app-preload.ts` — the `app:presence-peers` sink + `presence.onPeers(entityId,cb)` on the app runtime (mirrors `ydoc.onRemote`). Presence now flows end to end in the shell: app publish → service (cap-gated) → router → manager → engine → DEK relay → peer engine → applyRemoteAwareness → router → app-window push. 25 presence tests + 90 live-sync/integration green; typecheck:packages + lint clean. Remaining: PRES-3 (whiteboard swap to createSyncedAwareness + <PresenceStack> headers + consumer global typing) and the PRES-4 /security-review + /pentester gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the app-supplied one
Security review of the PRES-2b sandbox surface found a HIGH capability bypass:
`presence.publish({entityId, type, state})` checked `entities.read:<type>` against
the APP-SUPPLIED `type` but acted on the app-supplied `entityId`, with nothing
binding the two. An app holding only `entities.read:note` could call
`publish({entityId: <secret>, type: "note"})` — the gate passed, the router
subscribed it, and `#pushPeers` delivered the secret entity's collaborator
presence (identities + cursors) to an app with no read grant for that type. A
cross-type read-isolation leak (RECEIVE direction), not the cosmetic publish the
model waives.
Fix (mirrors entities-service, which resolves the row type server-side): inject
`resolveEntityType(entityId)` into the service; resolve the REAL type from
entities.db and gate on THAT, ignoring the client's `type`. Unknown entity ⇒
Invalid (deny). `untrack` is unchanged (clears only the caller's own subscription).
Tests: forged-type bypass now denies on the resolved `secret` type; unknown
entity denies. 22 presence-service/router/wiring tests + typecheck + lint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PRES-4
|
|
Marked ready for review (was draft). The PRES-4 |
Code-complete — the sandbox presence transport (design 74) end to end. Still draft: the PRES-4
/security-review+/pentesteron this new cross-sandbox surface is a hard merge gate.Presence now flows: app publish →
presenceservice (cap-gated) → router → manager →engine.emitLocalAwareness→ DEK relay → peer engine →applyRemoteAwareness→ router →app:presence-peerspush.Slices
bf31272) —presence-router.ts(per-entity subscriber set + peer fan-outdeliverPresencePeersToApps, security held at source) +presence-service.ts(broker service, re-checksentities.read:<type>, fail-closed). 16 tests.5d86dd0) —@brainstorm/react-yjscreatePresenceTransport(host)(pure PRES-1 adapter; departures →applyRemoteState(id,null)),@brainstorm/sdk-typesPresenceService,@brainstorm/sdkpresenceProxy. 10 tests.22c3329) —presence-wiring.ts(per-session install/dispose, mirrors live-sync-wiring),main/index.ts(register service + install router + engineapplyRemoteAwarenesshook — activates the dormant 10.6 awareness path),app-preload.ts(app:presence-peerssink +presence.onPeers). 4 tests.Security surface (for the PRES-4 review)
publishre-checksentities.read:<type>against the vault ledger (OQ-205, no new default cap);untrackneeds none (clears only our own presence). Fail-closed: no session/ledger →Unavailable.ydoc-remote-broadcast).engine.emitLocalAwareness— DEK-sealed, tracked-shared-entities only, never for a private/solo doc. Inbound state is untrusted (render side hardens viapeerFromState); display-only, grants + persists nothing.26 unit + 90 live-sync/integration tests green; full typecheck (packages + 12 apps) + lint clean.
Remaining
createLocalAwareness→createSyncedAwareness(createPresenceTransport(host)),<PresenceStack>in shared-app headers, consumer global typing forwindow.brainstorm.presence./security-review+/pentester(this PR's merge gate) + two-shell dogfood.🤖 Generated with Claude Code