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.
Files changed (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -4,15 +4,13 @@
4
4
  // S 综合/评审 → 决定: 接受最终答案 / 发现分歧 → 重新派发修正 / 升级人工。
5
5
  // 皮皮虾自研实现, 构建在现有 Legion (多进程) + delegate 仲裁之上。
6
6
  // 与 delegate.js 的 arbitrate (一次性聚合) 互补: supervisor 是完整编排循环 (可多轮修正)。
7
+ // ⚠ 接线状态 (2026-09-17 核对): runSupervisor **无内置调用点** —— 尚无 mode/工具把它接成可用入口;
8
+ // spawn_agent 当前走 tools/delegate.js 自带的 review 循环。属"编排器就绪、未接线", 需显式调用才生效。
7
9
 
8
- // 带超时等待 (防子 agent 卡死) — 定时器必须清理
9
- export function withTimeout(p, ms, label) {
10
- let timer;
11
- const timeout = new Promise((_, rej) => {
12
- timer = setTimeout(() => rej(new Error(`${label}超时 (${ms / 1000}s)`)), ms);
13
- });
14
- return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
15
- }
10
+ // 带超时等待 (防子 agent 卡死) — 实现收敛到 utils/async.js (原先本文件与 tools/delegate.js 各写一份)
11
+ // 本文件内部继续使用, 同时保留同名导出, 对外接口不变。
12
+ import { withTimeout } from "../utils/async.js";
13
+ export { withTimeout };
16
14
 
17
15
  export const SUPERVISOR_DEFAULTS = {
18
16
  maxRounds: 3, // 最多几轮修正
@@ -0,0 +1,378 @@
1
+ // src/permissions/index.js — 三合一权限引擎
2
+ // 吸收: codex(AskForApproval + SandboxPolicy) + opencode(通配符规则链 last-match-wins) + CASDK(canUseTool 回调)
3
+ // 纯 Node、零运行时依赖、ESM、简体中文注释。
4
+
5
+ import path from "node:path";
6
+
7
+ // ---- codex: 审批模式四档 ----
8
+ export const AskForApproval = {
9
+ UNLESS_TRUSTED: "unless-trusted", // 仅非白名单工具需审批
10
+ ON_FAILURE: "on-failure", // 默认放行, 失败时才问 (本引擎无失败回灌, 视作放行)
11
+ ON_REQUEST: "on-request", // 仅工具自带 requires_approval 语义时审批
12
+ NEVER: "never", // 全自动, ask 降级为 deny (codex: never 不询问)
13
+ };
14
+
15
+ // ---- codex: 沙箱策略三档 + 网络开关 ----
16
+ export const SandboxPolicy = {
17
+ READ_ONLY: "read-only", // 禁止一切写/执行类工具 (除非 allow 规则命中)
18
+ WORKSPACE_WRITE: "workspace-write", // 仅允许工作区内写, 路径越界拒绝
19
+ DANGER_FULL_ACCESS: "danger-full-access", // 完全放开
20
+ };
21
+
22
+ // ---- 可信工具白名单 (unless-trusted 模式自动放行) ----
23
+ export const TRUSTED_TOOLS = [
24
+ "get_time", "list_dir", "read_file", "read", "memory_search",
25
+ "repo_map", "goal_board", "status", "list_files", "glob", "grep", "search_files",
26
+ ];
27
+
28
+ // 写/执行类工具 (只读沙箱下需升级为 ask, 工作区沙箱下需校验路径)
29
+ const WRITE_EXEC_TOOLS = new Set([
30
+ "run_command", "shell", "exec", "bash", "apply_patch", "edit_file",
31
+ "write_file", "create_file", "delete_file", "move_file", "rm", "write", "edit",
32
+ ]);
33
+
34
+ // 网络类工具 (networkAccess=false 时升级 ask)
35
+ const HTTP_TOOLS = new Set([
36
+ "http_request", "fetch_page", "fetch", "web_fetch", "curl", "request",
37
+ ]);
38
+
39
+ // ON_REQUEST 模式下默认需要审批的工具 (其余按 args.requires_approval 决定)
40
+ const REQUIRES_APPROVAL_TOOLS = new Set([
41
+ "run_command", "shell", "exec", "bash", "apply_patch", "delete_file", "rm",
42
+ ]);
43
+
44
+ // ---- 通配符匹配 (opencode 语义) ----
45
+ // '*' -> 匹配任意
46
+ // 'git *' -> 前缀匹配 (git / git push ...)
47
+ // 'git push' -> 精确或前缀(后面带空格) 匹配
48
+ function wildcardMatch(pattern, str) {
49
+ str = String(str || "").toLowerCase();
50
+ if (pattern === "*") return true;
51
+ const pat = String(pattern || "").toLowerCase();
52
+ if (pat.endsWith(" *")) {
53
+ const pre = pat.slice(0, -2);
54
+ return str === pre || str.startsWith(pre + " ");
55
+ }
56
+ // 无通配: 精确或前缀(带空格)
57
+ return str === pat || str.startsWith(pat + " ");
58
+ }
59
+
60
+ // 规则是否命中: 同时校验工具名, 以及命令型工具的 command 字符串
61
+ function ruleMatches(rule, toolName, args) {
62
+ const candidates = [toolName];
63
+ if (typeof args?.command === "string") candidates.push(args.command);
64
+ return candidates.some((c) => wildcardMatch(rule.pattern, c));
65
+ }
66
+
67
+ // ---- 路径规范化 (Windows 大小写不敏感 + 分隔符统一) ----
68
+ function toCompare(p) {
69
+ return path.resolve(String(p)).toLowerCase().replace(/\\/g, "/");
70
+ }
71
+ function isWithinRoot(p, root) {
72
+ const a = toCompare(p);
73
+ const b = toCompare(root);
74
+ if (a === b) return true;
75
+ const sep = b.endsWith("/") ? b : b + "/";
76
+ return a.startsWith(sep);
77
+ }
78
+
79
+ // 从 args 收集需要校验的候选路径
80
+ function collectPaths(args) {
81
+ const out = [];
82
+ for (const key of ["path", "file_path", "filePath", "cwd", "dir", "destination", "dest"]) {
83
+ if (typeof args?.[key] === "string" && args[key]) out.push(args[key]);
84
+ }
85
+ if (typeof args?.command === "string") {
86
+ // 先剔除 URL (scheme://...): 否则 "curl http://x" 里的 "p:/" 会被盘符正则误判为
87
+ // Windows 绝对路径 "p://x", 进而触发"路径越界"误拒 (任何带 URL 的命令都跑不了)。
88
+ const cmd = args.command.replace(/[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"']*/g, " ");
89
+ const m = cmd.match(/(?:[A-Za-z]:[\\/][^\s"']+|\/[^\s"']+)/g);
90
+ if (m) out.push(...m);
91
+ }
92
+ return out;
93
+ }
94
+ function findEscape(args, root) {
95
+ for (const p of collectPaths(args)) {
96
+ if (path.isAbsolute(p) && !isWithinRoot(p, root)) return p;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ // ---- 兼容 CASDK canUseTool 回调的返回 ----
102
+ // 允许 { behavior:'allow'|'deny' } 或字符串 'allow'/'deny'
103
+ export function parseDecision(result) {
104
+ if (!result) return null;
105
+ if (typeof result === "string") return result;
106
+ if (typeof result.behavior === "string") return result.behavior;
107
+ if (typeof result.decision === "string") return result.decision;
108
+ return null;
109
+ }
110
+
111
+ // ---- DSH (DeepSeek Harness) 对齐: 具名权限预设 ----
112
+ // 沙箱模式 × 审批策略捆绑成单一「权限」选择器, 客户端一个开关完成授权 (DSH PresetSpec 语义)。
113
+ // danger-full-access 捆绑 never (全自动): 按官方事故复盘, 只应配给一次性、可丢弃的沙箱环境。
114
+ export const PERMISSION_PRESETS = {
115
+ "workspace-write": {
116
+ sandbox: SandboxPolicy.WORKSPACE_WRITE,
117
+ approval: AskForApproval.ON_REQUEST,
118
+ desc: "可写工作区 + 路径越界拒绝, 高危操作问人 (默认)",
119
+ },
120
+ "read-only": {
121
+ sandbox: SandboxPolicy.READ_ONLY,
122
+ approval: AskForApproval.ON_REQUEST,
123
+ desc: "只读巡检, 写/执行类工具一律审批",
124
+ },
125
+ "danger-full-access": {
126
+ sandbox: SandboxPolicy.DANGER_FULL_ACCESS,
127
+ approval: AskForApproval.NEVER,
128
+ desc: "完全放开不询问 — 仅用于可丢弃环境 (沙箱容器/临时机器)",
129
+ },
130
+ };
131
+
132
+ // 应用具名预设: 校验 → 热更新引擎两个 knob, 返回生效状态 (未知预设抛错, fail closed)
133
+ export function applyPreset(engine, presetName) {
134
+ const p = PERMISSION_PRESETS[presetName];
135
+ if (!p) {
136
+ throw new Error(`未知权限预设: ${presetName} (可用: ${Object.keys(PERMISSION_PRESETS).join(", ")})`);
137
+ }
138
+ engine.sandbox = p.sandbox;
139
+ engine.approvalMode = p.approval;
140
+ return { preset: presetName, sandbox: p.sandbox, approval: p.approval, desc: p.desc };
141
+ }
142
+
143
+ // 折叠当前 knob 组合 → 预设名; 组合不匹配任何预设时返回 "custom" (DSH: custom 保留给派生状态)
144
+ export function currentPreset(engine) {
145
+ const cfg = engine.config;
146
+ for (const [name, p] of Object.entries(PERMISSION_PRESETS)) {
147
+ if (p.sandbox === cfg.sandbox && p.approval === cfg.approvalMode) return name;
148
+ }
149
+ return "custom";
150
+ }
151
+
152
+ // ---- 工厂: 创建权限引擎 ----
153
+ export function createPermissionEngine({
154
+ approvalMode = AskForApproval.ON_REQUEST,
155
+ sandbox = SandboxPolicy.WORKSPACE_WRITE,
156
+ workspaceRoot = process.cwd(),
157
+ networkAccess = false,
158
+ rules = [],
159
+ canUseTool = null,
160
+ onAsk = null, // 可选 ask 应答者 (DSH 应答者链): 提供时 ask 自动询问; 异常/空值 → deny (fail closed)
161
+ // ZCode 工具能力门 (2026-10-02 吸收): getCapability 工具→能力元数据; capabilityGate 总开关
162
+ getCapability = null,
163
+ capabilityGate = false,
164
+ planEnabled = false, // plan 模式: 只读非破坏工具直通, 其余拒绝
165
+ autoApproveHighRisk = false, // high 风险免确认 (critical 永不免)
166
+ } = {}) {
167
+ const _rules = Array.isArray(rules) ? rules.map((r) => ({ pattern: r.pattern, action: r.action })) : [];
168
+ // DSH 一次性提权: 人工批准的显式沙箱模式, 胜过会话策略, 单次 check 消费后自动还原
169
+ let _escalation = null;
170
+ // ZCode 能力门状态 (可运行时热切换: plan 模式进出 / 配置热更新)
171
+ let capGetter = getCapability;
172
+ let capGate = capabilityGate;
173
+ let planOn = planEnabled;
174
+ let approveHigh = autoApproveHighRisk;
175
+
176
+ function addRule(pattern, action) {
177
+ if (!["allow", "deny", "ask"].includes(action)) {
178
+ throw new Error("addRule action 必须为 allow|deny|ask");
179
+ }
180
+ _rules.push({ pattern, action });
181
+ return _rules.length;
182
+ }
183
+
184
+ // 决策主流程
185
+ async function check(toolName, args = {}, ctx = {}) {
186
+ args = args || {};
187
+
188
+ // (0) DSH 一次性提权: 人工已批准的显式模式胜过会话沙箱策略, 本次调用后自动还原。
189
+ // 提权只放宽沙箱, 不改审批策略 — ask 类审批照常走 (审批是审批, 沙箱是沙箱)。
190
+ let effectiveSandbox = sandbox;
191
+ let escalated = false;
192
+ if (_escalation) {
193
+ effectiveSandbox = _escalation.mode;
194
+ escalated = true;
195
+ if (_escalation.oneShot) _escalation = null;
196
+ }
197
+
198
+ // (a) canUseTool 回调优先 (CASDK)
199
+ if (typeof canUseTool === "function") {
200
+ let res = null;
201
+ try {
202
+ res = await canUseTool(toolName, args, ctx);
203
+ } catch {
204
+ res = null;
205
+ }
206
+ const beh = parseDecision(res);
207
+ if (beh === "allow" || beh === "deny") {
208
+ return {
209
+ decision: beh,
210
+ rule: undefined,
211
+ reason: (res && (res.message || res.reason)) || "canUseTool 回调决策",
212
+ };
213
+ }
214
+ }
215
+
216
+ // (b) 规则链 last-match-wins + deny-wins
217
+ let matched = null;
218
+ for (const r of _rules) {
219
+ if (ruleMatches(r, toolName, args)) matched = r;
220
+ }
221
+ if (matched && matched.action === "deny") {
222
+ return { decision: "deny", rule: matched, reason: `命中 deny 规则: ${matched.pattern}` };
223
+ }
224
+ const allowRuleHit = !!(matched && matched.action === "allow");
225
+
226
+ // (b2) ZCode 工具能力门: 声明式元数据裁定 (2026-10-02 吸收 ZCode PermissionService)
227
+ // - alwaysAsk 压过一切放行分支 (yolo/白名单直通都要问)
228
+ // - riskLevel critical → 必问; high → 默认问 (autoApproveHighRisk 可放)
229
+ // - plan 模式: 只允许 readOnly 非破坏工具 (ZCode mode.plan.readOnly)
230
+ // - capabilityGate=false 时本门完全关闭 (向后兼容)
231
+ const cap = capGate && typeof capGetter === "function"
232
+ ? capGetter(toolName, args)
233
+ : null;
234
+ if (cap) {
235
+ if (planOn && !(cap.readOnly && !cap.destructive)) {
236
+ return { decision: "deny", rule: matched, reason: "plan 模式: 只允许只读非破坏工具" };
237
+ }
238
+ const askByCap = cap.alwaysAsk ||
239
+ cap.riskLevel === "critical" ||
240
+ (cap.riskLevel === "high" && !approveHigh);
241
+ if (askByCap) {
242
+ if (approvalMode === AskForApproval.NEVER) {
243
+ return { decision: "deny", rule: matched, reason: "高风险能力 + never 模式: 降级拒绝 (高危不可静默放行)" };
244
+ }
245
+ if (typeof onAsk === "function") {
246
+ const v = await onAsk(toolName, args, `高风险能力 (riskLevel=${cap.riskLevel || "high"}${cap.alwaysAsk ? "/alwaysAsk" : ""}) 需人工确认`);
247
+ const d = typeof v === "string" ? v : v?.decision;
248
+ if (d === "allow") return { decision: "allow", rule: matched, reason: "能力门 ask → 应答者放行" };
249
+ return { decision: "deny", rule: matched, reason: "能力门 ask → 应答者拒绝" };
250
+ }
251
+ return { decision: "ask", rule: matched, reason: `高风险能力 (riskLevel=${cap.riskLevel || "high"}${cap.alwaysAsk ? "/alwaysAsk" : ""}) 需人工确认` };
252
+ }
253
+ }
254
+
255
+ // (c) 沙箱检查
256
+ let sandboxAsk = false;
257
+ let sandboxDeny = false;
258
+ let reason = "";
259
+
260
+ if (effectiveSandbox === SandboxPolicy.READ_ONLY) {
261
+ if (WRITE_EXEC_TOOLS.has(toolName) && !allowRuleHit) {
262
+ sandboxAsk = true;
263
+ reason = "只读沙箱: 写/执行类工具需审批" + (escalated ? " [本次已提权]" : "");
264
+ }
265
+ } else if (effectiveSandbox === SandboxPolicy.WORKSPACE_WRITE) {
266
+ const escape = findEscape(args, workspaceRoot);
267
+ if (escape) {
268
+ sandboxDeny = true;
269
+ reason = `工作区写沙箱: 路径越界 ${escape}`;
270
+ }
271
+ }
272
+
273
+ // 网络开关
274
+ if (!networkAccess && HTTP_TOOLS.has(toolName) && !allowRuleHit) {
275
+ sandboxAsk = true;
276
+ if (!reason) reason = "禁止网络访问: 网络类工具需审批";
277
+ }
278
+
279
+ if (sandboxDeny) {
280
+ return { decision: "deny", rule: matched, reason };
281
+ }
282
+
283
+ // (d) DSH 应答者链: ask 交给 onAsk 应答者; 异常/空值/未知决策一律 deny (fail closed —
284
+ // DSH 语义: 缺失或抛异常的应答者产生 unavailable, 调用方对 unavailable 一律拒绝)。
285
+ // NEVER 模式不进应答者链 — never 在分发之前强制执行 (DSH: prepend 应答者也绕不过)。
286
+ const needAsk = approvalMode !== AskForApproval.NEVER && (
287
+ sandboxAsk || (matched && matched.action === "ask") ||
288
+ (approvalMode === AskForApproval.UNLESS_TRUSTED && !TRUSTED_TOOLS.includes(toolName)) ||
289
+ (approvalMode === AskForApproval.ON_REQUEST &&
290
+ (args.requires_approval === true || REQUIRES_APPROVAL_TOOLS.has(toolName))));
291
+ if (needAsk && typeof onAsk === "function") {
292
+ const askReason = sandboxAsk ? reason
293
+ : matched ? `命中 ask 规则: ${matched.pattern}`
294
+ : approvalMode === AskForApproval.UNLESS_TRUSTED ? "unless-trusted: 非白名单工具需审批"
295
+ : "on-request: 工具需审批";
296
+ try {
297
+ const verdict = await onAsk(toolName, args, askReason);
298
+ const decision = typeof verdict === "string" ? verdict : verdict?.decision;
299
+ if (decision === "allow") return { decision: "allow", rule: matched, reason: `${askReason} → 应答者放行` };
300
+ return { decision: "deny", rule: matched, reason: `${askReason} → 应答者拒绝` };
301
+ } catch (e) {
302
+ return { decision: "deny", rule: matched, reason: `${askReason} → 应答者异常, fail closed (${e.message})` };
303
+ }
304
+ }
305
+
306
+ // (e) never 模式: ask 降级为 deny
307
+ if (sandboxAsk && approvalMode === AskForApproval.NEVER) {
308
+ return { decision: "deny", rule: matched, reason: `${reason} (never 模式降级拒绝)` };
309
+ }
310
+ if (sandboxAsk) {
311
+ return { decision: "ask", rule: matched, reason };
312
+ }
313
+
314
+ // (f) 规则命中后的最终映射
315
+ if (matched && matched.action === "allow") {
316
+ return { decision: "allow", rule: matched, reason: `命中 allow 规则: ${matched.pattern}` };
317
+ }
318
+ if (matched && matched.action === "ask") {
319
+ if (approvalMode === AskForApproval.NEVER) {
320
+ return { decision: "deny", rule: matched, reason: "never 模式降级拒绝" };
321
+ }
322
+ return { decision: "ask", rule: matched, reason: `命中 ask 规则: ${matched.pattern}` };
323
+ }
324
+
325
+ // 未命中规则: approvalMode 映射
326
+ if (approvalMode === AskForApproval.UNLESS_TRUSTED) {
327
+ return TRUSTED_TOOLS.includes(toolName)
328
+ ? { decision: "allow", reason: "白名单工具自动放行" }
329
+ : { decision: "ask", reason: "unless-trusted: 非白名单工具需审批" };
330
+ }
331
+ if (approvalMode === AskForApproval.ON_REQUEST) {
332
+ const need = args.requires_approval === true || REQUIRES_APPROVAL_TOOLS.has(toolName);
333
+ return need
334
+ ? { decision: "ask", reason: "on-request: 工具需审批" }
335
+ : { decision: "allow", reason: "on-request: 无需审批" };
336
+ }
337
+ // ON_FAILURE / NEVER 默认放行
338
+ return { decision: "allow", reason: "默认放行 (on-failure/never)" };
339
+ }
340
+
341
+ // DSH 一次性提权: 人工批准后调用, 下一次 check 以显式沙箱模式执行 (oneShot=true 消费后自动还原)
342
+ function requestEscalation(mode, { oneShot = true } = {}) {
343
+ if (!Object.values(SandboxPolicy).includes(mode)) {
344
+ throw new Error(`无效沙箱模式: ${mode} (可用: ${Object.values(SandboxPolicy).join(", ")})`);
345
+ }
346
+ _escalation = { mode, oneShot };
347
+ return { escalated: true, mode, oneShot };
348
+ }
349
+
350
+ return {
351
+ addRule,
352
+ check,
353
+ requestEscalation, // DSH 一次性提权
354
+ // 暴露配置与内部状态, 便于调试/集成
355
+ get config() {
356
+ return { approvalMode, sandbox, workspaceRoot, networkAccess };
357
+ },
358
+ get rules() {
359
+ return _rules.slice();
360
+ },
361
+ // v3.0 集成: 运行时热更新 (HTTP POST /api/permissions)
362
+ get approvalMode() { return approvalMode; },
363
+ set approvalMode(v) { approvalMode = v; },
364
+ get sandbox() { return sandbox; },
365
+ set sandbox(v) { sandbox = v; },
366
+ get networkAccess() { return networkAccess; },
367
+ set networkAccess(v) { networkAccess = !!v; },
368
+ // ZCode 能力门运行时热切换
369
+ get getCapability() { return capGetter; },
370
+ set getCapability(v) { capGetter = v; },
371
+ get capabilityGate() { return capGate; },
372
+ set capabilityGate(v) { capGate = !!v; },
373
+ get planEnabled() { return planOn; },
374
+ set planEnabled(v) { planOn = !!v; },
375
+ get autoApproveHighRisk() { return approveHigh; },
376
+ set autoApproveHighRisk(v) { approveHigh = !!v; },
377
+ };
378
+ }
@@ -1,29 +1,28 @@
1
- // src/persona/index.js - 人格系统
2
- // 从 identity.md + ishiki.md 组装人格 prompt (参考 openhanako)
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { readText } from "../utils/store.js";
6
-
7
- export class Persona {
8
- constructor(rootDir) {
9
- this.configDir = path.join(rootDir, "config");
10
- this.identity = readText(path.join(this.configDir, "identity.md"), "皮皮虾");
11
- this.ishiki = readText(path.join(this.configDir, "ishiki.md"), "- 直接、务实");
12
- }
13
-
14
- // 组装 system prompt
15
- systemPrompt(userName = "兄弟") {
16
- return `${this.identity}
17
-
18
- ## 人格定义
19
- ${this.ishiki}
20
-
21
- ## 你面对的用户
22
- ${userName}`;
23
- }
24
-
25
- reload() {
26
- this.identity = readText(path.join(this.configDir, "identity.md"), "皮皮虾");
27
- this.ishiki = readText(path.join(this.configDir, "ishiki.md"), "- 直接、务实");
28
- }
29
- }
1
+ // src/persona/index.js - 人格系统
2
+ // 从 identity.md + ishiki.md 组装人格 prompt (参考 openhanako)
3
+ import path from "node:path";
4
+ import { readText } from "../utils/store.js";
5
+
6
+ export class Persona {
7
+ constructor(rootDir) {
8
+ this.configDir = path.join(rootDir, "config");
9
+ this.identity = readText(path.join(this.configDir, "identity.md"), "皮皮虾");
10
+ this.ishiki = readText(path.join(this.configDir, "ishiki.md"), "- 直接、务实");
11
+ }
12
+
13
+ // 组装 system prompt
14
+ systemPrompt(userName = "兄弟") {
15
+ return `${this.identity}
16
+
17
+ ## 人格定义
18
+ ${this.ishiki}
19
+
20
+ ## 你面对的用户
21
+ ${userName}`;
22
+ }
23
+
24
+ reload() {
25
+ this.identity = readText(path.join(this.configDir, "identity.md"), "皮皮虾");
26
+ this.ishiki = readText(path.join(this.configDir, "ishiki.md"), "- 直接、务实");
27
+ }
28
+ }