lembas -l / --local: everything in the project, nothing written elsewhere

Config, data and state are rooted at <project>/.agent/local/lembas (or
.lembas/), kept out of git; a local directory the repository carries is
refused. The user's logins and connections.yaml are read, never written; the
user's config.yaml is not read. The TUI is started with TMPDIR inside it (the
compiled binary unpacks its native library there) and OpenTUI's cache goes
there too; the agent's commands keep the user's TMPDIR.

An instance serves models, chat, speech and the balance only: the link,
/remote, the service and the hub are off, deletes are not sent, and the server
library, search, fetch, embeddings and personalization are not used.
Self-update and uninstall are refused. hardline_disable is ignored, and tools
treat the local directory as outside the project.

The smoke test runs a whole local session in the compiled binary and checks
that HOME, the XDG directories and TMPDIR gained nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-14 18:16:28 +00:00
1 parent 479f647cc9
commit b75923e724
27 files changed
+636 -105

No files matched your search

+2 -2
View File
@@ -20,11 +20,11 @@ export interface Driver {
modes(): { mouse: string; altScreen: boolean; cursorHidden: boolean }
}
export async function drive(opts: { cols: number; rows: number; bin?: string; home: string; cwd: string; args?: string[] }): Promise<Driver> {
export async function drive(opts: { cols: number; rows: number; bin?: string; home: string; cwd: string; args?: string[]; env?: Record<string, string> }): Promise<Driver> {
const cmd = opts.bin ? resolve(opts.bin) : `bun --config=${resolve(import.meta.dir, "../bunfig.toml")} ${resolve(import.meta.dir, "../src/cli.ts")}`
const p = Bun.spawn(["script", "-qfec", `stty cols ${opts.cols} rows ${opts.rows}; exec ${cmd} ${(opts.args ?? []).join(" ")}`, "/dev/null"], {
cwd: opts.cwd,
env: { ...process.env, LEMBAS_HOME: opts.home, TERM: "xterm-256color" },
env: { ...process.env, LEMBAS_HOME: opts.home, TERM: "xterm-256color", ...opts.env },
stdin: "pipe",
stdout: "pipe",
})
+43 -1
View File
@@ -2,7 +2,7 @@
// Smoke test of the COMPILED binary — what `bun test` cannot see: that the bundle, OpenTUI's
// native library and bun:sqlite FTS5 all survive `bun build --compile`.
// bun run scripts/smoke.ts [path/to/binary]
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join, resolve } from "node:path"
import { delta, fakeProvider, toolCall, usage } from "../tests/fake-provider.ts"
@@ -192,4 +192,46 @@ for (const [cols, rows] of [
check("/reload: the terminal is given back on quit", modes.mouse === "none" && !modes.altScreen, JSON.stringify(modes))
}
// 6. local mode (-l): everything in the project, nothing written anywhere else — the user's
// directories, HOME and TMPDIR are checked file by file before and after a whole session.
{
const { fake: f, home: h, cwd } = fakeHome([
{ chunks: [delta({ content: "Writing it." }), toolCall(0, "c1", "bash", '{"command":"echo local > out.txt","description":"Write out.txt"}'), usage(900, 40)] },
{ chunks: [delta({ content: "local done" }), usage(1200, 20)] },
])
const tmp = mkdtempSync(join(tmpdir(), "lembas-smoke-tmp-"))
const listing = (dir: string): string[] => {
const out: string[] = []
for (const e of readdirSync(dir, { withFileTypes: true, recursive: true })) out.push(join(e.parentPath, e.name))
return out.sort()
}
const before = { home: listing(h), tmp: listing(tmp) }
const d = await drive({ cols: 100, rows: 30, bin, home: h, cwd, args: ["-l"], env: { HOME: h, TMPDIR: tmp, XDG_CONFIG_HOME: join(h, ".config"), XDG_DATA_HOME: join(h, ".local/share"), XDG_STATE_HOME: join(h, ".local/state") } })
const firstRun = await d.waitFor("Trust this directory?")
await d.key("\r")
const drawn = await d.waitFor("☤ LLeMbas", 15000)
await d.type("make a file")
await d.key("\r")
const asked = await d.waitFor("Allow once")
await d.key("y")
const answered = await d.waitFor("local done")
await d.type("/remote")
await d.key("\r")
const refused = await d.waitFor("off in local mode")
const screen = d.screen()
await d.key("\x03")
await d.key("\x03")
const code = await d.close()
f.stop()
const after = { home: listing(h), tmp: listing(tmp) }
const grew = [...after.home.filter((p) => !before.home.includes(p)), ...after.tmp.filter((p) => !before.tmp.includes(p))]
const kept = ["data/sessions.db", "state/trust.json"].every((p) => existsSync(join(cwd, ".agent/local/lembas", p)))
check(
"local mode: a whole session works from the project",
firstRun && drawn && asked && answered && refused && kept && code === 0 && (screen[29] ?? "").includes("local"),
`firstRun=${firstRun} drawn=${drawn} asked=${asked} answered=${answered} refused=${refused} kept=${kept} exit=${code}\n${screen.join("\n")}`,
)
check("local mode: nothing written outside the project (home, XDG dirs, TMPDIR)", grew.length === 0, grew.join(", "))
}
process.exit(failed ? 1 : 0)