@miphamai/cli 0.20.1 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -271,8 +271,10 @@ export class SubAgent {
271
271
  continue
272
272
  }
273
273
 
274
- // Security: check permission before executing
275
- // Sub-agents run without user interaction — tools requiring approval are rejected
274
+ // Security: check permission before executing.
275
+ // Sub-agents run without user interaction — tools requiring approval are rejected.
276
+ // When permission system is absent (undefined), allow all tools (backward compat
277
+ // for tests and headless usage). When present, always enforce approval checks.
276
278
  if (this.permission?.needsApproval(tool, tu.input)) {
277
279
  currentMessages.push({
278
280
  role: 'user' as const,
@@ -561,11 +561,14 @@ export class QueryEngine {
561
561
  private async runScriptVerification(): Promise<boolean> {
562
562
  if (!this.goalVerifyScript) return false
563
563
  try {
564
- const { execSync } = await import('node:child_process')
565
- execSync(this.goalVerifyScript, { timeout: 30000, stdio: 'pipe' })
566
- return true // exit code 0 = success
564
+ const { spawnSync } = await import('node:child_process')
565
+ const { resolveSafe } = await import('../security/path')
566
+ // Validate the script path is within the workspace before execution
567
+ const safePath = resolveSafe(process.cwd(), this.goalVerifyScript)
568
+ const result = spawnSync(safePath, [], { timeout: 30000, stdio: 'pipe' })
569
+ return result.status === 0 // exit code 0 = success
567
570
  } catch {
568
- return false // non-zero exit = not yet achieved
571
+ return false // non-zero exit or path rejection = not yet achieved
569
572
  }
570
573
  }
571
574
 
@@ -797,7 +800,11 @@ export class QueryEngine {
797
800
 
798
801
  return result
799
802
  } catch (err) {
800
- return { success: false, content: '', error: String(err) }
803
+ // Sanitize error: strip stack traces and internal paths to prevent
804
+ // information disclosure to the LLM conversation context.
805
+ const message =
806
+ err instanceof Error ? err.message : String(err).split('\n')[0] || 'Unknown error'
807
+ return { success: false, content: '', error: `Tool execution failed: ${message}` }
801
808
  }
802
809
  }
803
810
 
@@ -1,4 +1,4 @@
1
- import { execSync } from 'node:child_process'
1
+ import { spawnSync } from 'node:child_process'
2
2
  import type { HookConfig, HookContext, HookResult } from '../shared/index.ts'
3
3
 
4
4
  /**
@@ -36,21 +36,22 @@ function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
36
36
  try {
37
37
  const args = cfg.args ? cfg.args.map((a) => substituteVars(a, ctx)) : []
38
38
 
39
- const cmd = [cfg.command, ...args].join(' ')
40
-
41
- execSync(cmd, {
39
+ // Use spawnSync with array args — no shell, no command injection
40
+ const result = spawnSync(cfg.command, args, {
42
41
  timeout: 30_000,
43
42
  encoding: 'utf-8',
44
43
  stdio: ['ignore', 'pipe', 'pipe'],
45
44
  })
46
45
 
47
46
  // Exit code 0 = success, allow
48
- return { allowed: true }
49
- } catch (err) {
50
- const stderr = (err as { stderr?: string }).stderr || String(err)
47
+ if (result.status === 0) {
48
+ return { allowed: true }
49
+ }
50
+
51
+ // Non-zero exit: check for block signal (exit code 2)
52
+ const stderr = result.stderr?.toString() || ''
51
53
 
52
- // Exit code 2 = block with reason
53
- if ((err as { status?: number }).status === 2) {
54
+ if (result.status === 2) {
54
55
  return {
55
56
  allowed: false,
56
57
  reason: stderr.trim() || 'Blocked by hook',
@@ -63,6 +64,15 @@ function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
63
64
  allowed: true,
64
65
  additionalContext: `Hook warning (${cfg.command}): ${stderr.trim()}`,
65
66
  }
67
+ } catch (err) {
68
+ // spawnSync errors (e.g., command not found, timeout, signal)
69
+ const message = (err as { message?: string }).message || String(err)
70
+
71
+ // Timeout/signal: treat as non-blocking warning
72
+ return {
73
+ allowed: true,
74
+ additionalContext: `Hook error (${cfg.command}): ${message}`,
75
+ }
66
76
  }
67
77
  }
68
78
 
@@ -42,6 +42,21 @@ export const webFetchTool: ToolDefinition = {
42
42
  error: `HTTP ${response.status}: ${response.statusText}`,
43
43
  }
44
44
  }
45
+
46
+ // SSRF defense: re-validate the resolved URL after redirects.
47
+ // `redirect: 'follow'` may have landed on an internal IP — check again.
48
+ // Guard: response.url may not exist in some environments (e.g. mocked fetch).
49
+ if (response.url) {
50
+ const redirectError = validateUrl(response.url)
51
+ if (redirectError) {
52
+ return {
53
+ success: false,
54
+ content: '',
55
+ error: `Redirect blocked: ${redirectError}`,
56
+ }
57
+ }
58
+ }
59
+
45
60
  const html = await response.text()
46
61
  // Simple HTML-to-text extraction
47
62
  const text = html
@@ -2921,16 +2921,15 @@ const forkCmd: CommandHandler = async (ctx, args) => {
2921
2921
  }
2922
2922
 
2923
2923
  async function openBrowser(url: string): Promise<void> {
2924
- const cmd =
2924
+ const { spawn } = await import('node:child_process')
2925
+ // Use spawn with array args — no shell, no command injection
2926
+ const [cmd, ...args] =
2925
2927
  process.platform === 'darwin'
2926
- ? `open "${url}"`
2928
+ ? ['open', url]
2927
2929
  : process.platform === 'win32'
2928
- ? `start "" "${url}"`
2929
- : `xdg-open "${url}"`
2930
- const { exec } = await import('node:child_process')
2931
- exec(cmd, () => {
2932
- /* fire-and-forget */
2933
- })
2930
+ ? ['cmd', '/c', 'start', '', url]
2931
+ : ['xdg-open', url]
2932
+ spawn(cmd!, args, { detached: true, stdio: 'ignore' }).unref()
2934
2933
  }
2935
2934
 
2936
2935
  const artifactOpenCmd: CommandHandler = async (ctx, args) => {