diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index ad77d10e6..91fbb880c 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -10,6 +10,7 @@ import { type ExecSecurity, type ExecApprovalsFile, addAllowlistEntry, + analyzeShellCommand, evaluateShellAllowlist, maxAsk, minSecurity, @@ -76,6 +77,7 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 120_000; const DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS = 130_000; const DEFAULT_APPROVAL_RUNNING_NOTICE_MS = 10_000; const APPROVAL_SLUG_LENGTH = 8; +const SHIELD_SHELL_BLOCKLIST = new Set(["rm", "chmod", "env", "curl"]); type PtyExitEvent = { exitCode: number; signal?: number }; type PtyListener = (event: T) => void; @@ -165,6 +167,12 @@ const execSchema = Type.Object({ "Run in a pseudo-terminal (PTY) when available (TTY-required CLIs, coding agents)", }), ), + dangerously_bypass_approvals_and_sandbox: Type.Optional( + Type.Boolean({ + description: + "Allow Shield-Shell blocked commands (rm, chmod, env, curl). Use only in a sandbox or when explicitly approved.", + }), + ), elevated: Type.Optional( Type.Boolean({ description: "Run on the host with elevated permissions (if allowed)", @@ -251,6 +259,60 @@ function normalizeNotifyOutput(value: string) { return value.replace(/\s+/g, " ").trim(); } +function isEnvAssignment(token: string) { + return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token); +} + +function resolveSegmentExecutable(argv: string[]): string | null { + let i = 0; + while (i < argv.length) { + const token = argv[i]?.trim(); + if (!token) { + i += 1; + continue; + } + if (token === "sudo") { + i += 1; + while (i < argv.length && argv[i]?.startsWith("-")) i += 1; + continue; + } + if (isEnvAssignment(token)) { + i += 1; + continue; + } + return token; + } + return null; +} + +function normalizeExecutableName(token: string) { + if (!token) return ""; + const parsed = path.parse(token); + return (parsed.name || token).toLowerCase(); +} + +function findShieldShellMatches(command: string, cwd?: string, env?: NodeJS.ProcessEnv) { + const matches = new Set(); + const analysis = analyzeShellCommand({ command, cwd, env }); + if (analysis.ok) { + for (const segment of analysis.segments) { + const token = resolveSegmentExecutable(segment.argv); + if (!token) continue; + const normalized = normalizeExecutableName(token); + if (SHIELD_SHELL_BLOCKLIST.has(normalized)) { + matches.add(normalized); + } + } + } else { + const fallback = /(?:^|[;&|]\s*)(?:sudo\s+)?(rm|chmod|env|curl)(?:\s|$)/gi; + let match: RegExpExecArray | null; + while ((match = fallback.exec(command))) { + matches.add(match[1]?.toLowerCase()); + } + } + return [...matches]; +} + function normalizePathPrepend(entries?: string[]) { if (!Array.isArray(entries)) return []; const seen = new Set(); @@ -749,6 +811,7 @@ export function createExecTool( security?: string; ask?: string; node?: string; + dangerously_bypass_approvals_and_sandbox?: boolean; }; if (!params.command) { @@ -885,6 +948,18 @@ export function createExecTool( } applyPathPrepend(env, defaultPathPrepend); + const bypassShield = params.dangerously_bypass_approvals_and_sandbox === true; + const shieldMatches = findShieldShellMatches(params.command, workdir, env); + if (shieldMatches.length > 0 && !bypassShield) { + const list = shieldMatches.join(", "); + throw new Error( + [ + `Shield-Shell blocked execution: command uses ${list}.`, + "Run inside the sandbox or set dangerously_bypass_approvals_and_sandbox=true to proceed.", + ].join(" "), + ); + } + if (host === "node") { const approvals = resolveExecApprovals(agentId, { security, ask }); const hostSecurity = minSecurity(security, approvals.agent.security); diff --git a/src/logging/redact.ts b/src/logging/redact.ts index c3926d868..3cc15ee8a 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -23,6 +23,9 @@ const DEFAULT_REDACT_PATTERNS: string[] = [ String.raw`\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`, // PEM blocks. String.raw`-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----`, + // Key-Mask provider keys. + String.raw`\b(sk-proj-[A-Za-z0-9_-]{10,})\b`, + String.raw`\b(sk-ant-[A-Za-z0-9_-]{10,})\b`, // Common token prefixes. String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`, String.raw`\b(ghp_[A-Za-z0-9]{20,})\b`,