@bolloon/bolloon-agent 0.1.35 → 0.1.36
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/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +45 -4
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +435 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +3 -0
- package/dist/bootstrap/context-collector.js +17 -1
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +86 -24
- package/dist/bootstrap/project-context.js +10 -3
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +38 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +103 -16
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +48 -0
- package/dist/pi-ecosystem-judgment/injection-gate.js +54 -12
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/context-router-tool.js +1 -1
- package/dist/security/input-scanner.js +223 -0
- package/dist/web/client.js +3677 -4161
- 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 +58 -67
- package/dist/web/server.js +299 -117
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +4 -0
- package/package.json +1 -1
- package/scripts/auto-evolve-loop.ts +105 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/validate-system-prompt.ts +142 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +461 -28
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +3 -0
- package/src/bootstrap/context-collector.ts +30 -1
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +139 -26
- package/src/bootstrap/project-context.ts +11 -3
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +36 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +111 -16
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/context-router-tool.ts +1 -1
- package/src/security/input-scanner.ts +287 -0
- package/src/web/client.js +271 -2817
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +58 -67
- package/src/web/server.ts +301 -141
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -0
- package/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
|
@@ -29,6 +29,18 @@ export interface BolloonContext {
|
|
|
29
29
|
projectRoot: string;
|
|
30
30
|
projectName: string;
|
|
31
31
|
bolloonMd: string | null;
|
|
32
|
+
// 1b. Bolloon.md 4 级层次 (Claude Code 论文对齐, 严格 1:1)
|
|
33
|
+
// managed: /etc/bolloon/Bolloon.md (企业 IT 部署)
|
|
34
|
+
// user: ~/.bolloon/Bolloon.md
|
|
35
|
+
// project: <cwd>/Bolloon.md (Bolloon.md 兼容)
|
|
36
|
+
// local: <cwd>/CLAUDE.local.md
|
|
37
|
+
hierarchy: {
|
|
38
|
+
managed: string | null;
|
|
39
|
+
user: string | null;
|
|
40
|
+
project: string | null;
|
|
41
|
+
local: string | null;
|
|
42
|
+
merged: string; // 已按优先级合并的 markdown 片段
|
|
43
|
+
};
|
|
32
44
|
// 2. git 层
|
|
33
45
|
git: {
|
|
34
46
|
branch: string;
|
|
@@ -106,6 +118,21 @@ async function collectProject(cwd: string, bolloonMdMaxBytes: number) {
|
|
|
106
118
|
return { projectName, bolloonMd };
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
async function collectHierarchy(cwd: string): Promise<BolloonContext['hierarchy']> {
|
|
122
|
+
// 动态 import 避免循环依赖
|
|
123
|
+
const { collectHierarchyLayers, mergeHierarchyLayers } = await import('./context-hierarchy.js');
|
|
124
|
+
try {
|
|
125
|
+
const layers = await collectHierarchyLayers({ cwd });
|
|
126
|
+
const merged = mergeHierarchyLayers(layers, { maxChars: DEFAULT_HIERARCHY_MERGED_MAX });
|
|
127
|
+
return { ...layers, merged };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.warn('[context-collector] collectHierarchy failed (silent):', err);
|
|
130
|
+
return { managed: null, user: null, project: null, local: null, merged: '' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const DEFAULT_HIERARCHY_MERGED_MAX = 2000; // 4000 → 2000 (P-Action 4), 跟 context-hierarchy.ts DEFAULT_MERGE_MAX_CHARS 对齐
|
|
135
|
+
|
|
109
136
|
async function collectGit(cwd: string, limit: number): Promise<BolloonContext['git']> {
|
|
110
137
|
const branchOut = await safeExecFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
111
138
|
if (!branchOut) return null;
|
|
@@ -296,13 +323,14 @@ export async function collectBolloonContext(opts: CollectOptions): Promise<Bollo
|
|
|
296
323
|
topValuesLimit = 10,
|
|
297
324
|
} = opts;
|
|
298
325
|
// 并行收集 (除 judgments 因为要动态 import + 依赖其它)
|
|
299
|
-
const [project, git, persona, skills, env, pending] = await Promise.all([
|
|
326
|
+
const [project, git, persona, skills, env, pending, hierarchy] = await Promise.all([
|
|
300
327
|
collectProject(cwd, bolloonMdMaxBytes),
|
|
301
328
|
collectGit(cwd, gitCommitLimit),
|
|
302
329
|
collectPersona(),
|
|
303
330
|
collectSkills(),
|
|
304
331
|
Promise.resolve(collectEnv()),
|
|
305
332
|
collectPending(opts),
|
|
333
|
+
collectHierarchy(cwd),
|
|
306
334
|
]);
|
|
307
335
|
// judgments 单独调 (内部 import)
|
|
308
336
|
const judgmentsSummary = await collectJudgmentsSummary(topValuesLimit);
|
|
@@ -311,6 +339,7 @@ export async function collectBolloonContext(opts: CollectOptions): Promise<Bollo
|
|
|
311
339
|
projectRoot: cwd,
|
|
312
340
|
projectName: project.projectName,
|
|
313
341
|
bolloonMd: project.bolloonMd,
|
|
342
|
+
hierarchy,
|
|
314
343
|
git,
|
|
315
344
|
persona,
|
|
316
345
|
judgmentsSummary,
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Hierarchy — 4 级 Bolloon.md 查找 + 合并优先级
|
|
3
|
+
*
|
|
4
|
+
* 设计: 严格对齐 Claude Code 论文 4 级层次
|
|
5
|
+
* 1. Managed — /etc/bolloon/Bolloon.md (企业 IT 部署, 系统级)
|
|
6
|
+
* 2. User — ~/.bolloon/Bolloon.md (用户级, 跨项目)
|
|
7
|
+
* 3. Project — <cwd>/Bolloon.md 或 .claude/rules/*.md
|
|
8
|
+
* 4. Local — <cwd>/CLAUDE.local.md (个人覆盖, .gitignore)
|
|
9
|
+
*
|
|
10
|
+
* 注入约定 (论文):
|
|
11
|
+
* - 拼到 system prompt 顶部 (作为 user context, 概率性遵守)
|
|
12
|
+
* - 不混入 user 消息 (避免 prompt injection)
|
|
13
|
+
* - 截断时反向砍 local → project → user → managed (优先保 managed, 因为是 bolloon 自身约束)
|
|
14
|
+
*
|
|
15
|
+
* 兼容:
|
|
16
|
+
* - 同时识别 Bolloon.md 和 Bolloon.md (论文用前者, bolloon 历史用后者)
|
|
17
|
+
* - 文件名按优先级: Bolloon.md > Bolloon.md
|
|
18
|
+
*
|
|
19
|
+
* 失败静默: 任何 IO 错误 → 返回 null, 不阻塞主流程.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from 'fs/promises';
|
|
23
|
+
import * as os from 'os';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// 4 级路径配置
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
export interface HierarchyPaths {
|
|
31
|
+
managed: string; // /etc/bolloon/Bolloon.md
|
|
32
|
+
user: string; // ~/.bolloon/Bolloon.md
|
|
33
|
+
project: string; // <cwd>/Bolloon.md
|
|
34
|
+
projectRulesDir: string; // <cwd>/.claude/rules/
|
|
35
|
+
local: string; // <cwd>/CLAUDE.local.md
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HierarchyChars {
|
|
39
|
+
managed: number;
|
|
40
|
+
user: number;
|
|
41
|
+
project: number;
|
|
42
|
+
local: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HierarchyLimits {
|
|
46
|
+
paths?: Partial<HierarchyPaths>;
|
|
47
|
+
maxChars?: Partial<HierarchyChars>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_PATHS: HierarchyPaths = {
|
|
51
|
+
managed: '/etc/bolloon/Bolloon.md',
|
|
52
|
+
user: '', // 用 resolveUserPath 填
|
|
53
|
+
project: '', // 用 resolveProjectPaths 填
|
|
54
|
+
projectRulesDir: '',
|
|
55
|
+
local: '',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 单层字符上限 (P-Action 4 收紧, 阶段 0 把 4 级层次控制在 ≤ 2KB).
|
|
60
|
+
* 反向截断策略保证 managed 优先, 单层超限由 truncate() 截断.
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_MAX_CHARS: HierarchyChars = {
|
|
63
|
+
managed: 700, // 1500 → 700, 保 bolloon 自身约束
|
|
64
|
+
user: 500, // 1500 → 500, 跨项目偏好
|
|
65
|
+
project: 500, // 2500 → 500, 项目规则
|
|
66
|
+
local: 300, // 1500 → 300, 个人覆盖
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ============================================================
|
|
70
|
+
// 路径解析
|
|
71
|
+
// ============================================================
|
|
72
|
+
|
|
73
|
+
export function resolveUserPath(home?: string): string {
|
|
74
|
+
const h = home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
|
|
75
|
+
return path.join(h, '.bolloon', 'Bolloon.md');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveProjectPaths(cwd: string): { project: string; projectRulesDir: string; local: string } {
|
|
79
|
+
return {
|
|
80
|
+
project: path.join(cwd, 'Bolloon.md'),
|
|
81
|
+
projectRulesDir: path.join(cwd, '.claude', 'rules'),
|
|
82
|
+
local: path.join(cwd, 'CLAUDE.local.md'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================
|
|
87
|
+
// 单层读取 (优先 Bolloon.md, fallback Bolloon.md)
|
|
88
|
+
// ============================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 读 1 个文件路径, 失败/缺失返回 null.
|
|
92
|
+
* 字符上限由调用方截断 (这里只负责读全文).
|
|
93
|
+
*/
|
|
94
|
+
async function readRuleFile(filePath: string): Promise<string | null> {
|
|
95
|
+
try {
|
|
96
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
97
|
+
return content.length > 0 ? content : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 读 project 层: 优先 Bolloon.md, fallback Bolloon.md, 最后尝试 .claude/rules/*.md
|
|
105
|
+
*/
|
|
106
|
+
async function readProjectLayer(projectPath: string, projectRulesDir: string, maxChars: number): Promise<string | null> {
|
|
107
|
+
// 1. Bolloon.md
|
|
108
|
+
let content = await readRuleFile(projectPath);
|
|
109
|
+
if (content) return truncate(content, maxChars);
|
|
110
|
+
|
|
111
|
+
// 2. Bolloon.md (向后兼容)
|
|
112
|
+
const bolloonMdPath = path.join(path.dirname(projectPath), 'Bolloon.md');
|
|
113
|
+
content = await readRuleFile(bolloonMdPath);
|
|
114
|
+
if (content) return truncate(content, maxChars);
|
|
115
|
+
|
|
116
|
+
// 3. .claude/rules/*.md (合并所有)
|
|
117
|
+
try {
|
|
118
|
+
const entries = await fs.readdir(projectRulesDir, { withFileTypes: true });
|
|
119
|
+
const ruleFiles = entries
|
|
120
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
121
|
+
.map((e) => path.join(projectRulesDir, e.name))
|
|
122
|
+
.sort();
|
|
123
|
+
if (ruleFiles.length > 0) {
|
|
124
|
+
const parts: string[] = [];
|
|
125
|
+
let total = 0;
|
|
126
|
+
for (const f of ruleFiles) {
|
|
127
|
+
const c = await readRuleFile(f);
|
|
128
|
+
if (c) {
|
|
129
|
+
parts.push(`### ${path.basename(f, '.md')}\n${c}`);
|
|
130
|
+
total += c.length;
|
|
131
|
+
if (total >= maxChars) break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (parts.length > 0) return truncate(parts.join('\n\n'), maxChars);
|
|
135
|
+
}
|
|
136
|
+
} catch { /* dir 不存在 */ }
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================
|
|
142
|
+
// 4 层收集
|
|
143
|
+
// ============================================================
|
|
144
|
+
|
|
145
|
+
export interface HierarchyLayers {
|
|
146
|
+
managed: string | null; // /etc/bolloon/Bolloon.md
|
|
147
|
+
user: string | null; // ~/.bolloon/Bolloon.md
|
|
148
|
+
project: string | null; // <cwd>/Bolloon.md
|
|
149
|
+
local: string | null; // <cwd>/CLAUDE.local.md
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface CollectHierarchyOptions {
|
|
153
|
+
cwd: string;
|
|
154
|
+
limits?: HierarchyLimits;
|
|
155
|
+
/** 覆盖 HOME (测试用) */
|
|
156
|
+
home?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 并行读 4 层, 任何一层失败 = null (不阻塞其他).
|
|
161
|
+
*/
|
|
162
|
+
export async function collectHierarchyLayers(opts: CollectHierarchyOptions): Promise<HierarchyLayers> {
|
|
163
|
+
const limits: HierarchyLimits['maxChars'] = { ...DEFAULT_MAX_CHARS, ...(opts.limits?.maxChars ?? {}) };
|
|
164
|
+
|
|
165
|
+
const home = opts.home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
|
|
166
|
+
const managedPath = opts.limits?.paths?.managed ?? DEFAULT_PATHS.managed;
|
|
167
|
+
const userPath = opts.limits?.paths?.user ?? resolveUserPath(home);
|
|
168
|
+
const projectPath = opts.limits?.paths?.project ?? path.join(opts.cwd, 'Bolloon.md');
|
|
169
|
+
const projectRulesDir = opts.limits?.paths?.projectRulesDir ?? path.join(opts.cwd, '.claude', 'rules');
|
|
170
|
+
const localPath = opts.limits?.paths?.local ?? path.join(opts.cwd, 'CLAUDE.local.md');
|
|
171
|
+
|
|
172
|
+
const [managed, user, project, local] = await Promise.all([
|
|
173
|
+
readRuleFile(managedPath).then((c) => c ? truncate(c, limits.managed!) : null),
|
|
174
|
+
readRuleFile(userPath).then((c) => c ? truncate(c, limits.user!) : null),
|
|
175
|
+
readProjectLayer(projectPath, projectRulesDir, limits.project!),
|
|
176
|
+
readRuleFile(localPath).then((c) => c ? truncate(c, limits.local!) : null),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
return { managed, user, project, local };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================
|
|
183
|
+
// 合并 + 截断
|
|
184
|
+
// ============================================================
|
|
185
|
+
|
|
186
|
+
export interface MergeOptions {
|
|
187
|
+
/** 总字符上限 (默认 8000, 之前 project-context.ts 用 4000) */
|
|
188
|
+
maxChars?: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 4 级合并总字符上限 (P-Action 4 收紧, ≤ 2KB ≈ 500 tokens).
|
|
193
|
+
* 反向截断 (local → project → user → managed) 保证 managed 不丢.
|
|
194
|
+
*/
|
|
195
|
+
export const DEFAULT_MERGE_MAX_CHARS = 2000;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 按 4 级顺序 (managed → user → project → local) 拼接为 markdown 片段.
|
|
199
|
+
*
|
|
200
|
+
* 截断策略: 反向砍 (local → project → user → managed), 优先保 managed.
|
|
201
|
+
* - 总长 < maxChars: 不截断
|
|
202
|
+
* - 总长 ≥ maxChars: 按 (local, project, user) 顺序逐步砍到 firstParagraphs=1
|
|
203
|
+
* - 最后只保 managed (bolloon 自身约束不能丢)
|
|
204
|
+
*/
|
|
205
|
+
export function mergeHierarchyLayers(layers: HierarchyLayers, opts: MergeOptions = {}): string {
|
|
206
|
+
const maxChars = opts.maxChars ?? DEFAULT_MERGE_MAX_CHARS;
|
|
207
|
+
const parts: string[] = [];
|
|
208
|
+
const labels: Array<[keyof HierarchyLayers, string]> = [
|
|
209
|
+
['managed', '管理规则 (Managed)'],
|
|
210
|
+
['user', '用户规则 (User)'],
|
|
211
|
+
['project', '项目规则 (Project)'],
|
|
212
|
+
['local', '本地规则 (Local)'],
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
for (const [key, label] of labels) {
|
|
216
|
+
const content = layers[key];
|
|
217
|
+
if (content) {
|
|
218
|
+
parts.push(`## ${label}\n\n${content}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (parts.length === 0) return '';
|
|
223
|
+
let result = parts.join('\n\n---\n\n');
|
|
224
|
+
if (result.length <= maxChars) return result;
|
|
225
|
+
|
|
226
|
+
// 超限: 反向砍 (local → project → user), 二级回退到 firstParagraphs=1
|
|
227
|
+
return truncateMerged(layers, maxChars);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function truncateMerged(layers: HierarchyLayers, maxChars: number): string {
|
|
231
|
+
const order: Array<[keyof HierarchyLayers, string]> = [
|
|
232
|
+
['local', '本地规则 (Local)'],
|
|
233
|
+
['project', '项目规则 (Project)'],
|
|
234
|
+
['user', '用户规则 (User)'],
|
|
235
|
+
['managed', '管理规则 (Managed)'],
|
|
236
|
+
];
|
|
237
|
+
const capped: HierarchyLayers = { ...layers };
|
|
238
|
+
// 反复压缩, 直到总长 ≤ maxChars
|
|
239
|
+
let depth = 0; // 0=原始, 1=firstParagraphs=1, 2=砍半
|
|
240
|
+
while (depth < 3) {
|
|
241
|
+
const parts: string[] = [];
|
|
242
|
+
for (const [key, label] of order) {
|
|
243
|
+
const c = capped[key];
|
|
244
|
+
if (!c) continue;
|
|
245
|
+
let text = c;
|
|
246
|
+
if (depth === 1) text = firstParagraphs(c, 1);
|
|
247
|
+
if (depth === 2) text = firstParagraphs(c, 1, Math.floor(maxChars / 4));
|
|
248
|
+
parts.push(`## ${label}\n\n${text}`);
|
|
249
|
+
}
|
|
250
|
+
const joined = parts.reverse().join('\n\n---\n\n'); // 恢复 managed → user → project → local
|
|
251
|
+
if (joined.length <= maxChars) return joined;
|
|
252
|
+
depth++;
|
|
253
|
+
}
|
|
254
|
+
// 最后保 managed 一行
|
|
255
|
+
return `## 管理规则 (Managed)\n\n${firstParagraphs(layers.managed ?? '(无)', 1, maxChars - 50)}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================
|
|
259
|
+
// 工具
|
|
260
|
+
// ============================================================
|
|
261
|
+
|
|
262
|
+
function truncate(s: string, maxChars: number): string {
|
|
263
|
+
if (s.length <= maxChars) return s;
|
|
264
|
+
return s.substring(0, maxChars) + '\n... (truncated)';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function firstParagraphs(text: string, count: number, maxLen?: number): string {
|
|
268
|
+
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
269
|
+
let result = paragraphs.slice(0, count).join('\n\n');
|
|
270
|
+
if (maxLen && result.length > maxLen) {
|
|
271
|
+
result = result.substring(0, maxLen) + '...';
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============================================================
|
|
277
|
+
// 测试钩子
|
|
278
|
+
// ============================================================
|
|
279
|
+
|
|
280
|
+
/** 重置模块级状态 (供测试). 当前无状态, 保留为占位. */
|
|
281
|
+
export function _resetHierarchyForTest(): void {
|
|
282
|
+
// no-op (collect 是纯函数, 但保留 API 一致)
|
|
283
|
+
}
|
|
@@ -121,49 +121,45 @@ function sanitizeChannelId(id: string): string {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// ============================================================
|
|
124
|
-
// PreToolUse (
|
|
124
|
+
// PreToolUse (P2: 4 步链式 validator + permission mode)
|
|
125
125
|
// ============================================================
|
|
126
126
|
|
|
127
|
+
import { resolvePermissionMode, type PermissionMode } from '../agents/permission-mode.js';
|
|
128
|
+
import { validatePreToolUse } from '../agents/pre-tool-validator.js';
|
|
129
|
+
|
|
127
130
|
export interface PreToolUseOptions {
|
|
128
131
|
tool: string;
|
|
129
132
|
args: Record<string, unknown>;
|
|
133
|
+
/** 可选, 不传则用 env BOLLOON_PERM_MODE 或 default */
|
|
134
|
+
permissionMode?: PermissionMode;
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
export interface PreToolUseResult {
|
|
133
138
|
allowed: boolean;
|
|
134
139
|
reason?: string;
|
|
140
|
+
/** P2: 哪一步拒绝的, 供 audit log */
|
|
141
|
+
rejectedBy?: 'mode' | 'blacklist' | 'shell-guard' | 'schema';
|
|
142
|
+
mode: PermissionMode;
|
|
143
|
+
shellGuardRetained?: boolean;
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
// 黑名单: 高危 shell 命令模式
|
|
138
|
-
const DANGEROUS_PATTERNS: Array<{ re: RegExp; reason: string }> = [
|
|
139
|
-
{ re: /\brm\s+(-[a-z]*f[a-z]*\s+)?-[a-z]*r[a-z]*\s+\//, reason: '禁止递归删除根目录' },
|
|
140
|
-
{ re: /\bgit\s+push\s+.*--force\b/, reason: '禁止 force push' },
|
|
141
|
-
{ re: /\brm\s+-rf\s+~\//, reason: '禁止递归删除 home' },
|
|
142
|
-
{ re: /\bdd\s+if=.*\s+of=\/dev\//, reason: '禁止 dd 覆盖块设备' },
|
|
143
|
-
{ re: /\bcurl\s+.*\|\s*(ba)?sh\b/, reason: '禁止 curl|sh 直执行' },
|
|
144
|
-
{ re: />\s*\/dev\/sd[a-z]/, reason: '禁止写裸设备' },
|
|
145
|
-
];
|
|
146
|
-
|
|
147
146
|
/**
|
|
148
|
-
* PreToolUse:
|
|
149
|
-
*
|
|
150
|
-
*
|
|
147
|
+
* PreToolUse: 4 步链式 validator
|
|
148
|
+
* 1. modeGate (bypassPermissions + 非 shell 直接放行)
|
|
149
|
+
* 2. blacklistGate (6 模式危险命令)
|
|
150
|
+
* 3. shellGuardGate (路径黑名单, 绕过 mode 永远生效)
|
|
151
|
+
* 4. schemaGate (第一版 stub: always allow)
|
|
152
|
+
*
|
|
153
|
+
* 向后兼容: 不传 permissionMode → 默认 'default' (env BOLLOON_PERM_MODE 仍生效)
|
|
154
|
+
* 失败静默: 任何异常 → allowed: true (与原行为一致)
|
|
151
155
|
*/
|
|
152
156
|
export async function onPreToolUse(opts: PreToolUseOptions): Promise<PreToolUseResult> {
|
|
153
157
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const cmd = String(opts.args.command || opts.args.cmd || '');
|
|
157
|
-
for (const { re, reason } of DANGEROUS_PATTERNS) {
|
|
158
|
-
if (re.test(cmd)) {
|
|
159
|
-
return { allowed: false, reason };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return { allowed: true };
|
|
158
|
+
const mode = resolvePermissionMode({ permissionMode: opts.permissionMode });
|
|
159
|
+
return validatePreToolUse(opts.tool, opts.args || {}, mode);
|
|
164
160
|
} catch (err) {
|
|
165
|
-
console.warn('[lifecycle-hooks] onPreToolUse failed (silent):', err);
|
|
166
|
-
return { allowed: true }; // 失败放行 (不阻塞)
|
|
161
|
+
console.warn('[lifecycle-hooks] onPreToolUse failed (silent, allowing):', err);
|
|
162
|
+
return { allowed: true, mode: 'default' }; // 失败放行 (不阻塞)
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
165
|
|
|
@@ -174,3 +170,120 @@ export async function onPreToolUse(opts: PreToolUseOptions): Promise<PreToolUseR
|
|
|
174
170
|
export function getStopLogPathForTest(): string {
|
|
175
171
|
return STOP_LOG;
|
|
176
172
|
}
|
|
173
|
+
|
|
174
|
+
// ============================================================
|
|
175
|
+
// PostToolUse (P+ : 工具调用后审计)
|
|
176
|
+
// ============================================================
|
|
177
|
+
|
|
178
|
+
export interface PostToolUseOptions {
|
|
179
|
+
tool: string;
|
|
180
|
+
args: Record<string, unknown>;
|
|
181
|
+
result: { success: boolean; output?: string; error?: string };
|
|
182
|
+
durationMs?: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface PostToolUseResult {
|
|
186
|
+
/** 是否允许继续 (false = abort 整个 ReAct 循环) */
|
|
187
|
+
continue: boolean;
|
|
188
|
+
reason?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* PostToolUse: 工具调用后审计
|
|
193
|
+
* 默认 always continue. 项目可挂 shell-guard audit log / judgment 注入门 / 监控门
|
|
194
|
+
* 失败静默: 任何异常 → continue: true (不阻塞主循环)
|
|
195
|
+
*/
|
|
196
|
+
export async function onPostToolUse(opts: PostToolUseOptions): Promise<PostToolUseResult> {
|
|
197
|
+
try {
|
|
198
|
+
// 审计日志: 写 ~/.bolloon/sessions/post-tool-audit.jsonl
|
|
199
|
+
const fs = await import('fs/promises');
|
|
200
|
+
const os = await import('os');
|
|
201
|
+
const path = await import('path');
|
|
202
|
+
const file = path.join(
|
|
203
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
204
|
+
'.bolloon', 'sessions', 'post-tool-audit.jsonl'
|
|
205
|
+
);
|
|
206
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
207
|
+
const entry = {
|
|
208
|
+
ts: new Date().toISOString(),
|
|
209
|
+
tool: opts.tool,
|
|
210
|
+
success: opts.result.success,
|
|
211
|
+
durationMs: opts.durationMs ?? 0,
|
|
212
|
+
// 不写 args (可能含密钥), 只写 result 的 success/error
|
|
213
|
+
error: opts.result.error?.substring(0, 200) || null,
|
|
214
|
+
};
|
|
215
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { /* ignore */ });
|
|
216
|
+
return { continue: true };
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.warn('[lifecycle-hooks] onPostToolUse failed (silent, allowing):', err);
|
|
219
|
+
return { continue: true };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================================
|
|
224
|
+
// JudgmentInjected (bolloon 独有 : 注入门使用时审计)
|
|
225
|
+
// ============================================================
|
|
226
|
+
|
|
227
|
+
export interface JudgmentInjectedOptions {
|
|
228
|
+
judgmentIds: string[];
|
|
229
|
+
channelId?: string;
|
|
230
|
+
userInput?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function onJudgmentInjected(opts: JudgmentInjectedOptions): Promise<void> {
|
|
234
|
+
try {
|
|
235
|
+
const fs = await import('fs/promises');
|
|
236
|
+
const os = await import('os');
|
|
237
|
+
const path = await import('path');
|
|
238
|
+
const file = path.join(
|
|
239
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
240
|
+
'.bolloon', 'sessions', 'judgment-injected.jsonl'
|
|
241
|
+
);
|
|
242
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
243
|
+
const entry = {
|
|
244
|
+
ts: new Date().toISOString(),
|
|
245
|
+
channelId: opts.channelId || 'unknown',
|
|
246
|
+
judgmentIds: opts.judgmentIds,
|
|
247
|
+
userInputSnippet: (opts.userInput || '').substring(0, 200),
|
|
248
|
+
};
|
|
249
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { /* ignore */ });
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.warn('[lifecycle-hooks] onJudgmentInjected failed (silent):', err);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============================================================
|
|
256
|
+
// MonitorViolation (bolloon 独有 : 监控门审计违规)
|
|
257
|
+
// ============================================================
|
|
258
|
+
|
|
259
|
+
export interface MonitorViolationOptions {
|
|
260
|
+
channelId?: string;
|
|
261
|
+
judgmentId: string;
|
|
262
|
+
rule: string;
|
|
263
|
+
actualBehavior: string;
|
|
264
|
+
severity: 'low' | 'medium' | 'high';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function onMonitorViolation(opts: MonitorViolationOptions): Promise<void> {
|
|
268
|
+
try {
|
|
269
|
+
const fs = await import('fs/promises');
|
|
270
|
+
const os = await import('os');
|
|
271
|
+
const path = await import('path');
|
|
272
|
+
const file = path.join(
|
|
273
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
274
|
+
'.bolloon', 'sessions', 'monitor-violation.jsonl'
|
|
275
|
+
);
|
|
276
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
277
|
+
const entry = {
|
|
278
|
+
ts: new Date().toISOString(),
|
|
279
|
+
channelId: opts.channelId || 'unknown',
|
|
280
|
+
judgmentId: opts.judgmentId,
|
|
281
|
+
rule: opts.rule.substring(0, 200),
|
|
282
|
+
actualBehavior: opts.actualBehavior.substring(0, 200),
|
|
283
|
+
severity: opts.severity,
|
|
284
|
+
};
|
|
285
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { /* ignore */ });
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.warn('[lifecycle-hooks] onMonitorViolation failed (silent):', err);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { BolloonContext } from './context-collector.js';
|
|
11
11
|
|
|
12
|
-
const DEFAULT_MAX_CHARS = 4000
|
|
12
|
+
const DEFAULT_MAX_CHARS = 8000; // 从 4000 提到 8000, 给 4 级 Bolloon.md 留空间
|
|
13
13
|
const BOLLOON_MD_KEEP = 500; // 砍到这么长
|
|
14
14
|
|
|
15
15
|
export function formatContextForSystemPrompt(
|
|
@@ -21,7 +21,15 @@ export function formatContextForSystemPrompt(
|
|
|
21
21
|
lines.push(`# 你的项目上下文 (自动 bootstrap, 时间: ${ctx.collectedAt})`);
|
|
22
22
|
lines.push('');
|
|
23
23
|
|
|
24
|
-
// 1.
|
|
24
|
+
// 1. Bolloon.md 4 级层次 (Claude Code 论文, 严格 1:1)
|
|
25
|
+
// managed → user → project → local, 已在 collector 层 merge
|
|
26
|
+
if (ctx.hierarchy && ctx.hierarchy.merged && ctx.hierarchy.merged.length > 0) {
|
|
27
|
+
lines.push('## 规则层次 (4 级, Claude Code 论文对齐)');
|
|
28
|
+
lines.push(ctx.hierarchy.merged);
|
|
29
|
+
lines.push('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 1b. 项目名 + Bolloon.md 摘要 (向后兼容, Bolloon.md 也算 project 层之一)
|
|
25
33
|
lines.push(`## 项目: ${ctx.projectName}`);
|
|
26
34
|
lines.push(`- 路径: ${ctx.projectRoot}`);
|
|
27
35
|
if (ctx.bolloonMd) {
|
|
@@ -49,7 +57,7 @@ export function formatContextForSystemPrompt(
|
|
|
49
57
|
|
|
50
58
|
// 3. Persona
|
|
51
59
|
if (ctx.persona) {
|
|
52
|
-
lines.push(`##
|
|
60
|
+
lines.push(`## 用户身份 (persona)`);
|
|
53
61
|
lines.push(`- 名字: ${ctx.persona.name}`);
|
|
54
62
|
if (ctx.persona.description) lines.push(`- 描述: ${ctx.persona.description}`);
|
|
55
63
|
if (ctx.persona.personality) lines.push(`- 性格: ${ctx.persona.personality}`);
|