Skip to content

Commit b451d8b

Browse files
fix(workshop): keep approval target names UTF-16 safe (#102963)
* fix(workshop): keep approval target names UTF-16 safe * test(workshop): tighten UTF-16 approval regression * test(workshop): mirror approval budget exactly * docs(plugins): correct approval description limit --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 88acda1 commit b451d8b

3 files changed

Lines changed: 31 additions & 7 deletions

File tree

docs/plugins/plugin-permission-requests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default definePluginEntry({
7777
Write prompt text for the person who will approve the action:
7878

7979
- Keep `title` short and action-focused; the Gateway caps it at 80 characters.
80-
- Keep `description` specific and bounded; the Gateway caps it at 256
80+
- Keep `description` specific and bounded; the Gateway caps it at 512
8181
characters.
8282
- Include the action, target, and risk. Do not include secrets, tokens, or
8383
private payloads that should not appear in chat approval surfaces.

src/skills/workshop/policy.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import { PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH } from "../../infra/plugin-approvals.js";
23
import {
34
createOpenClawTestState,
45
type OpenClawTestState,
@@ -69,14 +70,29 @@ describe("resolveSkillWorkshopToolApproval", () => {
6970
);
7071
});
7172

72-
it("bounds approval metadata without dropping required proposal facts", async () => {
73+
it("bounds approval metadata without splitting UTF-16 surrogates", async () => {
7374
const workspaceDir = await tempDirs.make("openclaw-skill-workshop-policy-long-name-");
7475
const description = "d".repeat(160);
76+
const content = "# Long name\n";
77+
const proposalIdLength = 60 + 1 + 8 + 1 + 10;
78+
const fixedLines = [
79+
`Proposal ID: ${"p".repeat(proposalIdLength)}`,
80+
`Description: ${description}`,
81+
"Support files: 0",
82+
`Body size: ${(Buffer.byteLength(content, "utf8") / 1024).toFixed(1)} KB`,
83+
];
84+
const skillPrefix = "Target skill: ";
85+
const fixedLength = fixedLines.join("\n").length + skillPrefix.length + fixedLines.length;
86+
const availableSkillNameLength = Math.max(
87+
1,
88+
PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH - fixedLength,
89+
);
90+
const prefix = "n".repeat(availableSkillNameLength - 2);
7591
const proposal = await proposeCreateSkill({
7692
workspaceDir,
77-
name: "n".repeat(240),
93+
name: `${prefix}\u{1F600}tail`,
7894
description,
79-
content: "# Long name\n",
95+
content,
8096
});
8197

8298
const result = await resolveSkillWorkshopToolApproval({
@@ -86,14 +102,21 @@ describe("resolveSkillWorkshopToolApproval", () => {
86102
});
87103
const approvalDescription = result?.requireApproval?.description ?? "";
88104

89-
expect(approvalDescription.length).toBeLessThanOrEqual(512);
105+
expect(approvalDescription.length).toBeLessThanOrEqual(
106+
PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH,
107+
);
90108
expect(approvalDescription).toContain(`Proposal ID: ${proposal.record.id}`);
91109
expect(approvalDescription).toContain(`Description: ${description}`);
92110
expect(approvalDescription).toContain("Support files: 0");
93111
expect(approvalDescription).toContain(
94112
`Body size: ${(Buffer.byteLength(proposal.content, "utf8") / 1024).toFixed(1)} KB`,
95113
);
96-
expect(approvalDescription).toContain("Target skill: nnn");
114+
const targetLine = result?.requireApproval?.description.split("\n")[1] ?? "";
115+
116+
expect(targetLine).toBe(`Target skill: ${prefix}…`);
117+
expect(approvalDescription).not.toMatch(
118+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/,
119+
);
97120
});
98121

99122
it("renders proposal-controlled fields without approval-line injection", async () => {

src/skills/workshop/policy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Workshop policy helpers validate generated skill drafts against workspace policy.
22
import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
3+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
34
import type { OpenClawConfig } from "../../config/types.openclaw.js";
45
import { PLUGIN_APPROVAL_DESCRIPTION_MAX_LENGTH } from "../../infra/plugin-approvals.js";
56
import type { PluginHookBeforeToolCallResult } from "../../plugins/hook-before-tool-call-result.js";
@@ -92,7 +93,7 @@ function buildLifecycleApprovalDescription(params: {
9293
const skillName =
9394
requestedSkillName.length <= availableSkillNameLength
9495
? requestedSkillName
95-
: `${requestedSkillName.slice(0, Math.max(0, availableSkillNameLength - 1))}…`;
96+
: `${truncateUtf16Safe(requestedSkillName, Math.max(0, availableSkillNameLength - 1))}…`;
9697
return [fixedLines[0], `${skillPrefix}${skillName}`, ...fixedLines.slice(1)].join("\n");
9798
}
9899

0 commit comments

Comments
 (0)