principles-disciple 1.183.0 → 1.184.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.
@@ -53,8 +53,8 @@ export interface EvolutionQueueItem {
53
53
  resultRef?: string;
54
54
  painEventId?: number;
55
55
  }
56
- import { migrateToV2, isLegacyQueueItem, migrateQueueToV2, LegacyEvolutionQueueItem, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES, type RawQueueItem } from './queue-migration.js';
57
- export { migrateToV2, isLegacyQueueItem, migrateQueueToV2, LegacyEvolutionQueueItem, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES };
56
+ import { migrateToV2, isLegacyQueueItem, migrateQueueToV2, LegacyEvolutionQueueItem, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES, validateQueueItem, VALID_TASK_KINDS, isValidQueueItem, type RawQueueItem } from './queue-migration.js';
57
+ export { migrateToV2, isLegacyQueueItem, migrateQueueToV2, LegacyEvolutionQueueItem, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES, validateQueueItem, VALID_TASK_KINDS, isValidQueueItem };
58
58
  export type { RawQueueItem };
59
59
  export declare function extractEvolutionTaskId(task: string): string | null;
60
60
  export declare function purgeStaleFailedTasks(queue: EvolutionQueueItem[], logger: PluginLogger): {
@@ -11,7 +11,7 @@ import { atomicWriteFileSync } from '../utils/io.js';
11
11
  // Re-export queue I/O (extracted to queue-io.ts)
12
12
  export { loadEvolutionQueue, saveEvolutionQueue, withQueueLock, acquireQueueLock, requireQueueLock } from './queue-io.js';
13
13
  export { EVOLUTION_QUEUE_LOCK_SUFFIX, LOCK_MAX_RETRIES, LOCK_RETRY_DELAY_MS, LOCK_STALE_MS } from './queue-io.js';
14
- import { saveEvolutionQueue, requireQueueLock } from './queue-io.js';
14
+ import { saveEvolutionQueue, requireQueueLock, loadEvolutionQueue } from './queue-io.js';
15
15
  import { WorkflowStore } from './subagent-workflow/workflow-store.js';
16
16
  import { PrincipleCompiler } from '../core/principle-compiler/index.js';
17
17
  import { loadLedger, updatePrinciple } from '../core/principle-tree-ledger.js';
@@ -58,8 +58,8 @@ import { runWorkflowWatchdog } from './workflow-watchdog.js';
58
58
  export { runWorkflowWatchdog };
59
59
  let timeoutId = null;
60
60
  // ── Queue Migration (extracted to queue-migration.ts) ────────────────────────
61
- import { migrateToV2, isLegacyQueueItem, migrateQueueToV2, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES } from './queue-migration.js';
62
- export { migrateToV2, isLegacyQueueItem, migrateQueueToV2, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES };
61
+ import { migrateToV2, isLegacyQueueItem, migrateQueueToV2, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES, validateQueueItem, VALID_TASK_KINDS, isValidQueueItem } from './queue-migration.js';
62
+ export { migrateToV2, isLegacyQueueItem, migrateQueueToV2, DEFAULT_TASK_KIND, DEFAULT_PRIORITY, DEFAULT_MAX_RETRIES, validateQueueItem, VALID_TASK_KINDS, isValidQueueItem };
63
63
  // Queue lock constants and requireQueueLock are imported from queue-io.ts
64
64
  export function extractEvolutionTaskId(task) {
65
65
  if (!task)
@@ -302,30 +302,12 @@ async function processEvolutionQueue(wctx, logger, _eventLog, _api) {
302
302
  logger?.info?.(`[PD:EvolutionWorker] Dropped ${beforeLegacyPainDrop - queue.length} legacy pain_diagnosis queue item(s); use PainSignalBridge/pd pain record`);
303
303
  }
304
304
  // Validate queue items — filter out malformed entries before processing.
305
- // Malformed items are logged + skipped; they never crash the evolution cycle.
305
+ // rc-1/rc-4 (ERR-007): consolidated in validateQueueItem (queue-migration.ts)
306
+ // so every load path applies the same filter. Malformed items are logged +
307
+ // skipped; they never crash the evolution cycle.
306
308
  const beforeValidation = queue.length;
307
309
  queue = queue.filter((item) => {
308
- const errors = [];
309
- if (!item.id || typeof item.id !== 'string')
310
- errors.push('missing/invalid id');
311
- if (!item.source || typeof item.source !== 'string')
312
- errors.push('missing/invalid source');
313
- if (typeof item.score !== 'number')
314
- errors.push('missing/invalid score');
315
- if (!item.status || typeof item.status !== 'string')
316
- errors.push('missing/invalid status');
317
- if (!item.taskKind || typeof item.taskKind !== 'string')
318
- errors.push('missing/invalid taskKind');
319
- else {
320
- const validTaskKinds = ['model_eval'];
321
- if (!validTaskKinds.includes(item.taskKind)) {
322
- errors.push(`invalid taskKind value '${item.taskKind}' (expected one of: ${validTaskKinds.join(', ')})`);
323
- }
324
- }
325
- if (typeof item.retryCount !== 'number')
326
- errors.push('missing/invalid retryCount');
327
- if (typeof item.maxRetries !== 'number')
328
- errors.push('missing/invalid maxRetries');
310
+ const errors = validateQueueItem(item);
329
311
  if (errors.length > 0) {
330
312
  logger?.warn?.(`[PD:EvolutionWorker] Skipping malformed queue item: ${errors.join(', ')} | ${JSON.stringify(item).slice(0, 200)}`);
331
313
  SystemLogger.log(wctx.workspaceDir, 'QUEUE_ITEM_MALFORMED', `Skipped: ${errors.join(', ')} | id=${item.id || 'N/A'}`);
@@ -361,16 +343,16 @@ export async function registerEvolutionTaskSession(workspaceResolve, taskId, ses
361
343
  return false;
362
344
  const releaseLock = await requireQueueLock(queuePath, logger, 'registerEvolutionTaskSession');
363
345
  try {
364
- let rawQueue;
365
- try {
366
- rawQueue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
367
- }
368
- catch (parseErr) {
369
- logger?.warn?.(`[PD:EvolutionWorker] Failed to parse EVOLUTION_QUEUE for session registration: ${queuePath} - ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
370
- return false;
371
- }
372
- // V2: Migrate queue to current schema
373
- const queue = migrateQueueToV2(rawQueue);
346
+ // loadEvolutionQueue handles JSON parse + migrate + validate (rc-1/rc-4).
347
+ // Previously this inlined JSON.parse + migrate cast without validating,
348
+ // so malformed queue items could reach the find() below. Reusing the
349
+ // shared loader keeps a single chokepoint for queue validation.
350
+ // Note: loadEvolutionQueue returns core/evolution-types.EvolutionQueueItem,
351
+ // which saveEvolutionQueue also accepts. We deliberately avoid annotating
352
+ // with the local EvolutionQueueItem interface (line 87) to prevent the
353
+ // known type-drift between the two definitions (see follow-up: unify the
354
+ // three EvolutionQueueItem copies).
355
+ const queue = loadEvolutionQueue(queuePath);
374
356
  const task = queue.find((item) => item.id === taskId && item.status === 'in_progress');
375
357
  if (!task) {
376
358
  logger?.warn?.(`[PD:EvolutionWorker] Could not find in-progress evolution task ${taskId} for session assignment`);
@@ -11,7 +11,7 @@ import { createHash } from 'crypto';
11
11
  import { acquireLockAsync, releaseLock as releaseImportedLock } from '../utils/file-lock.js';
12
12
  import { atomicWriteFileSync } from '../utils/io.js';
13
13
  import { LockUnavailableError } from '../config/errors.js';
14
- import { migrateQueueToV2 } from './queue-migration.js';
14
+ import { migrateQueueToV2, validateQueueItem } from './queue-migration.js';
15
15
  export const EVOLUTION_QUEUE_LOCK_SUFFIX = '.lock';
16
16
  export const LOCK_MAX_RETRIES = 50;
17
17
  export const LOCK_RETRY_DELAY_MS = 50;
@@ -72,7 +72,19 @@ export function loadEvolutionQueue(queuePath) {
72
72
  rawQueue = [];
73
73
  }
74
74
  }
75
- return migrateQueueToV2(rawQueue);
75
+ // rc-1/rc-4 (ERR-007): every element of the parsed array is untrusted disk
76
+ // JSON — validate before returning. Malformed items are filtered out and
77
+ // reported via console.warn (rc-9: no silent drop). This is the single
78
+ // chokepoint that loadEvolutionQueue callers inherit.
79
+ const migrated = migrateQueueToV2(rawQueue);
80
+ return migrated.filter((item) => {
81
+ const errors = validateQueueItem(item);
82
+ if (errors.length > 0) {
83
+ console.warn(`[queue-io] Dropping malformed queue item: ${errors.join(', ')} | id=${item?.id ?? 'N/A'}`);
84
+ return false;
85
+ }
86
+ return true;
87
+ });
76
88
  }
77
89
  export function saveEvolutionQueue(queuePath, queue) {
78
90
  atomicWriteFileSync(queuePath, JSON.stringify(queue, null, 2));
@@ -81,3 +81,28 @@ export declare function isLegacyQueueItem(item: RawQueueItem): boolean;
81
81
  * Returns a new array with all items migrated to V2 format.
82
82
  */
83
83
  export declare function migrateQueueToV2(queue: RawQueueItem[]): EvolutionQueueItem[];
84
+ /**
85
+ * Canonical set of valid taskKind values. Single source of truth — mirrors
86
+ * TaskKind in trajectory-types.ts. Previously the inline validator in
87
+ * evolution-worker.ts hard-coded `['model_eval']`, which silently rejected
88
+ * three legitimate taskKind values (pain_diagnosis / sleep_reflection /
89
+ * keyword_optimization). Keep this in sync with TaskKind.
90
+ */
91
+ export declare const VALID_TASK_KINDS: readonly TaskKind[];
92
+ /**
93
+ * Validate a single queue item loaded from untrusted disk JSON.
94
+ *
95
+ * Returns an array of human-readable error reasons; an empty array means the
96
+ * item is valid. Use {@link isValidQueueItem} for a type-guard form.
97
+ *
98
+ * This consolidates the inline validation that lived in processEvolutionQueue
99
+ * (evolution-worker.ts) so every queue load path (loadEvolutionQueue,
100
+ * registerEvolutionTaskSession) applies the same filter. rc-4 (ERR-007):
101
+ * array elements from parsed JSON must be element-wise validated.
102
+ */
103
+ export declare function validateQueueItem(item: unknown): string[];
104
+ /**
105
+ * Type-guard convenience over {@link validateQueueItem}.
106
+ * Drops the reason strings (callers that need them should call validateQueueItem).
107
+ */
108
+ export declare function isValidQueueItem(item: unknown): item is EvolutionQueueItem;
@@ -54,3 +54,60 @@ export function isLegacyQueueItem(item) {
54
54
  export function migrateQueueToV2(queue) {
55
55
  return queue.map(item => isLegacyQueueItem(item) ? migrateToV2(item) : item);
56
56
  }
57
+ // ── Validation (rc-1/rc-2, rc-4: validate array elements from untrusted JSON) ─
58
+ /**
59
+ * Canonical set of valid taskKind values. Single source of truth — mirrors
60
+ * TaskKind in trajectory-types.ts. Previously the inline validator in
61
+ * evolution-worker.ts hard-coded `['model_eval']`, which silently rejected
62
+ * three legitimate taskKind values (pain_diagnosis / sleep_reflection /
63
+ * keyword_optimization). Keep this in sync with TaskKind.
64
+ */
65
+ export const VALID_TASK_KINDS = [
66
+ 'pain_diagnosis',
67
+ 'sleep_reflection',
68
+ 'model_eval',
69
+ 'keyword_optimization',
70
+ ];
71
+ /**
72
+ * Validate a single queue item loaded from untrusted disk JSON.
73
+ *
74
+ * Returns an array of human-readable error reasons; an empty array means the
75
+ * item is valid. Use {@link isValidQueueItem} for a type-guard form.
76
+ *
77
+ * This consolidates the inline validation that lived in processEvolutionQueue
78
+ * (evolution-worker.ts) so every queue load path (loadEvolutionQueue,
79
+ * registerEvolutionTaskSession) applies the same filter. rc-4 (ERR-007):
80
+ * array elements from parsed JSON must be element-wise validated.
81
+ */
82
+ export function validateQueueItem(item) {
83
+ const errors = [];
84
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) {
85
+ return ['item is not an object'];
86
+ }
87
+ const it = item;
88
+ if (!it.id || typeof it.id !== 'string')
89
+ errors.push('missing/invalid id');
90
+ if (!it.source || typeof it.source !== 'string')
91
+ errors.push('missing/invalid source');
92
+ if (typeof it.score !== 'number')
93
+ errors.push('missing/invalid score');
94
+ if (!it.status || typeof it.status !== 'string')
95
+ errors.push('missing/invalid status');
96
+ if (!it.taskKind || typeof it.taskKind !== 'string')
97
+ errors.push('missing/invalid taskKind');
98
+ else if (!VALID_TASK_KINDS.includes(it.taskKind)) {
99
+ errors.push(`invalid taskKind value '${it.taskKind}' (expected one of: ${VALID_TASK_KINDS.join(', ')})`);
100
+ }
101
+ if (typeof it.retryCount !== 'number')
102
+ errors.push('missing/invalid retryCount');
103
+ if (typeof it.maxRetries !== 'number')
104
+ errors.push('missing/invalid maxRetries');
105
+ return errors;
106
+ }
107
+ /**
108
+ * Type-guard convenience over {@link validateQueueItem}.
109
+ * Drops the reason strings (callers that need them should call validateQueueItem).
110
+ */
111
+ export function isValidQueueItem(item) {
112
+ return validateQueueItem(item).length === 0;
113
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.183.0",
5
+ "version": "1.184.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.183.0",
3
+ "version": "1.184.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",