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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.62.0",
5
+ "version": "1.63.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.62.0",
3
+ "version": "1.63.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * PURPOSE: Provide ONE authoritative implementation for gate block persistence.
5
5
  *
6
- * All gate modules (progressive-trust-gate, gfi-gate, etc.) must use this
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 - Orchestration Layer
2
+ * Security Gate Hook - Rule Host Only
3
3
  *
4
- * HOOK CHAIN PRIORITY (short-circuits on first block):
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. 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
- * 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 { isRisky, normalizePath, planStatus as getPlanStatus } from '../utils/io.js';
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. Load Profile
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
- const hasRiskPath = profile.risk_paths.some(rp => command.includes(rp));
143
- const isMutation = /(?:>|>>|sed|rm|mv|mkdir|touch|cp|npm|yarn|pnpm|pip|cargo)/.test(command);
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: risky,
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
- // PD-FUNNEL-2.4: Always emit rulehost_evaluated on evaluate()
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 event: ${String(evErr)}`);
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 event: ${String(evErr)}`);
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 continue to Progressive Gate
273
- logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, degrading conservatively: ${String(hostError)}`);
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 for building RuleHostInput snapshot
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 = getPlanStatus(workspaceDir);
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 || '');