Skip to content

Commit 47c81b0

Browse files
refactor(approvals): narrow external verification contract
1 parent 66806d7 commit 47c81b0

6 files changed

Lines changed: 62 additions & 163 deletions

File tree

docs/.generated/plugin-sdk-api-baseline.sha256

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ b80e26a4dcf2c246d01755b8da4bee272637f11ced1689fa561397c1979613e5 module/agent-h
1212
7ddd81bd5f55de9adf64bf4d92d012f24b37b6da0a72805a3a220d8feff24ca3 module/approval-auth-runtime
1313
a973b03d88f2aa71d3b6c39241527c18d9b1b75f5639bf39cc9b080efbe7b6c6 module/approval-client-runtime
1414
37346f8621e2a12ca819f44075882aec1e4a7ebf12dbcd9ba772aeb8413404db module/approval-delivery-runtime
15-
e26d5faba8bae937a29cb7a8864a1f6ad7e999dd55b47245973ba5fa2dbecae4 module/approval-gateway-runtime
15+
00e1157ef3ea6a4c57510075c08b7e11223b997d1752bddae41ce629945d2813 module/approval-gateway-runtime
1616
aceab327f18983cfa074384b4510648be2156dfdd30415a2d7a48fd1f38da1c6 module/approval-handler-adapter-runtime
1717
a453261fc6c0d45ad652992bfa3139a1295b6e6296878ee5fde2cf12b995516e module/approval-handler-runtime
1818
dad0906b607cfc044805279c25f656b65579b92b458c8a7c248b7cc8e297d99b module/approval-native-runtime

src/infra/approval-presentation.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function buildPluginPresentation(request: {
2323
agentId?: string;
2424
externalResolution?: {
2525
label: string;
26-
decisions?: Array<"allow-once" | "allow-always">;
26+
decisions?: readonly ("allow-once" | "allow-always")[];
2727
};
2828
}) {
2929
return buildApprovalPresentation({ kind: "plugin", request, allowedDecisions });
@@ -180,6 +180,37 @@ describe("buildApprovalPresentation", () => {
180180
}),
181181
).toBeNull();
182182
});
183+
184+
it.each([
185+
{ label: " ", decisions: undefined },
186+
{ label: "Verify", decisions: [] },
187+
{ label: "Verify", decisions: ["allow-once", "allow-once"] as const },
188+
])("rejects malformed external verification metadata", (externalResolution) => {
189+
expect(
190+
buildPluginPresentation({
191+
title: "World verification",
192+
description: "Verify personhood before continuing.",
193+
pluginId: "agentkit",
194+
externalResolution,
195+
}),
196+
).toBeNull();
197+
});
198+
199+
it("defaults external verification to allow once", () => {
200+
expect(
201+
buildPluginPresentation({
202+
title: "World verification",
203+
description: "Verify personhood before continuing.",
204+
pluginId: "agentkit",
205+
externalResolution: { label: " Verify\nwith World " },
206+
}),
207+
).toMatchObject({
208+
externalResolution: {
209+
label: "Verify\\u{A}with World",
210+
decisions: ["allow-once"],
211+
},
212+
});
213+
});
183214
});
184215

185216
describe("buildApprovalPresentation (system-agent)", () => {

src/infra/approval-presentation.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import {
1414
} from "./exec-approval-command-display.js";
1515
import type { ExecApprovalRequestPayload } from "./exec-approvals.js";
1616
import {
17-
normalizePluginExternalResolution,
1817
PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH,
1918
PLUGIN_APPROVAL_TITLE_MAX_LENGTH,
2019
truncatePluginApprovalDetail,
2120
type PluginApprovalRequestPayload,
2221
} from "./plugin-approvals.js";
2322
import type { SystemAgentApprovalRequestPayload } from "./system-agent-approvals.js";
2423

24+
const PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH = 80;
25+
2526
function normalizeDecisionList(decisions: readonly ApprovalDecision[]): ApprovalDecision[] {
2627
const result: ApprovalDecision[] = [];
2728
for (const decision of decisions) {
@@ -44,6 +45,29 @@ function sanitizeOptionalSingleLine(value: unknown): string | null {
4445
return normalized ? sanitizeExecApprovalDisplayText(normalized) : null;
4546
}
4647

48+
function normalizePluginExternalResolution(
49+
value: PluginApprovalRequestPayload["externalResolution"],
50+
): NonNullable<PluginApprovalRequestPayload["externalResolution"]> | null {
51+
if (!value) {
52+
return null;
53+
}
54+
const rawLabel = value.label?.trim();
55+
const label = rawLabel ? sanitizeExecApprovalDisplayText(rawLabel) : "";
56+
if (!label || !isWithinCodePointLimit(label, PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH)) {
57+
throw new Error("invalid external approval label");
58+
}
59+
const decisions = value.decisions ?? ["allow-once"];
60+
if (
61+
decisions.length < 1 ||
62+
decisions.length > 2 ||
63+
decisions.some((decision) => decision !== "allow-once" && decision !== "allow-always") ||
64+
new Set(decisions).size !== decisions.length
65+
) {
66+
throw new Error("invalid external approval decisions");
67+
}
68+
return { label, decisions: [...decisions] };
69+
}
70+
4771
function buildExecApprovalPresentation(params: {
4872
request: unknown;
4973
allowedDecisions: readonly ApprovalDecision[];

src/infra/plugin-approvals.test.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/infra/plugin-approvals.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// Defines plugin approval request/resolution payloads and actions.
22
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
3-
import type { PluginExternalResolution } from "../plugins/external-verification-approval-types.js";
4-
import { sanitizeExecApprovalDisplayText } from "./exec-approval-command-display.js";
53
import type { ExecApprovalDecision } from "./exec-approvals.js";
64

75
// Plugin approval types and renderers mirror exec approval decisions while
@@ -26,12 +24,13 @@ export type PluginApprovalRequestPayload = {
2624
toolCallId?: string | null;
2725
allowedDecisions?: readonly ExecApprovalDecision[] | null;
2826
/** Trusted in-process metadata; public Gateway callers cannot submit this field. */
29-
externalResolution?: PluginExternalResolution | null;
27+
externalResolution?: {
28+
label: string;
29+
decisions?: readonly ("allow-once" | "allow-always")[];
30+
} | null;
3031
actions?: readonly PluginApprovalActionView[] | null;
3132
agentId?: string | null;
3233
sessionKey?: string | null;
33-
sessionId?: string | null;
34-
runId?: string | null;
3534
turnSourceChannel?: string | null;
3635
turnSourceTo?: string | null;
3736
turnSourceAccountId?: string | null;
@@ -60,7 +59,6 @@ export const MAX_PLUGIN_APPROVAL_TIMEOUT_MS = 600_000;
6059
export const PLUGIN_APPROVAL_TITLE_MAX_LENGTH = 80;
6160
export const PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH = 512;
6261
export const PLUGIN_APPROVAL_DETAIL_MAX_LENGTH = 16_384;
63-
export const PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH = 80;
6462
const PLUGIN_APPROVAL_DETAIL_TRUNCATION_SUFFIX = "…[truncated]";
6563
export const DEFAULT_PLUGIN_APPROVAL_DECISIONS = [
6664
"allow-once",
@@ -124,34 +122,6 @@ export function resolvePluginApprovalRequestAllowedDecisions(params?: {
124122
return explicit.length > 0 ? explicit : DEFAULT_PLUGIN_APPROVAL_DECISIONS;
125123
}
126124

127-
/** Validate and normalize the plugin-owned allow path before it crosses into Gateway state. */
128-
export function normalizePluginExternalResolution(
129-
value: PluginExternalResolution | null | undefined,
130-
): PluginExternalResolution | null {
131-
if (!value) {
132-
return null;
133-
}
134-
const rawLabel = value.label?.trim();
135-
const label = rawLabel ? sanitizeExecApprovalDisplayText(rawLabel) : "";
136-
if (!label || Array.from(label).length > PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH) {
137-
throw new Error(
138-
`external approval label must be 1-${PLUGIN_EXTERNAL_RESOLUTION_LABEL_MAX_LENGTH} characters`,
139-
);
140-
}
141-
const decisions = value.decisions ?? ["allow-once"];
142-
if (
143-
decisions.length < 1 ||
144-
decisions.length > 2 ||
145-
decisions.some((decision) => decision !== "allow-once" && decision !== "allow-always") ||
146-
new Set(decisions).size !== decisions.length
147-
) {
148-
throw new Error(
149-
"external approval decisions must contain unique allow-once/allow-always values",
150-
);
151-
}
152-
return { label, decisions: [...decisions] };
153-
}
154-
155125
/** Build the pending plugin approval message. */
156126
export function buildPluginApprovalRequestMessage(
157127
request: PluginApprovalRequest,

src/plugins/external-verification-approval-types.ts

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)