Merge 073e66a779 into 09be5d45d5
This commit is contained in:
commit
f96c50518a
@ -10,6 +10,7 @@ import {
|
|||||||
type ExecSecurity,
|
type ExecSecurity,
|
||||||
type ExecApprovalsFile,
|
type ExecApprovalsFile,
|
||||||
addAllowlistEntry,
|
addAllowlistEntry,
|
||||||
|
analyzeShellCommand,
|
||||||
evaluateShellAllowlist,
|
evaluateShellAllowlist,
|
||||||
maxAsk,
|
maxAsk,
|
||||||
minSecurity,
|
minSecurity,
|
||||||
@ -76,6 +77,7 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 120_000;
|
|||||||
const DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS = 130_000;
|
const DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS = 130_000;
|
||||||
const DEFAULT_APPROVAL_RUNNING_NOTICE_MS = 10_000;
|
const DEFAULT_APPROVAL_RUNNING_NOTICE_MS = 10_000;
|
||||||
const APPROVAL_SLUG_LENGTH = 8;
|
const APPROVAL_SLUG_LENGTH = 8;
|
||||||
|
const SHIELD_SHELL_BLOCKLIST = new Set(["rm", "chmod", "env", "curl"]);
|
||||||
|
|
||||||
type PtyExitEvent = { exitCode: number; signal?: number };
|
type PtyExitEvent = { exitCode: number; signal?: number };
|
||||||
type PtyListener<T> = (event: T) => void;
|
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)",
|
"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(
|
elevated: Type.Optional(
|
||||||
Type.Boolean({
|
Type.Boolean({
|
||||||
description: "Run on the host with elevated permissions (if allowed)",
|
description: "Run on the host with elevated permissions (if allowed)",
|
||||||
@ -251,6 +259,60 @@ function normalizeNotifyOutput(value: string) {
|
|||||||
return value.replace(/\s+/g, " ").trim();
|
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[]) {
|
function normalizePathPrepend(entries?: string[]) {
|
||||||
if (!Array.isArray(entries)) return [];
|
if (!Array.isArray(entries)) return [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@ -749,6 +811,7 @@ export function createExecTool(
|
|||||||
security?: string;
|
security?: string;
|
||||||
ask?: string;
|
ask?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
|
dangerously_bypass_approvals_and_sandbox?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!params.command) {
|
if (!params.command) {
|
||||||
@ -885,6 +948,18 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
applyPathPrepend(env, defaultPathPrepend);
|
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") {
|
if (host === "node") {
|
||||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
const approvals = resolveExecApprovals(agentId, { security, ask });
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
|
|||||||
@ -23,6 +23,9 @@ const DEFAULT_REDACT_PATTERNS: string[] = [
|
|||||||
String.raw`\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`,
|
String.raw`\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b`,
|
||||||
// PEM blocks.
|
// PEM blocks.
|
||||||
String.raw`-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----`,
|
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.
|
// Common token prefixes.
|
||||||
String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
||||||
String.raw`\b(ghp_[A-Za-z0-9]{20,})\b`,
|
String.raw`\b(ghp_[A-Za-z0-9]{20,})\b`,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user