chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:44:08 +08:00
commit 983960e2dd
1244 changed files with 281996 additions and 0 deletions
@@ -0,0 +1 @@
export { default } from './shell-tool'
@@ -0,0 +1,802 @@
import fs from 'node:fs'
import path from 'node:path'
import { Tool } from '@sdk/base-tool'
import { ToolkitConfig } from '@sdk/toolkit-config'
import { isWindows } from '@sdk/utils'
const DEFAULT_SETTINGS: Record<string, unknown> = {}
const REQUIRED_SETTINGS: string[] = []
interface ShellResult {
success: boolean
commandSucceeded?: boolean
error?: string
stdout: string
stderr: string
returncode: number
command: string
attempts: ShellAttempt[]
}
interface ShellAttempt {
attempt: number
timeoutMs: number
durationMs: number
status: 'success' | 'timeout' | 'error'
error?: string
}
interface ExecuteOptions {
cwd?: string
longRunning?: boolean
captureOutput?: boolean
}
interface ProcessExecutionError extends Error {
stdout?: Buffer | string
stderr?: Buffer | string
status?: number
code?: number | string | null
}
interface ShellInvocation {
binaryName: string
args: string[]
}
const DEFAULT_TIMEOUT_SECONDS = 30
const LONG_RUNNING_TIMEOUT_SECONDS = 86_400
const MILLISECONDS_PER_SECOND = 1_000
const CRITICAL_COMMAND_SEQUENCES = [
['rm', '-rf', '/'],
['rm', '-rf', '/*'],
['kill', '-9', '-1']
] as const
const CRITICAL_COMMAND_TOKENS = ['mkfs', 'format', 'fdisk'] as const
const HIGH_RISK_DD_TOKENS = ['dd'] as const
const HIGH_RISK_EVAL_DOWNLOAD_TOKENS = ['curl', 'wget'] as const
const ELEVATED_COMMAND_TOKENS = ['sudo', 'doas', 'pkexec', 'su'] as const
const POWERSHELL_ELEVATED_COMMAND_TOKENS = [
'start-process',
'runas'
] as const
const PERMISSION_COMMAND_TOKENS = ['chmod', 'chown'] as const
const PACKAGE_MANAGER_COMMAND_TOKENS = [
'apt',
'apt-get',
'yum',
'brew',
'pip',
'pip3'
] as const
const MEDIUM_RISK_COMMAND_PATTERNS: string[] = []
const UNSAFE_COMMAND_PATTERNS = [
'fork()',
'while true; do',
'while ($true)'
]
const POWERSHELL_HIGH_RISK_COMMAND_TOKENS = [
'invoke-expression',
'iex',
'set-executionpolicy',
'stop-computer',
'restart-computer'
] as const
const POWERSHELL_DESTRUCTIVE_COMMAND_TOKENS = [
'remove-item',
'del',
'erase',
'rmdir',
'rd'
] as const
const TERMINAL_AUTH_COMMANDS = new Set<string>([
...ELEVATED_COMMAND_TOKENS,
...POWERSHELL_ELEVATED_COMMAND_TOKENS
])
const TERMINAL_AUTH_WRAPPERS = new Set<string>([
'env',
'command',
'builtin',
'nohup',
'time'
])
export default class ShellTool extends Tool {
private static readonly TOOLKIT = 'operating_system_control'
private readonly config: ReturnType<typeof ToolkitConfig.load>
constructor() {
super()
this.config = ToolkitConfig.load(ShellTool.TOOLKIT, this.toolName)
const toolSettings = ToolkitConfig.loadToolSettings(
ShellTool.TOOLKIT,
this.toolName,
DEFAULT_SETTINGS
)
this.settings = toolSettings
this.requiredSettings = REQUIRED_SETTINGS
this.checkRequiredSettings(this.toolName)
}
get toolName(): string {
return 'shell'
}
get toolkit(): string {
return ShellTool.TOOLKIT
}
get description(): string {
return this.config['description']
}
async executeCommand(
command: string,
options: ExecuteOptions = {}
): Promise<ShellResult> {
const { cwd = process.cwd() } = options
const timeoutMs = ShellTool.getTimeoutMs(options)
const shellInvocation = ShellTool.getShellInvocation(command)
const analyzedCommand = await this.resolveCommandForSafetyAnalysis(command)
const isSafe = await this.isSafeCommand(analyzedCommand)
if (!isSafe) {
const riskLevel = await this.getCommandRiskLevel(analyzedCommand)
const riskDescription = await this.getRiskDescription(analyzedCommand)
return {
success: false,
error: `Blocked unsafe shell command (${riskLevel} risk): This command may ${riskDescription}.`,
stdout: '',
stderr: `Blocked unsafe shell command (${riskLevel} risk): This command may ${riskDescription}.`,
returncode: -1,
command,
attempts: []
}
}
const requiresVisibleTerminal = this.requiresVisibleTerminal(analyzedCommand)
const attempts: ShellAttempt[] = []
const attempt = 1
const attemptStartedAt = Date.now()
try {
if (requiresVisibleTerminal) {
await this.report('bridges.tools.command_requires_terminal_auth')
await super.executeCommand({
binaryName: shellInvocation.binaryName,
args: shellInvocation.args,
options: {
openInTerminal: true,
waitForExit: true,
cwd,
timeout: timeoutMs
},
skipBinaryDownload: true
})
attempts.push({
attempt,
timeoutMs,
durationMs: Date.now() - attemptStartedAt,
status: 'success'
})
return {
success: true,
commandSucceeded: true,
stdout:
'Command executed in a visible terminal. Review that terminal for command output.',
stderr: '',
returncode: 0,
command,
attempts
}
}
const resultOutput = await super.executeCommand({
binaryName: shellInvocation.binaryName,
args: shellInvocation.args,
options: {
sync: false,
cwd,
timeout: timeoutMs
},
skipBinaryDownload: true
})
attempts.push({
attempt,
timeoutMs,
durationMs: Date.now() - attemptStartedAt,
status: 'success'
})
return {
success: true,
commandSucceeded: true,
stdout: resultOutput.trim(),
stderr: '',
returncode: 0,
command,
attempts
}
} catch (error: unknown) {
const errorMessage = (error as Error).message
const processError = ShellTool.readProcessError(error)
const timedOut = ShellTool.isTimeoutErrorMessage(errorMessage)
const durationMs = Date.now() - attemptStartedAt
if (timedOut) {
const timeoutMessage = `Command timed out after ${ShellTool.formatTimeoutMs(timeoutMs)} (1 attempt)`
return {
success: false,
error: timeoutMessage,
stdout: '',
stderr: timeoutMessage,
returncode: -1,
command,
attempts: [
...attempts,
{
attempt,
timeoutMs,
durationMs,
status: 'timeout',
error: timeoutMessage
}
]
}
}
if (
errorMessage.includes('failed with exit code') ||
processError.exitCode !== -1
) {
const exitCodeMatch = errorMessage.match(/exit code (\d+)/)
const exitCode =
processError.exitCode !== -1
? processError.exitCode
: exitCodeMatch && exitCodeMatch[1]
? parseInt(exitCodeMatch[1], 10)
: -1
const stderrMatch = errorMessage.match(/exit code \d+: ([\s\S]*)$/)
const parsedErrorOutput =
stderrMatch && stderrMatch[1] ? stderrMatch[1].trim() : errorMessage
const stderr =
processError.stderr || (processError.stdout ? '' : parsedErrorOutput)
const failureOutput = ShellTool.joinOutput([
processError.stdout,
stderr
]) || errorMessage
const attemptResult: ShellAttempt = {
attempt,
timeoutMs,
durationMs,
status: 'error'
}
attemptResult.error = failureOutput
attempts.push(attemptResult)
return {
success: false,
commandSucceeded: false,
error: requiresVisibleTerminal
? `Command failed in the visible terminal with exit code ${exitCode}. Review that terminal for details.`
: failureOutput,
stdout: processError.stdout,
stderr: requiresVisibleTerminal
? `Command failed in the visible terminal with exit code ${exitCode}. Review that terminal for details.`
: stderr,
returncode: exitCode,
command,
attempts
}
}
const failureOutput = ShellTool.joinOutput([
processError.stdout,
processError.stderr
]) || errorMessage
attempts.push({
attempt,
timeoutMs,
durationMs,
status: 'error',
error: failureOutput
})
return {
success: false,
error: failureOutput,
stdout: processError.stdout,
stderr: processError.stderr || errorMessage,
returncode: -1,
command,
attempts
}
}
}
private static getShellInvocation(command: string): ShellInvocation {
if (isWindows()) {
const trimmedCommand = command.trim()
if (
trimmedCommand.toLowerCase().endsWith('.ps1') &&
!/\s/.test(trimmedCommand)
) {
return {
binaryName: 'powershell.exe',
args: [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-File',
trimmedCommand
]
}
}
return {
binaryName: 'powershell.exe',
args: [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
command
]
}
}
return {
binaryName: 'bash',
args: ['-c', command]
}
}
private static getTimeoutMs(options: ExecuteOptions): number {
const timeoutSeconds = options.longRunning
? LONG_RUNNING_TIMEOUT_SECONDS
: DEFAULT_TIMEOUT_SECONDS
return timeoutSeconds * MILLISECONDS_PER_SECOND
}
private static formatTimeoutMs(timeoutMs: number): string {
const seconds = timeoutMs / MILLISECONDS_PER_SECOND
if (Number.isInteger(seconds)) {
return `${seconds} seconds`
}
return `${timeoutMs}ms`
}
private static isTimeoutErrorMessage(errorMessage: string): boolean {
const normalizedErrorMessage = errorMessage.toLowerCase()
return (
normalizedErrorMessage.includes('timed out') ||
normalizedErrorMessage.includes('timeout') ||
normalizedErrorMessage.includes('etimedout')
)
}
private static readProcessError(error: unknown): {
stdout: string
stderr: string
exitCode: number
} {
const processError = error as ProcessExecutionError
const exitCode =
typeof processError.status === 'number'
? processError.status
: typeof processError.code === 'number'
? processError.code
: -1
return {
stdout: ShellTool.toOutputString(processError.stdout),
stderr: ShellTool.toOutputString(processError.stderr),
exitCode
}
}
private static toOutputString(output?: Buffer | string): string {
if (!output) {
return ''
}
return output.toString().trim()
}
private static joinOutput(outputs: string[]): string {
return outputs
.map((output) => output.trim())
.filter((output) => output.length > 0)
.join('\n')
}
async isSafeCommand(command: string): Promise<boolean> {
const commandLower = command.toLowerCase()
const tokens = this.tokenizeCommand(commandLower)
for (const pattern of UNSAFE_COMMAND_PATTERNS) {
if (commandLower.includes(pattern)) {
return false
}
}
if (
this.hasAnyTokenSequence(tokens, CRITICAL_COMMAND_SEQUENCES) ||
this.hasCommandToken(tokens, CRITICAL_COMMAND_TOKENS) ||
this.hasCommandToken(tokens, POWERSHELL_HIGH_RISK_COMMAND_TOKENS) ||
this.hasDangerousPowerShellRemovePattern(tokens) ||
this.hasDangerousDdPattern(tokens) ||
this.hasEvalDownloadPattern(tokens)
) {
return false
}
if (this.isDownloadPipedToShell(commandLower)) {
return false
}
return true
}
async getCommandRiskLevel(command: string): Promise<string> {
const commandLower = command.toLowerCase()
const tokens = this.tokenizeCommand(commandLower)
let riskLevel = 'low'
if (
this.hasAnyTokenSequence(tokens, CRITICAL_COMMAND_SEQUENCES) ||
this.hasCommandToken(tokens, CRITICAL_COMMAND_TOKENS) ||
this.hasDangerousPowerShellRemovePattern(tokens)
) {
riskLevel = 'critical'
}
if (riskLevel === 'low') {
if (
this.hasCommandToken(tokens, POWERSHELL_HIGH_RISK_COMMAND_TOKENS) ||
this.hasDangerousDdPattern(tokens) ||
this.hasEvalDownloadPattern(tokens)
) {
riskLevel = 'high'
}
}
if (riskLevel === 'low' && this.isDownloadPipedToShell(commandLower)) {
riskLevel = 'high'
}
if (riskLevel === 'low') {
for (const pattern of MEDIUM_RISK_COMMAND_PATTERNS) {
if (commandLower.includes(pattern)) {
riskLevel = 'medium'
break
}
}
}
return riskLevel
}
async getRiskDescription(command: string): Promise<string> {
const riskLevel = await this.getCommandRiskLevel(command)
const commandLower = command.toLowerCase()
const tokens = this.tokenizeCommand(commandLower)
if (this.hasCommandToken(tokens, ['rm'])) {
return 'delete files or directories permanently'
} else if (
this.hasCommandToken(tokens, [
...ELEVATED_COMMAND_TOKENS,
...POWERSHELL_ELEVATED_COMMAND_TOKENS
])
) {
return 'make system-level changes with elevated privileges'
} else if (
this.hasCommandToken(tokens, ['kill', 'stop-process'])
) {
return 'terminate running processes'
} else if (this.hasDangerousPowerShellRemovePattern(tokens)) {
return 'delete files or directories recursively'
} else if (this.hasCommandToken(tokens, PERMISSION_COMMAND_TOKENS)) {
return 'change file permissions or ownership'
} else if (
this.hasCommandToken(tokens, PACKAGE_MANAGER_COMMAND_TOKENS)
) {
return 'install or modify system packages'
} else if (this.isDownloadPipedToShell(commandLower)) {
return 'download remote content and execute it as a shell script'
} else if (this.hasCommandToken(tokens, HIGH_RISK_EVAL_DOWNLOAD_TOKENS)) {
return 'download content from the internet'
} else {
const descriptions: Record<string, string> = {
critical: 'cause severe system damage',
high: 'cause significant system changes',
medium: 'modify your system',
low: 'perform system operations'
}
return descriptions[riskLevel] || 'affect your system'
}
}
private async resolveCommandForSafetyAnalysis(command: string): Promise<string> {
const trimmedCommand = command.trim()
if (!trimmedCommand || /\s/.test(trimmedCommand)) {
return command
}
const resolvedPath = path.resolve(trimmedCommand)
try {
const stats = await fs.promises.stat(resolvedPath)
if (!stats.isFile()) {
return command
}
const fileContent = await fs.promises.readFile(resolvedPath, 'utf8')
if (!fileContent.trim()) {
return command
}
return fileContent
} catch {
return command
}
}
private isDownloadPipedToShell(commandLower: string): boolean {
const downloadsRemoteContent =
this.hasCommandToken(this.tokenizeCommand(commandLower), ['curl', 'wget'])
const pipesToShell =
commandLower.includes('| bash') ||
commandLower.includes('| sh') ||
commandLower.includes('| iex') ||
commandLower.includes('| invoke-expression')
return downloadsRemoteContent && pipesToShell
}
private tokenizeCommand(command: string): string[] {
const tokens: string[] = []
let currentToken = ''
let quote: '\'' | '"' | null = null
let escaped = false
const flushToken = (): void => {
if (!currentToken) {
return
}
tokens.push(currentToken)
currentToken = ''
}
for (const char of command) {
if (quote) {
if (escaped) {
currentToken += char
escaped = false
continue
}
if (char === '\\' && quote === '"') {
escaped = true
continue
}
if (char === quote) {
quote = null
continue
}
currentToken += char
continue
}
if (char === '\'' || char === '"') {
quote = char
continue
}
if (
char === '\n' ||
char === ';' ||
char === '|' ||
char === '&' ||
char === ' ' ||
char === '\t' ||
char === '\r' ||
char === '>' ||
char === '<'
) {
flushToken()
continue
}
currentToken += char
}
flushToken()
return tokens
}
private hasTokenSequence(tokens: string[], sequence: string[]): boolean {
if (sequence.length === 0 || tokens.length < sequence.length) {
return false
}
for (let index = 0; index <= tokens.length - sequence.length; index += 1) {
const matches = sequence.every(
(token, offset) => tokens[index + offset] === token
)
if (matches) {
return true
}
}
return false
}
private hasCommandToken(tokens: string[], commands: readonly string[]): boolean {
return tokens.some((token) => {
const normalizedToken = this.normalizeCommandToken(token)
return commands.some(
(command) =>
normalizedToken === command || normalizedToken.startsWith(`${command}.`)
)
})
}
private hasDangerousPowerShellRemovePattern(tokens: string[]): boolean {
if (!this.hasCommandToken(tokens, POWERSHELL_DESTRUCTIVE_COMMAND_TOKENS)) {
return false
}
return tokens.some((token) =>
['-recurse', '-r', '/s'].includes(token)
)
}
private hasDangerousDdPattern(tokens: string[]): boolean {
if (!this.hasCommandToken(tokens, HIGH_RISK_DD_TOKENS)) {
return false
}
return tokens.some((token) => token.startsWith('if='))
}
private hasEvalDownloadPattern(tokens: string[]): boolean {
for (let index = 0; index < tokens.length - 1; index += 1) {
if (tokens[index] !== 'eval') {
continue
}
const nextToken = tokens[index + 1] || ''
if (
HIGH_RISK_EVAL_DOWNLOAD_TOKENS.some((token) =>
nextToken.startsWith(`$(${token}`)
)
) {
return true
}
}
return false
}
private normalizeCommandToken(token: string): string {
const strippedToken = token.replace(/^[([{]+|[)\]}]+$/g, '')
if (strippedToken.includes('/')) {
return strippedToken.split('/').pop() || strippedToken
}
if (strippedToken.includes('\\')) {
return strippedToken.split('\\').pop() || strippedToken
}
return strippedToken
}
private hasAnyTokenSequence(
tokens: string[],
sequences: readonly (readonly string[])[]
): boolean {
return sequences.some((sequence) =>
this.hasTokenSequence(tokens, [...sequence])
)
}
private requiresVisibleTerminal(command: string): boolean {
let currentToken = ''
let quote: '\'' | '"' | null = null
let atCommandStart = true
let escaped = false
const flushToken = (): boolean => {
if (!currentToken) {
return false
}
const token = currentToken
currentToken = ''
if (!atCommandStart) {
return false
}
if (this.isShellAssignment(token) || TERMINAL_AUTH_WRAPPERS.has(token)) {
return false
}
atCommandStart = false
return TERMINAL_AUTH_COMMANDS.has(token)
}
for (const char of command) {
if (quote) {
if (escaped) {
escaped = false
continue
}
if (char === '\\' && quote === '"') {
escaped = true
continue
}
if (char === quote) {
quote = null
}
continue
}
if (char === '\'' || char === '"') {
quote = char
continue
}
if (char === '\n' || char === ';' || char === '|' || char === '&') {
if (flushToken()) {
return true
}
atCommandStart = true
continue
}
if (char === ' ' || char === '\t' || char === '\r') {
if (flushToken()) {
return true
}
continue
}
currentToken += char
}
return flushToken()
}
private isShellAssignment(token: string): boolean {
const separatorIndex = token.indexOf('=')
if (separatorIndex <= 0) {
return false
}
return !token.slice(0, separatorIndex).includes('/')
}
}
@@ -0,0 +1,597 @@
import os
import re
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
from bridges.python.src.sdk.base_tool import BaseTool, ExecuteCommandOptions
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
DEFAULT_SETTINGS: Dict[str, Any] = {}
REQUIRED_SETTINGS: List[str] = []
DEFAULT_TIMEOUT_SECONDS = 30
LONG_RUNNING_TIMEOUT_SECONDS = 86_400
MILLISECONDS_PER_SECOND = 1_000
CRITICAL_COMMAND_SEQUENCES: Sequence[Sequence[str]] = (
("rm", "-rf", "/"),
("rm", "-rf", "/*"),
("kill", "-9", "-1"),
)
CRITICAL_COMMAND_TOKENS: Sequence[str] = ("mkfs", "format", "fdisk")
HIGH_RISK_DD_TOKENS: Sequence[str] = ("dd",)
HIGH_RISK_EVAL_DOWNLOAD_TOKENS: Sequence[str] = ("curl", "wget")
ELEVATED_COMMAND_TOKENS: Sequence[str] = ("sudo", "doas", "pkexec", "su")
PERMISSION_COMMAND_TOKENS: Sequence[str] = ("chmod", "chown")
PACKAGE_MANAGER_COMMAND_TOKENS: Sequence[str] = (
"apt",
"apt-get",
"yum",
"brew",
"pip",
"pip3",
)
MEDIUM_RISK_COMMAND_PATTERNS: Sequence[str] = ()
UNSAFE_COMMAND_PATTERNS: Sequence[str] = (
"fork()",
"while true; do",
)
TERMINAL_AUTH_COMMANDS = set(ELEVATED_COMMAND_TOKENS)
TERMINAL_AUTH_WRAPPERS = {"env", "command", "builtin", "nohup", "time"}
class ShellTool(BaseTool):
TOOLKIT = "operating_system_control"
def __init__(self):
super().__init__()
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
self.settings = ToolkitConfig.load_tool_settings(
self.TOOLKIT, self.tool_name, DEFAULT_SETTINGS
)
self.required_settings = REQUIRED_SETTINGS
self._check_required_settings(self.tool_name)
@property
def tool_name(self) -> str:
return "shell"
@property
def toolkit(self) -> str:
return self.TOOLKIT
@property
def description(self) -> str:
return self.config["description"]
def execute_command(
self,
command: str,
cwd: Optional[str] = None,
long_running: bool = False,
capture_output: bool = True,
) -> Dict[str, Any]:
analyzed_command = self._resolve_command_for_safety_analysis(command)
is_safe = self.is_safe_command(analyzed_command)
if not is_safe:
risk_level = self.get_command_risk_level(analyzed_command)
risk_description = self.get_risk_description(analyzed_command)
return {
"success": False,
"stdout": "",
"stderr": f"Blocked unsafe shell command ({risk_level} risk): This command may {risk_description}.",
"returncode": -1,
"command": command,
"attempts": [],
}
binary_name, args = self._get_shell_invocation(command)
requires_visible_terminal = self._requires_visible_terminal(analyzed_command)
timeout_seconds = self._get_timeout_seconds(long_running)
timeout_milliseconds = int(timeout_seconds * MILLISECONDS_PER_SECOND)
attempts: List[Dict[str, Any]] = []
attempt = 1
attempt_started_at = self._now_milliseconds()
try:
if requires_visible_terminal:
self.report("bridges.tools.command_requires_terminal_auth")
super().execute_command(
ExecuteCommandOptions(
binary_name=binary_name,
args=args,
options={
"open_in_terminal": True,
"wait_for_exit": True,
"cwd": cwd or os.getcwd(),
"timeout": timeout_milliseconds,
},
skip_binary_download=True,
)
)
attempts.append(
self._build_attempt(
attempt,
timeout_milliseconds,
attempt_started_at,
"success",
)
)
return {
"success": True,
"commandSucceeded": True,
"stdout": "Command executed in a visible terminal. Review that terminal for command output.",
"stderr": "",
"returncode": 0,
"command": command,
"attempts": attempts,
}
result_output = super().execute_command(
ExecuteCommandOptions(
binary_name=binary_name,
args=args,
options={
"sync": False,
"cwd": cwd or os.getcwd(),
"timeout": timeout_seconds,
},
skip_binary_download=True,
)
)
return {
"success": True,
"commandSucceeded": True,
"stdout": result_output.strip(),
"stderr": "",
"returncode": 0,
"command": command,
"attempts": attempts
+ [
self._build_attempt(
attempt,
timeout_milliseconds,
attempt_started_at,
"success",
)
],
}
except Exception as error:
error_message = str(error)
timed_out = self._is_timeout_error_message(error_message)
duration_milliseconds = self._elapsed_milliseconds(attempt_started_at)
if timed_out:
timeout_message = (
f"Command timed out after "
f"{self._format_timeout_seconds(timeout_seconds)} "
"(1 attempt)"
)
return {
"success": False,
"stdout": "",
"stderr": timeout_message,
"returncode": -1,
"command": command,
"attempts": attempts
+ [
{
"attempt": attempt,
"timeoutMs": timeout_milliseconds,
"durationMs": duration_milliseconds,
"status": "timeout",
"error": timeout_message,
}
],
}
if "failed with exit code" in error_message:
exit_code_match = re.search(r"exit code (\d+)", error_message)
exit_code = int(exit_code_match.group(1)) if exit_code_match else -1
stderr_match = re.search(r"exit code \d+: ([\s\S]*)$", error_message)
stderr = stderr_match.group(1) if stderr_match else error_message
failure_output = (
f"Command failed in the visible terminal with exit code {exit_code}. Review that terminal for details."
if requires_visible_terminal
else stderr
)
attempt_result = {
"attempt": attempt,
"timeoutMs": timeout_milliseconds,
"durationMs": duration_milliseconds,
"status": "error",
"error": failure_output,
}
attempts.append(attempt_result)
return {
"success": False,
"commandSucceeded": False,
"stdout": "",
"stderr": failure_output,
"returncode": exit_code,
"command": command,
"attempts": attempts,
}
attempts.append(
{
"attempt": attempt,
"timeoutMs": timeout_milliseconds,
"durationMs": duration_milliseconds,
"status": "error",
"error": error_message,
}
)
return {
"success": False,
"stdout": "",
"stderr": error_message,
"returncode": -1,
"command": command,
"attempts": attempts,
}
@staticmethod
def _now_milliseconds() -> int:
return int(time.time() * MILLISECONDS_PER_SECOND)
@staticmethod
def _elapsed_milliseconds(started_at: int) -> int:
return max(ShellTool._now_milliseconds() - started_at, 0)
@staticmethod
def _build_attempt(
attempt: int,
timeout_milliseconds: int,
started_at: int,
status: str,
error: Optional[str] = None,
) -> Dict[str, Any]:
result: Dict[str, Any] = {
"attempt": attempt,
"timeoutMs": timeout_milliseconds,
"durationMs": ShellTool._elapsed_milliseconds(started_at),
"status": status,
}
if error:
result["error"] = error
return result
@staticmethod
def _get_shell_invocation(command: str) -> tuple[str, List[str]]:
if os.name == "nt":
trimmed_command = command.strip()
if trimmed_command.lower().endswith(".ps1") and not any(
char.isspace() for char in trimmed_command
):
return (
"powershell.exe",
[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
trimmed_command,
],
)
return (
"powershell.exe",
[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
command,
],
)
return "bash", ["-c", command]
@staticmethod
def _get_timeout_seconds(long_running: bool) -> float:
return float(
LONG_RUNNING_TIMEOUT_SECONDS
if long_running
else DEFAULT_TIMEOUT_SECONDS
)
@staticmethod
def _format_timeout_seconds(timeout_seconds: float) -> str:
if timeout_seconds.is_integer():
return f"{int(timeout_seconds)} seconds"
return f"{timeout_seconds:.3f} seconds"
@staticmethod
def _is_timeout_error_message(error_message: str) -> bool:
normalized_error_message = error_message.lower()
return (
"timed out" in normalized_error_message
or "timeout" in normalized_error_message
or "etimedout" in normalized_error_message
)
def is_safe_command(self, command: str) -> bool:
command_lower = command.lower()
tokens = self._tokenize_command(command_lower)
for pattern in UNSAFE_COMMAND_PATTERNS:
if pattern in command_lower:
return False
if (
self._has_any_token_sequence(tokens, CRITICAL_COMMAND_SEQUENCES)
or self._has_command_token(tokens, CRITICAL_COMMAND_TOKENS)
or self._has_dangerous_dd_pattern(tokens)
or self._has_eval_download_pattern(tokens)
):
return False
if self._is_download_piped_to_shell(command_lower):
return False
return True
def get_command_risk_level(self, command: str) -> str:
command_lower = command.lower()
tokens = self._tokenize_command(command_lower)
risk_level = "low"
if self._has_any_token_sequence(
tokens, CRITICAL_COMMAND_SEQUENCES
) or self._has_command_token(tokens, CRITICAL_COMMAND_TOKENS):
risk_level = "critical"
if risk_level == "low":
if self._has_dangerous_dd_pattern(
tokens
) or self._has_eval_download_pattern(tokens):
risk_level = "high"
if risk_level == "low" and self._is_download_piped_to_shell(command_lower):
risk_level = "high"
if risk_level == "low":
for pattern in MEDIUM_RISK_COMMAND_PATTERNS:
if pattern in command_lower:
risk_level = "medium"
break
return risk_level
def get_risk_description(self, command: str) -> str:
risk_level = self.get_command_risk_level(command)
command_lower = command.lower()
tokens = self._tokenize_command(command_lower)
if self._has_command_token(tokens, ("rm",)):
return "delete files or directories permanently"
if self._has_command_token(tokens, ELEVATED_COMMAND_TOKENS):
return "make system-level changes with elevated privileges"
if self._has_command_token(tokens, ("kill",)):
return "terminate running processes"
if self._has_command_token(tokens, PERMISSION_COMMAND_TOKENS):
return "change file permissions or ownership"
if self._has_command_token(tokens, PACKAGE_MANAGER_COMMAND_TOKENS):
return "install or modify system packages"
if self._is_download_piped_to_shell(command_lower):
return "download remote content and execute it as a shell script"
if self._has_command_token(tokens, HIGH_RISK_EVAL_DOWNLOAD_TOKENS):
return "download content from the internet"
descriptions = {
"critical": "cause severe system damage",
"high": "cause significant system changes",
"medium": "modify your system",
"low": "perform system operations",
}
return descriptions.get(risk_level, "affect your system")
def _resolve_command_for_safety_analysis(self, command: str) -> str:
trimmed_command = command.strip()
if not trimmed_command or any(char.isspace() for char in trimmed_command):
return command
resolved_path = Path(trimmed_command).expanduser().resolve()
try:
if not resolved_path.is_file():
return command
file_content = resolved_path.read_text(encoding="utf-8")
if not file_content.strip():
return command
return file_content
except Exception:
return command
def _is_download_piped_to_shell(self, command_lower: str) -> bool:
downloads_remote_content = self._has_command_token(
self._tokenize_command(command_lower), ("curl", "wget")
)
pipes_to_shell = "| bash" in command_lower or "| sh" in command_lower
return downloads_remote_content and pipes_to_shell
def _tokenize_command(self, command: str) -> List[str]:
tokens: List[str] = []
current_token = ""
quote: Optional[str] = None
escaped = False
def flush_token() -> None:
nonlocal current_token
if not current_token:
return
tokens.append(current_token)
current_token = ""
for char in command:
if quote:
if escaped:
current_token += char
escaped = False
continue
if char == "\\" and quote == '"':
escaped = True
continue
if char == quote:
quote = None
continue
current_token += char
continue
if char in ("'", '"'):
quote = char
continue
if char in ("\n", ";", "|", "&", " ", "\t", "\r", ">", "<"):
flush_token()
continue
current_token += char
flush_token()
return tokens
def _has_token_sequence(
self, tokens: Sequence[str], sequence: Sequence[str]
) -> bool:
if len(sequence) == 0 or len(tokens) < len(sequence):
return False
for index in range(len(tokens) - len(sequence) + 1):
matches = all(
tokens[index + offset] == token
for offset, token in enumerate(sequence)
)
if matches:
return True
return False
def _has_command_token(
self, tokens: Sequence[str], commands: Sequence[str]
) -> bool:
for token in tokens:
normalized_token = self._normalize_command_token(token)
for command in commands:
if normalized_token == command or normalized_token.startswith(
f"{command}."
):
return True
return False
def _has_dangerous_dd_pattern(self, tokens: Sequence[str]) -> bool:
if not self._has_command_token(tokens, HIGH_RISK_DD_TOKENS):
return False
return any(token.startswith("if=") for token in tokens)
def _has_eval_download_pattern(self, tokens: Sequence[str]) -> bool:
for index in range(len(tokens) - 1):
if tokens[index] != "eval":
continue
next_token = tokens[index + 1]
if any(
next_token.startswith(f"$({token}")
for token in HIGH_RISK_EVAL_DOWNLOAD_TOKENS
):
return True
return False
def _normalize_command_token(self, token: str) -> str:
stripped_token = re.sub(r"^[([{]+|[)\]}]+$", "", token)
if "/" in stripped_token:
return stripped_token.split("/")[-1] or stripped_token
return stripped_token
def _has_any_token_sequence(
self, tokens: Sequence[str], sequences: Sequence[Sequence[str]]
) -> bool:
return any(self._has_token_sequence(tokens, sequence) for sequence in sequences)
def _requires_visible_terminal(self, command: str) -> bool:
current_token = ""
quote: Optional[str] = None
at_command_start = True
escaped = False
def flush_token() -> bool:
nonlocal current_token, at_command_start
if not current_token:
return False
token = current_token
current_token = ""
if not at_command_start:
return False
if self._is_shell_assignment(token) or token in TERMINAL_AUTH_WRAPPERS:
return False
at_command_start = False
return token in TERMINAL_AUTH_COMMANDS
for char in command:
if quote:
if escaped:
escaped = False
continue
if char == "\\" and quote == '"':
escaped = True
continue
if char == quote:
quote = None
continue
if char in ("'", '"'):
quote = char
continue
if char in ("\n", ";", "|", "&"):
if flush_token():
return True
at_command_start = True
continue
if char in (" ", "\t", "\r"):
if flush_token():
return True
continue
current_token += char
return flush_token()
def _is_shell_assignment(self, token: str) -> bool:
separator_index = token.find("=")
if separator_index <= 0:
return False
return "/" not in token[:separator_index]