Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 221 additions & 9 deletions src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import { nanoid } from 'nanoid'
import { Sandbox } from '@e2b/code-interpreter'
import { RuntimeHelper } from '@/lib/runtimeHelper'

type ShellEnvironment = {
platform: 'windows' | 'mac' | 'linux'
shellName: 'powershell' | 'bash'
shellExecutable: string
buildArgs: (command: string) => string[]
promptHint: string
}
Comment on lines +16 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ShellEnvironment shape is sensible, but consider making promptHint deterministic (no trailing spaces) and future-proofing for more shells.
Minor nit: the Windows promptHint already includes a trailing space and is concatenated with another ' ' later, producing double-spacing.

🤖 Prompt for AI Agents
In src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts around
lines 16 to 22, remove trailing spaces from the promptHint values so they are
deterministic (no built-in space) and ensure any caller concatenates the
separating space when building prompts to avoid double-spacing; also make the
shellName type future-proof by widening the union to allow additional shells
(e.g., change "shellName: 'powershell' | 'bash'" to "shellName: 'powershell' |
'bash' | string" or simply "string") so new shell types can be added without
type changes.


// Schema 定义
const GetTimeArgsSchema = z.object({
offset: z
Expand All @@ -39,6 +47,26 @@ const RunNodeCodeArgsSchema = z.object({
.describe('Code execution timeout in milliseconds, default 5 seconds')
})

const RunShellCommandArgsSchema = z.object({
command: z
.string()
.min(1)
.describe(
'Shell command to execute. Provide the full command string including arguments and flags.'
),
timeout: z
.number()
.optional()
.default(60000)
.describe('Command execution timeout in milliseconds, defaults to 60 seconds.'),
workdir: z
.string()
.optional()
.describe(
'Optional working directory within the sandboxed temp area. If omitted, uses an isolated temporary directory under the application temp path.'
)
})

// E2B 代码执行 Schema
const E2BRunCodeArgsSchema = z.object({
code: z
Expand Down Expand Up @@ -72,19 +100,38 @@ const CODE_EXECUTION_FORBIDDEN_PATTERNS = [
/process\.env/gi
]

const SHELL_COMMAND_FORBIDDEN_PATTERNS = [
/\b(sudo|su)\b/gi,
/\brm\s+-[^\n]*\brf\b/gi,
/\b(del|erase|rmdir|rd)\b\s+\/[^\n]*\b(s|q)\b/gi,
/\b(shutdown|reboot|halt|poweroff)\b/gi,
/\b(mkfs|diskpart|format)\b/gi,
/:\s*\(\s*\)\s*{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/g
]

export class PowerpackServer {
private server: Server
private useE2B: boolean = false
private enableShellCommandTool: boolean = false
private e2bApiKey: string = ''
private readonly runtimeHelper = RuntimeHelper.getInstance()
private readonly shellEnvironment: ShellEnvironment
private readonly shellWorkdir: string

constructor(env?: Record<string, any>) {
// 从环境变量中获取 E2B 配置
this.parseE2BConfig(env)
this.parseShellCommandToolConfig(env)

// 查找内置的运行时路径
this.runtimeHelper.initializeRuntimes()

// 检测当前系统的 Shell 环境
this.shellEnvironment = this.detectShellEnvironment()

// 确保 Shell 使用的工作目录
this.shellWorkdir = this.ensureShellWorkdir()

// 创建服务器实例
this.server = new Server(
{
Expand Down Expand Up @@ -116,6 +163,36 @@ export class PowerpackServer {
}
}

private parseShellCommandToolConfig(env?: Record<string, any>): void {
const enableValue = env?.ENABLE_SHELL_COMMAND_TOOL ?? process.env.ENABLE_SHELL_COMMAND_TOOL
this.enableShellCommandTool = enableValue === true || enableValue === 'true'
}

private detectShellEnvironment(): ShellEnvironment {
if (process.platform === 'win32') {
return {
platform: 'windows',
shellName: 'powershell',
shellExecutable: 'powershell.exe',
buildArgs: (command) => ['-NoProfile', '-NonInteractive', '-Command', command],
promptHint:
'Windows environment detected. Commands run with PowerShell, you can use built-in cmdlets and scripts.'
}
}

const isMac = process.platform === 'darwin'

return {
platform: isMac ? 'mac' : 'linux',
shellName: 'bash',
shellExecutable: process.env.SHELL || '/bin/bash',
buildArgs: (command) => ['-c', command],
promptHint: isMac
? 'macOS environment detected. Commands run with bash; macOS utilities like osascript, open, defaults are available.'
: 'Linux environment detected. Commands run with bash and typical GNU utilities are available.'
}
}
Comment on lines +171 to +194
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure and see how detectShellEnvironment is used
cd src/main/presenter/mcpPresenter/inMemoryServers && cat -n powerpackServer.ts | head -200

Repository: ThinkInAIXYZ/deepchat

Length of output: 7150


🏁 Script executed:

# Search for other shell execution patterns in the codebase
rg "shellExecutable|buildArgs" -A 3 -B 3 src/main/

Repository: ThinkInAIXYZ/deepchat

Length of output: 3439


🏁 Script executed:

# Check if there's any fallback logic for shell detection elsewhere
rg "/bin/bash|/bin/sh|which bash" src/

Repository: ThinkInAIXYZ/deepchat

Length of output: 603


🏁 Script executed:

# Look for how these buildArgs are actually used
rg "buildArgs\(" src/main/

Repository: ThinkInAIXYZ/deepchat

Length of output: 214


🏁 Script executed:

# Check the runShellCommand method implementation to see error handling
rg -A 20 "private.*runShellCommand|async.*runShellCommand" src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 47


🏁 Script executed:

# Check if there's any existing validation/fallback for shell executable
rg -B 5 -A 10 "execFile.*shellExecutable" src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 529


🏁 Script executed:

# Look at how other shell helpers handle missing bash
cat -n src/main/presenter/llmProviderPresenter/agent/shellEnvHelper.ts | head -100

Repository: ThinkInAIXYZ/deepchat

Length of output: 3888


🏁 Script executed:

# Check what types of commands are executed through runShellCommand
rg -B 5 "runShellCommand" src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts | head -50

Repository: ThinkInAIXYZ/deepchat

Length of output: 47


🏁 Script executed:

# Search for where runShellCommand is called to understand the context
rg "runShellCommand\(" src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts -A 3 -B 3

Repository: ThinkInAIXYZ/deepchat

Length of output: 47


🏁 Script executed:

# Check if there are any explicit notes about why -lc is used
rg "-lc|login" src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts -B 5 -A 5

Repository: ThinkInAIXYZ/deepchat

Length of output: 469


🏁 Script executed:

# Check what tools/commands are defined in powerpackServer
grep -n "tool\|command" src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts | head -40

Repository: ThinkInAIXYZ/deepchat

Length of output: 1519


🏁 Script executed:

# Get a better picture of the tool definitions
sed -n '300,500p' src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 7825


Add security flags and fallback for shell executables.

For non-interactive command execution, avoid loading user profiles: use -NoProfile with PowerShell, -NonInteractive for consistency, and -c instead of -lc for bash (login shells load user profiles, causing non-deterministic behavior and potential security issues). Also add a fallback to process.env.SHELL before defaulting to /bin/bash, matching the pattern already used elsewhere in the codebase.

  private detectShellEnvironment(): ShellEnvironment {
    if (process.platform === 'win32') {
      return {
        platform: 'windows',
        shellName: 'powershell',
        shellExecutable: 'powershell.exe',
-        buildArgs: (command) => ['-Command', command],
+        buildArgs: (command) => ['-NoProfile', '-NonInteractive', '-Command', command],
         promptHint:
           'Windows environment detected. Commands run with PowerShell, you can use built-in cmdlets and scripts. '
       }
     }

     const isMac = process.platform === 'darwin'

     return {
       platform: isMac ? 'mac' : 'linux',
       shellName: 'bash',
-      shellExecutable: '/bin/bash',
+      shellExecutable: process.env.SHELL || '/bin/bash',
-      buildArgs: (command) => ['-lc', command],
+      buildArgs: (command) => ['-c', command],
       promptHint: isMac
         ? 'macOS environment detected. Commands run with bash; macOS utilities like osascript, open, defaults are available.'
         : 'Linux environment detected. Commands run with bash and typical GNU utilities are available.'
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private detectShellEnvironment(): ShellEnvironment {
if (process.platform === 'win32') {
return {
platform: 'windows',
shellName: 'powershell',
shellExecutable: 'powershell.exe',
buildArgs: (command) => ['-Command', command],
promptHint:
'Windows environment detected. Commands run with PowerShell, you can use built-in cmdlets and scripts. '
}
}
const isMac = process.platform === 'darwin'
return {
platform: isMac ? 'mac' : 'linux',
shellName: 'bash',
shellExecutable: '/bin/bash',
buildArgs: (command) => ['-lc', command],
promptHint: isMac
? 'macOS environment detected. Commands run with bash; macOS utilities like osascript, open, defaults are available.'
: 'Linux environment detected. Commands run with bash and typical GNU utilities are available.'
}
}
private detectShellEnvironment(): ShellEnvironment {
if (process.platform === 'win32') {
return {
platform: 'windows',
shellName: 'powershell',
shellExecutable: 'powershell.exe',
buildArgs: (command) => ['-NoProfile', '-NonInteractive', '-Command', command],
promptHint:
'Windows environment detected. Commands run with PowerShell, you can use built-in cmdlets and scripts. '
}
}
const isMac = process.platform === 'darwin'
return {
platform: isMac ? 'mac' : 'linux',
shellName: 'bash',
shellExecutable: process.env.SHELL || '/bin/bash',
buildArgs: (command) => ['-c', command],
promptHint: isMac
? 'macOS environment detected. Commands run with bash; macOS utilities like osascript, open, defaults are available.'
: 'Linux environment detected. Commands run with bash and typical GNU utilities are available.'
}
}


// 启动服务器
public async startServer(transport: Transport): Promise<void> {
this.server.connect(transport)
Expand All @@ -124,6 +201,7 @@ export class PowerpackServer {
// 检查代码的安全性
private checkCodeSafety(code: string): boolean {
for (const pattern of CODE_EXECUTION_FORBIDDEN_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(code)) {
return false
}
Expand All @@ -141,12 +219,12 @@ export class PowerpackServer {
// 所有平台都使用 Node.js
const nodeRuntimePath = this.runtimeHelper.getNodeRuntimePath()
if (!nodeRuntimePath) {
throw new Error('运行时未找到,无法执行代码')
throw new Error('Runtime not found; cannot execute code')
}

// 检查代码安全性
if (!this.checkCodeSafety(code)) {
throw new Error('代码包含不安全的操作,已被拒绝执行')
throw new Error('Code contains disallowed operations and was rejected')
}

// 创建临时文件
Expand Down Expand Up @@ -197,6 +275,104 @@ export class PowerpackServer {
}
}

private checkShellCommandSafety(command: string): void {
for (const pattern of SHELL_COMMAND_FORBIDDEN_PATTERNS) {
pattern.lastIndex = 0
if (pattern.test(command)) {
throw new Error('Shell command contains disallowed operations and was rejected')
}
}
}

private getRealPath(candidate: string): string {
try {
return fs.realpathSync(candidate)
} catch {
return path.resolve(candidate)
}
}

private resolveShellCwd(workdir?: string): string {
const requestedCwd = workdir || this.shellWorkdir
const resolvedCwd = this.getRealPath(path.resolve(requestedCwd))
const shellRoot = this.getRealPath(this.shellWorkdir)
const tempRoot = this.getRealPath(app.getPath('temp'))

const isWithin = (child: string, parent: string) =>
child === parent || child.startsWith(parent + path.sep)

if (!isWithin(resolvedCwd, shellRoot) && !isWithin(resolvedCwd, tempRoot)) {
throw new Error(`workdir must be within sandboxed temp directories: ${shellRoot}`)
}

if (!fs.existsSync(resolvedCwd) || !fs.statSync(resolvedCwd).isDirectory()) {
throw new Error(`workdir does not exist or is not a directory: ${resolvedCwd}`)
}

return resolvedCwd
}

private formatShellOutput(stdout?: string, stderr?: string, errorMessage?: string): string {
const outputParts: string[] = []
const trimmedStdout = stdout?.trim()
const trimmedStderr = stderr?.trim()

if (trimmedStdout) {
outputParts.push(`STDOUT:\n${trimmedStdout}`)
}

if (trimmedStderr) {
outputParts.push(`STDERR:\n${trimmedStderr}`)
}

if (errorMessage) {
const trimmedError = errorMessage.trim()
if (trimmedError) {
outputParts.push(`ERROR:\n${trimmedError}`)
}
}

return outputParts.join('\n\n') || 'Command executed with no output'
}

private async executeShellCommand(
command: string,
timeout: number,
workdir?: string
): Promise<string> {
this.checkShellCommandSafety(command)
const { shellExecutable, buildArgs } = this.shellEnvironment
const cwd = this.resolveShellCwd(workdir)

try {
const { stdout, stderr } = await promisify(execFile)(shellExecutable, buildArgs(command), {
timeout,
cwd,
windowsHide: true,
maxBuffer: 10 * 1024 * 1024
})

return this.formatShellOutput(stdout, stderr)
} catch (error) {
const stdout = typeof (error as any)?.stdout === 'string' ? (error as any).stdout : ''
const stderr = typeof (error as any)?.stderr === 'string' ? (error as any).stderr : ''
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(this.formatShellOutput(stdout, stderr, errorMessage))
}
}
Comment on lines +338 to +362
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

executeShellCommand() should preserve stdout/stderr on non-zero exit + set maxBuffer.
Today, command failures/timeouts throw and the outer catch only returns error.message, often losing stderr (the most useful part). Also, default maxBuffer can error on moderately large output.

   private async executeShellCommand(
     command: string,
     timeout: number,
     workdir?: string
   ): Promise<string> {
     const { shellExecutable, buildArgs } = this.shellEnvironment
-    const execPromise = promisify(execFile)(shellExecutable, buildArgs(command), {
-      timeout,
-      cwd: workdir || this.shellWorkdir,
-      windowsHide: true
-    })
-
-    const { stdout, stderr } = await execPromise
-    const outputParts: string[] = []
+    try {
+      const { stdout, stderr } = await promisify(execFile)(shellExecutable, buildArgs(command), {
+        timeout,
+        cwd: workdir || this.shellWorkdir,
+        windowsHide: true,
+        maxBuffer: 10 * 1024 * 1024
+      })
+
+      const outputParts: string[] = []
 
-    if (stdout) {
-      outputParts.push(stdout.trim())
-    }
+      if (stdout) outputParts.push(stdout.trim())
+      if (stderr) outputParts.push(`STDERR:\n${stderr.trim()}`)
 
-    if (stderr) {
-      outputParts.push(`STDERR:\n${stderr.trim()}`)
-    }
+      return outputParts.join('\n\n') || 'Command executed with no output'
+    } catch (error) {
+      const err = error as any
+      const stdout = typeof err?.stdout === 'string' ? err.stdout : ''
+      const stderr = typeof err?.stderr === 'string' ? err.stderr : ''
+      const outputParts: string[] = []
+      if (stdout) outputParts.push(stdout.trim())
+      if (stderr) outputParts.push(`STDERR:\n${stderr.trim()}`)
+      const detail = outputParts.join('\n\n')
+      const baseMessage = error instanceof Error ? error.message : String(error)
+      throw new Error(detail ? `${baseMessage}\n\n${detail}` : baseMessage)
+    }
-
-    return outputParts.join('\n\n') || 'Command executed with no output'
   }
🤖 Prompt for AI Agents
In src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts around
lines 261 to 285, update executeShellCommand so it sets a larger maxBuffer in
the execFile options (e.g. 10*1024*1024) and preserves stdout/stderr when the
child process exits non‑zero or times out: await the exec call inside a
try/catch, and on error extract and include error.stdout and error.stderr
(falling back to the error.message) in the returned/raised output instead of
dropping stderr; ensure cwd, timeout and windowsHide remain passed through and
return a combined string that contains trimmed stdout and stderr (with clear
labels) whether the command succeeded or failed.


private ensureShellWorkdir(): string {
const baseTempDir = app.getPath('temp')
const shellDirPrefix = path.join(baseTempDir, 'powerpack_shell_')

try {
return fs.mkdtempSync(shellDirPrefix)
} catch (error) {
console.error('Failed to ensure shell workdir, falling back to temp path:', error)
return baseTempDir
}
}

// 使用 E2B 执行代码
private async executeE2BCode(code: string): Promise<string> {
if (!this.useE2B) {
Expand Down Expand Up @@ -282,6 +458,19 @@ export class PowerpackServer {
}
]

if (this.enableShellCommandTool) {
const shellDescription =
`${this.shellEnvironment.promptHint} ` +
'Use this tool for day-to-day automation, file inspection, networking, and scripting. ' +
'Provide a full shell command string; output includes stdout and stderr. '

tools.push({
name: 'run_shell_command',
description: shellDescription,
inputSchema: zodToJsonSchema(RunShellCommandArgsSchema)
})
}

// 根据配置添加代码执行工具
if (this.useE2B) {
// 使用 E2B 执行代码
Expand Down Expand Up @@ -327,7 +516,7 @@ export class PowerpackServer {
case 'get_time': {
const parsed = GetTimeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`无效的时间参数: ${parsed.error}`)
throw new Error(`Invalid time arguments: ${parsed.error}`)
}

const { offset } = parsed.data
Expand All @@ -350,7 +539,7 @@ export class PowerpackServer {
case 'get_web_info': {
const parsed = GetWebInfoArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`无效的URL参数: ${parsed.error}`)
throw new Error(`Invalid URL arguments: ${parsed.error}`)
}

const { url } = parsed.data
Expand Down Expand Up @@ -383,6 +572,29 @@ export class PowerpackServer {
}
}

case 'run_shell_command': {
if (!this.enableShellCommandTool) {
throw new Error('run_shell_command tool is disabled by configuration')
}
const parsed = RunShellCommandArgsSchema.safeParse(args)

if (!parsed.success) {
throw new Error(`Invalid command arguments: ${parsed.error}`)
}

const { command, timeout, workdir } = parsed.data
const result = await this.executeShellCommand(command, timeout, workdir)

return {
content: [
{
type: 'text',
text: `Current shell environment: ${this.shellEnvironment.shellName}\n\nExecution result:\n${result}`
}
]
}
}

case 'run_code': {
// E2B 代码执行
if (!this.useE2B) {
Expand All @@ -391,7 +603,7 @@ export class PowerpackServer {

const parsed = E2BRunCodeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`无效的代码参数: ${parsed.error}`)
throw new Error(`Invalid code arguments: ${parsed.error}`)
}

const { code } = parsed.data
Expand All @@ -401,7 +613,7 @@ export class PowerpackServer {
content: [
{
type: 'text',
text: `代码执行结果 (E2B Sandbox):\n\n${result}`
text: `Code execution result (E2B Sandbox):\n\n${result}`
}
]
}
Expand All @@ -420,7 +632,7 @@ export class PowerpackServer {

const parsed = RunNodeCodeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`无效的代码参数: ${parsed.error}`)
throw new Error(`Invalid code arguments: ${parsed.error}`)
}

const { code, timeout } = parsed.data
Expand All @@ -430,7 +642,7 @@ export class PowerpackServer {
content: [
{
type: 'text',
text: `代码执行结果:\n\n${result}`
text: `Code execution result:\n\n${result}`
}
]
}
Expand All @@ -442,7 +654,7 @@ export class PowerpackServer {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: `错误: ${errorMessage}` }],
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/src/components/message/MessageBlockToolCall.vue
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ const parseJson = (jsonStr: string) => {
// Terminal detection
const isTerminalTool = computed(() => {
const name = props.block.tool_call?.name?.toLowerCase() || ''
const serverName = props.block.tool_call?.server_name?.toLowerCase() || ''
if (name == 'run_shell_command' && serverName === 'powerpack') {
return false
}
return name.includes('terminal') || name.includes('command') || name.includes('exec')
})

Expand Down