@bolloon/bolloon-agent 0.1.13 → 0.1.15

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.
Files changed (47) hide show
  1. package/dist/agents/pi-sdk.js +222 -9
  2. package/dist/agents/shell-guard.js +354 -0
  3. package/dist/agents/shell-tool.js +83 -0
  4. package/dist/agents/skill-loader.js +174 -0
  5. package/dist/bollharness-integration/context-chain-router.js +3 -3
  6. package/dist/bollharness-integration/context-router.js +1 -1
  7. package/dist/heartbeat/Watchdog.js +7 -5
  8. package/dist/heartbeat/index.js +1 -0
  9. package/dist/heartbeat/self-improve-bus.js +85 -0
  10. package/dist/pi-ecosystem-judgment/index.js +1 -2
  11. package/dist/utils/auto-update.js +44 -12
  12. package/dist/web/client.js +841 -103
  13. package/dist/web/index.html +88 -8
  14. package/dist/web/style.css +506 -9
  15. package/package.json +2 -2
  16. package/scripts/build-cli.js +11 -1
  17. package/src/agents/pi-sdk.ts +230 -10
  18. package/src/agents/shell-guard.ts +417 -0
  19. package/src/agents/shell-tool.ts +103 -0
  20. package/src/agents/skill-loader.ts +202 -0
  21. package/src/bollharness-integration/context-chain-router.ts +3 -3
  22. package/src/bollharness-integration/context-router.ts +1 -1
  23. package/src/heartbeat/Watchdog.ts +7 -5
  24. package/src/heartbeat/index.ts +1 -0
  25. package/src/heartbeat/self-improve-bus.ts +110 -0
  26. package/src/types.d.ts +12 -0
  27. package/src/utils/auto-update.ts +45 -14
  28. package/src/web/client.js +841 -103
  29. package/src/web/index.html +88 -8
  30. package/src/web/server.ts +427 -101
  31. package/src/web/style.css +506 -9
  32. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
  33. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
  34. package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
  35. package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
  36. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
  37. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
  38. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
  39. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
  40. package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
  41. package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
  42. package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
  43. package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
  44. package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
  45. package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
  46. package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
  47. package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Shell 命令硬护栏 (策略可配置版)
3
+ *
4
+ * 设计原则:
5
+ * 1. 白名单/黑名单从 ~/.bolloon/self-improve-policy.json 加载
6
+ * 2. 加载失败或缺失时, 使用**硬编码兜底** (永远拒绝 = 最安全)
7
+ * 3. 策略文件在禁区里, AI 即便拿到 shell_exec 也改不了
8
+ * 4. 提供 API 端点供**人**热加载策略, 并写审计日志
9
+ *
10
+ * 策略文件 schema (self-improve-policy.json):
11
+ * {
12
+ * "version": 1,
13
+ * "commandAllowlist": ["git", "npm", "tsc", "vitest", "cat", "ls", "..."],
14
+ * "commandDenylist": ["rm", "mv", "chmod", "sudo", "su", "curl", "wget"],
15
+ * "pathAllowlist": [
16
+ * "src/web/client.js",
17
+ * "src/agents/workflow-engine.ts",
18
+ * "*.md",
19
+ * "docs/**"
20
+ * ],
21
+ * "pathDenylist": [
22
+ * "src/agents/pi-sdk.ts",
23
+ * "src/agents/shell-guard.ts",
24
+ * "src/agents/shell-tool.ts",
25
+ * "src/heartbeat/**",
26
+ * "src/network/**",
27
+ * "src/pi-ecosystem-judgment/**",
28
+ * "package.json",
29
+ * ".env*",
30
+ * ".git/**",
31
+ * "dist/**",
32
+ * "node_modules/**"
33
+ * ],
34
+ * "cooldownMs": 21600000,
35
+ * "sandboxCwd": ".bolloon-shell-sandbox",
36
+ * "branchPrefix": "agent/self-imp-"
37
+ * }
38
+ *
39
+ * 匹配规则:
40
+ * 1. 路径先查 denylist (命中即拒), 再查 allowlist (没命中即拒)
41
+ * 2. 命令先查 denylist (命中即拒), 再查 allowlist (没命中即拒)
42
+ * 3. 通配符: * 匹配单层文件名, ** 匹配任意层级
43
+ */
44
+
45
+ import * as fs from 'fs';
46
+ import * as path from 'path';
47
+ import * as os from 'os';
48
+
49
+ // ============================================================================
50
+ // 硬编码兜底 (策略文件读不到时, 用这套)
51
+ // 这是**最后一道防线** - AI 即便能改 ~/.bolloon/ 也没法删这个常量
52
+ // ============================================================================
53
+ const FALLBACK_COMMAND_ALLOWLIST: ReadonlySet<string> = new Set([
54
+ 'git', 'node', 'npm', 'npx', 'tsx', 'tsc', 'vitest',
55
+ 'cat', 'head', 'tail', 'wc', 'ls', 'echo', 'pwd', 'date',
56
+ 'mkdir', 'touch'
57
+ ]);
58
+
59
+ const FALLBACK_PATH_ALLOWLIST: readonly string[] = [
60
+ // 自由区: AI 可以改
61
+ 'src/web/client.js',
62
+ 'src/web/style.css',
63
+ 'src/agents/workflow-engine.ts',
64
+ 'src/agents/workflow-pivot-loop.ts',
65
+ 'src/agents/constraint-layer.ts',
66
+ 'src/test/**',
67
+ 'docs/**',
68
+ '*.md',
69
+ 'README.md'
70
+ ];
71
+
72
+ const FALLBACK_PATH_DENYLIST: ReadonlyArray<RegExp> = [
73
+ /(^|\/)src\/agents\/pi-sdk\.ts$/, // LLM 抽象层
74
+ /(^|\/)src\/agents\/shell-guard\.ts$/, // 护栏本身
75
+ /(^|\/)src\/agents\/shell-tool\.ts$/, // shell 工具实现
76
+ /(^|\/)src\/heartbeat\//, // 心跳
77
+ /(^|\/)src\/network\//, // P2P / libp2p / iroh
78
+ /(^|\/)src\/pi-ecosystem-judgment\//, // judgment 系统
79
+ /(^|\/)package\.json$/,
80
+ /(^|\/)package-lock\.json$/,
81
+ /(^|\/)tsconfig.*\.json$/,
82
+ /(^|\/)\.env(\.|$)/,
83
+ /(^|\/)\.git\//,
84
+ /(^|\/)\.bolloon\//, // 策略文件 / sessions / persona
85
+ /(^|\/)dist\//,
86
+ /(^|\/)node_modules\//,
87
+ ];
88
+
89
+ const FALLBACK_ARG_DENYLIST: ReadonlyArray<RegExp> = [
90
+ /^\s*push\s+(-f|--force)/i,
91
+ /^\s*push\s+origin\s+(master|main)\b/i,
92
+ /^\s*reset\s+--hard\b/i,
93
+ /^\s*clean\s+-fd?\b/i,
94
+ /^\s*--inspect\b/,
95
+ /[|&;`$()<>]/, // shell 元字符
96
+ /\brm\s+-rf?\b/i,
97
+ /\bsudo\b/i,
98
+ /\bsu\b/i,
99
+ /\bcurl\b/i,
100
+ /\bwget\b/i,
101
+ /\.\.\//, // 路径逃逸
102
+ /^\//, // 绝对路径
103
+ /^[a-zA-Z]:\\/, // Windows 绝对路径
104
+ ];
105
+
106
+ // ============================================================================
107
+ // 策略加载
108
+ // ============================================================================
109
+
110
+ export interface SelfImprovePolicy {
111
+ version: number;
112
+ commandAllowlist: string[];
113
+ commandDenylist?: string[];
114
+ pathAllowlist: string[];
115
+ pathDenylist: string[];
116
+ cooldownMs: number;
117
+ sandboxCwd: string;
118
+ branchPrefix: string;
119
+ }
120
+
121
+ let cachedPolicy: SelfImprovePolicy | null = null;
122
+ let policyLoadedAt: number = 0;
123
+ const POLICY_TTL_MS = 60_000; // 60 秒缓存 (避免每次 shell_exec 都读盘)
124
+
125
+ const POLICY_PATH = path.join(os.homedir(), '.bolloon', 'self-improve-policy.json');
126
+ const POLICY_AUDIT_PATH = path.join(os.homedir(), '.bolloon', 'self-improve-audit.log');
127
+
128
+ /**
129
+ * 默认策略模板 - 第一次启动时写到磁盘
130
+ */
131
+ function getDefaultPolicy(): SelfImprovePolicy {
132
+ return {
133
+ version: 1,
134
+ commandAllowlist: Array.from(FALLBACK_COMMAND_ALLOWLIST),
135
+ pathAllowlist: [...FALLBACK_PATH_ALLOWLIST],
136
+ pathDenylist: FALLBACK_PATH_DENYLIST.map(r => r.source),
137
+ cooldownMs: 6 * 60 * 60 * 1000,
138
+ sandboxCwd: '.bolloon-shell-sandbox',
139
+ branchPrefix: 'agent/self-imp-'
140
+ };
141
+ }
142
+
143
+ /**
144
+ * 加载策略 (有缓存)
145
+ * 加载失败返回 null, 调用方应回退到硬编码兜底
146
+ */
147
+ export function loadPolicy(forceReload = false): SelfImprovePolicy | null {
148
+ const now = Date.now();
149
+ if (!forceReload && cachedPolicy && now - policyLoadedAt < POLICY_TTL_MS) {
150
+ return cachedPolicy;
151
+ }
152
+
153
+ try {
154
+ if (!fs.existsSync(POLICY_PATH)) {
155
+ // 第一次启动: 写入默认策略
156
+ const dir = path.dirname(POLICY_PATH);
157
+ fs.mkdirSync(dir, { recursive: true });
158
+ fs.writeFileSync(POLICY_PATH, JSON.stringify(getDefaultPolicy(), null, 2));
159
+ console.log(`[shell-guard] 已生成默认策略: ${POLICY_PATH}`);
160
+ }
161
+
162
+ const raw = fs.readFileSync(POLICY_PATH, 'utf-8');
163
+ const parsed = JSON.parse(raw) as SelfImprovePolicy;
164
+
165
+ // 极简 schema 校验
166
+ if (!parsed.version || !Array.isArray(parsed.commandAllowlist) || !Array.isArray(parsed.pathAllowlist) || !Array.isArray(parsed.pathDenylist)) {
167
+ console.warn('[shell-guard] 策略文件 schema 不对, 用硬编码兜底');
168
+ return null;
169
+ }
170
+
171
+ cachedPolicy = parsed;
172
+ policyLoadedAt = now;
173
+ return parsed;
174
+ } catch (err) {
175
+ console.warn('[shell-guard] 策略文件加载失败, 用硬编码兜底:', err);
176
+ return null;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 审计日志: 记录所有被拒/被允许的 shell_exec 调用
182
+ */
183
+ export function auditShellCall(
184
+ result: 'allowed' | 'denied',
185
+ cmd: string,
186
+ args: string[],
187
+ reason?: string,
188
+ targetPath?: string
189
+ ): void {
190
+ try {
191
+ const dir = path.dirname(POLICY_AUDIT_PATH);
192
+ fs.mkdirSync(dir, { recursive: true });
193
+ const line = JSON.stringify({
194
+ ts: new Date().toISOString(),
195
+ result,
196
+ cmd,
197
+ args,
198
+ reason,
199
+ targetPath
200
+ }) + '\n';
201
+ fs.appendFileSync(POLICY_AUDIT_PATH, line);
202
+ } catch {
203
+ // 审计失败不阻塞
204
+ }
205
+ }
206
+
207
+ // ============================================================================
208
+ // 检查逻辑
209
+ // ============================================================================
210
+
211
+ export interface ShellCheckResult {
212
+ allowed: boolean;
213
+ reason?: string;
214
+ /** 触发的是哪条规则 (denylist / allowlist / fallback) */
215
+ matchedBy?: 'cmd-denylist' | 'cmd-allowlist' | 'arg-denylist' | 'path-denylist' | 'path-allowlist' | 'fallback-deny';
216
+ }
217
+
218
+ /**
219
+ * 把通配符模式编译成正则
220
+ * * -> [^/]*
221
+ * ** -> .*
222
+ */
223
+ function compileGlob(pattern: string): RegExp {
224
+ // 转义正则元字符, 但保留 * 和 **
225
+ const escaped = pattern
226
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
227
+ .replace(/\*\*/g, '__DOUBLESTAR__')
228
+ .replace(/\*/g, '[^/]*')
229
+ .replace(/__DOUBLESTAR__/g, '.*');
230
+ return new RegExp('^' + escaped + '$');
231
+ }
232
+
233
+ /**
234
+ * 检查命令
235
+ */
236
+ export function checkCommand(cmd: string, args: string[]): ShellCheckResult {
237
+ const policy = loadPolicy();
238
+ const fullCmd = [cmd, ...args].join(' ').trim();
239
+
240
+ // ---- 1. 命令黑名单 (策略里有的话) ----
241
+ if (policy?.commandDenylist) {
242
+ for (const denied of policy.commandDenylist) {
243
+ if (cmd === denied || fullCmd.startsWith(denied)) {
244
+ auditShellCall('denied', cmd, args, `命中 commandDenylist: ${denied}`);
245
+ return { allowed: false, reason: `命令 '${cmd}' 命中策略黑名单: ${denied}`, matchedBy: 'cmd-denylist' };
246
+ }
247
+ }
248
+ }
249
+
250
+ // ---- 2. 命令白名单 ----
251
+ const allowSet = policy
252
+ ? new Set(policy.commandAllowlist)
253
+ : FALLBACK_COMMAND_ALLOWLIST as Set<string>;
254
+ if (!allowSet.has(cmd)) {
255
+ auditShellCall('denied', cmd, args, `命令 '${cmd}' 不在白名单`);
256
+ return {
257
+ allowed: false,
258
+ reason: `命令 '${cmd}' 不在白名单. 允许: ${Array.from(allowSet).join(', ')}`,
259
+ matchedBy: policy ? 'cmd-allowlist' : 'fallback-deny'
260
+ };
261
+ }
262
+
263
+ // ---- 3. 参数黑名单 (策略文件 + 硬编码兜底都查) ----
264
+ const argPatterns = policy
265
+ ? [
266
+ ...FALLBACK_ARG_DENYLIST,
267
+ // 策略里没有专门的 argDenylist 字段, 复用 pathDenylist 模式不太合适
268
+ // 故意**不**让用户在策略里加 arg denylist - 防止 AI 删自己的 arg 限制
269
+ ]
270
+ : FALLBACK_ARG_DENYLIST;
271
+
272
+ for (const arg of args) {
273
+ for (const pattern of argPatterns) {
274
+ if (pattern.test(arg)) {
275
+ auditShellCall('denied', cmd, args, `参数 '${arg}' 命中黑名单`);
276
+ return { allowed: false, reason: `参数 '${arg}' 命中黑名单模式 ${pattern}`, matchedBy: 'arg-denylist' };
277
+ }
278
+ }
279
+ }
280
+
281
+ // 整条命令再过一遍
282
+ for (const pattern of argPatterns) {
283
+ if (pattern.test(fullCmd)) {
284
+ auditShellCall('denied', cmd, args, `整条命令命中黑名单`);
285
+ return { allowed: false, reason: `整条命令命中黑名单模式 ${pattern}`, matchedBy: 'arg-denylist' };
286
+ }
287
+ }
288
+
289
+ auditShellCall('allowed', cmd, args);
290
+ return { allowed: true };
291
+ }
292
+
293
+ /**
294
+ * 检查路径
295
+ *
296
+ * 逻辑:
297
+ * 1. denylist 优先: 命中即拒 (用硬编码兜底正则)
298
+ * 2. allowlist: 命中放行
299
+ * 3. 都不命中: 拒 (默认拒绝)
300
+ */
301
+ export function checkWritePath(targetPath: string): ShellCheckResult {
302
+ const policy = loadPolicy();
303
+ const normalized = path.normalize(targetPath).replace(/\\/g, '/');
304
+
305
+ // ---- 1. 路径黑名单 (硬编码兜底不可绕过) ----
306
+ // 即便策略文件里 denylist 是空的, 硬编码兜底永远生效
307
+ const hardcodedDenylist = FALLBACK_PATH_DENYLIST;
308
+ for (const pattern of hardcodedDenylist) {
309
+ if (pattern.test(normalized)) {
310
+ auditShellCall('denied', '', [], `路径 '${targetPath}' 命中硬编码禁区`, targetPath);
311
+ return { allowed: false, reason: `路径 '${targetPath}' 命中硬编码禁区 ${pattern}`, matchedBy: 'fallback-deny' };
312
+ }
313
+ }
314
+
315
+ // 策略文件里的额外 denylist
316
+ if (policy?.pathDenylist) {
317
+ for (const patternStr of policy.pathDenylist) {
318
+ try {
319
+ const regex = compileGlob(patternStr);
320
+ if (regex.test(normalized)) {
321
+ auditShellCall('denied', '', [], `路径 '${targetPath}' 命中策略 denylist`, targetPath);
322
+ return { allowed: false, reason: `路径 '${targetPath}' 命中策略 denylist: ${patternStr}`, matchedBy: 'path-denylist' };
323
+ }
324
+ } catch {
325
+ // 编译失败的模式跳过
326
+ }
327
+ }
328
+ }
329
+
330
+ // ---- 2. 路径白名单 (来自策略或兜底) ----
331
+ const allowlist = policy?.pathAllowlist || FALLBACK_PATH_ALLOWLIST;
332
+ for (const patternStr of allowlist) {
333
+ try {
334
+ const regex = compileGlob(patternStr);
335
+ if (regex.test(normalized)) {
336
+ auditShellCall('allowed', '', [], undefined, targetPath);
337
+ return { allowed: true, matchedBy: 'path-allowlist' };
338
+ }
339
+ } catch {
340
+ // 编译失败的模式跳过
341
+ }
342
+ }
343
+
344
+ // 都不命中: 默认拒绝
345
+ auditShellCall('denied', '', [], `路径 '${targetPath}' 不在任何 allowlist 中`, targetPath);
346
+ return {
347
+ allowed: false,
348
+ reason: `路径 '${targetPath}' 不在白名单. 允许: ${allowlist.join(', ')}`,
349
+ matchedBy: 'path-allowlist'
350
+ };
351
+ }
352
+
353
+ // ============================================================================
354
+ // 运行时配置 (从策略文件读, 但有兜底)
355
+ // ============================================================================
356
+
357
+ /**
358
+ * 自改分支名前缀
359
+ */
360
+ export function getBranchPrefix(): string {
361
+ const policy = loadPolicy();
362
+ return policy?.branchPrefix || 'agent/self-imp-';
363
+ }
364
+
365
+ /**
366
+ * 冷却期 (毫秒)
367
+ */
368
+ export function getCooldownMs(): number {
369
+ const policy = loadPolicy();
370
+ return policy?.cooldownMs || 6 * 60 * 60 * 1000;
371
+ }
372
+
373
+ /**
374
+ * 沙箱工作目录
375
+ */
376
+ export function getSandboxCwd(): string {
377
+ const policy = loadPolicy();
378
+ const rel = policy?.sandboxCwd || '.bolloon-shell-sandbox';
379
+ return path.resolve(process.cwd(), rel);
380
+ }
381
+
382
+ // ============================================================================
383
+ // 兼容旧 API - 保留原导出名
384
+ // ============================================================================
385
+
386
+ /** @deprecated 用 getBranchPrefix() */
387
+ export const SELF_IMPROVE_BRANCH_PREFIX = 'agent/self-imp-';
388
+ /** @deprecated 用 getCooldownMs() */
389
+ export const SELF_IMPROVE_COOLDOWN_MS = 6 * 60 * 60 * 1000;
390
+ /** @deprecated 用 getSandboxCwd() */
391
+ export const SHELL_SANDBOX_CWD = path.resolve(process.cwd(), '.bolloon-shell-sandbox');
392
+
393
+ // ============================================================================
394
+ // 写策略 / 审计路径 (供 API 端点用)
395
+ // ============================================================================
396
+
397
+ export const POLICY_AUDIT_PATH_PUBLIC = POLICY_AUDIT_PATH;
398
+
399
+ /**
400
+ * 把新策略写到磁盘, 立即清缓存让下次 loadPolicy() 重读
401
+ * **只供人手动调用**
402
+ */
403
+ export function writePolicy(newPolicy: SelfImprovePolicy): boolean {
404
+ try {
405
+ newPolicy.version = (newPolicy.version || 0) + 1;
406
+ const dir = path.dirname(POLICY_PATH);
407
+ fs.mkdirSync(dir, { recursive: true });
408
+ fs.writeFileSync(POLICY_PATH, JSON.stringify(newPolicy, null, 2));
409
+ cachedPolicy = null; // 清缓存
410
+ policyLoadedAt = 0;
411
+ console.log(`[shell-guard] 策略已更新, version=${newPolicy.version}`);
412
+ return true;
413
+ } catch (err) {
414
+ console.error('[shell-guard] 写策略失败:', err);
415
+ return false;
416
+ }
417
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shell 工具: 给 Bolloon agent 跑受限的 shell 命令
3
+ *
4
+ * 这个工具**只做两件事**:
5
+ * 1. 把命令交给硬护栏检查
6
+ * 2. 在沙箱 cwd 下用 child_process 执行
7
+ *
8
+ * AI 完全自主触发自改, 但 shell 工具本身**只接受白名单内命令**.
9
+ * 禁区列表在 shell-guard.ts, AI 改不了那个文件.
10
+ */
11
+
12
+ import { spawn } from 'child_process';
13
+ import * as fs from 'fs';
14
+ import { checkCommand, checkWritePath, getSandboxCwd } from './shell-guard.js';
15
+
16
+ export interface ShellExecResult {
17
+ success: boolean;
18
+ output?: string;
19
+ error?: string;
20
+ exitCode?: number;
21
+ /** true 表示被护栏拒绝, AI 不应该重试 */
22
+ deniedByGuard?: boolean;
23
+ }
24
+
25
+ /**
26
+ * 在沙箱里跑一条命令
27
+ * @param cmd 可执行文件名, 必须命中白名单
28
+ * @param args 参数列表
29
+ * @param opts.timeoutMs 超时毫秒, 默认 30s
30
+ * @param opts.allowedWriteTargets 允许的写入路径, 命中禁区列表的路径会拒
31
+ */
32
+ export async function shellExec(
33
+ cmd: string,
34
+ args: string[] = [],
35
+ opts: { timeoutMs?: number; allowedWriteTargets?: string[] } = {}
36
+ ): Promise<ShellExecResult> {
37
+ // 1. 护栏检查
38
+ const cmdCheck = checkCommand(cmd, args);
39
+ if (!cmdCheck.allowed) {
40
+ return {
41
+ success: false,
42
+ error: `[shell-guard] ${cmdCheck.reason}`,
43
+ deniedByGuard: true
44
+ };
45
+ }
46
+
47
+ // 2. 写入目标检查
48
+ if (opts.allowedWriteTargets) {
49
+ for (const target of opts.allowedWriteTargets) {
50
+ const pathCheck = checkWritePath(target);
51
+ if (!pathCheck.allowed) {
52
+ return {
53
+ success: false,
54
+ error: `[shell-guard] ${pathCheck.reason}`,
55
+ deniedByGuard: true
56
+ };
57
+ }
58
+ }
59
+ }
60
+
61
+ // 3. 确保沙箱存在
62
+ const sandboxCwd = getSandboxCwd();
63
+ try {
64
+ fs.mkdirSync(sandboxCwd, { recursive: true });
65
+ } catch {
66
+ // 已经存在则忽略
67
+ }
68
+
69
+ // 4. 跑命令
70
+ return new Promise((resolve) => {
71
+ const proc = spawn(cmd, args, {
72
+ cwd: sandboxCwd,
73
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // 禁止 git 弹交互
74
+ shell: false, // **关键**: 禁用 shell, 防止元字符注入
75
+ windowsHide: true
76
+ });
77
+
78
+ let stdout = '';
79
+ let stderr = '';
80
+ const timeout = setTimeout(() => {
81
+ proc.kill('SIGKILL');
82
+ resolve({ success: false, error: `命令超时 (>${opts.timeoutMs || 30000}ms)`, exitCode: -1 });
83
+ }, opts.timeoutMs || 30000);
84
+
85
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
86
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
87
+
88
+ proc.on('error', (err) => {
89
+ clearTimeout(timeout);
90
+ resolve({ success: false, error: `启动失败: ${err.message}` });
91
+ });
92
+
93
+ proc.on('close', (code) => {
94
+ clearTimeout(timeout);
95
+ const output = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim();
96
+ if (code === 0) {
97
+ resolve({ success: true, output, exitCode: 0 });
98
+ } else {
99
+ resolve({ success: false, output, error: `exit code ${code}`, exitCode: code ?? -1 });
100
+ }
101
+ });
102
+ });
103
+ }