@bolloon/bolloon-agent 0.3.49 → 0.3.51
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/deny-pipeline.js +7 -3
- package/dist/agents/loop-review.js +12 -15
- package/dist/agents/pi-sdk-tools.js +36 -0
- package/dist/agents/pi-sdk.js +8 -1
- package/dist/agents/shell-guard.js +38 -0
- package/dist/index.js +16 -0
- package/package.json +1 -1
|
@@ -79,13 +79,17 @@ export class DenyPipeline {
|
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* 构建 permission-mode checker.
|
|
82
|
-
* default 模式: 禁用
|
|
82
|
+
* default 模式: 禁用 git 写操作 (commit/push/branch — "不搞乱")
|
|
83
83
|
* bypassPermissions: 放行所有
|
|
84
|
+
*
|
|
85
|
+
* 2026-08-10: shell_exec / write_file / edit_file / delete_file / terminal 移出 default
|
|
86
|
+
* 禁列表 — 它们已有各自护栏 (shell_exec 命令白名单 / terminal denylist-only /
|
|
87
|
+
* write_file checkWritePath), 用户明确"灵活一点, 少围栏, 核心的东西不碰不搞乱就行";
|
|
88
|
+
* 实测日志 "write_file 被权限拦了" → LLM 只能绕道, 任务无法推进.
|
|
84
89
|
*/
|
|
85
90
|
static permissionChecker() {
|
|
86
91
|
const DEFAULT_DENY_TOOLS = new Set([
|
|
87
|
-
'
|
|
88
|
-
'delete_file', 'write_file', 'edit_file',
|
|
92
|
+
'git_commit', 'git_push', 'git_branch',
|
|
89
93
|
]);
|
|
90
94
|
return (ctx) => {
|
|
91
95
|
if (ctx.permissionMode === 'bypassPermissions') {
|
|
@@ -21,24 +21,21 @@ export const DEFAULT_MAX_REVIEWS = 2;
|
|
|
21
21
|
/**
|
|
22
22
|
* 判定: LLM 想 <final gen> 时, 该续跑一次 review 还是真正结束.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* 2026-08-10 重构 (用户纠正: 不要硬编码词表, 循环要智能, 自动触发后续):
|
|
25
|
+
* - 旧逻辑: 无 intent → 直接 finish. 但"发布一个 ipfs 网站..." 被 classifyIntent 误判
|
|
26
|
+
* chitchat → intentHint 空 → 1 次循环直接 <final gen> (任务没做就结束).
|
|
27
|
+
* - 新逻辑: **final 前总是让 LLM 自查** (是否完成用户需求/是否还有后续步骤) — 结束权
|
|
28
|
+
* 完全交给 LLM 判断, 规则只做上限兜底 (maxReviews). 达上限才放行, 保证不过度深挖.
|
|
29
|
+
* - 效果: 任务场景 LLM 自查发现"还没发布" → 自动继续调工具 (自动触发后续);
|
|
30
|
+
* 闲聊场景 LLM 快速确认完成, 2 次自查后放行.
|
|
30
31
|
*/
|
|
31
32
|
export function decideAfterReview(state, maxReviews = DEFAULT_MAX_REVIEWS) {
|
|
32
|
-
const intent = (state.userIntent || '').trim();
|
|
33
|
-
if (!intent) {
|
|
34
|
-
return { kind: 'finish', reason: 'no-user-intent' };
|
|
35
|
-
}
|
|
36
33
|
if (state.reviewsDone >= maxReviews) {
|
|
37
34
|
return { kind: 'finish', reason: `max-reviews-${maxReviews}` };
|
|
38
35
|
}
|
|
39
36
|
return {
|
|
40
37
|
kind: 'continue-review',
|
|
41
|
-
reason: `${state.reviewsDone + 1}/${maxReviews}
|
|
38
|
+
reason: `${state.reviewsDone + 1}/${maxReviews} 完成度自查`,
|
|
42
39
|
hint: buildReviewHint(state, maxReviews),
|
|
43
40
|
};
|
|
44
41
|
}
|
|
@@ -60,13 +57,13 @@ export function buildReviewHint(state, maxReviews = DEFAULT_MAX_REVIEWS) {
|
|
|
60
57
|
})
|
|
61
58
|
.join('\n');
|
|
62
59
|
}
|
|
63
|
-
return (`[
|
|
60
|
+
return (`[完成度自查 ${state.reviewsDone + 1}/${maxReviews}]` +
|
|
64
61
|
`\n用户需求: ${intent.slice(0, 300)}` +
|
|
65
62
|
actionLines +
|
|
66
63
|
`\n已完成工具: ${tools}` +
|
|
67
|
-
`\n请对照需求逐条自查:
|
|
68
|
-
|
|
69
|
-
`\n[重要]
|
|
64
|
+
`\n请对照需求逐条自查: 用户需求是否每一项都真正完成了? 如果还有未完成的子目标或自然衔接的后续步骤 → 继续调用工具推进 (已完成动作不要重复, 直接基于已有结果做下一步); ` +
|
|
65
|
+
`如果用户需求已全部满足 → 直接输出 <final gen> 结束.` +
|
|
66
|
+
`\n[重要] 不要因为做了一部分就提前结束 — 结束前逐条对照「用户需求」确认每一项都完成.`);
|
|
70
67
|
}
|
|
71
68
|
/** 布尔门: 是否该继续 review (测试/消融快速断言) */
|
|
72
69
|
export function shouldReviewAgain(state, maxReviews = DEFAULT_MAX_REVIEWS) {
|
|
@@ -649,6 +649,42 @@ export function registerBuiltinTools(ctx) {
|
|
|
649
649
|
return { success: true, output: result.output };
|
|
650
650
|
}
|
|
651
651
|
});
|
|
652
|
+
// 2026-08-10: terminal — 灵活终端写命令 (用户要求: bolloon 自己写命令进 terminal, 少围栏).
|
|
653
|
+
// 与 shell_exec 的区别: 接受**完整 shell 命令字符串** (管道/重定向/写文件都行),
|
|
654
|
+
// 护栏只挡高危破坏模式 (checkTerminalCommand: 提权/格式化/删根/.bolloon 数据), 其余放行.
|
|
655
|
+
ctx.tools.set('terminal', {
|
|
656
|
+
name: 'terminal',
|
|
657
|
+
description: '执行完整 shell 命令 (支持管道/重定向/写文件/跑脚本). 护栏只挡高危破坏操作 (sudo/格式化/rm -rf 根目录/写 ~/.bolloon 数据), 其余灵活放行. 适合: 写 HTML 文件、跑 python/node 脚本、查系统状态、装依赖.',
|
|
658
|
+
parameters: { command: '完整 shell 命令 (必填, 如: echo "<html>" > /tmp/site/index.html && ls /tmp/site)', timeoutMs: '超时毫秒, 默认 30000' },
|
|
659
|
+
execute: async (args) => {
|
|
660
|
+
const raw = String(args.command || '').trim();
|
|
661
|
+
if (!raw)
|
|
662
|
+
return { success: false, error: 'command 必填' };
|
|
663
|
+
const { checkTerminalCommand } = await import('./shell-guard.js');
|
|
664
|
+
const guard = checkTerminalCommand(raw);
|
|
665
|
+
if (!guard.allowed) {
|
|
666
|
+
return { success: false, error: `[terminal-guard] ${guard.reason}`, deniedByGuard: true };
|
|
667
|
+
}
|
|
668
|
+
const { exec } = await import('child_process');
|
|
669
|
+
const timeoutMs = Number(args.timeoutMs) || 30000;
|
|
670
|
+
return new Promise((resolve) => {
|
|
671
|
+
exec(raw, {
|
|
672
|
+
cwd: process.cwd(),
|
|
673
|
+
timeout: timeoutMs,
|
|
674
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
675
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
676
|
+
}, (err, stdout, stderr) => {
|
|
677
|
+
const output = ((stdout || '') + (stderr ? `\n[stderr]\n${stderr}` : '')).trim().slice(0, 8000);
|
|
678
|
+
if (err) {
|
|
679
|
+
resolve({ success: false, error: `exit ${err.code ?? '?'}: ${String(err.message || '').slice(0, 200)}`, output: output || undefined });
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
resolve({ success: true, output: output || '(无输出)' });
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
});
|
|
652
688
|
// self_improve
|
|
653
689
|
ctx.tools.set('self_improve', {
|
|
654
690
|
name: 'self_improve',
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -209,6 +209,8 @@ export class PiAgentSession {
|
|
|
209
209
|
currentAgentId = '';
|
|
210
210
|
/** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
|
|
211
211
|
currentIntent = 'chitchat';
|
|
212
|
+
/** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
|
|
213
|
+
currentUserInput = '';
|
|
212
214
|
currentIntentHint = '';
|
|
213
215
|
/**
|
|
214
216
|
* 算 judgment 注入门: 失败静默, 不阻塞主对话
|
|
@@ -673,6 +675,7 @@ export class PiAgentSession {
|
|
|
673
675
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
674
676
|
this.currentIntent = classifyIntent(input);
|
|
675
677
|
this.currentIntentHint = intentHint(this.currentIntent);
|
|
678
|
+
this.currentUserInput = input;
|
|
676
679
|
}
|
|
677
680
|
catch (err) {
|
|
678
681
|
console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
|
|
@@ -766,6 +769,7 @@ export class PiAgentSession {
|
|
|
766
769
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
767
770
|
this.currentIntent = classifyIntent(userText);
|
|
768
771
|
this.currentIntentHint = intentHint(this.currentIntent);
|
|
772
|
+
this.currentUserInput = userText;
|
|
769
773
|
if (this.currentIntent !== 'chitchat') {
|
|
770
774
|
onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
|
|
771
775
|
}
|
|
@@ -1031,6 +1035,7 @@ export class PiAgentSession {
|
|
|
1031
1035
|
const { classifyIntent, intentHint } = await import('./intent-classifier.js');
|
|
1032
1036
|
this.currentIntent = classifyIntent(input);
|
|
1033
1037
|
this.currentIntentHint = intentHint(this.currentIntent);
|
|
1038
|
+
this.currentUserInput = input;
|
|
1034
1039
|
}
|
|
1035
1040
|
catch (err) {
|
|
1036
1041
|
console.warn('[PiAgent] classifyIntent in pivot failed:', err);
|
|
@@ -1779,7 +1784,9 @@ ${toolDefs}
|
|
|
1779
1784
|
// 达成用户需求才放行真正结束. 达上限或无需深挖则以用户需求为准结束.
|
|
1780
1785
|
const reviewDecision = decideAfterReview({
|
|
1781
1786
|
reviewsDone: loopReviewCount,
|
|
1782
|
-
|
|
1787
|
+
// 2026-08-10: 传用户原始输入 (不是派生 intentHint) — LLM 对照原文自查完成度,
|
|
1788
|
+
// 未完成 → 自动继续调工具 (自动触发后续步骤)
|
|
1789
|
+
userIntent: this.currentUserInput,
|
|
1783
1790
|
completedTools: Array.from(loopReviewCompletedTools),
|
|
1784
1791
|
actionLog: loopActionLog,
|
|
1785
1792
|
}, DEFAULT_MAX_REVIEWS);
|
|
@@ -335,6 +335,44 @@ export const SELF_IMPROVE_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
|
|
335
335
|
/** @deprecated 用 getSandboxCwd() */
|
|
336
336
|
export const SHELL_SANDBOX_CWD = path.resolve(process.cwd(), '.bolloon-shell-sandbox');
|
|
337
337
|
// ============================================================================
|
|
338
|
+
// terminal 工具宽松护栏 (2026-08-10) — denylist-only, 不碰核心即可
|
|
339
|
+
// 与 checkCommand 的区别: 不查命令白名单/参数 allowlist — 完整 shell 命令字符串放行,
|
|
340
|
+
// 只挡"高危破坏/碰核心数据"模式. 用户明确: 灵活一点, 少围栏, 核心的东西不碰不搞乱.
|
|
341
|
+
// ============================================================================
|
|
342
|
+
const TERMINAL_DENY_PATTERNS = [
|
|
343
|
+
/\bsudo\b/, // 提权
|
|
344
|
+
/\bsu\b/,
|
|
345
|
+
/\bmkfs\b/, // 格式化
|
|
346
|
+
/\bshred\b/, // 擦除
|
|
347
|
+
/\bdd\s+.*of=\/dev\//, // 写设备
|
|
348
|
+
/\brm\s+-(rf|fr)\b/, // 递归删除 (rm 单个文件允许)
|
|
349
|
+
/\bchmod\s+-R\s+777\b/, // 全权限
|
|
350
|
+
/\bcurl\b[^|]*\|\s*(sh|bash)\b/, // curl|sh 远程执行
|
|
351
|
+
/\bwget\b[^|]*\|\s*(sh|bash)\b/,
|
|
352
|
+
/\b:\(\)\s*\{[^}]*\}\s*;\s*:/, // fork bomb
|
|
353
|
+
/\b(>|>>)\s*(\/etc\/|\/usr\/|\/System\/|\/bin\/|\/sbin\/)/, // 写系统目录
|
|
354
|
+
/[\/\s]\.bolloon\b/, // Bolloon 数据 (sessions/persona/keys; 覆盖 ~/.bolloon, $HOME/.bolloon, 空格.bolloon)
|
|
355
|
+
/[\/\s]\.(diap|hermes|openclaw)\b/, // 其他 agent 数据
|
|
356
|
+
/\brm\s+-rf\s+(\/|~|\*|\.|\$HOME)\b/, // 删根/家/通配
|
|
357
|
+
/\bgit\s+push\b[^|]*\s+(-f|--force)/, // 强推
|
|
358
|
+
/\bgit\s+reset\s+--hard\b/,
|
|
359
|
+
/\bkill\s+-9\s+\d+\b/, // 杀进程 (保留给用户)
|
|
360
|
+
];
|
|
361
|
+
/** 检查完整 shell 命令 (terminal 工具用). denylist-only: 命中高危模式即拒, 否则放行. */
|
|
362
|
+
export function checkTerminalCommand(rawCmd) {
|
|
363
|
+
const cmd = (rawCmd || '').trim();
|
|
364
|
+
if (!cmd)
|
|
365
|
+
return { allowed: false, reason: '命令为空', matchedBy: 'fallback-deny' };
|
|
366
|
+
for (const pattern of TERMINAL_DENY_PATTERNS) {
|
|
367
|
+
if (pattern.test(cmd)) {
|
|
368
|
+
auditShellCall('denied', cmd, [], `terminal 高危模式 ${pattern}`);
|
|
369
|
+
return { allowed: false, reason: `命令命中高危护栏 ${pattern} (核心不碰: 提权/格式化/删根/.bolloon 数据). 换一条安全命令.`, matchedBy: 'arg-denylist' };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
auditShellCall('allowed', cmd, []);
|
|
373
|
+
return { allowed: true };
|
|
374
|
+
}
|
|
375
|
+
// ============================================================================
|
|
338
376
|
// 写策略 / 审计路径 (供 API 端点用)
|
|
339
377
|
// ============================================================================
|
|
340
378
|
export const POLICY_AUDIT_PATH_PUBLIC = POLICY_AUDIT_PATH;
|
package/dist/index.js
CHANGED
|
@@ -538,6 +538,22 @@ async function startCLI(comm) {
|
|
|
538
538
|
catch { /* config-store 失败静默, 用 env 结果 */ }
|
|
539
539
|
cliAgentName = agentIdentity?.name || 'bolloon';
|
|
540
540
|
cliStartTime = Date.now();
|
|
541
|
+
// 2026-08-10: 启动后台拉起本地 Kubo (IPFS) — fire-and-forget, 失败静默 (ipfs 工具内会再尝试).
|
|
542
|
+
// 背景: 实测日志 ipfs_add 失败 "发送上传请求失败: http://127.0.0.1:5001/api/v0/add" —
|
|
543
|
+
// Kubo daemon 没起, 而 CLI 启动路径 (startCLI) 之前从不调 checkKuboSetup (只有 Web server 调).
|
|
544
|
+
// BOLLOON_SKIP_KUBO=1 可禁用 (pty 测试用临时 HOME 时避免拉起指向临时 repo 的 daemon 污染 5001)
|
|
545
|
+
if (process.env.BOLLOON_SKIP_KUBO !== '1') {
|
|
546
|
+
void (async () => {
|
|
547
|
+
try {
|
|
548
|
+
const sdk = await import('@diap/sdk');
|
|
549
|
+
const checkKuboSetup = sdk.checkKuboSetup;
|
|
550
|
+
if (typeof checkKuboSetup === 'function') {
|
|
551
|
+
await checkKuboSetup(true, true);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch { /* Kubo 拉起失败静默 — ipfs 工具内 ensureKuboReady 会再尝试 */ }
|
|
555
|
+
})();
|
|
556
|
+
}
|
|
541
557
|
// 恢复上次 active channel (session 恢复: CLI 与 Web 共用 active-channel.json)
|
|
542
558
|
try {
|
|
543
559
|
const { getIdentityStore } = await import('./agents/agent-identity-store.js');
|