Restore Boat desktop input before computer access

Probe IBus without replacing a healthy daemon. Rebind a failed restored socket to the same user private runtime directory before viewer access or Cua MCP startup; headless execution stays independent.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 10:47:31 -05:00
1 parent 6e4bd77b58
commit 3308a116d4
6 files changed
+107 -2

No files matched your search

@@ -14,7 +14,7 @@ import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { remoteProgram } from "./remote-program.js";
import { afterEach, describe, it, expect, vi } from "vitest";
import { boatBackend, processProgram } from "./boat.js";
import { boatBackend, desktopReadinessProgram, processProgram } from "./boat.js";
import { buildGitAuthInvocation } from "../../../services/git-credentials.js";
import type { ComputerRecord } from "../domain/ledger.js";
const record: ComputerRecord = {
@@ -384,3 +384,66 @@ case "$*" in *show*) printf '%s\\n' '${state}';; esac
expect(existsSync(f.calls)).toBe(false);
});
});
describe("Boat desktop input readiness", () => {
function desktopFixture(initiallyHealthy: boolean) {
const temp = realpathSync(mkdtempSync(join(tmpdir(), "computer-desktop-")));
roots.push(temp);
const runtime = join(temp, String(process.getuid!()));
const bin = join(temp, "bin");
const healthy = join(temp, "healthy");
const calls = join(temp, "repairs");
mkdirSync(runtime, { mode: 0o700 });
mkdirSync(bin);
if (initiallyHealthy) writeFileSync(healthy, "ready");
writeFileSync(join(bin, "ibus"), "#!/bin/sh\nprintf '%s\\n' 'unix:path=/test/ibus'\n", { mode: 0o700 });
writeFileSync(join(bin, "gdbus"), `#!/bin/sh\ntest -f '${healthy}'\n`, { mode: 0o700 });
writeFileSync(join(bin, "ibus-daemon"), `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\ntouch '${healthy}'\n`, { mode: 0o700 });
const call = (program = desktopReadinessProgram) => spawnSync("python3", ["-c", program.replaceAll("/run/user/", `${temp}/`)], {
encoding: "utf8", env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
});
return { temp, runtime, healthy, calls, call };
}
it("preserves a healthy input service and emits no MCP protocol output", () => {
const f = desktopFixture(true);
const result = f.call();
expect(result.status).toBe(0);
expect(result.stdout).toBe("");
expect(existsSync(f.calls)).toBe(false);
});
it("repairs a disconnected restored socket and checks it again after another resume", () => {
const f = desktopFixture(false);
expect(f.call().status).toBe(0);
const expected = `--replace --daemonize --xim --address unix:path=${f.runtime}/paperclip-ibus/bus\n`;
expect(readFileSync(f.calls, "utf8")).toBe(expected);
expect(f.call().status).toBe(0);
expect(readFileSync(f.calls, "utf8")).toBe(expected);
rmSync(f.healthy);
expect(f.call().status).toBe(0);
expect(readFileSync(f.calls, "utf8")).toBe(expected.repeat(2));
});
it("rejects a symlink input directory without replacing the daemon", () => {
const f = desktopFixture(false);
symlinkSync(f.temp, join(f.runtime, "paperclip-ibus"));
const result = f.call();
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("invalid desktop input directory");
expect(existsSync(f.calls)).toBe(false);
});
it("executes the existing Cua MCP process only after input readiness", () => {
const f = desktopFixture(false);
const tool = boatBackend(async () => "unused").computerTool();
const cua = join(f.temp, "cua");
writeFileSync(cua, `#!/bin/sh\nprintf '%s\\n' "$*"\n`, { mode: 0o700 });
expect(tool.command).toBe("python3");
const result = f.call(tool.args[1]!.replaceAll("/opt/ascii/cua-driver/cua-driver", cua));
expect(result.status).toBe(0);
expect(result.stdout).toBe("mcp --socket /run/ascii-cua/driver.sock\n");
expect(existsSync(f.healthy)).toBe(true);
});
});
@@ -19,6 +19,38 @@ const desktopCache = new Map<
string,
{ viewerUrl: string; expiresAt: string }
>();
export const desktopReadinessProgram = String.raw`
import os,sys,json,stat,fcntl,subprocess,time
# Boat restores its persistent home, but Unix sockets there can refer to stale
# kernel listeners. Keep a healthy IBus unchanged; repair only a failed probe.
uid=os.getuid();runtime='/run/user/'+str(uid)
st=os.lstat(runtime)
if not stat.S_ISDIR(st.st_mode) or st.st_uid!=uid:raise RuntimeError('invalid desktop runtime directory')
env={**os.environ,'DISPLAY':':0','XAUTHORITY':'/home/user/.Xauthority','DBUS_SESSION_BUS_ADDRESS':'unix:path='+runtime+'/bus','XDG_RUNTIME_DIR':runtime,'IBUS_ENABLE_SYNC_MODE':'0'}
env.pop('IBUS_ADDRESS',None)
def healthy():
try:
address=subprocess.run(['ibus','address'],env=env,capture_output=True,text=True,timeout=5)
if address.returncode or not address.stdout.strip().startswith('unix:'):return False
return subprocess.run(['gdbus','call','--address',address.stdout.strip(),'--dest','org.freedesktop.IBus','--object-path','/org/freedesktop/IBus','--method','org.freedesktop.DBus.Peer.Ping'],env=env,capture_output=True,timeout=5).returncode==0
except subprocess.TimeoutExpired:return False
root=runtime+'/paperclip-ibus'
os.makedirs(root,mode=0o700,exist_ok=True)
st=os.lstat(root)
if not stat.S_ISDIR(st.st_mode) or st.st_uid!=uid:raise RuntimeError('invalid desktop input directory')
os.chmod(root,0o700)
fd=os.open(root+'/lock',os.O_CREAT|os.O_RDWR|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'a') as lock:
fcntl.flock(lock,fcntl.LOCK_EX)
if not healthy():
result=subprocess.run(['ibus-daemon','--replace','--daemonize','--xim','--address','unix:path='+root+'/bus'],env=env,capture_output=True,timeout=20)
if result.returncode:raise RuntimeError('desktop input repair failed')
for attempt in range(10):
if healthy():break
time.sleep(0.2)
else:raise RuntimeError('desktop input did not become ready')
`;
export const processProgram = String.raw`
import os,sys,json,fcntl,subprocess,shutil,re
p=json.load(sys.stdin);base='/home/user/.paperclip-owners';os.makedirs(base,exist_ok=True)
@@ -389,7 +421,14 @@ print('{}')
async renew(record) {
await api(record, "PATCH", "", { ttlSeconds: 300 });
},
computerTool() {
return {
command: "python3",
args: ["-c", `${desktopReadinessProgram}\nos.execv('/opt/ascii/cua-driver/cua-driver',['cua-driver','mcp','--socket','/run/ascii-cua/driver.sock'])`],
};
},
async desktop(record) {
await execute(record, `${desktopReadinessProgram}\nprint('{}')`, {});
const cached = desktopCache.get(record.id);
if (cached && Date.parse(cached.expiresAt) > Date.now() + 60_000)
return cached;
@@ -53,6 +53,7 @@ export interface ComputerBackend {
id: string,
): Promise<{ id: string; status: string }>;
renew(record: ComputerRecord): Promise<void>;
computerTool(): { command: string; args: string[] };
desktop(
record: ComputerRecord,
): Promise<{ viewerUrl: string; expiresAt: string }>;
@@ -63,6 +63,7 @@ function fixture() {
stop: vi.fn(async () => ({ id: "stop_1", status: stopStatus })),
stopStatus: vi.fn(async () => ({ id: "stop_1", status: stopStatus })),
renew: vi.fn(async () => {}),
computerTool: () => ({ command: "cua-driver", args: ["mcp"] }),
desktop: vi.fn(async () => ({
viewerUrl: "https://test.on.boat.dev/#credential",
expiresAt: new Date(clock.getTime() + 540_000).toISOString(),
@@ -289,6 +289,7 @@ finally:
return {
owner: ownerRef,
runner: runnerFor(false),
computerTool: backend.computerTool(),
process: {
runner: runnerFor(true),
async ingress(input?: { port?: number; path?: string }) {
@@ -131,6 +131,6 @@ export async function computerExecutionTarget(db: Db, lease: EnvironmentLease, i
if (port !== binding.listenerPort) throw new Error("computer_runner_port_mismatch");
return ingress(path);
},
computerTool: { command: "/opt/ascii/cua-driver/cua-driver", args: ["mcp", "--socket", "/run/ascii-cua/driver.sock"] },
computerTool: binding.computerTool,
};
}