feat(gateway): add node.invoke for iOS canvas
This commit is contained in:
@@ -36,6 +36,8 @@ import {
|
||||
GatewayFrameSchema,
|
||||
type HelloOk,
|
||||
HelloOkSchema,
|
||||
type NodeInvokeParams,
|
||||
NodeInvokeParamsSchema,
|
||||
type NodePairApproveParams,
|
||||
NodePairApproveParamsSchema,
|
||||
type NodePairListParams,
|
||||
@@ -103,6 +105,9 @@ export const validateNodePairRejectParams = ajv.compile<NodePairRejectParams>(
|
||||
export const validateNodePairVerifyParams = ajv.compile<NodePairVerifyParams>(
|
||||
NodePairVerifyParamsSchema,
|
||||
);
|
||||
export const validateNodeInvokeParams = ajv.compile<NodeInvokeParams>(
|
||||
NodeInvokeParamsSchema,
|
||||
);
|
||||
export const validateSessionsListParams = ajv.compile<SessionsListParams>(
|
||||
SessionsListParamsSchema,
|
||||
);
|
||||
@@ -158,6 +163,7 @@ export {
|
||||
NodePairApproveParamsSchema,
|
||||
NodePairRejectParamsSchema,
|
||||
NodePairVerifyParamsSchema,
|
||||
NodeInvokeParamsSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
CronJobSchema,
|
||||
@@ -199,6 +205,7 @@ export type {
|
||||
NodePairApproveParams,
|
||||
NodePairRejectParams,
|
||||
NodePairVerifyParams,
|
||||
NodeInvokeParams,
|
||||
SessionsListParams,
|
||||
SessionsPatchParams,
|
||||
CronJob,
|
||||
|
||||
@@ -242,6 +242,16 @@ export const NodePairVerifyParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const NodeInvokeParamsSchema = Type.Object(
|
||||
{
|
||||
nodeId: NonEmptyString,
|
||||
command: NonEmptyString,
|
||||
params: Type.Optional(Type.Unknown()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SessionsListParamsSchema = Type.Object(
|
||||
{
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
@@ -496,6 +506,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
NodePairApproveParams: NodePairApproveParamsSchema,
|
||||
NodePairRejectParams: NodePairRejectParamsSchema,
|
||||
NodePairVerifyParams: NodePairVerifyParamsSchema,
|
||||
NodeInvokeParams: NodeInvokeParamsSchema,
|
||||
SessionsListParams: SessionsListParamsSchema,
|
||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||
CronJob: CronJobSchema,
|
||||
@@ -533,6 +544,7 @@ export type NodePairListParams = Static<typeof NodePairListParamsSchema>;
|
||||
export type NodePairApproveParams = Static<typeof NodePairApproveParamsSchema>;
|
||||
export type NodePairRejectParams = Static<typeof NodePairRejectParamsSchema>;
|
||||
export type NodePairVerifyParams = Static<typeof NodePairVerifyParamsSchema>;
|
||||
export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;
|
||||
export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
||||
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
||||
export type CronJob = Static<typeof CronJobSchema>;
|
||||
|
||||
@@ -27,10 +27,24 @@ type BridgeStartOpts = {
|
||||
};
|
||||
|
||||
const bridgeStartCalls = vi.hoisted(() => [] as BridgeStartOpts[]);
|
||||
const bridgeInvoke = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
type: "invoke-res",
|
||||
id: "1",
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({ ok: true }),
|
||||
error: null,
|
||||
})),
|
||||
);
|
||||
vi.mock("../infra/bridge/server.js", () => ({
|
||||
startNodeBridgeServer: vi.fn(async (opts: BridgeStartOpts) => {
|
||||
bridgeStartCalls.push(opts);
|
||||
return { port: 0, close: async () => {} };
|
||||
return {
|
||||
port: 18790,
|
||||
close: async () => {},
|
||||
listConnected: () => [],
|
||||
invoke: bridgeInvoke,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -362,6 +376,54 @@ describe("gateway server", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("routes node.invoke to the node bridge", async () => {
|
||||
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-home-"));
|
||||
const prevHome = process.env.HOME;
|
||||
process.env.HOME = homeDir;
|
||||
|
||||
try {
|
||||
bridgeInvoke.mockResolvedValueOnce({
|
||||
type: "invoke-res",
|
||||
id: "inv-1",
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({ result: "4" }),
|
||||
error: null,
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
try {
|
||||
await connectOk(ws);
|
||||
|
||||
const res = await rpcReq(ws, "node.invoke", {
|
||||
nodeId: "ios-node",
|
||||
command: "screen.eval",
|
||||
params: { javaScript: "2+2" },
|
||||
timeoutMs: 123,
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
expect(bridgeInvoke).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nodeId: "ios-node",
|
||||
command: "screen.eval",
|
||||
paramsJSON: JSON.stringify({ javaScript: "2+2" }),
|
||||
timeoutMs: 123,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
ws.close();
|
||||
await server.close();
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(homeDir, { recursive: true, force: true });
|
||||
if (prevHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = prevHome;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("emits presence updates for bridge connect/disconnect", async () => {
|
||||
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-home-"));
|
||||
const prevHome = process.env.HOME;
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
validateCronRunsParams,
|
||||
validateCronStatusParams,
|
||||
validateCronUpdateParams,
|
||||
validateNodeInvokeParams,
|
||||
validateNodePairApproveParams,
|
||||
validateNodePairListParams,
|
||||
validateNodePairRejectParams,
|
||||
@@ -176,6 +177,7 @@ const METHODS = [
|
||||
"node.pair.approve",
|
||||
"node.pair.reject",
|
||||
"node.pair.verify",
|
||||
"node.invoke",
|
||||
"cron.list",
|
||||
"cron.status",
|
||||
"cron.add",
|
||||
@@ -2046,6 +2048,100 @@ export async function startGatewayServer(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "node.invoke": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateNodeInvokeParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid node.invoke params: ${formatValidationErrors(validateNodeInvokeParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (!bridge) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, "bridge not running"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const p = params as {
|
||||
nodeId: string;
|
||||
command: string;
|
||||
params?: unknown;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
const nodeId = String(p.nodeId ?? "").trim();
|
||||
const command = String(p.command ?? "").trim();
|
||||
if (!nodeId || !command) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"nodeId and command required",
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const paramsJSON =
|
||||
"params" in p && p.params !== undefined
|
||||
? JSON.stringify(p.params)
|
||||
: null;
|
||||
const res = await bridge.invoke({
|
||||
nodeId,
|
||||
command,
|
||||
paramsJSON,
|
||||
timeoutMs: p.timeoutMs,
|
||||
});
|
||||
if (!res.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.UNAVAILABLE,
|
||||
res.error?.message ?? "node invoke failed",
|
||||
{ details: { nodeError: res.error ?? null } },
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const payload =
|
||||
typeof res.payloadJSON === "string" && res.payloadJSON.trim()
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(res.payloadJSON) as unknown;
|
||||
} catch {
|
||||
return { payloadJSON: res.payloadJSON };
|
||||
}
|
||||
})()
|
||||
: undefined;
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
nodeId,
|
||||
command,
|
||||
payload,
|
||||
payloadJSON: res.payloadJSON ?? null,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "send": {
|
||||
const p = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSendParams(p)) {
|
||||
|
||||
Reference in New Issue
Block a user