// The link: this machine dials out to the LLeMbas instance it is logged in to, and the web
// UI drives sessions here over ACP. Nothing listens on this machine — the connection is outbound,
// one per device, re-dialled with backoff when it drops — and what the instance may ask is bounded
// by this machine's own global `remote:` block (agent.ts: Limits), never by the server.
//
// One WebSocket message is one JSON-RPC message. The token goes in the Authorization header and
// never in the address. A token the instance refuses (revoked from its Settings → Security, or the
// account lost the permission) ends the link for good: retrying it would only knock on a closed door.
import { existsSync, readFileSync } from "node:fs"
import { hostname } from "node:os"
import { loadConfig } from "../config/load.ts"
import type { Mode } from "../config/schema.ts"
import { HARNESS_VERSION } from "../harness.ts"
import { instances, keyFile, rememberProtocols, type Instance } from "../lembas/login.ts"
import { discover, linkProtocol, spoken } from "../lembas/client.ts"
import { VERSION } from "../version.ts"
import { AcpAgent, type Limits } from "./agent.ts"
import type { Hub } from "./hub.ts"
import { Peer, type Transport } from "./rpc.ts"
export class LinkError extends Error {}
export function limitsFromConfig(): Limits & { enabled: boolean } {
const r = loadConfig().config.remote ?? {}
return {
enabled: r.enabled === true,
roots: r.roots ?? [],
maxMode: (r.max_mode ?? "edit") as Mode,
approvalTimeoutMs: (r.approval_timeout ?? 600) * 1000,
requireTrust: r.require_trust !== false,
terminal: r.terminal === true,
terminalIntegration: r.terminal_integration !== false,
}
}
export function pickInstance(name?: string): Instance {
const all = instances()
if (name) {
const one = all[name]
if (!one) throw new LinkError(`not logged in to ${name}${Object.keys(all).length ? ` — logged in: ${Object.keys(all).join(", ")}` : ""}`)
return one
}
const list = Object.values(all)
if (!list.length) throw new LinkError("not logged in to any LLeMbas instance — lembas login
")
if (list.length > 1) throw new LinkError(`logged in to several instances; name one: ${Object.keys(all).join(", ")}`)
return list[0]!
}
export function linkUrl(instance: Instance): string {
const u = new URL(instance.base_url)
u.protocol = u.protocol === "https:" ? "wss:" : "ws:"
u.pathname = "/api/devices/link"
return u.toString()
}
function wsTransport(ws: WebSocket): Transport {
let onMsg: (t: string) => void = () => {}
let onEnd: () => void = () => {}
ws.addEventListener("message", (e) => onMsg(typeof e.data === "string" ? e.data : new TextDecoder().decode(e.data as ArrayBuffer)))
ws.addEventListener("close", () => onEnd())
return { send: (t) => ws.send(t), onMessage: (fn) => (onMsg = fn), onClose: (fn) => (onEnd = fn), close: () => ws.close() }
}
export interface LinkStatus {
state: "connecting" | "linked" | "waiting" | "stopped"
instance: string
since: string
detail?: string
attempts: number
}
export interface LinkOptions {
instance?: string
/** Each change of state, for the service's status file and log. */
onStatus?: (s: LinkStatus) => void
/** Stop for good when this aborts. */
signal?: AbortSignal
/** Only for tests: the backoff's first wait and its ceiling. */
backoffMs?: [number, number]
/** The hub this link serves: the terminals' sessions reach the instance through it. */
hub?: Hub
/** A terminal's link for /remote: it runs whatever `remote.enabled` says; with remote
* work off, only the sessions the terminal shares can be reached. */
sharedOnly?: boolean
}
/** Dial, serve, and dial again, until stopped or refused. Resolves when it stops for good. */
export async function runLink(o: LinkOptions = {}): Promise {
const configured = limitsFromConfig()
const limits = o.sharedOnly && !configured.enabled ? { ...configured, roots: [], sharedOnly: true } : configured
if (!o.sharedOnly && !limits.enabled) throw new LinkError("remote work is off on this machine — set remote.enabled: true (and remote.roots) in ~/.config/lembas/config.yaml")
if (!o.sharedOnly && !limits.roots.length) throw new LinkError("remote.roots is empty, so no directory could be worked in — list the directories a remote session may use")
const instance = pickInstance(o.instance)
const token = readFileSync(keyFile(instance.connection), "utf8").trim()
// A service runs for weeks: the webui connections' models are read again every ten minutes, so a
// session the web UI opens here starts with the instance's models as they are then.
const freshen = setInterval(() => void import("../lembas/webui.ts").then((w) => w.refreshWebui()), 10 * 60_000)
freshen.unref?.()
const [first, ceiling] = o.backoffMs ?? [1000, 60_000]
let wait = first
let attempts = 0
const status = (state: LinkStatus["state"], detail?: string): LinkStatus => {
const s = { state, instance: instance.base_url, since: new Date().toISOString(), detail, attempts }
o.onStatus?.(s)
return s
}
const ca = instance.ca && existsSync(instance.ca) ? readFileSync(instance.ca, "utf8") : undefined
/** A 4400 to a hello that said 2: the protocols known were stale (or discovery said more than the
* link takes) — the next dial, at once, says 1; only a 4400 to that stops the link. */
let downgraded = false
while (!o.signal?.aborted) {
attempts++
status("connecting")
// What the instance speaks now: asked at every dial, since it may have been updated
// since the login; what was said last when it cannot be asked.
let protocols = instances()[instance.connection]?.protocols ?? instance.protocols
try {
const { discovery } = await discover(instance.base_url, ca ? { ca: instance.ca } : undefined)
protocols = spoken(discovery)
rememberProtocols(instance.connection, protocols)
} catch {}
const protocol = downgraded ? 1 : linkProtocol(protocols)
const outcome = await new Promise<{ refused?: string; dropped?: string; final?: boolean }>((resolve) => {
let opened = false
const ws = new WebSocket(linkUrl(instance), {
headers: { authorization: `Bearer ${token}`, "user-agent": `lembas-cli/${VERSION}` },
...(ca ? { tls: { ca } } : {}),
} as unknown as string[])
const stop = () => ws.close()
o.signal?.addEventListener("abort", stop, { once: true })
ws.addEventListener("open", () => {
opened = true
wait = first
const peer = new Peer(wsTransport(ws))
new AcpAgent(peer, limits, instance.connection, o.hub, protocol)
peer.notify("_lembas/hello", {
protocol,
version: VERSION,
harness_spec: HARNESS_VERSION,
device: { host: hostname(), platform: `${process.platform}-${process.arch}` },
limits: { roots: limits.roots, max_mode: limits.maxMode, require_trust: limits.requireTrust },
})
status("linked")
})
ws.addEventListener("close", (e) => {
o.signal?.removeEventListener("abort", stop)
// 4401/4403: the instance's word that this token is done; 1008 policy, likewise.
if ([4401, 4403, 1008].includes(e.code)) return resolve({ refused: e.reason || "the instance refused this device's token" })
// 4400: the hello's protocol is not one it speaks. Dialling again would say the same.
if (e.code === 4400) return resolve({ refused: "the instance and this CLI speak different link protocols — update one of them", final: true })
resolve({ dropped: opened ? `the link dropped (${e.code}${e.reason ? `: ${e.reason}` : ""})` : "could not reach the instance" })
})
ws.addEventListener("error", () => {})
})
if (o.signal?.aborted) break
if (outcome.final && protocol === 2 && !downgraded) {
downgraded = true
status("waiting", "the instance does not take link protocol 2; trying again with 1")
continue
}
if (outcome.refused) {
clearInterval(freshen)
return status("stopped", outcome.final ? outcome.refused : `${outcome.refused} — sign in again: lembas login`)
}
status("waiting", `${outcome.dropped}; trying again in ${Math.round(wait / 1000)}s`)
await new Promise((r) => setTimeout(r, wait))
wait = Math.min(ceiling, wait * 2)
}
clearInterval(freshen)
return status("stopped", "stopped")
}