// zsh's tools โ€” the module's own, and its implementation of the login-shell seat's one verb // (novox/hq ADR 0176). Served by the node tools runtime (ADR 0175); nothing here runs a process. // // `execute` runs as the operator account. The runtime runs as the node's account โ€” root when the // host started it โ€” so the command is handed to the account through `runuser` when we are not // already that account. Root is the module's concern (ADR 0175 ยง4): a command that needs it uses // sudo inside the shell like a person would. import { spawn } from "node:child_process"; import { readFile } from "node:fs/promises"; import { homedir, userInfo } from "node:os"; import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; /** The operator account on this machine, as the mesh told the runtime; the current user otherwise. */ function account(env: NodeJS.ProcessEnv): string { return env.MESH_OPERATOR_ACCOUNT?.trim() || userInfo().username; } interface Executed { command: string; account: string; status: number | null; signal: string | null; stdout: string; stderr: string; timed_out: boolean; } /** Run one command line in a zsh login shell as the account, capturing everything. */ export async function execute(command: string, who: string, timeoutSeconds: number): Promise { const self = userInfo().username; const argv = who === self ? ["zsh", "-lc", command] : ["runuser", "-u", who, "--", "zsh", "-lc", command]; return new Promise((resolve) => { const child = spawn(argv[0], argv.slice(1), { stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; let timedOut = false; child.stdout.on("data", (d: Buffer) => { stdout += d.toString(); }); child.stderr.on("data", (d: Buffer) => { stderr += d.toString(); }); const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, timeoutSeconds * 1000); child.on("error", (err) => { clearTimeout(timer); resolve({ command, account: who, status: null, signal: null, stdout, stderr: stderr + err.message, timed_out: false }); }); child.on("close", (status, signal) => { clearTimeout(timer); resolve({ command, account: who, status, signal, stdout, stderr, timed_out: timedOut }); }); }); } function seatVerbs(env: NodeJS.ProcessEnv): ToolDefinition[] { return [ { name: "execute", description: "Run one command on this machine as the operator account, in a login shell; answers with what it printed and how it exited.", input: { type: "object", properties: { command: { type: "string", description: "the command line, as you would type it" }, timeout_seconds: { type: "number", description: "give up after this long (default 60)" }, }, required: ["command"], }, run: async (args) => { const command = String(args.command ?? "").trim(); if (!command) throw new Error("execute: a command is required"); const timeout = Number(args.timeout_seconds ?? 60); return execute(command, account(env), Number.isFinite(timeout) && timeout > 0 ? timeout : 60); }, }, ]; } function ownTools(env: NodeJS.ProcessEnv): ToolDefinition[] { return [ { name: "zsh_config", description: "The operator account's ~/.zshrc on this machine as it is now: the mesh's block and the lines around it.", input: { type: "object", properties: {} }, run: async () => { const who = account(env); const home = env.MESH_OPERATOR_HOME?.trim() || (who === userInfo().username ? homedir() : `/home/${who}`); const path = `${home}/.zshrc`; const text = await readFile(path, "utf8").catch(() => ""); const inBlock = /# BEGIN mesh [^\n]*\n([\s\S]*?)# END mesh/.exec(text); return { account: who, path, lines: text.split("\n").length, mesh_block_lines: inBlock ? inBlock[1].split("\n").length - 1 : 0, content: text }; }, }, ]; } // The seat's verb is registered under the seat's name (what the runtime serves on the seat's // subject when this module holds it) and the module's own tools under the module's. registerModuleTools("login-shell", seatVerbs); registerModuleTools("zsh", ownTools);