principles-disciple 1.62.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/hooks/gate-block-helper.ts +1 -1
- package/src/hooks/gate.ts +27 -205
- package/tests/hooks/gate-rule-host-pipeline.test.ts +159 -334
- package/tests/service/evolution-worker.compilation-backfill.test.ts +5 -1
- package/src/hooks/bash-risk.ts +0 -175
- package/src/hooks/edit-verification.ts +0 -302
- package/src/hooks/gfi-gate.ts +0 -186
- package/src/hooks/progressive-trust-gate.ts +0 -183
- package/src/hooks/thinking-checkpoint.ts +0 -76
- package/tests/hooks/bash-risk-integration.test.ts +0 -137
- package/tests/hooks/bash-risk.test.ts +0 -81
- package/tests/hooks/edit-verification.test.ts +0 -678
- package/tests/hooks/gate-edit-verification-p1.test.ts +0 -632
- package/tests/hooks/gate-pipeline-integration.test.ts +0 -404
- package/tests/hooks/gate.test.ts +0 -271
- package/tests/hooks/gfi-gate-unit.test.ts +0 -422
- package/tests/hooks/gfi-gate.test.ts +0 -669
- package/tests/hooks/thinking-gate.test.ts +0 -313
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* PURPOSE: Provide ONE authoritative implementation for gate block persistence.
|
|
5
5
|
*
|
|
6
|
-
* All gate
|
|
6
|
+
* All gate sources (rule-host) must use this
|
|
7
7
|
* helper to ensure consistent block tracking, event logging, and retry behavior.
|
|
8
8
|
*
|
|
9
9
|
* This eliminates the "multi-truth source" problem where different modules
|
package/src/hooks/gate.ts
CHANGED
|
@@ -1,45 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security Gate Hook -
|
|
2
|
+
* Security Gate Hook - Rule Host Only
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* This is the SINGLE AUTHORITATIVE orchestration path.
|
|
5
|
+
* All blocking logic is now dynamic via Rule Host — no hardcoded gates remain.
|
|
5
6
|
*
|
|
7
|
+
* Flow:
|
|
6
8
|
* 1. Early Return: Skip if not write/bash/agent tool or no workspace
|
|
7
|
-
* 2.
|
|
8
|
-
* 3. GFI Gate: Fatigue index-based blocking
|
|
9
|
-
* 4. Bash Mutation Detection: Heuristic for bash file modifications
|
|
10
|
-
* 4.5. Rule Host: Active code implementation evaluation (Phase 12)
|
|
11
|
-
* 5. Progressive Gate: EP tier-based access control
|
|
12
|
-
* 6. Edit Verification (P-03): Exact/fuzzy match for edit operations
|
|
13
|
-
*
|
|
14
|
-
* IMPORTANT: This is the SINGLE AUTHORITATIVE orchestration path.
|
|
15
|
-
* All policy modules (gfi-gate, progressive-trust-gate, rule-host) use the shared
|
|
16
|
-
* `recordGateBlockAndReturn` helper to ensure consistent block persistence.
|
|
17
|
-
*
|
|
18
|
-
* Zero-width character detection is handled in bash-risk.ts.
|
|
9
|
+
* 2. Rule Host: Dynamic principle-based evaluation (sole gate)
|
|
19
10
|
*/
|
|
20
11
|
|
|
21
12
|
import * as fs from 'fs';
|
|
22
13
|
import * as path from 'path';
|
|
23
|
-
import {
|
|
24
|
-
import { normalizeProfile } from '../core/profile.js';
|
|
25
|
-
import { estimateLineChanges } from '../core/risk-calculator.js';
|
|
14
|
+
import { normalizePath, planStatus } from '../utils/io.js';
|
|
26
15
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
27
|
-
import { checkThinkingCheckpoint } from './thinking-checkpoint.js';
|
|
28
|
-
import { handleEditVerification } from './edit-verification.js';
|
|
29
|
-
import { checkGfiGate } from './gfi-gate.js';
|
|
30
|
-
import { checkProgressiveTrustGate } from './progressive-trust-gate.js';
|
|
31
16
|
import { recordGateBlockAndReturn } from './gate-block-helper.js';
|
|
32
17
|
import { RuleHost } from '../core/rule-host.js';
|
|
33
18
|
import type { RuleHostInput } from '../core/rule-host-types.js';
|
|
34
19
|
import type { PluginHookBeforeToolCallEvent, PluginHookToolContext, PluginHookBeforeToolCallResult, PluginLogger } from '../openclaw-sdk.js';
|
|
35
|
-
import {
|
|
36
|
-
AGENT_TOOLS,
|
|
37
|
-
BASH_TOOLS_SET,
|
|
38
|
-
WRITE_TOOLS,
|
|
39
|
-
} from '../constants/tools.js';
|
|
20
|
+
import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js';
|
|
40
21
|
import { getSession, hasRecentThinking } from '../core/session-tracker.js';
|
|
41
22
|
import { getEvolutionEngine } from '../core/evolution-engine.js';
|
|
42
23
|
import { EventLogService } from '../core/event-log.js';
|
|
24
|
+
import { estimateLineChanges } from '../core/risk-calculator.js';
|
|
43
25
|
|
|
44
26
|
export function handleBeforeToolCall(
|
|
45
27
|
event: PluginHookBeforeToolCallEvent,
|
|
@@ -58,154 +40,57 @@ export function handleBeforeToolCall(
|
|
|
58
40
|
|
|
59
41
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
60
42
|
|
|
61
|
-
// 2.
|
|
62
|
-
const profilePath = wctx.resolve('PROFILE');
|
|
63
|
-
let profile = {
|
|
64
|
-
risk_paths: [] as string[],
|
|
65
|
-
gate: { require_plan_for_risk_paths: true },
|
|
66
|
-
progressive_gate: {
|
|
67
|
-
enabled: true,
|
|
68
|
-
plan_approvals: {
|
|
69
|
-
enabled: false,
|
|
70
|
-
max_lines_override: -1,
|
|
71
|
-
allowed_patterns: [] as string[],
|
|
72
|
-
allowed_operations: [] as string[],
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
edit_verification: {
|
|
76
|
-
enabled: true,
|
|
77
|
-
max_file_size_bytes: 10 * 1024 * 1024,
|
|
78
|
-
fuzzy_match_enabled: true,
|
|
79
|
-
fuzzy_match_threshold: 0.8,
|
|
80
|
-
skip_large_file_action: 'warn' as 'warn' | 'block',
|
|
81
|
-
},
|
|
82
|
-
thinking_checkpoint: {
|
|
83
|
-
enabled: false, // Default OFF
|
|
84
|
-
window_ms: 5 * 60 * 1000,
|
|
85
|
-
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'],
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (fs.existsSync(profilePath)) {
|
|
90
|
-
try {
|
|
91
|
-
const rawProfile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
|
92
|
-
profile = normalizeProfile(rawProfile);
|
|
93
|
-
} catch (e) {
|
|
94
|
-
logger?.error?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
-
// POLICY STEP 1: Thinking OS Checkpoint (P-10)
|
|
100
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
-
// Only enforced when thinking_checkpoint.enabled = true in PROFILE.json
|
|
102
|
-
const thinkingResult = checkThinkingCheckpoint(
|
|
103
|
-
event,
|
|
104
|
-
profile.thinking_checkpoint || {},
|
|
105
|
-
ctx.sessionId,
|
|
106
|
-
logger
|
|
107
|
-
);
|
|
108
|
-
if (thinkingResult) {
|
|
109
|
-
return thinkingResult;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
-
// POLICY STEP 2: GFI Gate - Hard Intercept
|
|
114
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
-
// 根据 GFI (疲劳指数) 精细化拦截工具调用
|
|
116
|
-
// 注意:TIER 0 (只读工具) 已在早期过滤中放行,此处不检查
|
|
117
|
-
const gfiGateConfig = wctx.config.get('gfi_gate');
|
|
118
|
-
const gfiResult = checkGfiGate(event, wctx, ctx.sessionId, gfiGateConfig, logger);
|
|
119
|
-
if (gfiResult) {
|
|
120
|
-
return gfiResult;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Merge pluginConfig (OpenClaw UI settings)
|
|
124
|
-
const configRiskPaths = (ctx.pluginConfig?.riskPaths as string[] | undefined) ?? [];
|
|
125
|
-
if (configRiskPaths.length > 0) {
|
|
126
|
-
profile.risk_paths = [...new Set([...profile.risk_paths, ...configRiskPaths])];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 3. Resolve the target file path
|
|
43
|
+
// 2. Resolve the target file path
|
|
130
44
|
let filePath = event.params.file_path || event.params.path || event.params.file || event.params.target;
|
|
131
45
|
|
|
132
46
|
// Heuristic for bash mutation detection
|
|
133
47
|
if (isBash && !filePath) {
|
|
134
|
-
const command = String(event.params.command || event.params.args ||
|
|
48
|
+
const command = String(event.params.command || event.params.args || '');
|
|
135
49
|
const mutationMatch = /(?:>|>>|sed\s+-i|rm|mv|mkdir|touch|cp)\s+(?:-[a-zA-Z]+\s+)*([^\s;&|<>]+)/.exec(command);
|
|
136
50
|
|
|
137
51
|
if (mutationMatch) {
|
|
138
|
-
|
|
139
|
-
|
|
140
52
|
filePath = mutationMatch[1];
|
|
141
53
|
} else {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (hasRiskPath && isMutation) {
|
|
146
|
-
filePath = command;
|
|
147
|
-
} else {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
54
|
+
// Bash command without a clear file target — let it through to Rule Host
|
|
55
|
+
filePath = command;
|
|
150
56
|
}
|
|
151
57
|
}
|
|
152
58
|
|
|
153
59
|
if (typeof filePath !== 'string') return;
|
|
154
60
|
|
|
155
61
|
const relPath = normalizePath(filePath, ctx.workspaceDir);
|
|
156
|
-
const risky = (isBash && filePath.includes(' '))
|
|
157
|
-
? profile.risk_paths.some(rp => filePath.includes(rp))
|
|
158
|
-
: isRisky(relPath, profile.risk_paths);
|
|
159
62
|
|
|
160
|
-
//
|
|
161
|
-
// POLICY STEP 2.5: Rule Host Evaluation (Phase 12, D-01/D-03)
|
|
162
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
-
// Inserted between GFI gate and Progressive Gate so principle rules can act
|
|
164
|
-
// before the capability-boundary fallback. Active code implementations run
|
|
165
|
-
// through a constrained vm context with minimal helpers only.
|
|
63
|
+
// 3. Rule Host Evaluation — sole gate
|
|
166
64
|
try {
|
|
167
65
|
const ruleHost = new RuleHost(wctx.stateDir, logger);
|
|
168
66
|
const hostInput: RuleHostInput = {
|
|
169
67
|
action: {
|
|
170
68
|
toolName: event.toolName,
|
|
171
69
|
normalizedPath: relPath,
|
|
172
|
-
|
|
173
|
-
|
|
174
70
|
paramsSummary: _extractParamsSummary(event.params),
|
|
175
71
|
},
|
|
176
72
|
workspace: {
|
|
177
|
-
isRiskPath:
|
|
178
|
-
|
|
179
|
-
|
|
73
|
+
isRiskPath: false, // Rule Host determines risk dynamically
|
|
180
74
|
planStatus: _getPlanStatus(ctx.workspaceDir),
|
|
181
|
-
|
|
182
|
-
|
|
183
75
|
hasPlanFile: _hasPlanFile(ctx.workspaceDir),
|
|
184
76
|
},
|
|
185
77
|
session: {
|
|
186
78
|
sessionId: ctx.sessionId,
|
|
187
|
-
|
|
188
|
-
|
|
189
79
|
currentGfi: _getCurrentGfi(ctx.sessionId),
|
|
190
|
-
|
|
191
|
-
|
|
192
80
|
recentThinking: _hasRecentThinking(ctx.sessionId),
|
|
193
81
|
},
|
|
194
82
|
evolution: {
|
|
195
|
-
|
|
196
|
-
|
|
197
83
|
epTier: _getEpTier(wctx.workspaceDir),
|
|
198
84
|
},
|
|
199
85
|
derived: {
|
|
200
86
|
estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params }),
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
bashRisk: _getBashRisk(event, profile),
|
|
87
|
+
bashRisk: _getBashRisk(event),
|
|
204
88
|
},
|
|
205
89
|
};
|
|
206
90
|
|
|
207
91
|
const hostResult = ruleHost.evaluate(hostInput);
|
|
208
|
-
|
|
92
|
+
|
|
93
|
+
// Always emit rulehost_evaluated
|
|
209
94
|
try {
|
|
210
95
|
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
211
96
|
eventLog.recordRuleHostEvaluated({
|
|
@@ -218,8 +103,8 @@ export function handleBeforeToolCall(
|
|
|
218
103
|
} catch (evErr) {
|
|
219
104
|
logger?.warn?.(`[PD_GATE] Failed to record rulehost_evaluated: ${String(evErr)}`);
|
|
220
105
|
}
|
|
106
|
+
|
|
221
107
|
if (hostResult?.decision === 'block') {
|
|
222
|
-
// C: Record rule_enforced event for matched rules
|
|
223
108
|
try {
|
|
224
109
|
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
225
110
|
eventLog.recordRuleEnforced({
|
|
@@ -236,18 +121,18 @@ export function handleBeforeToolCall(
|
|
|
236
121
|
ruleId: hostResult.ruleId,
|
|
237
122
|
});
|
|
238
123
|
} catch (evErr) {
|
|
239
|
-
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_blocked
|
|
124
|
+
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_blocked: ${String(evErr)}`);
|
|
240
125
|
}
|
|
241
126
|
|
|
242
|
-
const reason = hostResult.reason;
|
|
243
127
|
return recordGateBlockAndReturn(wctx, {
|
|
244
128
|
filePath: relPath,
|
|
245
|
-
reason,
|
|
129
|
+
reason: hostResult.reason,
|
|
246
130
|
toolName: event.toolName,
|
|
247
131
|
sessionId: ctx.sessionId,
|
|
248
132
|
blockSource: 'rule-host',
|
|
249
133
|
}, logger);
|
|
250
134
|
}
|
|
135
|
+
|
|
251
136
|
if (hostResult?.decision === 'requireApproval') {
|
|
252
137
|
try {
|
|
253
138
|
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
@@ -265,68 +150,12 @@ export function handleBeforeToolCall(
|
|
|
265
150
|
ruleId: hostResult.ruleId,
|
|
266
151
|
});
|
|
267
152
|
} catch (evErr) {
|
|
268
|
-
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval
|
|
153
|
+
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval: ${String(evErr)}`);
|
|
269
154
|
}
|
|
270
155
|
}
|
|
271
156
|
} catch (hostError: unknown) {
|
|
272
|
-
// D-08: Conservative degradation — log and
|
|
273
|
-
logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed,
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
277
|
-
// POLICY STEP 3: Progressive Trust Gate (Stage 1-4 access control)
|
|
278
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
279
|
-
// IMPORTANT: This step does NOT return early on allow.
|
|
280
|
-
// We must continue to edit verification for ALL allowed operations.
|
|
281
|
-
if (profile.progressive_gate?.enabled) {
|
|
282
|
-
const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
|
|
283
|
-
const progressiveGateResult = checkProgressiveTrustGate(
|
|
284
|
-
event,
|
|
285
|
-
wctx,
|
|
286
|
-
relPath,
|
|
287
|
-
risky,
|
|
288
|
-
lineChanges,
|
|
289
|
-
logger,
|
|
290
|
-
ctx,
|
|
291
|
-
profile
|
|
292
|
-
);
|
|
293
|
-
if (progressiveGateResult) {
|
|
294
|
-
return progressiveGateResult;
|
|
295
|
-
}
|
|
296
|
-
// NOTE: Do NOT return here! Continue to edit verification.
|
|
297
|
-
// All allowed operations (regardless of EP tier) should still run edit verification.
|
|
298
|
-
} else {
|
|
299
|
-
// FALLBACK: Legacy Gate Logic (when progressive gate is disabled)
|
|
300
|
-
if (risky && profile.gate?.require_plan_for_risk_paths) {
|
|
301
|
-
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
302
|
-
if (planStatus !== 'READY') {
|
|
303
|
-
return recordGateBlockAndReturn(wctx, {
|
|
304
|
-
filePath: relPath,
|
|
305
|
-
reason: `No READY plan found in PLAN.md.`,
|
|
306
|
-
toolName: event.toolName,
|
|
307
|
-
sessionId: ctx.sessionId,
|
|
308
|
-
blockSource: 'gate-legacy',
|
|
309
|
-
}, logger);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
315
|
-
// POLICY STEP 4: Edit Tool Verification (P-03)
|
|
316
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
317
|
-
// This MUST run after all other gate checks for ALL tools.
|
|
318
|
-
// Edit verification ensures oldText matches the actual file content.
|
|
319
|
-
if (event.toolName === 'edit' && profile.edit_verification?.enabled !== false) {
|
|
320
|
-
const verifyResult = handleEditVerification(event, wctx, ctx, {
|
|
321
|
-
enabled: profile.edit_verification.enabled,
|
|
322
|
-
max_file_size_bytes: profile.edit_verification.max_file_size_bytes,
|
|
323
|
-
fuzzy_match_enabled: profile.edit_verification.fuzzy_match_enabled,
|
|
324
|
-
fuzzy_match_threshold: profile.edit_verification.fuzzy_match_threshold,
|
|
325
|
-
skip_large_file_action: profile.edit_verification.skip_large_file_action as 'warn' | 'block' | undefined,
|
|
326
|
-
});
|
|
327
|
-
if (verifyResult) {
|
|
328
|
-
return verifyResult; // Block or modify params
|
|
329
|
-
}
|
|
157
|
+
// D-08: Conservative degradation — log and allow on Rule Host failure
|
|
158
|
+
logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, allowing conservatively: ${String(hostError)}`);
|
|
330
159
|
}
|
|
331
160
|
|
|
332
161
|
// All checks passed - allow the operation
|
|
@@ -334,9 +163,7 @@ export function handleBeforeToolCall(
|
|
|
334
163
|
}
|
|
335
164
|
|
|
336
165
|
// ---------------------------------------------------------------------------
|
|
337
|
-
// Private helpers
|
|
338
|
-
// These are NOT passed to hosted implementations — they only populate the
|
|
339
|
-
// frozen snapshot that implementations receive.
|
|
166
|
+
// Private helpers
|
|
340
167
|
// ---------------------------------------------------------------------------
|
|
341
168
|
|
|
342
169
|
function _extractParamsSummary(params: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -352,7 +179,7 @@ function _extractParamsSummary(params: Record<string, unknown>): Record<string,
|
|
|
352
179
|
|
|
353
180
|
function _getPlanStatus(workspaceDir: string): 'NONE' | 'DRAFT' | 'READY' | 'UNKNOWN' {
|
|
354
181
|
try {
|
|
355
|
-
const status =
|
|
182
|
+
const status = planStatus(workspaceDir);
|
|
356
183
|
if (status === 'READY') return 'READY';
|
|
357
184
|
if (status === 'DRAFT') return 'DRAFT';
|
|
358
185
|
if (status === '') return 'NONE';
|
|
@@ -397,12 +224,7 @@ function _getEpTier(workspaceDir: string): number {
|
|
|
397
224
|
}
|
|
398
225
|
}
|
|
399
226
|
|
|
400
|
-
|
|
401
|
-
function _getBashRisk(
|
|
402
|
-
event: PluginHookBeforeToolCallEvent,
|
|
403
|
-
_profile: { risk_paths: string[] }
|
|
404
|
-
): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
405
|
-
|
|
227
|
+
function _getBashRisk(event: PluginHookBeforeToolCallEvent): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
406
228
|
if (!BASH_TOOLS_SET.has(event.toolName)) return 'unknown';
|
|
407
229
|
try {
|
|
408
230
|
const command = String(event.params.command || event.params.args || '');
|