fix(agents): honor explicit cron tool allowlists

This commit is contained in:
Ayaan Zaidi
2026-04-21 10:34:28 +05:30
parent c18b6fc9da
commit 4f2d24f463
4 changed files with 44 additions and 12 deletions

View File

@@ -10,6 +10,7 @@ import {
buildAfterTurnRuntimeContextFromUsage,
composeSystemPromptWithHookContext,
decodeHtmlEntitiesInObject,
applyEmbeddedAttemptToolsAllow,
isPrimaryBootstrapRun,
mergeOrphanedTrailingUserPrompt,
prependSystemPromptAddition,
@@ -61,6 +62,16 @@ async function invokeWrappedTestStream(
return await Promise.resolve(wrappedFn({} as never, {} as never, {} as never));
}
describe("applyEmbeddedAttemptToolsAllow", () => {
it("keeps explicit toolsAllow authoritative after force-added tools are built", () => {
const tools = [{ name: "exec" }, { name: "read" }, { name: "message" }];
expect(
applyEmbeddedAttemptToolsAllow(tools, ["exec", "read"]).map((tool) => tool.name),
).toEqual(["exec", "read"]);
});
});
describe("resolvePromptBuildHookResult", () => {
function createLegacyOnlyHookRunner() {
return {

View File

@@ -403,6 +403,17 @@ function summarizeSessionContext(messages: AgentMessage[]): {
};
}
export function applyEmbeddedAttemptToolsAllow<T extends { name: string }>(
tools: T[],
toolsAllow?: string[],
): T[] {
if (!toolsAllow || toolsAllow.length === 0) {
return tools;
}
const allowSet = new Set(toolsAllow);
return tools.filter((tool) => allowSet.has(tool.name));
}
export async function runEmbeddedAttempt(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
@@ -536,14 +547,7 @@ export async function runEmbeddedAttempt(
abortSessionForYield?.();
},
});
if (params.toolsAllow && params.toolsAllow.length > 0) {
const allowSet = new Set(params.toolsAllow);
if (params.forceMessageTool) {
allowSet.add("message");
}
return allTools.filter((tool) => allowSet.has(tool.name));
}
return allTools;
return applyEmbeddedAttemptToolsAllow(allTools, params.toolsAllow);
})();
const toolsEnabled = supportsModelTools(params.model);
const bootstrapHasFileAccess = toolsEnabled && toolsRaw.some((tool) => tool.name === "read");

View File

@@ -279,6 +279,23 @@ describe("createOpenClawCodingTools", () => {
expect(cronTools.some((tool) => tool.name === "message")).toBe(true);
});
it("can keep message available when a cron route needs it under a provider coding profile", () => {
const providerProfileTools = createOpenClawCodingTools({
config: { tools: { byProvider: { openai: { profile: "coding" } } } },
modelProvider: "openai",
modelId: "gpt-5.4",
});
expect(providerProfileTools.some((tool) => tool.name === "message")).toBe(false);
const cronTools = createOpenClawCodingTools({
config: { tools: { byProvider: { openai: { profile: "coding" } } } },
modelProvider: "openai",
modelId: "gpt-5.4",
forceMessageTool: true,
});
expect(cronTools.some((tool) => tool.name === "message")).toBe(true);
});
it("expands group shorthands in global tool policy", () => {
const tools = createOpenClawCodingTools({
config: { tools: { allow: ["group:fs"] } },

View File

@@ -387,10 +387,10 @@ export function createOpenClawCodingTools(options?: {
...(profileAlsoAllow ?? []),
...runtimeProfileAlsoAllow,
]);
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(
providerProfilePolicy,
providerProfileAlsoAllow,
);
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(providerProfilePolicy, [
...(providerProfileAlsoAllow ?? []),
...runtimeProfileAlsoAllow,
]);
// Prefer sessionKey for process isolation scope to prevent cross-session process visibility/killing.
// Fallback to agentId if no sessionKey is available (e.g. legacy or global contexts).
const scopeKey =