This commit is contained in:
AI&Human 2026-01-30 12:56:08 -03:00 committed by GitHub
commit f96c50518a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 0 deletions

View File

@ -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<T> = (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<string>();
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<string>();
@ -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);

View File

@ -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`,