fix: default exec security to allowlist

This commit is contained in:
Peter Steinberger
2026-01-21 03:40:21 +00:00
parent 026e6c4df4
commit 28c49db494
5 changed files with 44 additions and 17 deletions

View File

@@ -37,6 +37,7 @@ Docs: https://docs.clawd.bot
- Gateway: preserve restart wake routing + thread replies across restarts. (#1337) — thanks @John-Rood. - Gateway: preserve restart wake routing + thread replies across restarts. (#1337) — thanks @John-Rood.
- Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner. - Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner.
- Config: log invalid config issues once per run and keep invalid-config errors stackless. - Config: log invalid config issues once per run and keep invalid-config errors stackless.
- Exec: default gateway/node exec security to allowlist when unset (sandbox stays deny).
- UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315) — thanks @MaudeBot. - UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315) — thanks @MaudeBot.
- UI: preserve ordered list numbering in chat markdown. (#1341) — thanks @bradleypriest. - UI: preserve ordered list numbering in chat markdown. (#1341) — thanks @bradleypriest.
- UI: allow Control UI to read gatewayUrl from URL params for remote WebSocket targets. (#1342) — thanks @ameno-. - UI: allow Control UI to read gatewayUrl from URL params for remote WebSocket targets. (#1342) — thanks @ameno-.

View File

@@ -39,7 +39,7 @@ Notes:
- `tools.exec.notifyOnExit` (default: true): when true, backgrounded exec sessions enqueue a system event and request a heartbeat on exit. - `tools.exec.notifyOnExit` (default: true): when true, backgrounded exec sessions enqueue a system event and request a heartbeat on exit.
- `tools.exec.host` (default: `sandbox`) - `tools.exec.host` (default: `sandbox`)
- `tools.exec.security` (default: `deny`) - `tools.exec.security` (default: `deny` for sandbox, `allowlist` for gateway + node when unset)
- `tools.exec.ask` (default: `on-miss`) - `tools.exec.ask` (default: `on-miss`)
- `tools.exec.node` (default: unset) - `tools.exec.node` (default: unset)
- `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs. - `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs.

View File

@@ -400,7 +400,7 @@ export function createExecTool(
host = "gateway"; host = "gateway";
} }
const configuredSecurity = defaults?.security ?? "deny"; const configuredSecurity = defaults?.security ?? (host === "sandbox" ? "deny" : "allowlist");
const requestedSecurity = normalizeExecSecurity(params.security); const requestedSecurity = normalizeExecSecurity(params.security);
let security = minSecurity(configuredSecurity, requestedSecurity ?? configuredSecurity); let security = minSecurity(configuredSecurity, requestedSecurity ?? configuredSecurity);
if (elevatedRequested) { if (elevatedRequested) {
@@ -447,7 +447,10 @@ export function createExecTool(
applyPathPrepend(env, defaultPathPrepend); applyPathPrepend(env, defaultPathPrepend);
if (host === "node") { if (host === "node") {
const approvals = resolveExecApprovals(defaults?.agentId); const approvals = resolveExecApprovals(
defaults?.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;
@@ -616,7 +619,7 @@ export function createExecTool(
} }
if (host === "gateway") { if (host === "gateway") {
const approvals = resolveExecApprovals(defaults?.agentId); const approvals = resolveExecApprovals(defaults?.agentId, { security: "allowlist" });
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;

View File

@@ -188,31 +188,54 @@ export function ensureExecApprovals(): ExecApprovalsFile {
return updated; return updated;
} }
function normalizeSecurity(value?: ExecSecurity): ExecSecurity { function normalizeSecurity(value: ExecSecurity | undefined, fallback: ExecSecurity): ExecSecurity {
if (value === "allowlist" || value === "full" || value === "deny") return value; if (value === "allowlist" || value === "full" || value === "deny") return value;
return DEFAULT_SECURITY; return fallback;
} }
function normalizeAsk(value?: ExecAsk): ExecAsk { function normalizeAsk(value: ExecAsk | undefined, fallback: ExecAsk): ExecAsk {
if (value === "always" || value === "off" || value === "on-miss") return value; if (value === "always" || value === "off" || value === "on-miss") return value;
return DEFAULT_ASK; return fallback;
} }
export function resolveExecApprovals(agentId?: string): ExecApprovalsResolved { export type ExecApprovalsDefaultOverrides = {
security?: ExecSecurity;
ask?: ExecAsk;
askFallback?: ExecSecurity;
autoAllowSkills?: boolean;
};
export function resolveExecApprovals(
agentId?: string,
overrides?: ExecApprovalsDefaultOverrides,
): ExecApprovalsResolved {
const file = ensureExecApprovals(); const file = ensureExecApprovals();
const defaults = file.defaults ?? {}; const defaults = file.defaults ?? {};
const agentKey = agentId ?? "default"; const agentKey = agentId ?? "default";
const agent = file.agents?.[agentKey] ?? {}; const agent = file.agents?.[agentKey] ?? {};
const fallbackSecurity = overrides?.security ?? DEFAULT_SECURITY;
const fallbackAsk = overrides?.ask ?? DEFAULT_ASK;
const fallbackAskFallback = overrides?.askFallback ?? DEFAULT_ASK_FALLBACK;
const fallbackAutoAllowSkills = overrides?.autoAllowSkills ?? DEFAULT_AUTO_ALLOW_SKILLS;
const resolvedDefaults: Required<ExecApprovalsDefaults> = { const resolvedDefaults: Required<ExecApprovalsDefaults> = {
security: normalizeSecurity(defaults.security), security: normalizeSecurity(defaults.security, fallbackSecurity),
ask: normalizeAsk(defaults.ask), ask: normalizeAsk(defaults.ask, fallbackAsk),
askFallback: normalizeSecurity(defaults.askFallback ?? DEFAULT_ASK_FALLBACK), askFallback: normalizeSecurity(
autoAllowSkills: Boolean(defaults.autoAllowSkills ?? DEFAULT_AUTO_ALLOW_SKILLS), defaults.askFallback ?? fallbackAskFallback,
fallbackAskFallback,
),
autoAllowSkills: Boolean(defaults.autoAllowSkills ?? fallbackAutoAllowSkills),
}; };
const resolvedAgent: Required<ExecApprovalsDefaults> = { const resolvedAgent: Required<ExecApprovalsDefaults> = {
security: normalizeSecurity(agent.security ?? resolvedDefaults.security), security: normalizeSecurity(
ask: normalizeAsk(agent.ask ?? resolvedDefaults.ask), agent.security ?? resolvedDefaults.security,
askFallback: normalizeSecurity(agent.askFallback ?? resolvedDefaults.askFallback), resolvedDefaults.security,
),
ask: normalizeAsk(agent.ask ?? resolvedDefaults.ask, resolvedDefaults.ask),
askFallback: normalizeSecurity(
agent.askFallback ?? resolvedDefaults.askFallback,
resolvedDefaults.askFallback,
),
autoAllowSkills: Boolean(agent.autoAllowSkills ?? resolvedDefaults.autoAllowSkills), autoAllowSkills: Boolean(agent.autoAllowSkills ?? resolvedDefaults.autoAllowSkills),
}; };
const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : []; const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : [];

View File

@@ -545,7 +545,7 @@ 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); const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
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;