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

+4 -2
View File
@@ -20,7 +20,7 @@
import { chmodSync, existsSync, mkdirSync, rmSync } from "node:fs"
import { connect, createServer, type Server, type Socket } from "node:net"
import { join } from "node:path"
import { paths } from "../config/paths.ts"
import { isLocal, paths } from "../config/paths.ts"
import type { Mode } from "../config/schema.ts"
import type { CommandInfo } from "../session/commands.ts"
import { clampMode, refusalFor, type AcpAgent, type Limits } from "./agent.ts"
@@ -77,7 +77,8 @@ export function answers(path = hubSocket(), timeoutMs = 1000): Promise<boolean>
/** Connect to the hub, if one runs. */
export function connectHub(path = hubSocket()): Promise<Peer | undefined> {
if (!existsSync(path)) return Promise.resolve(undefined)
// Local mode shares nothing: no hub is reached, and none is held.
if (isLocal() || !existsSync(path)) return Promise.resolve(undefined)
return new Promise((resolve) => {
const sock = connect(path)
const timer = setTimeout(() => (sock.destroy(), resolve(undefined)), 2000)
@@ -138,6 +139,7 @@ export class Hub {
/** Take the socket. False: another hub is alive on it. */
async listen(path = hubSocket()): Promise<boolean> {
if (isLocal()) return false
mkdirSync(paths.state, { recursive: true })
if (existsSync(path)) {
if (await answers(path)) return false
+3
View File
@@ -9,6 +9,7 @@
import { existsSync, readFileSync } from "node:fs"
import { hostname } from "node:os"
import { remoteBlock } from "../config/remote.ts"
import { refusedLocally } from "../project/local.ts"
import type { Mode } from "../config/schema.ts"
import { HARNESS_VERSION } from "../harness.ts"
import { instances, keyFile, rememberProtocols, type Instance } from "../lembas/login.ts"
@@ -124,6 +125,8 @@ export interface LinkOptions {
* `roots: []` and can tell a device that allows no folder yet from one that is not running, and a
* `lembas remote add` or `enable` makes it serve, without a restart. */
export async function runLink(o: LinkOptions = {}): Promise<LinkStatus> {
const local = refusedLocally("the link to a LLeMbas instance (lembas service, /remote)")
if (local) throw new LinkError(local)
const limits = limitsFromConfig({ sharedOnly: o.sharedOnly })
const poll = o.pollMs ?? 2000
const instance = pickInstance(o.instance)
+3 -2
View File
@@ -14,7 +14,7 @@
// out of anybody's mind.
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { paths } from "../config/paths.ts"
import { isLocal, paths } from "../config/paths.ts"
export const DELETED_MAX = 500
@@ -43,7 +43,8 @@ function write(ids: string[]) {
* directory that cannot be written to only loses the word. */
export function addPendingDeleted(ids: string[]) {
const add = ids.filter(Boolean)
if (!add.length) return
// Local mode tells the instance nothing, now or later.
if (!add.length || isLocal()) return
try {
// An id already waiting moves to the end: it is the newest word about it.
const list = [...pendingDeleted().filter((x) => !add.includes(x)), ...new Set(add)]
+5
View File
@@ -18,6 +18,8 @@ import { clampMode, effortOf, markRemote, permissionParams, planParams, planRepl
import { sessionTurnFile, sessionTurns, type TurnLog } from "./turns.ts"
import { dropAttachments, preparePrompt } from "./prompt.ts"
import { connectHub, Hub } from "./hub.ts"
import { isLocal } from "../config/paths.ts"
import { localRefusal, refusedLocally } from "../project/local.ts"
import { addPendingDeleted } from "./pending.ts"
import { REMOTE_META, type Store } from "../session/store.ts"
import { limitsFromConfig, runLink } from "./link.ts"
@@ -193,6 +195,7 @@ export class Share {
}
status(): string {
if (isLocal()) return localRefusal("sharing with the web UI")
if (this.sharedId) return `this session is shared with the web UI ${this.own ? "— the link is held by this terminal, so it lasts as long as the terminal does" : "through the service"}`
if (this.hub && this.service) return "not shared: this session is outside what the service shares (remote.roots, trust) — /remote shares it anyway"
if (this.hub) return "not shared — /remote shares it"
@@ -342,6 +345,8 @@ export class Share {
/** Hold the link in this terminal, for /remote while no service runs. Returns why not, if not. */
private async becomeHub(): Promise<string | undefined> {
const local = refusedLocally("/remote")
if (local) return local
const { instances } = await import("../lembas/login.ts")
if (!Object.keys(instances()).length) return "not logged in to a LLeMbas instance — /login first"
const hub = new Hub({ service: false, limits: limitsFromConfig() })