kolu
Docs

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.

Mirroring a surface over ssh in three hopsA remote agent serves its surface over stdio. The parent dials it over ssh (hop one), pumps its frames into a local implementSurface copy (hop two), and re-serves that copy over a direct or WebSocket link (hop three). The browser only ever faces the local copy; only the ssh pipe behind it swaps.remote agentserves its surfaceover stdiothe source of truthparent process② pumppumpRemoteSurfacefolds frames intoimplementSurfacethe LOCAL copya live mirror ofeverything served③ re-servedirectLink / wsbrowserfaces thelocal copy① dialssh · makeSession+ sshConnectorthis pipe swapsfolds inthe browser always faces the LOCAL copyonly the pipe behind it swaps
The remote agent serves over stdio; the parent ① dials it over ssh, ② pumps its frames into a local 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.