---
title: "The client bridge is read-model RPC, not Cordis injection"
version: "zh"
---

> Documentation Index
> Fetch the complete documentation index at: https://botharness.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The client bridge is read-model RPC, not Cordis injection

The Web Client is a separate browser Cordis application assembled independently from the Host; `ctx.provide('botharness', core)` is visible only to Host-side plugins, so the roster cannot inject the core service. Core therefore exposes its read model as explicit unary RPC methods on the generic Connection RPC channel: the client calls `ctx.connection.rpc.call('/api', 'botharness/<method>', payload, signal)`; the Host registers endpoints with `ctx.connection.rpc.intercept('/api', …)` or precise routes via `ctx.connection.fetch.register(...)`. Responses use one envelope, `{ ok: true, value }` or `{ ok: false, error }`, plus a change cursor carried by list/get. Status push is not direct `states.on` in the browser: the upstream remote event whitelist (`API_REMOTE_FORWARDED_EVENTS`) is a first-party static list that third-party packages cannot append to, so M3 refreshes after user actions and low-frequency polls, and may later add a private SSE route. The client ships as an independent `@botharness/client` package (`dsh.client.platform: 'web'`, `./client` export) with a hand-built lazy-CJS bundle; the shell module baseline stays external and everything else is inlined. Typert `@Remote` remains a later candidate once its build-time generator is shown to be reproducible outside the DSH workspace.

## Considered Options

- **Typert `@Remote` first** — deferred: it needs build-time codegen plus first-party `api-remotes` assembly; reproducibility outside the DSH workspace is unverified and the read model is still moving.
- **Expose the core service via `inject` to the client half** — impossible: the browser half is a separate Cordis app; there is no cross-process service injection, and `dsh.client.inject` is informational only.
- **Direct `states.on` push to the browser** — impossible today: forwarded events are a static first-party whitelist; a third party cannot register its own.
- **Direct SSE now** — deferred: M3 needs refresh-on-action, not real-time; a private route on `fetch.register` (`requestBody: 'streaming'`) can be added behind the same method surface once six-state liveness is proven necessary.
- **Fold the client into `@botharness/core` (DSH's default two-halves-per-package)** — rejected for now: the halves have different build targets and dependency laws, and the independent package keeps the Host half free of React; the cost is a deliberate deviation from the DSH packaging convention, recorded in the client spec.

## Consequences

- Core must own a wire contract — method names, payloads, envelope, cursor/version — with exact names draft until M3 lands; `docs/client-bridge.md` holds the surface.
- M3 client is refresh/polling only, no live push; "six-state realtime" may need a private stream later.
- Architecture docs are corrected: no `provide('botharness') → client` arrow; the communication table splits Host-internal Cordis / browser-internal Cordis / cross-process RPC.
- `@botharness/client` needs its own Loader entry and `dsh.client` manifest; DSH's shared client build preset is unpublished, so the lazy-CJS bundle (`window.__ModuleLoader__.load`) is hand-rolled — the M3 top engineering risk.
- The browser half treats every RPC result as a plain value and renders loading/error states; no Host object graph leaks into React props.
- Evidence: `docs/research/2026-09-18-dsh-client-ui-and-docs-ia.md` (§1.4, §1.5, §2).

## Update (2026-09-19) — registration goes through the Typert gateway

`ctx.connection.rpc.intercept('/api', …)` squats the single interceptor slot owned by `@deepseek-ai/dsh-api-gateway`, so every native API 404s while `botharness/*` keeps working (#50). The Host half now registers `BotharnessBridgeService` — a `TypertRemoteService` (Cordis key `botharnessBridge`, wire namespace `botharness`) with SRC remote-method markers — and the gateway claims `botharness/*` from the typert registry; no build-time codegen. The wire contract keeps its shape with a Typert argument envelope: the client calls `ctx.connection.rpc.call('/api', 'botharness/<method>', { args: { …named arguments } }, signal)`, and failures ride `RemoteError` back into the same `{ ok: false, error }` branch. The rest of this decision is unchanged.

Source: https://botharness.ai/zh/dev/adr/0023-client-bridge-is-rpc-not-cordis/index.mdx
