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.
- package/dist/commands/evolution-status.js +32 -44
- package/dist/commands/focus.js +30 -155
- package/dist/config/defaults/runtime.d.ts +40 -0
- package/dist/config/defaults/runtime.js +44 -0
- package/dist/config/errors.d.ts +84 -0
- package/dist/config/errors.js +94 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/constants/diagnostician.d.ts +12 -0
- package/dist/constants/diagnostician.js +56 -0
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +7 -0
- package/dist/core/control-ui-db.d.ts +27 -0
- package/dist/core/control-ui-db.js +18 -0
- package/dist/core/evolution-engine.js +1 -1
- package/dist/core/focus-history.d.ts +92 -0
- package/dist/core/focus-history.js +490 -0
- package/dist/core/init.js +2 -2
- package/dist/core/path-resolver.js +2 -1
- package/dist/core/profile.js +1 -1
- package/dist/core/trajectory.d.ts +60 -0
- package/dist/core/trajectory.js +72 -2
- package/dist/hooks/bash-risk.d.ts +57 -0
- package/dist/hooks/bash-risk.js +137 -0
- package/dist/hooks/edit-verification.d.ts +62 -0
- package/dist/hooks/edit-verification.js +256 -0
- package/dist/hooks/gate-block-helper.d.ts +44 -0
- package/dist/hooks/gate-block-helper.js +119 -0
- package/dist/hooks/gate.d.ts +18 -0
- package/dist/hooks/gate.js +63 -752
- package/dist/hooks/gfi-gate.d.ts +40 -0
- package/dist/hooks/gfi-gate.js +112 -0
- package/dist/hooks/progressive-trust-gate.d.ts +79 -0
- package/dist/hooks/progressive-trust-gate.js +242 -0
- package/dist/hooks/prompt.js +83 -28
- package/dist/hooks/subagent.js +1 -2
- package/dist/hooks/thinking-checkpoint.d.ts +37 -0
- package/dist/hooks/thinking-checkpoint.js +51 -0
- package/dist/http/principles-console-route.d.ts +7 -0
- package/dist/http/principles-console-route.js +255 -3
- package/dist/index.js +0 -2
- package/dist/service/central-database.d.ts +104 -0
- package/dist/service/central-database.js +649 -0
- package/dist/service/control-ui-query-service.d.ts +1 -1
- package/dist/service/control-ui-query-service.js +3 -3
- package/dist/service/evolution-query-service.d.ts +1 -1
- package/dist/service/evolution-query-service.js +5 -5
- package/dist/service/evolution-worker.d.ts +10 -0
- package/dist/service/evolution-worker.js +10 -6
- package/dist/service/phase3-input-filter.d.ts +57 -0
- package/dist/service/phase3-input-filter.js +93 -3
- package/dist/service/runtime-summary-service.d.ts +34 -0
- package/dist/service/runtime-summary-service.js +93 -1
- package/dist/tools/deep-reflect.js +1 -2
- package/dist/types/event-types.d.ts +2 -0
- package/dist/types/runtime-summary.d.ts +54 -0
- package/dist/types/runtime-summary.js +1 -0
- package/dist/utils/subagent-probe.d.ts +11 -0
- package/dist/utils/subagent-probe.js +46 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/templates/langs/en/core/AGENTS.md +1 -1
- package/templates/langs/en/core/TOOLS.md +1 -1
- package/templates/langs/zh/core/AGENTS.md +1 -1
- package/templates/langs/zh/core/TOOLS.md +1 -1
- package/{agents/auditor.md → templates/langs/zh/skills/pd-auditor/SKILL.md} +3 -3
- package/{agents/diagnostician.md → templates/langs/zh/skills/pd-diagnostician/SKILL.md} +39 -29
- package/{agents/explorer.md → templates/langs/zh/skills/pd-explorer/SKILL.md} +3 -3
- package/{agents/implementer.md → templates/langs/zh/skills/pd-implementer/SKILL.md} +3 -3
- package/{agents/planner.md → templates/langs/zh/skills/pd-planner/SKILL.md} +3 -3
- package/{agents/reporter.md → templates/langs/zh/skills/pd-reporter/SKILL.md} +3 -3
- package/{agents/reviewer.md → templates/langs/zh/skills/pd-reviewer/SKILL.md} +3 -3
- package/dist/core/agent-loader.d.ts +0 -44
- package/dist/core/agent-loader.js +0 -147
- package/dist/tools/agent-spawn.d.ts +0 -54
- package/dist/tools/agent-spawn.js +0 -456
package/dist/hooks/gate.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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'
|
|
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
|
-
//
|
|
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
|
|
152
|
-
if (
|
|
153
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
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
|
}
|