@bolloon/bolloon-agent 0.5.4 → 0.6.0
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/agent-identity-store.js +41 -5
- package/dist/agents/background-notices.js +124 -0
- package/dist/agents/channel-identity.js +54 -0
- package/dist/agents/code-write-gate.js +47 -0
- package/dist/agents/deny-pipeline.js +11 -2
- package/dist/agents/egress-sanitize.js +69 -0
- package/dist/agents/error-classifier.js +67 -5
- package/dist/agents/experience-review.js +226 -0
- package/dist/agents/gateway-group.js +153 -18
- package/dist/agents/iteration-budget.js +64 -0
- package/dist/agents/lesson-to-skill.js +211 -0
- package/dist/agents/managed-services.js +64 -0
- package/dist/agents/parse-tool-call.js +43 -32
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +201 -32
- package/dist/agents/pi-sdk.js +798 -206
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/agents/shell-guard.js +109 -13
- package/dist/agents/skill-health.js +90 -0
- package/dist/agents/skill-ledger.js +129 -0
- package/dist/agents/tool-loop-guard.js +303 -0
- package/dist/agents/tool-path-args.js +53 -0
- package/dist/agents/tool-result-gate.js +50 -0
- package/dist/agents/tool-subset.js +109 -0
- package/dist/agents/tool-telemetry.js +97 -0
- package/dist/bootstrap/context-collector.js +37 -7
- package/dist/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/bootstrap/persona-init.js +174 -0
- package/dist/bootstrap/persona-loader.js +5 -1
- package/dist/cli/ink-app.js +153 -6
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +3 -1
- package/dist/cli/log-gate.js +7 -0
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +87 -2
- package/dist/cli/trace-line.js +54 -0
- package/dist/context-compaction/auto-compact.js +37 -2
- package/dist/electron-build/agents/agent-identity-store.js +43 -5
- package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
- package/dist/electron-build/agents/background-notices.js +138 -0
- package/dist/electron-build/agents/background-notices.js.map +1 -0
- package/dist/electron-build/agents/channel-identity.js +64 -0
- package/dist/electron-build/agents/channel-identity.js.map +1 -0
- package/dist/electron-build/agents/code-write-gate.js +55 -0
- package/dist/electron-build/agents/code-write-gate.js.map +1 -0
- package/dist/electron-build/agents/deny-pipeline.js +11 -2
- package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
- package/dist/electron-build/agents/egress-sanitize.js +76 -0
- package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
- package/dist/electron-build/agents/error-classifier.js +68 -5
- package/dist/electron-build/agents/error-classifier.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +243 -0
- package/dist/electron-build/agents/experience-review.js.map +1 -0
- package/dist/electron-build/agents/iteration-budget.js +73 -0
- package/dist/electron-build/agents/iteration-budget.js.map +1 -0
- package/dist/electron-build/agents/lesson-to-skill.js +228 -0
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
- package/dist/electron-build/agents/managed-services.js +69 -0
- package/dist/electron-build/agents/managed-services.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +43 -32
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
- package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +200 -31
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +799 -206
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/plan-store.js +37 -0
- package/dist/electron-build/agents/plan-store.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +71 -2
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/run-context.js +24 -0
- package/dist/electron-build/agents/run-context.js.map +1 -0
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/shell-guard.js +110 -14
- package/dist/electron-build/agents/shell-guard.js.map +1 -1
- package/dist/electron-build/agents/skill-health.js +103 -0
- package/dist/electron-build/agents/skill-health.js.map +1 -0
- package/dist/electron-build/agents/skill-ledger.js +146 -0
- package/dist/electron-build/agents/skill-ledger.js.map +1 -0
- package/dist/electron-build/agents/tool-loop-guard.js +319 -0
- package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
- package/dist/electron-build/agents/tool-path-args.js +63 -0
- package/dist/electron-build/agents/tool-path-args.js.map +1 -0
- package/dist/electron-build/agents/tool-result-gate.js +59 -0
- package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
- package/dist/electron-build/agents/tool-subset.js +116 -0
- package/dist/electron-build/agents/tool-subset.js.map +1 -0
- package/dist/electron-build/agents/tool-telemetry.js +108 -0
- package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
- package/dist/electron-build/bootstrap/context-collector.js +38 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-init.js +216 -0
- package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
- package/dist/electron-build/bootstrap/persona-loader.js +5 -1
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/context-compaction/auto-compact.js +38 -2
- package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js +22 -0
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +314 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +55 -0
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/web/server-storage.js +26 -5
- package/dist/electron-build/web/server-storage.js.map +1 -1
- package/dist/external-engines/delegate.js +21 -0
- package/dist/index.js +526 -95
- package/dist/kernel/channel-actor.js +297 -0
- package/dist/kernel/control.js +95 -0
- package/dist/kernel/gate-scan.js +1036 -0
- package/dist/kernel/model-runtime.js +472 -0
- package/dist/kernel/plan-channel-actor.js +298 -0
- package/dist/kernel/plan-constraint.js +165 -0
- package/dist/kernel/plan-deletion.js +32 -0
- package/dist/kernel/plan-modelruntime.js +60 -0
- package/dist/kernel/plan-runcontext.js +163 -0
- package/dist/kernel/plan.js +153 -0
- package/dist/kernel/roster.js +242 -0
- package/dist/orbitdb/cid-database.js +55 -0
- package/dist/orbitdb/event-index.js +370 -0
- package/dist/orbitdb/event-query.js +131 -0
- package/dist/orbitdb/event-shell.js +510 -0
- package/dist/orbitdb/group-access.js +381 -0
- package/dist/orbitdb/group-shards.js +140 -0
- package/dist/orbitdb/ipfs-node.js +36 -11
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +4 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/routes-tasks.js +5 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +60 -18
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +473 -29
- package/dist/web/ui/step-timeline.js +13 -12
- package/package.json +2 -1
- package/scripts/build-web.ts +74 -1
- package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
- package/dist/constraint-runtime/_archive_helper.js +0 -9
- package/dist/constraint-runtime/assistant/index.d.ts +0 -11
- package/dist/constraint-runtime/assistant/index.js +0 -12
- package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
- package/dist/constraint-runtime/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/bridge/index.d.ts +0 -11
- package/dist/constraint-runtime/bridge/index.js +0 -12
- package/dist/constraint-runtime/buddy/index.d.ts +0 -11
- package/dist/constraint-runtime/buddy/index.js +0 -12
- package/dist/constraint-runtime/cli/index.d.ts +0 -11
- package/dist/constraint-runtime/cli/index.js +0 -12
- package/dist/constraint-runtime/components/index.d.ts +0 -11
- package/dist/constraint-runtime/components/index.js +0 -12
- package/dist/constraint-runtime/constants/index.d.ts +0 -11
- package/dist/constraint-runtime/constants/index.js +0 -12
- package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
- package/dist/constraint-runtime/coordinator/index.js +0 -12
- package/dist/constraint-runtime/cost_hook.d.ts +0 -2
- package/dist/constraint-runtime/cost_hook.js +0 -4
- package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
- package/dist/constraint-runtime/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/execution_registry.d.ts +0 -20
- package/dist/constraint-runtime/execution_registry.js +0 -44
- package/dist/constraint-runtime/hooks/index.d.ts +0 -11
- package/dist/constraint-runtime/hooks/index.js +0 -12
- package/dist/constraint-runtime/ink.d.ts +0 -1
- package/dist/constraint-runtime/ink.js +0 -4
- package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
- package/dist/constraint-runtime/keybindings/index.js +0 -12
- package/dist/constraint-runtime/memdir/index.d.ts +0 -11
- package/dist/constraint-runtime/memdir/index.js +0 -12
- package/dist/constraint-runtime/migrations/index.d.ts +0 -11
- package/dist/constraint-runtime/migrations/index.js +0 -12
- package/dist/constraint-runtime/moreright/index.d.ts +0 -11
- package/dist/constraint-runtime/moreright/index.js +0 -12
- package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
- package/dist/constraint-runtime/native_ts/index.js +0 -12
- package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
- package/dist/constraint-runtime/output_styles/index.js +0 -12
- package/dist/constraint-runtime/plugins/index.d.ts +0 -11
- package/dist/constraint-runtime/plugins/index.js +0 -12
- package/dist/constraint-runtime/port_manifest.d.ts +0 -7
- package/dist/constraint-runtime/port_manifest.js +0 -11
- package/dist/constraint-runtime/query.d.ts +0 -6
- package/dist/constraint-runtime/query.js +0 -1
- package/dist/constraint-runtime/remote/index.d.ts +0 -11
- package/dist/constraint-runtime/remote/index.js +0 -12
- package/dist/constraint-runtime/schemas/index.d.ts +0 -11
- package/dist/constraint-runtime/schemas/index.js +0 -12
- package/dist/constraint-runtime/screens/index.d.ts +0 -11
- package/dist/constraint-runtime/screens/index.js +0 -12
- package/dist/constraint-runtime/server/index.d.ts +0 -11
- package/dist/constraint-runtime/server/index.js +0 -12
- package/dist/constraint-runtime/services/index.d.ts +0 -11
- package/dist/constraint-runtime/services/index.js +0 -12
- package/dist/constraint-runtime/state/index.d.ts +0 -11
- package/dist/constraint-runtime/state/index.js +0 -12
- package/dist/constraint-runtime/system_init.d.ts +0 -1
- package/dist/constraint-runtime/system_init.js +0 -20
- package/dist/constraint-runtime/types/index.d.ts +0 -11
- package/dist/constraint-runtime/types/index.js +0 -12
- package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
- package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/utils/index.d.ts +0 -11
- package/dist/constraint-runtime/utils/index.js +0 -12
- package/dist/constraint-runtime/vim/index.d.ts +0 -11
- package/dist/constraint-runtime/vim/index.js +0 -12
- package/dist/constraint-runtime/voice/index.d.ts +0 -11
- package/dist/constraint-runtime/voice/index.js +0 -12
|
@@ -39,6 +39,8 @@ export async function createPlan(input, home) {
|
|
|
39
39
|
const now = new Date().toISOString();
|
|
40
40
|
const planId = `plan_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
|
|
41
41
|
const plan = {
|
|
42
|
+
// 初始版本 (落实④: 单调递增版本号, 供乐观并发)
|
|
43
|
+
rev: 1,
|
|
42
44
|
planId,
|
|
43
45
|
goal,
|
|
44
46
|
createdBy: input.createdBy || 'agent',
|
|
@@ -79,6 +81,13 @@ export async function updatePlan(planId, input, home) {
|
|
|
79
81
|
const plan = await loadPlan(planId, home);
|
|
80
82
|
if (!plan)
|
|
81
83
|
return { ok: false, error: `plan '${planId}' 不存在` };
|
|
84
|
+
// 2026-10-01 (落实④): 乐观并发校验 —— 带 expectedRev 且对不上 ⇒ **拒收**, 绝不默默覆盖
|
|
85
|
+
if (typeof input.expectedRev === 'number' && Number.isFinite(input.expectedRev)) {
|
|
86
|
+
const cur = Number(plan.rev ?? 1);
|
|
87
|
+
if (input.expectedRev !== cur) {
|
|
88
|
+
return { ok: false, error: `plan '${planId}' 已被更新 (期望 rev=${input.expectedRev}, 实际 rev=${cur}) —— 请重新读取后再改` };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
82
91
|
if (input.stepId && input.status) {
|
|
83
92
|
const step = plan.steps.find(s => s.id === input.stepId);
|
|
84
93
|
if (!step)
|
|
@@ -104,6 +113,9 @@ export async function updatePlan(planId, input, home) {
|
|
|
104
113
|
s.status = 'blocked';
|
|
105
114
|
}
|
|
106
115
|
}
|
|
116
|
+
// 2026-10-01 (落实④): 每次成功更新 ⇒ rev **单调递增** (乐观并发的基础; 也供 UI 丢弃过期快照)
|
|
117
|
+
plan.rev = Number(plan.rev ?? 1) + 1;
|
|
118
|
+
plan.updatedAt = new Date().toISOString();
|
|
107
119
|
try {
|
|
108
120
|
await savePlan(plan, home);
|
|
109
121
|
return { ok: true, plan };
|
|
@@ -165,3 +177,27 @@ export function planToContext(plan) {
|
|
|
165
177
|
}
|
|
166
178
|
return lines.join('\n');
|
|
167
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* 待办/计划重注入 (2026-10-01 落实④)。
|
|
182
|
+
*
|
|
183
|
+
* 为什么必须**每轮**注入: 上下文压缩会把早先注入的计划文本丢掉 —— 一旦丢了, 智能体就"忘了自己在做什么"。
|
|
184
|
+
* 所以注入不是一次性动作, 而是每轮带上**当前**计划(有界: 条数与字符都封顶, 免得计划本身吃爆上下文)。
|
|
185
|
+
* 与既有 `planToContext` 的分工: 那个给单条 plan 用; 这个给"当前活跃计划一览"用。
|
|
186
|
+
*/
|
|
187
|
+
export function formatPlansForPrompt(plans, opts = {}) {
|
|
188
|
+
const maxChars = Number.isFinite(opts.maxChars) && opts.maxChars > 0 ? Math.floor(opts.maxChars) : 1200;
|
|
189
|
+
const maxItems = Number.isFinite(opts.maxItems) && opts.maxItems > 0 ? Math.floor(opts.maxItems) : 3;
|
|
190
|
+
const active = (plans || []).filter((p) => p && p.status === 'active').slice(0, maxItems);
|
|
191
|
+
if (!active.length)
|
|
192
|
+
return '';
|
|
193
|
+
const lines = ['## 我在做的 (每轮重注入, 压缩后也不会丢)'];
|
|
194
|
+
for (const p of active) {
|
|
195
|
+
const done = p.steps.filter((x) => x.status === 'done').length;
|
|
196
|
+
lines.push(`- [${done}/${p.steps.length}] ${p.goal} (rev ${Number(p.rev ?? 1)})`);
|
|
197
|
+
for (const st of p.steps.filter((x) => x.status !== 'done').slice(0, 5)) {
|
|
198
|
+
lines.push(` · (${st.status}) ${st.description}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const out = lines.join('\n');
|
|
202
|
+
return out.length <= maxChars ? out : out.slice(0, maxChars) + '\n…[计划列表已截断]';
|
|
203
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 进程注册表: 模块级 Map<sessionId, {proc, cmd, startedAt, output[]}>, 供 process 工具查询.
|
|
10
10
|
*/
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import fs from 'node:fs';
|
|
12
14
|
import * as path from 'path';
|
|
13
15
|
const sessions = new Map();
|
|
14
16
|
let seq = 0;
|
|
@@ -30,10 +32,12 @@ export function spawnBackground(raw, cwd = process.cwd()) {
|
|
|
30
32
|
try {
|
|
31
33
|
const proc = spawn(shell, shellArgs, { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, windowsHide: true });
|
|
32
34
|
session.proc = proc;
|
|
35
|
+
session.pid = proc.pid;
|
|
36
|
+
persistSessions();
|
|
33
37
|
proc.stdout.on('data', (d) => { session.output = (session.output + d.toString()).slice(-16000); });
|
|
34
38
|
proc.stderr.on('data', (d) => { session.output = (session.output + d.toString()).slice(-16000); });
|
|
35
|
-
proc.on('close', (code) => { session.exitCode = code; session.status = 'exited'; });
|
|
36
|
-
proc.on('error', (e) => { session.status = 'error'; session.error = e.message; });
|
|
39
|
+
proc.on('close', (code) => { session.exitCode = code; session.status = 'exited'; persistSessions(); });
|
|
40
|
+
proc.on('error', (e) => { session.status = 'error'; session.error = e.message; persistSessions(); });
|
|
37
41
|
}
|
|
38
42
|
catch (e) {
|
|
39
43
|
session.status = 'error';
|
|
@@ -103,3 +107,61 @@ export function isValidSessionId(id) {
|
|
|
103
107
|
export function sessionIdFromPath(p) {
|
|
104
108
|
return path.basename(String(p || '').trim());
|
|
105
109
|
}
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// 2026-10-01 (用户: 「bolloon 后台开进程后无法回来进行管理」):
|
|
112
|
+
// 实测根因: 会话表是**内存 Map** ✗ 且不落盘 ⇒ 重启/换一轮后 bolloon **忘了**后台进程 ✗
|
|
113
|
+
// ⇒ `process list` 空的 ⇒ "回不来管理" ✓。修: 落盘 `~/.bolloon/processes.json` +
|
|
114
|
+
// **懒恢复**(list/poll 时若表里没有就从盘上读 ✓) + 按 pid 探活(活着 ⇒ detached=true 仍可 kill ✓; 死了 ⇒ lost ✓)。
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
export function processesPath(home = os.homedir()) {
|
|
117
|
+
return path.join(home, '.bolloon', 'processes.json');
|
|
118
|
+
}
|
|
119
|
+
/** 探活: 只发 0 号信号, 真 kill 什么都不做 ✓ (EPERM 也算活着 ✓) */
|
|
120
|
+
export function isPidAlive(pid) {
|
|
121
|
+
if (!pid || !Number.isFinite(pid))
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, 0);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return e?.code === 'EPERM';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** 把当前的会话表写到盘上 (best-effort, 绝不影响命令执行 ✓) */
|
|
132
|
+
export function persistSessions(home = os.homedir()) {
|
|
133
|
+
try {
|
|
134
|
+
const rows = [...sessions.values()].map((s) => ({
|
|
135
|
+
id: s.id, cmd: s.cmd, cwd: s.cwd, startedAt: s.startedAt,
|
|
136
|
+
status: s.status, exitCode: s.exitCode, pid: s.pid ?? null,
|
|
137
|
+
}));
|
|
138
|
+
fs.mkdirSync(path.dirname(processesPath(home)), { recursive: true });
|
|
139
|
+
fs.writeFileSync(processesPath(home), JSON.stringify(rows, null, 0), 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
catch { /* 落盘失败不影响执行 */ }
|
|
142
|
+
}
|
|
143
|
+
/** 懒恢复: 表里没有的就从盘上读回来, 并按 pid 探活 ✓ */
|
|
144
|
+
export function restoreSessions(home = os.homedir()) {
|
|
145
|
+
let n = 0;
|
|
146
|
+
try {
|
|
147
|
+
const file = processesPath(home);
|
|
148
|
+
if (!fs.existsSync(file))
|
|
149
|
+
return 0;
|
|
150
|
+
const rows = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
151
|
+
for (const r of Array.isArray(rows) ? rows : []) {
|
|
152
|
+
if (!r?.id || sessions.has(r.id))
|
|
153
|
+
continue;
|
|
154
|
+
const alive = isPidAlive(Number(r.pid));
|
|
155
|
+
sessions.set(r.id, {
|
|
156
|
+
id: String(r.id), cmd: String(r.cmd || ''), cwd: String(r.cwd || process.cwd()),
|
|
157
|
+
startedAt: Number(r.startedAt) || Date.now(), exitCode: r.exitCode ?? null,
|
|
158
|
+
status: r.status === 'running' ? (alive ? 'running' : 'exited') : (r.status || 'exited'),
|
|
159
|
+
output: alive ? '(重启后恢复: 进程仍在跑, 输出不再采集 —— 需要结果就重新跑或用文件落盘)' : '',
|
|
160
|
+
proc: null, pid: Number(r.pid) || undefined, detached: true,
|
|
161
|
+
});
|
|
162
|
+
n++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* 坏文件忽略 */ }
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let seq = 0;
|
|
2
|
+
/** 建一个 RunContext; 未给的字段一律显式置空 (不继承上一个 Run 的残留) */
|
|
3
|
+
export function createRunContext(partial = {}) {
|
|
4
|
+
seq += 1;
|
|
5
|
+
return {
|
|
6
|
+
requestId: partial.requestId ?? `rc-${Date.now().toString(36)}-${seq}`,
|
|
7
|
+
channelId: partial.channelId ?? '',
|
|
8
|
+
agentId: partial.agentId ?? '',
|
|
9
|
+
goalId: partial.goalId ?? '',
|
|
10
|
+
runId: partial.runId ?? '',
|
|
11
|
+
// 'chitchat' 是中性默认, 与旧实例字段初值一致 (不是继承上一个 Run 的残留)
|
|
12
|
+
intent: partial.intent ?? 'chitchat',
|
|
13
|
+
modelSnapshot: partial.modelSnapshot ?? null,
|
|
14
|
+
history: partial.history ?? null,
|
|
15
|
+
abortSignal: partial.abortSignal ?? null,
|
|
16
|
+
budget: partial.budget ?? null,
|
|
17
|
+
eventSink: partial.eventSink ?? null,
|
|
18
|
+
harnessContext: partial.harnessContext ?? null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 现在 `init_llm` 是必需阶段, LLM 不可用直接 unresolved (而不是偷偷退化成 fallback)。
|
|
16
16
|
*/
|
|
17
17
|
import * as os from 'os';
|
|
18
|
+
import { deliverThroughActor } from '../kernel/channel-actor.js';
|
|
18
19
|
import * as path from 'path';
|
|
19
20
|
import * as fsp from 'fs/promises';
|
|
20
21
|
import { readGoal } from './goal-store.js';
|
|
@@ -197,7 +198,7 @@ export async function resolveGoalRunner(req, opts = {}) {
|
|
|
197
198
|
}
|
|
198
199
|
agent.setGoalId?.(goal.goalId);
|
|
199
200
|
agent.setContinuationGuards?.(r.guards || []);
|
|
200
|
-
const reply = await agent.prompt(r.instruction);
|
|
201
|
+
const reply = await deliverThroughActor(agent, () => agent.prompt(r.instruction));
|
|
201
202
|
const runId = agent.getLastRunId?.() || agent.getRunId?.() || '';
|
|
202
203
|
// 没有 Run = 这次执行没有事实记录: 如实报告, 不让上层把"没跑"当"跑完"
|
|
203
204
|
if (!runId) {
|
|
@@ -83,7 +83,13 @@ const FALLBACK_PATH_DENYLIST = [
|
|
|
83
83
|
/(^|\/)tsconfig.*\.json$/,
|
|
84
84
|
/(^|\/)\.env(\.|$)/,
|
|
85
85
|
/(^|\/)\.git\//,
|
|
86
|
-
|
|
86
|
+
// 2026-10-01 (用户实测): 原先这行把**整个** ~/.bolloon/ 一刀封死 ⇒ patch 写自己的
|
|
87
|
+
// context-os/01-Me/… 被拒, 而同一目录 write_context_asset(Node fs) 却成功 ✗ —— 门在打自己的脚。
|
|
88
|
+
// 收窄口径: 只封**凭证 / 身份密钥 / 会话数据 / 钱包**; agent 自己的工作区
|
|
89
|
+
// (context-os · persona · memory · logs · goals · runs …) 一律放行。
|
|
90
|
+
/(^|\/)\.bolloon\/(identity\.json|keypair\.json|accounts\.json|llm-config\.json|bolloon-config\.json|agent-keys\/|iroh-secret[^/]*\.json|chain\.json)/,
|
|
91
|
+
/(^|\/)\.bolloon\/(sessions|wallets|bindings|eas-bind|_migrate_bak_[^/]*)\//,
|
|
92
|
+
/(^|\/)\.bolloon\/[^/]*\.token$/,
|
|
87
93
|
/(^|\/)dist\//,
|
|
88
94
|
// 2026-06-17: node_modules 不再 denylist, 因为 M3.4 自动 commit 阶段需要 npm install / npx vitest
|
|
89
95
|
// 通过 allowlist 限定 agent 只能 npm install, 不能 rm node_modules (shell arg denylist 仍禁 rm -rf)
|
|
@@ -116,7 +122,19 @@ function getDefaultPolicy() {
|
|
|
116
122
|
return {
|
|
117
123
|
version: 1,
|
|
118
124
|
commandAllowlist: Array.from(FALLBACK_COMMAND_ALLOWLIST),
|
|
119
|
-
|
|
125
|
+
// 2026-10-01: agent 自己的数据工作区必须显式允许 —— 这道护栏是 deny → allow → **默认拒**,
|
|
126
|
+
// 所以光把旧禁区收窄还不够: allowlist 里没有的路径照样被拒 ✗ (用户实测 context-os 被拒就是这个)。
|
|
127
|
+
pathAllowlist: [
|
|
128
|
+
...FALLBACK_PATH_ALLOWLIST,
|
|
129
|
+
...(process.env.HOME ? [
|
|
130
|
+
`${process.env.HOME}/.bolloon/context-os/**`,
|
|
131
|
+
`${process.env.HOME}/.bolloon/persona/**`,
|
|
132
|
+
`${process.env.HOME}/.bolloon/memory/**`,
|
|
133
|
+
`${process.env.HOME}/.bolloon/logs/**`,
|
|
134
|
+
`${process.env.HOME}/.bolloon/goals/**`,
|
|
135
|
+
`${process.env.HOME}/.bolloon/runs/**`,
|
|
136
|
+
] : []),
|
|
137
|
+
],
|
|
120
138
|
pathDenylist: FALLBACK_PATH_DENYLIST.map(r => r.source),
|
|
121
139
|
cooldownMs: 6 * 60 * 60 * 1000,
|
|
122
140
|
sandboxCwd: '.bolloon-shell-sandbox',
|
|
@@ -127,6 +145,44 @@ function getDefaultPolicy() {
|
|
|
127
145
|
* 加载策略 (有缓存)
|
|
128
146
|
* 加载失败返回 null, 调用方应回退到硬编码兜底
|
|
129
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* 已知过时的策略条目 (2026-10-01)。
|
|
150
|
+
*
|
|
151
|
+
* 事故: 盘上 `~/.bolloon/self-improve-policy.json` 是 2026-06-14 写下的**快照**,
|
|
152
|
+
* 里面还留着旧版的宽正则 `(^|\/)\.bolloon\/` —— 它把整个数据目录一刀封死,
|
|
153
|
+
* 于是 `patch ~/.bolloon/context-os/...` 被拒, 而 `write_context_asset`(走 Node fs) 同一目录能写 ✗,
|
|
154
|
+
* 用户看到的就是"同一个目录, 一个工具能写一个不能"。
|
|
155
|
+
* 更糟: 连 2026-06-17 已经明确解禁的 `pi-sdk.ts` 也还封着 ✗ ⇒ **改代码也压不过这份旧策略**。
|
|
156
|
+
* 规矩: 代码里的 FALLBACK_PATH_DENYLIST 是唯一真源; 盘上策略里凡属"过时快照"的条目一律作废并回写。
|
|
157
|
+
*/
|
|
158
|
+
/** agent 自己的数据工作区 (相对 ~/.bolloon) —— 必须在 allowlist 里, 否则默认拒 */
|
|
159
|
+
export const DATA_WORKSPACE_SUBDIRS = ['context-os', 'persona', 'memory', 'logs', 'goals', 'runs'];
|
|
160
|
+
/** 盘上策略缺这些条目时补齐 (与"作废过时条目"对称: 旧策略既要减法也要加法) */
|
|
161
|
+
function ensureWorkspaceAllowed(policy, home) {
|
|
162
|
+
if (!Array.isArray(policy.pathAllowlist))
|
|
163
|
+
policy.pathAllowlist = [];
|
|
164
|
+
let changed = false;
|
|
165
|
+
for (const sub of DATA_WORKSPACE_SUBDIRS) {
|
|
166
|
+
const need = `${home}/.bolloon/${sub}/**`;
|
|
167
|
+
if (!policy.pathAllowlist.includes(need)) {
|
|
168
|
+
policy.pathAllowlist.push(need);
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return changed;
|
|
173
|
+
}
|
|
174
|
+
const STALE_POLICY_PATTERNS = [
|
|
175
|
+
'(^|\\/)\\.bolloon\\/', // 旧: 整个数据目录一刀封 (已收窄为凭证/身份/会话/钱包)
|
|
176
|
+
'(^|\\/)src\\/agents\\/pi-sdk\\.ts$', // 旧: 2026-06-17 已解禁 (自进化需要)
|
|
177
|
+
'(^|\\/)node_modules\\/', // 旧: 2026-06-17 已从硬编码清单移除
|
|
178
|
+
];
|
|
179
|
+
function dropStalePolicyPatterns(policy) {
|
|
180
|
+
if (!Array.isArray(policy.pathDenylist) || policy.pathDenylist.length === 0)
|
|
181
|
+
return false;
|
|
182
|
+
const before = policy.pathDenylist.length;
|
|
183
|
+
policy.pathDenylist = policy.pathDenylist.filter((src) => !STALE_POLICY_PATTERNS.includes(String(src)));
|
|
184
|
+
return policy.pathDenylist.length !== before;
|
|
185
|
+
}
|
|
130
186
|
export function loadPolicy(forceReload = false) {
|
|
131
187
|
const now = Date.now();
|
|
132
188
|
if (!forceReload && cachedPolicy && now - policyLoadedAt < POLICY_TTL_MS) {
|
|
@@ -142,6 +198,17 @@ export function loadPolicy(forceReload = false) {
|
|
|
142
198
|
}
|
|
143
199
|
const raw = fs.readFileSync(POLICY_PATH, 'utf-8');
|
|
144
200
|
const parsed = JSON.parse(raw);
|
|
201
|
+
// 2026-10-01: 盘上策略是**快照** —— 先作废已知过时条目 (否则改代码也压不过它), 再回写。
|
|
202
|
+
// 用户实测: `(^|\/)\.bolloon\/` 这条旧宽正则把整个数据目录封死, 而它只存在于盘上策略里。
|
|
203
|
+
try {
|
|
204
|
+
const dropped = dropStalePolicyPatterns(parsed);
|
|
205
|
+
const added = ensureWorkspaceAllowed(parsed, os.homedir());
|
|
206
|
+
if (dropped || added) {
|
|
207
|
+
fs.writeFileSync(POLICY_PATH, JSON.stringify(parsed, null, 2));
|
|
208
|
+
console.log(`[shell-guard] 已作废过时策略条目并回写: ${POLICY_PATH}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch { /* 回写失败不影响本次使用 */ }
|
|
145
212
|
// 极简 schema 校验
|
|
146
213
|
if (!parsed.version || !Array.isArray(parsed.commandAllowlist) || !Array.isArray(parsed.pathAllowlist) || !Array.isArray(parsed.pathDenylist)) {
|
|
147
214
|
console.warn('[shell-guard] 策略文件 schema 不对, 用硬编码兜底');
|
|
@@ -214,7 +281,7 @@ export function checkCommand(cmd, args) {
|
|
|
214
281
|
auditShellCall('denied', cmd, args, `命令 '${cmd}' 不在白名单`);
|
|
215
282
|
return {
|
|
216
283
|
allowed: false,
|
|
217
|
-
reason: `命令 '${cmd}' 不在白名单.
|
|
284
|
+
reason: `命令 '${cmd}' 不在白名单. 允许的命令: ${shortRoots(Array.from(allowSet))}`,
|
|
218
285
|
matchedBy: policy ? 'cmd-allowlist' : 'fallback-deny'
|
|
219
286
|
};
|
|
220
287
|
}
|
|
@@ -297,7 +364,7 @@ export function checkWritePath(targetPath) {
|
|
|
297
364
|
auditShellCall('denied', '', [], `路径 '${targetPath}' 不在任何 allowlist 中`, targetPath);
|
|
298
365
|
return {
|
|
299
366
|
allowed: false,
|
|
300
|
-
reason: `路径 '${targetPath}' 不在白名单.
|
|
367
|
+
reason: `路径 '${targetPath}' 不在白名单. 允许的根: ${shortRoots(allowlist)}`, // 2026-10-01: 不再把整张白名单(含临时目录)塞进错误 ✓
|
|
301
368
|
matchedBy: 'path-allowlist'
|
|
302
369
|
};
|
|
303
370
|
}
|
|
@@ -340,6 +407,19 @@ export const SHELL_SANDBOX_CWD = path.resolve(process.cwd(), '.bolloon-shell-san
|
|
|
340
407
|
// 与 checkCommand 的区别: 不查命令白名单/参数 allowlist — 完整 shell 命令字符串放行,
|
|
341
408
|
// 只挡"高危破坏/碰核心数据"模式. 用户明确: 灵活一点, 少围栏, 核心的东西不碰不搞乱.
|
|
342
409
|
// ============================================================================
|
|
410
|
+
/**
|
|
411
|
+
* 自生命周期模式 (唯一的归类依据, 2026-10-01)。
|
|
412
|
+
* 为什么单列: 报错文案要按"是不是自生命周期"分岔, 原先用"模式串里有没有 bolloon 等词"猜 ⇒
|
|
413
|
+
* 数据目录那条 (串里含 .bolloon) 被误判, 把只读命令报成"会杀死宿主服务" ✗。
|
|
414
|
+
*/
|
|
415
|
+
const SELF_LIFECYCLE_PATTERNS = [
|
|
416
|
+
/\b(bolloon|bolloon-agent|bolloon-agent-service)\s+(restart|stop|kill|quit|shutdown)\b/i,
|
|
417
|
+
/\bpm2\s+(restart|stop|delete|kill)\s+[\w./-]*bolloon\b/i,
|
|
418
|
+
/\b(systemctl|service)\s+(restart|stop|kill)\s+[\w.-]*bolloon\b/i,
|
|
419
|
+
/\bpkill\s+(-[a-z]+\s+)*(-f\s+)?[\w./-]*bolloon\b/i,
|
|
420
|
+
/\btaskkill\s+[^|]*\/IM\s+[\w-]*bolloon\b/i,
|
|
421
|
+
/\btaskkill\s+[^|]*\/FI\s+\"?[^\"]*bolloon/i,
|
|
422
|
+
];
|
|
343
423
|
const TERMINAL_DENY_PATTERNS = [
|
|
344
424
|
/\bsudo\b/, // 提权
|
|
345
425
|
/\bsu\b/,
|
|
@@ -352,8 +432,15 @@ const TERMINAL_DENY_PATTERNS = [
|
|
|
352
432
|
/\bwget\b[^|]*\|\s*(sh|bash)\b/,
|
|
353
433
|
/\b:\(\)\s*\{[^}]*\}\s*;\s*:/, // fork bomb
|
|
354
434
|
/\b(>|>>)\s*(\/etc\/|\/usr\/|\/System\/|\/bin\/|\/sbin\/)/, // 写系统目录
|
|
355
|
-
/[\/\s]\.bolloon\b
|
|
356
|
-
/
|
|
435
|
+
// 2026-10-01 (用户报「偶尔会出反水 bug」): 原先 /[\/\s]\.bolloon\b/ 会匹配**任何**含 .bolloon 的路径
|
|
436
|
+
// ⇒ 连 `ls ~/.bolloon/`、`cat .bolloon/x`、`grep -rn x .bolloon/` 这种**只读**命令都被拒;
|
|
437
|
+
// 而且因为模式串里带 "bolloon", 还会被下面的分类逻辑误判成"自生命周期命令", 打出
|
|
438
|
+
// "命令会重启或杀死 bolloon 宿主服务" —— 门在打自己的脚。
|
|
439
|
+
// 收窄口径: **只在删/移/截断/重定向写入**这些数据目录时拒; 读 (ls/cat/grep/find/du/stat/mkdir -p) 一律放行。
|
|
440
|
+
/\b(rm|rmdir|mv|truncate|shred)\b[^|;]*[\s/]\.bolloon\b/,
|
|
441
|
+
/(>|>>)\s*[^\s]*[\s/]?\.bolloon\b/,
|
|
442
|
+
/\b(rm|rmdir|mv|truncate|shred)\b[^|;]*[\s/]\.(diap|hermes|openclaw)\b/, // 其他 agent 数据 (同样只拦写/删)
|
|
443
|
+
/(>|>>)\s*[^\s]*[\s/]?\.(diap|hermes|openclaw)\b/,
|
|
357
444
|
/\brm\s+-rf\s+(\/|~|\*|\.|\$HOME)\b/, // 删根/家/通配
|
|
358
445
|
/\bgit\s+push\b[^|]*\s+(-f|--force)/, // 强推
|
|
359
446
|
/\bgit\s+reset\s+--hard\b/,
|
|
@@ -361,12 +448,7 @@ const TERMINAL_DENY_PATTERNS = [
|
|
|
361
448
|
// 2026-08-11 (Hermes cron/lifecycle_guard 模式): 自生命周期命令 — agent 通过 terminal
|
|
362
449
|
// 重启/杀掉自己宿主服务 (bolloon) 会形成复活循环: 服务死 → supervisor 复活 → 自动 resume
|
|
363
450
|
// → 同一命令再自杀. 策略: 命令形状锚定 (只匹配真实命令标识符, 不匹配散文, 误报率低).
|
|
364
|
-
|
|
365
|
-
/\bpm2\s+(restart|stop|delete|kill)\s+[\w./-]*bolloon\b/i,
|
|
366
|
-
/\b(systemctl|service)\s+(restart|stop|kill)\s+[\w.-]*bolloon\b/i,
|
|
367
|
-
/\bpkill\s+(-[a-z]+\s+)*(-f\s+)?[\w./-]*bolloon\b/i,
|
|
368
|
-
/\btaskkill\s+[^|]*\/IM\s+[\w-]*bolloon\b/i,
|
|
369
|
-
/\btaskkill\s+[^|]*\/FI\s+"?[^"]*bolloon/i,
|
|
451
|
+
...SELF_LIFECYCLE_PATTERNS,
|
|
370
452
|
];
|
|
371
453
|
/** 检查完整 shell 命令 (terminal 工具用). denylist-only: 命中高危模式即拒, 否则放行. */
|
|
372
454
|
export function checkTerminalCommand(rawCmd) {
|
|
@@ -379,7 +461,9 @@ export function checkTerminalCommand(rawCmd) {
|
|
|
379
461
|
// 2026-08-11: 用户可见静态文案走 i18n 目录 (Hermes locales 模式); 目录不可用时中文直出
|
|
380
462
|
let reason;
|
|
381
463
|
try {
|
|
382
|
-
|
|
464
|
+
// 2026-10-01: 归类改成**显式名单** —— 原先靠"模式串里有没有 bolloon 等词"猜, 于是
|
|
465
|
+
// 数据目录那条 (串里含 .bolloon) 被误判成自生命周期, 报错文案完全指错方向 ✗。
|
|
466
|
+
const isSelfLifecycle = SELF_LIFECYCLE_PATTERNS.includes(pattern);
|
|
383
467
|
reason = isSelfLifecycle
|
|
384
468
|
? i18nT('guard.self_lifecycle_reason', undefined, { pattern: String(pattern) })
|
|
385
469
|
: i18nT('guard.deny_reason', undefined, { pattern: String(pattern) });
|
|
@@ -417,3 +501,15 @@ export function writePolicy(newPolicy) {
|
|
|
417
501
|
return false;
|
|
418
502
|
}
|
|
419
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* 错误信息里的白名单**短形态** (2026-10-01)。
|
|
506
|
+
* 为什么: 原来把**整张白名单**拼进 `reason` ✗ ⇒ 这条报错随后被塞进
|
|
507
|
+
* ① 反思框(用户看到一屏白名单 ✗) ② 最终回复框(看着像"回复不完整" ✗ —— 其实是白名单切片 ✗)
|
|
508
|
+
* ③ 熔断判定(长文本撞上无关关键词 ⇒ 被误判成"鉴权类错误" ⇒ 直接熔断停在 needs_human ✗)。
|
|
509
|
+
* 做法: 滤掉临时目录噪音(不该出现在给人看的错误里 ✓) + 只列前若干条 + 给总数 ✓。
|
|
510
|
+
*/
|
|
511
|
+
function shortRoots(entries, max = 6) {
|
|
512
|
+
const clean = entries.filter((e) => !/bolloon-bootstrap|\/var\/folders\/|\/tmp\//.test(e));
|
|
513
|
+
const shown = clean.slice(0, max).join(', ');
|
|
514
|
+
return clean.length > max ? `${shown} …(共 ${clean.length} 条)` : (shown || '(空)');
|
|
515
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能用量遥测 + 生命周期 (2026-10-01)。
|
|
3
|
+
* 形态要点: 遥测放**旁挂文件** ✓ —— 绝不写进用户自己写的 SKILL.md ✗(那是用户的文件 ✓)。
|
|
4
|
+
* 生命周期: active → stale(久未用) → archived; `pinned` 可退出自动流转 ✓。
|
|
5
|
+
* 边界(如实): 本模块只**计算状态并给建议** ✓, **不自动移动/删除任何技能文件** ✗(那是破坏性动作, 交人决定 ✓)。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
export function usagePath(home = os.homedir()) {
|
|
11
|
+
return path.join(home, '.bolloon', 'skills', '.usage.json');
|
|
12
|
+
}
|
|
13
|
+
function load(home) {
|
|
14
|
+
try {
|
|
15
|
+
const o = JSON.parse(fs.readFileSync(usagePath(home), 'utf-8'));
|
|
16
|
+
return o && typeof o === 'object' ? o : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function save(m, home) {
|
|
23
|
+
try {
|
|
24
|
+
const p = usagePath(home);
|
|
25
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
26
|
+
fs.writeFileSync(p, JSON.stringify(m), { mode: 0o600 });
|
|
27
|
+
}
|
|
28
|
+
catch { /* 遥测失败不影响工具调用 */ }
|
|
29
|
+
}
|
|
30
|
+
/** 用了一次(读技能时调 ✓); 失败只吞 ✓ */
|
|
31
|
+
export function bumpUsage(skill, home, now = Date.now()) {
|
|
32
|
+
try {
|
|
33
|
+
const m = load(home);
|
|
34
|
+
const cur = m[skill] || { count: 0, lastUsedAt: 0 };
|
|
35
|
+
m[skill] = { ...cur, count: cur.count + 1, lastUsedAt: now };
|
|
36
|
+
save(m, home);
|
|
37
|
+
}
|
|
38
|
+
catch { /* best-effort */ }
|
|
39
|
+
}
|
|
40
|
+
/** 记下创建者(自审 / 用户点名) */
|
|
41
|
+
export function markCreatedBy(skill, by, home, now = Date.now()) {
|
|
42
|
+
try {
|
|
43
|
+
const m = load(home);
|
|
44
|
+
const cur = m[skill] || { count: 0, lastUsedAt: 0 };
|
|
45
|
+
m[skill] = { ...cur, createdBy: by, lastUsedAt: cur.lastUsedAt || now };
|
|
46
|
+
save(m, home);
|
|
47
|
+
}
|
|
48
|
+
catch { /* best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
/** 钉住(退出自动流转) */
|
|
51
|
+
export function setPinned(skill, pinned, home) {
|
|
52
|
+
try {
|
|
53
|
+
const m = load(home);
|
|
54
|
+
const cur = m[skill] || { count: 0, lastUsedAt: 0 };
|
|
55
|
+
m[skill] = { ...cur, pinned };
|
|
56
|
+
save(m, home);
|
|
57
|
+
}
|
|
58
|
+
catch { /* best-effort */ }
|
|
59
|
+
}
|
|
60
|
+
export const STALE_AFTER_MS = 45 * 24 * 3600 * 1000; // 45 天没用 ⇒ stale
|
|
61
|
+
/** 生命周期(只算不给动作 ✓): active / stale / archived-eligible */
|
|
62
|
+
export function lifecycleOf(skill, home, now = Date.now()) {
|
|
63
|
+
const u = load(home)[skill];
|
|
64
|
+
if (!u)
|
|
65
|
+
return { state: 'unknown', reason: '旁挂遥测里没有它(可能是导入/内置技能)', count: 0, pinned: false };
|
|
66
|
+
if (u.pinned)
|
|
67
|
+
return { state: 'active', reason: '已 pinned(退出自动流转)', count: u.count, pinned: true };
|
|
68
|
+
const idle = now - (u.lastUsedAt || 0);
|
|
69
|
+
return {
|
|
70
|
+
state: idle > STALE_AFTER_MS ? 'stale' : 'active',
|
|
71
|
+
reason: idle > STALE_AFTER_MS ? `已 ${Math.round(idle / 86400000)} 天未用(仅状态, 不会自动删 ✓)` : `近用过(${u.count} 次)`,
|
|
72
|
+
count: u.count,
|
|
73
|
+
pinned: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** 汇总(给面板/工具用 ✓) */
|
|
77
|
+
export function usageSummary(home, now = Date.now()) {
|
|
78
|
+
const m = load(home);
|
|
79
|
+
let active = 0, stale = 0, pinned = 0;
|
|
80
|
+
for (const k of Object.keys(m)) {
|
|
81
|
+
const l = lifecycleOf(k, home, now);
|
|
82
|
+
if (l.pinned)
|
|
83
|
+
pinned++;
|
|
84
|
+
if (l.state === 'active')
|
|
85
|
+
active++;
|
|
86
|
+
else if (l.state === 'stale')
|
|
87
|
+
stale++;
|
|
88
|
+
}
|
|
89
|
+
return { total: Object.keys(m).length, active, stale, pinned };
|
|
90
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能账本 + 单条回滚 + 写来源隔离 (2026-10-01, 按"账本而非闸门"的形态做)。
|
|
3
|
+
*
|
|
4
|
+
* 三件事:
|
|
5
|
+
* ① **账本**: 每次技能变更追加一条 JSONL(前后清单 + 内容寻址备份)⇒ 可单条回滚 ✓。
|
|
6
|
+
* 放 JSONL 不放数据库: 耐久 · 可 grep · 数据库重置也不丢 ✓。
|
|
7
|
+
* ② **单条回滚**: 按 id 还原那一次变更; **唯独回滚是 fail-closed** ✓(安全快照拿不到就拒绝回滚,
|
|
8
|
+
* 绝不"半还原"✗); 其余全部吞错只记日志(账本坏掉不该挡住干活 ✓)。
|
|
9
|
+
* ③ **写来源隔离**: 用 AsyncLocalStorage 区分"用户点名要的写"(foreground ✓)与
|
|
10
|
+
* "自审/复盘产生的写"(review ✓)⇒ 自审**只许治理自己造出来的技能** ✓, 用户要的归用户 ✓。
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
17
|
+
const originStore = new AsyncLocalStorage();
|
|
18
|
+
/** 在"自审"上下文里跑一段(其余一律按 foreground ✓) */
|
|
19
|
+
export function runWithWriteOrigin(origin, fn) {
|
|
20
|
+
return originStore.run({ origin }, fn);
|
|
21
|
+
}
|
|
22
|
+
export function currentWriteOrigin() {
|
|
23
|
+
return originStore.getStore()?.origin ?? 'foreground';
|
|
24
|
+
}
|
|
25
|
+
export function isReviewWrite() {
|
|
26
|
+
return currentWriteOrigin() === 'review';
|
|
27
|
+
}
|
|
28
|
+
export function ledgerPath(home = os.homedir()) {
|
|
29
|
+
return path.join(home, '.bolloon', 'skills', '.skill-ledger.jsonl');
|
|
30
|
+
}
|
|
31
|
+
export function blobsDir(home = os.homedir()) {
|
|
32
|
+
return path.join(home, '.bolloon', 'skills', '.ledger-blobs');
|
|
33
|
+
}
|
|
34
|
+
function sha256(text) {
|
|
35
|
+
return crypto.createHash('sha256').update(text, 'utf-8').digest('hex');
|
|
36
|
+
}
|
|
37
|
+
/** 把内容按哈希存进 blobs(已存在则复用 ✓ 天然去重) */
|
|
38
|
+
function putBlob(text, home) {
|
|
39
|
+
const h = sha256(text);
|
|
40
|
+
const p = path.join(blobsDir(home), h);
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(p)) {
|
|
43
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
44
|
+
fs.writeFileSync(p, text, { mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch { /* 备份失败 ⇒ 由调用方决定是否允许这次变更(见 captureBefore) */ }
|
|
48
|
+
return h;
|
|
49
|
+
}
|
|
50
|
+
/** 变更前快照: 返回 beforeSha(拿不到安全快照时返回 undefined ⇒ 回滚能力缺失, 调用方须知道 ✓) */
|
|
51
|
+
export function captureSkillBefore(file, home) {
|
|
52
|
+
try {
|
|
53
|
+
const text = fs.readFileSync(file, 'utf-8');
|
|
54
|
+
return { beforeSha: putBlob(text, home), bytesBefore: text.length };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {}; // 文件不存在(新建)= 正常; 读不到 = 快照缺失
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** 追加一条账本(吞错只记日志 ✓ —— 账本坏掉不该挡住干活) */
|
|
61
|
+
export function appendLedger(entry, home) {
|
|
62
|
+
const full = { id: entry.id || crypto.randomBytes(8).toString('hex'), at: entry.at ?? Date.now(), ...entry };
|
|
63
|
+
try {
|
|
64
|
+
const p = ledgerPath(home);
|
|
65
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
66
|
+
fs.appendFileSync(p, JSON.stringify(full) + '\n', { mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
catch { /* telemetry, not a gate */ }
|
|
69
|
+
return full;
|
|
70
|
+
}
|
|
71
|
+
export function readLedger(home) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs.readFileSync(ledgerPath(home), 'utf-8');
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const line of raw.split('\n')) {
|
|
76
|
+
if (!line.trim())
|
|
77
|
+
continue;
|
|
78
|
+
try {
|
|
79
|
+
out.push(JSON.parse(line));
|
|
80
|
+
}
|
|
81
|
+
catch { /* 坏行跳过 */ }
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** 这个技能是不是"自审造出来的"?(决定自审有没有资格改它 ✓) */
|
|
90
|
+
export function isAgentCreated(skill, home) {
|
|
91
|
+
return readLedger(home).some((e) => e.skill === skill && e.tool === 'create_skill' && e.origin === 'review');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 自审写前的资格检查(只读判断, 不改任何东西 ✓):
|
|
95
|
+
* 自审只许动**自己造出来**的技能 ✓; 用户点名要的归用户 ✓。
|
|
96
|
+
*/
|
|
97
|
+
export function reviewMayTouch(skill, home) {
|
|
98
|
+
if (!isReviewWrite())
|
|
99
|
+
return { ok: true, reason: 'foreground(用户点名要的写)' };
|
|
100
|
+
return isAgentCreated(skill, home)
|
|
101
|
+
? { ok: true, reason: 'review(该技能由自审创建, 可治理)' }
|
|
102
|
+
: { ok: false, reason: '该技能不是自审创建的(用户点名要的归用户)⇒ 自审只记候选, 不动它' };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 单条回滚: 把某次变更前的快照还原回去 ✓。
|
|
106
|
+
* **fail-closed**: 快照缺失/内容哈希对不上 ⇒ 拒绝(绝不半还原 ✗)。
|
|
107
|
+
*/
|
|
108
|
+
export function rollbackEntry(id, home) {
|
|
109
|
+
const entry = readLedger(home).find((e) => e.id === id);
|
|
110
|
+
if (!entry)
|
|
111
|
+
return { ok: false, reason: `账本里没有 id=${id}` };
|
|
112
|
+
if (entry.tool === 'create_skill') {
|
|
113
|
+
// 创建类变更: 回滚 = 删掉那个文件(要用户明确同意 ⇒ 这里只报告, 不擅自删 ✗)
|
|
114
|
+
return { ok: false, reason: '该条是"新建技能"; 回滚等于删除文件 —— 需要你明确同意, 我不擅自删' };
|
|
115
|
+
}
|
|
116
|
+
if (!entry.beforeSha)
|
|
117
|
+
return { ok: false, reason: '该条没有变更前快照 ⇒ 拒绝回滚(fail-closed, 不做半还原)' };
|
|
118
|
+
try {
|
|
119
|
+
const blob = fs.readFileSync(path.join(blobsDir(home), entry.beforeSha), 'utf-8');
|
|
120
|
+
if (sha256(blob) !== entry.beforeSha)
|
|
121
|
+
return { ok: false, reason: '快照内容与哈希不符 ⇒ 拒绝回滚' };
|
|
122
|
+
fs.writeFileSync(entry.file, blob, { mode: 0o600 });
|
|
123
|
+
appendLedger({ origin: 'foreground', tool: 'rollback', skill: entry.skill, file: entry.file, beforeSha: entry.afterSha, afterSha: entry.beforeSha, note: `回到 ${entry.id}` }, home);
|
|
124
|
+
return { ok: true, reason: `已回滚到变更前(${entry.id})`, restored: entry.file };
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
return { ok: false, reason: `回滚失败: ${String(e?.message || e).slice(0, 120)}` };
|
|
128
|
+
}
|
|
129
|
+
}
|