mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 07:01:40 +03:00
refactor(cli): unify on clawdis CLI + node permissions
This commit is contained in:
@@ -228,6 +228,7 @@ describe("node bridge server", () => {
|
||||
deviceFamily?: string;
|
||||
modelIdentifier?: string;
|
||||
remoteIp?: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
} | null = null;
|
||||
|
||||
let disconnected: {
|
||||
@@ -238,6 +239,7 @@ describe("node bridge server", () => {
|
||||
deviceFamily?: string;
|
||||
modelIdentifier?: string;
|
||||
remoteIp?: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
} | null = null;
|
||||
|
||||
let resolveDisconnected: (() => void) | null = null;
|
||||
@@ -268,6 +270,7 @@ describe("node bridge server", () => {
|
||||
version: "1.0",
|
||||
deviceFamily: "iPad",
|
||||
modelIdentifier: "iPad16,6",
|
||||
permissions: { screenRecording: true, notifications: false },
|
||||
});
|
||||
|
||||
// Approve the pending request from the gateway side.
|
||||
@@ -304,6 +307,7 @@ describe("node bridge server", () => {
|
||||
version: "2.0",
|
||||
deviceFamily: "iPad",
|
||||
modelIdentifier: "iPad99,1",
|
||||
permissions: { screenRecording: false },
|
||||
});
|
||||
const line3 = JSON.parse(await readLine2()) as { type: string };
|
||||
expect(line3.type).toBe("hello-ok");
|
||||
@@ -320,6 +324,10 @@ describe("node bridge server", () => {
|
||||
expect(lastAuthed?.version).toBe("1.0");
|
||||
expect(lastAuthed?.deviceFamily).toBe("iPad");
|
||||
expect(lastAuthed?.modelIdentifier).toBe("iPad16,6");
|
||||
expect(lastAuthed?.permissions).toEqual({
|
||||
screenRecording: false,
|
||||
notifications: false,
|
||||
});
|
||||
expect(lastAuthed?.remoteIp?.includes("127.0.0.1")).toBe(true);
|
||||
|
||||
socket2.destroy();
|
||||
@@ -432,6 +440,7 @@ describe("node bridge server", () => {
|
||||
modelIdentifier: "iPad14,5",
|
||||
caps: ["canvas", "camera"],
|
||||
commands: ["canvas.eval", "canvas.snapshot", "camera.snap"],
|
||||
permissions: { accessibility: true },
|
||||
});
|
||||
|
||||
// Approve the pending request from the gateway side.
|
||||
@@ -464,6 +473,7 @@ describe("node bridge server", () => {
|
||||
"canvas.snapshot",
|
||||
"camera.snap",
|
||||
]);
|
||||
expect(node?.permissions).toEqual({ accessibility: true });
|
||||
|
||||
const after = await listNodePairing(baseDir);
|
||||
const paired = after.paired.find((p) => p.nodeId === "n-caps");
|
||||
@@ -473,6 +483,7 @@ describe("node bridge server", () => {
|
||||
"canvas.snapshot",
|
||||
"camera.snap",
|
||||
]);
|
||||
expect(paired?.permissions).toEqual({ accessibility: true });
|
||||
|
||||
socket.destroy();
|
||||
await server.close();
|
||||
|
||||
@@ -22,6 +22,7 @@ type BridgeHelloFrame = {
|
||||
modelIdentifier?: string;
|
||||
caps?: string[];
|
||||
commands?: string[];
|
||||
permissions?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
type BridgePairRequestFrame = {
|
||||
@@ -34,6 +35,7 @@ type BridgePairRequestFrame = {
|
||||
modelIdentifier?: string;
|
||||
caps?: string[];
|
||||
commands?: string[];
|
||||
permissions?: Record<string, boolean>;
|
||||
remoteAddress?: string;
|
||||
silent?: boolean;
|
||||
};
|
||||
@@ -123,6 +125,7 @@ export type NodeBridgeClientInfo = {
|
||||
remoteIp?: string;
|
||||
caps?: string[];
|
||||
commands?: string[];
|
||||
permissions?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
export type NodeBridgeServerOpts = {
|
||||
@@ -288,6 +291,18 @@ export async function startNodeBridgeServer(
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const normalizePermissions = (
|
||||
raw: unknown,
|
||||
): Record<string, boolean> | undefined => {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
||||
return undefined;
|
||||
const entries = Object.entries(raw as Record<string, unknown>)
|
||||
.map(([key, value]) => [String(key).trim(), value === true] as const)
|
||||
.filter(([key]) => key.length > 0);
|
||||
if (entries.length === 0) return undefined;
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
const caps =
|
||||
(Array.isArray(hello.caps)
|
||||
? hello.caps.map((c) => String(c)).filter(Boolean)
|
||||
@@ -299,6 +314,10 @@ export async function startNodeBridgeServer(
|
||||
Array.isArray(hello.commands) && hello.commands.length > 0
|
||||
? hello.commands.map((c) => String(c)).filter(Boolean)
|
||||
: verified.node.commands;
|
||||
const helloPermissions = normalizePermissions(hello.permissions);
|
||||
const permissions = helloPermissions
|
||||
? { ...(verified.node.permissions ?? {}), ...helloPermissions }
|
||||
: verified.node.permissions;
|
||||
|
||||
isAuthenticated = true;
|
||||
const existing = connections.get(nodeId);
|
||||
@@ -318,6 +337,7 @@ export async function startNodeBridgeServer(
|
||||
modelIdentifier: verified.node.modelIdentifier ?? hello.modelIdentifier,
|
||||
caps,
|
||||
commands,
|
||||
permissions,
|
||||
remoteIp: remoteAddress,
|
||||
};
|
||||
await updatePairedNodeMetadata(
|
||||
@@ -331,6 +351,7 @@ export async function startNodeBridgeServer(
|
||||
remoteIp: nodeInfo.remoteIp,
|
||||
caps: nodeInfo.caps,
|
||||
commands: nodeInfo.commands,
|
||||
permissions: nodeInfo.permissions,
|
||||
},
|
||||
opts.pairingBaseDir,
|
||||
);
|
||||
@@ -396,6 +417,10 @@ export async function startNodeBridgeServer(
|
||||
commands: Array.isArray(req.commands)
|
||||
? req.commands.map((c) => String(c)).filter(Boolean)
|
||||
: undefined,
|
||||
permissions:
|
||||
req.permissions && typeof req.permissions === "object"
|
||||
? (req.permissions as Record<string, boolean>)
|
||||
: undefined,
|
||||
remoteIp: remoteAddress,
|
||||
silent: req.silent === true ? true : undefined,
|
||||
},
|
||||
@@ -433,6 +458,10 @@ export async function startNodeBridgeServer(
|
||||
commands: Array.isArray(req.commands)
|
||||
? req.commands.map((c) => String(c)).filter(Boolean)
|
||||
: undefined,
|
||||
permissions:
|
||||
req.permissions && typeof req.permissions === "object"
|
||||
? (req.permissions as Record<string, boolean>)
|
||||
: undefined,
|
||||
remoteIp: remoteAddress,
|
||||
};
|
||||
connections.set(nodeId, { socket, nodeInfo, invokeWaiters });
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import fsp from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
const runExecCalls = vi.hoisted(
|
||||
() => [] as Array<{ cmd: string; args: string[] }>,
|
||||
);
|
||||
const runCommandCalls = vi.hoisted(
|
||||
() => [] as Array<{ argv: string[]; timeoutMs: number }>,
|
||||
);
|
||||
|
||||
let runExecThrows = false;
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runExec: vi.fn(async (cmd: string, args: string[]) => {
|
||||
runExecCalls.push({ cmd, args });
|
||||
if (runExecThrows) throw new Error("which failed");
|
||||
return { stdout: "/usr/local/bin/clawdis-mac\n", stderr: "" };
|
||||
}),
|
||||
runCommandWithTimeout: vi.fn(async (argv: string[], timeoutMs: number) => {
|
||||
runCommandCalls.push({ argv, timeoutMs });
|
||||
return { stdout: "ok", stderr: "", code: 0 };
|
||||
}),
|
||||
}));
|
||||
|
||||
import { resolveClawdisMacBinary, runClawdisMac } from "./clawdis-mac.js";
|
||||
|
||||
describe("clawdis-mac binary resolver", () => {
|
||||
it("uses env override on macOS and errors elsewhere", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
};
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
vi.stubEnv("CLAWDIS_MAC_BIN", "/opt/bin/clawdis-mac");
|
||||
await expect(resolveClawdisMacBinary(runtime)).resolves.toBe(
|
||||
"/opt/bin/clawdis-mac",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(resolveClawdisMacBinary(runtime)).rejects.toThrow(/exit 1/);
|
||||
});
|
||||
|
||||
it("runs the helper with --json when requested", async () => {
|
||||
if (process.platform !== "darwin") return;
|
||||
vi.stubEnv("CLAWDIS_MAC_BIN", "/opt/bin/clawdis-mac");
|
||||
|
||||
const res = await runClawdisMac(["browser", "status"], {
|
||||
json: true,
|
||||
timeoutMs: 1234,
|
||||
});
|
||||
|
||||
expect(res).toMatchObject({ stdout: "ok", code: 0 });
|
||||
expect(runCommandCalls.length).toBeGreaterThan(0);
|
||||
expect(runCommandCalls.at(-1)?.argv).toEqual([
|
||||
"/opt/bin/clawdis-mac",
|
||||
"--json",
|
||||
"browser",
|
||||
"status",
|
||||
]);
|
||||
expect(runCommandCalls.at(-1)?.timeoutMs).toBe(1234);
|
||||
});
|
||||
|
||||
it("falls back to `which clawdis-mac` when no override is set", async () => {
|
||||
if (process.platform !== "darwin") return;
|
||||
vi.stubEnv("CLAWDIS_MAC_BIN", "");
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = await resolveClawdisMacBinary(runtime);
|
||||
expect(resolved).toBe("/usr/local/bin/clawdis-mac");
|
||||
expect(runExecCalls.some((c) => c.cmd === "which")).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to ./bin/clawdis-mac when which fails", async () => {
|
||||
if (process.platform !== "darwin") return;
|
||||
|
||||
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdis-mac-test-"));
|
||||
const oldCwd = process.cwd();
|
||||
try {
|
||||
const binDir = path.join(tmp, "bin");
|
||||
await fsp.mkdir(binDir, { recursive: true });
|
||||
const exePath = path.join(binDir, "clawdis-mac");
|
||||
await fsp.writeFile(exePath, "#!/bin/sh\necho ok\n", "utf-8");
|
||||
await fsp.chmod(exePath, 0o755);
|
||||
|
||||
process.chdir(tmp);
|
||||
vi.stubEnv("CLAWDIS_MAC_BIN", "");
|
||||
runExecThrows = true;
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number) => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = await resolveClawdisMacBinary(runtime);
|
||||
const expectedReal = await fsp.realpath(exePath);
|
||||
const resolvedReal = await fsp.realpath(resolved);
|
||||
expect(resolvedReal).toBe(expectedReal);
|
||||
} finally {
|
||||
runExecThrows = false;
|
||||
process.chdir(oldCwd);
|
||||
await fsp.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { runCommandWithTimeout, runExec } from "../process/exec.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
|
||||
export type ClawdisMacExecResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
};
|
||||
|
||||
function isFileExecutable(p: string): boolean {
|
||||
try {
|
||||
const stat = fs.statSync(p);
|
||||
if (!stat.isFile()) return false;
|
||||
fs.accessSync(p, fs.constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveClawdisMacBinary(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
): Promise<string> {
|
||||
if (process.platform !== "darwin") {
|
||||
runtime.error("clawdis-mac is only available on macOS.");
|
||||
runtime.exit(1);
|
||||
}
|
||||
|
||||
const override = process.env.CLAWDIS_MAC_BIN?.trim();
|
||||
if (override) return override;
|
||||
|
||||
try {
|
||||
const { stdout } = await runExec("which", ["clawdis-mac"], 2000);
|
||||
const resolved = stdout.trim();
|
||||
if (resolved) return resolved;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
const local = path.resolve(process.cwd(), "bin", "clawdis-mac");
|
||||
if (isFileExecutable(local)) return local;
|
||||
|
||||
runtime.error(
|
||||
"Missing required binary: clawdis-mac. Install the Clawdis mac app/CLI helper (or set CLAWDIS_MAC_BIN).",
|
||||
);
|
||||
runtime.exit(1);
|
||||
}
|
||||
|
||||
export async function runClawdisMac(
|
||||
args: string[],
|
||||
opts?: { json?: boolean; timeoutMs?: number; runtime?: RuntimeEnv },
|
||||
): Promise<ClawdisMacExecResult> {
|
||||
const runtime = opts?.runtime ?? defaultRuntime;
|
||||
const cmd = await resolveClawdisMacBinary(runtime);
|
||||
|
||||
const argv: string[] = [cmd];
|
||||
if (opts?.json) argv.push("--json");
|
||||
argv.push(...args);
|
||||
|
||||
const res = await runCommandWithTimeout(argv, opts?.timeoutMs ?? 30_000);
|
||||
return { stdout: res.stdout, stderr: res.stderr, code: res.code };
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export type NodePairingPendingRequest = {
|
||||
modelIdentifier?: string;
|
||||
caps?: string[];
|
||||
commands?: string[];
|
||||
permissions?: Record<string, boolean>;
|
||||
remoteIp?: string;
|
||||
silent?: boolean;
|
||||
isRepair?: boolean;
|
||||
@@ -29,6 +30,7 @@ export type NodePairingPairedNode = {
|
||||
modelIdentifier?: string;
|
||||
caps?: string[];
|
||||
commands?: string[];
|
||||
permissions?: Record<string, boolean>;
|
||||
remoteIp?: string;
|
||||
createdAtMs: number;
|
||||
approvedAtMs: number;
|
||||
@@ -185,6 +187,7 @@ export async function requestNodePairing(
|
||||
modelIdentifier: req.modelIdentifier,
|
||||
caps: req.caps,
|
||||
commands: req.commands,
|
||||
permissions: req.permissions,
|
||||
remoteIp: req.remoteIp,
|
||||
silent: req.silent,
|
||||
isRepair,
|
||||
@@ -217,6 +220,7 @@ export async function approveNodePairing(
|
||||
modelIdentifier: pending.modelIdentifier,
|
||||
caps: pending.caps,
|
||||
commands: pending.commands,
|
||||
permissions: pending.permissions,
|
||||
remoteIp: pending.remoteIp,
|
||||
createdAtMs: existing?.createdAtMs ?? now,
|
||||
approvedAtMs: now,
|
||||
@@ -281,6 +285,7 @@ export async function updatePairedNodeMetadata(
|
||||
remoteIp: patch.remoteIp ?? existing.remoteIp,
|
||||
caps: patch.caps ?? existing.caps,
|
||||
commands: patch.commands ?? existing.commands,
|
||||
permissions: patch.permissions ?? existing.permissions,
|
||||
};
|
||||
|
||||
state.pairedByNodeId[normalized] = next;
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("system-presence", () => {
|
||||
const instanceIdLower = instanceIdUpper.toLowerCase();
|
||||
|
||||
upsertPresence(instanceIdUpper, {
|
||||
host: "clawdis-mac",
|
||||
host: "clawdis",
|
||||
mode: "app",
|
||||
instanceId: instanceIdUpper,
|
||||
reason: "connect",
|
||||
|
||||
Reference in New Issue
Block a user