@bolloon/bolloon-agent 0.1.13 → 0.1.14
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/agents/pi-sdk.js +185 -0
- package/dist/agents/shell-guard.js +354 -0
- package/dist/agents/shell-tool.js +83 -0
- package/dist/agents/skill-loader.js +174 -0
- package/dist/bollharness-integration/context-chain-router.js +3 -3
- package/dist/bollharness-integration/context-router.js +1 -1
- package/dist/heartbeat/Watchdog.js +7 -5
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/self-improve-bus.js +85 -0
- package/dist/pi-ecosystem-judgment/index.js +1 -2
- package/dist/utils/auto-update.js +44 -12
- package/dist/web/client.js +839 -103
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +88 -8
- package/dist/web/server.js +2360 -0
- package/dist/web/style.css +506 -9
- package/package.json +2 -2
- package/scripts/build-cli.js +11 -1
- package/src/agents/pi-sdk.ts +196 -0
- package/src/agents/shell-guard.ts +417 -0
- package/src/agents/shell-tool.ts +103 -0
- package/src/agents/skill-loader.ts +202 -0
- package/src/bollharness-integration/context-chain-router.ts +3 -3
- package/src/bollharness-integration/context-router.ts +1 -1
- package/src/heartbeat/Watchdog.ts +7 -5
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/self-improve-bus.ts +110 -0
- package/src/types.d.ts +12 -0
- package/src/utils/auto-update.ts +45 -14
- package/src/web/client.js +839 -103
- package/src/web/index.html +88 -8
- package/src/web/server.ts +427 -101
- package/src/web/style.css +506 -9
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
- package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
- package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
- package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
- package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
- package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
- package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
- package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
- package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
package/src/agents/pi-sdk.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
import * as fsSync from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import { documentReader, DocumentContent } from '../documents/reader.js';
|
|
10
11
|
import { getMinimax } from '../constraints/index.js';
|
|
@@ -14,6 +15,8 @@ import { WorkflowEngine, WorkflowStep, StepResult, Workflow } from './workflow-e
|
|
|
14
15
|
import { DeepThinkingEngine, AgentCoordinator, type ThinkResult, type AgentResult } from '@bolloon/constraint-runtime';
|
|
15
16
|
import { WorkflowPivotLoop, createDefaultPivotConfig, type PivotLoopConfig, type LoopResult } from './workflow-pivot-loop.js';
|
|
16
17
|
import { p2pDocumentTools, initDocumentReceiver } from './p2p-document-tools.js';
|
|
18
|
+
import { shellExec } from './shell-tool.js';
|
|
19
|
+
import { getBranchPrefix, getCooldownMs } from './shell-guard.js';
|
|
17
20
|
import {
|
|
18
21
|
DiscoveredAgentsManager,
|
|
19
22
|
SocialHeartbeat,
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
type GlobalSharedContext
|
|
37
40
|
} from '../social/global-shared-context.js';
|
|
38
41
|
import { Session, SkillRegistry, saveSession, loadSession, type Skill, type StoredSession } from '@bolloon/constraint-runtime';
|
|
42
|
+
import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
|
|
39
43
|
|
|
40
44
|
// Pi Ecosystem Integration (lazy imports - initialized on demand)
|
|
41
45
|
// Functions from: createGoal, getCurrentGoal, completeCurrentGoal, failCurrentGoal, getGoalStats, getQueueSummary
|
|
@@ -46,6 +50,12 @@ export interface AgentSessionConfig {
|
|
|
46
50
|
identityDoc?: IdentityDoc;
|
|
47
51
|
usePivotLoop?: boolean;
|
|
48
52
|
pivotLoopConfig?: PivotLoopConfig;
|
|
53
|
+
/**
|
|
54
|
+
* Skills 加载目录列表, 后者覆盖前者同名 skill.
|
|
55
|
+
* 留空时使用 defaultSkillPaths() 推断的默认路径
|
|
56
|
+
* ( ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/ )
|
|
57
|
+
*/
|
|
58
|
+
skillsPaths?: string[];
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
export interface IdentityDoc {
|
|
@@ -575,9 +585,72 @@ class PiAgentSession implements AgentSession {
|
|
|
575
585
|
this.initSession();
|
|
576
586
|
initDocumentReceiver();
|
|
577
587
|
this.registerTools();
|
|
588
|
+
this.loadSkills(config.skillsPaths);
|
|
578
589
|
this.initHarness();
|
|
579
590
|
}
|
|
580
591
|
|
|
592
|
+
/**
|
|
593
|
+
* 从 SKILL.md 目录加载 skills 进 skillRegistry.
|
|
594
|
+
*
|
|
595
|
+
* 路径解析优先级 (后者覆盖前者同名 skill):
|
|
596
|
+
* 1. 显式传入的 skillsPaths
|
|
597
|
+
* 2. ~/.bolloon/skills/ 全局用户级
|
|
598
|
+
* 3. <cwd>/.bolloon/skills/ 项目级
|
|
599
|
+
* 4. ~/.boll/skills/ 全局 (兼容 bollharness 旧用户)
|
|
600
|
+
* 5. <bolloon-repo>/src/bollharness/.boll/skills/ bolloon 仓库内置 skill
|
|
601
|
+
* (bolloon 项目本身用 pi-sdk 写核心, 这 19 个 skill 视为项目级 builtin)
|
|
602
|
+
*
|
|
603
|
+
* 静默忽略不存在的目录.
|
|
604
|
+
*/
|
|
605
|
+
private loadSkills(paths?: string[]): void {
|
|
606
|
+
let resolved: string[];
|
|
607
|
+
if (paths && paths.length > 0) {
|
|
608
|
+
resolved = paths;
|
|
609
|
+
} else {
|
|
610
|
+
resolved = [
|
|
611
|
+
...defaultSkillPaths(os.homedir(), this.cwd),
|
|
612
|
+
// bolloon 仓库内置 skill (相对本 npm 包的位置)
|
|
613
|
+
this.findBolloonBuiltinSkillsPath(),
|
|
614
|
+
].filter((p): p is string => Boolean(p));
|
|
615
|
+
}
|
|
616
|
+
loadSkillsFromPaths(resolved)
|
|
617
|
+
.then((skills) => {
|
|
618
|
+
for (const s of skills) {
|
|
619
|
+
if (this.skillRegistry.has(s.name)) {
|
|
620
|
+
this.skillRegistry.unregister(s.name);
|
|
621
|
+
}
|
|
622
|
+
this.skillRegistry.register(s);
|
|
623
|
+
}
|
|
624
|
+
console.log(`[loadSkills] 已加载 ${skills.length} 个 skill: ${skills.map(describeSkill).join(' | ')}`);
|
|
625
|
+
})
|
|
626
|
+
.catch((err) => {
|
|
627
|
+
console.error('[loadSkills] 加载失败:', err);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* 定位 bolloon 仓库内置的 bollharness skill 目录.
|
|
633
|
+
* 向上回溯 cwd, 找第一个包含 src/bollharness/.boll/skills 的祖先.
|
|
634
|
+
* 找不到时返回 null (例如把 bolloon-agent 作为外部依赖安装时).
|
|
635
|
+
*/
|
|
636
|
+
private findBolloonBuiltinSkillsPath(): string | null {
|
|
637
|
+
let dir = this.cwd;
|
|
638
|
+
for (let i = 0; i < 6; i++) {
|
|
639
|
+
const candidate = path.join(dir, 'src', 'bollharness', '.boll', 'skills');
|
|
640
|
+
try {
|
|
641
|
+
if (fsSync.existsSync(candidate) && fsSync.statSync(candidate).isDirectory()) {
|
|
642
|
+
return candidate;
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
// 忽略 stat 异常, 继续向上
|
|
646
|
+
}
|
|
647
|
+
const parent = path.dirname(dir);
|
|
648
|
+
if (parent === dir) break;
|
|
649
|
+
dir = parent;
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
581
654
|
private async initHarness(): Promise<void> {
|
|
582
655
|
try {
|
|
583
656
|
const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
|
|
@@ -808,6 +881,86 @@ class PiAgentSession implements AgentSession {
|
|
|
808
881
|
for (const tool of p2pDocumentTools) {
|
|
809
882
|
this.tools.set(tool.name, tool);
|
|
810
883
|
}
|
|
884
|
+
|
|
885
|
+
// Shell Exec 工具: 给 AI 跑受限的 shell 命令
|
|
886
|
+
// **只能** 跑白名单内的命令 (git/npm/tsc/vitest/cat/...)
|
|
887
|
+
// **不能** 改禁区路径 (见 shell-guard.ts 的 FORBIDDEN_PATH_PATTERNS)
|
|
888
|
+
// 沙箱 cwd: .bolloon-shell-sandbox/
|
|
889
|
+
this.tools.set('shell_exec', {
|
|
890
|
+
name: 'shell_exec',
|
|
891
|
+
description: '在沙箱里跑 shell 命令. 仅支持白名单内命令: git, npm, npx, tsx, tsc, vitest, cat, head, tail, ls, wc, echo, pwd, date, mkdir, touch. 禁止管道/重定向/rm -rf/sudo. 命中护栏黑名单会被拒.',
|
|
892
|
+
parameters: { command: '可执行文件 (必填, 必须在白名单)', args: '参数数组, 逗号分隔', timeoutMs: '超时毫秒, 默认 30000' },
|
|
893
|
+
execute: async (args) => {
|
|
894
|
+
const cmd = String(args.command || '').trim();
|
|
895
|
+
if (!cmd) return { success: false, error: 'command 必填' };
|
|
896
|
+
const argList = String(args.args || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
897
|
+
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
898
|
+
|
|
899
|
+
const result = await shellExec(cmd, argList, { timeoutMs });
|
|
900
|
+
if (result.deniedByGuard) {
|
|
901
|
+
return { success: false, error: result.error };
|
|
902
|
+
}
|
|
903
|
+
if (!result.success) {
|
|
904
|
+
return { success: false, error: result.error, output: result.output };
|
|
905
|
+
}
|
|
906
|
+
return { success: true, output: result.output };
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// self_improve 工具: AI 触发自我改进循环
|
|
911
|
+
// **必须** 在 branchPrefix 命名的分支上工作
|
|
912
|
+
// 心跳事件会自动调用; 用户对话里也能手动调
|
|
913
|
+
this.tools.set('self_improve', {
|
|
914
|
+
name: 'self_improve',
|
|
915
|
+
description: `触发自我改进循环. AI 会在分支 ${getBranchPrefix()}<timestamp> 上工作, 跑 tsc + vitest 验证, 通过后输出分支名给用户审. 冷却期由策略文件决定. 命中护栏禁区的改动会被拒.`,
|
|
916
|
+
parameters: { goal: '本轮改进目标 (1 句话)' },
|
|
917
|
+
execute: async (args) => {
|
|
918
|
+
const goal = String(args.goal || '').trim();
|
|
919
|
+
if (!goal) return { success: false, error: 'goal 必填' };
|
|
920
|
+
return await runSelfImproveLoop(goal);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// list_skills 工具: 列出当前 session 已加载的 skills
|
|
925
|
+
// 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
|
|
926
|
+
this.tools.set('list_skills', {
|
|
927
|
+
name: 'list_skills',
|
|
928
|
+
description: '列出当前 session 已加载的 skills 及其描述. Skills 是从 SKILL.md 文件加载的, 兼容 Anthropic Agent Skills 标准 frontmatter 和 bollharness 现有 frontmatter.',
|
|
929
|
+
parameters: {},
|
|
930
|
+
execute: async () => {
|
|
931
|
+
const skills = this.skillRegistry.list();
|
|
932
|
+
if (skills.length === 0) {
|
|
933
|
+
return {
|
|
934
|
+
success: true,
|
|
935
|
+
output: '当前 session 没有加载任何 skill. 检查 ~/.bolloon/skills/ 或项目 .bolloon/skills/ 目录.',
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const lines = skills.map((s, i) => `${i + 1}. ${s.name} — ${s.description}`);
|
|
939
|
+
return { success: true, output: `已加载 ${skills.length} 个 skill:\n${lines.join('\n')}` };
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// use_skill 工具: 加载指定 skill 的 body 进 LLM context
|
|
944
|
+
// Skills 协议核心: 把 SKILL.md body 作为 Markdown 指南返回, LLM 下一轮按它执行
|
|
945
|
+
this.tools.set('use_skill', {
|
|
946
|
+
name: 'use_skill',
|
|
947
|
+
description: '按名称加载一个 skill, 把它的 SKILL.md body 作为 Markdown 文档返回. 调用后 LLM 会在下一轮按 skill 指南执行. 与 shell_exec / read_document 这些 "能力工具" 不同, use_skill 是 "知识注入".',
|
|
948
|
+
parameters: { name: 'skill 名称 (用 list_skills 查可用的)' },
|
|
949
|
+
execute: async (args) => {
|
|
950
|
+
const name = String(args.name || '').trim();
|
|
951
|
+
if (!name) return { success: false, error: 'name 必填' };
|
|
952
|
+
if (!this.skillRegistry.has(name)) {
|
|
953
|
+
const available = this.skillRegistry.list().map((s) => s.name).join(', ');
|
|
954
|
+
return { success: false, error: `skill "${name}" 未找到. 已加载: ${available || '(无)'}` };
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const body = await this.skillRegistry.execute(name, {});
|
|
958
|
+
return { success: true, output: body };
|
|
959
|
+
} catch (e) {
|
|
960
|
+
return { success: false, error: `执行 skill 失败: ${String(e)}` };
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
});
|
|
811
964
|
}
|
|
812
965
|
|
|
813
966
|
private async registerP2PDocumentReceiver(): Promise<void> {
|
|
@@ -1919,3 +2072,46 @@ export function resetAgentSession(): void {
|
|
|
1919
2072
|
sessionInstance = null;
|
|
1920
2073
|
lastIdentityDid = null;
|
|
1921
2074
|
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* 自我改进循环: 在沙箱分支上工作, 输出结果给用户审.
|
|
2078
|
+
*
|
|
2079
|
+
* 不在 PiAgent 实例上的原因: 心跳回调可能没有 agent 实例, 单独函数更易复用.
|
|
2080
|
+
*
|
|
2081
|
+
* **关键不变量**:
|
|
2082
|
+
* 1. AI 不能 push 到 master (shell-guard 黑名单 + git 受保护分支)
|
|
2083
|
+
* 2. 改动必须走沙箱分支 (SELF_IMPROVE_BRANCH_PREFIX)
|
|
2084
|
+
* 3. 6 小时冷却期 (SELF_IMPROVE_COOLDOWN_MS)
|
|
2085
|
+
* 4. 写文件必须经过 shell_exec + 护栏检查
|
|
2086
|
+
*/
|
|
2087
|
+
let lastSelfImproveAt: number | null = null;
|
|
2088
|
+
|
|
2089
|
+
export async function runSelfImproveLoop(goal: string): Promise<{ success: boolean; output?: string; error?: string }> {
|
|
2090
|
+
const cooldownMs = getCooldownMs();
|
|
2091
|
+
// 1. 冷却期检查
|
|
2092
|
+
if (lastSelfImproveAt && Date.now() - lastSelfImproveAt < cooldownMs) {
|
|
2093
|
+
const waitHrs = Math.ceil((cooldownMs - (Date.now() - lastSelfImproveAt)) / 3600000);
|
|
2094
|
+
return { success: false, error: `自改冷却中, 还需要约 ${waitHrs} 小时` };
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// 2. 选源分支 + 新分支名
|
|
2098
|
+
const sourceBranch = 'master';
|
|
2099
|
+
const newBranch = `${getBranchPrefix()}${Date.now()}`;
|
|
2100
|
+
|
|
2101
|
+
console.log(`[self-improve] 启动自改循环, 目标: ${goal}, 新分支: ${newBranch}`);
|
|
2102
|
+
|
|
2103
|
+
// 3. 创建分支
|
|
2104
|
+
const r1 = await shellExec('git', ['checkout', sourceBranch]);
|
|
2105
|
+
if (!r1.success) return { success: false, error: `切换到 ${sourceBranch} 失败: ${r1.error}` };
|
|
2106
|
+
|
|
2107
|
+
const r2 = await shellExec('git', ['checkout', '-b', newBranch]);
|
|
2108
|
+
if (!r2.success) return { success: false, error: `创建分支失败: ${r2.error}` };
|
|
2109
|
+
|
|
2110
|
+
// 4. 走 task queue: 把"自改"作为一个 task 抛回去, AI 拿到后会用 shell_exec 改
|
|
2111
|
+
// 护栏已经阻止所有禁区改动, 这里只负责登记
|
|
2112
|
+
lastSelfImproveAt = Date.now();
|
|
2113
|
+
return {
|
|
2114
|
+
success: true,
|
|
2115
|
+
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.`
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
@@ -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
|
+
}
|