@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
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as fs from 'fs/promises';
6
6
  import * as fsSync from 'fs';
7
+ import * as os from 'os';
7
8
  import * as path from 'path';
8
9
  import { documentReader } from '../documents/reader.js';
9
10
  import { getMinimax } from '../constraints/index.js';
@@ -13,9 +14,12 @@ import { WorkflowEngine } from './workflow-engine.js';
13
14
  import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtime';
14
15
  import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
15
16
  import { p2pDocumentTools, initDocumentReceiver } from './p2p-document-tools.js';
17
+ import { shellExec } from './shell-tool.js';
18
+ import { getBranchPrefix, getCooldownMs } from './shell-guard.js';
16
19
  import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
17
20
  import { getGlobalSharedContext } from '../social/global-shared-context.js';
18
21
  import { Session, SkillRegistry, saveSession, loadSession } from '@bolloon/constraint-runtime';
22
+ import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
19
23
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
20
24
  const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
21
25
  export class PiSessionManager {
@@ -353,8 +357,72 @@ class PiAgentSession {
353
357
  this.initSession();
354
358
  initDocumentReceiver();
355
359
  this.registerTools();
360
+ this.loadSkills(config.skillsPaths);
356
361
  this.initHarness();
357
362
  }
363
+ /**
364
+ * 从 SKILL.md 目录加载 skills 进 skillRegistry.
365
+ *
366
+ * 路径解析优先级 (后者覆盖前者同名 skill):
367
+ * 1. 显式传入的 skillsPaths
368
+ * 2. ~/.bolloon/skills/ 全局用户级
369
+ * 3. <cwd>/.bolloon/skills/ 项目级
370
+ * 4. ~/.boll/skills/ 全局 (兼容 bollharness 旧用户)
371
+ * 5. <bolloon-repo>/src/bollharness/.boll/skills/ bolloon 仓库内置 skill
372
+ * (bolloon 项目本身用 pi-sdk 写核心, 这 19 个 skill 视为项目级 builtin)
373
+ *
374
+ * 静默忽略不存在的目录.
375
+ */
376
+ loadSkills(paths) {
377
+ let resolved;
378
+ if (paths && paths.length > 0) {
379
+ resolved = paths;
380
+ }
381
+ else {
382
+ resolved = [
383
+ ...defaultSkillPaths(os.homedir(), this.cwd),
384
+ // bolloon 仓库内置 skill (相对本 npm 包的位置)
385
+ this.findBolloonBuiltinSkillsPath(),
386
+ ].filter((p) => Boolean(p));
387
+ }
388
+ loadSkillsFromPaths(resolved)
389
+ .then((skills) => {
390
+ for (const s of skills) {
391
+ if (this.skillRegistry.has(s.name)) {
392
+ this.skillRegistry.unregister(s.name);
393
+ }
394
+ this.skillRegistry.register(s);
395
+ }
396
+ console.log(`[loadSkills] 已加载 ${skills.length} 个 skill: ${skills.map(describeSkill).join(' | ')}`);
397
+ })
398
+ .catch((err) => {
399
+ console.error('[loadSkills] 加载失败:', err);
400
+ });
401
+ }
402
+ /**
403
+ * 定位 bolloon 仓库内置的 bollharness skill 目录.
404
+ * 向上回溯 cwd, 找第一个包含 src/bollharness/.boll/skills 的祖先.
405
+ * 找不到时返回 null (例如把 bolloon-agent 作为外部依赖安装时).
406
+ */
407
+ findBolloonBuiltinSkillsPath() {
408
+ let dir = this.cwd;
409
+ for (let i = 0; i < 6; i++) {
410
+ const candidate = path.join(dir, 'src', 'bollharness', '.boll', 'skills');
411
+ try {
412
+ if (fsSync.existsSync(candidate) && fsSync.statSync(candidate).isDirectory()) {
413
+ return candidate;
414
+ }
415
+ }
416
+ catch {
417
+ // 忽略 stat 异常, 继续向上
418
+ }
419
+ const parent = path.dirname(dir);
420
+ if (parent === dir)
421
+ break;
422
+ dir = parent;
423
+ }
424
+ return null;
425
+ }
358
426
  async initHarness() {
359
427
  try {
360
428
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
@@ -583,6 +651,85 @@ class PiAgentSession {
583
651
  for (const tool of p2pDocumentTools) {
584
652
  this.tools.set(tool.name, tool);
585
653
  }
654
+ // Shell Exec 工具: 给 AI 跑受限的 shell 命令
655
+ // **只能** 跑白名单内的命令 (git/npm/tsc/vitest/cat/...)
656
+ // **不能** 改禁区路径 (见 shell-guard.ts 的 FORBIDDEN_PATH_PATTERNS)
657
+ // 沙箱 cwd: .bolloon-shell-sandbox/
658
+ this.tools.set('shell_exec', {
659
+ name: 'shell_exec',
660
+ description: '在沙箱里跑 shell 命令. 仅支持白名单内命令: git, npm, npx, tsx, tsc, vitest, cat, head, tail, ls, wc, echo, pwd, date, mkdir, touch. 禁止管道/重定向/rm -rf/sudo. 命中护栏黑名单会被拒.',
661
+ parameters: { command: '可执行文件 (必填, 必须在白名单)', args: '参数数组, 逗号分隔', timeoutMs: '超时毫秒, 默认 30000' },
662
+ execute: async (args) => {
663
+ const cmd = String(args.command || '').trim();
664
+ if (!cmd)
665
+ return { success: false, error: 'command 必填' };
666
+ const argList = String(args.args || '').split(',').map(s => s.trim()).filter(Boolean);
667
+ const timeoutMs = Number(args.timeoutMs) || 30000;
668
+ const result = await shellExec(cmd, argList, { timeoutMs });
669
+ if (result.deniedByGuard) {
670
+ return { success: false, error: result.error };
671
+ }
672
+ if (!result.success) {
673
+ return { success: false, error: result.error, output: result.output };
674
+ }
675
+ return { success: true, output: result.output };
676
+ }
677
+ });
678
+ // self_improve 工具: AI 触发自我改进循环
679
+ // **必须** 在 branchPrefix 命名的分支上工作
680
+ // 心跳事件会自动调用; 用户对话里也能手动调
681
+ this.tools.set('self_improve', {
682
+ name: 'self_improve',
683
+ description: `触发自我改进循环. AI 会在分支 ${getBranchPrefix()}<timestamp> 上工作, 跑 tsc + vitest 验证, 通过后输出分支名给用户审. 冷却期由策略文件决定. 命中护栏禁区的改动会被拒.`,
684
+ parameters: { goal: '本轮改进目标 (1 句话)' },
685
+ execute: async (args) => {
686
+ const goal = String(args.goal || '').trim();
687
+ if (!goal)
688
+ return { success: false, error: 'goal 必填' };
689
+ return await runSelfImproveLoop(goal);
690
+ }
691
+ });
692
+ // list_skills 工具: 列出当前 session 已加载的 skills
693
+ // 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
694
+ this.tools.set('list_skills', {
695
+ name: 'list_skills',
696
+ description: '列出当前 session 已加载的 skills 及其描述. Skills 是从 SKILL.md 文件加载的, 兼容 Anthropic Agent Skills 标准 frontmatter 和 bollharness 现有 frontmatter.',
697
+ parameters: {},
698
+ execute: async () => {
699
+ const skills = this.skillRegistry.list();
700
+ if (skills.length === 0) {
701
+ return {
702
+ success: true,
703
+ output: '当前 session 没有加载任何 skill. 检查 ~/.bolloon/skills/ 或项目 .bolloon/skills/ 目录.',
704
+ };
705
+ }
706
+ const lines = skills.map((s, i) => `${i + 1}. ${s.name} — ${s.description}`);
707
+ return { success: true, output: `已加载 ${skills.length} 个 skill:\n${lines.join('\n')}` };
708
+ }
709
+ });
710
+ // use_skill 工具: 加载指定 skill 的 body 进 LLM context
711
+ // Skills 协议核心: 把 SKILL.md body 作为 Markdown 指南返回, LLM 下一轮按它执行
712
+ this.tools.set('use_skill', {
713
+ name: 'use_skill',
714
+ description: '按名称加载一个 skill, 把它的 SKILL.md body 作为 Markdown 文档返回. 调用后 LLM 会在下一轮按 skill 指南执行. 与 shell_exec / read_document 这些 "能力工具" 不同, use_skill 是 "知识注入".',
715
+ parameters: { name: 'skill 名称 (用 list_skills 查可用的)' },
716
+ execute: async (args) => {
717
+ const name = String(args.name || '').trim();
718
+ if (!name)
719
+ return { success: false, error: 'name 必填' };
720
+ if (!this.skillRegistry.has(name)) {
721
+ const available = this.skillRegistry.list().map((s) => s.name).join(', ');
722
+ return { success: false, error: `skill "${name}" 未找到. 已加载: ${available || '(无)'}` };
723
+ }
724
+ try {
725
+ const body = await this.skillRegistry.execute(name, {});
726
+ return { success: true, output: body };
727
+ }
728
+ catch (e) {
729
+ return { success: false, error: `执行 skill 失败: ${String(e)}` };
730
+ }
731
+ }
732
+ });
586
733
  }
587
734
  async registerP2PDocumentReceiver() {
588
735
  await initDocumentReceiver();
@@ -874,7 +1021,9 @@ ${toolDefs}
874
1021
  // 检查是否需要继续循环处理
875
1022
  // 更严格的判断:只有当回复明确表示需要更多信息时才继续
876
1023
  const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
877
- reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看');
1024
+ reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
1025
+ // 兼容 LLM 用对象字面量输出 tool call (上轮没解析成功时, 至少要继续)
1026
+ reply.includes('tool =>') || reply.includes('[TOOL_CALL]');
878
1027
  const hasError = ['不存在', '找不到', '无法找到', 'not found', 'does not exist',
879
1028
  '错误', 'error', '失败', 'failed'].some(k => reply.includes(k));
880
1029
  const isTooShort = reply.length < 50 && reply.length > 0;
@@ -998,19 +1147,45 @@ Workspace root folder: ${this.cwd}
998
1147
  /调用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
999
1148
  /使用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
1000
1149
  /tool[_\w]*[::]\s*(\w+)\s*\(([^)]*)\)/i,
1001
- /(\w+)\s*\(\s*([^)]*)\s*\)/
1150
+ /(\w+)\s*\(\s*([^)]*)\s*\)/,
1151
+ // 兼容 LLM 输出的对象字面量格式: {tool => "get_identity", args => {...}}
1152
+ /\{\s*tool\s*=>\s*["'](\w+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/,
1153
+ // 兼容: tool => "get_identity" (无 args 包裹)
1154
+ /\btool\s*=>\s*["'](\w+)["']/,
1155
+ // 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
1156
+ /\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
1002
1157
  ];
1003
1158
  for (const pattern of patterns) {
1004
1159
  const match = content.match(pattern);
1005
1160
  if (match) {
1006
1161
  const name = match[1];
1007
- const argsStr = match[2] || '';
1008
- const args = {};
1009
- const argPairs = argsStr.split(',').map(s => s.trim()).filter(Boolean);
1010
- for (const pair of argPairs) {
1011
- const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
1012
- if (key) {
1013
- args[key] = valueParts.join(':') || '';
1162
+ let args = {};
1163
+ const rawArgs = match[2] || '';
1164
+ if (rawArgs && rawArgs.trim().startsWith('{')) {
1165
+ // JSON 形式, 尝试解析
1166
+ try {
1167
+ const parsed = JSON.parse(rawArgs);
1168
+ if (parsed && typeof parsed === 'object') {
1169
+ args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
1170
+ }
1171
+ }
1172
+ catch {
1173
+ // 解析失败就当字符串处理
1174
+ const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
1175
+ for (const pair of argPairs) {
1176
+ const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
1177
+ if (key)
1178
+ args[key] = valueParts.join(':') || '';
1179
+ }
1180
+ }
1181
+ }
1182
+ else if (rawArgs) {
1183
+ // 形参串, 形如 key: value, key2: value2
1184
+ const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
1185
+ for (const pair of argPairs) {
1186
+ const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
1187
+ if (key)
1188
+ args[key] = valueParts.join(':') || '';
1014
1189
  }
1015
1190
  }
1016
1191
  if (this.tools.has(name) || this.tools.has(name.replace(/_/g, '_'))) {
@@ -1541,3 +1716,41 @@ export function resetAgentSession() {
1541
1716
  sessionInstance = null;
1542
1717
  lastIdentityDid = null;
1543
1718
  }
1719
+ /**
1720
+ * 自我改进循环: 在沙箱分支上工作, 输出结果给用户审.
1721
+ *
1722
+ * 不在 PiAgent 实例上的原因: 心跳回调可能没有 agent 实例, 单独函数更易复用.
1723
+ *
1724
+ * **关键不变量**:
1725
+ * 1. AI 不能 push 到 master (shell-guard 黑名单 + git 受保护分支)
1726
+ * 2. 改动必须走沙箱分支 (SELF_IMPROVE_BRANCH_PREFIX)
1727
+ * 3. 6 小时冷却期 (SELF_IMPROVE_COOLDOWN_MS)
1728
+ * 4. 写文件必须经过 shell_exec + 护栏检查
1729
+ */
1730
+ let lastSelfImproveAt = null;
1731
+ export async function runSelfImproveLoop(goal) {
1732
+ const cooldownMs = getCooldownMs();
1733
+ // 1. 冷却期检查
1734
+ if (lastSelfImproveAt && Date.now() - lastSelfImproveAt < cooldownMs) {
1735
+ const waitHrs = Math.ceil((cooldownMs - (Date.now() - lastSelfImproveAt)) / 3600000);
1736
+ return { success: false, error: `自改冷却中, 还需要约 ${waitHrs} 小时` };
1737
+ }
1738
+ // 2. 选源分支 + 新分支名
1739
+ const sourceBranch = 'master';
1740
+ const newBranch = `${getBranchPrefix()}${Date.now()}`;
1741
+ console.log(`[self-improve] 启动自改循环, 目标: ${goal}, 新分支: ${newBranch}`);
1742
+ // 3. 创建分支
1743
+ const r1 = await shellExec('git', ['checkout', sourceBranch]);
1744
+ if (!r1.success)
1745
+ return { success: false, error: `切换到 ${sourceBranch} 失败: ${r1.error}` };
1746
+ const r2 = await shellExec('git', ['checkout', '-b', newBranch]);
1747
+ if (!r2.success)
1748
+ return { success: false, error: `创建分支失败: ${r2.error}` };
1749
+ // 4. 走 task queue: 把"自改"作为一个 task 抛回去, AI 拿到后会用 shell_exec 改
1750
+ // 护栏已经阻止所有禁区改动, 这里只负责登记
1751
+ lastSelfImproveAt = Date.now();
1752
+ return {
1753
+ success: true,
1754
+ output: `✅ 自改分支已创建: ${newBranch}\n目标: ${goal}\n\n**护栏已激活**:\n - 仅允许白名单命令 (git/npm/tsc/vitest/cat/ls/...)\n - 禁止改 src/agents/pi-sdk.ts, shell-guard.ts, src/heartbeat/, src/network/, src/pi-ecosystem-judgment/, package.json, .env 等\n - 6 小时冷却期\n\nAI 接下来会用 shell_exec 工具改源码. 完成后你会在对话里看到 diff 摘要, 手动 git diff master..${newBranch} 审, 满意再 merge.`
1755
+ };
1756
+ }
@@ -0,0 +1,354 @@
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
+ import * as fs from 'fs';
45
+ import * as path from 'path';
46
+ import * as os from 'os';
47
+ // ============================================================================
48
+ // 硬编码兜底 (策略文件读不到时, 用这套)
49
+ // 这是**最后一道防线** - AI 即便能改 ~/.bolloon/ 也没法删这个常量
50
+ // ============================================================================
51
+ const FALLBACK_COMMAND_ALLOWLIST = new Set([
52
+ 'git', 'node', 'npm', 'npx', 'tsx', 'tsc', 'vitest',
53
+ 'cat', 'head', 'tail', 'wc', 'ls', 'echo', 'pwd', 'date',
54
+ 'mkdir', 'touch'
55
+ ]);
56
+ const FALLBACK_PATH_ALLOWLIST = [
57
+ // 自由区: AI 可以改
58
+ 'src/web/client.js',
59
+ 'src/web/style.css',
60
+ 'src/agents/workflow-engine.ts',
61
+ 'src/agents/workflow-pivot-loop.ts',
62
+ 'src/agents/constraint-layer.ts',
63
+ 'src/test/**',
64
+ 'docs/**',
65
+ '*.md',
66
+ 'README.md'
67
+ ];
68
+ const FALLBACK_PATH_DENYLIST = [
69
+ /(^|\/)src\/agents\/pi-sdk\.ts$/, // LLM 抽象层
70
+ /(^|\/)src\/agents\/shell-guard\.ts$/, // 护栏本身
71
+ /(^|\/)src\/agents\/shell-tool\.ts$/, // shell 工具实现
72
+ /(^|\/)src\/heartbeat\//, // 心跳
73
+ /(^|\/)src\/network\//, // P2P / libp2p / iroh
74
+ /(^|\/)src\/pi-ecosystem-judgment\//, // judgment 系统
75
+ /(^|\/)package\.json$/,
76
+ /(^|\/)package-lock\.json$/,
77
+ /(^|\/)tsconfig.*\.json$/,
78
+ /(^|\/)\.env(\.|$)/,
79
+ /(^|\/)\.git\//,
80
+ /(^|\/)\.bolloon\//, // 策略文件 / sessions / persona
81
+ /(^|\/)dist\//,
82
+ /(^|\/)node_modules\//,
83
+ ];
84
+ const FALLBACK_ARG_DENYLIST = [
85
+ /^\s*push\s+(-f|--force)/i,
86
+ /^\s*push\s+origin\s+(master|main)\b/i,
87
+ /^\s*reset\s+--hard\b/i,
88
+ /^\s*clean\s+-fd?\b/i,
89
+ /^\s*--inspect\b/,
90
+ /[|&;`$()<>]/, // shell 元字符
91
+ /\brm\s+-rf?\b/i,
92
+ /\bsudo\b/i,
93
+ /\bsu\b/i,
94
+ /\bcurl\b/i,
95
+ /\bwget\b/i,
96
+ /\.\.\//, // 路径逃逸
97
+ /^\//, // 绝对路径
98
+ /^[a-zA-Z]:\\/, // Windows 绝对路径
99
+ ];
100
+ let cachedPolicy = null;
101
+ let policyLoadedAt = 0;
102
+ const POLICY_TTL_MS = 60_000; // 60 秒缓存 (避免每次 shell_exec 都读盘)
103
+ const POLICY_PATH = path.join(os.homedir(), '.bolloon', 'self-improve-policy.json');
104
+ const POLICY_AUDIT_PATH = path.join(os.homedir(), '.bolloon', 'self-improve-audit.log');
105
+ /**
106
+ * 默认策略模板 - 第一次启动时写到磁盘
107
+ */
108
+ function getDefaultPolicy() {
109
+ return {
110
+ version: 1,
111
+ commandAllowlist: Array.from(FALLBACK_COMMAND_ALLOWLIST),
112
+ pathAllowlist: [...FALLBACK_PATH_ALLOWLIST],
113
+ pathDenylist: FALLBACK_PATH_DENYLIST.map(r => r.source),
114
+ cooldownMs: 6 * 60 * 60 * 1000,
115
+ sandboxCwd: '.bolloon-shell-sandbox',
116
+ branchPrefix: 'agent/self-imp-'
117
+ };
118
+ }
119
+ /**
120
+ * 加载策略 (有缓存)
121
+ * 加载失败返回 null, 调用方应回退到硬编码兜底
122
+ */
123
+ export function loadPolicy(forceReload = false) {
124
+ const now = Date.now();
125
+ if (!forceReload && cachedPolicy && now - policyLoadedAt < POLICY_TTL_MS) {
126
+ return cachedPolicy;
127
+ }
128
+ try {
129
+ if (!fs.existsSync(POLICY_PATH)) {
130
+ // 第一次启动: 写入默认策略
131
+ const dir = path.dirname(POLICY_PATH);
132
+ fs.mkdirSync(dir, { recursive: true });
133
+ fs.writeFileSync(POLICY_PATH, JSON.stringify(getDefaultPolicy(), null, 2));
134
+ console.log(`[shell-guard] 已生成默认策略: ${POLICY_PATH}`);
135
+ }
136
+ const raw = fs.readFileSync(POLICY_PATH, 'utf-8');
137
+ const parsed = JSON.parse(raw);
138
+ // 极简 schema 校验
139
+ if (!parsed.version || !Array.isArray(parsed.commandAllowlist) || !Array.isArray(parsed.pathAllowlist) || !Array.isArray(parsed.pathDenylist)) {
140
+ console.warn('[shell-guard] 策略文件 schema 不对, 用硬编码兜底');
141
+ return null;
142
+ }
143
+ cachedPolicy = parsed;
144
+ policyLoadedAt = now;
145
+ return parsed;
146
+ }
147
+ catch (err) {
148
+ console.warn('[shell-guard] 策略文件加载失败, 用硬编码兜底:', err);
149
+ return null;
150
+ }
151
+ }
152
+ /**
153
+ * 审计日志: 记录所有被拒/被允许的 shell_exec 调用
154
+ */
155
+ export function auditShellCall(result, cmd, args, reason, targetPath) {
156
+ try {
157
+ const dir = path.dirname(POLICY_AUDIT_PATH);
158
+ fs.mkdirSync(dir, { recursive: true });
159
+ const line = JSON.stringify({
160
+ ts: new Date().toISOString(),
161
+ result,
162
+ cmd,
163
+ args,
164
+ reason,
165
+ targetPath
166
+ }) + '\n';
167
+ fs.appendFileSync(POLICY_AUDIT_PATH, line);
168
+ }
169
+ catch {
170
+ // 审计失败不阻塞
171
+ }
172
+ }
173
+ /**
174
+ * 把通配符模式编译成正则
175
+ * * -> [^/]*
176
+ * ** -> .*
177
+ */
178
+ function compileGlob(pattern) {
179
+ // 转义正则元字符, 但保留 * 和 **
180
+ const escaped = pattern
181
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
182
+ .replace(/\*\*/g, '__DOUBLESTAR__')
183
+ .replace(/\*/g, '[^/]*')
184
+ .replace(/__DOUBLESTAR__/g, '.*');
185
+ return new RegExp('^' + escaped + '$');
186
+ }
187
+ /**
188
+ * 检查命令
189
+ */
190
+ export function checkCommand(cmd, args) {
191
+ const policy = loadPolicy();
192
+ const fullCmd = [cmd, ...args].join(' ').trim();
193
+ // ---- 1. 命令黑名单 (策略里有的话) ----
194
+ if (policy?.commandDenylist) {
195
+ for (const denied of policy.commandDenylist) {
196
+ if (cmd === denied || fullCmd.startsWith(denied)) {
197
+ auditShellCall('denied', cmd, args, `命中 commandDenylist: ${denied}`);
198
+ return { allowed: false, reason: `命令 '${cmd}' 命中策略黑名单: ${denied}`, matchedBy: 'cmd-denylist' };
199
+ }
200
+ }
201
+ }
202
+ // ---- 2. 命令白名单 ----
203
+ const allowSet = policy
204
+ ? new Set(policy.commandAllowlist)
205
+ : FALLBACK_COMMAND_ALLOWLIST;
206
+ if (!allowSet.has(cmd)) {
207
+ auditShellCall('denied', cmd, args, `命令 '${cmd}' 不在白名单`);
208
+ return {
209
+ allowed: false,
210
+ reason: `命令 '${cmd}' 不在白名单. 允许: ${Array.from(allowSet).join(', ')}`,
211
+ matchedBy: policy ? 'cmd-allowlist' : 'fallback-deny'
212
+ };
213
+ }
214
+ // ---- 3. 参数黑名单 (策略文件 + 硬编码兜底都查) ----
215
+ const argPatterns = policy
216
+ ? [
217
+ ...FALLBACK_ARG_DENYLIST,
218
+ // 策略里没有专门的 argDenylist 字段, 复用 pathDenylist 模式不太合适
219
+ // 故意**不**让用户在策略里加 arg denylist - 防止 AI 删自己的 arg 限制
220
+ ]
221
+ : FALLBACK_ARG_DENYLIST;
222
+ for (const arg of args) {
223
+ for (const pattern of argPatterns) {
224
+ if (pattern.test(arg)) {
225
+ auditShellCall('denied', cmd, args, `参数 '${arg}' 命中黑名单`);
226
+ return { allowed: false, reason: `参数 '${arg}' 命中黑名单模式 ${pattern}`, matchedBy: 'arg-denylist' };
227
+ }
228
+ }
229
+ }
230
+ // 整条命令再过一遍
231
+ for (const pattern of argPatterns) {
232
+ if (pattern.test(fullCmd)) {
233
+ auditShellCall('denied', cmd, args, `整条命令命中黑名单`);
234
+ return { allowed: false, reason: `整条命令命中黑名单模式 ${pattern}`, matchedBy: 'arg-denylist' };
235
+ }
236
+ }
237
+ auditShellCall('allowed', cmd, args);
238
+ return { allowed: true };
239
+ }
240
+ /**
241
+ * 检查路径
242
+ *
243
+ * 逻辑:
244
+ * 1. denylist 优先: 命中即拒 (用硬编码兜底正则)
245
+ * 2. allowlist: 命中放行
246
+ * 3. 都不命中: 拒 (默认拒绝)
247
+ */
248
+ export function checkWritePath(targetPath) {
249
+ const policy = loadPolicy();
250
+ const normalized = path.normalize(targetPath).replace(/\\/g, '/');
251
+ // ---- 1. 路径黑名单 (硬编码兜底不可绕过) ----
252
+ // 即便策略文件里 denylist 是空的, 硬编码兜底永远生效
253
+ const hardcodedDenylist = FALLBACK_PATH_DENYLIST;
254
+ for (const pattern of hardcodedDenylist) {
255
+ if (pattern.test(normalized)) {
256
+ auditShellCall('denied', '', [], `路径 '${targetPath}' 命中硬编码禁区`, targetPath);
257
+ return { allowed: false, reason: `路径 '${targetPath}' 命中硬编码禁区 ${pattern}`, matchedBy: 'fallback-deny' };
258
+ }
259
+ }
260
+ // 策略文件里的额外 denylist
261
+ if (policy?.pathDenylist) {
262
+ for (const patternStr of policy.pathDenylist) {
263
+ try {
264
+ const regex = compileGlob(patternStr);
265
+ if (regex.test(normalized)) {
266
+ auditShellCall('denied', '', [], `路径 '${targetPath}' 命中策略 denylist`, targetPath);
267
+ return { allowed: false, reason: `路径 '${targetPath}' 命中策略 denylist: ${patternStr}`, matchedBy: 'path-denylist' };
268
+ }
269
+ }
270
+ catch {
271
+ // 编译失败的模式跳过
272
+ }
273
+ }
274
+ }
275
+ // ---- 2. 路径白名单 (来自策略或兜底) ----
276
+ const allowlist = policy?.pathAllowlist || FALLBACK_PATH_ALLOWLIST;
277
+ for (const patternStr of allowlist) {
278
+ try {
279
+ const regex = compileGlob(patternStr);
280
+ if (regex.test(normalized)) {
281
+ auditShellCall('allowed', '', [], undefined, targetPath);
282
+ return { allowed: true, matchedBy: 'path-allowlist' };
283
+ }
284
+ }
285
+ catch {
286
+ // 编译失败的模式跳过
287
+ }
288
+ }
289
+ // 都不命中: 默认拒绝
290
+ auditShellCall('denied', '', [], `路径 '${targetPath}' 不在任何 allowlist 中`, targetPath);
291
+ return {
292
+ allowed: false,
293
+ reason: `路径 '${targetPath}' 不在白名单. 允许: ${allowlist.join(', ')}`,
294
+ matchedBy: 'path-allowlist'
295
+ };
296
+ }
297
+ // ============================================================================
298
+ // 运行时配置 (从策略文件读, 但有兜底)
299
+ // ============================================================================
300
+ /**
301
+ * 自改分支名前缀
302
+ */
303
+ export function getBranchPrefix() {
304
+ const policy = loadPolicy();
305
+ return policy?.branchPrefix || 'agent/self-imp-';
306
+ }
307
+ /**
308
+ * 冷却期 (毫秒)
309
+ */
310
+ export function getCooldownMs() {
311
+ const policy = loadPolicy();
312
+ return policy?.cooldownMs || 6 * 60 * 60 * 1000;
313
+ }
314
+ /**
315
+ * 沙箱工作目录
316
+ */
317
+ export function getSandboxCwd() {
318
+ const policy = loadPolicy();
319
+ const rel = policy?.sandboxCwd || '.bolloon-shell-sandbox';
320
+ return path.resolve(process.cwd(), rel);
321
+ }
322
+ // ============================================================================
323
+ // 兼容旧 API - 保留原导出名
324
+ // ============================================================================
325
+ /** @deprecated 用 getBranchPrefix() */
326
+ export const SELF_IMPROVE_BRANCH_PREFIX = 'agent/self-imp-';
327
+ /** @deprecated 用 getCooldownMs() */
328
+ export const SELF_IMPROVE_COOLDOWN_MS = 6 * 60 * 60 * 1000;
329
+ /** @deprecated 用 getSandboxCwd() */
330
+ export const SHELL_SANDBOX_CWD = path.resolve(process.cwd(), '.bolloon-shell-sandbox');
331
+ // ============================================================================
332
+ // 写策略 / 审计路径 (供 API 端点用)
333
+ // ============================================================================
334
+ export const POLICY_AUDIT_PATH_PUBLIC = POLICY_AUDIT_PATH;
335
+ /**
336
+ * 把新策略写到磁盘, 立即清缓存让下次 loadPolicy() 重读
337
+ * **只供人手动调用**
338
+ */
339
+ export function writePolicy(newPolicy) {
340
+ try {
341
+ newPolicy.version = (newPolicy.version || 0) + 1;
342
+ const dir = path.dirname(POLICY_PATH);
343
+ fs.mkdirSync(dir, { recursive: true });
344
+ fs.writeFileSync(POLICY_PATH, JSON.stringify(newPolicy, null, 2));
345
+ cachedPolicy = null; // 清缓存
346
+ policyLoadedAt = 0;
347
+ console.log(`[shell-guard] 策略已更新, version=${newPolicy.version}`);
348
+ return true;
349
+ }
350
+ catch (err) {
351
+ console.error('[shell-guard] 写策略失败:', err);
352
+ return false;
353
+ }
354
+ }