@miphamai/cli 0.20.1 → 0.20.3
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/dist/mipham +0 -0
- package/package.json +1 -1
- package/src/agent/sub-agent.ts +4 -2
- package/src/core/engine.ts +12 -5
- package/src/core/hooks-executor.ts +19 -9
- package/src/tools/network/web-fetch.ts +15 -0
- package/src/ui/commands.ts +7 -8
package/dist/mipham
ADDED
|
Binary file
|
package/package.json
CHANGED
package/src/agent/sub-agent.ts
CHANGED
|
@@ -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,
|
package/src/core/engine.ts
CHANGED
|
@@ -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 {
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
package/src/ui/commands.ts
CHANGED
|
@@ -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
|
|
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
|
-
?
|
|
2928
|
+
? ['open', url]
|
|
2927
2929
|
: process.platform === 'win32'
|
|
2928
|
-
?
|
|
2929
|
-
:
|
|
2930
|
-
|
|
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) => {
|