mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 09:33:06 +00:00
fix(exec): harden shell-side approval guardrails (#57839)
* fix(exec): harden approval handling * fix(exec): tighten approval guardrails * fix(exec): reject prefixed approval commands * fix(exec): isolate shell approval guardrails * fix(exec): recurse through wrapped approval commands * fix(exec): restore allowlist wrapper import * fix(exec): strip env wrappers before approval detection * fix(exec): inspect nested shell wrapper options
This commit is contained in:
@@ -1,69 +1,80 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./tools/gateway.js", () => ({
|
||||
callGatewayTool: vi.fn(async () => ({ ok: true })),
|
||||
}));
|
||||
|
||||
let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup.js").sendExecApprovalFollowup;
|
||||
let callGatewayTool: typeof import("./tools/gateway.js").callGatewayTool;
|
||||
let buildExecApprovalFollowupPrompt: typeof import("./bash-tools.exec-approval-followup.js").buildExecApprovalFollowupPrompt;
|
||||
let sendExecApprovalFollowup: typeof import("./bash-tools.exec-approval-followup.js").sendExecApprovalFollowup;
|
||||
|
||||
describe("sendExecApprovalFollowup", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("./tools/gateway.js", () => ({
|
||||
callGatewayTool: vi.fn(),
|
||||
}));
|
||||
({ sendExecApprovalFollowup } = await import("./bash-tools.exec-approval-followup.js"));
|
||||
({ callGatewayTool } = await import("./tools/gateway.js"));
|
||||
vi.mocked(callGatewayTool).mockReset();
|
||||
vi.mocked(callGatewayTool).mockResolvedValue({});
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ callGatewayTool } = await import("./tools/gateway.js"));
|
||||
({ buildExecApprovalFollowupPrompt, sendExecApprovalFollowup } = await import(
|
||||
"./bash-tools.exec-approval-followup.js"
|
||||
));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("exec approval followup", () => {
|
||||
it("uses an explicit denial prompt when the command did not run", () => {
|
||||
const prompt = buildExecApprovalFollowupPrompt(
|
||||
"Exec denied (gateway id=req-1, user-denied): uname -a",
|
||||
);
|
||||
|
||||
expect(prompt).toContain("did not run");
|
||||
expect(prompt).toContain("Do not mention, summarize, or reuse output");
|
||||
expect(prompt).not.toContain("already approved has completed");
|
||||
});
|
||||
|
||||
it("keeps followup session-only when no external delivery target exists", async () => {
|
||||
const ok = await sendExecApprovalFollowup({
|
||||
approvalId: "approval-1",
|
||||
it("keeps followups internal when no external route is available", async () => {
|
||||
await sendExecApprovalFollowup({
|
||||
approvalId: "req-1",
|
||||
sessionKey: "agent:main:main",
|
||||
resultText: "Exec finished (gateway id=approval-1, code 0)",
|
||||
resultText: "Exec completed: echo ok",
|
||||
});
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(callGatewayTool).toHaveBeenCalledTimes(1);
|
||||
const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(payload.deliver).toBe(false);
|
||||
expect(payload).not.toHaveProperty("bestEffortDeliver");
|
||||
expect(payload.channel).toBeUndefined();
|
||||
expect(payload.to).toBeUndefined();
|
||||
expect(callGatewayTool).toHaveBeenCalledWith(
|
||||
"agent",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
deliver: false,
|
||||
channel: undefined,
|
||||
to: undefined,
|
||||
}),
|
||||
{ expectFinal: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps followup session-only when turn source is internal webchat", async () => {
|
||||
const ok = await sendExecApprovalFollowup({
|
||||
approvalId: "approval-2",
|
||||
sessionKey: "agent:main:main",
|
||||
turnSourceChannel: "webchat",
|
||||
turnSourceTo: "chat:123",
|
||||
resultText: "Exec finished (gateway id=approval-2, code 0)",
|
||||
});
|
||||
|
||||
expect(ok).toBe(true);
|
||||
const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(payload.deliver).toBe(false);
|
||||
expect(payload).not.toHaveProperty("bestEffortDeliver");
|
||||
expect(payload.channel).toBe("webchat");
|
||||
expect(payload.to).toBe("chat:123");
|
||||
});
|
||||
|
||||
it("enables delivery for valid external turn source targets", async () => {
|
||||
const ok = await sendExecApprovalFollowup({
|
||||
approvalId: "approval-3",
|
||||
sessionKey: "agent:main:main",
|
||||
turnSourceChannel: " discord ",
|
||||
turnSourceTo: "channel:123",
|
||||
it("uses external delivery when a deliverable route is available", async () => {
|
||||
await sendExecApprovalFollowup({
|
||||
approvalId: "req-2",
|
||||
sessionKey: "agent:main:discord:channel:123",
|
||||
turnSourceChannel: "discord",
|
||||
turnSourceTo: "123",
|
||||
turnSourceAccountId: "default",
|
||||
resultText: "Exec finished (gateway id=approval-3, code 0)",
|
||||
turnSourceThreadId: "456",
|
||||
resultText: "Exec completed: echo ok",
|
||||
});
|
||||
|
||||
expect(ok).toBe(true);
|
||||
const payload = vi.mocked(callGatewayTool).mock.calls[0]?.[2] as Record<string, unknown>;
|
||||
expect(payload.deliver).toBe(true);
|
||||
expect(payload.bestEffortDeliver).toBe(true);
|
||||
expect(payload.channel).toBe("discord");
|
||||
expect(payload.to).toBe("channel:123");
|
||||
expect(payload.accountId).toBe("default");
|
||||
expect(callGatewayTool).toHaveBeenCalledWith(
|
||||
"agent",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:discord:channel:123",
|
||||
deliver: true,
|
||||
bestEffortDeliver: true,
|
||||
channel: "discord",
|
||||
to: "123",
|
||||
accountId: "default",
|
||||
threadId: "456",
|
||||
}),
|
||||
{ expectFinal: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,7 +433,7 @@ describe("exec approvals", () => {
|
||||
expect(calls).toContain("exec.approval.waitDecision");
|
||||
});
|
||||
|
||||
it("starts a direct agent follow-up after approved gateway exec completes", async () => {
|
||||
it("starts an internal agent follow-up after approved gateway exec completes without an external route", async () => {
|
||||
const agentCalls: Array<Record<string, unknown>> = [];
|
||||
|
||||
mockAcceptedApprovalFlow({
|
||||
@@ -639,6 +639,55 @@ describe("exec approvals", () => {
|
||||
expect(calls).toContain("exec.approval.request");
|
||||
});
|
||||
|
||||
it("runs a skill wrapper chain without prompting when the wrapper is allowlisted", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-wrapper-"));
|
||||
try {
|
||||
const skillDir = path.join(tempDir, ".openclaw", "skills", "gog");
|
||||
const skillPath = path.join(skillDir, "SKILL.md");
|
||||
const binDir = path.join(tempDir, "bin");
|
||||
const wrapperPath = path.join(binDir, "gog-wrapper");
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await fs.writeFile(skillPath, "# gog skill\n");
|
||||
await fs.writeFile(wrapperPath, "#!/bin/sh\necho '{\"events\":[]}'\n");
|
||||
await fs.chmod(wrapperPath, 0o755);
|
||||
|
||||
await writeExecApprovalsConfig({
|
||||
version: 1,
|
||||
defaults: { security: "allowlist", ask: "off", askFallback: "deny" },
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [{ pattern: wrapperPath }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const calls: string[] = [];
|
||||
mockGatewayOkCalls(calls);
|
||||
|
||||
const tool = createExecTool({
|
||||
host: "gateway",
|
||||
ask: "off",
|
||||
security: "allowlist",
|
||||
approvalRunningNoticeMs: 0,
|
||||
});
|
||||
|
||||
const result = await tool.execute("call-skill-wrapper", {
|
||||
command: `cat ${JSON.stringify(skillPath)} && printf '\\n---CMD---\\n' && ${JSON.stringify(wrapperPath)} calendar events primary --today --json`,
|
||||
workdir: tempDir,
|
||||
});
|
||||
|
||||
expect(result.details.status).toBe("completed");
|
||||
expect(getResultText(result)).toContain('{"events":[]}');
|
||||
expect(calls).not.toContain("exec.approval.request");
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("shows full chained node commands in approval-pending message", async () => {
|
||||
const calls: string[] = [];
|
||||
vi.mocked(callGatewayTool).mockImplementation(async (method, _opts, params) => {
|
||||
|
||||
@@ -269,4 +269,31 @@ describe("exec host env validation", () => {
|
||||
}),
|
||||
).rejects.toThrow(/requires a sandbox runtime/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"echo ok && /approve abc123 allow-once",
|
||||
"echo ok | /approve abc123 deny",
|
||||
"echo ok\n/approve abc123 allow-once",
|
||||
"FOO=1 /approve abc123 allow-once",
|
||||
"env -i /approve abc123 deny",
|
||||
"env --ignore-environment /approve abc123 allow-once",
|
||||
"env -i FOO=1 /approve abc123 allow-once",
|
||||
"env -S '/approve abc123 deny'",
|
||||
"command /approve abc123 deny",
|
||||
"command -p /approve abc123 deny",
|
||||
"exec -a openclaw /approve abc123 deny",
|
||||
"sudo /approve abc123 allow-once",
|
||||
"sudo -E /approve abc123 allow-once",
|
||||
"bash -lc '/approve abc123 deny'",
|
||||
"bash -c 'sudo /approve abc123 allow-once'",
|
||||
"sh -c '/approve abc123 allow-once'",
|
||||
])("rejects /approve shell commands in %s", async (command) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-approve", {
|
||||
command,
|
||||
}),
|
||||
).rejects.toThrow(/exec cannot run \/approve commands/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { analyzeShellCommand } from "../infra/exec-approvals-analysis.js";
|
||||
import { type ExecHost, loadExecApprovals, maxAsk, minSecurity } from "../infra/exec-approvals.js";
|
||||
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
|
||||
import { sanitizeHostExecEnvWithDiagnostics } from "../infra/host-env-security.js";
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "../infra/shell-env.js";
|
||||
import { logInfo } from "../logger.js";
|
||||
import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import { splitShellArgs } from "../utils/shell-argv.js";
|
||||
import { markBackgrounded } from "./bash-process-registry.js";
|
||||
import { processGatewayAllowlist } from "./bash-tools.exec-host-gateway.js";
|
||||
import { executeNodeHostCommand } from "./bash-tools.exec-host-node.js";
|
||||
@@ -173,6 +175,261 @@ async function validateScriptFileForShellBleed(params: {
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedExecApprovalCommand = {
|
||||
approvalId: string;
|
||||
decision: "allow-once" | "allow-always" | "deny";
|
||||
};
|
||||
|
||||
function parseExecApprovalShellCommand(raw: string): ParsedExecApprovalCommand | null {
|
||||
const normalized = raw.trimStart();
|
||||
const match = normalized.match(
|
||||
/^\/approve(?:@[^\s]+)?\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s+(allow-once|allow-always|always|deny)\b/i,
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
approvalId: match[1],
|
||||
decision:
|
||||
match[2].toLowerCase() === "always"
|
||||
? "allow-always"
|
||||
: (match[2].toLowerCase() as ParsedExecApprovalCommand["decision"]),
|
||||
};
|
||||
}
|
||||
|
||||
function rejectExecApprovalShellCommand(command: string): void {
|
||||
const isEnvAssignmentToken = (token: string): boolean =>
|
||||
/^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(token);
|
||||
const shellWrappers = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"]);
|
||||
const commandStandaloneOptions = new Set(["-p", "-v", "-V"]);
|
||||
const envOptionsWithValues = new Set([
|
||||
"-C",
|
||||
"-S",
|
||||
"-u",
|
||||
"--argv0",
|
||||
"--block-signal",
|
||||
"--chdir",
|
||||
"--default-signal",
|
||||
"--ignore-signal",
|
||||
"--split-string",
|
||||
"--unset",
|
||||
]);
|
||||
const execOptionsWithValues = new Set(["-a"]);
|
||||
const execStandaloneOptions = new Set(["-c", "-l"]);
|
||||
const sudoOptionsWithValues = new Set([
|
||||
"-C",
|
||||
"-D",
|
||||
"-g",
|
||||
"-p",
|
||||
"-R",
|
||||
"-T",
|
||||
"-U",
|
||||
"-u",
|
||||
"--chdir",
|
||||
"--close-from",
|
||||
"--group",
|
||||
"--host",
|
||||
"--other-user",
|
||||
"--prompt",
|
||||
"--role",
|
||||
"--type",
|
||||
"--user",
|
||||
]);
|
||||
const sudoStandaloneOptions = new Set(["-A", "-E", "--askpass", "--preserve-env"]);
|
||||
const extractEnvSplitStringPayload = (argv: string[]): string[] => {
|
||||
const remaining = [...argv];
|
||||
while (remaining[0] && isEnvAssignmentToken(remaining[0])) {
|
||||
remaining.shift();
|
||||
}
|
||||
if (remaining[0] !== "env") {
|
||||
return [];
|
||||
}
|
||||
remaining.shift();
|
||||
const payloads: string[] = [];
|
||||
while (remaining.length > 0) {
|
||||
while (remaining[0] && isEnvAssignmentToken(remaining[0])) {
|
||||
remaining.shift();
|
||||
}
|
||||
const token: string | undefined = remaining[0];
|
||||
if (!token) {
|
||||
break;
|
||||
}
|
||||
if (token === "--") {
|
||||
remaining.shift();
|
||||
continue;
|
||||
}
|
||||
if (!token.startsWith("-") || token === "-") {
|
||||
break;
|
||||
}
|
||||
const option = remaining.shift()!;
|
||||
const normalized = option.split("=", 1)[0];
|
||||
if (normalized === "-S" || normalized === "--split-string") {
|
||||
const value = option.includes("=")
|
||||
? option.slice(option.indexOf("=") + 1)
|
||||
: remaining.shift();
|
||||
if (value?.trim()) {
|
||||
payloads.push(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (envOptionsWithValues.has(normalized) && !option.includes("=") && remaining[0]) {
|
||||
remaining.shift();
|
||||
}
|
||||
}
|
||||
return payloads;
|
||||
};
|
||||
const stripApprovalCommandPrefixes = (argv: string[]): string[] => {
|
||||
const remaining = [...argv];
|
||||
while (remaining.length > 0) {
|
||||
while (remaining[0] && isEnvAssignmentToken(remaining[0])) {
|
||||
remaining.shift();
|
||||
}
|
||||
|
||||
const token = remaining[0];
|
||||
if (!token) {
|
||||
break;
|
||||
}
|
||||
if (token === "--") {
|
||||
remaining.shift();
|
||||
continue;
|
||||
}
|
||||
if (token === "env") {
|
||||
remaining.shift();
|
||||
while (remaining.length > 0) {
|
||||
while (remaining[0] && isEnvAssignmentToken(remaining[0])) {
|
||||
remaining.shift();
|
||||
}
|
||||
const envToken = remaining[0];
|
||||
if (!envToken) {
|
||||
break;
|
||||
}
|
||||
if (envToken === "--") {
|
||||
remaining.shift();
|
||||
continue;
|
||||
}
|
||||
if (!envToken.startsWith("-") || envToken === "-") {
|
||||
break;
|
||||
}
|
||||
const option = remaining.shift()!;
|
||||
const normalized = option.split("=", 1)[0];
|
||||
if (envOptionsWithValues.has(normalized) && !option.includes("=") && remaining[0]) {
|
||||
remaining.shift();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token === "command" || token === "builtin") {
|
||||
remaining.shift();
|
||||
while (remaining[0]?.startsWith("-")) {
|
||||
const option = remaining.shift()!;
|
||||
if (option === "--") {
|
||||
break;
|
||||
}
|
||||
if (!commandStandaloneOptions.has(option.split("=", 1)[0])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token === "exec") {
|
||||
remaining.shift();
|
||||
while (remaining[0]?.startsWith("-")) {
|
||||
const option = remaining.shift()!;
|
||||
if (option === "--") {
|
||||
break;
|
||||
}
|
||||
const normalized = option.split("=", 1)[0];
|
||||
if (execStandaloneOptions.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (execOptionsWithValues.has(normalized) && !option.includes("=") && remaining[0]) {
|
||||
remaining.shift();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token === "sudo") {
|
||||
remaining.shift();
|
||||
while (remaining[0]?.startsWith("-")) {
|
||||
const option = remaining.shift()!;
|
||||
if (option === "--") {
|
||||
break;
|
||||
}
|
||||
const normalized = option.split("=", 1)[0];
|
||||
if (sudoStandaloneOptions.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (sudoOptionsWithValues.has(normalized) && !option.includes("=") && remaining[0]) {
|
||||
remaining.shift();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return remaining;
|
||||
};
|
||||
const extractShellWrapperPayload = (argv: string[]): string[] => {
|
||||
const [commandName, ...rest] = argv;
|
||||
if (!commandName || !shellWrappers.has(path.basename(commandName))) {
|
||||
return [];
|
||||
}
|
||||
for (let i = 0; i < rest.length; i += 1) {
|
||||
const token = rest[i];
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (token === "-c" || token === "-lc" || token === "-ic" || token === "-xc") {
|
||||
return rest[i + 1] ? [rest[i + 1]] : [];
|
||||
}
|
||||
if (/^-[^-]*c[^-]*$/u.test(token)) {
|
||||
return rest[i + 1] ? [rest[i + 1]] : [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const buildCandidates = (argv: string[]): string[] => {
|
||||
const envSplitCandidates = extractEnvSplitStringPayload(argv).flatMap((payload) => {
|
||||
const innerArgv = splitShellArgs(payload);
|
||||
return innerArgv ? buildCandidates(innerArgv) : [payload];
|
||||
});
|
||||
const stripped = stripApprovalCommandPrefixes(argv);
|
||||
const shellWrapperCandidates = extractShellWrapperPayload(stripped).flatMap((payload) => {
|
||||
const innerArgv = splitShellArgs(payload);
|
||||
return innerArgv ? buildCandidates(innerArgv) : [payload];
|
||||
});
|
||||
return [
|
||||
...(stripped.length > 0 ? [stripped.join(" ")] : []),
|
||||
...envSplitCandidates,
|
||||
...shellWrapperCandidates,
|
||||
];
|
||||
};
|
||||
|
||||
const rawCommand = command.trim();
|
||||
const analysis = analyzeShellCommand({ command: rawCommand });
|
||||
const candidates = analysis.ok
|
||||
? analysis.segments.flatMap((segment) => buildCandidates(segment.argv))
|
||||
: rawCommand
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.flatMap((line) => {
|
||||
const argv = splitShellArgs(line);
|
||||
return argv ? buildCandidates(argv) : [line];
|
||||
});
|
||||
for (const candidate of candidates) {
|
||||
if (!parseExecApprovalShellCommand(candidate)) {
|
||||
continue;
|
||||
}
|
||||
throw new Error(
|
||||
[
|
||||
"exec cannot run /approve commands.",
|
||||
"Show the /approve command to the user as chat text, or route it through the approval command handler instead of shell execution.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createExecTool(
|
||||
defaults?: ExecToolDefaults,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
@@ -378,6 +635,7 @@ export function createExecTool(
|
||||
// fall back to the gateway's cwd. The node is responsible for validating its own cwd.
|
||||
workdir = resolveWorkdir(rawWorkdir, warnings);
|
||||
}
|
||||
rejectExecApprovalShellCommand(params.command);
|
||||
|
||||
const inheritedBaseEnv = coerceEnv(process.env);
|
||||
const hostEnvResult =
|
||||
|
||||
@@ -149,6 +149,16 @@ describe("buildAgentSystemPrompt", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("tells the agent not to execute /approve through exec", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
|
||||
);
|
||||
});
|
||||
|
||||
it("omits skills in minimal prompt mode when skillsPrompt is absent", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
|
||||
@@ -450,7 +450,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
"Keep narration brief and value-dense; avoid repeating obvious steps.",
|
||||
"Use plain human language for narration unless in a technical context.",
|
||||
"When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI or slash commands.",
|
||||
"When exec returns approval-pending, include the concrete /approve command from tool output (with allow-once|allow-always|deny) and do not ask for a different or rotated code.",
|
||||
"When exec returns approval-pending, include the concrete /approve command from tool output (with allow-once|allow-always|deny) as plain chat text for the user, and do not ask for a different or rotated code.",
|
||||
"Never execute /approve through exec or any other shell/tool path; /approve is a user-facing approval command, not a shell command.",
|
||||
"Treat allow-once as single-command only: if another elevated command needs approval, request a fresh /approve and do not claim prior approval covered it.",
|
||||
"When approvals are required, preserve and show the full command/script exactly as provided (including chained operators like &&, ||, |, ;, or multiline shells) so the user can approve what will actually run.",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user