mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 06:41:08 +00:00
core: dedupe approval not-found handling (#60932)
Merged via squash.
Prepared head SHA: 108221fdfe
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
ef7c84ae92
commit
e627f53d24
@@ -121,6 +121,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.
|
||||
- Cron: suppress exact `NO_REPLY` sentinel direct-delivery payloads, keep silent direct replies from falling back into duplicate main-summary sends, and treat structured `deleteAfterRun` silent replies the same as text silent replies. (#45737) Thanks @openperf.
|
||||
- Cron: keep exact silent-token detection case-insensitive again so mixed-case `NO_REPLY` outputs still stay silent in text and direct delivery paths. Thanks @obviyus.
|
||||
- Core/approvals: share approval-not-found fallback classification through the narrow `plugin-sdk/error-runtime` seam so core `/approve` and Telegram stay aligned without widening `plugin-sdk/infra-runtime`. (#60932) Thanks @gumadeiras.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
8c38d3e2d0a61c02db70070e0d032b54b1de474000e1c1221332efc495e8681d plugin-sdk-api-baseline.json
|
||||
d057310712f83b27f64b53dbe45ef2e92795407e56503a255de0f29d915c1ee4 plugin-sdk-api-baseline.jsonl
|
||||
0cd9a43c490bb5511890171543a3029754d44c9f1fe1ebf6f5c845fb49f44452 plugin-sdk-api-baseline.json
|
||||
66e1a9dff2b6c170dd1caceef1f15ad63c18f89c897d98f502cac1f2f46d26c2 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -207,7 +207,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/ssrf-runtime` | SSRF runtime helpers | Pinned-dispatcher, guarded fetch, SSRF policy helpers |
|
||||
| `plugin-sdk/collection-runtime` | Bounded cache helpers | `pruneMapToMaxSize` |
|
||||
| `plugin-sdk/diagnostic-runtime` | Diagnostic gating helpers | `isDiagnosticFlagEnabled`, `isDiagnosticsEnabled` |
|
||||
| `plugin-sdk/error-runtime` | Error formatting helpers | `formatUncaughtError`, error graph helpers |
|
||||
| `plugin-sdk/error-runtime` | Error formatting helpers | `formatUncaughtError`, `isApprovalNotFoundError`, error graph helpers |
|
||||
| `plugin-sdk/fetch-runtime` | Wrapped fetch/proxy helpers | `resolveFetch`, proxy helpers |
|
||||
| `plugin-sdk/host-runtime` | Host normalization helpers | `normalizeHostname`, `normalizeScpRemoteHost` |
|
||||
| `plugin-sdk/retry-runtime` | Retry helpers | `RetryConfig`, `retryAsync`, policy runners |
|
||||
|
||||
@@ -210,7 +210,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/infra-runtime` | System event/heartbeat helpers |
|
||||
| `plugin-sdk/collection-runtime` | Small bounded cache helpers |
|
||||
| `plugin-sdk/diagnostic-runtime` | Diagnostic flag and event helpers |
|
||||
| `plugin-sdk/error-runtime` | Error graph and formatting helpers |
|
||||
| `plugin-sdk/error-runtime` | Error graph, formatting, and shared error classification helpers |
|
||||
| `plugin-sdk/fetch-runtime` | Wrapped fetch, proxy, and pinned lookup helpers |
|
||||
| `plugin-sdk/host-runtime` | Hostname and SCP host normalization helpers |
|
||||
| `plugin-sdk/retry-runtime` | Retry config and retry runner helpers |
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { createOperatorApprovalsGatewayClient } from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/infra-runtime";
|
||||
|
||||
@@ -11,38 +12,6 @@ export type ResolveTelegramExecApprovalParams = {
|
||||
gatewayUrl?: string;
|
||||
};
|
||||
|
||||
const INVALID_REQUEST = "INVALID_REQUEST";
|
||||
const APPROVAL_NOT_FOUND = "APPROVAL_NOT_FOUND";
|
||||
|
||||
function readErrorCode(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
function readApprovalNotFoundDetailsReason(value: unknown): string | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const reason = (value as { reason?: unknown }).reason;
|
||||
return typeof reason === "string" && reason.trim() ? reason : null;
|
||||
}
|
||||
|
||||
function isApprovalNotFoundError(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const gatewayCode = readErrorCode((err as { gatewayCode?: unknown }).gatewayCode);
|
||||
if (gatewayCode === APPROVAL_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const detailsReason = readApprovalNotFoundDetailsReason((err as { details?: unknown }).details);
|
||||
if (gatewayCode === INVALID_REQUEST && detailsReason === APPROVAL_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /unknown or expired approval id/i.test(err.message);
|
||||
}
|
||||
|
||||
export async function resolveTelegramExecApproval(
|
||||
params: ResolveTelegramExecApprovalParams,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
resolveChannelApprovalCapability,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { ErrorCodes } from "../../gateway/protocol/index.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { isApprovalNotFoundError } from "../../infra/approval-errors.js";
|
||||
import { resolveApprovalCommandAuthorization } from "../../infra/channel-approval-auth.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { resolveChannelAccountId } from "./channel-context.js";
|
||||
@@ -78,39 +78,6 @@ function buildResolvedByLabel(params: Parameters<CommandHandler>[0]): string {
|
||||
return `${channel}:${sender}`;
|
||||
}
|
||||
|
||||
function readErrorCode(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
function readApprovalNotFoundDetailsReason(value: unknown): string | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const reason = (value as { reason?: unknown }).reason;
|
||||
return typeof reason === "string" && reason.trim() ? reason : null;
|
||||
}
|
||||
|
||||
function isApprovalNotFoundError(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const gatewayCode = readErrorCode((err as { gatewayCode?: unknown }).gatewayCode);
|
||||
if (gatewayCode === ErrorCodes.APPROVAL_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const detailsReason = readApprovalNotFoundDetailsReason((err as { details?: unknown }).details);
|
||||
if (
|
||||
gatewayCode === ErrorCodes.INVALID_REQUEST &&
|
||||
detailsReason === ErrorCodes.APPROVAL_NOT_FOUND
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy server/client combinations may only include the message text.
|
||||
return /unknown or expired approval id/i.test(err.message);
|
||||
}
|
||||
|
||||
function formatApprovalSubmitError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
29
src/infra/approval-errors.test.ts
Normal file
29
src/infra/approval-errors.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isApprovalNotFoundError } from "./approval-errors.js";
|
||||
|
||||
describe("isApprovalNotFoundError", () => {
|
||||
it("matches direct approval-not-found gateway codes", () => {
|
||||
const err = new Error("approval not found") as Error & { gatewayCode?: string };
|
||||
err.gatewayCode = "APPROVAL_NOT_FOUND";
|
||||
expect(isApprovalNotFoundError(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("matches structured invalid-request approval-not-found details", () => {
|
||||
const err = new Error("approval not found") as Error & {
|
||||
gatewayCode?: string;
|
||||
details?: { reason?: string };
|
||||
};
|
||||
err.gatewayCode = "INVALID_REQUEST";
|
||||
err.details = { reason: "APPROVAL_NOT_FOUND" };
|
||||
expect(isApprovalNotFoundError(err)).toBe(true);
|
||||
});
|
||||
|
||||
it("matches legacy message-only not-found errors", () => {
|
||||
expect(isApprovalNotFoundError(new Error("unknown or expired approval id"))).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores unrelated errors", () => {
|
||||
expect(isApprovalNotFoundError(new Error("network timeout"))).toBe(false);
|
||||
expect(isApprovalNotFoundError("unknown or expired approval id")).toBe(false);
|
||||
});
|
||||
});
|
||||
29
src/infra/approval-errors.ts
Normal file
29
src/infra/approval-errors.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
const INVALID_REQUEST = "INVALID_REQUEST";
|
||||
const APPROVAL_NOT_FOUND = "APPROVAL_NOT_FOUND";
|
||||
|
||||
function readErrorCode(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value : null;
|
||||
}
|
||||
|
||||
function readApprovalNotFoundDetailsReason(value: unknown): string | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const reason = (value as { reason?: unknown }).reason;
|
||||
return typeof reason === "string" && reason.trim() ? reason : null;
|
||||
}
|
||||
|
||||
export function isApprovalNotFoundError(err: unknown): boolean {
|
||||
if (!(err instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const gatewayCode = readErrorCode((err as { gatewayCode?: unknown }).gatewayCode);
|
||||
if (gatewayCode === APPROVAL_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
const detailsReason = readApprovalNotFoundDetailsReason((err as { details?: unknown }).details);
|
||||
if (gatewayCode === INVALID_REQUEST && detailsReason === APPROVAL_NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
return /unknown or expired approval id/i.test(err.message);
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export {
|
||||
formatUncaughtError,
|
||||
readErrorName,
|
||||
} from "../infra/errors.js";
|
||||
export { isApprovalNotFoundError } from "../infra/approval-errors.ts";
|
||||
|
||||
@@ -841,6 +841,7 @@ describe("plugin-sdk subpath exports", () => {
|
||||
|
||||
expectSourceMentions("infra-runtime", ["createRuntimeOutboundDelegates"]);
|
||||
expectSourceContains("infra-runtime", "../infra/outbound/send-deps.js");
|
||||
expectSourceMentions("error-runtime", ["formatUncaughtError", "isApprovalNotFoundError"]);
|
||||
|
||||
expect(typeof channelLifecycleSdk.createDraftStreamLoop).toBe("function");
|
||||
expect(typeof channelLifecycleSdk.createFinalizableDraftLifecycle).toBe("function");
|
||||
|
||||
Reference in New Issue
Block a user