fix #88773: [Bug]: Telegram DM exec requires approval despite allowlist + ask:off — works in webchat, not in Telegram (#89035)

* fix exec ask policy source

* fix gateway test type fixtures

* docs: update exec ask parameter docs to match runtime behavior

* fix: preserve trusted per-call exec ask hardening while blocking model-supplied overrides for channel runs

* docs: align exec ask contract with runtime

* refactor(agents): simplify exec ask policy cleanup

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
zhang-guiping
2026-06-03 21:03:08 +08:00
committed by GitHub
parent b3b203bf67
commit 60dcaa3cf5
5 changed files with 83 additions and 7 deletions
@@ -13,6 +13,24 @@ vi.mock("./tools/gateway.js", () => ({
readGatewayCallOptions: vi.fn(() => ({})),
}));
function installAllowlistedGogFixture(root: string): string {
const binDir = path.join(root, "bin");
const openclawDir = path.join(root, ".openclaw");
fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(openclawDir, { recursive: true });
const gogPath = path.join(binDir, "gog");
fs.writeFileSync(gogPath, "#!/bin/sh\nprintf 'gog-ok %s\\n' \"$*\"\n", { mode: 0o755 });
fs.writeFileSync(
path.join(openclawDir, "exec-approvals.json"),
`${JSON.stringify({
version: 1,
defaults: { security: "allowlist", ask: "off", askFallback: "allowlist" },
agents: { "*": { allowlist: [{ pattern: gogPath }] } },
})}\n`,
);
return binDir;
}
describe("exec security floor", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
let tempRoot: string | undefined;
@@ -88,6 +106,52 @@ describe("exec security floor", () => {
).rejects.toThrow(/exec denied: allowlist miss/i);
});
it("ignores model-supplied ask overrides when configured ask is off", async () => {
const root = tempRoot ?? os.tmpdir();
const binDir = installAllowlistedGogFixture(root);
const tool = createExecTool({
host: "gateway",
security: "allowlist",
ask: "off",
safeBins: [],
pathPrepend: [binDir],
messageProvider: "telegram",
currentChannelId: "telegram:12345",
accountId: "default",
});
const result = await tool.execute("call-model-ask-ignored", {
command: "gog tasks add tasklist --title test",
ask: "always",
});
expect(result.details.status).toBe("completed");
expect((result.content[0] as { text?: string }).text ?? "").toContain(
"gog-ok tasks add tasklist --title test",
);
expect(callGatewayTool).not.toHaveBeenCalled();
});
it("honors per-call ask hardening for trusted callers without messageProvider", async () => {
const root = tempRoot ?? os.tmpdir();
const binDir = installAllowlistedGogFixture(root);
const tool = createExecTool({
host: "gateway",
security: "allowlist",
ask: "off",
safeBins: [],
pathPrepend: [binDir],
});
const result = await tool.execute("call-trusted-ask-always", {
command: "gog tasks add tasklist --title test",
ask: "always",
});
expect(callGatewayTool).toHaveBeenCalled();
expect(result.details.status).toBe("approval-pending");
});
it("ignores model-supplied deny security when configured security is allowlist", async () => {
const tool = createExecTool({
security: "allowlist",
+3 -2
View File
@@ -16,6 +16,7 @@ import {
loadExecApprovals,
maxAsk,
minSecurity,
normalizeExecAsk,
requireValidExecTarget,
resolveExecApprovalsFromFile,
resolveExecModePolicy,
@@ -56,7 +57,6 @@ import {
type ExecProcessOutcome,
applyPathPrepend,
applyShellPath,
normalizeExecAsk,
normalizePathPrepend,
resolveExecTarget,
resolveApprovalRunningNoticeMs,
@@ -1613,7 +1613,8 @@ export function createExecTool(
// Keep local exec defaults in sync with exec-approvals.json when tools.exec.* is unset.
const requestedAsk = normalizeExecAsk(params.ask);
const hostAsk = maxAsk(modePolicy.ask, approvalPolicy?.ask ?? modePolicy.ask);
let ask = maxAsk(hostAsk, requestedAsk ?? hostAsk);
const trustedAsk = defaults?.messageProvider && hostAsk === "off" ? undefined : requestedAsk;
let ask = maxAsk(hostAsk, trustedAsk ?? hostAsk);
const bypassApprovals =
elevatedRequested &&
elevatedMode === "full" &&
+2 -1
View File
@@ -40,7 +40,8 @@ export const execSchema = Type.Object({
),
ask: Type.Optional(
Type.String({
description: "Exec ask mode (off|on-miss|always).",
description:
"Baseline ask comes from tools.exec.ask and host approvals; channel-origin calls ignore per-call ask when effective host ask is off.",
}),
),
node: Type.Optional(