ppxans-harness 2.4.0 → 3.2.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/LICENSE +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
package/src/tools/builtin.js
CHANGED
|
@@ -1,298 +1,385 @@
|
|
|
1
|
-
// src/tools/builtin.js - 内置工具集 (皮皮虾的手脚)
|
|
2
|
-
// 文件/命令/时间/记忆查询 — 全部零依赖, 用 Node 原生
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
|
-
import { scrubPII } from "../utils/pii.js";
|
|
9
|
-
import { LocalShellProvider } from "../seam/shell.js";
|
|
10
|
-
import { checkCommand, DENY_HINT } from "./command-guard.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (!
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1
|
+
// src/tools/builtin.js - 内置工具集 (皮皮虾的手脚)
|
|
2
|
+
// 文件/命令/时间/记忆查询 — 全部零依赖, 用 Node 原生
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { scrubPII } from "../utils/pii.js";
|
|
9
|
+
import { LocalShellProvider } from "../seam/shell.js";
|
|
10
|
+
import { checkCommand, DENY_HINT } from "./command-guard.js";
|
|
11
|
+
import { formatToolResultHeader, countLines } from "./seam.js";
|
|
12
|
+
import { debug } from "../utils/logger.js";
|
|
13
|
+
|
|
14
|
+
const execFileP = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
// 默认 shell provider (未通过 seam 注入时的兜底, 供测试/独立工具目录使用)
|
|
17
|
+
const defaultShell = new LocalShellProvider();
|
|
18
|
+
|
|
19
|
+
// 图片 MIME 表 (read_image + 多模态注入共用)
|
|
20
|
+
const IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp" };
|
|
21
|
+
|
|
22
|
+
// 读图片文件转 base64 data URL (供 read_image 工具与多模态 user 消息注入复用)
|
|
23
|
+
export function imageFileToDataUrl(rootDir, p, { maxBytes = 8 * 1024 * 1024 } = {}) {
|
|
24
|
+
const fp = safePath(rootDir, p);
|
|
25
|
+
const mime = IMAGE_MIME[path.extname(fp).toLowerCase()];
|
|
26
|
+
if (!mime) throw new Error("不支持的文件类型: " + path.extname(fp));
|
|
27
|
+
if (!fs.existsSync(fp)) throw new Error("文件不存在: " + p);
|
|
28
|
+
const buf = fs.readFileSync(fp);
|
|
29
|
+
if (buf.length > maxBytes) throw new Error(`图片过大 (>${Math.round(maxBytes / 1024 / 1024)}MB)`);
|
|
30
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---- run_command 安全策略 (P0): 统一走命令守卫 (src/tools/command-guard.js) ----
|
|
34
|
+
// 三层防线: 用户 deny(security.deny) -> 硬黑名单(allow_all 也拦) -> 常规高危 + 前缀白名单
|
|
35
|
+
// 命令守卫的 isDeniedCommand/isAllowedCommand 兼容导出见 command-guard.js
|
|
36
|
+
|
|
37
|
+
// 安全路径: 阻止逃出工作目录 (防路径穿越)
|
|
38
|
+
// v1.0.9: 追加 realpath 校验 — 字符串前缀检查可被工作区内 symlink 指向外部绕过 (resolve 后仍在 root 内但实际文件在外部)
|
|
39
|
+
export function safePath(root, p) {
|
|
40
|
+
// 跨平台一致防护: Windows 盘符路径 (C:\... / C:/... / C:...) 在 Windows 宿主上
|
|
41
|
+
// 会被 resolve 判为绝对路径而越界拒绝, 但在 POSIX 宿主上会被当作普通相对路径
|
|
42
|
+
// 放行 (创建出名为 "C:\Windows" 的怪异文件)。统一在入口拒绝, 保证安全不变量
|
|
43
|
+
// 与宿主平台无关 (2026-10-01 修复: repo_map 测试在 Linux 失败暴露)。
|
|
44
|
+
if (typeof p === "string" && /^[a-zA-Z]:/.test(p)) {
|
|
45
|
+
throw new Error(`路径越界拒绝: ${p}`);
|
|
46
|
+
}
|
|
47
|
+
const resolved = path.resolve(root, p);
|
|
48
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
49
|
+
throw new Error(`路径越界拒绝: ${p}`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const realRoot = fs.realpathSync(root);
|
|
53
|
+
let target = resolved;
|
|
54
|
+
if (fs.existsSync(target)) {
|
|
55
|
+
target = fs.realpathSync(target); // 存在: 直接解析真实路径
|
|
56
|
+
} else {
|
|
57
|
+
// 不存在: 用最近已存在父目录的真实路径 + 剩余部分 (新建文件场景)
|
|
58
|
+
let dir = path.dirname(target);
|
|
59
|
+
while (dir !== root && dir !== path.dirname(dir) && !fs.existsSync(dir)) dir = path.dirname(dir);
|
|
60
|
+
target = path.join(fs.realpathSync(fs.existsSync(dir) ? dir : root), path.relative(dir, resolved));
|
|
61
|
+
}
|
|
62
|
+
if (target !== realRoot && !target.startsWith(realRoot + path.sep)) {
|
|
63
|
+
throw new Error(`路径越界拒绝 (符号链接): ${p}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (e && e.message && e.message.includes("路径越界拒绝")) throw e;
|
|
67
|
+
// root 不存在等边缘: 退回前缀检查 (已通过)
|
|
68
|
+
}
|
|
69
|
+
return resolved;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- code_act (CodeAct 出口): 一次提交脚本批量操作, 压 N 轮工具往返 → 1 轮 ----// 安全: 默认关闭 (security.code_act), 开启后限 python/node 解释器 + 工作目录 + 超时 + PII + 黑名单扫描
|
|
73
|
+
// 相比 run_command 的增量风险: 脚本体绕过命令串黑名单, 故独立开关 + 默认关闭
|
|
74
|
+
// 沙箱加固 (进程级): 干净环境变量(剥离密钥/令牌) + node 内存上限 + 超时强杀进程树 + 输出上限
|
|
75
|
+
// 真正隔离需外部 Docker/MicroVM (见 docs/CONFIG.md), 此处为无依赖下的最大进程级约束
|
|
76
|
+
|
|
77
|
+
// 干净沙箱环境: 只保留运行必需变量, 剥离一切敏感密钥/令牌/凭证, 防脚本窃取宿主凭据
|
|
78
|
+
const SANDBOX_ENV_KEEP = /^(PATH|PATHEXT|SYSTEMROOT|WINDIR|TEMP|TMP|USERPROFILE|HOMEDRIVE|HOMEPATH|COMSPEC|OS|PROCESSOR_ARCHITECTURE|PROCESSOR_IDENTIFIER|NUMBER_OF_PROCESSORS|LANG|LC_|PYTHONIOENCODING|PYTHONPATH)$/i;
|
|
79
|
+
function sandboxEnv() {
|
|
80
|
+
const env = {};
|
|
81
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
82
|
+
if (v && SANDBOX_ENV_KEEP.test(k)) env[k] = v;
|
|
83
|
+
}
|
|
84
|
+
return env;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const CODE_ACT_MEMORY_MB = 256; // node 解释器内存上限
|
|
88
|
+
const CODE_ACT_OUTPUT_MAX = 512 * 1024; // 输出上限 512KB
|
|
89
|
+
|
|
90
|
+
export async function runCodeAct(rootDir, lang, code, timeoutMs) {
|
|
91
|
+
const isWin = process.platform === "win32";
|
|
92
|
+
const ext = lang === "node" ? "js" : "py";
|
|
93
|
+
const tmp = path.join(os.tmpdir(), `ppx_codeact_${Date.now()}_${Math.random().toString(36).slice(2, 6)}.${ext}`);
|
|
94
|
+
fs.writeFileSync(tmp, code, "utf8");
|
|
95
|
+
const interpreter = lang === "node" ? process.execPath : (isWin ? "python" : "python3");
|
|
96
|
+
// node 加内存上限; python 无等价零依赖参数 (可外接 Docker 时再限制)
|
|
97
|
+
const args = lang === "node" ? [`--max-old-space-size=${CODE_ACT_MEMORY_MB}`, tmp] : [tmp];
|
|
98
|
+
const t0 = Date.now();
|
|
99
|
+
try {
|
|
100
|
+
const { stdout, stderr } = await execFileP(interpreter, args, {
|
|
101
|
+
cwd: rootDir,
|
|
102
|
+
timeout: timeoutMs || 30000,
|
|
103
|
+
maxBuffer: CODE_ACT_OUTPUT_MAX,
|
|
104
|
+
env: sandboxEnv(),
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
});
|
|
107
|
+
const ms = Date.now() - t0;
|
|
108
|
+
const out = (stdout || "") + (stderr ? "\n[stderr] " + stderr : "");
|
|
109
|
+
// B1: code_act 同样带统一元数据头 (成功即 exit=0)
|
|
110
|
+
const head = formatToolResultHeader({ ms, lineCount: countLines(out), exitCode: 0 });
|
|
111
|
+
return head + "\n" + (scrubPII(out).cleaned.slice(0, 20000) || "(无输出)");
|
|
112
|
+
} catch (e) {
|
|
113
|
+
const ms = Date.now() - t0;
|
|
114
|
+
const timedOut = !!(e && (e.killed || /timed out|ETIMEDOUT/i.test(e.message)));
|
|
115
|
+
const head = formatToolResultHeader({ ms, timedOut, timedOutMs: timeoutMs || 30000 });
|
|
116
|
+
// 超时/输出超限/内存超限的友好提示
|
|
117
|
+
if (timedOut) return head;
|
|
118
|
+
return head + "\n" + JSON.stringify({ error: e.message, code: e.code });
|
|
119
|
+
} finally {
|
|
120
|
+
try { fs.rmSync(tmp, { force: true }); } catch (e) { debug(`[tools/builtin] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 注册全部内置工具
|
|
125
|
+
export function registerBuiltinTools(catalog, { rootDir, facts, memory }) {
|
|
126
|
+
// 1. 读文件
|
|
127
|
+
catalog.register({
|
|
128
|
+
name: "read_file",
|
|
129
|
+
capability: { readOnly: true, riskLevel: "low", sideEffect: "none" },
|
|
130
|
+
description: "读取文件内容。返回文件文本。",
|
|
131
|
+
parameters: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: { path: { type: "string", description: "文件路径 (相对工作目录)" } },
|
|
134
|
+
required: ["path"],
|
|
135
|
+
},
|
|
136
|
+
execute: async (args) => {
|
|
137
|
+
const p = safePath(rootDir, args.path);
|
|
138
|
+
if (!fs.existsSync(p)) return JSON.stringify({ error: `文件不存在: ${args.path}` });
|
|
139
|
+
const content = fs.readFileSync(p, "utf8");
|
|
140
|
+
// v1.0.9: 输出 PII 脱敏 (与 run_command/code_act 一致, 文件可能含密钥/手机号)
|
|
141
|
+
return scrubPII(content).cleaned.slice(0, 20000);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 2. 写文件
|
|
146
|
+
catalog.register({
|
|
147
|
+
name: "write_file",
|
|
148
|
+
capability: { riskLevel: "medium", sideEffect: "workspace" },
|
|
149
|
+
description: "写入文件 (覆盖)。可用于创建/修改文件。",
|
|
150
|
+
parameters: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
path: { type: "string", description: "文件路径" },
|
|
154
|
+
content: { type: "string", description: "要写入的内容" },
|
|
155
|
+
},
|
|
156
|
+
required: ["path", "content"],
|
|
157
|
+
},
|
|
158
|
+
execute: async (args) => {
|
|
159
|
+
// v1.0.9: 写入内容上限 512KB (防撑爆磁盘); 路径是目录时给友好错误
|
|
160
|
+
const content = String(args.content ?? "");
|
|
161
|
+
if (content.length > 512 * 1024) return JSON.stringify({ error: `写入内容过大 (>512KB, 当前 ${content.length} 字符)` });
|
|
162
|
+
const p = safePath(rootDir, args.path);
|
|
163
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
164
|
+
return JSON.stringify({ error: `目标是目录: ${args.path}` });
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
168
|
+
fs.writeFileSync(p, content, "utf8");
|
|
169
|
+
return JSON.stringify({ ok: true, bytes: Buffer.byteLength(content) });
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return JSON.stringify({ error: `写入失败: ${e.message}` });
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// 2b. 追加文件 (2026-10-02 基线暴露缺口: 无追加能力时 agent 只能整体重写, 易丢原内容)
|
|
177
|
+
catalog.register({
|
|
178
|
+
name: "append_file",
|
|
179
|
+
capability: { riskLevel: "medium", sideEffect: "workspace" },
|
|
180
|
+
description: "向文件末尾追加内容 (不覆盖原文件)。文件不存在时等同创建。",
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
path: { type: "string", description: "文件路径" },
|
|
185
|
+
content: { type: "string", description: "要追加的内容 (追加在文件末尾)" },
|
|
186
|
+
},
|
|
187
|
+
required: ["path", "content"],
|
|
188
|
+
},
|
|
189
|
+
execute: async (args) => {
|
|
190
|
+
const content = String(args.content ?? "");
|
|
191
|
+
if (content.length > 512 * 1024) return JSON.stringify({ error: `追加内容过大 (>512KB, 当前 ${content.length} 字符)` });
|
|
192
|
+
const p = safePath(rootDir, args.path);
|
|
193
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
194
|
+
return JSON.stringify({ error: `目标是目录: ${args.path}` });
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
198
|
+
// 已有内容且不以换行结尾时补一个换行, 避免追加粘到原末行
|
|
199
|
+
let prefix = "";
|
|
200
|
+
if (fs.existsSync(p)) {
|
|
201
|
+
const old = fs.readFileSync(p, "utf8");
|
|
202
|
+
if (old.length && !old.endsWith("\n")) prefix = "\n";
|
|
203
|
+
}
|
|
204
|
+
fs.appendFileSync(p, prefix + content, "utf8");
|
|
205
|
+
return JSON.stringify({ ok: true, appended: Buffer.byteLength(content) });
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return JSON.stringify({ error: `追加失败: ${e.message}` });
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// 2c. 删除文件 (2026-10-02 基线暴露缺口: 无删除工具, agent 只能放弃或绕道)
|
|
213
|
+
catalog.register({
|
|
214
|
+
name: "delete_file",
|
|
215
|
+
capability: { destructive: true, riskLevel: "high", sideEffect: "workspace" },
|
|
216
|
+
description: "删除指定文件 (仅限工作区内, 不能删目录)。",
|
|
217
|
+
parameters: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: { path: { type: "string", description: "要删除的文件路径" } },
|
|
220
|
+
required: ["path"],
|
|
221
|
+
},
|
|
222
|
+
execute: async (args) => {
|
|
223
|
+
const p = safePath(rootDir, args.path);
|
|
224
|
+
if (!fs.existsSync(p)) return JSON.stringify({ error: `文件不存在: ${args.path}` });
|
|
225
|
+
if (fs.statSync(p).isDirectory()) return JSON.stringify({ error: `目标是目录, 拒绝删除 (只支持文件): ${args.path}` });
|
|
226
|
+
try {
|
|
227
|
+
fs.unlinkSync(p);
|
|
228
|
+
return JSON.stringify({ ok: true, deleted: args.path });
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return JSON.stringify({ error: `删除失败: ${e.message}` });
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 3. 列目录
|
|
236
|
+
catalog.register({
|
|
237
|
+
name: "list_dir",
|
|
238
|
+
capability: { readOnly: true, riskLevel: "low", sideEffect: "none" },
|
|
239
|
+
description: "列出目录内容 (文件名列表)。",
|
|
240
|
+
parameters: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: { path: { type: "string", description: "目录路径, 默认工作目录" } },
|
|
243
|
+
},
|
|
244
|
+
execute: async (args) => {
|
|
245
|
+
const p = safePath(rootDir, args.path || ".");
|
|
246
|
+
if (!fs.existsSync(p) || !fs.statSync(p).isDirectory()) {
|
|
247
|
+
return JSON.stringify({ error: `目录不存在: ${args.path || "."}` });
|
|
248
|
+
}
|
|
249
|
+
const items = fs.readdirSync(p).map((f) => {
|
|
250
|
+
const fp = path.join(p, f);
|
|
251
|
+
const st = fs.statSync(fp);
|
|
252
|
+
return `${st.isDirectory() ? "[D]" : "[F]"} ${f}`;
|
|
253
|
+
});
|
|
254
|
+
return items.join("\n");
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 4. 执行命令 (安全: 限制在允许目录, 超时)
|
|
259
|
+
catalog.register({
|
|
260
|
+
name: "run_command",
|
|
261
|
+
capability: { destructive: true, riskLevel: "high", sideEffect: "system" },
|
|
262
|
+
description: "执行 shell 命令并返回输出。只能在工作目录内执行, 有超时。",
|
|
263
|
+
parameters: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: { command: { type: "string", description: "要执行的命令" } },
|
|
266
|
+
required: ["command"],
|
|
267
|
+
},
|
|
268
|
+
execute: async (args, ctx) => {
|
|
269
|
+
const cmd = String(args.command || "").trim();
|
|
270
|
+
if (!cmd) return JSON.stringify({ error: "空命令" });
|
|
271
|
+
const opts = (ctx && ctx.agent && ctx.agent.config && ctx.agent.config.security) || {};
|
|
272
|
+
const guard = checkCommand(cmd, opts);
|
|
273
|
+
if (!guard.ok) {
|
|
274
|
+
return JSON.stringify({ error: guard.reason + DENY_HINT });
|
|
275
|
+
}
|
|
276
|
+
// 通过 shell seam 调用 (可替换 provider: 本地/沙箱/Docker), 换 provider 即换执行环境
|
|
277
|
+
const shell = (ctx?.agent?.ctx && ctx.agent.ctx.consume("shell")) || defaultShell;
|
|
278
|
+
const t0 = Date.now();
|
|
279
|
+
const r = await shell.exec(cmd, { cwd: rootDir, timeoutMs: opts.command_timeout_ms || 30000 });
|
|
280
|
+
const ms = Date.now() - t0;
|
|
281
|
+
// B1: 工具结果标准化 — 统一元数据头, 模型可判成败 (吸收 codex format_exec_output_for_model)
|
|
282
|
+
const timedOut = !!(r && r.timedOut);
|
|
283
|
+
const head = formatToolResultHeader({ ms, lineCount: timedOut ? 0 : countLines(r.stdout + " " + (r.stderr || "")), timedOut, timedOutMs: opts.command_timeout_ms || 30000, exitCode: r.code });
|
|
284
|
+
if (!r.ok && timedOut) return head + "\n[工具错误] run_command: 超时";
|
|
285
|
+
if (!r.ok) return head + "\n[工具错误] run_command: " + (r.stderr || r.stdout || "");
|
|
286
|
+
const out = r.stdout + (r.stderr ? "\n[stderr] " + r.stderr : "");
|
|
287
|
+
const cleaned = scrubPII(out).cleaned.slice(0, 20000) || "(无输出)";
|
|
288
|
+
return head + "\n" + cleaned;
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// 4.5 code_act (CodeAct 出口): 脚本批量操作, 压 N 轮工具往返 → 1 轮
|
|
293
|
+
catalog.register({
|
|
294
|
+
name: "code_act",
|
|
295
|
+
description: "用 Python/Node 脚本一次性完成多个操作(读文件/处理数据/写结果), 用 print/console.log 输出结果。默认关闭, 需 security.code_act=true。",
|
|
296
|
+
parameters: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
language: { type: "string", enum: ["python", "node"], description: "脚本语言" },
|
|
300
|
+
code: { type: "string", description: "脚本内容" },
|
|
301
|
+
},
|
|
302
|
+
required: ["language", "code"],
|
|
303
|
+
},
|
|
304
|
+
execute: async (args, ctx) => {
|
|
305
|
+
const sec = (ctx && ctx.agent && ctx.agent.config && ctx.agent.config.security) || {};
|
|
306
|
+
if (!sec.allow_all && !sec.code_act) {
|
|
307
|
+
return JSON.stringify({ error: "code_act 未开启: 在 security 设置 code_act=true (或 allow_all=true)" });
|
|
308
|
+
}
|
|
309
|
+
const lang = String(args.language || "").toLowerCase();
|
|
310
|
+
if (!["python", "node"].includes(lang)) return JSON.stringify({ error: "language 仅支持 python/node" });
|
|
311
|
+
const code = String(args.code || "");
|
|
312
|
+
if (!code) return JSON.stringify({ error: "空代码" });
|
|
313
|
+
// code_act 是脚本体: 只做 deny 检查 (硬黑名单 + 用户 deny + 常规高危), 不做前缀白名单 (脚本无"命令前缀")
|
|
314
|
+
const guard = checkCommand(code, { ...sec, allowAll: true });
|
|
315
|
+
if (!guard.ok) return JSON.stringify({ error: guard.reason + DENY_HINT });
|
|
316
|
+
return runCodeAct(rootDir, lang, code, sec.command_timeout_ms);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// 5. 当前时间
|
|
321
|
+
catalog.register({
|
|
322
|
+
name: "get_time",
|
|
323
|
+
capability: { readOnly: true, riskLevel: "low", sideEffect: "none" },
|
|
324
|
+
description: "获取当前日期和时间。",
|
|
325
|
+
parameters: { type: "object", properties: {} },
|
|
326
|
+
execute: async () => new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// 6. 记忆查询 (皮皮虾自己查记忆)
|
|
330
|
+
catalog.register({
|
|
331
|
+
name: "memory_search",
|
|
332
|
+
capability: { readOnly: true, riskLevel: "low", sideEffect: "none" },
|
|
333
|
+
description: "搜索皮皮虾的记忆库, 返回相关事实。",
|
|
334
|
+
parameters: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: { query: { type: "string", description: "要搜的内容" }, limit: { type: "number" } },
|
|
337
|
+
required: ["query"],
|
|
338
|
+
},
|
|
339
|
+
execute: async (args) => {
|
|
340
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
341
|
+
const results = facts.query(args.query, { limit: args.limit || 5 });
|
|
342
|
+
return results.length
|
|
343
|
+
? results.map((r) => `- [${r.score}] ${r.content}`).join("\n")
|
|
344
|
+
: "(无匹配记忆)";
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// 7. 记住新事实
|
|
349
|
+
catalog.register({
|
|
350
|
+
name: "memory_add",
|
|
351
|
+
capability: { riskLevel: "low", sideEffect: "workspace" },
|
|
352
|
+
description: "把一条重要信息写进皮皮虾的长期记忆。",
|
|
353
|
+
parameters: {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: { content: { type: "string", description: "要记住的内容" } },
|
|
356
|
+
required: ["content"],
|
|
357
|
+
},
|
|
358
|
+
execute: async (args) => {
|
|
359
|
+
if (!facts) return JSON.stringify({ error: "记忆未初始化" });
|
|
360
|
+
const f = facts.add(args.content, { source: "agent-self" });
|
|
361
|
+
return JSON.stringify({ ok: true, id: f.id });
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// 8. 读图片 (多模态): 返回 base64 data URL, 供多模态模型视觉理解
|
|
366
|
+
catalog.register({
|
|
367
|
+
name: "read_image",
|
|
368
|
+
capability: { readOnly: true, riskLevel: "low", sideEffect: "none" },
|
|
369
|
+
description: "读取图片文件, 返回 base64 data URL 供多模态模型理解图片内容 (需配置支持视觉的模型, 如 gpt-4o/qwen-vl/glm-4v)。",
|
|
370
|
+
parameters: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: { path: { type: "string", description: "图片文件路径 (相对工作目录)" } },
|
|
373
|
+
required: ["path"],
|
|
374
|
+
},
|
|
375
|
+
execute: async (args) => {
|
|
376
|
+
try {
|
|
377
|
+
return imageFileToDataUrl(rootDir, args.path);
|
|
378
|
+
} catch (e) {
|
|
379
|
+
return JSON.stringify({ error: e.message });
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return catalog;
|
|
298
385
|
}
|