fix: honor tools.exec ask/security in approvals
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
|
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Exec approvals are the **companion app / node host guardrail** for letting a san
|
|||||||
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
||||||
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
||||||
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
||||||
|
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
|
||||||
|
|
||||||
If the companion app UI is **not available**, any request that requires a prompt is
|
If the companion app UI is **not available**, any request that requires a prompt is
|
||||||
resolved by the **ask fallback** (default: deny).
|
resolved by the **ask fallback** (default: deny).
|
||||||
|
|||||||
@@ -129,4 +129,25 @@ describe("exec approvals", () => {
|
|||||||
expect(calls).toContain("node.invoke");
|
expect(calls).toContain("node.invoke");
|
||||||
expect(calls).not.toContain("exec.approval.request");
|
expect(calls).not.toContain("exec.approval.request");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("honors ask=off for elevated gateway exec without prompting", async () => {
|
||||||
|
const { callGatewayTool } = await import("./tools/gateway.js");
|
||||||
|
const calls: string[] = [];
|
||||||
|
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
||||||
|
calls.push(method);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { createExecTool } = await import("./bash-tools.exec.js");
|
||||||
|
const tool = createExecTool({
|
||||||
|
ask: "off",
|
||||||
|
security: "full",
|
||||||
|
approvalRunningNoticeMs: 0,
|
||||||
|
elevated: { enabled: true, allowed: true, defaultLevel: "ask" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await tool.execute("call3", { command: "echo ok", elevated: true });
|
||||||
|
expect(result.details.status).toBe("completed");
|
||||||
|
expect(calls).not.toContain("exec.approval.request");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -838,10 +838,7 @@ export function createExecTool(
|
|||||||
applyPathPrepend(env, defaultPathPrepend);
|
applyPathPrepend(env, defaultPathPrepend);
|
||||||
|
|
||||||
if (host === "node") {
|
if (host === "node") {
|
||||||
const approvals = resolveExecApprovals(
|
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||||
agentId,
|
|
||||||
host === "node" ? { security: "allowlist" } : undefined,
|
|
||||||
);
|
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
@@ -1112,7 +1109,7 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (host === "gateway" && !bypassApprovals) {
|
if (host === "gateway" && !bypassApprovals) {
|
||||||
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
|
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
|||||||
const approvals = resolveExecApprovalsFromFile({
|
const approvals = resolveExecApprovalsFromFile({
|
||||||
file: approvalsFile as ExecApprovalsFile,
|
file: approvalsFile as ExecApprovalsFile,
|
||||||
agentId,
|
agentId,
|
||||||
overrides: { security: "allowlist" },
|
overrides: { security, ask },
|
||||||
});
|
});
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
readExecApprovalsSnapshot,
|
readExecApprovalsSnapshot,
|
||||||
resolveExecApprovalsSocketPath,
|
resolveExecApprovalsSocketPath,
|
||||||
saveExecApprovals,
|
saveExecApprovals,
|
||||||
|
type ExecAsk,
|
||||||
|
type ExecSecurity,
|
||||||
type ExecApprovalsFile,
|
type ExecApprovalsFile,
|
||||||
type ExecAllowlistEntry,
|
type ExecAllowlistEntry,
|
||||||
type ExecCommandSegment,
|
type ExecCommandSegment,
|
||||||
@@ -110,6 +112,14 @@ type RunResult = {
|
|||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveExecSecurity(value?: string): ExecSecurity {
|
||||||
|
return value === "deny" || value === "allowlist" || value === "full" ? value : "allowlist";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExecAsk(value?: string): ExecAsk {
|
||||||
|
return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss";
|
||||||
|
}
|
||||||
|
|
||||||
type ExecEventPayload = {
|
type ExecEventPayload = {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
@@ -794,15 +804,20 @@ async function handleInvoke(
|
|||||||
const rawCommand = typeof params.rawCommand === "string" ? params.rawCommand.trim() : "";
|
const rawCommand = typeof params.rawCommand === "string" ? params.rawCommand.trim() : "";
|
||||||
const cmdText = rawCommand || formatCommand(argv);
|
const cmdText = rawCommand || formatCommand(argv);
|
||||||
const agentId = params.agentId?.trim() || undefined;
|
const agentId = params.agentId?.trim() || undefined;
|
||||||
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
|
const cfg = loadConfig();
|
||||||
|
const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined;
|
||||||
|
const configuredSecurity = resolveExecSecurity(agentExec?.security ?? cfg.tools?.exec?.security);
|
||||||
|
const configuredAsk = resolveExecAsk(agentExec?.ask ?? cfg.tools?.exec?.ask);
|
||||||
|
const approvals = resolveExecApprovals(agentId, {
|
||||||
|
security: configuredSecurity,
|
||||||
|
ask: configuredAsk,
|
||||||
|
});
|
||||||
const security = approvals.agent.security;
|
const security = approvals.agent.security;
|
||||||
const ask = approvals.agent.ask;
|
const ask = approvals.agent.ask;
|
||||||
const autoAllowSkills = approvals.agent.autoAllowSkills;
|
const autoAllowSkills = approvals.agent.autoAllowSkills;
|
||||||
const sessionKey = params.sessionKey?.trim() || "node";
|
const sessionKey = params.sessionKey?.trim() || "node";
|
||||||
const runId = params.runId?.trim() || crypto.randomUUID();
|
const runId = params.runId?.trim() || crypto.randomUUID();
|
||||||
const env = sanitizeEnv(params.env ?? undefined);
|
const env = sanitizeEnv(params.env ?? undefined);
|
||||||
const cfg = loadConfig();
|
|
||||||
const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined;
|
|
||||||
const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins);
|
const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins);
|
||||||
const bins = autoAllowSkills ? await skillBins.current() : new Set<string>();
|
const bins = autoAllowSkills ? await skillBins.current() : new Set<string>();
|
||||||
let analysisOk = false;
|
let analysisOk = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user