mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-01 04:11:03 +00:00
fix(approvals): restore queue targeting and plugin id prefixes
This commit is contained in:
@@ -106,6 +106,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/splitting: replace proportional text estimate with verified HTML-length search so long messages split at word boundaries instead of mid-word; gracefully degrade when tag overhead exceeds the limit. (#56595)
|
||||
- Telegram/delivery: skip whitespace-only and hook-blanked text replies in bot delivery to prevent GrammyError 400 empty-text crashes. (#56620)
|
||||
- Telegram/send: validate `replyToMessageId` at all four API sinks with a shared normalizer that rejects non-numeric, NaN, and mixed-content strings. (#56587)
|
||||
- Approvals/UI: keep the newest pending approval at the front of the Control UI queue so approving one request does not accidentally target an older expired id. Thanks @vincentkoc.
|
||||
- Plugin approvals: accept unique short approval-id prefixes on `plugin.approval.resolve`, matching exec approvals and restoring `/approve` fallback flows on chat approval surfaces. Thanks @vincentkoc.
|
||||
- Mistral: normalize OpenAI-compatible request flags so official Mistral API runs no longer fail with remaining `422 status code (no body)` chat errors.
|
||||
- Control UI/config: keep sensitive raw config hidden by default, replace the blank blocked editor with an explicit reveal-to-edit state, and restore raw JSON editing without auto-exposing secrets. Fixes #55322.
|
||||
- CLI/zsh: defer `compdef` registration until `compinit` is available so zsh completion loads cleanly with plugin managers and manual setups. (#56555)
|
||||
|
||||
@@ -431,7 +431,7 @@ describe("createPluginApprovalHandlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("requires exact id and rejects prefixes", async () => {
|
||||
it("accepts unique short id prefixes", async () => {
|
||||
const handlers = createPluginApprovalHandlers(manager);
|
||||
const record = manager.create({ title: "T", description: "D" }, 60_000, "abcdef-1234");
|
||||
void manager.register(record, 60_000);
|
||||
@@ -441,15 +441,8 @@ describe("createPluginApprovalHandlers", () => {
|
||||
decision: "allow-always",
|
||||
});
|
||||
await handlers["plugin.approval.resolve"](opts);
|
||||
expect(opts.respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
code: "INVALID_REQUEST",
|
||||
message: expect.stringContaining("unknown or expired"),
|
||||
details: expect.objectContaining({ reason: "APPROVAL_NOT_FOUND" }),
|
||||
}),
|
||||
);
|
||||
expect(opts.respond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
expect(manager.getSnapshot(record.id)?.decision).toBe("allow-always");
|
||||
});
|
||||
|
||||
it("does not leak candidate ids when prefixes are ambiguous", async () => {
|
||||
|
||||
@@ -217,7 +217,18 @@ export function createPluginApprovalHandlers(
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid decision"));
|
||||
return;
|
||||
}
|
||||
const approvalId = p.id.trim();
|
||||
const resolvedId = manager.lookupPendingId(p.id);
|
||||
if (resolvedId.kind === "none" || resolvedId.kind === "ambiguous") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const approvalId = resolvedId.id;
|
||||
const snapshot = manager.getSnapshot(approvalId);
|
||||
if (!snapshot || snapshot.resolvedAtMs !== undefined) {
|
||||
respond(
|
||||
|
||||
@@ -49,6 +49,9 @@ vi.mock("./gateway.ts", () => ({
|
||||
}));
|
||||
|
||||
const { handleGatewayEvent } = await import("./app-gateway.ts");
|
||||
const { addExecApproval } = await vi.importActual<typeof import("./controllers/exec-approval.ts")>(
|
||||
"./controllers/exec-approval.ts",
|
||||
);
|
||||
|
||||
function createHost() {
|
||||
return {
|
||||
@@ -120,3 +123,28 @@ describe("handleGatewayEvent sessions.changed", () => {
|
||||
expect(loadSessionsMock).toHaveBeenCalledWith(host);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addExecApproval", () => {
|
||||
it("keeps the newest approval at the front of the queue", () => {
|
||||
const queue = addExecApproval(
|
||||
[
|
||||
{
|
||||
id: "approval-old",
|
||||
kind: "exec",
|
||||
request: { command: "echo old" },
|
||||
createdAtMs: 1,
|
||||
expiresAtMs: Date.now() + 120_000,
|
||||
},
|
||||
],
|
||||
{
|
||||
id: "approval-new",
|
||||
kind: "exec",
|
||||
request: { command: "echo new" },
|
||||
createdAtMs: 2,
|
||||
expiresAtMs: Date.now() + 120_000,
|
||||
},
|
||||
);
|
||||
|
||||
expect(queue.map((entry) => entry.id)).toEqual(["approval-new", "approval-old"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ export function addExecApproval(
|
||||
entry: ExecApprovalRequest,
|
||||
): ExecApprovalRequest[] {
|
||||
const next = pruneExecApprovalQueue(queue).filter((item) => item.id !== entry.id);
|
||||
next.push(entry);
|
||||
next.unshift(entry);
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user