principles-disciple 1.5.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/commands/context.d.ts +5 -0
  2. package/dist/commands/context.js +312 -0
  3. package/dist/commands/evolution-status.d.ts +4 -0
  4. package/dist/commands/evolution-status.js +138 -0
  5. package/dist/commands/export.d.ts +2 -0
  6. package/dist/commands/export.js +45 -0
  7. package/dist/commands/focus.d.ts +14 -0
  8. package/dist/commands/focus.js +582 -0
  9. package/dist/commands/pain.js +143 -6
  10. package/dist/commands/principle-rollback.d.ts +4 -0
  11. package/dist/commands/principle-rollback.js +22 -0
  12. package/dist/commands/rollback.d.ts +19 -0
  13. package/dist/commands/rollback.js +119 -0
  14. package/dist/commands/samples.d.ts +2 -0
  15. package/dist/commands/samples.js +55 -0
  16. package/dist/core/config.d.ts +37 -0
  17. package/dist/core/config.js +47 -0
  18. package/dist/core/control-ui-db.d.ts +68 -0
  19. package/dist/core/control-ui-db.js +274 -0
  20. package/dist/core/detection-funnel.d.ts +1 -1
  21. package/dist/core/detection-funnel.js +4 -0
  22. package/dist/core/dictionary.d.ts +2 -0
  23. package/dist/core/dictionary.js +13 -0
  24. package/dist/core/event-log.d.ts +22 -1
  25. package/dist/core/event-log.js +319 -0
  26. package/dist/core/evolution-engine.d.ts +5 -5
  27. package/dist/core/evolution-engine.js +18 -18
  28. package/dist/core/evolution-migration.d.ts +5 -0
  29. package/dist/core/evolution-migration.js +65 -0
  30. package/dist/core/evolution-reducer.d.ts +69 -0
  31. package/dist/core/evolution-reducer.js +369 -0
  32. package/dist/core/evolution-types.d.ts +103 -0
  33. package/dist/core/focus-history.d.ts +65 -0
  34. package/dist/core/focus-history.js +266 -0
  35. package/dist/core/init.js +30 -7
  36. package/dist/core/migration.js +0 -2
  37. package/dist/core/path-resolver.d.ts +3 -0
  38. package/dist/core/path-resolver.js +90 -31
  39. package/dist/core/paths.d.ts +7 -8
  40. package/dist/core/paths.js +48 -40
  41. package/dist/core/profile.js +1 -1
  42. package/dist/core/session-tracker.d.ts +4 -0
  43. package/dist/core/session-tracker.js +15 -0
  44. package/dist/core/thinking-models.d.ts +38 -0
  45. package/dist/core/thinking-models.js +170 -0
  46. package/dist/core/trajectory.d.ts +184 -0
  47. package/dist/core/trajectory.js +817 -0
  48. package/dist/core/trust-engine.d.ts +2 -0
  49. package/dist/core/trust-engine.js +30 -4
  50. package/dist/core/workspace-context.d.ts +13 -0
  51. package/dist/core/workspace-context.js +50 -7
  52. package/dist/hooks/gate.js +301 -30
  53. package/dist/hooks/llm.d.ts +8 -0
  54. package/dist/hooks/llm.js +347 -69
  55. package/dist/hooks/message-sanitize.d.ts +3 -0
  56. package/dist/hooks/message-sanitize.js +37 -0
  57. package/dist/hooks/pain.js +105 -5
  58. package/dist/hooks/prompt.d.ts +20 -11
  59. package/dist/hooks/prompt.js +558 -158
  60. package/dist/hooks/subagent.d.ts +9 -2
  61. package/dist/hooks/subagent.js +40 -3
  62. package/dist/http/principles-console-route.d.ts +2 -0
  63. package/dist/http/principles-console-route.js +257 -0
  64. package/dist/i18n/commands.js +48 -20
  65. package/dist/index.js +264 -8
  66. package/dist/service/control-ui-query-service.d.ts +217 -0
  67. package/dist/service/control-ui-query-service.js +537 -0
  68. package/dist/service/empathy-observer-manager.d.ts +42 -0
  69. package/dist/service/empathy-observer-manager.js +147 -0
  70. package/dist/service/evolution-worker.d.ts +10 -0
  71. package/dist/service/evolution-worker.js +156 -24
  72. package/dist/service/trajectory-service.d.ts +2 -0
  73. package/dist/service/trajectory-service.js +15 -0
  74. package/dist/tools/agent-spawn.d.ts +27 -6
  75. package/dist/tools/agent-spawn.js +339 -87
  76. package/dist/tools/deep-reflect.d.ts +27 -7
  77. package/dist/tools/deep-reflect.js +282 -113
  78. package/dist/types/event-types.d.ts +84 -2
  79. package/dist/types/event-types.js +33 -0
  80. package/dist/types.d.ts +52 -0
  81. package/dist/types.js +24 -1
  82. package/openclaw.plugin.json +43 -11
  83. package/package.json +16 -6
  84. package/templates/langs/zh/core/HEARTBEAT.md +28 -4
  85. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
  86. package/templates/pain_settings.json +54 -2
  87. package/templates/workspace/.principles/PROFILE.json +2 -0
  88. package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
@@ -43,6 +43,7 @@ export declare class TrustEngine {
43
43
  sessionId?: string;
44
44
  api?: any;
45
45
  toolName?: string;
46
+ error?: string;
46
47
  }): void;
47
48
  private updateScore;
48
49
  resetTrust(newScore?: number): void;
@@ -67,6 +68,7 @@ export declare function recordFailure(type: 'tool' | 'risky' | 'bypass', workspa
67
68
  sessionId?: string;
68
69
  api?: any;
69
70
  toolName?: string;
71
+ error?: string;
70
72
  }): void;
71
73
  export declare function getAgentScorecard(workspaceDir: string): TrustScorecard;
72
74
  export declare function getTrustStats(scorecard: TrustScorecard): {
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import { EventLogService } from './event-log.js';
8
8
  import { resolvePdPath } from './paths.js';
9
9
  import { ConfigService } from './config-service.js';
10
+ import { TrajectoryRegistry } from './trajectory.js';
10
11
  export const EXPLORATORY_TOOLS = [
11
12
  // 文件读取
12
13
  'read', 'read_file', 'read_many_files', 'image_read',
@@ -26,7 +27,7 @@ export const EXPLORATORY_TOOLS = [
26
27
  export const CONSTRUCTIVE_TOOLS = [
27
28
  'write', 'write_file', 'edit', 'edit_file', 'replace', 'apply_patch',
28
29
  'insert', 'patch', 'delete_file', 'move_file', 'run_shell_command',
29
- 'pd_spawn_agent', 'sessions_spawn', 'evolve-task', 'init-strategy'
30
+ 'pd_run_worker', 'sessions_spawn', 'evolve-task', 'init-strategy'
30
31
  ];
31
32
  export class TrustEngine {
32
33
  scorecard;
@@ -49,7 +50,8 @@ export class TrustEngine {
49
50
  return settings || {
50
51
  stages: { stage_1_observer: 30, stage_2_editor: 60, stage_3_developer: 80 },
51
52
  cold_start: { initial_trust: 85, grace_failures: 5, cold_start_period_ms: 86400000 },
52
- penalties: { tool_failure_base: -2, risky_failure_base: -10, gate_bypass_attempt: -5, failure_streak_multiplier: -2, max_penalty: -20 },
53
+ // BUGFIX #84: Reduced penalties to prevent Trust collapse
54
+ penalties: { tool_failure_base: -1, risky_failure_base: -5, gate_bypass_attempt: -3, failure_streak_multiplier: -1, max_penalty: -10 },
53
55
  rewards: { success_base: 2, subagent_success: 5, tool_success_reward: 0.2, streak_bonus_threshold: 3, streak_bonus: 5, recovery_boost: 5, max_reward: 15 },
54
56
  limits: { stage_2_max_lines: 50, stage_3_max_lines: 300 }
55
57
  };
@@ -170,6 +172,13 @@ export class TrustEngine {
170
172
  this.updateScore(-1, `Exploratory Failure: ${toolName}`, 'failure', context);
171
173
  return;
172
174
  }
175
+ // BUGFIX #84: sessions_send timeout should not be penalized
176
+ // Communication timeouts are not agent failures - the message may have been delivered
177
+ const errorStr = String(context?.error || '');
178
+ if (toolName === 'sessions_send' && (errorStr.includes('timeout') || errorStr === 'timeout')) {
179
+ this.updateScore(0, `Communication timeout (sessions_send): ignored`, 'info', context);
180
+ return;
181
+ }
173
182
  // 3. Constructive Failure (Risky or failed writes)
174
183
  let delta = 0;
175
184
  switch (type) {
@@ -198,8 +207,9 @@ export class TrustEngine {
198
207
  updateScore(delta, reason, type, context) {
199
208
  const oldScore = this.scorecard.trust_score;
200
209
  this.scorecard.trust_score += delta;
201
- if (this.scorecard.trust_score < 0)
202
- this.scorecard.trust_score = 0;
210
+ // Floor score: never drop below 30 (prevents Trust collapse from cascades)
211
+ if (this.scorecard.trust_score < 30)
212
+ this.scorecard.trust_score = 30;
203
213
  if (this.scorecard.trust_score > 100)
204
214
  this.scorecard.trust_score = 100;
205
215
  this.scorecard.last_updated = new Date().toISOString();
@@ -210,6 +220,22 @@ export class TrustEngine {
210
220
  const eventLog = EventLogService.get(this.stateDir);
211
221
  eventLog.recordTrustChange(context.sessionId, { previousScore: oldScore, newScore: this.scorecard.trust_score, delta, reason });
212
222
  }
223
+ if (context?.sessionId) {
224
+ try {
225
+ TrajectoryRegistry.use(this.workspaceDir, (trajectory) => {
226
+ trajectory.recordTrustChange({
227
+ sessionId: context.sessionId,
228
+ previousScore: oldScore,
229
+ newScore: this.scorecard.trust_score,
230
+ delta,
231
+ reason,
232
+ });
233
+ });
234
+ }
235
+ catch {
236
+ // Do not block trust updates if trajectory storage is unavailable.
237
+ }
238
+ }
213
239
  const limit = this.trustSettings.history_limit || 50;
214
240
  if (this.scorecard.history.length > limit) {
215
241
  this.scorecard.history.shift();
@@ -4,6 +4,8 @@ import { EventLog } from './event-log.js';
4
4
  import { PainDictionary } from './dictionary.js';
5
5
  import { TrustEngine } from './trust-engine.js';
6
6
  import { HygieneTracker } from './hygiene/tracker.js';
7
+ import { EvolutionReducerImpl } from './evolution-reducer.js';
8
+ import { TrajectoryDatabase } from './trajectory.js';
7
9
  /**
8
10
  * WorkspaceContext - Centralized management of workspace-specific paths and services.
9
11
  * Implements a cached singleton pattern per workspace directory.
@@ -18,6 +20,8 @@ export declare class WorkspaceContext {
18
20
  private _dictionary?;
19
21
  private _trust?;
20
22
  private _hygiene?;
23
+ private _evolutionReducer?;
24
+ private _trajectory?;
21
25
  private constructor();
22
26
  /**
23
27
  * Governance configuration for this workspace.
@@ -39,6 +43,15 @@ export declare class WorkspaceContext {
39
43
  * Hygiene tracking service for this workspace.
40
44
  */
41
45
  get hygiene(): HygieneTracker;
46
+ /**
47
+ * Evolution reducer singleton for this workspace.
48
+ */
49
+ get evolutionReducer(): EvolutionReducerImpl;
50
+ /**
51
+ * Trajectory database for analytics and sample curation.
52
+ */
53
+ get trajectory(): TrajectoryDatabase;
54
+ private getTrajectoryOptions;
42
55
  /**
43
56
  * Creates or retrieves a WorkspaceContext instance from an OpenClaw hook context.
44
57
  * Uses PathResolver to handle path normalization and fallback logic.
@@ -5,6 +5,8 @@ import { EventLogService } from './event-log.js';
5
5
  import { DictionaryService } from './dictionary-service.js';
6
6
  import { TrustEngine } from './trust-engine.js';
7
7
  import { HygieneTracker } from './hygiene/tracker.js';
8
+ import { EvolutionReducerImpl } from './evolution-reducer.js';
9
+ import { TrajectoryRegistry } from './trajectory.js';
8
10
  /**
9
11
  * WorkspaceContext - Centralized management of workspace-specific paths and services.
10
12
  * Implements a cached singleton pattern per workspace directory.
@@ -19,6 +21,8 @@ export class WorkspaceContext {
19
21
  _dictionary;
20
22
  _trust;
21
23
  _hygiene;
24
+ _evolutionReducer;
25
+ _trajectory;
22
26
  constructor(workspaceDir, stateDir) {
23
27
  this.workspaceDir = workspaceDir;
24
28
  this.stateDir = stateDir;
@@ -68,22 +72,53 @@ export class WorkspaceContext {
68
72
  }
69
73
  return this._hygiene;
70
74
  }
75
+ /**
76
+ * Evolution reducer singleton for this workspace.
77
+ */
78
+ get evolutionReducer() {
79
+ if (!this._evolutionReducer) {
80
+ this._evolutionReducer = new EvolutionReducerImpl({ workspaceDir: this.workspaceDir });
81
+ }
82
+ return this._evolutionReducer;
83
+ }
84
+ /**
85
+ * Trajectory database for analytics and sample curation.
86
+ */
87
+ get trajectory() {
88
+ if (!this._trajectory) {
89
+ this._trajectory = TrajectoryRegistry.get(this.workspaceDir, this.getTrajectoryOptions());
90
+ }
91
+ return this._trajectory;
92
+ }
93
+ getTrajectoryOptions() {
94
+ const inlineThreshold = Number(this.config.get('trajectory.blob_inline_threshold_bytes'));
95
+ const busyTimeoutMs = Number(this.config.get('trajectory.busy_timeout_ms'));
96
+ const orphanBlobGraceDays = Number(this.config.get('trajectory.orphan_blob_grace_days'));
97
+ return {
98
+ blobInlineThresholdBytes: Number.isFinite(inlineThreshold) && inlineThreshold > 0 ? inlineThreshold : undefined,
99
+ busyTimeoutMs: Number.isFinite(busyTimeoutMs) && busyTimeoutMs >= 0 ? busyTimeoutMs : undefined,
100
+ orphanBlobGraceDays: Number.isFinite(orphanBlobGraceDays) && orphanBlobGraceDays >= 0 ? orphanBlobGraceDays : undefined,
101
+ };
102
+ }
71
103
  /**
72
104
  * Creates or retrieves a WorkspaceContext instance from an OpenClaw hook context.
73
105
  * Uses PathResolver to handle path normalization and fallback logic.
74
106
  * @throws Error if workspaceDir is missing and no fallback available.
75
107
  */
76
108
  static fromHookContext(ctx) {
109
+ const logger = ctx.logger;
110
+ const log = (msg) => logger?.info?.(msg) ?? console.log(msg);
111
+ const logWarn = (msg) => logger?.warn?.(msg) ?? console.warn(msg);
77
112
  let workspaceDir = ctx.workspaceDir;
78
113
  if (!workspaceDir) {
79
- console.warn('[PD:WorkspaceContext] workspaceDir not provided in context, using PathResolver fallback');
114
+ logWarn('[PD:WorkspaceContext] workspaceDir not provided in context, using PathResolver fallback');
80
115
  workspaceDir = this.pathResolver.getWorkspaceDir();
81
- console.log(`[PD:WorkspaceContext] Resolved workspaceDir to: ${workspaceDir}`);
116
+ log(`[PD:WorkspaceContext] Resolved workspaceDir to: ${workspaceDir}`);
82
117
  }
83
118
  else {
84
119
  const normalized = this.pathResolver.normalizeWorkspacePath(workspaceDir);
85
120
  if (normalized !== workspaceDir) {
86
- console.log(`[PD:WorkspaceContext] Normalized workspaceDir: ${workspaceDir} -> ${normalized}`);
121
+ log(`[PD:WorkspaceContext] Normalized workspaceDir: ${workspaceDir} -> ${normalized}`);
87
122
  workspaceDir = normalized;
88
123
  }
89
124
  }
@@ -93,11 +128,11 @@ export class WorkspaceContext {
93
128
  let stateDir = ctx.stateDir;
94
129
  if (!stateDir) {
95
130
  stateDir = resolvePdPath(workspaceDir, 'STATE_DIR');
96
- console.log(`[PD:WorkspaceContext] Computed stateDir: ${stateDir}`);
131
+ log(`[PD:WorkspaceContext] Computed stateDir: ${stateDir}`);
97
132
  }
98
133
  const instance = new WorkspaceContext(workspaceDir, stateDir);
99
134
  this.instances.set(workspaceDir, instance);
100
- console.log(`[PD:WorkspaceContext] Created new context for workspace: ${workspaceDir}`);
135
+ log(`[PD:WorkspaceContext] Created new context for workspace: ${workspaceDir}`);
101
136
  return instance;
102
137
  }
103
138
  /**
@@ -114,21 +149,29 @@ export class WorkspaceContext {
114
149
  this._eventLog = undefined;
115
150
  this._dictionary = undefined;
116
151
  this._trust = undefined;
152
+ this._evolutionReducer = undefined;
153
+ this._trajectory = undefined;
117
154
  }
118
155
  /**
119
156
  * Removes a workspace from the cache.
120
157
  */
121
158
  static dispose(workspaceDir) {
122
- const instance = this.instances.get(workspaceDir);
159
+ const normalized = this.pathResolver.normalizeWorkspacePath(workspaceDir);
160
+ const instance = this.instances.get(normalized);
123
161
  if (instance) {
124
162
  instance.invalidate();
125
- this.instances.delete(workspaceDir);
163
+ this.instances.delete(normalized);
126
164
  }
165
+ TrajectoryRegistry.dispose(normalized);
127
166
  }
128
167
  /**
129
168
  * Clears the instance cache (primarily for testing).
130
169
  */
131
170
  static clearCache() {
171
+ for (const instance of this.instances.values()) {
172
+ instance.invalidate();
173
+ }
132
174
  this.instances.clear();
175
+ TrajectoryRegistry.clear();
133
176
  }
134
177
  }
@@ -3,16 +3,122 @@ import * as path from 'path';
3
3
  import { isRisky, normalizePath, planStatus as getPlanStatus } from '../utils/io.js';
4
4
  import { matchesAnyPattern } from '../utils/glob-match.js';
5
5
  import { normalizeProfile } from '../core/profile.js';
6
- import { trackBlock, hasRecentThinking } from '../core/session-tracker.js';
6
+ import { trackBlock, hasRecentThinking, getSession } from '../core/session-tracker.js';
7
7
  import { assessRiskLevel, estimateLineChanges } from '../core/risk-calculator.js';
8
8
  import { WorkspaceContext } from '../core/workspace-context.js';
9
9
  import { checkEvolutionGate } from '../core/evolution-engine.js';
10
+ import { EventLogService } from '../core/event-log.js';
11
+ // ═══ GFI Gate Tool Tiers ═══
12
+ // TIER 0: 只读工具 - 永不拦截
13
+ const READ_ONLY_TOOLS = new Set([
14
+ 'read', 'read_file', 'read_many_files', 'image_read',
15
+ 'search_file_content', 'grep', 'grep_search', 'list_directory', 'ls', 'glob',
16
+ 'lsp_hover', 'lsp_goto_definition', 'lsp_find_references',
17
+ 'web_fetch', 'web_search', 'ref_search_documentation', 'ref_read_url',
18
+ 'resolve-library-id', 'get-library-docs',
19
+ 'todo_read', 'save_memory',
20
+ 'deep_reflect',
21
+ ]);
22
+ // TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
23
+ // 注意:pd_run_worker、sessions_spawn、task 是 Agent 派生工具,不应被 GFI Gate 拦截
24
+ // 它们属于 AGENT_TOOLS,在早期过滤后直接放行
25
+ const LOW_RISK_WRITE_TOOLS = new Set([
26
+ 'write', 'write_file',
27
+ 'edit', 'edit_file', 'replace', 'apply_patch', 'insert', 'patch',
28
+ ]);
29
+ // TIER 2: 高风险操作 - GFI >= high_risk_block 时拦截
30
+ const HIGH_RISK_TOOLS = new Set([
31
+ 'delete_file', 'move_file',
32
+ ]);
33
+ // TIER 3: Bash 命令 - 根据内容判断
34
+ const BASH_TOOLS_SET = new Set([
35
+ 'bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd',
36
+ ]);
37
+ /**
38
+ * 分析 Bash 命令风险等级
39
+ */
40
+ function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
41
+ let normalizedCmd = command.trim().toLowerCase();
42
+ // P2 fix: Unicode de-obfuscation — convert Cyrillic/Unicode lookalikes to ASCII equivalents
43
+ // Common Cyrillic lookalikes that could bypass detection: аеорсух (Cyrillic) → aeopcyx (Latin)
44
+ const CYRILLIC_TO_LATIN = {
45
+ 'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'у': 'y', 'х': 'x',
46
+ 'А': 'a', 'Е': 'e', 'О': 'o', 'Р': 'p', 'С': 'c', 'У': 'y', 'Х': 'x',
47
+ // Additional confusable chars
48
+ 'і': 'i', 'ј': 'j', 'ѕ': 's', 'ԁ': 'd', 'ɡ': 'g', 'һ': 'h', 'ⅰ': 'i',
49
+ 'ƚ': 'l', 'м': 'm', 'п': 'n', 'ѵ': 'v', 'ѡ': 'w', 'ᴦ': 'r', 'ꜱ': 's',
50
+ };
51
+ normalizedCmd = normalizedCmd.replace(/[а-яА-Яіјѕԁɡһⅰƚмпеꜱѵѡᴦꜱ]/g, m => CYRILLIC_TO_LATIN[m] ?? m);
52
+ // P2 fix: Tokenize command chain before pattern matching to catch `cmd1 && cmd2` bypasses
53
+ // Only split on statement separators (; && ||), NOT on pipe (|) which is part of the command
54
+ const tokens = normalizedCmd
55
+ .split(/\s*(?:;|&&|\|\|)\s*/)
56
+ .map(t => t.trim())
57
+ .filter(t => t.length > 0);
58
+ // If no tokens (e.g., pure pipe-only), use the original
59
+ const segments = tokens.length > 0 ? tokens : [normalizedCmd];
60
+ // P2 fix: Also strip outer $() and backticks from each segment
61
+ const cleanSegments = segments.map(seg => {
62
+ let s = seg;
63
+ // Strip leading $() or ${} or backtick-wrapped commands
64
+ s = s.replace(/^\$\([^)]+\)$/, '').replace(/^\$\{[^}]+\}$/, '').replace(/^`([^`]+)`$/, '$1');
65
+ return s.trim();
66
+ }).filter(s => s.length > 0);
67
+ // 1. Check dangerous patterns against each segment
68
+ for (const seg of cleanSegments) {
69
+ for (const pattern of dangerousPatterns) {
70
+ try {
71
+ if (new RegExp(pattern, 'i').test(seg)) {
72
+ return 'dangerous';
73
+ }
74
+ }
75
+ catch {
76
+ // 忽略无效正则
77
+ }
78
+ }
79
+ }
80
+ // 2. Check safe patterns (only if ALL segments are safe)
81
+ for (const seg of cleanSegments) {
82
+ let isSafe = false;
83
+ for (const pattern of safePatterns) {
84
+ try {
85
+ if (new RegExp(pattern, 'i').test(seg)) {
86
+ isSafe = true;
87
+ break;
88
+ }
89
+ }
90
+ catch {
91
+ // ignore
92
+ }
93
+ }
94
+ if (!isSafe) {
95
+ // Not all segments are safe → treat as normal
96
+ return 'normal';
97
+ }
98
+ }
99
+ // All segments are safe
100
+ return 'safe';
101
+ }
102
+ /**
103
+ * 计算动态 GFI 阈值
104
+ */
105
+ function calculateDynamicThreshold(baseThreshold, trustStage, lineChanges, config) {
106
+ // 1. Trust Stage 乘数
107
+ const stageMultiplier = config.trust_stage_multipliers[trustStage.toString()] || 1.0;
108
+ let threshold = baseThreshold * stageMultiplier;
109
+ // 2. 大规模修改降低阈值
110
+ if (lineChanges > config.large_change_lines) {
111
+ const ratio = Math.min(lineChanges / 200, 0.5); // 最多降低 50%
112
+ threshold = threshold * (1 - ratio);
113
+ }
114
+ return Math.round(Math.max(threshold, 0));
115
+ }
10
116
  export function handleBeforeToolCall(event, ctx) {
11
117
  const logger = ctx.logger || console;
12
118
  // 1. Identify tool type
13
119
  const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'replace', 'insert', 'patch', 'edit_file', 'delete_file', 'move_file'];
14
120
  const BASH_TOOLS = ['bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd'];
15
- const AGENT_TOOLS = ['pd_spawn_agent', 'sessions_spawn'];
121
+ const AGENT_TOOLS = ['pd_run_worker', 'sessions_spawn'];
16
122
  const isBash = BASH_TOOLS.includes(event.toolName);
17
123
  const isWriteTool = WRITE_TOOLS.includes(event.toolName);
18
124
  const isAgentTool = AGENT_TOOLS.includes(event.toolName);
@@ -45,7 +151,7 @@ export function handleBeforeToolCall(event, ctx) {
45
151
  thinking_checkpoint: {
46
152
  enabled: false, // Default OFF
47
153
  window_ms: 5 * 60 * 1000,
48
- high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_spawn_agent'],
154
+ high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_run_worker'],
49
155
  }
50
156
  };
51
157
  if (fs.existsSync(profilePath)) {
@@ -71,6 +177,158 @@ export function handleBeforeToolCall(event, ctx) {
71
177
  };
72
178
  }
73
179
  }
180
+ // ═══ GFI GATE - Hard Intercept ═══
181
+ // 根据 GFI (疲劳指数) 精细化拦截工具调用
182
+ // 注意:TIER 0 (只读工具) 已在早期过滤中放行,此处不检查
183
+ const gfiGateConfig = wctx.config.get('gfi_gate');
184
+ if (gfiGateConfig?.enabled !== false && ctx.sessionId) {
185
+ const session = getSession(ctx.sessionId);
186
+ const currentGfi = session?.currentGfi || 0;
187
+ // TIER 3: Bash 命令 - 根据内容判断
188
+ if (BASH_TOOLS_SET.has(event.toolName)) {
189
+ const command = String(event.params.command || event.params.args || '');
190
+ const bashRisk = analyzeBashCommand(command, gfiGateConfig?.bash_safe_patterns || [], gfiGateConfig?.bash_dangerous_patterns || []);
191
+ if (bashRisk === 'dangerous') {
192
+ // 危险命令 - 直接拦截
193
+ logger?.warn?.(`[PD:GFI_GATE] Dangerous bash command blocked: ${command.substring(0, 50)}...`);
194
+ return {
195
+ block: true,
196
+ blockReason: `[GFI Gate] 危险命令被拦截。
197
+
198
+ 命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}
199
+
200
+ 原因: 检测到危险命令模式,需要确认执行意图。
201
+
202
+ 解决方案:
203
+ 1. 如果确实需要执行,请确认操作意图后重试
204
+ 2. 使用更安全的方式(如手动操作)
205
+ 3. 咨询用户确认是否继续
206
+
207
+ 注意: 危险命令需要更严格的审批流程。`,
208
+ };
209
+ }
210
+ // safe 命令 - 放行
211
+ else if (bashRisk === 'safe') {
212
+ // 继续执行
213
+ }
214
+ // normal 命令 - 按 GFI 阈值判断
215
+ else {
216
+ const trustEngine = wctx.trust;
217
+ const stage = trustEngine.getStage();
218
+ const baseThreshold = gfiGateConfig?.thresholds?.low_risk_block || 70;
219
+ const dynamicThreshold = calculateDynamicThreshold(baseThreshold, stage, 0, // bash 命令没有行数概念
220
+ {
221
+ large_change_lines: gfiGateConfig?.large_change_lines || 50,
222
+ trust_stage_multipliers: gfiGateConfig?.trust_stage_multipliers || { '1': 0.5, '2': 0.75, '3': 1.0, '4': 1.5 },
223
+ });
224
+ if (currentGfi >= dynamicThreshold) {
225
+ logger?.warn?.(`[PD:GFI_GATE] Bash blocked by GFI: ${currentGfi} >= ${dynamicThreshold}`);
226
+ return {
227
+ block: true,
228
+ blockReason: `[GFI Gate] 疲劳指数过高,操作被拦截。
229
+
230
+ 命令: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}
231
+ GFI: ${currentGfi}/100
232
+ 动态阈值: ${dynamicThreshold} (Stage ${stage})
233
+
234
+ 原因: 当前疲劳指数超过阈值,系统进入保护模式。
235
+
236
+ 解决方案:
237
+ 1. 执行 /pd-status reset 清零疲劳值
238
+ 2. 检查是否存在理解偏差或死循环
239
+ 3. 等待问题自然解决后再尝试
240
+
241
+ 注意: 这是系统级硬性拦截,AI 无法绕过。`,
242
+ };
243
+ }
244
+ }
245
+ }
246
+ // TIER 2: 高风险操作 - GFI >= high_risk_block 时拦截
247
+ else if (HIGH_RISK_TOOLS.has(event.toolName)) {
248
+ const trustEngine = wctx.trust;
249
+ const stage = trustEngine.getStage();
250
+ const baseThreshold = gfiGateConfig?.thresholds?.high_risk_block || 40;
251
+ const dynamicThreshold = calculateDynamicThreshold(baseThreshold, stage, 0, {
252
+ large_change_lines: gfiGateConfig?.large_change_lines || 50,
253
+ trust_stage_multipliers: gfiGateConfig?.trust_stage_multipliers || { '1': 0.5, '2': 0.75, '3': 1.0, '4': 1.5 },
254
+ });
255
+ if (currentGfi >= dynamicThreshold) {
256
+ const filePath = event.params.file_path || event.params.path || event.params.file || event.params.target || 'unknown';
257
+ logger?.warn?.(`[PD:GFI_GATE] High-risk tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${dynamicThreshold}`);
258
+ return {
259
+ block: true,
260
+ blockReason: `[GFI Gate] 高风险操作被拦截。
261
+
262
+ 工具: ${event.toolName}
263
+ 文件: ${filePath}
264
+ GFI: ${currentGfi}/100
265
+ 动态阈值: ${dynamicThreshold} (Stage ${stage})
266
+
267
+ 原因: 高风险工具需要更低的 GFI 阈值才能执行。
268
+
269
+ 解决方案:
270
+ 1. 执行 /pd-status reset 清零疲劳值
271
+ 2. 检查是否存在理解偏差或死循环
272
+ 3. 等待 GFI 自然衰减后重试
273
+
274
+ 注意: 这是系统级硬性拦截,AI 无法绕过。`,
275
+ };
276
+ }
277
+ }
278
+ // TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
279
+ else if (LOW_RISK_WRITE_TOOLS.has(event.toolName)) {
280
+ const trustEngine = wctx.trust;
281
+ const stage = trustEngine.getStage();
282
+ const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
283
+ const baseThreshold = gfiGateConfig?.thresholds?.low_risk_block || 70;
284
+ const dynamicThreshold = calculateDynamicThreshold(baseThreshold, stage, lineChanges, {
285
+ large_change_lines: gfiGateConfig?.large_change_lines || 50,
286
+ trust_stage_multipliers: gfiGateConfig?.trust_stage_multipliers || { '1': 0.5, '2': 0.75, '3': 1.0, '4': 1.5 },
287
+ });
288
+ if (currentGfi >= dynamicThreshold) {
289
+ const filePath = event.params.file_path || event.params.path || event.params.file || event.params.target || 'unknown';
290
+ logger?.warn?.(`[PD:GFI_GATE] Low-risk tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${dynamicThreshold}`);
291
+ return {
292
+ block: true,
293
+ blockReason: `[GFI Gate] 疲劳指数过高,操作被拦截。
294
+
295
+ 工具: ${event.toolName}
296
+ 文件: ${filePath}
297
+ GFI: ${currentGfi}/100
298
+ 动态阈值: ${dynamicThreshold} (Stage ${stage}${lineChanges > 50 ? `, ${lineChanges}行修改` : ''})
299
+
300
+ 原因: 当前疲劳指数超过阈值,系统进入保护模式。
301
+
302
+ 解决方案:
303
+ 1. 执行 /pd-status reset 清零疲劳值
304
+ 2. 检查是否存在理解偏差或死循环
305
+ 3. 等待问题自然解决后再尝试
306
+
307
+ 注意: 这是系统级硬性拦截,AI 无法绕过。`,
308
+ };
309
+ }
310
+ }
311
+ // AGENT_TOOLS: Block subagent spawn when GFI is critically high (P0 fix: prevent privilege escalation via spawned subagents)
312
+ if (isAgentTool) {
313
+ const AGENT_SPAWN_GFI_THRESHOLD = 90;
314
+ if (currentGfi >= AGENT_SPAWN_GFI_THRESHOLD) {
315
+ logger?.warn?.(`[PD:GFI_GATE] Agent tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${AGENT_SPAWN_GFI_THRESHOLD}`);
316
+ return {
317
+ block: true,
318
+ blockReason: `[GFI Gate] 疲劳指数过高,禁止派生子智能体。
319
+
320
+ GFI: ${currentGfi}/100
321
+ 阈值: ${AGENT_SPAWN_GFI_THRESHOLD} (Stage ${wctx.trust.getStage()})
322
+
323
+ 原因: 高疲劳状态下派生子智能体会放大错误风险。
324
+
325
+ 解决方案:
326
+ 1. 执行 /pd-status reset 清零疲劳值
327
+ 2. 简化任务后重试`,
328
+ };
329
+ }
330
+ }
331
+ }
74
332
  // Merge pluginConfig (OpenClaw UI settings)
75
333
  const configRiskPaths = ctx.pluginConfig?.riskPaths ?? [];
76
334
  if (configRiskPaths.length > 0) {
@@ -112,37 +370,35 @@ export function handleBeforeToolCall(event, ctx) {
112
370
  };
113
371
  const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
114
372
  const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
373
+ const planApprovals = profile.progressive_gate?.plan_approvals;
374
+ const canUsePlanApproval = Boolean(stage === 1 &&
375
+ planApprovals?.enabled &&
376
+ getPlanStatus(ctx.workspaceDir) === 'READY' &&
377
+ planApprovals.allowed_operations?.includes(event.toolName) &&
378
+ matchesAnyPattern(relPath, planApprovals.allowed_patterns || []) &&
379
+ ((planApprovals.max_lines_override ?? -1) === -1 || lineChanges <= (planApprovals.max_lines_override ?? -1)));
115
380
  logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
116
381
  // Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
117
382
  if (stage === 1) {
383
+ if (canUsePlanApproval) {
384
+ const planStatus = 'READY';
385
+ wctx.eventLog.recordPlanApproval(ctx.sessionId, {
386
+ toolName: event.toolName,
387
+ filePath: relPath,
388
+ pattern: relPath,
389
+ planStatus
390
+ });
391
+ wctx.trajectory?.recordGateBlock?.({
392
+ sessionId: ctx.sessionId,
393
+ toolName: event.toolName,
394
+ filePath: relPath,
395
+ reason: 'plan_approval',
396
+ planStatus,
397
+ });
398
+ logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
399
+ return;
400
+ }
118
401
  if (risky || riskLevel !== 'LOW') {
119
- // Check if PLAN whitelist is enabled
120
- if (profile.progressive_gate?.plan_approvals?.enabled) {
121
- const planApprovals = profile.progressive_gate.plan_approvals;
122
- const planStatus = getPlanStatus(ctx.workspaceDir);
123
- // Must have READY plan
124
- if (planStatus === 'READY') {
125
- // Check operation type
126
- if (planApprovals.allowed_operations?.includes(event.toolName)) {
127
- // Check path pattern
128
- if (matchesAnyPattern(relPath, planApprovals.allowed_patterns || [])) {
129
- // Check line limit (if configured)
130
- const maxLines = planApprovals.max_lines_override ?? -1;
131
- if (maxLines === -1 || lineChanges <= maxLines) {
132
- // Record PLAN approval event
133
- wctx.eventLog.recordPlanApproval(ctx.sessionId, {
134
- toolName: event.toolName,
135
- filePath: relPath,
136
- pattern: relPath,
137
- planStatus
138
- });
139
- logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
140
- return; // Allow the operation
141
- }
142
- }
143
- }
144
- }
145
- }
146
402
  // Block if not approved by whitelist
147
403
  return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName);
148
404
  }
@@ -173,6 +429,21 @@ export function handleBeforeToolCall(event, ctx) {
173
429
  // Stage 4 (Architect): Full bypass
174
430
  if (stage === 4) {
175
431
  logger.info(`[PD_GATE] Trusted Architect bypass for ${relPath}`);
432
+ // Audit log for Stage 4 bypass (security traceability)
433
+ try {
434
+ const stateDir = wctx.resolve('STATE_DIR');
435
+ const eventLog = EventLogService.get(stateDir);
436
+ eventLog.recordGateBypass(ctx.sessionId, {
437
+ toolName: event.toolName,
438
+ filePath: relPath,
439
+ bypassType: 'stage4_architect',
440
+ trustScore,
441
+ trustStage: stage,
442
+ });
443
+ }
444
+ catch (auditErr) {
445
+ logger?.warn?.(`[PD_GATE] Failed to record Stage 4 bypass audit: ${String(auditErr)}`);
446
+ }
176
447
  return;
177
448
  }
178
449
  // ── EP SIMULATION MODE (M6验证) ──
@@ -1,4 +1,12 @@
1
1
  import { PluginHookLlmOutputEvent, PluginHookAgentContext } from '../openclaw-sdk.js';
2
+ export interface EmpathySignal {
3
+ detected: boolean;
4
+ severity: 'mild' | 'moderate' | 'severe';
5
+ confidence: number;
6
+ reason?: string;
7
+ mode?: 'structured' | 'legacy_tag';
8
+ }
9
+ export declare function extractEmpathySignal(text: string): EmpathySignal;
2
10
  export declare function handleLlmOutput(event: PluginHookLlmOutputEvent, ctx: PluginHookAgentContext & {
3
11
  workspaceDir?: string;
4
12
  }): void;