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.
@@ -1,183 +0,0 @@
1
- /**
2
- * Progressive Gate Module (EP-Only Version)
3
- *
4
- * EP (Evolution Points) 是唯一的门控机制。
5
- *
6
- * **EP 门控逻辑:**
7
- * - Seed (0分): 只读 + 基础文档
8
- * - Sprout (50分): 单文件编辑
9
- * - Sapling (200分): 多文件 + 测试 + 子智能体
10
- * - Tree (500分): 重构 + 风险路径
11
- * - Forest (1000分): 完全自主
12
- *
13
- * **风险路径控制:**
14
- * - 低等级不能修改风险路径
15
- * - 高等级解锁风险路径权限
16
- *
17
- * **不再有:**
18
- * - Trust Score (30-100) 系统
19
- * - Stage 1-4 分级
20
- * - Plan Approval 白名单机制
21
- * - 基于行数的限制
22
- */
23
-
24
- import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
25
- import type { WorkspaceContext } from '../core/workspace-context.js';
26
- import { checkEvolutionGate } from '../core/evolution-engine.js';
27
- import { recordGateBlockAndReturn } from './gate-block-helper.js';
28
-
29
- // ═══ P-16: Core Governance Files — Exempt from all Blocking ═══
30
- // 这些文件是团队协作的基础,必须始终放行,不受 GFI 和 Risk Path 限制
31
- // 可通过 PROFILE.core_governance_files 扩展(merge 而非覆盖)
32
- const DEFAULT_CORE_GOVERNANCE_PATTERNS = [
33
- 'PLAN.md',
34
- 'AGENTS.md',
35
- 'VERSION.md',
36
- '.team/',
37
- 'MEMORY.md',
38
- 'SOUL.md',
39
- 'IDENTITY.md',
40
- 'USER.md',
41
- 'HEARTBEAT.md',
42
- 'BOOTSTRAP.md',
43
- 'PRINCIPLES.md',
44
- 'TEAM_ROLE.md',
45
- 'REPAIR_OPERATING_PROMPT.md',
46
- ];
47
-
48
- /**
49
- * Get effective core governance patterns from PROFILE config, merged with defaults.
50
- * PROFILE.core_governance_files extends (not replaces) the default list.
51
- */
52
- function getCoreGovernancePatterns(profile?: { core_governance_files?: string[] }): string[] {
53
- const base = DEFAULT_CORE_GOVERNANCE_PATTERNS;
54
- const extra = profile?.core_governance_files ?? [];
55
- return Array.from(new Set([...base, ...extra]));
56
- }
57
-
58
- /**
59
- * Check if a file path matches a core governance pattern.
60
- * Core governance files are exempt from all gate blocking (P-16).
61
- */
62
- function isCoreGovernanceFile(filePath?: string, corePatterns?: string[]): boolean {
63
- if (!filePath) return false;
64
- const patterns = corePatterns ?? DEFAULT_CORE_GOVERNANCE_PATTERNS;
65
- const normalized = filePath.replace(/\\/g, '/');
66
- return patterns.some(pattern =>
67
- pattern.endsWith('/')
68
- ? normalized.includes(pattern)
69
- : normalized.endsWith(pattern) || normalized.includes(`/${pattern}`)
70
- );
71
- }
72
-
73
-
74
-
75
- /**
76
- * Build EP gate rejection reason
77
- */
78
- export function buildEvolutionGateReason(
79
- tier: number,
80
- tierName: string,
81
- reason: string
82
- ): string {
83
- return `[EP Gate] Tier ${tier} (${tierName}): ${reason}`;
84
- }
85
-
86
- /**
87
- * Internal helper to call the shared block helper with progressive-trust-gate source tag.
88
- */
89
-
90
-
91
- function block(
92
- filePath: string,
93
- reason: string,
94
- wctx: WorkspaceContext,
95
- toolName: string,
96
-
97
- logger: { warn?: (message: string) => void; error?: (message: string) => void },
98
-
99
- sessionId?: string
100
- ): PluginHookBeforeToolCallResult {
101
- return recordGateBlockAndReturn(wctx, {
102
- filePath,
103
- reason,
104
- toolName,
105
- sessionId,
106
- blockSource: 'progressive-trust-gate',
107
- }, logger);
108
- }
109
-
110
- /**
111
- * Check EP-based gate
112
- *
113
- * @param event - The tool call event
114
- * @param wctx - Workspace context
115
- * @param relPath - Relative path to target file
116
- * @param risky - Whether the path is a risk path
117
- * @param lineChanges - Estimated line changes (kept for interface compatibility, not used for gating)
118
- * @param logger - Logger instance
119
- * @param ctx - Hook context
120
- * @param profile - Gate profile containing risk_paths config
121
- * @returns PluginHookBeforeToolCallResult to block, or undefined to allow
122
- */
123
-
124
-
125
- export function checkProgressiveTrustGate(
126
- event: PluginHookBeforeToolCallEvent,
127
- wctx: WorkspaceContext,
128
- relPath: string,
129
- risky: boolean,
130
- lineChanges: number,
131
-
132
- logger: { warn?: (message: string) => void; error?: (message: string) => void; info?: (message: string) => void },
133
-
134
- ctx: { workspaceDir?: string; sessionId?: string },
135
- profile?: { risk_paths: string[]; core_governance_files?: string[] }
136
- ): PluginHookBeforeToolCallResult | void {
137
- // P-16: Core governance files are exempt from all gate blocking
138
- if (isCoreGovernanceFile(relPath, getCoreGovernancePatterns(profile))) {
139
- logger.info?.(`[PD_GATE:P-16] Core governance file exempt — bypass all gates: ${relPath}`);
140
- return;
141
- }
142
-
143
- // EP is the only gate now - use actual gate decision
144
- if (!ctx.workspaceDir) {
145
- logger.warn?.('[PD_GATE] No workspaceDir, skipping EP gate check');
146
- return;
147
- }
148
-
149
- // Call EP gate - this is the actual gate, not simulation
150
- const epDecision = checkEvolutionGate(ctx.workspaceDir, {
151
- toolName: event.toolName,
152
- isRiskPath: risky,
153
- });
154
-
155
- const currentTier = epDecision.currentTier ?? 1;
156
-
157
-
158
- const tierName = getTierName(currentTier);
159
-
160
- logger.info?.(`[PD_GATE] EP Gate: Tier ${currentTier} (${tierName}), Tool: ${event.toolName}, Risk: ${risky}, Allowed: ${epDecision.allowed}`);
161
-
162
- if (!epDecision.allowed) {
163
- const reason = buildEvolutionGateReason(currentTier, tierName, epDecision.reason ?? 'Unknown restriction');
164
- return block(relPath, reason, wctx, event.toolName, logger, ctx.sessionId);
165
- }
166
-
167
- // Gate passed - allow
168
- return;
169
- }
170
-
171
- /**
172
- * Get tier name from tier number
173
- */
174
- function getTierName(tier: number): string {
175
- const names: Record<number, string> = {
176
- 1: 'Seed',
177
- 2: 'Sprout',
178
- 3: 'Sapling',
179
- 4: 'Tree',
180
- 5: 'Forest',
181
- };
182
- return names[tier] ?? 'Unknown';
183
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * Thinking Checkpoint Module
3
- *
4
- * Enforces P-10 deep reflection requirement for high-risk tool operations.
5
- *
6
- * **Responsibilities:**
7
- * - Check if high-risk tools have recent deep thinking (T-01 through T-10)
8
- * - Block high-risk operations without preceding deep reflection
9
- * - Configurable time window for thinking validity (default 5 minutes)
10
- * - Provide clear guidance on required action (deep_reflect tool usage)
11
- *
12
- * **Configuration:**
13
- * - Thinking checkpoint settings from profile.thinking_checkpoint
14
- * - Window duration for thinking validity
15
- * - High-risk tool list
16
- */
17
-
18
- import { hasRecentThinking } from '../core/session-tracker.js';
19
- import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
20
- import {
21
- THINKING_CHECKPOINT_WINDOW_MS,
22
- THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS
23
- } from '../config/index.js';
24
-
25
- export interface ThinkingCheckpointConfig {
26
- enabled?: boolean;
27
- window_ms?: number;
28
- high_risk_tools?: string[];
29
- }
30
-
31
- /**
32
- * Checks if a tool call requires a recent deep thinking checkpoint.
33
- *
34
- * This enforces P-10 (Thinking OS Checkpoint) - high-risk operations must
35
- * be preceded by deep reflection within the configured time window.
36
- *
37
- * @param event - The before_tool_call event
38
- * @param config - Thinking checkpoint configuration from profile
39
- * @param sessionId - Current session ID
40
- * @param logger - Optional logger for info messages
41
- * @returns Block result if thinking required, undefined otherwise
42
- */
43
-
44
-
45
- export function checkThinkingCheckpoint(
46
- event: PluginHookBeforeToolCallEvent,
47
- config: ThinkingCheckpointConfig,
48
- sessionId: string | undefined,
49
-
50
- logger?: { info?: (message: string) => void }
51
- ): PluginHookBeforeToolCallResult | undefined {
52
- const enabled = config.enabled ?? false;
53
- const windowMs = config.window_ms ?? THINKING_CHECKPOINT_WINDOW_MS;
54
- const highRiskTools = config.high_risk_tools ?? [...THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS];
55
-
56
- if (!enabled || !sessionId) {
57
- return undefined;
58
- }
59
-
60
- const isHighRisk = highRiskTools.includes(event.toolName);
61
- if (!isHighRisk) {
62
- return undefined;
63
- }
64
-
65
- const hasThinking = hasRecentThinking(sessionId, windowMs);
66
- if (!hasThinking) {
67
- logger?.info?.(`[PD:THINKING_GATE] High-risk tool "${event.toolName}" called without recent deep thinking`);
68
-
69
- return {
70
- block: true,
71
- blockReason: `[Thinking OS Checkpoint] 高风险操作 "${event.toolName}" 需要先进行深度思考。\n\n请先使用 deep_reflect 工具分析当前情况,然后再尝试此操作。\n\n这是强制性检查点,目的是确保决策质量。\n\n提示:调用 deep_reflect 后,${Math.round(windowMs/60000)}分钟内的操作将自动放行。\n\n可在PROFILE.json中设置 thinking_checkpoint.enabled: false 来禁用此检查。`,
72
- };
73
- }
74
-
75
- return undefined;
76
- }
@@ -1,137 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { analyzeBashCommand } from '../../src/hooks/bash-risk.js';
3
-
4
- /**
5
- * Integration tests for bash-risk module
6
- * Tests zero-width character detection and command chain analysis
7
- */
8
- describe('Bash Risk Analysis - Integration', () => {
9
- describe('Zero-width character detection', () => {
10
- it('should block commands with zero-width space (U+200B)', () => {
11
- // Zero-width space injected between characters
12
- const maliciousCmd = 'rm\u200B -rf /';
13
- const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
14
- expect(result).toBe('dangerous');
15
- });
16
-
17
- it('should block commands with zero-width non-joiner (U+200C)', () => {
18
- const maliciousCmd = 'rm\u200C -rf /';
19
- const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
20
- expect(result).toBe('dangerous');
21
- });
22
-
23
- it('should block commands with zero-width joiner (U+200D)', () => {
24
- const maliciousCmd = 'rm\u200D -rf /';
25
- const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
26
- expect(result).toBe('dangerous');
27
- });
28
-
29
- it('should block commands with word joiner (U+2060)', () => {
30
- const maliciousCmd = 'rm\u2060 -rf /';
31
- const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
32
- expect(result).toBe('dangerous');
33
- });
34
-
35
- it('should block commands with zero-width invisible separator (U+FEFF)', () => {
36
- const maliciousCmd = 'rm\uFEFF -rf /';
37
- const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
38
- expect(result).toBe('dangerous');
39
- });
40
-
41
- it('should block commands with multiple zero-width characters', () => {
42
- const maliciousCmd = 'rm\u200B\u200C\u200D -rf /';
43
- const result = analyzeBashCommand(maliciousCmd, [], [], { warn: vi.fn() });
44
- expect(result).toBe('dangerous');
45
- });
46
-
47
- it('should allow normal commands without zero-width characters', () => {
48
- const normalCmd = 'rm -rf /tmp/test';
49
- const result = analyzeBashCommand(normalCmd, ['^rm\\s'], [], { warn: vi.fn() });
50
- expect(result).toBe('safe');
51
- });
52
-
53
- it('should detect zero-width characters hidden in normal-looking commands', () => {
54
- // Command looks like 'git status' but contains hidden chars
55
- const hiddenCmd = 'git\u200B status';
56
- const result = analyzeBashCommand(hiddenCmd, [], [], { warn: vi.fn() });
57
- expect(result).toBe('dangerous');
58
- });
59
- });
60
-
61
- describe('Cyrillic homograph attack detection', () => {
62
- it('should de-obfuscate Cyrillic characters and match dangerous patterns', () => {
63
- // Cyrillic 'р' (U+0440) looks like Latin 'p' - 'g\u0440ush' → 'gpush' then 'push'
64
- // After toLowerCase + deobfuscation: 'gpush' won't match dangerous, but 'push' alone might
65
- // Actually: '\u0440' maps to 'p' so 'g\u0440ush' → 'gp' + 'ush' = 'gpush'
66
- // Let's use Cyrillic 'е' which maps to 'e': '\u0435' → 'e'
67
- const cyrillicCmd = 'r\u0435m -rf /tmp';
68
- const result = analyzeBashCommand(cyrillicCmd, [], ['^rem'], { warn: vi.fn() });
69
- expect(result).toBe('dangerous');
70
- });
71
-
72
- it('should handle Cyrillic а being converted to Latin a', () => {
73
- // Cyrillic 'а' (U+0430) maps to Latin 'a' - 's\u0430do' → 'sado'
74
- // After de-obfuscation: 'sado' - doesn't match dangerous patterns
75
- const mixedCmd = 's\u0430do apt update';
76
- const result = analyzeBashCommand(mixedCmd, [], ['^sudo'], { warn: vi.fn() });
77
- expect(result).toBe('normal');
78
- });
79
- });
80
-
81
- describe('Command chain tokenization', () => {
82
- it('should detect dangerous commands in chains using &&', () => {
83
- const chainCmd = 'echo "hello" && rm -rf /tmp/test';
84
- const result = analyzeBashCommand(chainCmd, [], ['rm\\s+-rf'], { warn: vi.fn() });
85
- expect(result).toBe('dangerous');
86
- });
87
-
88
- it('should detect dangerous commands in chains using ||', () => {
89
- const chainCmd = 'ls || rm -rf /tmp/test';
90
- const result = analyzeBashCommand(chainCmd, [], ['rm\\s+-rf'], { warn: vi.fn() });
91
- expect(result).toBe('dangerous');
92
- });
93
-
94
- it('should detect dangerous commands in chains using ;', () => {
95
- const chainCmd = 'ls ; rm -rf /tmp/test';
96
- const result = analyzeBashCommand(chainCmd, [], ['rm\\s+-rf'], { warn: vi.fn() });
97
- expect(result).toBe('dangerous');
98
- });
99
-
100
- it('should return safe if no segment matches dangerous patterns', () => {
101
- const safeChain = 'echo "hello" && echo "world"';
102
- const result = analyzeBashCommand(safeChain, [], ['rm\\s+-rf'], { warn: vi.fn() });
103
- expect(result).toBe('normal');
104
- });
105
- });
106
-
107
- describe('Fail-closed behavior', () => {
108
- it('should return dangerous for invalid regex in dangerousPatterns', () => {
109
- const cmd = 'ls';
110
- const result = analyzeBashCommand(cmd, [], ['[invalid'], { warn: vi.fn() });
111
- expect(result).toBe('dangerous'); // Fail-closed
112
- });
113
-
114
- it('should ignore invalid regex in safePatterns', () => {
115
- const cmd = 'echo hello';
116
- const warnFn = vi.fn();
117
- const result = analyzeBashCommand(cmd, ['[invalid'], [], { warn: warnFn });
118
- // Should not crash, should return normal (not all segments safe)
119
- expect(result).toBe('normal');
120
- expect(warnFn).toHaveBeenCalled();
121
- });
122
- });
123
-
124
- describe('Safe pattern override', () => {
125
- it('should return safe when all segments match safe patterns', () => {
126
- const safeCmd = 'git status';
127
- const result = analyzeBashCommand(safeCmd, ['^git\\s+status', '^ls'], []);
128
- expect(result).toBe('safe');
129
- });
130
-
131
- it('should return normal when not all segments are safe', () => {
132
- const mixedCmd = 'git status && rm -rf /tmp';
133
- const result = analyzeBashCommand(mixedCmd, ['^git\\s+status'], ['rm\\s+-rf']);
134
- expect(result).toBe('dangerous');
135
- });
136
- });
137
- });
@@ -1,81 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { analyzeBashCommand } from '../../src/hooks/bash-risk.js';
3
-
4
- describe('analyzeBashCommand', () => {
5
- it('should return safe for commands matching safe patterns', () => {
6
- const result = analyzeBashCommand('npm install lodash', ['^npm\\s+install'], []);
7
- expect(result).toBe('safe');
8
- });
9
-
10
- it('should return dangerous for commands matching dangerous patterns', () => {
11
- const result = analyzeBashCommand('rm -rf /', [], ['rm\\s+.*-rf']);
12
- expect(result).toBe('dangerous');
13
- });
14
-
15
- it('should return normal for commands not in safe/dangerous lists', () => {
16
- const result = analyzeBashCommand('npm install lodash', [], []);
17
- expect(result).toBe('normal');
18
- });
19
-
20
- it('should de-obfuscate Cyrillic lookalikes', () => {
21
- // Using Cyrillic 'rеset' (with Cyrillic 'е' U+0435) instead of 'reset'
22
- // This should de-obfuscate to 'git reset --hard' and match the dangerous pattern
23
- const result = analyzeBashCommand('git rеset --hard', [], ['git\\s+(push\\s+.*--force|reset\\s+--hard|clean\\s+-fd)']);
24
- expect(result).toBe('dangerous');
25
- });
26
-
27
- it('should tokenize command chains', () => {
28
- const result = analyzeBashCommand('npm install && npm test', ['^npm\\s+install'], ['npm\\s+publish']);
29
- expect(result).toBe('normal');
30
- });
31
-
32
- it('should fail-closed on invalid dangerous regex', () => {
33
- const mockLogger = { warn: vi.fn() };
34
- const result = analyzeBashCommand('echo test', ['^echo'], ['invalid('], mockLogger);
35
- expect(result).toBe('dangerous'); // Fail-closed behavior
36
- expect(mockLogger.warn).toHaveBeenCalledWith(
37
- expect.stringContaining('Invalid dangerous bash regex')
38
- );
39
- });
40
-
41
- it('should ignore safe pattern on invalid safe regex', () => {
42
- const mockLogger = { warn: vi.fn() };
43
- const result = analyzeBashCommand('echo test', ['invalid('], [], mockLogger);
44
- expect(result).toBe('normal'); // Not safe because safe pattern is invalid and ignored
45
- expect(mockLogger.warn).toHaveBeenCalledWith(
46
- expect.stringContaining('Invalid safe bash regex')
47
- );
48
- });
49
-
50
- it('should strip $() from commands before pattern matching', () => {
51
- const result = analyzeBashCommand('$(npm install)', ['^npm\\s+install'], []);
52
- expect(result).toBe('safe');
53
- });
54
-
55
- it('should strip backticks from commands before pattern matching', () => {
56
- const result = analyzeBashCommand('`npm install`', ['^npm\\s+install'], []);
57
- expect(result).toBe('safe');
58
- });
59
-
60
- it('should handle command chains with semicolon separator', () => {
61
- const result = analyzeBashCommand('npm install ; npm test', ['^npm\\s+install'], ['rm\\s+']);
62
- expect(result).toBe('normal'); // Second command not safe
63
- });
64
-
65
- it('should handle command chains with OR separator', () => {
66
- const result = analyzeBashCommand('npm install || npm test', ['^npm\\s+install'], ['rm\\s+']);
67
- expect(result).toBe('normal');
68
- });
69
-
70
- it('should convert uppercase Cyrillic to lowercase Latin', () => {
71
- // Using uppercase Cyrillic 'Е' (U+0415) which should convert to 'e'
72
- // 'git REsET --hard' should become 'git reset --hard' and match the dangerous pattern
73
- const result = analyzeBashCommand('git rEsET --hard', [], ['git\\s+(push\\s+.*--force|reset\\s+--hard|clean\\s+-fd)']);
74
- expect(result).toBe('dangerous');
75
- });
76
-
77
- it('should handle additional confusable Unicode characters', () => {
78
- const result = analyzeBashCommand('del node_modules', [], ['rm\\s+']);
79
- expect(result).toBe('normal'); // 'del' is not in dangerous patterns
80
- });
81
- });