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.
Files changed (37) hide show
  1. package/.planning/codebase/ARCHITECTURE.md +157 -0
  2. package/.planning/codebase/CONCERNS.md +145 -0
  3. package/.planning/codebase/CONVENTIONS.md +148 -0
  4. package/.planning/codebase/INTEGRATIONS.md +81 -0
  5. package/.planning/codebase/STACK.md +87 -0
  6. package/.planning/codebase/STRUCTURE.md +193 -0
  7. package/.planning/codebase/TESTING.md +243 -0
  8. package/openclaw.plugin.json +1 -1
  9. package/package.json +1 -1
  10. package/src/commands/pain.ts +12 -5
  11. package/src/commands/promote-impl.ts +13 -7
  12. package/src/commands/rollback.ts +10 -3
  13. package/src/core/event-log.ts +8 -6
  14. package/src/core/evolution-types.ts +33 -1
  15. package/src/hooks/message-sanitize.ts +18 -5
  16. package/src/hooks/prompt.ts +15 -4
  17. package/src/hooks/subagent.ts +2 -3
  18. package/src/http/principles-console-route.ts +21 -4
  19. package/src/service/evolution-worker.ts +89 -365
  20. package/src/service/queue-io.ts +375 -0
  21. package/src/service/queue-migration.ts +122 -0
  22. package/src/service/sleep-cycle.ts +157 -0
  23. package/src/service/subagent-workflow/runtime-direct-driver.ts +1 -1
  24. package/src/service/workflow-watchdog.ts +168 -0
  25. package/src/tools/deep-reflect.ts +22 -11
  26. package/src/types/event-payload.ts +80 -0
  27. package/src/types/queue.ts +70 -0
  28. package/src/utils/file-lock.ts +2 -2
  29. package/src/utils/io.ts +11 -3
  30. package/tests/core/evolution-migration.test.ts +325 -1
  31. package/tests/core/queue-purge.test.ts +337 -0
  32. package/tests/fixtures/legacy-queue-v1.json +74 -0
  33. package/tests/queue/async-lock.test.ts +200 -0
  34. package/tests/service/evolution-worker.queue.test.ts +296 -0
  35. package/tests/service/queue-io.test.ts +229 -0
  36. package/tests/service/queue-migration.test.ts +147 -0
  37. 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 (typeof msg.content === 'string') {
41
+ if (isAssistantMessageWithContent(msg)) {
27
42
  const sanitized = sanitizeAssistantText(msg.content);
28
43
  if (sanitized !== msg.content) {
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: message content is dynamically modified, type preserved from event.message union
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: message content is dynamically modified, type preserved from event.message union
43
- return { message: { ...msg, content: next } as any };
56
+ return { message: { ...msg, content: next } };
44
57
  }
45
58
 
46
59
  return;
@@ -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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: runtimeSubagent has structurally compatible shape but differs from workflow manager's subagent type
594
- subagent: runtimeSubagent as any,
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: api.runtime.subagent has structurally compatible shape but differs from workflow manager's subagent type
630
- subagent: api.runtime.subagent as any,
640
+
641
+ subagent: toWorkflowSubagent(api.runtime.subagent),
631
642
  });
632
643
 
633
644
  empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
@@ -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
- // eslint-disable-next-line @typescript-eslint/max-params
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
- } as unknown as PluginLogger;
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
- // eslint-disable-next-line @typescript-eslint/max-params
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
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
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
- // eslint-disable-next-line @typescript-eslint/init-declarations
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
- return providedToken === gatewayToken;
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
  /**