How to mirror a surface over ssh
An agent serves its surface on the machine it runs on. A browser can’t reach that process. This guide fronts the remote surface with a local mirror: you dial the host, keep a live local copy of everything it serves, and re-serve that copy to browsers over the same contract. The browser only ever talks to the local copy; the ssh pipe swaps invisibly behind it.
implementSurface, and ③ re-serves that copy to the browser. The browser only ever talks to the local copy — only the ssh pipe behind it swaps.You need the remote agent’s defineSurface(…) contract and its binary’s .drv
(a Nix derivation).
1. Dial the host
A session over ssh is makeSession (the transport-agnostic reconnect loop) plugged
with sshConnector (the ssh transport). You own the session — key your own map and
tear it down yourself; there is no shared pool.
const session: Session<
AgentClient<typeof base.contract>,
SshProv
> = makeSession({
initialConnection: "probing", // an ssh session provisions before it connects
connectOnce: sshConnector<typeof base.contract>({
host: "[email protected]", // any ssh target; "localhost" short-circuits
binary: "my-agent", // exe name inside the realised closure
resolveDrvPath: () => resolveDrv("bob.example"), // deferred — see the caution
}),
});
2. Pump its frames inward
Implement the mirror surface locally, then run pumpRemoteSurface to fold the
agent’s frames into it. Wrap the base contract with mirroredSurface first — it
adds the parent-authoritative connection cell (below).
const source = mirroredSurface(base); // adds + reserves the `connection` cell
const fragment = implementSurface(source, {
channel: inMemoryChannelByName(),
cells: { load: { store: loadStore }, connection: seedConnectionCell() },
collections: {
processes: {
readAll: () => processes,
upsert: (k, v) => {
processes.set(k, v);
},
remove: (k) => {
processes.delete(k);
},
},
},
});
void pumpRemoteSurface({
source,
session,
makeSink: ({ seq: _seq }) => ({
// built per spawn — per-client state resets
cells: { load: (v) => fragment.ctx.cells.load.set(v) },
collections: {
processes: {
upsert: (k, v) => fragment.ctx.collections.processes.upsert(k, v),
remove: (k) => fragment.ctx.collections.processes.remove(k),
},
},
}),
// The parent is the sole writer of `connection`: the pump projects the
// session's lifecycle onto it off `session.onState`.
connection: { set: (info) => fragment.ctx.cells.connection.set(info) },
});
makeSink is a factory: it takes { seq } (the spawn counter, not a client)
and the pump rebuilds it on every spawn, so no per-client fold survives a
reconnect. When the mirrored surface carries connection, pass the pump a
connection: { set } and it drives that cell for you off the session’s state.
3. Re-serve the same contract locally
Flatten the fragment into a top-level router. A directLink over it is the
in-process consumer; a browser reaches the same router over a WebSocket, with the
app layer’s acceptSurfaceSocket gating each upgrade (see
@kolu/surface-app). The browser now consumes the
local copy exactly as if the agent were in-process.
// Flatten the mirror fragment into a top-level router; a browser consumes the
// local copy exactly as if the agent were in-process.
const router = implement(source.contract).router({ ...fragment.router });
const link = directLink<typeof source.contract>(router);
The connection cell is parent-authoritative
Connection health is state the remote agent cannot report — it can’t observe
the link to itself. So the parent is the sole writer of the connection cell:
mirroredSurface(base) reserves the name (it throws on a base that already
declares one), and the pump projects the session’s lifecycle onto it. Everything
else in the surface is the agent’s truth, mirrored; connection is the one cell
the parent authors. For the model behind this pump, see
The server half.