principles-disciple 1.41.0 → 1.43.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/.planning/codebase/ARCHITECTURE.md +157 -0
- package/.planning/codebase/CONCERNS.md +145 -0
- package/.planning/codebase/CONVENTIONS.md +148 -0
- package/.planning/codebase/INTEGRATIONS.md +81 -0
- package/.planning/codebase/STACK.md +87 -0
- package/.planning/codebase/STRUCTURE.md +193 -0
- package/.planning/codebase/TESTING.md +243 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +5 -3
- package/src/commands/context.ts +1 -0
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/pain.ts +12 -5
- package/src/commands/principle-rollback.ts +1 -1
- package/src/commands/promote-impl.ts +13 -7
- package/src/commands/rollback.ts +10 -4
- package/src/commands/samples.ts +1 -1
- package/src/commands/thinking-os.ts +1 -0
- package/src/commands/workflow-debug.ts +1 -1
- package/src/core/config.ts +1 -0
- package/src/core/dictionary.ts +1 -0
- package/src/core/event-log.ts +8 -6
- package/src/core/evolution-types.ts +33 -1
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/merge-gate-audit.ts +3 -3
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-compliance.ts +21 -21
- package/src/core/nocturnal-executability.ts +1 -1
- package/src/core/nocturnal-reasoning-deriver.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +1 -1
- package/src/core/pain-context-extractor.ts +2 -2
- package/src/core/path-resolver.ts +1 -0
- package/src/core/pd-task-reconciler.ts +1 -0
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-internalization/principle-lifecycle-service.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-migration.ts +1 -1
- package/src/core/replay-engine.ts +1 -0
- package/src/core/risk-calculator.ts +2 -1
- package/src/core/rule-host.ts +1 -1
- package/src/core/session-tracker.ts +1 -0
- package/src/core/shadow-observation-registry.ts +1 -1
- package/src/core/thinking-models.ts +1 -1
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/trajectory.ts +2 -0
- package/src/hooks/bash-risk.ts +2 -2
- package/src/hooks/edit-verification.ts +3 -3
- package/src/hooks/gate.ts +8 -8
- package/src/hooks/gfi-gate.ts +2 -2
- package/src/hooks/lifecycle-routing.ts +1 -1
- package/src/hooks/message-sanitize.ts +18 -5
- package/src/hooks/pain.ts +2 -2
- package/src/hooks/progressive-trust-gate.ts +3 -3
- package/src/hooks/prompt.ts +17 -4
- package/src/hooks/subagent.ts +2 -3
- package/src/hooks/thinking-checkpoint.ts +1 -1
- package/src/http/principles-console-route.ts +21 -4
- package/src/service/central-database.ts +3 -2
- package/src/service/central-health-service.ts +2 -1
- package/src/service/central-overview-service.ts +3 -2
- package/src/service/control-ui-query-service.ts +2 -2
- package/src/service/event-log-auditor.ts +2 -2
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +96 -370
- package/src/service/health-query-service.ts +11 -10
- package/src/service/monitoring-query-service.ts +4 -4
- package/src/service/nocturnal-target-selector.ts +2 -2
- package/src/service/queue-io.ts +375 -0
- package/src/service/queue-migration.ts +122 -0
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/sleep-cycle.ts +157 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +1 -0
- package/src/service/subagent-workflow/runtime-direct-driver.ts +1 -1
- package/src/service/subagent-workflow/subagent-error-utils.ts +1 -1
- package/src/service/subagent-workflow/workflow-store.ts +3 -2
- package/src/service/workflow-watchdog.ts +168 -0
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +22 -11
- package/src/tools/model-index.ts +1 -1
- package/src/types/event-payload.ts +80 -0
- package/src/types/queue.ts +70 -0
- package/src/utils/file-lock.ts +2 -2
- package/src/utils/io.ts +11 -3
- package/tests/core/evolution-migration.test.ts +325 -1
- package/tests/core/queue-purge.test.ts +337 -0
- package/tests/fixtures/legacy-queue-v1.json +74 -0
- package/tests/queue/async-lock.test.ts +200 -0
- package/tests/service/evolution-worker.queue.test.ts +296 -0
- package/tests/service/queue-io.test.ts +229 -0
- package/tests/service/queue-migration.test.ts +147 -0
- package/tests/service/workflow-watchdog.test.ts +372 -0
|
@@ -6,6 +6,21 @@ const INTERNAL_TAG_PATTERNS = [
|
|
|
6
6
|
/<empathy\s+[^>]*\/?>(?:<\/empathy>)?/gi,
|
|
7
7
|
];
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Type predicate: true if msg is an assistant message with content.
|
|
11
|
+
* Used for safe narrowing after spread operations on message union.
|
|
12
|
+
*/
|
|
13
|
+
function isAssistantMessageWithContent(
|
|
14
|
+
msg: unknown
|
|
15
|
+
): msg is { role: 'assistant'; content: string } {
|
|
16
|
+
return (
|
|
17
|
+
typeof msg === 'object' &&
|
|
18
|
+
msg !== null &&
|
|
19
|
+
(msg as { role?: string }).role === 'assistant' &&
|
|
20
|
+
typeof (msg as { content?: unknown }).content === 'string'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export function sanitizeAssistantText(text: string): string {
|
|
10
25
|
let result = text;
|
|
11
26
|
for (const pattern of INTERNAL_TAG_PATTERNS) {
|
|
@@ -23,11 +38,10 @@ export function handleBeforeMessageWrite(
|
|
|
23
38
|
const msg = event.message as { role?: string; content?: unknown } | undefined;
|
|
24
39
|
if (!msg || msg.role !== 'assistant') return;
|
|
25
40
|
|
|
26
|
-
if (
|
|
41
|
+
if (isAssistantMessageWithContent(msg)) {
|
|
27
42
|
const sanitized = sanitizeAssistantText(msg.content);
|
|
28
43
|
if (sanitized !== msg.content) {
|
|
29
|
-
|
|
30
|
-
return { message: { ...msg, content: sanitized } as any };
|
|
44
|
+
return { message: { ...msg, content: sanitized } };
|
|
31
45
|
}
|
|
32
46
|
return;
|
|
33
47
|
}
|
|
@@ -39,8 +53,7 @@ export function handleBeforeMessageWrite(
|
|
|
39
53
|
}
|
|
40
54
|
return part;
|
|
41
55
|
});
|
|
42
|
-
|
|
43
|
-
return { message: { ...msg, content: next } as any };
|
|
56
|
+
return { message: { ...msg, content: next } };
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
return;
|
package/src/hooks/pain.ts
CHANGED
|
@@ -131,7 +131,7 @@ export function handleAfterToolCall(
|
|
|
131
131
|
|
|
132
132
|
// ── Trust Engine: Record failure ──
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
const errorType = extractErrorType(event.error || errorText);
|
|
136
136
|
const filePath = params.file_path || params.path || params.file;
|
|
137
137
|
const relPath = typeof filePath === 'string' ? normalizePath(filePath, effectiveWorkspaceDir) : 'unknown';
|
|
@@ -194,7 +194,7 @@ export function handleAfterToolCall(
|
|
|
194
194
|
const session = getSession(sessionId);
|
|
195
195
|
const toolFailureGfi = session?.gfiBySource?.tool_failure || 0;
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
let resetState: SessionState;
|
|
199
199
|
if (toolFailureGfi > 0) {
|
|
200
200
|
// Reduce tool_failure source by 50% (relief from successful tool execution)
|
|
@@ -87,7 +87,7 @@ export function buildEvolutionGateReason(
|
|
|
87
87
|
* Internal helper to call the shared block helper with progressive-trust-gate source tag.
|
|
88
88
|
*/
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
function block(
|
|
92
92
|
filePath: string,
|
|
93
93
|
reason: string,
|
|
@@ -121,7 +121,7 @@ function block(
|
|
|
121
121
|
* @returns PluginHookBeforeToolCallResult to block, or undefined to allow
|
|
122
122
|
*/
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
export function checkProgressiveTrustGate(
|
|
126
126
|
event: PluginHookBeforeToolCallEvent,
|
|
127
127
|
wctx: WorkspaceContext,
|
|
@@ -154,7 +154,7 @@ export function checkProgressiveTrustGate(
|
|
|
154
154
|
|
|
155
155
|
const currentTier = epDecision.currentTier ?? 1;
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
const tierName = getTierName(currentTier);
|
|
159
159
|
|
|
160
160
|
logger.info?.(`[PD_GATE] EP Gate: Tier ${currentTier} (${tierName}), Tool: ${event.toolName}, Risk: ${risky}, Allowed: ${epDecision.allowed}`);
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
1
3
|
import * as fs from 'fs';
|
|
2
4
|
import * as path from 'path';
|
|
3
5
|
import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult, PluginLogger, OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
@@ -20,6 +22,17 @@ import {
|
|
|
20
22
|
} from '../core/empathy-keyword-matcher.js';
|
|
21
23
|
import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
|
|
22
24
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
25
|
+
import type { PluginRuntimeSubagent } from '../service/subagent-workflow/runtime-direct-driver.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Type assertion: OpenClaw SDK subagent -> workflow manager subagent type.
|
|
29
|
+
* Both types are structurally identical but come from different import paths.
|
|
30
|
+
*/
|
|
31
|
+
function toWorkflowSubagent(
|
|
32
|
+
subagent: NonNullable<OpenClawPluginApi['runtime']>['subagent']
|
|
33
|
+
): PluginRuntimeSubagent {
|
|
34
|
+
return subagent as unknown as PluginRuntimeSubagent;
|
|
35
|
+
}
|
|
23
36
|
|
|
24
37
|
// ---------------------------------------------------------------------------
|
|
25
38
|
// Static file cache — avoids re-reading rarely-changing files every message
|
|
@@ -590,8 +603,8 @@ The empathy observer subagent handles pain detection independently.
|
|
|
590
603
|
const empathyManager = new EmpathyObserverWorkflowManager({
|
|
591
604
|
workspaceDir,
|
|
592
605
|
logger: api.logger ?? console,
|
|
593
|
-
|
|
594
|
-
subagent: runtimeSubagent
|
|
606
|
+
|
|
607
|
+
subagent: toWorkflowSubagent(runtimeSubagent),
|
|
595
608
|
});
|
|
596
609
|
empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
|
|
597
610
|
parentSessionId: sessionId,
|
|
@@ -626,8 +639,8 @@ The empathy observer subagent handles pain detection independently.
|
|
|
626
639
|
const empathyManager = new EmpathyObserverWorkflowManager({
|
|
627
640
|
workspaceDir,
|
|
628
641
|
logger: api.logger ?? console,
|
|
629
|
-
|
|
630
|
-
subagent: api.runtime.subagent
|
|
642
|
+
|
|
643
|
+
subagent: toWorkflowSubagent(api.runtime.subagent),
|
|
631
644
|
});
|
|
632
645
|
|
|
633
646
|
empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
|
package/src/hooks/subagent.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { WorkflowManager } from '../service/subagent-workflow/types.js';
|
|
|
14
14
|
* Used by the subagent_ended hook to dispatch lifecycle recovery to the right manager.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
function createWorkflowManagerForType(
|
|
19
19
|
workflowType: string,
|
|
20
20
|
workspaceDir: string,
|
|
@@ -25,9 +25,8 @@ function createWorkflowManagerForType(
|
|
|
25
25
|
info: (m: string) => logger.info(String(m)),
|
|
26
26
|
warn: (m: string) => logger.warn(String(m)),
|
|
27
27
|
error: (m: string) => logger.error(String(m)),
|
|
28
|
-
|
|
29
28
|
debug: () => { /* no-op */ },
|
|
30
|
-
}
|
|
29
|
+
};
|
|
31
30
|
|
|
32
31
|
switch (workflowType) {
|
|
33
32
|
case 'empathy-observer':
|
|
@@ -41,7 +41,7 @@ export interface ThinkingCheckpointConfig {
|
|
|
41
41
|
* @returns Block result if thinking required, undefined otherwise
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
export function checkThinkingCheckpoint(
|
|
46
46
|
event: PluginHookBeforeToolCallEvent,
|
|
47
47
|
config: ThinkingCheckpointConfig,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
1
2
|
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
@@ -96,7 +97,7 @@ function createService(api: OpenClawPluginApi): ControlUiQueryService {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
|
|
100
101
|
function handleApiRoute(
|
|
101
102
|
api: OpenClawPluginApi,
|
|
102
103
|
pathname: string,
|
|
@@ -105,13 +106,13 @@ function handleApiRoute(
|
|
|
105
106
|
): Promise<boolean> | boolean {
|
|
106
107
|
// Check authentication for API routes
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
|
|
109
110
|
if (!validateGatewayAuth(req)) {
|
|
110
111
|
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
111
112
|
return true;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
let service: ControlUiQueryService;
|
|
116
117
|
try {
|
|
117
118
|
service = createService(api);
|
|
@@ -566,7 +567,23 @@ function validateGatewayAuth(req: IncomingMessage): boolean {
|
|
|
566
567
|
const authHeader = (req.headers?.authorization as string) || '';
|
|
567
568
|
const tokenMatch = /^Bearer\s+(.+)$/i.exec(authHeader);
|
|
568
569
|
const providedToken = tokenMatch?.[1];
|
|
569
|
-
|
|
570
|
+
|
|
571
|
+
if (!providedToken) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Constant-time comparison to prevent timing attacks (per D-07)
|
|
576
|
+
// Use Buffer comparison — both tokens must be same length for timingSafeEqual
|
|
577
|
+
const providedBuffer = Buffer.from(providedToken, 'utf8');
|
|
578
|
+
const expectedBuffer = Buffer.from(gatewayToken, 'utf8');
|
|
579
|
+
|
|
580
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
581
|
+
// Length mismatch — fail fast but without timing leak
|
|
582
|
+
// Return false immediately rather than letting timingSafeEqual throw
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return crypto.timingSafeEqual(providedBuffer, expectedBuffer);
|
|
570
587
|
}
|
|
571
588
|
|
|
572
589
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
import Database from 'better-sqlite3';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import path from 'path';
|
|
@@ -200,7 +201,7 @@ export class CentralDatabase {
|
|
|
200
201
|
/**
|
|
201
202
|
* Sync data from a single workspace into the central database
|
|
202
203
|
*/
|
|
203
|
-
|
|
204
|
+
|
|
204
205
|
syncWorkspace(workspaceName: string): number {
|
|
205
206
|
const workspace = this.workspaces.find(w => w.name === workspaceName);
|
|
206
207
|
if (!workspace) {
|
|
@@ -714,7 +715,7 @@ export class CentralDatabase {
|
|
|
714
715
|
syncEnabled: c.sync_enabled === 1,
|
|
715
716
|
}));
|
|
716
717
|
}
|
|
717
|
-
|
|
718
|
+
|
|
718
719
|
|
|
719
720
|
updateWorkspaceConfig(
|
|
720
721
|
workspaceName: string,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
import { getCentralDatabase } from './central-database.js';
|
|
2
3
|
import { HealthQueryService } from './health-query-service.js';
|
|
3
4
|
|
|
@@ -18,7 +19,7 @@ export interface CentralHealthResponse {
|
|
|
18
19
|
*/
|
|
19
20
|
export class CentralHealthService {
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
getAllWorkspaceHealth(): CentralHealthResponse {
|
|
23
24
|
const centralDb = getCentralDatabase();
|
|
24
25
|
const workspaces: WorkspaceHealthEntry[] = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
import { getCentralDatabase, type CentralDatabase } from './central-database.js';
|
|
2
3
|
import { getThinkingModelDefinitions } from '../core/thinking-models.js';
|
|
3
4
|
import type { OverviewResponse } from './control-ui-query-service.js';
|
|
@@ -24,7 +25,7 @@ export class CentralOverviewService {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
dispose(): void {
|
|
29
30
|
// Do NOT dispose centralDb — it's a singleton shared across all requests.
|
|
30
31
|
// Individual services that open per-request connections (e.g. HealthQueryService)
|
|
@@ -62,7 +63,7 @@ export class CentralOverviewService {
|
|
|
62
63
|
|
|
63
64
|
// D-06: sampleQueue.counters from aggregated_correction_samples GROUP BY review_status
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
let sampleCounters: Record<string, number> = {};
|
|
67
68
|
try {
|
|
68
69
|
sampleCounters = this.centralDb.getSampleCountersByStatus();
|
|
@@ -255,7 +255,7 @@ export class ControlUiQueryService {
|
|
|
255
255
|
this.uiDb.dispose();
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
|
|
259
259
|
getOverview(days = 30): OverviewResponse {
|
|
260
260
|
const stats = this.trajectory.getDataStats();
|
|
261
261
|
const regressionRows = this.uiDb.all<{
|
|
@@ -400,7 +400,7 @@ export class ControlUiQueryService {
|
|
|
400
400
|
};
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
|
|
404
404
|
listSamples(filters: SampleListFilters = {}): SamplesResponse {
|
|
405
405
|
const page = Math.max(1, Number(filters.page ?? 1));
|
|
406
406
|
const pageSize = clampPageSize(filters.pageSize);
|
|
@@ -137,7 +137,7 @@ function countAllHooks(filePath: string): Record<string, number> {
|
|
|
137
137
|
* @param openclawDir - Base OpenClaw directory (e.g., ~/.openclaw)
|
|
138
138
|
* @param expectedToolHooks - Hook names that should appear in the primary workspace
|
|
139
139
|
*/
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
export async function auditEventLogs(
|
|
142
142
|
openclawDir: string,
|
|
143
143
|
expectedToolHooks: string[] = ['before_tool_call', 'after_tool_call'],
|
|
@@ -210,7 +210,7 @@ export async function auditEventLogs(
|
|
|
210
210
|
/**
|
|
211
211
|
* Format audit report for display.
|
|
212
212
|
*/
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
export function formatAuditReport(report: AuditReport): string {
|
|
215
215
|
const lines: string[] = [];
|
|
216
216
|
|
|
@@ -155,7 +155,7 @@ export class EvolutionQueryService {
|
|
|
155
155
|
* 注意:不关闭 trajectory,因为它是单例由 TrajectoryRegistry 管理
|
|
156
156
|
*/
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
dispose(): void {
|
|
160
160
|
// EvolutionQueryService 不拥有 trajectory,所以不关闭它
|
|
161
161
|
// trajectory 是由 TrajectoryRegistry 管理的单例
|