mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 00:10:25 +00:00
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:
161
src/auto-reply/reply.directive.exec-agent-defaults.test.ts
Normal file
161
src/auto-reply/reply.directive.exec-agent-defaults.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user