principles-disciple 1.7.4 → 1.7.6

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 (78) hide show
  1. package/dist/commands/evolution-status.js +32 -44
  2. package/dist/commands/focus.js +30 -155
  3. package/dist/config/defaults/runtime.d.ts +40 -0
  4. package/dist/config/defaults/runtime.js +44 -0
  5. package/dist/config/errors.d.ts +84 -0
  6. package/dist/config/errors.js +94 -0
  7. package/dist/config/index.d.ts +7 -0
  8. package/dist/config/index.js +7 -0
  9. package/dist/constants/diagnostician.d.ts +12 -0
  10. package/dist/constants/diagnostician.js +56 -0
  11. package/dist/constants/tools.d.ts +2 -2
  12. package/dist/constants/tools.js +1 -1
  13. package/dist/core/config.d.ts +12 -0
  14. package/dist/core/config.js +7 -0
  15. package/dist/core/control-ui-db.d.ts +27 -0
  16. package/dist/core/control-ui-db.js +18 -0
  17. package/dist/core/evolution-engine.js +1 -1
  18. package/dist/core/focus-history.d.ts +92 -0
  19. package/dist/core/focus-history.js +490 -0
  20. package/dist/core/init.js +2 -2
  21. package/dist/core/path-resolver.js +2 -1
  22. package/dist/core/profile.js +1 -1
  23. package/dist/core/trajectory.d.ts +60 -0
  24. package/dist/core/trajectory.js +72 -2
  25. package/dist/hooks/bash-risk.d.ts +57 -0
  26. package/dist/hooks/bash-risk.js +137 -0
  27. package/dist/hooks/edit-verification.d.ts +62 -0
  28. package/dist/hooks/edit-verification.js +256 -0
  29. package/dist/hooks/gate-block-helper.d.ts +44 -0
  30. package/dist/hooks/gate-block-helper.js +119 -0
  31. package/dist/hooks/gate.d.ts +18 -0
  32. package/dist/hooks/gate.js +63 -752
  33. package/dist/hooks/gfi-gate.d.ts +40 -0
  34. package/dist/hooks/gfi-gate.js +112 -0
  35. package/dist/hooks/progressive-trust-gate.d.ts +79 -0
  36. package/dist/hooks/progressive-trust-gate.js +242 -0
  37. package/dist/hooks/prompt.js +83 -28
  38. package/dist/hooks/subagent.js +1 -2
  39. package/dist/hooks/thinking-checkpoint.d.ts +37 -0
  40. package/dist/hooks/thinking-checkpoint.js +51 -0
  41. package/dist/http/principles-console-route.d.ts +7 -0
  42. package/dist/http/principles-console-route.js +255 -3
  43. package/dist/index.js +0 -2
  44. package/dist/service/central-database.d.ts +104 -0
  45. package/dist/service/central-database.js +649 -0
  46. package/dist/service/control-ui-query-service.d.ts +1 -1
  47. package/dist/service/control-ui-query-service.js +3 -3
  48. package/dist/service/evolution-query-service.d.ts +1 -1
  49. package/dist/service/evolution-query-service.js +5 -5
  50. package/dist/service/evolution-worker.d.ts +10 -0
  51. package/dist/service/evolution-worker.js +10 -6
  52. package/dist/service/phase3-input-filter.d.ts +57 -0
  53. package/dist/service/phase3-input-filter.js +93 -3
  54. package/dist/service/runtime-summary-service.d.ts +34 -0
  55. package/dist/service/runtime-summary-service.js +93 -1
  56. package/dist/tools/deep-reflect.js +1 -2
  57. package/dist/types/event-types.d.ts +2 -0
  58. package/dist/types/runtime-summary.d.ts +54 -0
  59. package/dist/types/runtime-summary.js +1 -0
  60. package/dist/utils/subagent-probe.d.ts +11 -0
  61. package/dist/utils/subagent-probe.js +46 -1
  62. package/openclaw.plugin.json +1 -1
  63. package/package.json +2 -1
  64. package/templates/langs/en/core/AGENTS.md +1 -1
  65. package/templates/langs/en/core/TOOLS.md +1 -1
  66. package/templates/langs/zh/core/AGENTS.md +1 -1
  67. package/templates/langs/zh/core/TOOLS.md +1 -1
  68. package/{agents/auditor.md → templates/langs/zh/skills/pd-auditor/SKILL.md} +3 -3
  69. package/{agents/diagnostician.md → templates/langs/zh/skills/pd-diagnostician/SKILL.md} +39 -29
  70. package/{agents/explorer.md → templates/langs/zh/skills/pd-explorer/SKILL.md} +3 -3
  71. package/{agents/implementer.md → templates/langs/zh/skills/pd-implementer/SKILL.md} +3 -3
  72. package/{agents/planner.md → templates/langs/zh/skills/pd-planner/SKILL.md} +3 -3
  73. package/{agents/reporter.md → templates/langs/zh/skills/pd-reporter/SKILL.md} +3 -3
  74. package/{agents/reviewer.md → templates/langs/zh/skills/pd-reviewer/SKILL.md} +3 -3
  75. package/dist/core/agent-loader.d.ts +0 -44
  76. package/dist/core/agent-loader.js +0 -147
  77. package/dist/tools/agent-spawn.d.ts +0 -54
  78. package/dist/tools/agent-spawn.js +0 -456
@@ -1,111 +1,38 @@
1
+ /**
2
+ * Security Gate Hook - Orchestration Layer
3
+ *
4
+ * HOOK CHAIN PRIORITY (short-circuits on first block):
5
+ *
6
+ * 1. Early Return: Skip if not write/bash/agent tool or no workspace
7
+ * 2. Thinking OS Checkpoint (P-10): Deep reflection enforcement
8
+ * 3. GFI Gate: Fatigue index-based blocking
9
+ * 4. Bash Mutation Detection: Heuristic for bash file modifications
10
+ * 5. Progressive Trust Gate: Stage 1-4 access control
11
+ * 6. Edit Verification (P-03): Exact/fuzzy match for edit operations
12
+ *
13
+ * IMPORTANT: This is the SINGLE AUTHORITATIVE orchestration path.
14
+ * All policy modules (gfi-gate, progressive-trust-gate) use the shared
15
+ * `recordGateBlockAndReturn` helper to ensure consistent block persistence.
16
+ *
17
+ * Zero-width character detection is handled in bash-risk.ts.
18
+ */
1
19
  import * as fs from 'fs';
2
- import * as path from 'path';
3
20
  import { isRisky, normalizePath, planStatus as getPlanStatus } from '../utils/io.js';
4
- import { matchesAnyPattern } from '../utils/glob-match.js';
5
21
  import { normalizeProfile } from '../core/profile.js';
6
- import { trackBlock, hasRecentThinking, getSession } from '../core/session-tracker.js';
7
- import { assessRiskLevel, estimateLineChanges, getTargetFileLineCount, calculatePercentageThreshold } from '../core/risk-calculator.js';
22
+ import { estimateLineChanges } from '../core/risk-calculator.js';
8
23
  import { WorkspaceContext } from '../core/workspace-context.js';
9
- import { checkEvolutionGate } from '../core/evolution-engine.js';
10
- import { EventLogService } from '../core/event-log.js';
11
- import { AGENT_TOOLS, BASH_TOOLS_SET, HIGH_RISK_TOOLS, LOW_RISK_WRITE_TOOLS, WRITE_TOOLS, } from '../constants/tools.js';
12
- const TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS = 250;
13
- const TRAJECTORY_GATE_BLOCK_MAX_RETRIES = 3;
14
- // ═══ GFI Gate Tool Tiers ═══
15
- // TIER 0: 只读工具 - 永不拦截
16
- // TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
17
- // 注意:pd_run_worker、sessions_spawn、task 是 Agent 派生工具,不应被 GFI Gate 拦截
18
- // 它们属于 AGENT_TOOLS,在早期过滤后直接放行
19
- // TIER 2: 高风险操作 - GFI >= high_risk_block 时拦截
20
- // TIER 3: Bash 命令 - 根据内容判断
21
- /**
22
- * 分析 Bash 命令风险等级
23
- */
24
- function analyzeBashCommand(command, safePatterns, dangerousPatterns, logger) {
25
- let normalizedCmd = command.trim().toLowerCase();
26
- // P2 fix: Unicode de-obfuscation — convert Cyrillic/Unicode lookalikes to ASCII equivalents
27
- // Common Cyrillic lookalikes that could bypass detection: аеорсух (Cyrillic) → aeopcyx (Latin)
28
- const CYRILLIC_TO_LATIN = {
29
- 'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'у': 'y', 'х': 'x',
30
- 'А': 'a', 'Е': 'e', 'О': 'o', 'Р': 'p', 'С': 'c', 'У': 'y', 'Х': 'x',
31
- // Additional confusable chars
32
- 'і': 'i', 'ј': 'j', 'ѕ': 's', 'ԁ': 'd', 'ɡ': 'g', 'һ': 'h', 'ⅰ': 'i',
33
- 'ƚ': 'l', 'м': 'm', 'п': 'n', 'ѵ': 'v', 'ѡ': 'w', 'ᴦ': 'r', 'ꜱ': 's',
34
- };
35
- normalizedCmd = normalizedCmd.replace(/[а-яА-Яіјѕԁɡһⅰƚмпеꜱѵѡᴦꜱ]/g, m => CYRILLIC_TO_LATIN[m] ?? m);
36
- // P2 fix: Tokenize command chain before pattern matching to catch `cmd1 && cmd2` bypasses
37
- // Only split on statement separators (; && ||), NOT on pipe (|) which is part of the command
38
- const tokens = normalizedCmd
39
- .split(/\s*(?:;|&&|\|\|)\s*/)
40
- .map(t => t.trim())
41
- .filter(t => t.length > 0);
42
- // If no tokens (e.g., pure pipe-only), use the original
43
- const segments = tokens.length > 0 ? tokens : [normalizedCmd];
44
- // P2 fix: Also strip outer $() and backticks from each segment
45
- const cleanSegments = segments.map(seg => {
46
- let s = seg;
47
- // Strip leading $() or ${} or backtick-wrapped commands
48
- s = s.replace(/^\$\([^)]+\)$/, '').replace(/^\$\{[^}]+\}$/, '').replace(/^`([^`]+)`$/, '$1');
49
- return s.trim();
50
- }).filter(s => s.length > 0);
51
- // 1. Check dangerous patterns against each segment
52
- for (const seg of cleanSegments) {
53
- for (const pattern of dangerousPatterns) {
54
- try {
55
- if (new RegExp(pattern, 'i').test(seg)) {
56
- return 'dangerous';
57
- }
58
- }
59
- catch (error) {
60
- logger?.warn?.(`[PD_GATE] Invalid dangerous bash regex "${pattern}": ${String(error)}. Failing closed.`);
61
- return 'dangerous';
62
- // Fail-closed: 无效的危险模式正则视为匹配危险命令
63
- }
64
- }
65
- }
66
- // 2. Check safe patterns (only if ALL segments are safe)
67
- for (const seg of cleanSegments) {
68
- let isSafe = false;
69
- for (const pattern of safePatterns) {
70
- try {
71
- if (new RegExp(pattern, 'i').test(seg)) {
72
- isSafe = true;
73
- break;
74
- }
75
- }
76
- catch (error) {
77
- logger?.warn?.(`[PD_GATE] Invalid safe bash regex "${pattern}": ${String(error)}. Ignoring safe override.`);
78
- }
79
- }
80
- if (!isSafe) {
81
- // Not all segments are safe → treat as normal
82
- return 'normal';
83
- }
84
- }
85
- // All segments are safe
86
- return 'safe';
87
- }
88
- /**
89
- * 计算动态 GFI 阈值
90
- */
91
- function calculateDynamicThreshold(baseThreshold, trustStage, lineChanges, config) {
92
- // 1. Trust Stage 乘数
93
- const stageMultiplier = config.trust_stage_multipliers[trustStage.toString()] || 1.0;
94
- let threshold = baseThreshold * stageMultiplier;
95
- // 2. 大规模修改降低阈值
96
- if (lineChanges > config.large_change_lines) {
97
- const ratio = Math.min(lineChanges / 200, 0.5); // 最多降低 50%
98
- threshold = threshold * (1 - ratio);
99
- }
100
- return Math.round(Math.max(threshold, 0));
101
- }
24
+ import { checkThinkingCheckpoint } from './thinking-checkpoint.js';
25
+ import { handleEditVerification } from './edit-verification.js';
26
+ import { checkGfiGate } from './gfi-gate.js';
27
+ import { checkProgressiveTrustGate } from './progressive-trust-gate.js';
28
+ import { recordGateBlockAndReturn } from './gate-block-helper.js';
29
+ import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS, } from '../constants/tools.js';
102
30
  export function handleBeforeToolCall(event, ctx) {
103
31
  const logger = ctx.logger || console;
104
32
  // 1. Identify tool type
105
33
  const isBash = BASH_TOOLS_SET.has(event.toolName);
106
34
  const isWriteTool = WRITE_TOOLS.has(event.toolName);
107
35
  const isAgentTool = AGENT_TOOLS.has(event.toolName);
108
- // Profile loaded first for config-driven behavior (see below)
109
36
  if (!ctx.workspaceDir || (!isWriteTool && !isBash && !isAgentTool)) {
110
37
  return;
111
38
  }
@@ -134,7 +61,7 @@ export function handleBeforeToolCall(event, ctx) {
134
61
  thinking_checkpoint: {
135
62
  enabled: false, // Default OFF
136
63
  window_ms: 5 * 60 * 1000,
137
- high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_run_worker'],
64
+ high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'],
138
65
  }
139
66
  };
140
67
  if (fs.existsSync(profilePath)) {
@@ -146,171 +73,23 @@ export function handleBeforeToolCall(event, ctx) {
146
73
  logger?.error?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
147
74
  }
148
75
  }
149
- // ═══ THINKING OS CHECKPOINT (P-10) — Config-gated ═══
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+ // POLICY STEP 1: Thinking OS Checkpoint (P-10)
78
+ // ─────────────────────────────────────────────────────────────────────────────
150
79
  // Only enforced when thinking_checkpoint.enabled = true in PROFILE.json
151
- const isHighRisk = profile.thinking_checkpoint?.high_risk_tools?.includes(event.toolName) ?? false;
152
- if (profile.thinking_checkpoint?.enabled && isHighRisk && ctx.sessionId) {
153
- const windowMs = profile.thinking_checkpoint.window_ms ?? 5 * 60 * 1000;
154
- const hasThinking = hasRecentThinking(ctx.sessionId, windowMs);
155
- if (!hasThinking) {
156
- logger?.info?.(`[PD:THINKING_GATE] High-risk tool "${event.toolName}" called without recent deep thinking`);
157
- return {
158
- block: true,
159
- blockReason: `[Thinking OS Checkpoint] 高风险操作 "${event.toolName}" 需要先进行深度思考。\n\n请先使用 deep_reflect 工具分析当前情况,然后再尝试此操作。\n\n这是强制性检查点,目的是确保决策质量。\n\n提示:调用 deep_reflect 后,${Math.round(windowMs / 60000)}分钟内的操作将自动放行。\n\n可在PROFILE.json中设置 thinking_checkpoint.enabled: false 来禁用此检查。`,
160
- };
161
- }
80
+ const thinkingResult = checkThinkingCheckpoint(event, profile.thinking_checkpoint || {}, ctx.sessionId, logger);
81
+ if (thinkingResult) {
82
+ return thinkingResult;
162
83
  }
163
- // ═══ GFI GATE - Hard Intercept ═══
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ // POLICY STEP 2: GFI Gate - Hard Intercept
86
+ // ─────────────────────────────────────────────────────────────────────────────
164
87
  // 根据 GFI (疲劳指数) 精细化拦截工具调用
165
88
  // 注意:TIER 0 (只读工具) 已在早期过滤中放行,此处不检查
166
89
  const gfiGateConfig = wctx.config.get('gfi_gate');
167
- if (gfiGateConfig?.enabled !== false && ctx.sessionId) {
168
- const session = getSession(ctx.sessionId);
169
- const currentGfi = session?.currentGfi || 0;
170
- // TIER 3: Bash 命令 - 根据内容判断
171
- if (BASH_TOOLS_SET.has(event.toolName)) {
172
- const command = String(event.params.command || event.params.args || '');
173
- const bashRisk = analyzeBashCommand(command, gfiGateConfig?.bash_safe_patterns || [], gfiGateConfig?.bash_dangerous_patterns || [], logger);
174
- if (bashRisk === 'dangerous') {
175
- // 危险命令 - 直接拦截
176
- logger?.warn?.(`[PD:GFI_GATE] Dangerous bash command blocked: ${command.substring(0, 50)}...`);
177
- return {
178
- block: true,
179
- blockReason: `[GFI Gate] 危险命令被拦截。
180
-
181
- 命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}
182
-
183
- 原因: 检测到危险命令模式,需要确认执行意图。
184
-
185
- 解决方案:
186
- 1. 如果确实需要执行,请确认操作意图后重试
187
- 2. 使用更安全的方式(如手动操作)
188
- 3. 咨询用户确认是否继续
189
-
190
- 注意: 危险命令需要更严格的审批流程。`,
191
- };
192
- }
193
- // safe 命令 - 放行
194
- else if (bashRisk === 'safe') {
195
- // 继续执行
196
- }
197
- // normal 命令 - 按 GFI 阈值判断
198
- else {
199
- const trustEngine = wctx.trust;
200
- const stage = trustEngine.getStage();
201
- const baseThreshold = gfiGateConfig?.thresholds?.low_risk_block || 70;
202
- const dynamicThreshold = calculateDynamicThreshold(baseThreshold, stage, 0, // bash 命令没有行数概念
203
- {
204
- large_change_lines: gfiGateConfig?.large_change_lines || 50,
205
- trust_stage_multipliers: gfiGateConfig?.trust_stage_multipliers || { '1': 0.5, '2': 0.75, '3': 1.0, '4': 1.5 },
206
- });
207
- if (currentGfi >= dynamicThreshold) {
208
- logger?.warn?.(`[PD:GFI_GATE] Bash blocked by GFI: ${currentGfi} >= ${dynamicThreshold}`);
209
- return {
210
- block: true,
211
- blockReason: `[GFI Gate] 疲劳指数过高,操作被拦截。
212
-
213
- 命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}
214
- GFI: ${currentGfi}/100
215
- 动态阈值: ${dynamicThreshold} (Stage ${stage})
216
-
217
- 原因: 当前疲劳指数超过阈值,系统进入保护模式。
218
-
219
- 解决方案:
220
- 1. 执行 /pd-status reset 清零疲劳值
221
- 2. 检查是否存在理解偏差或死循环
222
- 3. 等待问题自然解决后再尝试
223
-
224
- 注意: 这是系统级硬性拦截,AI 无法绕过。`,
225
- };
226
- }
227
- }
228
- }
229
- // TIER 2: 高风险操作 - GFI >= high_risk_block 时拦截
230
- else if (HIGH_RISK_TOOLS.has(event.toolName)) {
231
- const trustEngine = wctx.trust;
232
- const stage = trustEngine.getStage();
233
- const baseThreshold = gfiGateConfig?.thresholds?.high_risk_block || 40;
234
- const dynamicThreshold = calculateDynamicThreshold(baseThreshold, stage, 0, {
235
- large_change_lines: gfiGateConfig?.large_change_lines || 50,
236
- trust_stage_multipliers: gfiGateConfig?.trust_stage_multipliers || { '1': 0.5, '2': 0.75, '3': 1.0, '4': 1.5 },
237
- });
238
- if (currentGfi >= dynamicThreshold) {
239
- const filePath = event.params.file_path || event.params.path || event.params.file || event.params.target || 'unknown';
240
- logger?.warn?.(`[PD:GFI_GATE] High-risk tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${dynamicThreshold}`);
241
- return {
242
- block: true,
243
- blockReason: `[GFI Gate] 高风险操作被拦截。
244
-
245
- 工具: ${event.toolName}
246
- 文件: ${filePath}
247
- GFI: ${currentGfi}/100
248
- 动态阈值: ${dynamicThreshold} (Stage ${stage})
249
-
250
- 原因: 高风险工具需要更低的 GFI 阈值才能执行。
251
-
252
- 解决方案:
253
- 1. 执行 /pd-status reset 清零疲劳值
254
- 2. 检查是否存在理解偏差或死循环
255
- 3. 等待 GFI 自然衰减后重试
256
-
257
- 注意: 这是系统级硬性拦截,AI 无法绕过。`,
258
- };
259
- }
260
- }
261
- // TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
262
- else if (LOW_RISK_WRITE_TOOLS.has(event.toolName)) {
263
- const trustEngine = wctx.trust;
264
- const stage = trustEngine.getStage();
265
- const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
266
- const baseThreshold = gfiGateConfig?.thresholds?.low_risk_block || 70;
267
- const dynamicThreshold = calculateDynamicThreshold(baseThreshold, stage, lineChanges, {
268
- large_change_lines: gfiGateConfig?.large_change_lines || 50,
269
- trust_stage_multipliers: gfiGateConfig?.trust_stage_multipliers || { '1': 0.5, '2': 0.75, '3': 1.0, '4': 1.5 },
270
- });
271
- if (currentGfi >= dynamicThreshold) {
272
- const filePath = event.params.file_path || event.params.path || event.params.file || event.params.target || 'unknown';
273
- logger?.warn?.(`[PD:GFI_GATE] Low-risk tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${dynamicThreshold}`);
274
- return {
275
- block: true,
276
- blockReason: `[GFI Gate] 疲劳指数过高,操作被拦截。
277
-
278
- 工具: ${event.toolName}
279
- 文件: ${filePath}
280
- GFI: ${currentGfi}/100
281
- 动态阈值: ${dynamicThreshold} (Stage ${stage}${lineChanges > 50 ? `, ${lineChanges}行修改` : ''})
282
-
283
- 原因: 当前疲劳指数超过阈值,系统进入保护模式。
284
-
285
- 解决方案:
286
- 1. 执行 /pd-status reset 清零疲劳值
287
- 2. 检查是否存在理解偏差或死循环
288
- 3. 等待问题自然解决后再尝试
289
-
290
- 注意: 这是系统级硬性拦截,AI 无法绕过。`,
291
- };
292
- }
293
- }
294
- // AGENT_TOOLS: Block subagent spawn when GFI is critically high (P0 fix: prevent privilege escalation via spawned subagents)
295
- if (isAgentTool) {
296
- const AGENT_SPAWN_GFI_THRESHOLD = 90;
297
- if (currentGfi >= AGENT_SPAWN_GFI_THRESHOLD) {
298
- logger?.warn?.(`[PD:GFI_GATE] Agent tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${AGENT_SPAWN_GFI_THRESHOLD}`);
299
- return {
300
- block: true,
301
- blockReason: `[GFI Gate] 疲劳指数过高,禁止派生子智能体。
302
-
303
- GFI: ${currentGfi}/100
304
- 阈值: ${AGENT_SPAWN_GFI_THRESHOLD} (Stage ${wctx.trust.getStage()})
305
-
306
- 原因: 高疲劳状态下派生子智能体会放大错误风险。
307
-
308
- 解决方案:
309
- 1. 执行 /pd-status reset 清零疲劳值
310
- 2. 简化任务后重试`,
311
- };
312
- }
313
- }
90
+ const gfiResult = checkGfiGate(event, wctx, ctx.sessionId, gfiGateConfig, logger);
91
+ if (gfiResult) {
92
+ return gfiResult;
314
93
  }
315
94
  // Merge pluginConfig (OpenClaw UI settings)
316
95
  const configRiskPaths = ctx.pluginConfig?.riskPaths ?? [];
@@ -343,186 +122,40 @@ GFI: ${currentGfi}/100
343
122
  const risky = (isBash && filePath.includes(' '))
344
123
  ? profile.risk_paths.some(rp => filePath.includes(rp))
345
124
  : isRisky(relPath, profile.risk_paths);
346
- // ── PROGRESSIVE GATE LOGIC ──
125
+ // ─────────────────────────────────────────────────────────────────────────────
126
+ // POLICY STEP 3: Progressive Trust Gate (Stage 1-4 access control)
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+ // IMPORTANT: This step does NOT return early on allow.
129
+ // We must continue to edit verification for ALL allowed operations.
347
130
  if (profile.progressive_gate?.enabled) {
348
- const trustEngine = wctx.trust;
349
- const trustScore = trustEngine.getScore();
350
- const stage = trustEngine.getStage();
351
- const trustSettings = wctx.config.get('trust') || {
352
- limits: {
353
- stage_2_max_lines: 50,
354
- stage_3_max_lines: 300,
355
- stage_2_max_percentage: 10,
356
- stage_3_max_percentage: 15,
357
- min_lines_fallback: 20,
358
- }
359
- };
360
- const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
361
131
  const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
362
- const planApprovals = profile.progressive_gate?.plan_approvals;
363
- const canUsePlanApproval = Boolean(stage === 1 &&
364
- planApprovals?.enabled &&
365
- getPlanStatus(ctx.workspaceDir) === 'READY' &&
366
- planApprovals.allowed_operations?.includes(event.toolName) &&
367
- matchesAnyPattern(relPath, planApprovals.allowed_patterns || []) &&
368
- ((planApprovals.max_lines_override ?? -1) === -1 || lineChanges <= (planApprovals.max_lines_override ?? -1)));
369
- logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
370
- // ── EP SIMULATION MODE (M6验证) ──
371
- // 记录EP系统的模拟决策,但不生效(仅用于对比分析)
372
- // BUGFIX #90: 移到所有Stage检查之前,确保所有Stage都触发EP simulation记录
373
- try {
374
- const epDecision = checkEvolutionGate(ctx.workspaceDir, {
375
- toolName: event.toolName,
376
- content: event.params?.content,
377
- lineCount: lineChanges,
378
- isRiskPath: risky,
379
- });
380
- const epLogEntry = {
381
- timestamp: new Date().toISOString(),
382
- toolName: event.toolName,
383
- filePath: relPath,
384
- trustEngine: { score: trustScore, stage, decision: 'allow' },
385
- epSystem: { tier: epDecision.currentTier ?? 'UNKNOWN', allowed: epDecision.allowed, reason: epDecision.reason },
386
- conflict: epDecision.allowed === false, // Trust允许但EP拒绝(任何阶段)
387
- };
388
- const epLogPath = path.join(ctx.workspaceDir, '.state', 'ep_simulation.jsonl');
389
- // 安全创建目录(如果失败则跳过日志写入,但不影响 Trust Engine 决策)
390
- let canWriteEpLog = true;
391
- try {
392
- fs.mkdirSync(path.dirname(epLogPath), { recursive: true });
393
- }
394
- catch (mkdirErr) {
395
- if (!mkdirErr || mkdirErr.code !== 'EEXIST') {
396
- logger.warn(`[PD_EP_SIM] Failed to create log dir: ${mkdirErr?.message ?? String(mkdirErr)}, skipping log`);
397
- canWriteEpLog = false;
398
- }
399
- }
400
- if (canWriteEpLog) {
401
- fs.appendFileSync(epLogPath, JSON.stringify(epLogEntry) + '\n');
402
- }
403
- logger.info(`[PD_EP_SIM] Tier: ${epDecision.currentTier}, Allowed: ${epDecision.allowed}, Trust: ${trustScore} (Stage ${stage})`);
404
- }
405
- catch (err) {
406
- // EP 模拟失败不应该影响 Trust Engine 决策
407
- const errMsg = err instanceof Error ? err.message : String(err);
408
- logger.warn(`[PD_EP_SIM] Simulation failed: ${errMsg}, continuing with Trust Engine`);
409
- }
410
- // Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
411
- if (stage === 1) {
412
- if (canUsePlanApproval) {
413
- const planStatus = 'READY';
414
- wctx.eventLog.recordPlanApproval(ctx.sessionId, {
415
- toolName: event.toolName,
416
- filePath: relPath,
417
- pattern: relPath,
418
- planStatus
419
- });
420
- wctx.trajectory?.recordGateBlock?.({
421
- sessionId: ctx.sessionId,
422
- toolName: event.toolName,
423
- filePath: relPath,
424
- reason: 'plan_approval',
425
- planStatus,
426
- });
427
- logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
428
- return;
429
- }
430
- if (risky || riskLevel !== 'LOW') {
431
- // Block if not approved by whitelist
432
- return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName, logger, ctx.sessionId);
433
- }
434
- }
435
- // Stage 2 (Editor): Block writes to risk paths. Block large changes.
436
- if (stage === 2) {
437
- if (risky) {
438
- return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName, logger, ctx.sessionId);
439
- }
440
- // Percentage-based threshold calculation
441
- const targetAbsolutePath = typeof filePath === 'string' ? path.join(ctx.workspaceDir, filePath) : null;
442
- const targetLineCount = targetAbsolutePath ? getTargetFileLineCount(targetAbsolutePath) : null;
443
- const minLinesFallback = trustSettings.limits?.min_lines_fallback ?? 20;
444
- const stage2MaxPercentage = trustSettings.limits?.stage_2_max_percentage ?? 10;
445
- const stage2FixedLimit = trustSettings.limits?.stage_2_max_lines ?? 50;
446
- let effectiveLimit;
447
- let limitType;
448
- let actualPercentage = null;
449
- if (targetLineCount !== null && targetLineCount > 0) {
450
- effectiveLimit = calculatePercentageThreshold(targetLineCount, stage2MaxPercentage, minLinesFallback);
451
- actualPercentage = Math.round((lineChanges / targetLineCount) * 100);
452
- limitType = 'percentage';
453
- }
454
- else {
455
- effectiveLimit = stage2FixedLimit;
456
- limitType = 'fixed';
457
- }
458
- if (lineChanges > effectiveLimit) {
459
- return block(relPath, buildLineLimitReason(lineChanges, effectiveLimit, limitType, targetLineCount, actualPercentage, 2), wctx, event.toolName, logger, ctx.sessionId);
460
- }
461
- }
462
- // Stage 3 (Developer): Allow normal writes. Require READY plan for risk paths.
463
- if (stage === 3) {
464
- if (risky) {
465
- const planStatus = getPlanStatus(ctx.workspaceDir);
466
- if (planStatus !== 'READY') {
467
- return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName, logger, ctx.sessionId);
468
- }
469
- }
470
- // Percentage-based threshold calculation
471
- const targetAbsolutePath = typeof filePath === 'string' ? path.join(ctx.workspaceDir, filePath) : null;
472
- const targetLineCount = targetAbsolutePath ? getTargetFileLineCount(targetAbsolutePath) : null;
473
- const minLinesFallback = trustSettings.limits?.min_lines_fallback ?? 20;
474
- const stage3MaxPercentage = trustSettings.limits?.stage_3_max_percentage ?? 15;
475
- const stage3FixedLimit = trustSettings.limits?.stage_3_max_lines ?? 300;
476
- let effectiveLimit;
477
- let limitType;
478
- let actualPercentage = null;
479
- if (targetLineCount !== null && targetLineCount > 0) {
480
- effectiveLimit = calculatePercentageThreshold(targetLineCount, stage3MaxPercentage, minLinesFallback);
481
- actualPercentage = Math.round((lineChanges / targetLineCount) * 100);
482
- limitType = 'percentage';
483
- }
484
- else {
485
- effectiveLimit = stage3FixedLimit;
486
- limitType = 'fixed';
487
- }
488
- if (lineChanges > effectiveLimit) {
489
- return block(relPath, buildLineLimitReason(lineChanges, effectiveLimit, limitType, targetLineCount, actualPercentage, 3), wctx, event.toolName, logger, ctx.sessionId);
490
- }
491
- }
492
- // Stage 4 (Architect): Full bypass
493
- if (stage === 4) {
494
- logger.info(`[PD_GATE] Trusted Architect bypass for ${relPath}`);
495
- // Audit log for Stage 4 bypass (security traceability)
496
- try {
497
- const stateDir = wctx.resolve('STATE_DIR');
498
- const eventLog = EventLogService.get(stateDir);
499
- eventLog.recordGateBypass(ctx.sessionId, {
500
- toolName: event.toolName,
501
- filePath: relPath,
502
- bypassType: 'stage4_architect',
503
- trustScore,
504
- trustStage: stage,
505
- });
506
- }
507
- catch (auditErr) {
508
- logger?.warn?.(`[PD_GATE] Failed to record Stage 4 bypass audit: ${String(auditErr)}`);
509
- }
510
- return;
132
+ const progressiveGateResult = checkProgressiveTrustGate(event, wctx, relPath, risky, lineChanges, logger, ctx, profile);
133
+ if (progressiveGateResult) {
134
+ return progressiveGateResult;
511
135
  }
136
+ // NOTE: Do NOT return here! Continue to edit verification.
137
+ // Stage 4 bypass or Stage 1-3 allow should still run edit verification.
512
138
  }
513
139
  else {
514
- // FALLBACK: Legacy Gate Logic
140
+ // FALLBACK: Legacy Gate Logic (when progressive gate is disabled)
515
141
  if (risky && profile.gate?.require_plan_for_risk_paths) {
516
142
  const planStatus = getPlanStatus(ctx.workspaceDir);
517
143
  if (planStatus !== 'READY') {
518
- return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName, logger, ctx.sessionId);
144
+ return recordGateBlockAndReturn(wctx, {
145
+ filePath: relPath,
146
+ reason: `No READY plan found in PLAN.md.`,
147
+ toolName: event.toolName,
148
+ sessionId: ctx.sessionId,
149
+ blockSource: 'gate-legacy',
150
+ }, logger);
519
151
  }
520
152
  }
521
153
  }
522
- // ═══════════════════════════════════════════════════════════════
523
- // P-03: Edit Tool Force Verification
524
- // ═══════════════════════════════════════════════════════════════
525
- // After all gate checks, verify edit operations (enforces P-03)
154
+ // ─────────────────────────────────────────────────────────────────────────────
155
+ // POLICY STEP 4: Edit Tool Verification (P-03)
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // This MUST run after all other gate checks for ALL tools.
158
+ // Edit verification ensures oldText matches the actual file content.
526
159
  if (event.toolName === 'edit' && profile.edit_verification?.enabled !== false) {
527
160
  const verifyResult = handleEditVerification(event, wctx, ctx, {
528
161
  enabled: profile.edit_verification.enabled,
@@ -535,328 +168,6 @@ GFI: ${currentGfi}/100
535
168
  return verifyResult; // Block or modify params
536
169
  }
537
170
  }
538
- }
539
- /**
540
- * Build a detailed reason message for line limit blocks.
541
- */
542
- function buildLineLimitReason(lineChanges, effectiveLimit, limitType, targetLineCount, actualPercentage, stage) {
543
- if (limitType === 'percentage' && targetLineCount !== null && actualPercentage !== null) {
544
- return `Modification too large: ${lineChanges} lines (${actualPercentage}% of ${targetLineCount} lines). ` +
545
- `Stage ${stage} limit is ${effectiveLimit} lines (${limitType}). ` +
546
- `Threshold calculation: min(${targetLineCount} × ${actualPercentage}%, ${effectiveLimit} lines).`;
547
- }
548
- else {
549
- return `Modification too large: ${lineChanges} lines. ` +
550
- `Stage ${stage} limit is ${effectiveLimit} lines (fixed threshold). ` +
551
- `Note: Could not read target file to calculate percentage-based limit. Check file permissions and encoding.`;
552
- }
553
- }
554
- function block(filePath, reason, wctx, toolName, logger, sessionId) {
555
- logger.error?.(`[PD_GATE] BLOCKED: ${filePath}. Reason: ${reason}`);
556
- if (sessionId) {
557
- trackBlock(sessionId);
558
- }
559
- const trajectoryPayload = {
560
- sessionId: sessionId ?? null,
561
- toolName,
562
- filePath,
563
- reason,
564
- };
565
- try {
566
- wctx.eventLog.recordGateBlock(sessionId, {
567
- toolName,
568
- filePath,
569
- reason,
570
- });
571
- }
572
- catch (error) {
573
- logger.warn?.(`[PD_GATE] Failed to record gate block event: ${String(error)}`);
574
- }
575
- try {
576
- wctx.trajectory?.recordGateBlock?.(trajectoryPayload);
577
- }
578
- catch (error) {
579
- logger.warn?.(`[PD_GATE] Failed to record trajectory gate block: ${String(error)}`);
580
- scheduleTrajectoryGateBlockRetry(wctx, trajectoryPayload, 1, {
581
- warn: (message) => logger.warn?.(message),
582
- error: (message) => logger.error?.(message),
583
- });
584
- }
585
- return {
586
- block: true,
587
- blockReason: `[Principles Disciple] Security Gate Blocked this action.
588
- File: ${filePath}
589
- Reason: ${reason}
590
-
591
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
592
- 📋 How to unblock this operation:
593
-
594
- 1. Use the plan-script skill to create a PLAN.md:
595
- → Invoke: skill:plan-script
596
-
597
- 2. Fill in the plan with:
598
- - Target Files: ${filePath}
599
- - Steps: What you want to do (be specific)
600
- - Metrics: How to verify success
601
- - Active Mental Models: Select 2 relevant models from .principles/THINKING_OS.md
602
- - Rollback: How to restore if it fails
603
-
604
- 3. After completing the plan, set STATUS: READY in PLAN.md
605
-
606
- 4. Retry the operation
607
-
608
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
609
- This is a mandatory security gate. The operation was blocked because the modification exceeds the allowed threshold for your current trust stage.
610
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
611
- };
612
- }
613
- function scheduleTrajectoryGateBlockRetry(wctx, payload, attempt, logger) {
614
- if (attempt > TRAJECTORY_GATE_BLOCK_MAX_RETRIES) {
615
- logger.error?.(`[PD_GATE] Failed to persist trajectory gate block after ${TRAJECTORY_GATE_BLOCK_MAX_RETRIES} retries: ${payload.toolName} ${payload.filePath}`);
616
- return;
617
- }
618
- setTimeout(() => {
619
- try {
620
- wctx.trajectory?.recordGateBlock?.(payload);
621
- }
622
- catch (error) {
623
- logger.warn(`[PD_GATE] Retrying trajectory gate block persistence: ${String(error)}`);
624
- scheduleTrajectoryGateBlockRetry(wctx, payload, attempt + 1, logger);
625
- }
626
- }, TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS);
627
- }
628
- // ═══════════════════════════════════════════════════════════════
629
- // P-03: Edit Tool Force Verification
630
- // ═══════════════════════════════════════════════════════════════
631
- /**
632
- * Normalize a line for fuzzy matching by collapsing whitespace
633
- */
634
- function normalizeLine(line) {
635
- return line.replace(/\s+/g, ' ').trim();
636
- }
637
- /**
638
- * Find fuzzy match between oldText and current file content
639
- * @param lines - File content split into lines
640
- * @param oldLines - oldText split into lines
641
- * @param threshold - Match threshold (0-1)
642
- * @returns Match index or -1 if not found
643
- */
644
- function findFuzzyMatch(lines, oldLines, threshold = 0.8) {
645
- if (oldLines.length === 0)
646
- return -1; // P2 fix: empty array boundary check
647
- const normalizedLines = lines.map(normalizeLine);
648
- const normalizedOldLines = oldLines.map(normalizeLine);
649
- // Try to find matching sequence
650
- for (let i = 0; i <= lines.length - oldLines.length; i++) {
651
- let matchCount = 0;
652
- for (let j = 0; j < oldLines.length; j++) {
653
- if (normalizedLines[i + j] === normalizedOldLines[j]) {
654
- matchCount++;
655
- }
656
- }
657
- // Use threshold from config
658
- if (matchCount >= oldLines.length * threshold) {
659
- return i;
660
- }
661
- }
662
- return -1;
663
- }
664
- /**
665
- * Try to find a fuzzy match for oldText in the current content
666
- * @param currentContent - Current file content
667
- * @param oldText - Text to match
668
- * @param threshold - Match threshold (0-1)
669
- * @returns Object with found status and corrected text if found
670
- */
671
- function tryFuzzyMatch(currentContent, oldText, threshold = 0.8) {
672
- const lines = currentContent.split('\n');
673
- const oldLines = oldText.split('\n');
674
- const matchIndex = findFuzzyMatch(lines, oldLines, threshold);
675
- if (matchIndex !== -1) {
676
- // Found fuzzy match, extract actual text from file
677
- const correctedText = lines.slice(matchIndex, matchIndex + oldLines.length).join('\n');
678
- return { found: true, correctedText };
679
- }
680
- return { found: false };
681
- }
682
- /**
683
- * Generate a helpful error message for edit verification failure
684
- */
685
- function generateEditError(filePath, oldText, currentContent) {
686
- const expectedSnippet = oldText.split('\n').slice(0, 3).join('\n').substring(0, 200);
687
- const actualSnippet = currentContent.substring(0, 200);
688
- return `[P-03 Violation] Edit verification failed
689
-
690
- File: ${filePath}
691
-
692
- The text you're trying to replace does not match the current file content.
693
-
694
- Expected to find:
695
- ${expectedSnippet}${oldText.length > 200 ? '...' : ''}
696
-
697
- Actual file contains:
698
- ${actualSnippet}${currentContent.length > 200 ? '...' : ''}
699
-
700
- Possible reasons:
701
- - File has been modified by another process
702
- - Whitespace characters do not match (spaces, tabs, newlines)
703
- - Context compression caused outdated information
704
-
705
- Solution:
706
- 1. Use the 'read' tool to get the current file content
707
- 2. Update your edit command with the exact text from the file
708
- 3. Retry the edit operation
709
-
710
- This is enforced by P-03 (精确匹配前验证原则).`;
711
- }
712
- /**
713
- * Handle edit tool verification before allowing the operation
714
- * This enforces P-03 at the tool layer
715
- */
716
- function handleEditVerification(event, wctx, ctx, config = {}) {
717
- const logger = ctx.logger || console;
718
- const maxSizeBytes = config.max_file_size_bytes ?? 10 * 1024 * 1024; // Default 10MB
719
- const fuzzyMatchEnabled = config.fuzzy_match_enabled !== false;
720
- const fuzzyMatchThreshold = config.fuzzy_match_threshold ?? 0.8;
721
- const skipAction = config.skip_large_file_action ?? 'warn';
722
- // 1. Extract parameters (handle both parameter naming conventions)
723
- const filePath = event.params.file_path || event.params.path || event.params.file;
724
- const oldText = event.params.oldText || event.params.old_string;
725
- if (!filePath || !oldText) {
726
- // Missing required parameters, let it fail naturally
727
- return;
728
- }
729
- // 2. Resolve and read file
730
- let absolutePath;
731
- try {
732
- absolutePath = wctx.resolve(filePath);
733
- }
734
- catch (error) {
735
- // Path resolution error, let it fail naturally
736
- return;
737
- }
738
- // 2.5. Skip verification for binary files
739
- const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg',
740
- '.pdf', '.zip', '.tar', '.gz', '.7z', '.rar',
741
- '.exe', '.dll', '.so', '.dylib', '.bin',
742
- '.mp3', '.mp4', '.avi', '.mov', '.wav',
743
- '.ttf', '.otf', '.woff', '.woff2',
744
- '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
745
- const ext = path.extname(absolutePath).toLowerCase();
746
- if (BINARY_EXTENSIONS.includes(ext)) {
747
- logger?.info?.(`[PD_GATE:EDIT_VERIFY] Skipping verification for binary file: ${path.basename(filePath)}`);
748
- return;
749
- }
750
- try {
751
- // 2.6. Check file size before reading (P-03 improvement)
752
- try {
753
- const stats = fs.statSync(absolutePath);
754
- const fileSizeBytes = stats.size;
755
- const fileSizeMB = fileSizeBytes / (1024 * 1024);
756
- if (fileSizeBytes > maxSizeBytes) {
757
- const message = `[PD_GATE:EDIT_VERIFY] File size check: ${path.basename(filePath)} is ${fileSizeMB.toFixed(2)}MB (threshold: ${(maxSizeBytes / (1024 * 1024)).toFixed(2)}MB)`;
758
- if (skipAction === 'block') {
759
- logger?.warn?.(message + ' - BLOCKED');
760
- return {
761
- block: true,
762
- blockReason: `${message}\n\nFile is too large for edit verification. Increase max_file_size_bytes in PROFILE.json or reduce file size.`
763
- };
764
- }
765
- else {
766
- logger?.warn?.(message + ' - SKIPPING verification');
767
- return; // Skip verification but allow operation
768
- }
769
- }
770
- logger?.info?.(`[PD_GATE:EDIT_VERIFY] File size check passed: ${path.basename(filePath)} (${fileSizeMB.toFixed(2)}MB)`);
771
- }
772
- catch (statError) {
773
- // File stat error (e.g., permission denied)
774
- const errStr = statError instanceof Error ? statError.message : String(statError);
775
- const errCode = statError.code;
776
- if (errCode === 'EACCES' || errCode === 'EPERM') {
777
- logger?.error?.(`[PD_GATE:EDIT_VERIFY] Permission denied accessing file: ${path.basename(filePath)} (${errStr})`);
778
- return {
779
- block: true,
780
- blockReason: `[P-03 Error] Permission denied: Cannot access file ${absolutePath}\n\nError: ${errStr}\n\nSolution: Check file permissions or run with appropriate access rights.`
781
- };
782
- }
783
- else if (errCode === 'ENOENT') {
784
- logger?.warn?.(`[PD_GATE:EDIT_VERIFY] File not found: ${path.basename(filePath)} (${errStr})`);
785
- // File doesn't exist - let the edit operation proceed (it will create the file)
786
- return;
787
- }
788
- else {
789
- logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Stat error: ${errStr}`);
790
- // Let it fail naturally on read attempt
791
- }
792
- }
793
- // 3. Read current file content with improved error handling
794
- let currentContent;
795
- try {
796
- currentContent = fs.readFileSync(absolutePath, 'utf-8');
797
- }
798
- catch (readError) {
799
- const errStr = readError instanceof Error ? readError.message : String(readError);
800
- const errCode = readError.code;
801
- if (errCode === 'EACCES' || errCode === 'EPERM') {
802
- logger?.error?.(`[PD_GATE:EDIT_VERIFY] Permission denied reading file: ${path.basename(filePath)} (${errStr})`);
803
- return {
804
- block: true,
805
- blockReason: `[P-03 Error] Permission denied: Cannot read file ${absolutePath}\n\nError: ${errStr}\n\nSolution: Check file permissions or run with appropriate access rights.`
806
- };
807
- }
808
- else if (errCode === 'ENOENT') {
809
- logger?.warn?.(`[PD_GATE:EDIT_VERIFY] File not found: ${path.basename(filePath)} (${errStr})`);
810
- // File doesn't exist - let the edit operation proceed
811
- return;
812
- }
813
- else if (errStr.includes('UTF-8') || errStr.includes('encoding')) {
814
- logger?.error?.(`[PD_GATE:EDIT_VERIFY] Encoding error reading file: ${path.basename(filePath)} (${errStr})`);
815
- return {
816
- block: true,
817
- blockReason: `[P-03 Error] Encoding error: Cannot read file ${absolutePath}\n\nError: ${errStr}\n\nThe file appears to use an encoding other than UTF-8. Edit verification requires UTF-8 readable text files.\n\nSolution: Ensure the file is UTF-8 encoded text, or mark binary extensions to skip verification.`
818
- };
819
- }
820
- else {
821
- logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Read error: ${errStr}`);
822
- // Let it fail naturally
823
- return;
824
- }
825
- }
826
- // 4. Verify oldText exists in current content
827
- if (!currentContent.includes(oldText)) {
828
- logger?.info?.(`[PD_GATE:EDIT_VERIFY] Exact match failed for ${path.basename(filePath)}, trying fuzzy match`);
829
- // 5. Try fuzzy matching (if enabled)
830
- if (fuzzyMatchEnabled) {
831
- const fuzzyResult = tryFuzzyMatch(currentContent, oldText, fuzzyMatchThreshold);
832
- if (fuzzyResult.found && fuzzyResult.correctedText) {
833
- logger?.info?.(`[PD_GATE:EDIT_VERIFY] Fuzzy match found for ${path.basename(filePath)}, auto-correcting oldText`);
834
- // Return corrected parameters
835
- return {
836
- params: {
837
- ...event.params,
838
- oldText: fuzzyResult.correctedText,
839
- old_string: fuzzyResult.correctedText
840
- }
841
- };
842
- }
843
- }
844
- // 6. No match found, block the operation with helpful error
845
- const errorMsg = generateEditError(absolutePath, oldText, currentContent);
846
- logger?.error?.(`[PD_GATE:EDIT_VERIFY] Block edit on ${path.basename(filePath)}: oldText not found`);
847
- return {
848
- block: true,
849
- blockReason: errorMsg
850
- };
851
- }
852
- // 7. Verification passed, allow edit to proceed
853
- logger?.info?.(`[PD_GATE:EDIT_VERIFY] Verified edit on ${path.basename(filePath)}`);
854
- return;
855
- }
856
- catch (error) {
857
- // Unexpected error - let it fail naturally
858
- const errorStr = error instanceof Error ? error.message : String(error);
859
- logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Unexpected error: ${errorStr}`);
860
- return;
861
- }
171
+ // All checks passed - allow the operation
172
+ return;
862
173
  }