fix(auto-reply): thread per-agent tools.exec defaults into reply directives (#57689)

* fix(auto-reply): thread per-agent tools.exec defaults into exec overrides

* test(auto-reply): add session-override and inline-directive priority tests for exec agent defaults
This commit is contained in:
pgondhi987
2026-03-30 21:16:54 +05:30
committed by GitHub
parent 09bb93c6e0
commit 6d341cf366
2 changed files with 177 additions and 5 deletions

View File

@@ -0,0 +1,161 @@
import fs from "node:fs/promises";
import "./reply.directive.directive-behavior.e2e-mocks.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
DEFAULT_TEST_MODEL_CATALOG,
MAIN_SESSION_KEY,
installDirectiveBehaviorE2EHooks,
installFreshDirectiveBehaviorReplyMocks,
makeEmbeddedTextResult,
sessionStorePath,
withTempHome,
} from "./reply.directive.directive-behavior.e2e-harness.js";
import {
loadModelCatalogMock,
runEmbeddedPiAgentMock,
} from "./reply.directive.directive-behavior.e2e-mocks.js";
let getReplyFromConfig: typeof import("./reply/get-reply.js").getReplyFromConfig;
function makeAgentExecConfig(home: string) {
return {
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: `${home}/openclaw`,
},
list: [
{
id: "main",
tools: {
exec: {
host: "node" as const,
security: "allowlist" as const,
ask: "always" as const,
node: "worker-alpha",
},
},
},
],
},
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: sessionStorePath(home) },
};
}
describe("directive behavior exec agent defaults", () => {
installDirectiveBehaviorE2EHooks();
beforeEach(async () => {
vi.resetModules();
loadModelCatalogMock.mockReset();
loadModelCatalogMock.mockResolvedValue(DEFAULT_TEST_MODEL_CATALOG);
installFreshDirectiveBehaviorReplyMocks();
({ getReplyFromConfig } = await import("./reply/get-reply.js"));
});
it("threads per-agent tools.exec defaults into live runs without a persisted session override", async () => {
await withTempHome(async (home) => {
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
await getReplyFromConfig(
{
Body: "run a command",
From: "+1004",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1004",
},
{},
makeAgentExecConfig(home),
);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
expect(call?.execOverrides).toEqual({
host: "node",
security: "allowlist",
ask: "always",
node: "worker-alpha",
});
});
});
it("prefers standalone inline exec directives over per-agent exec defaults on the next live run", async () => {
await withTempHome(async (home) => {
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
await getReplyFromConfig(
{
Body: "/exec host=auto",
From: "+1004",
To: "+2000",
CommandAuthorized: true,
},
{},
makeAgentExecConfig(home),
);
runEmbeddedPiAgentMock.mockClear();
await getReplyFromConfig(
{
Body: "run a command",
From: "+1004",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1004",
},
{},
makeAgentExecConfig(home),
);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
expect(call?.execOverrides).toEqual({
host: "auto",
security: "allowlist",
ask: "always",
node: "worker-alpha",
});
});
});
it("prefers persisted session exec overrides over per-agent exec defaults", async () => {
await withTempHome(async (home) => {
runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedTextResult("done"));
await fs.writeFile(
sessionStorePath(home),
JSON.stringify({
[MAIN_SESSION_KEY]: {
sessionId: "main",
updatedAt: Date.now(),
execHost: "auto",
},
}),
"utf-8",
);
await getReplyFromConfig(
{
Body: "run a command",
From: "+1004",
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1004",
},
{},
makeAgentExecConfig(home),
);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
expect(call?.execOverrides).toEqual({
host: "auto",
security: "allowlist",
ask: "always",
node: "worker-alpha",
});
});
});
});

View File

@@ -25,6 +25,7 @@ import type { TypingController } from "./typing.js";
type AgentDefaults = NonNullable<OpenClawConfig["agents"]>["defaults"];
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
let commandsRegistryPromise: Promise<typeof import("../commands-registry.runtime.js")> | null =
null;
@@ -83,14 +84,24 @@ export type ReplyDirectiveContinuation = {
function resolveExecOverrides(params: {
directives: InlineDirectives;
sessionEntry?: SessionEntry;
agentEntry?: AgentEntry;
}): ExecOverrides | undefined {
const host =
params.directives.execHost ?? (params.sessionEntry?.execHost as ExecOverrides["host"]);
params.directives.execHost ??
(params.sessionEntry?.execHost as ExecOverrides["host"]) ??
(params.agentEntry?.tools?.exec?.host as ExecOverrides["host"]);
const security =
params.directives.execSecurity ??
(params.sessionEntry?.execSecurity as ExecOverrides["security"]);
const ask = params.directives.execAsk ?? (params.sessionEntry?.execAsk as ExecOverrides["ask"]);
const node = params.directives.execNode ?? params.sessionEntry?.execNode;
(params.sessionEntry?.execSecurity as ExecOverrides["security"]) ??
(params.agentEntry?.tools?.exec?.security as ExecOverrides["security"]);
const ask =
params.directives.execAsk ??
(params.sessionEntry?.execAsk as ExecOverrides["ask"]) ??
(params.agentEntry?.tools?.exec?.ask as ExecOverrides["ask"]);
const node =
params.directives.execNode ??
params.sessionEntry?.execNode ??
params.agentEntry?.tools?.exec?.node;
if (!host && !security && !ask && !node) {
return undefined;
}
@@ -506,7 +517,7 @@ export async function resolveReplyDirectives(params: {
model = applyResult.model;
contextTokens = applyResult.contextTokens;
const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = applyResult;
const execOverrides = resolveExecOverrides({ directives, sessionEntry });
const execOverrides = resolveExecOverrides({ directives, sessionEntry, agentEntry });
return {
kind: "continue",