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.
// `AgentClient` is the structural member face — there is no contract type to be
// generic over. The connector takes the SURFACE as a VALUE: Effect RPC builds
// its client from `surface.group`, and the face is re-nested from
// `surface.spec` — neither is recoverable from a type alone, and passing the
// surface is what makes the dialled face and the served group provably the same
// tag set.
const session: Session<AgentClient, SshProv> = makeSession({
initialConnection: "probing", // an ssh session provisions before it connects
connectOnce: sshConnector({
surface: base,
host: "[email protected]", // any ssh target; "localhost" short-circuits
binary: "my-agent", // exe name inside the realised closure
// Policy-free: YOU (the consumer) compose the localhost arm's spawn env, keeping only
// the keys that are SET (an empty HOME/PATH would misdirect lookups). kolu uses
// kolu-pty's `composeSpawnEnv`. Never the caller's ambient `process.env`; unused for ssh.
localEnv: Object.fromEntries(
(["HOME", "PATH"] as const)
.map((k): [string, string | undefined] => [k, process.env[k]])
.filter((e): e is [string, string] => e[1] !== undefined),
),
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. A standalone re-serve serves the agent’s base surface
verbatim.
// Re-serve the agent's base surface verbatim. (SR9: per-host connection health is a
// host-map concept — a `@kolu/surface-map` map's `entries` channel carries the fine
// connection payload, produced by `serveHostMap`; a standalone re-serve like this
// carries no `connection` cell.)
const source = base;
const runtime = implementSurface(source, {
cells: { load: { store: loadStore } },
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) => runtime.ctx.cells.load.set(v) },
collections: {
processes: {
upsert: (k, v) => runtime.ctx.collections.processes.upsert(k, v),
remove: (k) => runtime.ctx.collections.processes.remove(k),
},
},
}),
});
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.
3. Re-serve the same surface locally
The mirror runtime’s { group, handlers } is what any transport takes — there is
nothing to finalize and nothing to re-prefix, because a tag carries its own route.
A directDispatch over the runtime is the in-process consumer; a browser reaches
the same pair over a WebSocket, with the app layer’s acceptSurfaceSocket gating
each upgrade and serveSurfaceSocket serving it (see
@kolu/surface-app). The browser now consumes the
local copy exactly as if the agent were in-process.
// The mirror runtime's `{ group, handlers }` is what any transport takes; a
// browser consumes the local copy exactly as if the agent were in-process.
const dispatch = directDispatch(runtime);
Connection health is a host-map concept
Connection health is state the remote agent cannot report — it can’t observe
the link to itself. When you serve many hosts as a host map
(serve a map), that health rides each entry as a fine
connection payload — serveHostMap produces the coarse dot and the fine word
from the same session frame in one projection, the ONE authority both derive from
(see @kolu/surface-remote).
A standalone re-serve like the one above carries no per-host connection state;
if a real one ever needs it, the mechanism graduates from the host map with that
consumer in hand. For the model behind this pump, see
The server half.