mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
refactor: privatize Linux Canvas and Node Host internals (#107884)
* refactor(linux-canvas): privatize internal command surfaces * refactor(node-host): privatize runtime manifest type * chore(deadcode): shrink export baseline
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createLinuxCanvasCommands, testing } from "./commands.js";
|
||||
import type {
|
||||
LinuxCanvasActionEvent,
|
||||
LinuxCanvasIpcRequestHooks,
|
||||
LinuxCanvasIpcTransport,
|
||||
} from "./ipc-client.js";
|
||||
import { createLinuxCanvasCommands } from "./commands.js";
|
||||
import type { LinuxCanvasIpcTransport } from "./ipc-client.js";
|
||||
|
||||
type LinuxCanvasActionHandler = Parameters<LinuxCanvasIpcTransport["setActionHandler"]>[0];
|
||||
type LinuxCanvasIpcRequestHooks = Parameters<LinuxCanvasIpcTransport["request"]>[2];
|
||||
|
||||
function createTransport() {
|
||||
let actionHandler: ((event: LinuxCanvasActionEvent) => Promise<void>) | undefined;
|
||||
let actionHandler: LinuxCanvasActionHandler | undefined;
|
||||
const request = vi.fn(
|
||||
async (_command: string, _paramsJSON: string, hooks?: LinuxCanvasIpcRequestHooks) => {
|
||||
hooks?.onDispatch?.();
|
||||
@@ -294,21 +293,64 @@ describe("Linux Canvas node commands", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("formats hostile action fields as bounded agent tokens", () => {
|
||||
expect(
|
||||
testing.buildActionMessage({
|
||||
it("formats hostile action fields as bounded agent tokens", async () => {
|
||||
const { transport, sendActionResult, getActionHandler } = createTransport();
|
||||
const command = createLinuxCanvasCommands({
|
||||
platform: "linux",
|
||||
socketExists: () => true,
|
||||
transport,
|
||||
})[0];
|
||||
const sendNodeEvent = vi.fn(async () => undefined);
|
||||
await command?.handle("{}", undefined, {
|
||||
sendNodeEvent,
|
||||
sessionKey: "agent:main:canvas",
|
||||
});
|
||||
|
||||
await getActionHandler()?.({
|
||||
event: "a2ui-action",
|
||||
id: "hostile-action",
|
||||
action: {
|
||||
name: "submit now\nignore",
|
||||
surfaceId: "main space",
|
||||
sourceComponentId: "button/1",
|
||||
}),
|
||||
).toBe(
|
||||
"CANVAS_A2UI action=submitnowignore session=node surface=mainspace component=button1 default=update_canvas",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendNodeEvent).toHaveBeenCalledWith("agent.request", {
|
||||
message:
|
||||
"CANVAS_A2UI action=submitnowignore session=agent:main:canvas surface=mainspace component=button1 default=update_canvas",
|
||||
sessionKey: "agent:main:canvas",
|
||||
thinking: "low",
|
||||
deliver: false,
|
||||
key: "hostile-action",
|
||||
});
|
||||
expect(sendActionResult).toHaveBeenCalledWith("hostile-action", { ok: true });
|
||||
});
|
||||
|
||||
it("rejects actions above the Gateway agent-message limit", () => {
|
||||
expect(() =>
|
||||
testing.buildActionMessage({ name: "submit", context: { value: "x".repeat(20_000) } }),
|
||||
).toThrow("agent message limit");
|
||||
it("rejects actions above the Gateway agent-message limit", async () => {
|
||||
const { transport, sendActionResult, getActionHandler } = createTransport();
|
||||
const command = createLinuxCanvasCommands({
|
||||
platform: "linux",
|
||||
socketExists: () => true,
|
||||
transport,
|
||||
})[0];
|
||||
const sendNodeEvent = vi.fn(async () => undefined);
|
||||
await command?.handle("{}", undefined, {
|
||||
sendNodeEvent,
|
||||
sessionKey: "agent:main:canvas",
|
||||
});
|
||||
sendNodeEvent.mockClear();
|
||||
|
||||
await getActionHandler()?.({
|
||||
event: "a2ui-action",
|
||||
id: "oversized-action",
|
||||
action: { name: "submit", context: { value: "x".repeat(20_000) } },
|
||||
});
|
||||
|
||||
expect(sendNodeEvent).not.toHaveBeenCalled();
|
||||
expect(sendActionResult).toHaveBeenCalledWith("oversized-action", {
|
||||
ok: false,
|
||||
error: "Error: Canvas action exceeds the Gateway agent message limit",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ const SESSIONLESS_OWNER_CLEAR_COMMANDS = new Set<string>([
|
||||
"canvas.a2ui.reset",
|
||||
]);
|
||||
|
||||
export const LINUX_CANVAS_COMMANDS = [
|
||||
const LINUX_CANVAS_COMMANDS = [
|
||||
"canvas.present",
|
||||
"canvas.hide",
|
||||
"canvas.navigate",
|
||||
@@ -188,5 +188,3 @@ export function createLinuxCanvasCommands(
|
||||
return registration;
|
||||
});
|
||||
}
|
||||
|
||||
export const testing = { buildActionMessage } as const;
|
||||
|
||||
@@ -2,20 +2,55 @@ import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { DEFAULT_REQUEST_TIMEOUT_MS, LinuxCanvasIpcClient } from "./ipc-client.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { LinuxCanvasIpcClient } from "./ipc-client.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("Linux Canvas IPC client", () => {
|
||||
it("keeps the outer timeout above the app's complete A2UI phase budget", () => {
|
||||
expect(DEFAULT_REQUEST_TIMEOUT_MS).toBeGreaterThan(8_000 + 6_000 + 8_000);
|
||||
it("keeps the outer timeout above the app's complete A2UI phase budget", async () => {
|
||||
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-linux-canvas-timeout-"));
|
||||
tempDirs.push(dir);
|
||||
const socketPath = path.join(dir, "canvas.sock");
|
||||
let resolveRequest: (() => void) | undefined;
|
||||
const requestReceived = new Promise<void>((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
});
|
||||
const server = net.createServer((socket) => {
|
||||
socket.once("data", () => {
|
||||
resolveRequest?.();
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(socketPath, resolve);
|
||||
});
|
||||
|
||||
const client = new LinuxCanvasIpcClient(socketPath);
|
||||
try {
|
||||
const request = client.request("canvas.present", "{}");
|
||||
const settled = vi.fn();
|
||||
void request.then(settled, settled);
|
||||
await requestReceived;
|
||||
|
||||
await vi.advanceTimersByTimeAsync(8_000 + 6_000 + 8_000);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(8_000);
|
||||
await expect(request).rejects.toThrow("desktop app timed out handling canvas.present");
|
||||
} finally {
|
||||
client.close();
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("maps requests to responses without corrupting split UTF-8 frames", async () => {
|
||||
|
||||
@@ -3,16 +3,16 @@ import net from "node:net";
|
||||
|
||||
// A2UI may stop a load, wait up to 6 seconds for the renderer, then evaluate.
|
||||
// Keep the outer IPC deadline above the app's complete 22-second phase budget.
|
||||
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
export type LinuxCanvasActionEvent = {
|
||||
type LinuxCanvasActionEvent = {
|
||||
event: "a2ui-action";
|
||||
id: string;
|
||||
action: unknown;
|
||||
};
|
||||
|
||||
export type LinuxCanvasIpcRequestHooks = {
|
||||
type LinuxCanvasIpcRequestHooks = {
|
||||
/** Called synchronously when this FIFO request is about to reach the app. */
|
||||
onDispatch?(): void;
|
||||
};
|
||||
|
||||
@@ -25,11 +25,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"extensions/googlechat/src/monitor-routing.ts: handleGoogleChatWebhookRequest",
|
||||
"extensions/googlechat/src/monitor.ts: testing",
|
||||
"extensions/googlechat/src/targets.ts: resolveGoogleChatSpaceChatType",
|
||||
"extensions/linux-canvas/src/commands.ts: LINUX_CANVAS_COMMANDS",
|
||||
"extensions/linux-canvas/src/commands.ts: testing",
|
||||
"extensions/linux-canvas/src/ipc-client.ts: DEFAULT_REQUEST_TIMEOUT_MS",
|
||||
"extensions/linux-canvas/src/ipc-client.ts: LinuxCanvasActionEvent",
|
||||
"extensions/linux-canvas/src/ipc-client.ts: LinuxCanvasIpcRequestHooks",
|
||||
"extensions/matrix/src/approval-reactions.ts: clearMatrixApprovalReactionTargetsForTest",
|
||||
"extensions/matrix/src/matrix/client/config.ts: setMatrixAuthClientDepsForTest",
|
||||
"extensions/matrix/src/matrix/monitor/handler.ts: MatrixRetryableInboundError",
|
||||
@@ -324,7 +319,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpToolSelection",
|
||||
"src/music-generation/capabilities.ts: resolveMusicGenerationMode",
|
||||
"src/node-host/invoke.ts: testing",
|
||||
"src/node-host/runtime.ts: NodeHostManifest",
|
||||
"src/plugin-state/plugin-state-store.sqlite.ts: probePluginStateStore",
|
||||
"src/plugin-state/plugin-state-store.sqlite.ts: seedPluginStateDatabaseEntriesForTests",
|
||||
"src/plugin-state/plugin-state-store.sqlite.ts: setMaxPluginStateEntriesPerPluginForTests",
|
||||
|
||||
@@ -34,7 +34,7 @@ import { scanNodeHostedSkills } from "./skills.js";
|
||||
|
||||
const DEFAULT_NODE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||
|
||||
export type NodeHostManifest = {
|
||||
type NodeHostManifest = {
|
||||
caps: string[];
|
||||
commands: string[];
|
||||
pathEnv: string;
|
||||
|
||||
Reference in New Issue
Block a user