principles-disciple 1.16.0 → 1.18.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/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +4 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +480 -158
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +221 -109
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +11 -4
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
|
@@ -33,6 +33,7 @@ export interface SessionState {
|
|
|
33
33
|
lastErrorSource?: string;
|
|
34
34
|
lastErrorHash: string;
|
|
35
35
|
consecutiveErrors: number;
|
|
36
|
+
lastGfiDecayAt?: number; // Timestamp of last GFI decay (for time-based decay)
|
|
36
37
|
|
|
37
38
|
// Daily statistics (persisted)
|
|
38
39
|
dailyToolCalls: number;
|
|
@@ -82,7 +83,7 @@ export function initPersistence(stateDir: string): void {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// Load all existing sessions
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
loadAllSessions();
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -138,6 +139,14 @@ function persistSession(state: SessionState): void {
|
|
|
138
139
|
|
|
139
140
|
try {
|
|
140
141
|
fs.writeFileSync(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
142
|
+
// Log successful persistence with GFI snapshot for debugging
|
|
143
|
+
if (state.currentGfi > 0) {
|
|
144
|
+
SystemLogger.log(
|
|
145
|
+
state.workspaceDir,
|
|
146
|
+
'GFI_PERSIST',
|
|
147
|
+
`Session ${state.sessionId.slice(0, 8)} persisted: GFI=${state.currentGfi.toFixed(1)}, sources=${JSON.stringify(state.gfiBySource)}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
141
150
|
} catch (error) {
|
|
142
151
|
logSessionTrackerWarning(`Failed to persist session ${state.sessionId}`, error);
|
|
143
152
|
}
|
|
@@ -171,7 +180,7 @@ export function flushAllSessions(): void {
|
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
|
|
183
|
+
|
|
175
184
|
function getOrCreateSession(sessionId: string, workspaceDir?: string, sessionKey?: string, trigger?: string): SessionState {
|
|
176
185
|
let state = sessions.get(sessionId);
|
|
177
186
|
if (!state) {
|
|
@@ -194,6 +203,7 @@ function getOrCreateSession(sessionId: string, workspaceDir?: string, sessionKey
|
|
|
194
203
|
lastErrorSource: '',
|
|
195
204
|
lastErrorHash: '',
|
|
196
205
|
consecutiveErrors: 0,
|
|
206
|
+
lastGfiDecayAt: Date.now(),
|
|
197
207
|
dailyToolCalls: 0,
|
|
198
208
|
dailyToolFailures: 0,
|
|
199
209
|
dailyPainSignals: 0,
|
|
@@ -232,7 +242,7 @@ export function trackToolRead(sessionId: string, filePath: string, workspaceDir?
|
|
|
232
242
|
return state;
|
|
233
243
|
}
|
|
234
244
|
|
|
235
|
-
|
|
245
|
+
|
|
236
246
|
export function trackLlmOutput(sessionId: string, usage: TokenUsage | undefined, config?: PainConfig, workspaceDir?: string, sessionKey?: string, trigger?: string): SessionState {
|
|
237
247
|
const state = getOrCreateSession(sessionId, workspaceDir, sessionKey, trigger);
|
|
238
248
|
state.llmTurns += 1;
|
|
@@ -271,7 +281,7 @@ export function trackLlmOutput(sessionId: string, usage: TokenUsage | undefined,
|
|
|
271
281
|
/**
|
|
272
282
|
* Tracks physical friction based on tool execution failures.
|
|
273
283
|
*/
|
|
274
|
-
|
|
284
|
+
|
|
275
285
|
export function trackFriction(
|
|
276
286
|
sessionId: string,
|
|
277
287
|
deltaF: number,
|
|
@@ -305,6 +315,8 @@ export function trackFriction(
|
|
|
305
315
|
state.dailyGfiPeak = Math.max(state.dailyGfiPeak, state.currentGfi);
|
|
306
316
|
|
|
307
317
|
// Schedule persistence
|
|
318
|
+
// Update decay anchor to prevent retroactive decay of the new friction
|
|
319
|
+
state.lastGfiDecayAt = Date.now();
|
|
308
320
|
schedulePersistence(state);
|
|
309
321
|
|
|
310
322
|
return state;
|
|
@@ -516,3 +528,83 @@ export function resetDailyStats(sessionId: string): void {
|
|
|
516
528
|
schedulePersistence(state);
|
|
517
529
|
}
|
|
518
530
|
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Apply time-based decay to GFI using segmented exponential decay.
|
|
534
|
+
*
|
|
535
|
+
* Decay rates:
|
|
536
|
+
* - GFI >= 70 (severe): 3%/min - fast recovery to avoid prolonged blocking
|
|
537
|
+
* - GFI 40-70 (moderate): 2%/min - medium decay
|
|
538
|
+
* - GFI < 40 (mild): 1%/min - slow decay to retain as warning
|
|
539
|
+
*
|
|
540
|
+
* Formula: GFI_new = GFI * (1 - λ)^elapsedMinutes
|
|
541
|
+
*
|
|
542
|
+
* @param sessionId - The session to decay
|
|
543
|
+
* @param elapsedMinutes - Minutes since last decay
|
|
544
|
+
* @returns Updated session state, or undefined if session not found or GFI is 0
|
|
545
|
+
*/
|
|
546
|
+
export function decayGfi(sessionId: string, elapsedMinutes: number): SessionState | undefined {
|
|
547
|
+
const state = sessions.get(sessionId);
|
|
548
|
+
if (!state || state.currentGfi <= 0 || elapsedMinutes <= 0) return undefined;
|
|
549
|
+
|
|
550
|
+
// Determine decay rate based on current GFI level (segmented)
|
|
551
|
+
let decayRate: number;
|
|
552
|
+
if (state.currentGfi >= 70) {
|
|
553
|
+
decayRate = 0.03; // 3%/min for severe friction
|
|
554
|
+
} else if (state.currentGfi >= 40) {
|
|
555
|
+
decayRate = 0.02; // 2%/min for moderate friction
|
|
556
|
+
} else {
|
|
557
|
+
decayRate = 0.01; // 1%/min for mild friction
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Exponential decay: GFI_new = GFI * (1-λ)^Δt
|
|
561
|
+
const decayFactor = Math.pow(1 - decayRate, elapsedMinutes);
|
|
562
|
+
const previousGfi = state.currentGfi;
|
|
563
|
+
state.currentGfi = Math.max(0, state.currentGfi * decayFactor);
|
|
564
|
+
|
|
565
|
+
// Apply same decay factor to all sources
|
|
566
|
+
const ledger = ensureGfiLedger(state);
|
|
567
|
+
for (const source of Object.keys(ledger)) {
|
|
568
|
+
ledger[source] = Math.max(0, ledger[source] * decayFactor);
|
|
569
|
+
// Remove sources that have decayed below 0.1
|
|
570
|
+
if (ledger[source] < 0.1) {
|
|
571
|
+
delete ledger[source];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Round to 1 decimal place
|
|
576
|
+
state.currentGfi = Math.round(state.currentGfi * 10) / 10;
|
|
577
|
+
|
|
578
|
+
// Update last decay timestamp
|
|
579
|
+
state.lastGfiDecayAt = Date.now();
|
|
580
|
+
|
|
581
|
+
// Log if significant decay
|
|
582
|
+
const decayedAmount = previousGfi - state.currentGfi;
|
|
583
|
+
if (decayedAmount >= 1) {
|
|
584
|
+
SystemLogger.log(
|
|
585
|
+
state.workspaceDir,
|
|
586
|
+
'GFI_DECAY',
|
|
587
|
+
`GFI decayed by ${decayedAmount.toFixed(1)} (${elapsedMinutes}min at ${decayRate*100}%/min). ${previousGfi.toFixed(1)} → ${state.currentGfi.toFixed(1)}`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
schedulePersistence(state);
|
|
592
|
+
return state;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check if GFI decay should be applied and return elapsed minutes since last decay.
|
|
597
|
+
* @param sessionId - The session to check
|
|
598
|
+
* @returns Elapsed minutes since last decay, or 0 if no decay needed
|
|
599
|
+
*/
|
|
600
|
+
export function getGfiDecayElapsed(sessionId: string): number {
|
|
601
|
+
const state = sessions.get(sessionId);
|
|
602
|
+
if (!state || state.currentGfi <= 0) return 0;
|
|
603
|
+
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
const lastDecay = state.lastGfiDecayAt || state.lastControlActivityAt || state.lastActivityAt || now;
|
|
606
|
+
const elapsedMs = now - lastDecay;
|
|
607
|
+
|
|
608
|
+
// Return elapsed minutes (floor to whole minutes)
|
|
609
|
+
return Math.floor(elapsedMs / 60000);
|
|
610
|
+
}
|
|
@@ -222,12 +222,12 @@ function writeRegistry(stateDir: string, registry: ShadowRegistry): void {
|
|
|
222
222
|
/**
|
|
223
223
|
* Execute a read-modify-write under an exclusive file lock.
|
|
224
224
|
*/
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
function withShadowRegistryLock<T>(
|
|
227
227
|
stateDir: string,
|
|
228
228
|
fn: (_registry: ShadowRegistry) => T
|
|
229
229
|
): T {
|
|
230
|
-
|
|
230
|
+
|
|
231
231
|
const registryPath = getRegistryPath(stateDir);
|
|
232
232
|
return withLock(registryPath, () => {
|
|
233
233
|
const registry = readRegistry(stateDir);
|
|
@@ -341,7 +341,7 @@ export function completeShadowObservation(
|
|
|
341
341
|
* @param failureSignals - Runtime failure signals
|
|
342
342
|
* @returns The updated ShadowObservation, or null if not found
|
|
343
343
|
*/
|
|
344
|
-
|
|
344
|
+
|
|
345
345
|
export function completeShadowObservationByTask(
|
|
346
346
|
stateDir: string,
|
|
347
347
|
taskFingerprint: string,
|
|
@@ -25,10 +25,10 @@ export const SystemLogger = {
|
|
|
25
25
|
const logEntry = `[${timestamp}] [${eventType.padEnd(15)}] ${message}\n`;
|
|
26
26
|
|
|
27
27
|
// Use fire-and-forget async append to prevent blocking
|
|
28
|
-
fs.appendFile(logFile, logEntry, 'utf8', (_err) => {
|
|
28
|
+
fs.appendFile(logFile, logEntry, 'utf8', (_err) => {
|
|
29
29
|
// Silently drop errors (e.g. disk full) to not crash the gateway
|
|
30
30
|
});
|
|
31
|
-
} catch (e) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
31
|
+
} catch (e) { // eslint-disable-line @typescript-eslint/no-unused-vars -- Reason: intentionally unused - silently fail if we can't setup the log
|
|
32
32
|
// Silently fail if we can't setup the log
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -44,7 +44,7 @@ export function parseThinkingOsMd(content: string): ThinkingOsDirective[] {
|
|
|
44
44
|
|
|
45
45
|
// Match all <directive ...> ... </directive> blocks
|
|
46
46
|
const directiveRegex = /<directive\s+([^>]*)>([\s\S]*?)<\/directive>/gi;
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
let match: RegExpExecArray | null = null;
|
|
49
49
|
|
|
50
50
|
while ((match = directiveRegex.exec(content)) !== null) {
|
|
@@ -565,9 +565,9 @@ export function processTrainerResult(
|
|
|
565
565
|
* ```
|
|
566
566
|
*/
|
|
567
567
|
export class TrainingProgram {
|
|
568
|
-
|
|
568
|
+
|
|
569
569
|
constructor(private readonly stateDir: string) {}
|
|
570
|
-
|
|
570
|
+
|
|
571
571
|
|
|
572
572
|
/**
|
|
573
573
|
* Create a new training experiment.
|
package/src/core/trajectory.ts
CHANGED
|
@@ -208,7 +208,7 @@ export class TrajectoryDatabase {
|
|
|
208
208
|
const createdAt = input.createdAt ?? nowIso();
|
|
209
209
|
// Extract filePath from paramsJson if provided and is an object with filePath
|
|
210
210
|
const paramsObj = input.paramsJson as Record<string, unknown> | undefined;
|
|
211
|
-
/* eslint-disable @typescript-eslint/no-unused-vars
|
|
211
|
+
/* eslint-disable @typescript-eslint/no-unused-vars -- Reason: _filePath extracted for potential future use but currently unused */
|
|
212
212
|
const _filePath = paramsObj && typeof paramsObj.filePath === 'string' ? paramsObj.filePath : null;
|
|
213
213
|
const rowId = this.withWrite(() => {
|
|
214
214
|
const result = this.db.prepare(`
|
|
@@ -587,7 +587,7 @@ export class TrajectoryDatabase {
|
|
|
587
587
|
const limit = filters.limit ?? 100;
|
|
588
588
|
const offset = filters.offset ?? 0;
|
|
589
589
|
|
|
590
|
-
|
|
590
|
+
|
|
591
591
|
let rows: Record<string, unknown>[];
|
|
592
592
|
if (traceId) {
|
|
593
593
|
rows = this.db.prepare(`
|
|
@@ -783,7 +783,7 @@ export class TrajectoryDatabase {
|
|
|
783
783
|
try {
|
|
784
784
|
const params = JSON.parse(row.params_json);
|
|
785
785
|
if (params && typeof params.filePath === 'string') {
|
|
786
|
-
|
|
786
|
+
|
|
787
787
|
filePath = params.filePath;
|
|
788
788
|
}
|
|
789
789
|
} catch {
|
|
@@ -1324,7 +1324,7 @@ export class TrajectoryDatabase {
|
|
|
1324
1324
|
this.importLegacyEvolution();
|
|
1325
1325
|
}
|
|
1326
1326
|
|
|
1327
|
-
|
|
1327
|
+
|
|
1328
1328
|
private migrateSchema(_fromVersion?: number): void {
|
|
1329
1329
|
this.db.exec(`
|
|
1330
1330
|
DROP VIEW IF EXISTS v_daily_metrics;
|
|
@@ -1545,7 +1545,7 @@ export class TrajectoryDatabase {
|
|
|
1545
1545
|
});
|
|
1546
1546
|
}
|
|
1547
1547
|
|
|
1548
|
-
|
|
1548
|
+
|
|
1549
1549
|
private recordExportAudit(
|
|
1550
1550
|
exportKind: string,
|
|
1551
1551
|
mode: CorrectionExportMode,
|
|
@@ -1610,7 +1610,7 @@ export class TrajectoryDatabase {
|
|
|
1610
1610
|
for (const entry of fs.readdirSync(this.blobDir)) {
|
|
1611
1611
|
if (referenced.has(entry)) continue;
|
|
1612
1612
|
const fullPath = path.join(this.blobDir, entry);
|
|
1613
|
-
|
|
1613
|
+
|
|
1614
1614
|
let stat: fs.Stats;
|
|
1615
1615
|
try {
|
|
1616
1616
|
stat = fs.statSync(fullPath);
|
|
@@ -1660,9 +1660,9 @@ export class TrajectoryRegistry {
|
|
|
1660
1660
|
this.instances.clear();
|
|
1661
1661
|
}
|
|
1662
1662
|
|
|
1663
|
-
|
|
1663
|
+
|
|
1664
1664
|
static use<T>(workspaceDir: string, fn: (_db: TrajectoryDatabase) => T, opts: Omit<TrajectoryDatabaseOptions, 'workspaceDir'> = {}): T {
|
|
1665
|
-
|
|
1665
|
+
|
|
1666
1666
|
const normalized = path.resolve(workspaceDir);
|
|
1667
1667
|
const existing = this.instances.get(normalized);
|
|
1668
1668
|
if (existing) {
|
|
@@ -21,13 +21,13 @@ import {
|
|
|
21
21
|
import type { Principle, PrincipleValueMetrics } from '../types/principle-tree-schema.js';
|
|
22
22
|
import type { Principle as ActivePrinciple } from './evolution-types.js';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
interface PrincipleTreeLedgerAccessor {
|
|
26
26
|
getPrincipleSubtree(_principleId: string): PrincipleSubtree | undefined;
|
|
27
27
|
updatePrinciple(_principleId: string, updates: Partial<Principle>): Principle;
|
|
28
28
|
updatePrincipleValueMetrics(principleId: string, _metrics: PrincipleValueMetrics): PrincipleValueMetrics;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* WorkspaceContext - Centralized management of workspace-specific paths and services.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
|
|
2
|
+
import { validateWorkspaceDir } from './workspace-dir-validation.js';
|
|
3
|
+
|
|
4
|
+
export interface WorkspaceResolutionContext {
|
|
5
|
+
workspaceDir?: string;
|
|
6
|
+
agentId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkspaceResolutionOptions {
|
|
10
|
+
source?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
fallbackAgentId?: string;
|
|
13
|
+
logger?: PluginLogger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildResolutionFailureMessage(source: string, attempts: string[]): string {
|
|
17
|
+
const suffix = attempts.length > 0 ? ` Attempts: ${attempts.join(' | ')}` : '';
|
|
18
|
+
return `[PD:WorkspaceDir] ${source}: unable to resolve a valid workspace directory.${suffix}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tryResolveFromAgent(
|
|
22
|
+
api: OpenClawPluginApi,
|
|
23
|
+
agentId: string,
|
|
24
|
+
attempts: string[],
|
|
25
|
+
): string | undefined {
|
|
26
|
+
try {
|
|
27
|
+
const resolved = api.runtime.agent.resolveAgentWorkspaceDir(api.config, agentId);
|
|
28
|
+
const issue = validateWorkspaceDir(resolved);
|
|
29
|
+
if (!issue) {
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
attempts.push(`agent:${agentId} invalid (${issue})`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
attempts.push(`agent:${agentId} threw (${String(error)})`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveWorkspaceDir(
|
|
41
|
+
api: OpenClawPluginApi,
|
|
42
|
+
ctx: WorkspaceResolutionContext,
|
|
43
|
+
options: WorkspaceResolutionOptions = {},
|
|
44
|
+
): string | undefined {
|
|
45
|
+
const source = options.source ?? 'unknown';
|
|
46
|
+
const logger = options.logger ?? api.logger;
|
|
47
|
+
const attempts: string[] = [];
|
|
48
|
+
|
|
49
|
+
if (ctx.workspaceDir) {
|
|
50
|
+
const issue = validateWorkspaceDir(ctx.workspaceDir);
|
|
51
|
+
if (!issue) {
|
|
52
|
+
return ctx.workspaceDir;
|
|
53
|
+
}
|
|
54
|
+
attempts.push(`ctx.workspaceDir invalid (${issue})`);
|
|
55
|
+
} else {
|
|
56
|
+
attempts.push('ctx.workspaceDir missing');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agentCandidates = [ctx.agentId, options.fallbackAgentId]
|
|
60
|
+
.filter((value, index, all): value is string => !!value && all.indexOf(value) === index);
|
|
61
|
+
|
|
62
|
+
for (const agentId of agentCandidates) {
|
|
63
|
+
const resolved = tryResolveFromAgent(api, agentId, attempts);
|
|
64
|
+
if (resolved) {
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const message = buildResolutionFailureMessage(source, attempts);
|
|
70
|
+
if (options.required) {
|
|
71
|
+
logger.error(message);
|
|
72
|
+
throw new Error(message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.warn(message);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveRequiredWorkspaceDir(
|
|
80
|
+
api: OpenClawPluginApi,
|
|
81
|
+
ctx: WorkspaceResolutionContext,
|
|
82
|
+
options: Omit<WorkspaceResolutionOptions, 'required'> = {},
|
|
83
|
+
): string {
|
|
84
|
+
return resolveWorkspaceDir(api, ctx, { ...options, required: true }) as string;
|
|
85
|
+
}
|
|
@@ -1,152 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WorkspaceDir Validation Utilities
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* silently writing to the wrong directory.
|
|
4
|
+
* This module only validates candidate workspace directories and delegates
|
|
5
|
+
* actual resolution policy to workspace-dir-service.ts.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
/* eslint-disable no-unused-vars -- Reason: type definitions require param names that implementations may not use */
|
|
10
|
-
|
|
11
8
|
import * as os from 'os';
|
|
12
9
|
import type { PluginLogger } from '../openclaw-sdk.js';
|
|
10
|
+
import { resolveWorkspaceDir, type WorkspaceResolutionContext } from './workspace-dir-service.js';
|
|
13
11
|
|
|
14
|
-
/**
|
|
15
|
-
* Check if a path looks like a home directory (not a real workspace).
|
|
16
|
-
* Returns the reason if suspicious, or null if it looks valid.
|
|
17
|
-
*/
|
|
18
12
|
export function validateWorkspaceDir(dir: string | undefined): string | null {
|
|
19
13
|
if (!dir) {
|
|
20
14
|
return 'workspaceDir is undefined/null';
|
|
21
15
|
}
|
|
22
|
-
|
|
16
|
+
|
|
23
17
|
const homeDir = os.homedir();
|
|
24
|
-
|
|
25
|
-
// Home directory itself is not a valid workspace
|
|
18
|
+
|
|
26
19
|
if (dir === homeDir) {
|
|
27
20
|
return `workspaceDir equals home directory (${homeDir}), likely missing context field`;
|
|
28
21
|
}
|
|
29
|
-
|
|
30
|
-
// Root directory is definitely not a workspace
|
|
22
|
+
|
|
31
23
|
if (dir === '/' || dir === '') {
|
|
32
24
|
return `workspaceDir is root or empty: "${dir}"`;
|
|
33
25
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Common bad patterns:
|
|
26
|
+
|
|
27
|
+
const escapedHome = homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
28
|
const badPatterns = [
|
|
38
|
-
|
|
39
|
-
{ pattern: new RegExp(`^${
|
|
40
|
-
{ pattern: new RegExp(`^${homeDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/$`), desc: 'is home directory with trailing slash' },
|
|
29
|
+
{ pattern: new RegExp(`^${escapedHome}$`), desc: 'is home directory itself' },
|
|
30
|
+
{ pattern: new RegExp(`^${escapedHome}/$`), desc: 'is home directory with trailing slash' },
|
|
41
31
|
];
|
|
42
|
-
|
|
32
|
+
|
|
43
33
|
for (const { pattern, desc } of badPatterns) {
|
|
44
34
|
if (pattern.test(dir)) {
|
|
45
35
|
return `workspaceDir ${desc}: "${dir}"`;
|
|
46
36
|
}
|
|
47
37
|
}
|
|
48
|
-
|
|
49
|
-
return null;
|
|
38
|
+
|
|
39
|
+
return null;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* Returns the resolved path if successful and valid, or null.
|
|
55
|
-
*/
|
|
56
|
-
function tryResolveFromAgentId(
|
|
57
|
-
agentId: string,
|
|
42
|
+
export function resolveValidWorkspaceDir(
|
|
43
|
+
ctx: WorkspaceResolutionContext,
|
|
58
44
|
api: {
|
|
59
45
|
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
60
46
|
config: unknown;
|
|
47
|
+
logger: PluginLogger;
|
|
61
48
|
},
|
|
62
|
-
|
|
63
|
-
): string |
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
return resolved;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
onWarning(`failed to resolve from agentId: ${String(err)}`);
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
49
|
+
options?: { source?: string; fallbackAgentId?: string },
|
|
50
|
+
): string | undefined {
|
|
51
|
+
return resolveWorkspaceDir(api as never, ctx, {
|
|
52
|
+
source: options?.source,
|
|
53
|
+
fallbackAgentId: options?.fallbackAgentId,
|
|
54
|
+
logger: api.logger,
|
|
55
|
+
});
|
|
76
56
|
}
|
|
77
57
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*/
|
|
82
|
-
function validateFallback(path: string, onWarning: (msg: string) => void): string {
|
|
83
|
-
const issue = validateWorkspaceDir(path);
|
|
84
|
-
if (issue) {
|
|
85
|
-
onWarning(`FINAL FALLBACK "${path}" is also invalid: ${issue}. Events will be written to wrong location!`);
|
|
86
|
-
}
|
|
87
|
-
return path;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Resolve workspaceDir with validation and warning.
|
|
92
|
-
*
|
|
93
|
-
* Usage:
|
|
94
|
-
* const workspaceDir = resolveValidWorkspaceDir(ctx, api, { source: 'after_tool_call' });
|
|
95
|
-
*
|
|
96
|
-
* Fallback chain:
|
|
97
|
-
* 1. ctx.workspaceDir (validated)
|
|
98
|
-
* 2. api.runtime.agent.resolveAgentWorkspaceDir(config, ctx.agentId)
|
|
99
|
-
* 3. api.resolvePath('.') (last resort, warns loudly)
|
|
100
|
-
*/
|
|
101
|
-
export function resolveValidWorkspaceDir(
|
|
102
|
-
ctx: { workspaceDir?: string; agentId?: string },
|
|
58
|
+
export function logWorkspaceDirHealth(
|
|
59
|
+
ctx: WorkspaceResolutionContext,
|
|
60
|
+
source: string,
|
|
103
61
|
api: {
|
|
104
62
|
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
105
63
|
config: unknown;
|
|
106
|
-
resolvePath: (input: string) => string;
|
|
107
64
|
logger: PluginLogger;
|
|
108
65
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const source = options?.source || 'unknown';
|
|
112
|
-
const onWarning = options?.onWarning || ((msg: string) => api.logger.warn(`[PD:workspaceDir] ${msg}`));
|
|
113
|
-
|
|
114
|
-
// 1. Try ctx.workspaceDir
|
|
115
|
-
if (ctx.workspaceDir) {
|
|
116
|
-
const issue = validateWorkspaceDir(ctx.workspaceDir);
|
|
117
|
-
if (issue) {
|
|
118
|
-
onWarning(`${source}: ctx.workspaceDir="${ctx.workspaceDir}" is invalid: ${issue}`);
|
|
119
|
-
} else {
|
|
120
|
-
return ctx.workspaceDir;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 2. Try agentId resolution
|
|
125
|
-
if (ctx.agentId) {
|
|
126
|
-
const fromAgent = tryResolveFromAgentId(ctx.agentId, api, onWarning);
|
|
127
|
-
if (fromAgent) return fromAgent;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 3. Final fallback
|
|
131
|
-
return validateFallback(api.resolvePath('.'), onWarning);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Log workspaceDir resolution for debugging.
|
|
136
|
-
* Call this once during plugin startup to verify hook contexts.
|
|
137
|
-
*/
|
|
138
|
-
export function logWorkspaceDirHealth(ctx: { workspaceDir?: string; agentId?: string }, source: string, api: {
|
|
139
|
-
runtime: { agent: { resolveAgentWorkspaceDir: (config: unknown, agentId: string) => string } };
|
|
140
|
-
config: unknown;
|
|
141
|
-
resolvePath: (input: string) => string;
|
|
142
|
-
logger: PluginLogger;
|
|
143
|
-
}): void {
|
|
144
|
-
const resolved = resolveValidWorkspaceDir(ctx, api, { source });
|
|
66
|
+
): void {
|
|
67
|
+
const resolved = resolveValidWorkspaceDir(ctx, api, { source, fallbackAgentId: 'main' });
|
|
145
68
|
const issue = validateWorkspaceDir(resolved);
|
|
146
|
-
|
|
69
|
+
|
|
147
70
|
if (issue) {
|
|
148
71
|
api.logger.error(`[PD:health] ${source}: workspaceDir="${resolved}" - ${issue}`);
|
|
149
72
|
} else {
|
|
150
|
-
api.logger.info(`[PD:health] ${source}: workspaceDir="${resolved}"
|
|
73
|
+
api.logger.info(`[PD:health] ${source}: workspaceDir="${resolved}" OK`);
|
|
151
74
|
}
|
|
152
75
|
}
|
package/src/hooks/bash-risk.ts
CHANGED
|
@@ -38,12 +38,12 @@ export type BashRiskLevel = 'safe' | 'dangerous' | 'normal';
|
|
|
38
38
|
* @param logger - Optional logger for warnings about invalid patterns
|
|
39
39
|
* @returns The risk level: 'safe', 'dangerous', or 'normal'
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
export function analyzeBashCommand(
|
|
43
43
|
command: string,
|
|
44
44
|
safePatterns: string[],
|
|
45
45
|
dangerousPatterns: string[],
|
|
46
|
-
logger?: { warn?: (
|
|
46
|
+
logger?: { warn?: ( _message: string) => void }
|
|
47
47
|
): BashRiskLevel {
|
|
48
48
|
let normalizedCmd = command.trim().toLowerCase();
|
|
49
49
|
|
|
@@ -65,7 +65,7 @@ export function analyzeBashCommand(
|
|
|
65
65
|
// - Zero-width joiner (U+200D)
|
|
66
66
|
// - Word joiner (U+2060)
|
|
67
67
|
// - Zero-width invisible separator (U+FEFF)
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
const ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\u2060\uFEFF]/g;
|
|
70
70
|
if (ZERO_WIDTH_CHARS.test(command)) {
|
|
71
71
|
logger?.warn?.(`[PD_GATE] Bash command contains zero-width characters — blocking as dangerous`);
|
|
@@ -126,7 +126,7 @@ This is enforced by P-03 (精确匹配前验证原则).`;
|
|
|
126
126
|
* Handle edit tool verification before allowing operation
|
|
127
127
|
* This enforces P-03 at the tool layer
|
|
128
128
|
*/
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
export function handleEditVerification(
|
|
131
131
|
event: PluginHookBeforeToolCallEvent,
|
|
132
132
|
wctx: WorkspaceContext,
|
|
@@ -155,11 +155,11 @@ export function handleEditVerification(
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// 2. Resolve and read file
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
let absolutePath: string;
|
|
160
160
|
try {
|
|
161
161
|
absolutePath = wctx.resolve(filePath);
|
|
162
|
-
} catch (_error) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
162
|
+
} catch (_error) { // eslint-disable-line @typescript-eslint/no-unused-vars -- Reason: intentionally unused - let it fail naturally on path resolution error
|
|
163
163
|
// Path resolution error, let it fail naturally
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
@@ -222,7 +222,7 @@ export function handleEditVerification(
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// 3. Read current file content with improved error handling
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
let currentContent: string;
|
|
227
227
|
try {
|
|
228
228
|
currentContent = fs.readFileSync(absolutePath, 'utf-8');
|
|
@@ -48,9 +48,9 @@ export interface BlockContext {
|
|
|
48
48
|
export function recordGateBlockAndReturn(
|
|
49
49
|
wctx: WorkspaceContext,
|
|
50
50
|
blockCtx: BlockContext,
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
logger: { warn?: (_message: string) => void; error?: (_message: string) => void; info?: (_message: string) => void }
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
): PluginHookBeforeToolCallResult {
|
|
55
55
|
const { filePath, reason, toolName, sessionId, blockSource } = blockCtx;
|
|
56
56
|
|
|
@@ -93,7 +93,7 @@ export function recordGateBlockAndReturn(
|
|
|
93
93
|
wctx.trajectory?.recordGateBlock?.(trajectoryPayload);
|
|
94
94
|
} catch (error: unknown) {
|
|
95
95
|
logWarn(`[PD_GATE] Failed to record trajectory gate block: ${String(error)}`);
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
scheduleTrajectoryGateBlockRetry(wctx, trajectoryPayload, 1, logWarn, logError);
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -133,7 +133,7 @@ This is a mandatory security gate. The operation was blocked because the modific
|
|
|
133
133
|
* Uses exponential backoff with max retries.
|
|
134
134
|
* Failures are logged but do not affect the runtime block decision.
|
|
135
135
|
*/
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
function scheduleTrajectoryGateBlockRetry(
|
|
138
138
|
wctx: WorkspaceContext,
|
|
139
139
|
payload: {
|