principles-disciple 1.41.0 → 1.42.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/pain.ts +12 -5
- package/src/commands/promote-impl.ts +13 -7
- package/src/commands/rollback.ts +10 -3
- package/src/core/event-log.ts +8 -6
- package/src/core/evolution-types.ts +33 -1
- package/src/hooks/message-sanitize.ts +18 -5
- package/src/hooks/prompt.ts +15 -4
- package/src/hooks/subagent.ts +2 -3
- package/src/http/principles-console-route.ts +21 -4
- package/src/service/evolution-worker.ts +89 -365
- package/src/service/queue-io.ts +375 -0
- package/src/service/queue-migration.ts +122 -0
- package/src/service/sleep-cycle.ts +157 -0
- package/src/service/subagent-workflow/runtime-direct-driver.ts +1 -1
- package/src/service/workflow-watchdog.ts +168 -0
- package/src/tools/deep-reflect.ts +22 -11
- 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/prompt.ts
CHANGED
|
@@ -20,6 +20,17 @@ import {
|
|
|
20
20
|
} from '../core/empathy-keyword-matcher.js';
|
|
21
21
|
import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
|
|
22
22
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
23
|
+
import type { PluginRuntimeSubagent } from '../service/subagent-workflow/runtime-direct-driver.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type assertion: OpenClaw SDK subagent -> workflow manager subagent type.
|
|
27
|
+
* Both types are structurally identical but come from different import paths.
|
|
28
|
+
*/
|
|
29
|
+
function toWorkflowSubagent(
|
|
30
|
+
subagent: NonNullable<OpenClawPluginApi['runtime']>['subagent']
|
|
31
|
+
): PluginRuntimeSubagent {
|
|
32
|
+
return subagent as unknown as PluginRuntimeSubagent;
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
// ---------------------------------------------------------------------------
|
|
25
36
|
// Static file cache — avoids re-reading rarely-changing files every message
|
|
@@ -590,8 +601,8 @@ The empathy observer subagent handles pain detection independently.
|
|
|
590
601
|
const empathyManager = new EmpathyObserverWorkflowManager({
|
|
591
602
|
workspaceDir,
|
|
592
603
|
logger: api.logger ?? console,
|
|
593
|
-
|
|
594
|
-
subagent: runtimeSubagent
|
|
604
|
+
|
|
605
|
+
subagent: toWorkflowSubagent(runtimeSubagent),
|
|
595
606
|
});
|
|
596
607
|
empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
|
|
597
608
|
parentSessionId: sessionId,
|
|
@@ -626,8 +637,8 @@ The empathy observer subagent handles pain detection independently.
|
|
|
626
637
|
const empathyManager = new EmpathyObserverWorkflowManager({
|
|
627
638
|
workspaceDir,
|
|
628
639
|
logger: api.logger ?? console,
|
|
629
|
-
|
|
630
|
-
subagent: api.runtime.subagent
|
|
640
|
+
|
|
641
|
+
subagent: toWorkflowSubagent(api.runtime.subagent),
|
|
631
642
|
});
|
|
632
643
|
|
|
633
644
|
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':
|
|
@@ -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
|
/**
|