@ian2018cs/agenthub 0.1.54 → 0.1.56

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": "@ian2018cs/agenthub",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "A web-based UI for AI Agents",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -19,13 +19,13 @@
19
19
  "dist/",
20
20
  "README.md"
21
21
  ],
22
- "homepage": "https://github.com/IAn2018cs/claudecodeui",
22
+ "homepage": "https://github.com/IAn2018cs/AgentHub",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/IAn2018cs/claudecodeui.git"
25
+ "url": "git+https://github.com/IAn2018cs/AgentHub.git"
26
26
  },
27
27
  "bugs": {
28
- "url": "https://github.com/IAn2018cs/claudecodeui/issues"
28
+ "url": "https://github.com/IAn2018cs/AgentHub/issues"
29
29
  },
30
30
  "scripts": {
31
31
  "dev": "concurrently --kill-others \"npm run server\" \"npm run client\"",
@@ -34,8 +34,7 @@
34
34
  "build": "vite build",
35
35
  "preview": "vite preview",
36
36
  "start": "npm run build && npm run server",
37
- "release": "./release.sh",
38
- "setup-feishu-cards": "node scripts/setup-feishu-cards.mjs"
37
+ "release": "./release.sh"
39
38
  },
40
39
  "keywords": [
41
40
  "claude coode",
@@ -44,14 +43,14 @@
44
43
  "ui",
45
44
  "mobile"
46
45
  ],
47
- "author": "Claude Code UI Contributors",
46
+ "author": "IAn2018cs",
48
47
  "license": "MIT",
49
48
  "publishConfig": {
50
49
  "registry": "https://registry.npmjs.org",
51
50
  "access": "public"
52
51
  },
53
52
  "dependencies": {
54
- "@anthropic-ai/claude-agent-sdk": "^0.2.70",
53
+ "@anthropic-ai/claude-agent-sdk": "^0.2.72",
55
54
  "@codemirror/lang-css": "^6.3.1",
56
55
  "@codemirror/lang-html": "^6.4.9",
57
56
  "@codemirror/lang-javascript": "^6.2.4",
@@ -22,6 +22,7 @@ import { CLAUDE_MODELS } from '../shared/modelConstants.js';
22
22
  import { getUserPaths } from './services/user-directories.js';
23
23
  import { usageDb } from './database/db.js';
24
24
  import { calculateCost, normalizeModelName } from './services/pricing.js';
25
+ import { evaluate as evaluateToolGuard } from './services/tool-guard/index.js';
25
26
 
26
27
  // Session tracking: Map of session IDs to active query instances
27
28
  const activeSessions = new Map();
@@ -204,10 +205,12 @@ function mapCliOptionsToSDK(options = {}) {
204
205
  console.log(`Using model: ${sdkOptions.model}`);
205
206
 
206
207
  // Map system prompt configuration
208
+ const baseAppend = 'You are an AI assistant running inside AgentHub, a platform that supports diverse work tasks beyond coding.';
209
+ const extraAppend = options.appendSystemPrompt ? `\n\n${options.appendSystemPrompt}` : '';
207
210
  sdkOptions.systemPrompt = {
208
211
  type: 'preset',
209
212
  preset: 'claude_code', // Required to use CLAUDE.md
210
- ...(options.appendSystemPrompt ? { append: options.appendSystemPrompt } : {}),
213
+ append: baseAppend + extraAppend,
211
214
  };
212
215
 
213
216
  // Map setting sources for CLAUDE.md loading
@@ -520,6 +523,46 @@ async function queryClaudeSDK(command, options = {}, ws) {
520
523
  tempImagePaths = imageResult.tempImagePaths;
521
524
  tempDir = imageResult.tempDir;
522
525
 
526
+ // ===== 安全守卫(PreToolUse hook,最高优先级)=====
527
+ // 使用 PreToolUse hook 而非 canUseTool 回调,因为 hook 会在每次工具执行前触发,
528
+ // 包括已被 allowedTools 自动放行的工具和 bypassPermissions 模式下的工具。
529
+ // canUseTool 仅在 CLI 发送 can_use_tool 控制消息时触发(即工具未被预批准时)。
530
+ if (userUuid) {
531
+ sdkOptions.hooks = {
532
+ ...(sdkOptions.hooks || {}),
533
+ PreToolUse: [
534
+ ...((sdkOptions.hooks || {}).PreToolUse || []),
535
+ {
536
+ hooks: [async (hookInput) => {
537
+ try {
538
+ const guardResult = await evaluateToolGuard(hookInput.tool_name, hookInput.tool_input, {
539
+ userUuid,
540
+ cwd: sdkOptions.cwd,
541
+ });
542
+ if (!guardResult.allowed) {
543
+ console.log(`[ToolGuard] DENIED ${hookInput.tool_name} for user ${userUuid}: ${guardResult.reason}`);
544
+ return {
545
+ hookSpecificOutput: {
546
+ hookEventName: 'PreToolUse',
547
+ permissionDecision: 'deny',
548
+ permissionDecisionReason: `[系统安全策略] ${guardResult.reason}`,
549
+ }
550
+ };
551
+ }
552
+ } catch (err) {
553
+ console.error(`[ToolGuard] Error evaluating ${hookInput.tool_name}:`, err.message);
554
+ // 守卫出错时不阻塞正常流程,降级为原有权限控制
555
+ }
556
+ if (process.env.TOOL_GUARD_VERBOSE === 'true') {
557
+ console.log(`[ToolGuard] HOOK-ALLOW ${hookInput.tool_name}`);
558
+ }
559
+ return {};
560
+ }]
561
+ }
562
+ ]
563
+ };
564
+ }
565
+
523
566
  // Gate tool usage with explicit UI approval when not auto-approved.
524
567
  // This does not render UI or persist permissions; it only bridges to the UI
525
568
  // via WebSocket and waits for the response, introduced so tool calls pause
package/server/cli.js CHANGED
@@ -135,9 +135,9 @@ function showStatus() {
135
135
 
136
136
  console.log('\n' + c.dim('═'.repeat(60)));
137
137
  console.log(`\n${c.tip('[TIP]')} Hints:`);
138
- console.log(` ${c.dim('>')} Use ${c.bright('cloudcli --port 8080')} to run on a custom port`);
139
- console.log(` ${c.dim('>')} Use ${c.bright('cloudcli --database-path /path/to/db')} for custom database`);
140
- console.log(` ${c.dim('>')} Run ${c.bright('cloudcli help')} for all options`);
138
+ console.log(` ${c.dim('>')} Use ${c.bright('agenthub --port 8080')} to run on a custom port`);
139
+ console.log(` ${c.dim('>')} Use ${c.bright('agenthub --database-path /path/to/db')} for custom database`);
140
+ console.log(` ${c.dim('>')} Run ${c.bright('agenthub help')} for all options`);
141
141
  console.log(` ${c.dim('>')} Access the UI at http://localhost:${process.env.PORT || '3001'}\n`);
142
142
  }
143
143
 
@@ -149,8 +149,7 @@ function showHelp() {
149
149
  ╚═══════════════════════════════════════════════════════════════╝
150
150
 
151
151
  Usage:
152
- claude-code-ui [command] [options]
153
- cloudcli [command] [options]
152
+ agenthub [command] [options]
154
153
 
155
154
  Commands:
156
155
  start Start the Claude Code UI server (default)
@@ -167,11 +166,11 @@ Options:
167
166
  -v, --version Show version information
168
167
 
169
168
  Examples:
170
- $ cloudcli # Start with defaults
171
- $ cloudcli --port 8080 # Start on port 8080
172
- $ cloudcli -p 3000 # Short form for port
173
- $ cloudcli start --port 4000 # Explicit start command
174
- $ cloudcli status # Show configuration
169
+ $ agenthub # Start with defaults
170
+ $ agenthub --port 8080 # Start on port 8080
171
+ $ agenthub -p 3000 # Short form for port
172
+ $ agenthub start --port 4000 # Explicit start command
173
+ $ agenthub status # Show configuration
175
174
 
176
175
  Environment Variables:
177
176
  PORT Set server port (default: 3001)
@@ -180,10 +179,10 @@ Environment Variables:
180
179
  CONTEXT_WINDOW Set context window size (default: 160000)
181
180
 
182
181
  Documentation:
183
- ${packageJson.homepage || 'https://github.com/siteboon/claudecodeui'}
182
+ ${packageJson.homepage || 'https://github.com/IAn2018cs/AgentHub'}
184
183
 
185
184
  Report Issues:
186
- ${packageJson.bugs?.url || 'https://github.com/siteboon/claudecodeui/issues'}
185
+ ${packageJson.bugs?.url || 'https://github.com/IAn2018cs/AgentHub/issues'}
187
186
  `);
188
187
  }
189
188
 
@@ -203,17 +202,49 @@ function isNewerVersion(v1, v2) {
203
202
  return false;
204
203
  }
205
204
 
205
+ // Download a file with optional GitHub token auth, following redirects
206
+ async function downloadWithAuth(url, destPath, token) {
207
+ const https = await import('https');
208
+ const fs = await import('fs');
209
+ return new Promise((resolve, reject) => {
210
+ const headers = { 'User-Agent': 'agenthub-cli' };
211
+ if (token) headers['Authorization'] = `Bearer ${token}`;
212
+ const download = (downloadUrl) => {
213
+ const req = https.get(downloadUrl, { headers }, (res) => {
214
+ if (res.statusCode === 301 || res.statusCode === 302) {
215
+ download(res.headers.location);
216
+ return;
217
+ }
218
+ if (res.statusCode !== 200) {
219
+ reject(new Error(`HTTP ${res.statusCode}`));
220
+ return;
221
+ }
222
+ const file = fs.createWriteStream(destPath);
223
+ res.pipe(file);
224
+ file.on('finish', () => file.close(resolve));
225
+ file.on('error', reject);
226
+ });
227
+ req.on('error', reject);
228
+ req.setTimeout(60000, () => { req.destroy(); reject(new Error('Download timeout')); });
229
+ };
230
+ download(url);
231
+ });
232
+ }
233
+
206
234
  // Check for updates via GitHub Releases
207
235
  async function checkForUpdates(silent = false) {
208
236
  try {
209
237
  const https = await import('https');
210
- const REPO = 'IAn2018cs/claudecodeui';
238
+ const REPO = 'IAn2018cs/AgentHub';
211
239
  const currentVersion = packageJson.version;
240
+ const githubToken = process.env.GITHUB_TOKEN;
241
+ const headers = { 'User-Agent': 'agenthub-cli' };
242
+ if (githubToken) headers['Authorization'] = `Bearer ${githubToken}`;
212
243
 
213
244
  const latestVersion = await new Promise((resolve, reject) => {
214
245
  const req = https.get(
215
246
  `https://api.github.com/repos/${REPO}/releases/latest`,
216
- { headers: { 'User-Agent': 'agenthub-cli' } },
247
+ { headers },
217
248
  (res) => {
218
249
  let data = '';
219
250
  res.on('data', chunk => data += chunk);
@@ -270,15 +301,27 @@ async function updatePackage() {
270
301
 
271
302
  console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
272
303
  const tgzName = `ian2018cs-agenthub-${latestVersion}.tgz`;
273
- const url = `https://github.com/IAn2018cs/claudecodeui/releases/download/v${latestVersion}/${tgzName}`;
274
- execSync(`npm install -g "${url}"`, { stdio: 'inherit' });
304
+ const url = `https://github.com/IAn2018cs/AgentHub/releases/download/v${latestVersion}/${tgzName}`;
305
+ const githubToken = process.env.GITHUB_TOKEN;
306
+ if (githubToken) {
307
+ const os = await import('os');
308
+ const path = await import('path');
309
+ const fsSync = await import('fs');
310
+ const tmpFile = path.join(os.tmpdir(), tgzName);
311
+ console.log(`${c.info('[INFO]')} Downloading with GitHub token...`);
312
+ await downloadWithAuth(url, tmpFile, githubToken);
313
+ execSync(`npm install -g "${tmpFile}"`, { stdio: 'inherit' });
314
+ fsSync.unlinkSync(tmpFile);
315
+ } else {
316
+ execSync(`npm install -g "${url}"`, { stdio: 'inherit' });
317
+ }
275
318
  console.log(`${c.ok('[OK]')} Update complete!`);
276
319
  console.log(` Please restart the server to use the new version:`);
277
320
  console.log(` - pm2: ${c.bright('pm2 restart agenthub')} or ${c.bright('./pm2.sh restart')}`);
278
321
  console.log(` - manual: stop and run ${c.bright('agenthub')} again`);
279
322
  } catch (e) {
280
323
  console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
281
- console.log(`${c.tip('[TIP]')} Check releases: https://github.com/IAn2018cs/claudecodeui/releases`);
324
+ console.log(`${c.tip('[TIP]')} Check releases: https://github.com/IAn2018cs/AgentHub/releases`);
282
325
  }
283
326
  }
284
327
 
@@ -392,7 +435,7 @@ async function main() {
392
435
  break;
393
436
  default:
394
437
  console.error(`\n❌ Unknown command: ${command}`);
395
- console.log(' Run "cloudcli help" for usage information.\n');
438
+ console.log(' Run "agenthub help" for usage information.\n');
396
439
  process.exit(1);
397
440
  }
398
441
  }
package/server/index.js CHANGED
@@ -38,7 +38,6 @@ import os from 'os';
38
38
  import http from 'http';
39
39
  import cors from 'cors';
40
40
  import { promises as fsPromises } from 'fs';
41
- import { spawn } from 'child_process';
42
41
  import pty from 'node-pty';
43
42
  import fetch from 'node-fetch';
44
43
  import mime from 'mime-types';
@@ -225,19 +224,6 @@ const wss = new WebSocketServer({
225
224
  verifyClient: (info) => {
226
225
  console.log('WebSocket connection attempt to:', info.req.url);
227
226
 
228
- // Platform mode: always allow connection
229
- if (process.env.VITE_IS_PLATFORM === 'true') {
230
- const user = authenticateWebSocket(null); // Will return first user
231
- if (!user) {
232
- console.log('[WARN] Platform mode: No user found in database');
233
- return false;
234
- }
235
- info.req.user = user;
236
- console.log('[OK] Platform mode WebSocket authenticated for user:', user.username);
237
- return true;
238
- }
239
-
240
- // Normal mode: verify token
241
227
  // Extract token from query parameters or headers
242
228
  const url = new URL(info.req.url, 'http://localhost');
243
229
  const token = url.searchParams.get('token') ||
@@ -2359,7 +2345,7 @@ async function startServer() {
2359
2345
  console.log('');
2360
2346
  console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://0.0.0.0:' + PORT)}`);
2361
2347
  console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
2362
- console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
2348
+ console.log(`${c.tip('[TIP]')} Run "agenthub status" for full configuration details`);
2363
2349
  console.log('');
2364
2350
 
2365
2351
  // Start usage scanner service
@@ -20,22 +20,6 @@ const validateApiKey = (req, res, next) => {
20
20
 
21
21
  // JWT authentication middleware
22
22
  const authenticateToken = async (req, res, next) => {
23
- // Platform mode: use single database user
24
- if (process.env.VITE_IS_PLATFORM === 'true') {
25
- try {
26
- const user = userDb.getFirstUser();
27
- if (!user) {
28
- return res.status(500).json({ error: 'Platform mode: No user found in database' });
29
- }
30
- req.user = user;
31
- return next();
32
- } catch (error) {
33
- console.error('Platform mode error:', error);
34
- return res.status(500).json({ error: 'Platform mode: Failed to fetch user' });
35
- }
36
- }
37
-
38
- // Normal OSS JWT validation
39
23
  const authHeader = req.headers['authorization'];
40
24
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
41
25
 
@@ -88,21 +72,6 @@ const generateToken = (user) => {
88
72
 
89
73
  // WebSocket authentication function
90
74
  const authenticateWebSocket = (token) => {
91
- // Platform mode: bypass token validation, return first user
92
- if (process.env.VITE_IS_PLATFORM === 'true') {
93
- try {
94
- const user = userDb.getFirstUser();
95
- if (user) {
96
- return { userId: user.id, username: user.username };
97
- }
98
- return null;
99
- } catch (error) {
100
- console.error('Platform mode WebSocket error:', error);
101
- return null;
102
- }
103
- }
104
-
105
- // Normal OSS JWT validation
106
75
  if (!token) {
107
76
  return null;
108
77
  }
@@ -888,7 +888,7 @@ async function addProjectManually(projectPath, displayName = null, userUuid) {
888
888
  }
889
889
 
890
890
  // Allow adding projects even if the directory exists - this enables tracking
891
- // existing Claude Code or Cursor projects in the UI
891
+ // existing Claude Code projects in the UI
892
892
 
893
893
  const createdAt = new Date().toISOString();
894
894
 
@@ -2,7 +2,7 @@
2
2
  * command-handler.js — 飞书斜杠命令处理
3
3
  *
4
4
  * 支持命令:
5
- * /auth <token> 绑定 claudecodeui 账号
5
+ * /auth <token> 绑定 agenthub 账号
6
6
  * /unbind 解除绑定
7
7
  * /new 新建 Claude 会话
8
8
  * /list 列出当前项目的会话
@@ -397,7 +397,7 @@ class FakeSendWriter {
397
397
  * @param {string} opts.messageId 原始消息 ID
398
398
  * @param {string} opts.content 用户输入文字
399
399
  * @param {Array|null} opts.images 图片附件(已转换为 data URI 格式)
400
- * @param {string} opts.userUuid claudecodeui 用户 UUID
400
+ * @param {string} opts.userUuid agenthub 用户 UUID
401
401
  * @param {Object} opts.state feishu_session_state 行
402
402
  * @param {Object} opts.larkClient LarkClient 实例
403
403
  * @param {Map} opts.pendingApprovals engine 共享的 pendingApprovals Map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Tool Guard — 工具调用安全守卫
3
+ *
4
+ * 在每次工具执行前进行安全检查,优先级高于所有用户权限模式。
5
+ * 双层检测:正则规则(快速)→ LLM 审查(兜底)
6
+ */
7
+
8
+ import { evaluateRules } from './rules.js';
9
+ import { reviewWithLLM } from './llm-reviewer.js';
10
+ import { getUserPaths, DATA_DIR } from '../user-directories.js';
11
+ import path from 'path';
12
+
13
+ const VERBOSE = process.env.TOOL_GUARD_VERBOSE === 'true';
14
+
15
+ /**
16
+ * 评估工具调用是否安全
17
+ * @param {string} toolName - 工具名称(如 Bash, Read, Write, Edit, Glob, Grep)
18
+ * @param {Record<string, unknown>} input - 工具参数
19
+ * @param {{ userUuid: string, cwd?: string }} opts
20
+ * @returns {Promise<{ allowed: boolean, reason?: string }>}
21
+ */
22
+ export async function evaluate(toolName, input, { userUuid, cwd }) {
23
+ if (process.env.TOOL_GUARD_ENABLED === 'false') {
24
+ return { allowed: true };
25
+ }
26
+
27
+ const context = buildContext(userUuid, cwd);
28
+
29
+ // Phase 1: 正则规则快速检查
30
+ const { denied, uncertain, denyReasons, uncertainReasons } = evaluateRules(toolName, input, context);
31
+
32
+ if (denied) {
33
+ if (VERBOSE) console.log(`[ToolGuard] RULE-DENY ${toolName}: ${denyReasons.join('; ')}`);
34
+ return { allowed: false, reason: denyReasons.join('; ') };
35
+ }
36
+
37
+ if (!uncertain) {
38
+ if (VERBOSE) console.log(`[ToolGuard] RULE-ALLOW ${toolName}`);
39
+ return { allowed: true };
40
+ }
41
+
42
+ // Phase 2: LLM 兜底审查
43
+ if (process.env.TOOL_GUARD_LLM_ENABLED === 'false') {
44
+ if (VERBOSE) console.log(`[ToolGuard] LLM disabled, UNCERTAIN→ALLOW ${toolName}`);
45
+ return { allowed: true };
46
+ }
47
+
48
+ try {
49
+ const llmResult = await reviewWithLLM(toolName, input, context, uncertainReasons);
50
+ if (VERBOSE) console.log(`[ToolGuard] LLM-${llmResult.allowed ? 'ALLOW' : 'DENY'} ${toolName}: ${llmResult.reason || ''}`);
51
+ return llmResult;
52
+ } catch (err) {
53
+ console.error(`[ToolGuard] LLM review error for ${toolName}:`, err.message);
54
+ // LLM 失败时安全兜底:拒绝
55
+ return { allowed: false, reason: 'LLM 审查失败,出于安全考虑已拒绝' };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 构建安全检查上下文
61
+ */
62
+ function buildContext(userUuid, cwd) {
63
+ const resolvedDataDir = path.resolve(DATA_DIR);
64
+ return {
65
+ userUuid,
66
+ cwd: cwd ? path.resolve(cwd) : null,
67
+ allowedPrefixes: [
68
+ path.join(resolvedDataDir, 'user-data', userUuid),
69
+ path.join(resolvedDataDir, 'user-projects', userUuid),
70
+ path.join(resolvedDataDir, 'images', userUuid),
71
+ ],
72
+ dataDir: resolvedDataDir,
73
+ tempDirs: ['/tmp', '/var/tmp', '/private/tmp'],
74
+ };
75
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Tool Guard — LLM 审查模块
3
+ *
4
+ * 当正则规则无法确定安全性时(返回 UNCERTAIN),调用 LLM 进行兜底审查。
5
+ * 复用 server/services/llm.js 的 chatCompletion(),使用 OpenAI 兼容格式。
6
+ */
7
+
8
+ import { chatCompletion } from '../llm.js';
9
+
10
+ const MODEL = process.env.TOOL_GUARD_LLM_MODEL || 'gemini-3.1-flash-lite-preview';
11
+ const TIMEOUT_MS = parseInt(process.env.TOOL_GUARD_LLM_TIMEOUT_MS || '5000', 10);
12
+
13
+ /**
14
+ * 使用 LLM 审查工具调用安全性
15
+ * @param {string} toolName
16
+ * @param {Record<string, unknown>} input
17
+ * @param {object} context - { userUuid, allowedPrefixes, cwd, tempDirs }
18
+ * @param {string[]} uncertainReasons - 触发 LLM 审查的原因
19
+ * @returns {Promise<{ allowed: boolean, reason?: string }>}
20
+ */
21
+ export async function reviewWithLLM(toolName, input, context, uncertainReasons) {
22
+ const { userUuid, allowedPrefixes, cwd } = context;
23
+
24
+ // 如果 OPENAI_API_KEY 未配置,降级为放行
25
+ if (!process.env.OPENAI_API_KEY) {
26
+ console.warn('[ToolGuard] OPENAI_API_KEY 未配置,LLM 审查降级为放行');
27
+ return { allowed: true };
28
+ }
29
+
30
+ const systemPrompt = buildSystemPrompt(userUuid, allowedPrefixes, cwd);
31
+ const userPrompt = buildUserPrompt(toolName, input, uncertainReasons);
32
+
33
+ try {
34
+ // 带超时的 LLM 调用
35
+ const result = await Promise.race([
36
+ chatCompletion({
37
+ systemPrompt,
38
+ userPrompt,
39
+ model: MODEL,
40
+ maxTokens: 256,
41
+ temperature: 0,
42
+ }),
43
+ new Promise((_, reject) =>
44
+ setTimeout(() => reject(new Error('LLM 审查超时')), TIMEOUT_MS)
45
+ ),
46
+ ]);
47
+
48
+ return parseResponse(result);
49
+ } catch (err) {
50
+ if (err.message === 'LLM 审查超时') {
51
+ return { allowed: false, reason: 'LLM 审查超时,出于安全考虑已拒绝' };
52
+ }
53
+ throw err; // 其他错误向上抛出,由 index.js 的 catch 处理
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 构建系统提示词
59
+ */
60
+ function buildSystemPrompt(userUuid, allowedPrefixes, cwd) {
61
+ return `你是一个多用户 AI 编程平台的安全审查员。你需要判断一个 AI 助手的工具调用是否安全。
62
+
63
+ ## 用户上下文
64
+ - 用户 ID: ${userUuid}
65
+ - 用户允许的目录:
66
+ - ${allowedPrefixes[0]}
67
+ - ${allowedPrefixes[1]}
68
+ - 当前工作目录: ${cwd || '未指定'}
69
+ - 临时目录要求: 必须使用 /tmp/${userUuid}/ 子文件夹
70
+
71
+ ## 安全规则
72
+ 1. 用户只能访问自己目录下的文件,不能访问其他用户的文件或系统文件
73
+ 2. 临时目录(/tmp/、/var/tmp/)必须使用用户 ID 子文件夹(/tmp/${userUuid}/)
74
+ 3. 不允许修改系统配置(nginx、docker、systemd、防火墙等)
75
+ 4. 不允许执行危险命令(sudo、kill 系统进程、rm -rf /、shutdown 等)
76
+ 5. 不允许全局安装包(npm -g、pip 非虚拟环境、pip --user 等);pip 必须在虚拟环境内执行(venv/bin/pip 或 .venv/bin/pip)
77
+ 6. 允许的操作:运行用户自己项目(node/npm/python)、本地安装依赖、git 操作、创建虚拟环境
78
+
79
+ ## 判断原则
80
+ - 如果操作明显安全(在用户目录内操作、本地安装依赖等),判定为 safe
81
+ - 如果操作有潜在风险但无法确定,倾向于判定为 unsafe(宁可误拒不可漏放)
82
+ - 判定为 unsafe 时,在 reason 中给出简短的中文说明,帮助用户理解被拒绝的原因
83
+
84
+ ## 响应格式
85
+ 仅输出 JSON,不要输出其他任何内容:
86
+ {"safe": true, "reason": "安全操作说明"}
87
+
88
+ {"safe": false, "reason": "拒绝原因说明"}`;
89
+ }
90
+
91
+ /**
92
+ * 构建用户提示词
93
+ */
94
+ function buildUserPrompt(toolName, input, uncertainReasons) {
95
+ const inputStr = typeof input === 'object' ? JSON.stringify(input, null, 2) : String(input);
96
+ return `## 待审查的工具调用
97
+ - 工具名称: ${toolName}
98
+ - 工具参数:
99
+ ${inputStr}
100
+
101
+ ## 需要额外关注的原因
102
+ ${uncertainReasons.map(r => `- ${r}`).join('\n')}`;
103
+ }
104
+
105
+ /**
106
+ * 解析 LLM 响应
107
+ */
108
+ function parseResponse(responseText) {
109
+ try {
110
+ // 尝试提取 JSON(LLM 可能返回 markdown 包裹的 JSON)
111
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
112
+ if (!jsonMatch) {
113
+ return { allowed: false, reason: 'LLM 审查返回格式异常,出于安全考虑已拒绝' };
114
+ }
115
+
116
+ const parsed = JSON.parse(jsonMatch[0]);
117
+
118
+ if (typeof parsed.safe !== 'boolean') {
119
+ return { allowed: false, reason: 'LLM 审查返回格式异常,出于安全考虑已拒绝' };
120
+ }
121
+
122
+ return parsed.safe
123
+ ? { allowed: true }
124
+ : { allowed: false, reason: parsed.reason || 'LLM 审查判定为不安全操作' };
125
+ } catch {
126
+ return { allowed: false, reason: 'LLM 审查响应解析失败,出于安全考虑已拒绝' };
127
+ }
128
+ }