diff --git a/CHANGELOG.md b/CHANGELOG.md index b3be2f1ce..38dfb57db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Docs: https://docs.clawd.bot - 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. - 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: 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-. diff --git a/docs/tools/exec.md b/docs/tools/exec.md index 02865060f..759f0fe30 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -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.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.node` (default: unset) - `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs. diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index dc2865ccc..98496a842 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -400,7 +400,7 @@ export function createExecTool( host = "gateway"; } - const configuredSecurity = defaults?.security ?? "deny"; + const configuredSecurity = defaults?.security ?? (host === "sandbox" ? "deny" : "allowlist"); const requestedSecurity = normalizeExecSecurity(params.security); let security = minSecurity(configuredSecurity, requestedSecurity ?? configuredSecurity); if (elevatedRequested) { @@ -447,7 +447,10 @@ export function createExecTool( applyPathPrepend(env, defaultPathPrepend); 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 hostAsk = maxAsk(ask, approvals.agent.ask); const askFallback = approvals.agent.askFallback; @@ -616,7 +619,7 @@ export function createExecTool( } if (host === "gateway") { - const approvals = resolveExecApprovals(defaults?.agentId); + const approvals = resolveExecApprovals(defaults?.agentId, { security: "allowlist" }); const hostSecurity = minSecurity(security, approvals.agent.security); const hostAsk = maxAsk(ask, approvals.agent.ask); const askFallback = approvals.agent.askFallback; diff --git a/src/infra/exec-approvals.ts b/src/infra/exec-approvals.ts index eb51d369a..cedbab2e7 100644 --- a/src/infra/exec-approvals.ts +++ b/src/infra/exec-approvals.ts @@ -188,31 +188,54 @@ export function ensureExecApprovals(): ExecApprovalsFile { return updated; } -function normalizeSecurity(value?: ExecSecurity): ExecSecurity { +function normalizeSecurity(value: ExecSecurity | undefined, fallback: ExecSecurity): ExecSecurity { 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; - 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 defaults = file.defaults ?? {}; const agentKey = agentId ?? "default"; 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 = { - security: normalizeSecurity(defaults.security), - ask: normalizeAsk(defaults.ask), - askFallback: normalizeSecurity(defaults.askFallback ?? DEFAULT_ASK_FALLBACK), - autoAllowSkills: Boolean(defaults.autoAllowSkills ?? DEFAULT_AUTO_ALLOW_SKILLS), + security: normalizeSecurity(defaults.security, fallbackSecurity), + ask: normalizeAsk(defaults.ask, fallbackAsk), + askFallback: normalizeSecurity( + defaults.askFallback ?? fallbackAskFallback, + fallbackAskFallback, + ), + autoAllowSkills: Boolean(defaults.autoAllowSkills ?? fallbackAutoAllowSkills), }; const resolvedAgent: Required = { - security: normalizeSecurity(agent.security ?? resolvedDefaults.security), - ask: normalizeAsk(agent.ask ?? resolvedDefaults.ask), - askFallback: normalizeSecurity(agent.askFallback ?? resolvedDefaults.askFallback), + security: normalizeSecurity( + agent.security ?? resolvedDefaults.security, + resolvedDefaults.security, + ), + ask: normalizeAsk(agent.ask ?? resolvedDefaults.ask, resolvedDefaults.ask), + askFallback: normalizeSecurity( + agent.askFallback ?? resolvedDefaults.askFallback, + resolvedDefaults.askFallback, + ), autoAllowSkills: Boolean(agent.autoAllowSkills ?? resolvedDefaults.autoAllowSkills), }; const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : []; diff --git a/src/node-host/runner.ts b/src/node-host/runner.ts index fd8dad184..6093beb77 100644 --- a/src/node-host/runner.ts +++ b/src/node-host/runner.ts @@ -545,7 +545,7 @@ async function handleInvoke( const rawCommand = typeof params.rawCommand === "string" ? params.rawCommand.trim() : ""; const cmdText = rawCommand || formatCommand(argv); const agentId = params.agentId?.trim() || undefined; - const approvals = resolveExecApprovals(agentId); + const approvals = resolveExecApprovals(agentId, { security: "allowlist" }); const security = approvals.agent.security; const ask = approvals.agent.ask; const autoAllowSkills = approvals.agent.autoAllowSkills;