Files
HomerandClaude Opus 5.5 f9bad01ed7
ci / check (push) Waiting to run
LLeMbas CLI 1.0.0
The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM
API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a
link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64
and arm64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:59:03 +00:00

436 lines
23 KiB
TypeScript

// ACP: the JSON-RPC peer, LLeMbas CLI as an agent (serve --stdio and the LLeMbas link
// share it), the device's own limits on remote work, the link itself against a fake LLeMbas, the
// service unit, and library: lembas.
import { afterEach, expect, test } from "bun:test"
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { AcpAgent, clampMode, underRoots, type Limits } from "../src/acp/agent.ts"
import { LinkError, runLink } from "../src/acp/link.ts"
import { Peer, type Transport } from "../src/acp/rpc.ts"
import { paths } from "../src/config/paths.ts"
import { setTrust } from "../src/project/root.ts"
import { unitText } from "../src/service.ts"
import { delta, fakeProvider, toolCall, type Fake } from "./fake-provider.ts"
let fake: Fake | undefined
const stops: (() => void)[] = []
afterEach(() => {
fake?.stop()
for (const s of stops.splice(0)) s()
})
/** Two transports wired to each other, as a pipe would be. */
function pair(): [Transport, Transport] {
const make = () => ({ msg: (_: string) => {}, end: () => {} })
const a = make()
const b = make()
const side = (me: typeof a, other: typeof a): Transport => ({
send: (t) => queueMicrotask(() => other.msg(t)),
onMessage: (fn) => (me.msg = fn),
onClose: (fn) => (me.end = fn),
close: () => (me.end(), other.end()),
})
return [side(a, b), side(b, a)]
}
function config(url: string, extra = "") {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${url}\n models: { m: {} }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), `model: f/m\n${extra}`)
}
function project(): string {
const dir = realpathSync(mkdtempSync(join(tmpdir(), "ph-acp-")))
setTrust(dir, "trusted")
return dir
}
test("the peer: requests both ways, notifications, errors, and a close", async () => {
const [ta, tb] = pair()
const a = new Peer(ta)
const b = new Peer(tb)
b.handle("add", (p) => p.x + p.y)
b.handle("boom", () => {
throw new Error("no")
})
const heard: unknown[] = []
b.on("note", (p) => void heard.push(p))
expect(await a.request<number>("add", { x: 2, y: 3 })).toBe(5)
await expect(a.request("boom")).rejects.toThrow("no")
await expect(a.request("missing")).rejects.toThrow("Method not found")
a.notify("note", { n: 1 })
await Bun.sleep(5)
expect(heard).toEqual([{ n: 1 }])
a.handle("slow", () => new Promise(() => {}))
const pending = b.request("slow")
ta.close()
await expect(pending).rejects.toThrow("closed")
})
test("limits: roots, and a mode never above the device's", () => {
expect(underRoots("/srv/app/src", ["/srv/app"])).toBe(true)
expect(underRoots("/srv/application", ["/srv/app"])).toBe(false)
expect(underRoots("/etc", [])).toBe(false)
expect(clampMode("auto", "edit")).toBe("edit")
expect(clampMode("plan", "edit")).toBe("plan")
expect(clampMode("manual", "edit")).toBe("manual")
})
async function client(limits?: Limits, answer?: (p: any) => unknown, instanceConnection?: string) {
const [ta, tb] = pair()
const agentSide = new Peer(ta)
new AcpAgent(agentSide, limits, instanceConnection)
const c = new Peer(tb)
const updates: any[] = []
const events: any[] = []
c.on("session/update", (p) => void updates.push(p.update))
c.on("_lembas/event", (p) => void events.push(p.event))
c.handle("session/request_permission", (p) => answer?.(p) ?? { outcome: { outcome: "selected", optionId: "once" } })
const init: any = await c.request("initialize", { protocolVersion: 1, clientCapabilities: { _meta: { lembas: true } } })
return { c, updates, events, init }
}
test("a prompt streams its reply and ends with a stop reason", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "Hello from " }), delta({ content: "the device." })] }])
config(fake.url)
const { c, updates, events, init } = await client()
expect(init.protocolVersion).toBe(1)
expect(init.agentInfo.name).toBe("LLeMbas CLI")
const s: any = await c.request("session/new", { cwd: project(), mcpServers: [] })
const r: any = await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "hi" }] })
expect(r.stopReason).toBe("end_turn")
const text = updates.filter((u) => u.sessionUpdate === "agent_message_chunk").map((u) => u.content.text).join("")
expect(text).toBe("Hello from the device.")
// LLeMbas asked for the full events, and got them.
expect(events.some((e) => e.type === "done")).toBe(true)
})
test("an approval is asked of the client, and its answer decides", async () => {
fake = fakeProvider([
{ chunks: [toolCall(0, "t1", "bash", JSON.stringify({ command: "echo approved-run", description: "say it" }))] },
{ chunks: [delta({ content: "done" })] },
{ chunks: [toolCall(0, "t2", "bash", JSON.stringify({ command: "echo denied-run", description: "say it" }))] },
{ chunks: [delta({ content: "fine" })] },
])
config(fake.url, "mode: manual\n")
let asked = 0
const { c, updates } = await client(undefined, (p) => {
asked++
expect(p.toolCall.title).toContain("echo")
return asked === 1 ? { outcome: { outcome: "selected", optionId: "once" } } : { outcome: { outcome: "selected", optionId: "deny" } }
})
const s: any = await c.request("session/new", { cwd: project() })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "run it" }] })
const done = updates.find((u) => u.sessionUpdate === "tool_call_update" && u.toolCallId === "t1")
expect(done.status).toBe("completed")
expect(done.content[0].content.text).toContain("approved-run")
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "again" }] })
const refused = updates.find((u) => u.sessionUpdate === "tool_call_update" && u.toolCallId === "t2")
expect(refused.status).toBe("failed")
expect(asked).toBe(2)
})
test("the device's limits: outside the roots, untrusted, a higher mode", async () => {
fake = fakeProvider([])
config(fake.url)
const root = project()
const limits: Limits = { roots: [root], maxMode: "edit", approvalTimeoutMs: 50, requireTrust: true }
const { c, init } = await client(limits)
expect(init._meta.lembas.limits).toEqual({ roots: [root], max_mode: "edit" })
await expect(c.request("session/new", { cwd: tmpdir() })).rejects.toThrow("outside the directories")
const untrusted = join(root, "..", `ph-untrusted-${Date.now()}`)
mkdirSync(untrusted)
await expect(c.request("session/new", { cwd: untrusted })).rejects.toThrow("outside")
const s: any = await c.request("session/new", { cwd: root, _meta: { mode: "auto" } })
expect(s.modes.currentModeId).toBe("edit")
expect(s.modes.availableModes.map((m: any) => m.id).sort()).toEqual(["edit", "manual", "plan"])
await c.request("session/set_mode", { sessionId: s.sessionId, modeId: "auto" })
})
test("inside a trusted directory is trusted for remote work, a git repository in it too", async () => {
fake = fakeProvider([])
config(fake.url)
const home = project()
const repo = join(home, "Ambitious")
mkdirSync(join(repo, ".git", "objects"), { recursive: true })
writeFileSync(join(repo, ".git", "HEAD"), "ref: refs/heads/main\n")
const { c } = await client({ roots: [home], maxMode: "auto", approvalTimeoutMs: 50, requireTrust: true })
const s: any = await c.request("session/new", { cwd: repo })
expect(s.sessionId).toBeTruthy()
})
test("_lembas/directories: the trusted places, then what is in one, each said accepted or not", async () => {
fake = fakeProvider([])
config(fake.url)
const parent = realpathSync(mkdtempSync(join(tmpdir(), "ph-dirs-")))
const home = join(parent, "home")
const other = join(parent, "elsewhere")
for (const d of [join(home, "Ambitious", "src"), join(home, ".hidden"), join(home, "node_modules"), other]) mkdirSync(d, { recursive: true })
writeFileSync(join(home, "a-file.txt"), "x")
setTrust(home, "trusted")
setTrust(other, "trusted") // trusted, but not under the roots: never offered
const { c } = await client({ roots: [home], maxMode: "auto", approvalTimeoutMs: 50, requireTrust: true })
const places: any = await c.request("_lembas/directories", {})
expect(places.places).toEqual([{ path: home, name: "home", accepted: true }])
const inside: any = await c.request("_lembas/directories", { path: home })
expect(inside.accepted).toBe(true)
expect(inside.parent).toBe("")
// Directories only, no hidden ones, no node_modules.
expect(inside.entries).toEqual([{ path: join(home, "Ambitious"), name: "Ambitious", accepted: true }])
const deeper: any = await c.request("_lembas/directories", { path: join(home, "Ambitious") })
expect(deeper.parent).toBe(home)
expect(deeper.entries.map((e: any) => e.name)).toEqual(["src"])
await expect(c.request("_lembas/directories", { path: other })).rejects.toThrow("outside the directories")
await expect(c.request("_lembas/directories", { path: "relative/path" })).rejects.toThrow("absolute")
})
test("_lembas/directories: an untrusted directory is listed, and said to be refused", async () => {
fake = fakeProvider([])
config(fake.url)
const root = realpathSync(mkdtempSync(join(tmpdir(), "ph-dirs-untrusted-")))
mkdirSync(join(root, "sub"))
const { c } = await client({ roots: [root], maxMode: "edit", approvalTimeoutMs: 50, requireTrust: true })
const places: any = await c.request("_lembas/directories", {})
expect(places.places[0].accepted).toBe(false)
expect(places.places[0].reason).toContain("not in a trusted project")
const inside: any = await c.request("_lembas/directories", { path: root })
expect(inside.entries[0]).toMatchObject({ name: "sub", accepted: false })
})
test("_lembas/directories is for the link only: an editor's ACP session has no roots to offer", async () => {
const { c } = await client()
await expect(c.request("_lembas/directories", {})).rejects.toThrow("LLeMbas link only")
})
test("the chat's model and effort: named by the instance, at the start and between prompts", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "one" })] }, { chunks: [delta({ content: "two" })] }])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n example:\n dialect: openai-chat\n base_url: ${fake.url}\n models: { bonsai: {}, deepseek-flash: { efforts: [low, medium, high] } }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: example/bonsai\n")
const root = project()
const { c } = await client({ roots: [root], maxMode: "auto", approvalTimeoutMs: 50, requireTrust: true }, undefined, "example")
// Not the device's own default: the model the chat was started on there.
const s: any = await c.request("session/new", { cwd: root, _meta: { mode: "auto", lembas_model: "deepseek-flash", effort: "high" } })
expect(s._meta.lembas).toMatchObject({ model: "example/deepseek-flash", effort: "high" })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "hi" }] })
expect(fake.requests[0].model).toBe("deepseek-flash")
expect(JSON.stringify(fake.requests[0])).toContain('"reasoning_effort":"high"')
// Changed in the web UI between two prompts.
const changed: any = await c.request("_lembas/configure", { sessionId: s.sessionId, model: "bonsai", effort: "off" })
expect(changed).toEqual({ model: "example/bonsai", effort: "off" })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "again" }] })
expect(fake.requests.at(-1).model).toBe("bonsai")
expect(JSON.stringify(fake.requests.at(-1))).not.toContain("reasoning_effort")
})
test("the terminal: a real shell through the link, only where remote.terminal says so", async () => {
fake = fakeProvider([])
config(fake.url)
const root = project()
const off = await client({ roots: [root], maxMode: "edit", approvalTimeoutMs: 50, requireTrust: true })
expect(off.init._meta.lembas.capabilities).toEqual(["directories", "configure", "steer", "sessions", "status", "delete", "compact", "title", "files", "commands", "attachments", "ask", "plan", "model", "turns"])
await expect(off.c.request("_lembas/terminal/open", { cwd: root })).rejects.toThrow("remote.terminal is off")
const { c, init } = await client({ roots: [root], maxMode: "edit", approvalTimeoutMs: 50, requireTrust: true, terminal: true })
expect(init._meta.lembas.capabilities).toContain("terminal")
expect(init._meta.lembas.capabilities).toContain("shell")
await expect(c.request("_lembas/terminal/open", { cwd: tmpdir() })).rejects.toThrow("outside")
let out = ""
let exited: any
c.on("_lembas/terminal/output", (p) => void (out += Buffer.from(p.data, "base64").toString()))
c.on("_lembas/terminal/exit", (p) => void (exited = p))
const t: any = await c.request("_lembas/terminal/open", { cwd: root, cols: 100, rows: 30 })
expect(t.cwd).toBe(root)
// Which shell, and whether its prompts are marked: bash, zsh and fish are, by default.
const kind = ["bash", "zsh", "fish"].includes((process.env.SHELL || "/bin/bash").split("/").pop()!) ? (process.env.SHELL || "/bin/bash").split("/").pop() : "other"
expect(t.shell).toBe(kind)
expect(t.integration).toBe(kind !== "other")
c.notify("_lembas/terminal/input", { terminalId: t.terminalId, data: Buffer.from("pwd; stty size; exit\n").toString("base64") })
for (let i = 0; i < 100 && !exited; i++) await Bun.sleep(50)
expect(out).toContain(root)
expect(out).toContain("30 100")
expect(exited.terminalId).toBe(t.terminalId)
})
test("a message sent mid-task reaches the model at its next step, as in the TUI", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "done" })] }, { chunks: [delta({ content: "and that too" })] }])
config(fake.url)
const root = project()
const { c } = await client({ roots: [root], maxMode: "edit", approvalTimeoutMs: 50, requireTrust: true })
const s: any = await c.request("session/new", { cwd: root })
await expect(c.request("_lembas/steer", { sessionId: s.sessionId, text: "also this" })).rejects.toThrow("not working on anything")
const running = c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "hi" }] })
const queued: any = await c.request("_lembas/steer", { sessionId: s.sessionId, text: "also this" })
expect(queued.queued).toBe(1)
const r: any = await running
// Taken in at the next step boundary — the first one, here, or after the answer — never lost.
expect(r._meta.lembas.unsent).toBeUndefined()
expect(fake.requests.some((q: any) => JSON.stringify(q.messages).includes("also this"))).toBe(true)
})
test("with busy_input: queue, a message the task never took is handed back as unsent", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "done" })] }])
config(fake.url, "busy_input: queue\n")
const root = project()
const { c } = await client({ roots: [root], maxMode: "edit", approvalTimeoutMs: 50, requireTrust: true })
const s: any = await c.request("session/new", { cwd: root })
const running = c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "hi" }] })
await c.request("_lembas/steer", { sessionId: s.sessionId, text: "later" })
const r: any = await running
expect(r._meta.lembas.unsent).toEqual(["later"])
})
test("an approval nobody answers is a denial", async () => {
fake = fakeProvider([
{ chunks: [toolCall(0, "t1", "bash", JSON.stringify({ command: "echo never", description: "x" }))] },
{ chunks: [delta({ content: "ok" })] },
])
config(fake.url, "mode: manual\n")
const root = project()
const { c, updates } = await client({ roots: [root], maxMode: "manual", approvalTimeoutMs: 50, requireTrust: true }, () => new Promise(() => {}))
const s: any = await c.request("session/new", { cwd: root })
await c.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "x" }] })
const u = updates.find((x) => x.sessionUpdate === "tool_call_update" && x.toolCallId === "t1")
expect(u.status).toBe("failed")
expect(fake.requests[1].messages.at(-1).content).toContain("Nobody answered")
})
test("the link: off unless the device says so", async () => {
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "config.yaml"), "")
await expect(runLink()).rejects.toBeInstanceOf(LinkError)
writeFileSync(join(paths.config, "config.yaml"), "remote:\n enabled: true\n")
await expect(runLink()).rejects.toThrow("remote.roots is empty")
})
test("the link dials out with the token, says hello, is driven, and stops when refused", async () => {
fake = fakeProvider([{ chunks: [delta({ content: "linked reply" })] }])
const root = project()
config(fake.url, `remote:\n enabled: true\n roots: [${root}]\n max_mode: edit\n`)
let hello: any
let authorization = ""
let connections = 0
let reply = ""
const server = Bun.serve({
port: 0,
fetch(req, srv) {
const base = `http://${new URL(req.url).host}`
// A discovery saying protocol 1 for older CLIs, protocols [1, 2] for this one.
if (new URL(req.url).pathname === "/.well-known/lembas.json")
return Response.json({ service: "lembas", version: "2.2.0", protocol: 1, protocols: [1, 2], harness_spec: "2.1.0", base_url: base, api: { openai: `${base}/v1` }, login: { device: { code: `${base}/c`, token: `${base}/t`, verify: `${base}/d` } } })
authorization = req.headers.get("authorization") ?? ""
if (new URL(req.url).pathname === "/api/devices/link" && srv.upgrade(req)) return
return new Response("no", { status: 404 })
},
websocket: {
async open(ws) {
connections++
if (connections > 1) return ws.close(4401, "revoked")
const [mine, theirs] = pair()
// Bridge: the fake instance is an ACP client over this socket.
;(ws as any).data = theirs
const peer = new Peer({ send: (t) => ws.send(t), onMessage: (fn) => ((ws as any).recv = fn), onClose: () => {}, close: () => ws.close() })
void mine
peer.on("_lembas/hello", (p) => void (hello = p))
peer.on("session/update", (p) => {
if (p.update.sessionUpdate === "agent_message_chunk") reply += p.update.content.text
})
await peer.request("initialize", { protocolVersion: 1, clientCapabilities: {} })
const s: any = await peer.request("session/new", { cwd: root })
await peer.request("session/prompt", { sessionId: s.sessionId, prompt: [{ type: "text", text: "go" }] })
ws.close(1000, "done")
},
message(ws, msg) {
;(ws as any).recv?.(typeof msg === "string" ? msg : new TextDecoder().decode(msg))
},
},
})
stops.push(() => server.stop(true))
const base = `http://127.0.0.1:${server.port}`
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { fake: { base_url: base, connection: "fake", logged_in_at: "" } } }))
mkdirSync(join(paths.config, "lembas"), { recursive: true })
writeFileSync(join(paths.config, "lembas", "fake.key"), "lmb_device\n", { mode: 0o600 })
const states: string[] = []
const end = await runLink({ backoffMs: [10, 20], onStatus: (s) => states.push(s.state) })
expect(authorization).toBe("Bearer lmb_device")
expect(hello.protocol).toBe(2)
expect(hello.limits.max_mode).toBe("edit")
expect(reply).toBe("linked reply")
expect(end.state).toBe("stopped")
expect(end.detail).toContain("revoked")
expect(states).toContain("linked")
expect(states).toContain("waiting")
})
test("the service unit runs the link as the user, restarted on failure", () => {
const unit = unitText("/usr/local/bin/lembas service run")
expect(unit).toContain("ExecStart=/usr/local/bin/lembas service run")
expect(unit).toContain("Restart=on-failure")
expect(unit).toContain("WantedBy=default.target")
expect(unit).not.toContain("User=")
})
test("the link says protocol 1 to an instance that lists only 1, and stops on 4400 instead of redialling", async () => {
const root = project()
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "config.yaml"), `remote:\n enabled: true\n roots: [${root}]\n`)
const hellos: number[] = []
let dials = 0
let lists = [1]
let everything4400 = false
const server = Bun.serve({
port: 0,
fetch(req, srv) {
const u = new URL(req.url)
const base = `http://${u.host}`
if (u.pathname === "/.well-known/lembas.json")
return Response.json({ service: "lembas", version: "2.1.0", protocol: 1, ...(lists.length > 1 ? { protocols: lists } : {}), harness_spec: "2.0.1", base_url: base, api: { openai: `${base}/v1` }, login: { device: { code: `${base}/c`, token: `${base}/t`, verify: `${base}/d` } } })
if (u.pathname === "/api/devices/link" && srv.upgrade(req)) return
return new Response("no", { status: 404 })
},
websocket: {
open() {
dials++
},
message(ws, msg) {
const m = JSON.parse(String(msg))
if (m.method !== "_lembas/hello") return
hellos.push(m.params.protocol)
// LLeMbas 2.1.0: a hello saying a protocol it does not speak is closed with 4400.
if (m.params.protocol !== 1 || everything4400) ws.close(4400, "protocol")
else ws.close(4401, "revoked")
},
},
})
stops.push(() => server.stop(true))
writeFileSync(join(paths.config, "lembas.json"), JSON.stringify({ instances: { fake: { base_url: `http://127.0.0.1:${server.port}`, connection: "fake", logged_in_at: "" } } }))
mkdirSync(join(paths.config, "lembas"), { recursive: true })
writeFileSync(join(paths.config, "lembas", "fake.key"), "lmb_device\n", { mode: 0o600 })
const one = await runLink({ backoffMs: [10, 20] })
expect(hellos).toEqual([1])
expect(one.detail).toContain("revoked")
// The instance updated since: discovery lists 2 now, the next link says 2 (and the index keeps it).
lists = [1, 2]
const old = (await import("../src/lembas/login.ts")).instances().fake
expect(old?.protocols).toEqual([1])
// An instance that closes a hello of 2 with 4400 anyway (discovery said more than its link takes):
// one dial again at once, saying 1 — here refused for the token, which stops it as usual.
const two = await runLink({ backoffMs: [10, 20] })
expect(hellos).toEqual([1, 2, 1])
expect(two.state).toBe("stopped")
expect(two.detail).toContain("revoked")
expect(dials).toBe(3)
expect((await import("../src/lembas/login.ts")).instances().fake?.protocols).toEqual([1, 2])
// A 4400 to protocol 1 as well: stopped, said plainly, not dialled again.
everything4400 = true
const three = await runLink({ backoffMs: [10, 20] })
expect(hellos).toEqual([1, 2, 1, 2, 1])
expect(three.detail).toBe("the instance and this CLI speak different link protocols — update one of them")
expect(dials).toBe(5)
})