principles-disciple 1.6.0 → 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.
- package/dist/commands/context.js +7 -3
- package/dist/commands/evolution-status.d.ts +4 -0
- package/dist/commands/evolution-status.js +138 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.js +45 -0
- package/dist/commands/focus.js +9 -6
- package/dist/commands/pain.js +8 -0
- package/dist/commands/principle-rollback.d.ts +4 -0
- package/dist/commands/principle-rollback.js +22 -0
- package/dist/commands/samples.d.ts +2 -0
- package/dist/commands/samples.js +55 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/control-ui-db.d.ts +68 -0
- package/dist/core/control-ui-db.js +274 -0
- package/dist/core/detection-funnel.d.ts +1 -1
- package/dist/core/detection-funnel.js +4 -0
- package/dist/core/dictionary.d.ts +2 -0
- package/dist/core/dictionary.js +13 -0
- package/dist/core/event-log.d.ts +2 -1
- package/dist/core/event-log.js +3 -0
- package/dist/core/evolution-engine.d.ts +5 -5
- package/dist/core/evolution-engine.js +18 -18
- package/dist/core/evolution-migration.d.ts +5 -0
- package/dist/core/evolution-migration.js +65 -0
- package/dist/core/evolution-reducer.d.ts +69 -0
- package/dist/core/evolution-reducer.js +369 -0
- package/dist/core/evolution-types.d.ts +103 -0
- package/dist/core/path-resolver.js +75 -36
- package/dist/core/paths.d.ts +7 -8
- package/dist/core/paths.js +48 -40
- package/dist/core/profile.js +1 -1
- package/dist/core/session-tracker.d.ts +4 -0
- package/dist/core/session-tracker.js +15 -0
- package/dist/core/thinking-models.d.ts +38 -0
- package/dist/core/thinking-models.js +170 -0
- package/dist/core/trajectory.d.ts +184 -0
- package/dist/core/trajectory.js +817 -0
- package/dist/core/trust-engine.d.ts +2 -0
- package/dist/core/trust-engine.js +30 -4
- package/dist/core/workspace-context.d.ts +13 -0
- package/dist/core/workspace-context.js +50 -7
- package/dist/hooks/gate.js +117 -48
- package/dist/hooks/llm.js +114 -69
- package/dist/hooks/pain.js +105 -5
- package/dist/hooks/prompt.d.ts +11 -14
- package/dist/hooks/prompt.js +283 -57
- package/dist/hooks/subagent.js +27 -1
- package/dist/http/principles-console-route.d.ts +2 -0
- package/dist/http/principles-console-route.js +257 -0
- package/dist/i18n/commands.js +16 -0
- package/dist/index.js +83 -4
- package/dist/service/control-ui-query-service.d.ts +217 -0
- package/dist/service/control-ui-query-service.js +537 -0
- package/dist/service/evolution-worker.d.ts +9 -0
- package/dist/service/evolution-worker.js +152 -22
- package/dist/service/trajectory-service.d.ts +2 -0
- package/dist/service/trajectory-service.js +15 -0
- package/dist/tools/agent-spawn.d.ts +27 -6
- package/dist/tools/agent-spawn.js +339 -87
- package/dist/tools/deep-reflect.d.ts +27 -7
- package/dist/tools/deep-reflect.js +210 -121
- package/dist/types/event-types.d.ts +9 -2
- package/dist/types.d.ts +10 -0
- package/dist/types.js +5 -0
- package/openclaw.plugin.json +43 -11
- package/package.json +14 -4
- package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
|
@@ -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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
114
|
+
logWarn('[PD:WorkspaceContext] workspaceDir not provided in context, using PathResolver fallback');
|
|
80
115
|
workspaceDir = this.pathResolver.getWorkspaceDir();
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
}
|
package/dist/hooks/gate.js
CHANGED
|
@@ -7,6 +7,7 @@ import { trackBlock, hasRecentThinking, getSession } from '../core/session-track
|
|
|
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';
|
|
10
11
|
// ═══ GFI Gate Tool Tiers ═══
|
|
11
12
|
// TIER 0: 只读工具 - 永不拦截
|
|
12
13
|
const READ_ONLY_TOOLS = new Set([
|
|
@@ -19,7 +20,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
19
20
|
'deep_reflect',
|
|
20
21
|
]);
|
|
21
22
|
// TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
|
|
22
|
-
// 注意:
|
|
23
|
+
// 注意:pd_run_worker、sessions_spawn、task 是 Agent 派生工具,不应被 GFI Gate 拦截
|
|
23
24
|
// 它们属于 AGENT_TOOLS,在早期过滤后直接放行
|
|
24
25
|
const LOW_RISK_WRITE_TOOLS = new Set([
|
|
25
26
|
'write', 'write_file',
|
|
@@ -37,31 +38,66 @@ const BASH_TOOLS_SET = new Set([
|
|
|
37
38
|
* 分析 Bash 命令风险等级
|
|
38
39
|
*/
|
|
39
40
|
function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
// 忽略无效正则
|
|
46
77
|
}
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// 忽略无效正则
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
|
-
// 2.
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
57
92
|
}
|
|
58
93
|
}
|
|
59
|
-
|
|
60
|
-
//
|
|
94
|
+
if (!isSafe) {
|
|
95
|
+
// Not all segments are safe → treat as normal
|
|
96
|
+
return 'normal';
|
|
61
97
|
}
|
|
62
98
|
}
|
|
63
|
-
//
|
|
64
|
-
return '
|
|
99
|
+
// All segments are safe
|
|
100
|
+
return 'safe';
|
|
65
101
|
}
|
|
66
102
|
/**
|
|
67
103
|
* 计算动态 GFI 阈值
|
|
@@ -82,7 +118,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
82
118
|
// 1. Identify tool type
|
|
83
119
|
const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'replace', 'insert', 'patch', 'edit_file', 'delete_file', 'move_file'];
|
|
84
120
|
const BASH_TOOLS = ['bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd'];
|
|
85
|
-
const AGENT_TOOLS = ['
|
|
121
|
+
const AGENT_TOOLS = ['pd_run_worker', 'sessions_spawn'];
|
|
86
122
|
const isBash = BASH_TOOLS.includes(event.toolName);
|
|
87
123
|
const isWriteTool = WRITE_TOOLS.includes(event.toolName);
|
|
88
124
|
const isAgentTool = AGENT_TOOLS.includes(event.toolName);
|
|
@@ -115,7 +151,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
115
151
|
thinking_checkpoint: {
|
|
116
152
|
enabled: false, // Default OFF
|
|
117
153
|
window_ms: 5 * 60 * 1000,
|
|
118
|
-
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', '
|
|
154
|
+
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_run_worker'],
|
|
119
155
|
}
|
|
120
156
|
};
|
|
121
157
|
if (fs.existsSync(profilePath)) {
|
|
@@ -272,6 +308,26 @@ GFI: ${currentGfi}/100
|
|
|
272
308
|
};
|
|
273
309
|
}
|
|
274
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
|
+
}
|
|
275
331
|
}
|
|
276
332
|
// Merge pluginConfig (OpenClaw UI settings)
|
|
277
333
|
const configRiskPaths = ctx.pluginConfig?.riskPaths ?? [];
|
|
@@ -314,37 +370,35 @@ GFI: ${currentGfi}/100
|
|
|
314
370
|
};
|
|
315
371
|
const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
|
|
316
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)));
|
|
317
380
|
logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
|
|
318
381
|
// Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
|
|
319
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
|
+
}
|
|
320
401
|
if (risky || riskLevel !== 'LOW') {
|
|
321
|
-
// Check if PLAN whitelist is enabled
|
|
322
|
-
if (profile.progressive_gate?.plan_approvals?.enabled) {
|
|
323
|
-
const planApprovals = profile.progressive_gate.plan_approvals;
|
|
324
|
-
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
325
|
-
// Must have READY plan
|
|
326
|
-
if (planStatus === 'READY') {
|
|
327
|
-
// Check operation type
|
|
328
|
-
if (planApprovals.allowed_operations?.includes(event.toolName)) {
|
|
329
|
-
// Check path pattern
|
|
330
|
-
if (matchesAnyPattern(relPath, planApprovals.allowed_patterns || [])) {
|
|
331
|
-
// Check line limit (if configured)
|
|
332
|
-
const maxLines = planApprovals.max_lines_override ?? -1;
|
|
333
|
-
if (maxLines === -1 || lineChanges <= maxLines) {
|
|
334
|
-
// Record PLAN approval event
|
|
335
|
-
wctx.eventLog.recordPlanApproval(ctx.sessionId, {
|
|
336
|
-
toolName: event.toolName,
|
|
337
|
-
filePath: relPath,
|
|
338
|
-
pattern: relPath,
|
|
339
|
-
planStatus
|
|
340
|
-
});
|
|
341
|
-
logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
|
|
342
|
-
return; // Allow the operation
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
402
|
// Block if not approved by whitelist
|
|
349
403
|
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName);
|
|
350
404
|
}
|
|
@@ -375,6 +429,21 @@ GFI: ${currentGfi}/100
|
|
|
375
429
|
// Stage 4 (Architect): Full bypass
|
|
376
430
|
if (stage === 4) {
|
|
377
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
|
+
}
|
|
378
447
|
return;
|
|
379
448
|
}
|
|
380
449
|
// ── EP SIMULATION MODE (M6验证) ──
|