refactor(cli): unify on clawdis CLI + node permissions

This commit is contained in:
Peter Steinberger
2025-12-20 02:08:04 +00:00
parent 479720c169
commit 849446ae17
49 changed files with 1205 additions and 2735 deletions

View File

@@ -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();

View File

@@ -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 });