@ian2018cs/agenthub 0.1.59 → 0.1.60
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
package/server/claude-sdk.js
CHANGED
|
@@ -549,6 +549,16 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
549
549
|
}
|
|
550
550
|
};
|
|
551
551
|
}
|
|
552
|
+
// 路径重写:将 .claude 路径替换为用户专属目录后告知 SDK 使用新参数
|
|
553
|
+
if (guardResult.modifiedInput) {
|
|
554
|
+
return {
|
|
555
|
+
hookSpecificOutput: {
|
|
556
|
+
hookEventName: 'PreToolUse',
|
|
557
|
+
permissionDecision: 'allow',
|
|
558
|
+
updatedInput: guardResult.modifiedInput,
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
552
562
|
} catch (err) {
|
|
553
563
|
console.error(`[ToolGuard] Error evaluating ${hookInput.tool_name}:`, err.message);
|
|
554
564
|
// 守卫出错时不阻塞正常流程,降级为原有权限控制
|
|
@@ -7,27 +7,85 @@
|
|
|
7
7
|
|
|
8
8
|
import { evaluateRules } from './rules.js';
|
|
9
9
|
import { reviewWithLLM } from './llm-reviewer.js';
|
|
10
|
-
import {
|
|
10
|
+
import { DATA_DIR, getUserPaths } from '../user-directories.js';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
|
|
13
13
|
const VERBOSE = process.env.TOOL_GUARD_VERBOSE === 'true';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 将工具输入中的 .claude 路径重写为用户专属 claudeDir
|
|
17
|
+
*
|
|
18
|
+
* 处理以下模式(以便多用户环境下正确访问技能、配置等):
|
|
19
|
+
* ~/.claude/... → claudeDir/...
|
|
20
|
+
* $HOME/.claude/... → claudeDir/...
|
|
21
|
+
* ./.claude/... → claudeDir/...(相对路径,常见于技能脚本调用)
|
|
22
|
+
*
|
|
23
|
+
* @param {string} toolName
|
|
24
|
+
* @param {Record<string, unknown>} input
|
|
25
|
+
* @param {string} claudeDir - 用户的 .claude 绝对路径
|
|
26
|
+
* @returns {Record<string, unknown>|null} 有改动时返回新 input 对象,否则返回 null
|
|
27
|
+
*/
|
|
28
|
+
function rewriteClaudePaths(toolName, input, claudeDir) {
|
|
29
|
+
if (!input || !claudeDir) return null;
|
|
30
|
+
|
|
31
|
+
function replaceInStr(str) {
|
|
32
|
+
if (!str || typeof str !== 'string') return str;
|
|
33
|
+
let s = str;
|
|
34
|
+
// ~/.claude
|
|
35
|
+
s = s.replace(/~\/\.claude(\/|$)/g, `${claudeDir}$1`);
|
|
36
|
+
// $HOME/.claude
|
|
37
|
+
s = s.replace(/\$HOME\/\.claude(\/|$)/g, `${claudeDir}$1`);
|
|
38
|
+
// ./.claude(前面是行首、空白或引号)
|
|
39
|
+
s = s.replace(/(^|[\s"'])\.\/\.claude(\/|$)/g, `$1${claudeDir}$2`);
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const newInput = { ...input };
|
|
44
|
+
let modified = false;
|
|
45
|
+
|
|
46
|
+
if (['Read', 'Write', 'Edit'].includes(toolName) && typeof input.file_path === 'string') {
|
|
47
|
+
const rewritten = replaceInStr(input.file_path);
|
|
48
|
+
if (rewritten !== input.file_path) { newInput.file_path = rewritten; modified = true; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (['Glob', 'Grep'].includes(toolName) && typeof input.path === 'string') {
|
|
52
|
+
const rewritten = replaceInStr(input.path);
|
|
53
|
+
if (rewritten !== input.path) { newInput.path = rewritten; modified = true; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
57
|
+
const rewritten = replaceInStr(input.command);
|
|
58
|
+
if (rewritten !== input.command) { newInput.command = rewritten; modified = true; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return modified ? newInput : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
15
64
|
/**
|
|
16
65
|
* 评估工具调用是否安全
|
|
17
66
|
* @param {string} toolName - 工具名称(如 Bash, Read, Write, Edit, Glob, Grep)
|
|
18
67
|
* @param {Record<string, unknown>} input - 工具参数
|
|
19
68
|
* @param {{ userUuid: string, cwd?: string }} opts
|
|
20
|
-
* @returns {Promise<{ allowed: boolean, reason?: string }>}
|
|
69
|
+
* @returns {Promise<{ allowed: boolean, reason?: string, modifiedInput?: Record<string, unknown> }>}
|
|
21
70
|
*/
|
|
22
71
|
export async function evaluate(toolName, input, { userUuid, cwd }) {
|
|
23
72
|
if (process.env.TOOL_GUARD_ENABLED === 'false') {
|
|
24
73
|
return { allowed: true };
|
|
25
74
|
}
|
|
26
75
|
|
|
76
|
+
// 路径适配:将 .claude 相关路径重写为用户专属目录(多用户模式兼容)
|
|
77
|
+
const { claudeDir } = getUserPaths(userUuid);
|
|
78
|
+
const rewrittenInput = rewriteClaudePaths(toolName, input, claudeDir);
|
|
79
|
+
const effectiveInput = rewrittenInput || input;
|
|
80
|
+
|
|
81
|
+
if (rewrittenInput && VERBOSE) {
|
|
82
|
+
console.log(`[ToolGuard] PATH-REWRITE ${toolName}: .claude paths → ${claudeDir}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
27
85
|
const context = buildContext(userUuid, cwd);
|
|
28
86
|
|
|
29
87
|
// Phase 1: 正则规则快速检查
|
|
30
|
-
const { denied, uncertain, denyReasons, uncertainReasons } = evaluateRules(toolName,
|
|
88
|
+
const { denied, uncertain, denyReasons, uncertainReasons } = evaluateRules(toolName, effectiveInput, context);
|
|
31
89
|
|
|
32
90
|
if (denied) {
|
|
33
91
|
if (VERBOSE) console.log(`[ToolGuard] RULE-DENY ${toolName}: ${denyReasons.join('; ')}`);
|
|
@@ -36,19 +94,21 @@ export async function evaluate(toolName, input, { userUuid, cwd }) {
|
|
|
36
94
|
|
|
37
95
|
if (!uncertain) {
|
|
38
96
|
if (VERBOSE) console.log(`[ToolGuard] RULE-ALLOW ${toolName}`);
|
|
39
|
-
return { allowed: true };
|
|
97
|
+
return rewrittenInput ? { allowed: true, modifiedInput: rewrittenInput } : { allowed: true };
|
|
40
98
|
}
|
|
41
99
|
|
|
42
100
|
// Phase 2: LLM 兜底审查
|
|
43
101
|
if (process.env.TOOL_GUARD_LLM_ENABLED === 'false') {
|
|
44
102
|
if (VERBOSE) console.log(`[ToolGuard] LLM disabled, UNCERTAIN→ALLOW ${toolName}`);
|
|
45
|
-
return { allowed: true };
|
|
103
|
+
return rewrittenInput ? { allowed: true, modifiedInput: rewrittenInput } : { allowed: true };
|
|
46
104
|
}
|
|
47
105
|
|
|
48
106
|
try {
|
|
49
|
-
const llmResult = await reviewWithLLM(toolName,
|
|
107
|
+
const llmResult = await reviewWithLLM(toolName, effectiveInput, context, uncertainReasons);
|
|
50
108
|
if (VERBOSE) console.log(`[ToolGuard] LLM-${llmResult.allowed ? 'ALLOW' : 'DENY'} ${toolName}: ${llmResult.reason || ''}`);
|
|
51
|
-
return llmResult
|
|
109
|
+
return rewrittenInput && llmResult.allowed
|
|
110
|
+
? { ...llmResult, modifiedInput: rewrittenInput }
|
|
111
|
+
: llmResult;
|
|
52
112
|
} catch (err) {
|
|
53
113
|
console.error(`[ToolGuard] LLM review error for ${toolName}:`, err.message);
|
|
54
114
|
// LLM 失败时安全兜底:拒绝
|
|
@@ -86,19 +86,8 @@ function checkPathsInContent(content, context) {
|
|
|
86
86
|
const pathPattern = /(?:^|\s|=|"|')(\/[^\s"'`;|&><){}$]+)/gm;
|
|
87
87
|
let match;
|
|
88
88
|
while ((match = pathPattern.exec(nonCommentContent)) !== null) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
if (SAFE_SPECIAL_PATHS.includes(resolvedPath)) continue;
|
|
92
|
-
if (SAFE_COMMAND_PATHS.some(prefix => resolvedPath.startsWith(prefix))) continue;
|
|
93
|
-
if (isPathAllowed(resolvedPath, context)) continue;
|
|
94
|
-
if (isInTempDir(resolvedPath, context)) continue;
|
|
95
|
-
// 占位符/示例路径跳过
|
|
96
|
-
if (PLACEHOLDER_PATH_PREFIXES.some(prefix => resolvedPath.startsWith(prefix))) continue;
|
|
97
|
-
// API 端点路径跳过(/v1/..., /api/... 等,不是文件系统路径)
|
|
98
|
-
if (API_PATH_PATTERN.test(resolvedPath)) continue;
|
|
99
|
-
// 路径在文件系统上不存在 → 不是真实文件(API 路径、变量占位符等),无法被访问
|
|
100
|
-
if (!existsSync(resolvedPath)) continue;
|
|
101
|
-
return { denied: true, reason: `访问项目目录之外的路径: ${resolvedPath}` };
|
|
89
|
+
const bad = checkResolvedPath(path.resolve(match[1]), context);
|
|
90
|
+
if (bad) return { denied: true, reason: `访问项目目录之外的路径: ${bad}` };
|
|
102
91
|
}
|
|
103
92
|
return { denied: false };
|
|
104
93
|
}
|
|
@@ -151,8 +140,19 @@ function hasTempUserSubdir(resolvedPath, context) {
|
|
|
151
140
|
return resolvedPath.includes('/' + context.userUuid + '/') || resolvedPath.includes('/' + context.userUuid);
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
143
|
+
/**
|
|
144
|
+
* 对已解析的绝对路径执行统一白名单检查(供内容扫描和 Bash 命令扫描复用)
|
|
145
|
+
* @returns {string|null} null = 通过;字符串 = 越界的路径
|
|
146
|
+
*/
|
|
147
|
+
function checkResolvedPath(resolvedPath, context) {
|
|
148
|
+
if (SAFE_SPECIAL_PATHS.includes(resolvedPath)) return null;
|
|
149
|
+
if (SAFE_COMMAND_PATHS.some(prefix => resolvedPath.startsWith(prefix))) return null;
|
|
150
|
+
if (isPathAllowed(resolvedPath, context)) return null;
|
|
151
|
+
if (isInTempDir(resolvedPath, context)) return null;
|
|
152
|
+
if (PLACEHOLDER_PATH_PREFIXES.some(prefix => resolvedPath.startsWith(prefix))) return null;
|
|
153
|
+
if (!existsSync(resolvedPath)) return null;
|
|
154
|
+
return resolvedPath;
|
|
155
|
+
}
|
|
156
156
|
|
|
157
157
|
/** 安全命令路径前缀(用于 Bash 路径提取时排除) */
|
|
158
158
|
const SAFE_COMMAND_PATHS = ['/usr/bin/', '/usr/local/bin/', '/bin/', '/sbin/', '/usr/sbin/', '/opt/homebrew/bin/'];
|
|
@@ -163,8 +163,6 @@ const SAFE_SPECIAL_PATHS = ['/dev/null', '/dev/stdin', '/dev/stdout', '/dev/stde
|
|
|
163
163
|
/** 常见占位符路径前缀(示例/文档用途,不应被视为真实访问路径) */
|
|
164
164
|
const PLACEHOLDER_PATH_PREFIXES = ['/path/to/', '/your/', '/example/', '/placeholder/'];
|
|
165
165
|
|
|
166
|
-
/** API 路径模式:匹配 REST API 风格路径(如 /v1/images/generations),不是文件系统路径 */
|
|
167
|
-
const API_PATH_PATTERN = /^\/(?:v\d+|api|graphql|rpc|rest|openapi|swagger)\//;
|
|
168
166
|
|
|
169
167
|
/**
|
|
170
168
|
* 检查单个文件路径的安全性
|
|
@@ -589,23 +587,8 @@ const rules = [
|
|
|
589
587
|
}
|
|
590
588
|
|
|
591
589
|
for (const rawPath of extractedPaths) {
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
// 白名单:只有以下路径允许通过,其他一律拒绝
|
|
595
|
-
// 1. 安全的特殊路径(/dev/null 等)
|
|
596
|
-
if (SAFE_SPECIAL_PATHS.includes(resolvedPath)) continue;
|
|
597
|
-
|
|
598
|
-
// 2. 命令路径(/usr/bin/ 等)— 执行命令本身,不是访问文件
|
|
599
|
-
if (SAFE_COMMAND_PATHS.some(prefix => resolvedPath.startsWith(prefix))) continue;
|
|
600
|
-
|
|
601
|
-
// 3. 在用户允许的目录内(user-data/{uuid}/, user-projects/{uuid}/, cwd)
|
|
602
|
-
if (isPathAllowed(resolvedPath, context)) continue;
|
|
603
|
-
|
|
604
|
-
// 4. 在临时目录中且有用户子目录 → 已由 temp-dir-isolation 规则处理,跳过
|
|
605
|
-
if (isInTempDir(resolvedPath, context)) continue;
|
|
606
|
-
|
|
607
|
-
// 不在白名单中 → DENY
|
|
608
|
-
return { result: 'deny', reason: `不允许访问项目目录之外的路径: ${resolvedPath}` };
|
|
590
|
+
const bad = checkResolvedPath(path.resolve(rawPath), context);
|
|
591
|
+
if (bad) return { result: 'deny', reason: `不允许访问项目目录之外的路径: ${bad}` };
|
|
609
592
|
}
|
|
610
593
|
|
|
611
594
|
// 检查命令复杂度:包含变量替换、子 shell 等 → UNCERTAIN
|