principles-disciple 1.7.5 → 1.7.8

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 (129) hide show
  1. package/dist/commands/context.js +5 -15
  2. package/dist/commands/evolution-status.js +29 -48
  3. package/dist/commands/export.js +61 -8
  4. package/dist/commands/nocturnal-review.d.ts +24 -0
  5. package/dist/commands/nocturnal-review.js +265 -0
  6. package/dist/commands/nocturnal-rollout.d.ts +27 -0
  7. package/dist/commands/nocturnal-rollout.js +671 -0
  8. package/dist/commands/nocturnal-train.d.ts +25 -0
  9. package/dist/commands/nocturnal-train.js +919 -0
  10. package/dist/commands/pain.js +8 -21
  11. package/dist/config/defaults/runtime.d.ts +40 -0
  12. package/dist/config/defaults/runtime.js +44 -0
  13. package/dist/config/errors.d.ts +84 -0
  14. package/dist/config/errors.js +94 -0
  15. package/dist/config/index.d.ts +7 -0
  16. package/dist/config/index.js +7 -0
  17. package/dist/constants/diagnostician.d.ts +0 -4
  18. package/dist/constants/diagnostician.js +0 -4
  19. package/dist/constants/tools.d.ts +2 -2
  20. package/dist/constants/tools.js +1 -1
  21. package/dist/core/adaptive-thresholds.d.ts +186 -0
  22. package/dist/core/adaptive-thresholds.js +300 -0
  23. package/dist/core/config.d.ts +2 -38
  24. package/dist/core/config.js +6 -61
  25. package/dist/core/control-ui-db.d.ts +27 -0
  26. package/dist/core/control-ui-db.js +18 -0
  27. package/dist/core/event-log.d.ts +1 -2
  28. package/dist/core/event-log.js +0 -3
  29. package/dist/core/evolution-engine.js +1 -21
  30. package/dist/core/evolution-reducer.d.ts +7 -1
  31. package/dist/core/evolution-reducer.js +56 -4
  32. package/dist/core/evolution-types.d.ts +61 -9
  33. package/dist/core/evolution-types.js +31 -9
  34. package/dist/core/external-training-contract.d.ts +276 -0
  35. package/dist/core/external-training-contract.js +269 -0
  36. package/dist/core/local-worker-routing.d.ts +175 -0
  37. package/dist/core/local-worker-routing.js +525 -0
  38. package/dist/core/model-deployment-registry.d.ts +218 -0
  39. package/dist/core/model-deployment-registry.js +503 -0
  40. package/dist/core/model-training-registry.d.ts +295 -0
  41. package/dist/core/model-training-registry.js +475 -0
  42. package/dist/core/nocturnal-arbiter.d.ts +159 -0
  43. package/dist/core/nocturnal-arbiter.js +534 -0
  44. package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
  45. package/dist/core/nocturnal-candidate-scoring.js +266 -0
  46. package/dist/core/nocturnal-compliance.d.ts +175 -0
  47. package/dist/core/nocturnal-compliance.js +824 -0
  48. package/dist/core/nocturnal-dataset.d.ts +224 -0
  49. package/dist/core/nocturnal-dataset.js +443 -0
  50. package/dist/core/nocturnal-executability.d.ts +85 -0
  51. package/dist/core/nocturnal-executability.js +331 -0
  52. package/dist/core/nocturnal-export.d.ts +124 -0
  53. package/dist/core/nocturnal-export.js +275 -0
  54. package/dist/core/nocturnal-paths.d.ts +124 -0
  55. package/dist/core/nocturnal-paths.js +214 -0
  56. package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
  57. package/dist/core/nocturnal-trajectory-extractor.js +307 -0
  58. package/dist/core/nocturnal-trinity.d.ts +311 -0
  59. package/dist/core/nocturnal-trinity.js +880 -0
  60. package/dist/core/path-resolver.js +2 -1
  61. package/dist/core/paths.d.ts +6 -0
  62. package/dist/core/paths.js +6 -0
  63. package/dist/core/principle-training-state.d.ts +121 -0
  64. package/dist/core/principle-training-state.js +321 -0
  65. package/dist/core/promotion-gate.d.ts +238 -0
  66. package/dist/core/promotion-gate.js +529 -0
  67. package/dist/core/session-tracker.d.ts +10 -0
  68. package/dist/core/session-tracker.js +14 -0
  69. package/dist/core/shadow-observation-registry.d.ts +217 -0
  70. package/dist/core/shadow-observation-registry.js +308 -0
  71. package/dist/core/training-program.d.ts +233 -0
  72. package/dist/core/training-program.js +433 -0
  73. package/dist/core/trajectory.d.ts +155 -1
  74. package/dist/core/trajectory.js +292 -8
  75. package/dist/core/workspace-context.d.ts +0 -6
  76. package/dist/core/workspace-context.js +0 -12
  77. package/dist/hooks/bash-risk.d.ts +57 -0
  78. package/dist/hooks/bash-risk.js +137 -0
  79. package/dist/hooks/edit-verification.d.ts +62 -0
  80. package/dist/hooks/edit-verification.js +256 -0
  81. package/dist/hooks/gate-block-helper.d.ts +44 -0
  82. package/dist/hooks/gate-block-helper.js +119 -0
  83. package/dist/hooks/gate.d.ts +18 -0
  84. package/dist/hooks/gate.js +62 -751
  85. package/dist/hooks/gfi-gate.d.ts +40 -0
  86. package/dist/hooks/gfi-gate.js +113 -0
  87. package/dist/hooks/pain.js +6 -9
  88. package/dist/hooks/progressive-trust-gate.d.ts +51 -0
  89. package/dist/hooks/progressive-trust-gate.js +89 -0
  90. package/dist/hooks/prompt.d.ts +11 -11
  91. package/dist/hooks/prompt.js +167 -77
  92. package/dist/hooks/subagent.js +43 -6
  93. package/dist/hooks/thinking-checkpoint.d.ts +37 -0
  94. package/dist/hooks/thinking-checkpoint.js +51 -0
  95. package/dist/http/principles-console-route.js +13 -3
  96. package/dist/i18n/commands.js +8 -8
  97. package/dist/index.js +129 -28
  98. package/dist/service/central-database.js +2 -1
  99. package/dist/service/control-ui-query-service.d.ts +1 -1
  100. package/dist/service/control-ui-query-service.js +3 -3
  101. package/dist/service/evolution-query-service.d.ts +1 -1
  102. package/dist/service/evolution-query-service.js +5 -5
  103. package/dist/service/evolution-worker.d.ts +52 -4
  104. package/dist/service/evolution-worker.js +328 -16
  105. package/dist/service/nocturnal-runtime.d.ts +183 -0
  106. package/dist/service/nocturnal-runtime.js +352 -0
  107. package/dist/service/nocturnal-service.d.ts +163 -0
  108. package/dist/service/nocturnal-service.js +787 -0
  109. package/dist/service/nocturnal-target-selector.d.ts +145 -0
  110. package/dist/service/nocturnal-target-selector.js +315 -0
  111. package/dist/service/phase3-input-filter.d.ts +48 -12
  112. package/dist/service/phase3-input-filter.js +84 -18
  113. package/dist/service/runtime-summary-service.d.ts +34 -10
  114. package/dist/service/runtime-summary-service.js +87 -48
  115. package/dist/tools/deep-reflect.js +2 -1
  116. package/dist/types/event-types.d.ts +4 -10
  117. package/dist/types/runtime-summary.d.ts +47 -0
  118. package/dist/types/runtime-summary.js +1 -0
  119. package/dist/types.d.ts +0 -3
  120. package/dist/types.js +0 -2
  121. package/openclaw.plugin.json +1 -1
  122. package/package.json +1 -1
  123. package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
  124. package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
  125. package/templates/pain_settings.json +0 -6
  126. package/dist/commands/trust.d.ts +0 -4
  127. package/dist/commands/trust.js +0 -78
  128. package/dist/core/trust-engine.d.ts +0 -96
  129. package/dist/core/trust-engine.js +0 -286
@@ -3,7 +3,6 @@ import { PathResolver } from './path-resolver.js';
3
3
  import { ConfigService } from './config-service.js';
4
4
  import { EventLogService } from './event-log.js';
5
5
  import { DictionaryService } from './dictionary-service.js';
6
- import { TrustEngine } from './trust-engine.js';
7
6
  import { HygieneTracker } from './hygiene/tracker.js';
8
7
  import { EvolutionReducerImpl } from './evolution-reducer.js';
9
8
  import { TrajectoryRegistry } from './trajectory.js';
@@ -19,7 +18,6 @@ export class WorkspaceContext {
19
18
  _config;
20
19
  _eventLog;
21
20
  _dictionary;
22
- _trust;
23
21
  _hygiene;
24
22
  _evolutionReducer;
25
23
  _trajectory;
@@ -54,15 +52,6 @@ export class WorkspaceContext {
54
52
  }
55
53
  return this._dictionary;
56
54
  }
57
- /**
58
- * Trust engine service bound to this workspace.
59
- */
60
- get trust() {
61
- if (!this._trust) {
62
- this._trust = new TrustEngine(this.workspaceDir);
63
- }
64
- return this._trust;
65
- }
66
55
  /**
67
56
  * Hygiene tracking service for this workspace.
68
57
  */
@@ -148,7 +137,6 @@ export class WorkspaceContext {
148
137
  this._config = undefined;
149
138
  this._eventLog = undefined;
150
139
  this._dictionary = undefined;
151
- this._trust = undefined;
152
140
  this._evolutionReducer = undefined;
153
141
  this._trajectory = undefined;
154
142
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Bash Risk Analysis Module
3
+ *
4
+ * Analyzes bash command security risks and determines command categorization.
5
+ *
6
+ * **Responsibilities:**
7
+ * - De-obfuscate Unicode/Cyrillic lookalike characters (security bypass prevention)
8
+ * - Tokenize command chains to detect multi-command bypasses
9
+ * - Classify commands as: 'safe', 'dangerous', or 'normal'
10
+ * - Pattern matching against safe/dangerous regex patterns
11
+ * - Fail-closed behavior (invalid regex = dangerous)
12
+ *
13
+ * **Configuration:**
14
+ * - Bash safe patterns from gfi_gate.bash_safe_patterns
15
+ * - Bash dangerous patterns from gfi_gate.bash_dangerous_patterns
16
+ */
17
+ export interface BashRiskConfig {
18
+ bash_safe_patterns?: string[];
19
+ bash_dangerous_patterns?: string[];
20
+ }
21
+ export type BashRiskLevel = 'safe' | 'dangerous' | 'normal';
22
+ /**
23
+ * Analyzes a bash command to determine its risk level.
24
+ *
25
+ * Implements security features:
26
+ * - Unicode/Cyrillic de-obfuscation to detect homograph attacks
27
+ * - Command chain tokenization to catch multi-command bypasses
28
+ * - Pattern matching against safe/dangerous regex patterns
29
+ * - Fail-closed behavior (invalid dangerous regex = dangerous)
30
+ *
31
+ * @param command - The bash command to analyze
32
+ * @param safePatterns - Regex patterns that indicate safe commands
33
+ * @param dangerousPatterns - Regex patterns that indicate dangerous commands
34
+ * @param logger - Optional logger for warnings about invalid patterns
35
+ * @returns The risk level: 'safe', 'dangerous', or 'normal'
36
+ */
37
+ export declare function analyzeBashCommand(command: string, safePatterns: string[], dangerousPatterns: string[], logger?: {
38
+ warn?: (message: string) => void;
39
+ }): BashRiskLevel;
40
+ export interface DynamicThresholdConfig {
41
+ large_change_lines: number;
42
+ ep_tier_multipliers: Record<string, number>;
43
+ }
44
+ /**
45
+ * Calculates the dynamic GFI threshold based on EP tier and line changes.
46
+ *
47
+ * The threshold is adjusted by:
48
+ * 1. EP tier multiplier (higher tiers get higher thresholds)
49
+ * 2. Large change reduction (big edits lower the threshold to catch more issues)
50
+ *
51
+ * @param baseThreshold - The base GFI threshold (typically 50 for GFI)
52
+ * @param epTier - Current EP tier (1-5)
53
+ * @param lineChanges - Number of lines being changed
54
+ * @param config - Configuration with large_change_lines and ep_tier_multipliers
55
+ * @returns The adjusted threshold (minimum 0)
56
+ */
57
+ export declare function calculateDynamicThreshold(baseThreshold: number, epTier: number, lineChanges: number, config: DynamicThresholdConfig): number;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Bash Risk Analysis Module
3
+ *
4
+ * Analyzes bash command security risks and determines command categorization.
5
+ *
6
+ * **Responsibilities:**
7
+ * - De-obfuscate Unicode/Cyrillic lookalike characters (security bypass prevention)
8
+ * - Tokenize command chains to detect multi-command bypasses
9
+ * - Classify commands as: 'safe', 'dangerous', or 'normal'
10
+ * - Pattern matching against safe/dangerous regex patterns
11
+ * - Fail-closed behavior (invalid regex = dangerous)
12
+ *
13
+ * **Configuration:**
14
+ * - Bash safe patterns from gfi_gate.bash_safe_patterns
15
+ * - Bash dangerous patterns from gfi_gate.bash_dangerous_patterns
16
+ */
17
+ /**
18
+ * Analyzes a bash command to determine its risk level.
19
+ *
20
+ * Implements security features:
21
+ * - Unicode/Cyrillic de-obfuscation to detect homograph attacks
22
+ * - Command chain tokenization to catch multi-command bypasses
23
+ * - Pattern matching against safe/dangerous regex patterns
24
+ * - Fail-closed behavior (invalid dangerous regex = dangerous)
25
+ *
26
+ * @param command - The bash command to analyze
27
+ * @param safePatterns - Regex patterns that indicate safe commands
28
+ * @param dangerousPatterns - Regex patterns that indicate dangerous commands
29
+ * @param logger - Optional logger for warnings about invalid patterns
30
+ * @returns The risk level: 'safe', 'dangerous', or 'normal'
31
+ */
32
+ export function analyzeBashCommand(command, safePatterns, dangerousPatterns, logger) {
33
+ let normalizedCmd = command.trim().toLowerCase();
34
+ // Unicode de-obfuscation — convert Cyrillic/Unicode lookalikes to ASCII equivalents
35
+ // Common Cyrillic lookalikes that could bypass detection: аеорсух (Cyrillic) → aeopcyx (Latin)
36
+ const CYRILLIC_TO_LATIN = {
37
+ 'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'у': 'y', 'х': 'x',
38
+ 'А': 'a', 'Е': 'e', 'О': 'o', 'Р': 'p', 'С': 'c', 'У': 'y', 'Х': 'x',
39
+ // Additional confusable chars
40
+ 'і': 'i', 'ј': 'j', 'ѕ': 's', 'ԁ': 'd', 'ɡ': 'g', 'һ': 'h', 'ⅰ': 'i',
41
+ 'ƚ': 'l', 'м': 'm', 'п': 'n', 'ѵ': 'v', 'ѡ': 'w', 'ᴦ': 'r', 'ꜱ': 's',
42
+ };
43
+ normalizedCmd = normalizedCmd.replace(/[а-яА-Яіјѕԁɡһⅰƚмпеꜱѵѡᴦꜱ]/g, m => CYRILLIC_TO_LATIN[m] ?? m);
44
+ // Zero-width character detection — detect hidden characters that could bypass pattern matching
45
+ // Common zero-width characters used in command injection:
46
+ // - Zero-width space (U+200B)
47
+ // - Zero-width non-joiner (U+200C)
48
+ // - Zero-width joiner (U+200D)
49
+ // - Word joiner (U+2060)
50
+ // - Zero-width invisible separator (U+FEFF)
51
+ const ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\u2060\uFEFF]/g;
52
+ if (ZERO_WIDTH_CHARS.test(command)) {
53
+ logger?.warn?.(`[PD_GATE] Bash command contains zero-width characters — blocking as dangerous`);
54
+ return 'dangerous'; // Fail-closed: zero-width chars are suspicious
55
+ }
56
+ // Tokenize command chain before pattern matching to catch `cmd1 && cmd2` bypasses
57
+ // Only split on statement separators (; && ||), NOT on pipe (|) which is part of the command
58
+ const tokens = normalizedCmd
59
+ .split(/\s*(?:;|&&|\|\|)\s*/)
60
+ .map(t => t.trim())
61
+ .filter(t => t.length > 0);
62
+ // If no tokens (e.g., pure pipe-only), use the original
63
+ const segments = tokens.length > 0 ? tokens : [normalizedCmd];
64
+ // Also strip outer $() and backticks from each segment, but PRESERVE inner content
65
+ const cleanSegments = segments.map(seg => {
66
+ let s = seg;
67
+ // Extract inner content from $() or ${} or backtick-wrapped commands
68
+ // IMPORTANT: Preserve the inner command for analysis, don't drop it entirely
69
+ s = s.replace(/^\$\(([^)]+)\)$/, '$1').replace(/^\$\{([^}]+)\}$/, '$1').replace(/^`([^`]+)`$/, '$1');
70
+ return s.trim();
71
+ }).filter(s => s.length > 0);
72
+ // SECURITY: If original input was non-empty but we have no analyzable content, fail closed
73
+ if (cleanSegments.length === 0 && normalizedCmd.trim().length > 0) {
74
+ logger?.warn?.(`[PD_GATE] Bash command analysis produced empty segments from non-empty input, failing closed: ${normalizedCmd.substring(0, 100)}`);
75
+ return 'dangerous';
76
+ }
77
+ // 1. Check dangerous patterns against each segment
78
+ for (const seg of cleanSegments) {
79
+ for (const pattern of dangerousPatterns) {
80
+ try {
81
+ if (new RegExp(pattern, 'i').test(seg)) {
82
+ return 'dangerous';
83
+ }
84
+ }
85
+ catch (error) {
86
+ logger?.warn?.(`[PD_GATE] Invalid dangerous bash regex "${pattern}": ${String(error)}. Failing closed.`);
87
+ return 'dangerous';
88
+ // Fail-closed: 无效的危险模式正则视为匹配危险命令
89
+ }
90
+ }
91
+ }
92
+ // 2. Check safe patterns (only if ALL segments are safe)
93
+ for (const seg of cleanSegments) {
94
+ let isSafe = false;
95
+ for (const pattern of safePatterns) {
96
+ try {
97
+ if (new RegExp(pattern, 'i').test(seg)) {
98
+ isSafe = true;
99
+ break;
100
+ }
101
+ }
102
+ catch (error) {
103
+ logger?.warn?.(`[PD_GATE] Invalid safe bash regex "${pattern}": ${String(error)}. Ignoring safe override.`);
104
+ }
105
+ }
106
+ if (!isSafe) {
107
+ // Not all segments are safe → treat as normal
108
+ return 'normal';
109
+ }
110
+ }
111
+ // All segments are safe
112
+ return 'safe';
113
+ }
114
+ /**
115
+ * Calculates the dynamic GFI threshold based on EP tier and line changes.
116
+ *
117
+ * The threshold is adjusted by:
118
+ * 1. EP tier multiplier (higher tiers get higher thresholds)
119
+ * 2. Large change reduction (big edits lower the threshold to catch more issues)
120
+ *
121
+ * @param baseThreshold - The base GFI threshold (typically 50 for GFI)
122
+ * @param epTier - Current EP tier (1-5)
123
+ * @param lineChanges - Number of lines being changed
124
+ * @param config - Configuration with large_change_lines and ep_tier_multipliers
125
+ * @returns The adjusted threshold (minimum 0)
126
+ */
127
+ export function calculateDynamicThreshold(baseThreshold, epTier, lineChanges, config) {
128
+ // 1. EP Tier multiplier
129
+ const tierMultiplier = config.ep_tier_multipliers[epTier.toString()] || 1.0;
130
+ let threshold = baseThreshold * tierMultiplier;
131
+ // 2. Large scale modification reduces threshold
132
+ if (lineChanges > config.large_change_lines) {
133
+ const ratio = Math.min(lineChanges / 200, 0.5); // Reduce by up to 50%
134
+ threshold = threshold * (1 - ratio);
135
+ }
136
+ return Math.round(Math.max(threshold, 0));
137
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Edit Verification Module
3
+ *
4
+ * Enforces P-03 (precise verification principle) for edit tool operations.
5
+ *
6
+ * **Responsibilities:**
7
+ * - Verify oldText matches current file content before edit
8
+ * - Fuzzy matching for whitespace-agnostic comparison
9
+ * - File size limits and binary file detection
10
+ * - Automatic correction of whitespace mismatches
11
+ * - Detailed error messages with guidance for fix
12
+ *
13
+ * **Configuration:**
14
+ * - Edit verification settings from profile.edit_verification
15
+ * - Max file size threshold (default 10MB)
16
+ * - Fuzzy match threshold (default 0.8)
17
+ * - Skip action for large files (warn/block)
18
+ */
19
+ import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
20
+ import type { WorkspaceContext } from '../core/workspace-context.js';
21
+ export interface EditVerificationConfig {
22
+ enabled?: boolean;
23
+ max_file_size_bytes?: number;
24
+ fuzzy_match_enabled?: boolean;
25
+ fuzzy_match_threshold?: number;
26
+ skip_large_file_action?: 'warn' | 'block';
27
+ }
28
+ /**
29
+ * Normalize a line for fuzzy matching by collapsing whitespace
30
+ */
31
+ export declare function normalizeLine(line: string): string;
32
+ /**
33
+ * Find fuzzy match between oldText and current file content
34
+ * @param lines - File content split into lines
35
+ * @param oldLines - oldText split into lines
36
+ * @param threshold - Match threshold (0-1)
37
+ * @returns Match index or -1 if not found
38
+ */
39
+ export declare function findFuzzyMatch(lines: string[], oldLines: string[], threshold?: number): number;
40
+ /**
41
+ * Try to find a fuzzy match for oldText in current content
42
+ * @param currentContent - Current file content
43
+ * @param oldText - Text to match
44
+ * @param threshold - Match threshold (0-1)
45
+ * @returns Object with found status and corrected text if found
46
+ */
47
+ export declare function tryFuzzyMatch(currentContent: string, oldText: string, threshold?: number): {
48
+ found: boolean;
49
+ correctedText?: string;
50
+ };
51
+ /**
52
+ * Generate a helpful error message for edit verification failure
53
+ */
54
+ export declare function generateEditError(filePath: string, oldText: string, currentContent: string): string;
55
+ /**
56
+ * Handle edit tool verification before allowing operation
57
+ * This enforces P-03 at the tool layer
58
+ */
59
+ export declare function handleEditVerification(event: PluginHookBeforeToolCallEvent, wctx: WorkspaceContext, ctx: {
60
+ logger?: any;
61
+ sessionId?: string;
62
+ }, config?: EditVerificationConfig): PluginHookBeforeToolCallResult | void;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Edit Verification Module
3
+ *
4
+ * Enforces P-03 (precise verification principle) for edit tool operations.
5
+ *
6
+ * **Responsibilities:**
7
+ * - Verify oldText matches current file content before edit
8
+ * - Fuzzy matching for whitespace-agnostic comparison
9
+ * - File size limits and binary file detection
10
+ * - Automatic correction of whitespace mismatches
11
+ * - Detailed error messages with guidance for fix
12
+ *
13
+ * **Configuration:**
14
+ * - Edit verification settings from profile.edit_verification
15
+ * - Max file size threshold (default 10MB)
16
+ * - Fuzzy match threshold (default 0.8)
17
+ * - Skip action for large files (warn/block)
18
+ */
19
+ import * as fs from 'fs';
20
+ import * as path from 'path';
21
+ /**
22
+ * Normalize a line for fuzzy matching by collapsing whitespace
23
+ */
24
+ export function normalizeLine(line) {
25
+ return line.replace(/\s+/g, ' ').trim();
26
+ }
27
+ /**
28
+ * Find fuzzy match between oldText and current file content
29
+ * @param lines - File content split into lines
30
+ * @param oldLines - oldText split into lines
31
+ * @param threshold - Match threshold (0-1)
32
+ * @returns Match index or -1 if not found
33
+ */
34
+ export function findFuzzyMatch(lines, oldLines, threshold = 0.8) {
35
+ if (oldLines.length === 0)
36
+ return -1; // P2 fix: empty array boundary check
37
+ const normalizedLines = lines.map(normalizeLine);
38
+ const normalizedOldLines = oldLines.map(normalizeLine);
39
+ // Try to find matching sequence
40
+ for (let i = 0; i <= lines.length - oldLines.length; i++) {
41
+ let matchCount = 0;
42
+ for (let j = 0; j < oldLines.length; j++) {
43
+ if (normalizedLines[i + j] === normalizedOldLines[j]) {
44
+ matchCount++;
45
+ }
46
+ }
47
+ // Use threshold from config
48
+ if (matchCount >= oldLines.length * threshold) {
49
+ return i;
50
+ }
51
+ }
52
+ return -1;
53
+ }
54
+ /**
55
+ * Try to find a fuzzy match for oldText in current content
56
+ * @param currentContent - Current file content
57
+ * @param oldText - Text to match
58
+ * @param threshold - Match threshold (0-1)
59
+ * @returns Object with found status and corrected text if found
60
+ */
61
+ export function tryFuzzyMatch(currentContent, oldText, threshold = 0.8) {
62
+ const lines = currentContent.split('\n');
63
+ const oldLines = oldText.split('\n');
64
+ const matchIndex = findFuzzyMatch(lines, oldLines, threshold);
65
+ if (matchIndex !== -1) {
66
+ // Found fuzzy match, extract actual text from file
67
+ const correctedText = lines.slice(matchIndex, matchIndex + oldLines.length).join('\n');
68
+ return { found: true, correctedText };
69
+ }
70
+ return { found: false };
71
+ }
72
+ /**
73
+ * Generate a helpful error message for edit verification failure
74
+ */
75
+ export function generateEditError(filePath, oldText, currentContent) {
76
+ const expectedSnippet = oldText.split('\n').slice(0, 3).join('\n').substring(0, 200);
77
+ const actualSnippet = currentContent.substring(0, 200);
78
+ return `[P-03 Violation] Edit verification failed
79
+
80
+ File: ${filePath}
81
+
82
+ The text you're trying to replace does not match the current file content.
83
+
84
+ Expected to find:
85
+ ${expectedSnippet}${oldText.length > 200 ? '...' : ''}
86
+
87
+ Actual file contains:
88
+ ${actualSnippet}${currentContent.length > 200 ? '...' : ''}
89
+
90
+ Possible reasons:
91
+ - File has been modified by another process
92
+ - Whitespace characters do not match (spaces, tabs, newlines)
93
+ - Context compression caused outdated information
94
+
95
+ Solution:
96
+ 1. Use 'read' tool to get current file content
97
+ 2. Update your edit command with exact text from file
98
+ 3. Retry edit operation
99
+
100
+ This is enforced by P-03 (精确匹配前验证原则).`;
101
+ }
102
+ /**
103
+ * Handle edit tool verification before allowing operation
104
+ * This enforces P-03 at the tool layer
105
+ */
106
+ export function handleEditVerification(event, wctx, ctx, config = {}) {
107
+ // Skip verification if disabled - return early without any processing or logging
108
+ if (config.enabled === false) {
109
+ return;
110
+ }
111
+ const logger = ctx.logger || console;
112
+ const maxSizeBytes = config.max_file_size_bytes ?? 10 * 1024 * 1024; // Default 10MB
113
+ const fuzzyMatchEnabled = config.fuzzy_match_enabled !== false;
114
+ const fuzzyMatchThreshold = config.fuzzy_match_threshold ?? 0.8;
115
+ const skipAction = config.skip_large_file_action ?? 'warn';
116
+ // 1. Extract parameters (handle both parameter naming conventions)
117
+ const filePath = event.params.file_path || event.params.path || event.params.file;
118
+ const oldText = event.params.oldText || event.params.old_string;
119
+ if (!filePath || !oldText) {
120
+ // Missing required parameters, let it fail naturally
121
+ return;
122
+ }
123
+ // 2. Resolve and read file
124
+ let absolutePath;
125
+ try {
126
+ absolutePath = wctx.resolve(filePath);
127
+ }
128
+ catch (error) {
129
+ // Path resolution error, let it fail naturally
130
+ return;
131
+ }
132
+ // 2.5. Skip verification for binary files
133
+ const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg',
134
+ '.pdf', '.zip', '.tar', '.gz', '.7z', '.rar',
135
+ '.exe', '.dll', '.so', '.dylib', '.bin',
136
+ '.mp3', '.mp4', '.avi', '.mov', '.wav',
137
+ '.ttf', '.otf', '.woff', '.woff2',
138
+ '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
139
+ const ext = path.extname(absolutePath).toLowerCase();
140
+ if (BINARY_EXTENSIONS.includes(ext)) {
141
+ logger?.info?.(`[PD_GATE:EDIT_VERIFY] Skipping verification for binary file: ${path.basename(filePath)}`);
142
+ return;
143
+ }
144
+ try {
145
+ // 2.6. Check file size before reading (P-03 improvement)
146
+ try {
147
+ const stats = fs.statSync(absolutePath);
148
+ const fileSizeBytes = stats.size;
149
+ const fileSizeMB = fileSizeBytes / (1024 * 1024);
150
+ if (fileSizeBytes > maxSizeBytes) {
151
+ const message = `[PD_GATE:EDIT_VERIFY] File size check: ${path.basename(filePath)} is ${fileSizeMB.toFixed(2)}MB (threshold: ${(maxSizeBytes / (1024 * 1024)).toFixed(2)}MB)`;
152
+ if (skipAction === 'block') {
153
+ logger?.warn?.(message + ' - BLOCKED');
154
+ return {
155
+ block: true,
156
+ blockReason: `${message}\n\nFile is too large for edit verification. Increase max_file_size_bytes in PROFILE.json or reduce file size.`
157
+ };
158
+ }
159
+ else {
160
+ logger?.warn?.(message + ' - SKIPPING verification');
161
+ return; // Skip verification but allow operation
162
+ }
163
+ }
164
+ logger?.info?.(`[PD_GATE:EDIT_VERIFY] File size check passed: ${path.basename(filePath)} (${fileSizeMB.toFixed(2)}MB)`);
165
+ }
166
+ catch (statError) {
167
+ // File stat error (e.g., permission denied)
168
+ const errStr = statError instanceof Error ? statError.message : String(statError);
169
+ const errCode = statError.code;
170
+ if (errCode === 'EACCES' || errCode === 'EPERM') {
171
+ logger?.error?.(`[PD_GATE:EDIT_VERIFY] Permission denied accessing file: ${path.basename(filePath)} (${errStr})`);
172
+ return {
173
+ block: true,
174
+ blockReason: `[P-03 Error] Permission denied: Cannot access file ${absolutePath}\n\nError: ${errStr}\n\nSolution: Check file permissions or run with appropriate access rights.`
175
+ };
176
+ }
177
+ else if (errCode === 'ENOENT') {
178
+ logger?.warn?.(`[PD_GATE:EDIT_VERIFY] File not found: ${path.basename(filePath)} (${errStr})`);
179
+ // File doesn't exist - let edit operation proceed (it will create file)
180
+ return;
181
+ }
182
+ else {
183
+ logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Stat error: ${errStr}`);
184
+ // Let it fail naturally on read attempt
185
+ }
186
+ }
187
+ // 3. Read current file content with improved error handling
188
+ let currentContent;
189
+ try {
190
+ currentContent = fs.readFileSync(absolutePath, 'utf-8');
191
+ }
192
+ catch (readError) {
193
+ const errStr = readError instanceof Error ? readError.message : String(readError);
194
+ const errCode = readError.code;
195
+ if (errCode === 'EACCES' || errCode === 'EPERM') {
196
+ logger?.error?.(`[PD_GATE:EDIT_VERIFY] Permission denied reading file: ${path.basename(filePath)} (${errStr})`);
197
+ return {
198
+ block: true,
199
+ blockReason: `[P-03 Error] Permission denied: Cannot read file ${absolutePath}\n\nError: ${errStr}\n\nSolution: Check file permissions or run with appropriate access rights.`
200
+ };
201
+ }
202
+ else if (errCode === 'ENOENT') {
203
+ logger?.warn?.(`[PD_GATE:EDIT_VERIFY] File not found: ${path.basename(filePath)} (${errStr})`);
204
+ // File doesn't exist - let edit operation proceed
205
+ return;
206
+ }
207
+ else if (errStr.includes('UTF-8') || errStr.includes('encoding')) {
208
+ logger?.error?.(`[PD_GATE:EDIT_VERIFY] Encoding error reading file: ${path.basename(filePath)} (${errStr})`);
209
+ return {
210
+ block: true,
211
+ 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 file is UTF-8 encoded text, or mark binary extensions to skip verification.`
212
+ };
213
+ }
214
+ else {
215
+ logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Read error: ${errStr}`);
216
+ // Let it fail naturally
217
+ return;
218
+ }
219
+ }
220
+ // 4. Verify oldText exists in current content
221
+ if (!currentContent.includes(oldText)) {
222
+ logger?.info?.(`[PD_GATE:EDIT_VERIFY] Exact match failed for ${path.basename(filePath)}, trying fuzzy match`);
223
+ // 5. Try fuzzy matching (if enabled)
224
+ if (fuzzyMatchEnabled) {
225
+ const fuzzyResult = tryFuzzyMatch(currentContent, oldText, fuzzyMatchThreshold);
226
+ if (fuzzyResult.found && fuzzyResult.correctedText) {
227
+ logger?.info?.(`[PD_GATE:EDIT_VERIFY] Fuzzy match found for ${path.basename(filePath)}, auto-correcting oldText`);
228
+ // Return corrected parameters
229
+ return {
230
+ params: {
231
+ ...event.params,
232
+ oldText: fuzzyResult.correctedText,
233
+ old_string: fuzzyResult.correctedText
234
+ }
235
+ };
236
+ }
237
+ }
238
+ // 6. No match found, block operation with helpful error
239
+ const errorMsg = generateEditError(absolutePath, oldText, currentContent);
240
+ logger?.error?.(`[PD_GATE:EDIT_VERIFY] Block edit on ${path.basename(filePath)}: oldText not found`);
241
+ return {
242
+ block: true,
243
+ blockReason: errorMsg
244
+ };
245
+ }
246
+ // 7. Verification passed, allow edit to proceed
247
+ logger?.info?.(`[PD_GATE:EDIT_VERIFY] Verified edit on ${path.basename(filePath)}`);
248
+ return;
249
+ }
250
+ catch (error) {
251
+ // Unexpected error - let it fail naturally
252
+ const errorStr = error instanceof Error ? error.message : String(error);
253
+ logger?.warn?.(`[PD_GATE:EDIT_VERIFY] Unexpected error: ${errorStr}`);
254
+ return;
255
+ }
256
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gate Block Helper - Single Authoritative Block Persistence
3
+ *
4
+ * PURPOSE: Provide ONE authoritative implementation for gate block persistence.
5
+ *
6
+ * All gate modules (progressive-trust-gate, gfi-gate, etc.) must use this
7
+ * helper to ensure consistent block tracking, event logging, and retry behavior.
8
+ *
9
+ * This eliminates the "multi-truth source" problem where different modules
10
+ * had their own block persistence implementations.
11
+ */
12
+ import type { WorkspaceContext } from '../core/workspace-context.js';
13
+ import type { PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
14
+ /**
15
+ * Block context containing all information needed for block persistence
16
+ */
17
+ export interface BlockContext {
18
+ filePath: string;
19
+ reason: string;
20
+ toolName: string;
21
+ sessionId?: string;
22
+ /** Source module that triggered the block (for audit trail) */
23
+ blockSource?: string;
24
+ }
25
+ /**
26
+ * Single authoritative block helper.
27
+ *
28
+ * Responsibilities:
29
+ * 1. Call trackBlock() for session-level GFI tracking
30
+ * 2. Record to EventLog for operator visibility
31
+ * 3. Record to trajectory for analytics
32
+ * 4. Handle retry logic for trajectory persistence failures
33
+ * 5. Generate consistent operator-facing block message
34
+ *
35
+ * @param wctx - Workspace context
36
+ * @param blockCtx - Block context with file, reason, tool info
37
+ * @param logger - Logger instance
38
+ * @returns PluginHookBeforeToolCallResult with block=true
39
+ */
40
+ export declare function recordGateBlockAndReturn(wctx: WorkspaceContext, blockCtx: BlockContext, logger: {
41
+ warn?: (message: string) => void;
42
+ error?: (message: string) => void;
43
+ info?: (message: string) => void;
44
+ }): PluginHookBeforeToolCallResult;