Skip to content

Commit f372abd

Browse files
committed
fix(agents): validate extension tool names
1 parent 285a792 commit f372abd

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

src/agents/sessions/extensions/loader.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,75 @@ export default async function(api) {
4343
expect(result.extensions).toHaveLength(1);
4444
expect(result.extensions[0]?.commands.has("sdk-subpath-probe")).toBe(true);
4545
});
46+
47+
it("rejects extension tools without a readable non-empty string name", async () => {
48+
const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-tool-name-"));
49+
tempDirs.push(dir);
50+
const extensionPath = join(dir, "extension.ts");
51+
await writeFile(
52+
extensionPath,
53+
`
54+
export default async function(api) {
55+
api.registerTool({
56+
name: 123,
57+
label: "Broken",
58+
description: "broken",
59+
parameters: { type: "object", properties: {} },
60+
async execute() {
61+
return { content: [], details: {} };
62+
},
63+
});
64+
}
65+
`,
66+
);
67+
68+
const result = await loadExtensions([extensionPath], dir);
69+
70+
expect(result.extensions).toEqual([]);
71+
expect(result.errors).toEqual([
72+
{
73+
path: extensionPath,
74+
error:
75+
"Failed to load extension: Extension tool registration requires a non-empty string name; received 123",
76+
},
77+
]);
78+
});
79+
80+
it("rejects extension tools whose name accessor throws", async () => {
81+
const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-tool-name-"));
82+
tempDirs.push(dir);
83+
const extensionPath = join(dir, "extension.ts");
84+
await writeFile(
85+
extensionPath,
86+
`
87+
export default async function(api) {
88+
const tool = {
89+
label: "Broken",
90+
description: "broken",
91+
parameters: { type: "object", properties: {} },
92+
async execute() {
93+
return { content: [], details: {} };
94+
},
95+
};
96+
Object.defineProperty(tool, "name", {
97+
get() {
98+
throw new Error("tool name getter exploded");
99+
},
100+
});
101+
api.registerTool(tool);
102+
}
103+
`,
104+
);
105+
106+
const result = await loadExtensions([extensionPath], dir);
107+
108+
expect(result.extensions).toEqual([]);
109+
expect(result.errors).toEqual([
110+
{
111+
path: extensionPath,
112+
error:
113+
"Failed to load extension: Extension tool registration requires a readable tool name: tool name getter exploded",
114+
},
115+
]);
116+
});
46117
});

src/agents/sessions/extensions/loader.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,25 @@ function resolvePath(extPath: string, cwd: string): string {
148148

149149
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
150150

151+
function readRegisteredToolName(tool: ToolDefinition): string {
152+
let name: unknown;
153+
try {
154+
name = tool.name;
155+
} catch (error) {
156+
const message = error instanceof Error ? error.message : String(error);
157+
throw new Error(
158+
`Extension tool registration requires a readable tool name: ${message || "unknown error"}`,
159+
{ cause: error },
160+
);
161+
}
162+
if (typeof name !== "string" || name.trim().length === 0) {
163+
throw new Error(
164+
`Extension tool registration requires a non-empty string name; received ${JSON.stringify(name)}`,
165+
);
166+
}
167+
return name;
168+
}
169+
151170
/**
152171
* Create a runtime with throwing stubs for action methods.
153172
* Runner.bindCore() replaces these with real implementations.
@@ -226,7 +245,8 @@ function createExtensionAPI(
226245

227246
registerTool(tool: ToolDefinition): void {
228247
runtime.assertActive();
229-
extension.tools.set(tool.name, {
248+
const name = readRegisteredToolName(tool);
249+
extension.tools.set(name, {
230250
definition: tool,
231251
sourceInfo: extension.sourceInfo,
232252
});

0 commit comments

Comments
 (0)