mirror of https://github.com/openclaw/openclaw.git
Nodes: recheck queued actions before delivery
This commit is contained in:
parent
d1e4ee03ff
commit
c133fc6a49
|
|
@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
|
||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. Thanks @vincentkoc.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
|
|
|||
|
|
@ -518,6 +518,63 @@ describe("node.invoke APNs wake path", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("drops queued actions that are no longer allowed at pull time", async () => {
|
||||
mocks.loadApnsRegistration.mockResolvedValue(null);
|
||||
mocks.resolveNodeCommandAllowlist.mockReturnValue(new Set(["canvas.navigate"]));
|
||||
mocks.isNodeCommandAllowed.mockImplementation(
|
||||
({
|
||||
command,
|
||||
declaredCommands,
|
||||
allowlist,
|
||||
}: {
|
||||
command: string;
|
||||
declaredCommands: string[];
|
||||
allowlist: Set<string>;
|
||||
}) => {
|
||||
if (!allowlist.has(command)) {
|
||||
return { ok: false, reason: "command not allowlisted" };
|
||||
}
|
||||
if (!declaredCommands.includes(command)) {
|
||||
return { ok: false, reason: "command not declared by node" };
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
);
|
||||
|
||||
const nodeRegistry = {
|
||||
get: vi.fn(() => ({
|
||||
nodeId: "ios-node-policy",
|
||||
commands: ["camera.snap", "canvas.navigate"],
|
||||
platform: "iOS 26.4.0",
|
||||
})),
|
||||
invoke: vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "NODE_BACKGROUND_UNAVAILABLE",
|
||||
message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
await invokeNode({
|
||||
nodeRegistry,
|
||||
requestParams: {
|
||||
nodeId: "ios-node-policy",
|
||||
command: "camera.snap",
|
||||
params: { facing: "front" },
|
||||
idempotencyKey: "idem-policy",
|
||||
},
|
||||
});
|
||||
|
||||
const pullRespond = await pullPending("ios-node-policy");
|
||||
const pullCall = pullRespond.mock.calls[0] as RespondCall | undefined;
|
||||
expect(pullCall?.[0]).toBe(true);
|
||||
expect(pullCall?.[1]).toMatchObject({
|
||||
nodeId: "ios-node-policy",
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("dedupes queued foreground actions by idempotency key", async () => {
|
||||
mocks.loadApnsRegistration.mockResolvedValue(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js";
|
||||
import { sanitizeNodeInvokeParamsForForwarding } from "../node-invoke-sanitize.js";
|
||||
import {
|
||||
type ConnectParams,
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
validateNodeDescribeParams,
|
||||
|
|
@ -218,6 +219,38 @@ function listPendingNodeActions(nodeId: string): PendingNodeAction[] {
|
|||
return prunePendingNodeActions(nodeId, Date.now());
|
||||
}
|
||||
|
||||
function resolveAllowedPendingNodeActions(params: {
|
||||
nodeId: string;
|
||||
client: { connect?: ConnectParams | null } | null;
|
||||
}): PendingNodeAction[] {
|
||||
const pending = listPendingNodeActions(params.nodeId);
|
||||
if (pending.length === 0) {
|
||||
return pending;
|
||||
}
|
||||
const connect = params.client?.connect;
|
||||
const declaredCommands = Array.isArray(connect?.commands) ? connect.commands : [];
|
||||
const allowlist = resolveNodeCommandAllowlist(loadConfig(), {
|
||||
platform: connect?.client?.platform,
|
||||
deviceFamily: connect?.client?.deviceFamily,
|
||||
});
|
||||
const allowed = pending.filter((entry) => {
|
||||
const result = isNodeCommandAllowed({
|
||||
command: entry.command,
|
||||
declaredCommands,
|
||||
allowlist,
|
||||
});
|
||||
return result.ok;
|
||||
});
|
||||
if (allowed.length !== pending.length) {
|
||||
if (allowed.length === 0) {
|
||||
pendingNodeActionsById.delete(params.nodeId);
|
||||
} else {
|
||||
pendingNodeActionsById.set(params.nodeId, allowed);
|
||||
}
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
function ackPendingNodeActions(nodeId: string, ids: string[]): PendingNodeAction[] {
|
||||
if (ids.length === 0) {
|
||||
return listPendingNodeActions(nodeId);
|
||||
|
|
@ -805,7 +838,7 @@ export const nodeHandlers: GatewayRequestHandlers = {
|
|||
return;
|
||||
}
|
||||
|
||||
const pending = listPendingNodeActions(trimmedNodeId);
|
||||
const pending = resolveAllowedPendingNodeActions({ nodeId: trimmedNodeId, client });
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue