monomind 1.11.14 → 1.12.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 (172) hide show
  1. package/.claude/agents/generated/channel-intelligence-director.md +87 -0
  2. package/.claude/agents/generated/chief-growth-officer.md +88 -0
  3. package/.claude/agents/generated/content-seo-strategist.md +90 -0
  4. package/.claude/agents/generated/developer-community-strategist.md +91 -0
  5. package/.claude/agents/generated/outreach-partnership-strategist.md +90 -0
  6. package/.claude/agents/generated/social-media-strategist.md +91 -0
  7. package/.claude/agents/generated/video-visual-strategist.md +90 -0
  8. package/.claude/commands/mastermind/master.md +1 -1
  9. package/.claude/helpers/auto-memory-hook.mjs +13 -4
  10. package/.claude/helpers/control-start.cjs +5 -0
  11. package/.claude/helpers/event-logger.cjs +114 -0
  12. package/.claude/helpers/handlers/adr-draft-handler.cjs +19 -5
  13. package/.claude/helpers/handlers/agent-start-handler.cjs +13 -4
  14. package/.claude/helpers/handlers/compact-handler.cjs +2 -0
  15. package/.claude/helpers/handlers/edit-handler.cjs +1 -1
  16. package/.claude/helpers/handlers/gates-handler.cjs +3 -0
  17. package/.claude/helpers/handlers/graph-status-handler.cjs +14 -8
  18. package/.claude/helpers/handlers/loops-status-handler.cjs +5 -2
  19. package/.claude/helpers/handlers/route-handler.cjs +13 -6
  20. package/.claude/helpers/handlers/session-handler.cjs +11 -4
  21. package/.claude/helpers/handlers/session-restore-handler.cjs +21 -11
  22. package/.claude/helpers/handlers/task-handler.cjs +13 -5
  23. package/.claude/helpers/intelligence.cjs +7 -2
  24. package/.claude/helpers/loop-tracker.cjs +15 -3
  25. package/.claude/helpers/memory.cjs +6 -1
  26. package/.claude/helpers/router.cjs +5 -2
  27. package/.claude/helpers/session.cjs +2 -0
  28. package/.claude/helpers/statusline.cjs +10 -2
  29. package/.claude/helpers/utils/micro-agents.cjs +20 -4
  30. package/.claude/scheduled_tasks.lock +1 -1
  31. package/.claude/settings.json +92 -1
  32. package/.claude/skills/mastermind/_protocol.md +25 -15
  33. package/.claude/skills/mastermind/architect.md +3 -3
  34. package/.claude/skills/mastermind/autodev.md +4 -2
  35. package/.claude/skills/mastermind/idea.md +10 -0
  36. package/.claude/skills/mastermind/ops.md +3 -3
  37. package/.claude/skills/mastermind/runorg.md +153 -86
  38. package/package.json +20 -3
  39. package/packages/@monomind/cli/dist/src/agents/registry-builder.js +2 -0
  40. package/packages/@monomind/cli/dist/src/autopilot-state.js +10 -5
  41. package/packages/@monomind/cli/dist/src/benchmarks/benchmark-runner.js +13 -0
  42. package/packages/@monomind/cli/dist/src/benchmarks/metric-evaluators.js +20 -9
  43. package/packages/@monomind/cli/dist/src/browser/actions.js +10 -3
  44. package/packages/@monomind/cli/dist/src/browser/browser.js +12 -2
  45. package/packages/@monomind/cli/dist/src/browser/cdp.js +21 -3
  46. package/packages/@monomind/cli/dist/src/browser/har.js +27 -5
  47. package/packages/@monomind/cli/dist/src/commands/agent.js +11 -8
  48. package/packages/@monomind/cli/dist/src/commands/analyze.js +36 -21
  49. package/packages/@monomind/cli/dist/src/commands/autopilot.js +12 -4
  50. package/packages/@monomind/cli/dist/src/commands/benchmark.js +51 -8
  51. package/packages/@monomind/cli/dist/src/commands/browse.js +5 -2
  52. package/packages/@monomind/cli/dist/src/commands/claims.js +29 -11
  53. package/packages/@monomind/cli/dist/src/commands/cleanup.js +25 -5
  54. package/packages/@monomind/cli/dist/src/commands/config.js +15 -7
  55. package/packages/@monomind/cli/dist/src/commands/daemon.js +6 -0
  56. package/packages/@monomind/cli/dist/src/commands/deployment.js +34 -19
  57. package/packages/@monomind/cli/dist/src/commands/doctor.js +97 -20
  58. package/packages/@monomind/cli/dist/src/commands/guidance.js +15 -2
  59. package/packages/@monomind/cli/dist/src/commands/hive-mind.js +37 -14
  60. package/packages/@monomind/cli/dist/src/commands/hooks.js +42 -25
  61. package/packages/@monomind/cli/dist/src/commands/init.js +9 -4
  62. package/packages/@monomind/cli/dist/src/commands/issues.js +29 -26
  63. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -5
  64. package/packages/@monomind/cli/dist/src/commands/memory.js +10 -0
  65. package/packages/@monomind/cli/dist/src/commands/migrate.js +5 -5
  66. package/packages/@monomind/cli/dist/src/commands/monograph.js +18 -5
  67. package/packages/@monomind/cli/dist/src/commands/monovector/backup.js +8 -2
  68. package/packages/@monomind/cli/dist/src/commands/monovector/benchmark.js +20 -7
  69. package/packages/@monomind/cli/dist/src/commands/monovector/import.js +15 -0
  70. package/packages/@monomind/cli/dist/src/commands/monovector/migrate.js +4 -1
  71. package/packages/@monomind/cli/dist/src/commands/monovector/optimize.js +11 -0
  72. package/packages/@monomind/cli/dist/src/commands/monovector/setup.js +11 -1
  73. package/packages/@monomind/cli/dist/src/commands/neural.js +1 -1
  74. package/packages/@monomind/cli/dist/src/commands/performance.js +20 -7
  75. package/packages/@monomind/cli/dist/src/commands/platforms.js +90 -8
  76. package/packages/@monomind/cli/dist/src/commands/plugins.js +12 -5
  77. package/packages/@monomind/cli/dist/src/commands/process.js +33 -10
  78. package/packages/@monomind/cli/dist/src/commands/progress.js +5 -3
  79. package/packages/@monomind/cli/dist/src/commands/providers.js +5 -5
  80. package/packages/@monomind/cli/dist/src/commands/replay.js +8 -2
  81. package/packages/@monomind/cli/dist/src/commands/route.js +27 -7
  82. package/packages/@monomind/cli/dist/src/commands/security.js +4 -0
  83. package/packages/@monomind/cli/dist/src/commands/session.js +12 -1
  84. package/packages/@monomind/cli/dist/src/commands/start.js +11 -4
  85. package/packages/@monomind/cli/dist/src/commands/status.js +7 -4
  86. package/packages/@monomind/cli/dist/src/commands/swarm.js +27 -13
  87. package/packages/@monomind/cli/dist/src/commands/task.js +26 -11
  88. package/packages/@monomind/cli/dist/src/commands/tokens.js +7 -2
  89. package/packages/@monomind/cli/dist/src/commands/transfer-store.js +36 -22
  90. package/packages/@monomind/cli/dist/src/commands/update.js +15 -3
  91. package/packages/@monomind/cli/dist/src/commands/workflow.js +39 -6
  92. package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +18 -7
  93. package/packages/@monomind/cli/dist/src/consensus/vote-signer.js +25 -8
  94. package/packages/@monomind/cli/dist/src/index.js +7 -3
  95. package/packages/@monomind/cli/dist/src/init/executor.js +14 -11
  96. package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.js +20 -4
  97. package/packages/@monomind/cli/dist/src/init/statusline-generator.js +36 -15
  98. package/packages/@monomind/cli/dist/src/mcp-tools/a2a-tools.js +98 -13
  99. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +16 -3
  100. package/packages/@monomind/cli/dist/src/mcp-tools/analyze-tools.js +80 -17
  101. package/packages/@monomind/cli/dist/src/mcp-tools/browser-tools.js +84 -22
  102. package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +35 -7
  103. package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +82 -17
  104. package/packages/@monomind/cli/dist/src/mcp-tools/coordination-tools.js +37 -4
  105. package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +49 -7
  106. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +45 -18
  107. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +75 -25
  108. package/packages/@monomind/cli/dist/src/mcp-tools/guidance-tools.js +32 -10
  109. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +91 -20
  110. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +188 -29
  111. package/packages/@monomind/cli/dist/src/mcp-tools/memory-tools.js +25 -7
  112. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +11 -2
  113. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +148 -26
  114. package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.js +44 -9
  115. package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +45 -10
  116. package/packages/@monomind/cli/dist/src/mcp-tools/progress-tools.js +7 -4
  117. package/packages/@monomind/cli/dist/src/mcp-tools/request-tracker.js +15 -1
  118. package/packages/@monomind/cli/dist/src/mcp-tools/security-tools.js +61 -9
  119. package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +45 -14
  120. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +15 -3
  121. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +14 -7
  122. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +52 -10
  123. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +40 -6
  124. package/packages/@monomind/cli/dist/src/mcp-tools/transfer-tools.js +37 -4
  125. package/packages/@monomind/cli/dist/src/mcp-tools/workflow-tools.js +29 -6
  126. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +26 -10
  127. package/packages/@monomind/cli/dist/src/memory/intelligence.js +80 -19
  128. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +21 -2
  129. package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +67 -3
  130. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.js +14 -4
  131. package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +43 -7
  132. package/packages/@monomind/cli/dist/src/monovector/coverage-router.js +8 -4
  133. package/packages/@monomind/cli/dist/src/monovector/coverage-tools.js +6 -3
  134. package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +13 -0
  135. package/packages/@monomind/cli/dist/src/monovector/route-outcomes.d.ts +2 -1
  136. package/packages/@monomind/cli/dist/src/monovector/route-outcomes.js +46 -4
  137. package/packages/@monomind/cli/dist/src/plugins/manager.js +8 -3
  138. package/packages/@monomind/cli/dist/src/plugins/store/discovery.js +46 -2
  139. package/packages/@monomind/cli/dist/src/plugins/store/search.js +5 -4
  140. package/packages/@monomind/cli/dist/src/production/circuit-breaker.js +17 -3
  141. package/packages/@monomind/cli/dist/src/production/error-handler.js +3 -0
  142. package/packages/@monomind/cli/dist/src/production/monitoring.js +20 -3
  143. package/packages/@monomind/cli/dist/src/production/rate-limiter.js +13 -4
  144. package/packages/@monomind/cli/dist/src/production/retry.js +17 -9
  145. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +6 -2
  146. package/packages/@monomind/cli/dist/src/routing/embedder.js +0 -0
  147. package/packages/@monomind/cli/dist/src/routing/llm-caller.js +13 -2
  148. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +18 -3
  149. package/packages/@monomind/cli/dist/src/services/claim-service.d.ts +1 -0
  150. package/packages/@monomind/cli/dist/src/services/claim-service.js +8 -0
  151. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +14 -2
  152. package/packages/@monomind/cli/dist/src/services/headless-worker-executor.js +18 -2
  153. package/packages/@monomind/cli/dist/src/services/worker-daemon.js +53 -12
  154. package/packages/@monomind/cli/dist/src/transfer/anonymization/index.d.ts +0 -3
  155. package/packages/@monomind/cli/dist/src/transfer/anonymization/index.js +16 -1
  156. package/packages/@monomind/cli/dist/src/transfer/export.js +8 -0
  157. package/packages/@monomind/cli/dist/src/transfer/ipfs/upload.js +33 -3
  158. package/packages/@monomind/cli/dist/src/transfer/serialization/cfp.js +9 -3
  159. package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +37 -3
  160. package/packages/@monomind/cli/dist/src/transfer/store/discovery.js +45 -3
  161. package/packages/@monomind/cli/dist/src/transfer/store/download.js +5 -0
  162. package/packages/@monomind/cli/dist/src/transfer/store/publish.js +13 -1
  163. package/packages/@monomind/cli/dist/src/transfer/store/registry.d.ts +8 -0
  164. package/packages/@monomind/cli/dist/src/transfer/store/registry.js +30 -5
  165. package/packages/@monomind/cli/dist/src/transfer/store/search.js +20 -5
  166. package/packages/@monomind/cli/dist/src/update/checker.js +59 -7
  167. package/packages/@monomind/cli/dist/src/update/executor.js +50 -3
  168. package/packages/@monomind/cli/dist/src/update/index.js +18 -1
  169. package/packages/@monomind/cli/dist/src/update/rate-limiter.d.ts +6 -0
  170. package/packages/@monomind/cli/dist/src/update/rate-limiter.js +79 -7
  171. package/packages/@monomind/cli/dist/src/update/validator.js +52 -1
  172. package/packages/@monomind/cli/package.json +2 -3
@@ -3,6 +3,7 @@
3
3
  * Shared JSON config file persistence with atomic writes and Zod validation
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as os from 'os';
6
7
  import * as path from 'path';
7
8
  /** Config file search paths in priority order */
8
9
  const CONFIG_FILENAMES = [
@@ -123,7 +124,7 @@ export class ConfigFileManager {
123
124
  * drop the first writer's API key.
124
125
  */
125
126
  set(cwd, key, value) {
126
- const KNOWN_SET_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks', 'neural']);
127
+ const KNOWN_SET_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks', 'neural', 'providers']);
127
128
  const topSection = String(key).split('.')[0];
128
129
  if (!KNOWN_SET_SECTIONS.has(topSection)) {
129
130
  throw new Error(`Unknown config section: "${topSection}". Allowed: ${[...KNOWN_SET_SECTIONS].join(', ')}`);
@@ -181,6 +182,17 @@ export class ConfigFileManager {
181
182
  /** Import config from a specific path */
182
183
  importFrom(cwd, importPath) {
183
184
  const resolved = path.resolve(cwd, importPath);
185
+ // Guard against path traversal: the resolved path must be within the
186
+ // project cwd or the user's home directory. Without this check an
187
+ // automated script can pass "/etc/passwd" or "../../.env" and exfiltrate
188
+ // files outside the project tree.
189
+ const projectRoot = path.resolve(cwd);
190
+ const home = os.homedir();
191
+ const isUnderProject = resolved === projectRoot || resolved.startsWith(projectRoot + path.sep);
192
+ const isUnderHome = resolved === home || resolved.startsWith(home + path.sep);
193
+ if (!isUnderProject && !isUnderHome) {
194
+ throw new Error(`Import path must be within the project directory or home directory: ${resolved}`);
195
+ }
184
196
  if (!fs.existsSync(resolved)) {
185
197
  throw new Error(`Import file not found: ${resolved}`);
186
198
  }
@@ -199,7 +211,7 @@ export class ConfigFileManager {
199
211
  // KNOWN_SECTIONS only validates top-level keys, leaving nested
200
212
  // {agents:{providers:[{__proto__:{...}}]}} unsanitized.
201
213
  const imported = sanitizeConfigObject(importedRaw);
202
- const KNOWN_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks']);
214
+ const KNOWN_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks', 'neural', 'providers']);
203
215
  for (const key of Object.keys(imported)) {
204
216
  if (!KNOWN_SECTIONS.has(key)) {
205
217
  throw new Error(`Unknown config section: "${key}"`);
@@ -1017,6 +1017,12 @@ Analyze the above codebase context and provide your response following the forma
1017
1017
  let stdout = '';
1018
1018
  let stderr = '';
1019
1019
  let resolved = false;
1020
+ // Cap stdout + stderr to 10 MB each to prevent OOM if a spawned Claude
1021
+ // Code process emits unexpectedly large output (e.g. a worker that dumps
1022
+ // a large file listing). Once the cap is reached we stop appending; the
1023
+ // truncation marker lets callers detect that output was cut.
1024
+ const MAX_HEADLESS_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB
1025
+ const TRUNCATION_MARKER = '\n[... output truncated at 10 MB ...]';
1020
1026
  const cleanup = () => {
1021
1027
  clearTimeout(timeoutHandle);
1022
1028
  clearTimeout(sigkillTimer);
@@ -1024,7 +1030,12 @@ Analyze the above codebase context and provide your response following the forma
1024
1030
  };
1025
1031
  child.stdout?.on('data', (data) => {
1026
1032
  const chunk = data.toString();
1027
- stdout += chunk;
1033
+ if (stdout.length < MAX_HEADLESS_OUTPUT_BYTES) {
1034
+ stdout += chunk;
1035
+ if (stdout.length >= MAX_HEADLESS_OUTPUT_BYTES) {
1036
+ stdout = stdout.slice(0, MAX_HEADLESS_OUTPUT_BYTES) + TRUNCATION_MARKER;
1037
+ }
1038
+ }
1028
1039
  this.emit('output', {
1029
1040
  executionId: options.executionId,
1030
1041
  type: 'stdout',
@@ -1033,7 +1044,12 @@ Analyze the above codebase context and provide your response following the forma
1033
1044
  });
1034
1045
  child.stderr?.on('data', (data) => {
1035
1046
  const chunk = data.toString();
1036
- stderr += chunk;
1047
+ if (stderr.length < MAX_HEADLESS_OUTPUT_BYTES) {
1048
+ stderr += chunk;
1049
+ if (stderr.length >= MAX_HEADLESS_OUTPUT_BYTES) {
1050
+ stderr = stderr.slice(0, MAX_HEADLESS_OUTPUT_BYTES) + TRUNCATION_MARKER;
1051
+ }
1052
+ }
1037
1053
  this.emit('output', {
1038
1054
  executionId: options.executionId,
1039
1055
  type: 'stderr',
@@ -10,7 +10,7 @@
10
10
  * - testgaps: Test coverage analysis (20 min interval)
11
11
  */
12
12
  import { EventEmitter } from 'events';
13
- import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
13
+ import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, appendFileSync, unlinkSync, statSync } from 'fs';
14
14
  import { cpus } from 'os';
15
15
  import { join, resolve } from 'path';
16
16
  import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
@@ -106,14 +106,15 @@ export class WorkerDaemon extends EventEmitter {
106
106
  this.headlessAvailable = await this.headlessExecutor.isAvailable();
107
107
  if (this.headlessAvailable) {
108
108
  this.log('info', 'Claude Code headless mode available - AI workers enabled');
109
- // Forward headless executor events
110
- this.headlessExecutor.on('execution:start', (data) => {
109
+ // Forward headless executor events.
110
+ // The executor emits 'start', 'complete', 'error' (not 'execution:*').
111
+ this.headlessExecutor.on('start', (data) => {
111
112
  this.emit('headless:start', data);
112
113
  });
113
- this.headlessExecutor.on('execution:complete', (data) => {
114
+ this.headlessExecutor.on('complete', (data) => {
114
115
  this.emit('headless:complete', data);
115
116
  });
116
- this.headlessExecutor.on('execution:error', (data) => {
117
+ this.headlessExecutor.on('error', (data) => {
117
118
  this.emit('headless:error', data);
118
119
  });
119
120
  this.headlessExecutor.on('output', (data) => {
@@ -189,6 +190,12 @@ export class WorkerDaemon extends EventEmitter {
189
190
  return {};
190
191
  }
191
192
  try {
193
+ // Guard against OOM from an oversized config file (tampered or corrupted).
194
+ const configSize = statSync(configPath).size;
195
+ if (configSize > 1_048_576 /* 1 MB */) {
196
+ this.log('warn', `config.json is unusually large (${configSize} bytes) — ignoring daemon config`);
197
+ return {};
198
+ }
192
199
  const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
193
200
  // Support both flat keys at root and nested under scopes.project
194
201
  const cfg = raw?.scopes?.project ?? raw;
@@ -271,6 +278,12 @@ export class WorkerDaemon extends EventEmitter {
271
278
  // Try to restore state from file
272
279
  if (existsSync(this.config.stateFile)) {
273
280
  try {
281
+ // Guard against OOM from an oversized state file (tampered or corrupted).
282
+ const stateSize = statSync(this.config.stateFile).size;
283
+ if (stateSize > 5_242_880 /* 5 MB */) {
284
+ this.log('warn', `daemon-state.json is unusually large (${stateSize} bytes) — starting with fresh state`);
285
+ return;
286
+ }
274
287
  const saved = JSON.parse(readFileSync(this.config.stateFile, 'utf-8'));
275
288
  // CRITICAL: Restore worker config (including enabled flag) from saved state
276
289
  // This fixes #950: daemon enable command not persisting worker state
@@ -555,6 +568,17 @@ export class WorkerDaemon extends EventEmitter {
555
568
  const state = this.workers.get(workerConfig.type);
556
569
  const workerId = `${workerConfig.type}_${Date.now()}`;
557
570
  const startTime = Date.now();
571
+ // Track the headless executionId assigned by HeadlessWorkerExecutor so we
572
+ // can cancel it precisely on timeout. The executor generates its own ID
573
+ // (format: `${type}_${ts}_${random}`) which never matches workerId, so we
574
+ // capture it from the 'start' event instead.
575
+ let headlessExecutionId = null;
576
+ const onHeadlessStart = (data) => {
577
+ headlessExecutionId = data.executionId ?? null;
578
+ };
579
+ if (this.headlessExecutor) {
580
+ this.headlessExecutor.on('start', onHeadlessStart);
581
+ }
558
582
  // Track running worker
559
583
  this.runningWorkers.add(workerConfig.type);
560
584
  state.isRunning = true;
@@ -566,11 +590,13 @@ export class WorkerDaemon extends EventEmitter {
566
590
  // cancelAll() was too broad — it would kill concurrent healthy workers.
567
591
  const output = await this.runWithTimeout(() => this.runWorkerLogic(workerConfig), this.config.workerTimeoutMs, `Worker ${workerConfig.type} timed out after ${this.config.workerTimeoutMs / 1000}s`, () => {
568
592
  if (this.headlessExecutor) {
569
- // Try exact-ID cancel first; fall back to type-based cancel which
570
- // avoids killing unrelated concurrent workers (cancelByType).
571
- if (!this.headlessExecutor.cancel(workerId)) {
572
- this.headlessExecutor.cancelByType(workerConfig.type);
593
+ // Use the exact executionId captured from the 'start' event.
594
+ // Fall back to cancelByType only if we didn't capture an ID yet
595
+ // (e.g. timeout fired before the executor emitted 'start').
596
+ if (headlessExecutionId && this.headlessExecutor.cancel(headlessExecutionId)) {
597
+ return;
573
598
  }
599
+ this.headlessExecutor.cancelByType(workerConfig.type);
574
600
  }
575
601
  });
576
602
  const durationMs = Date.now() - startTime;
@@ -615,6 +641,10 @@ export class WorkerDaemon extends EventEmitter {
615
641
  finally {
616
642
  // Remove from running set and process queue
617
643
  this.runningWorkers.delete(workerConfig.type);
644
+ // Unsubscribe the executionId capture listener regardless of outcome
645
+ if (this.headlessExecutor) {
646
+ this.headlessExecutor.off('start', onHeadlessStart);
647
+ }
618
648
  this.processPendingWorkers();
619
649
  }
620
650
  }
@@ -752,7 +782,7 @@ export class WorkerDaemon extends EventEmitter {
752
782
  checks: {
753
783
  envFilesProtected: !existsSync(join(this.projectRoot, '.env.local')),
754
784
  gitIgnoreExists: existsSync(join(this.projectRoot, '.gitignore')),
755
- noHardcodedSecrets: true, // Would need actual scanning
785
+ noHardcodedSecrets: null, // Not checked in local mode — requires AI-powered scan
756
786
  },
757
787
  riskLevel: 'low',
758
788
  recommendations: [],
@@ -779,8 +809,8 @@ export class WorkerDaemon extends EventEmitter {
779
809
  memoryUsage: process.memoryUsage(),
780
810
  uptime: process.uptime(),
781
811
  optimizations: {
782
- cacheHitRate: 0.78,
783
- avgResponseTime: 45,
812
+ cacheHitRate: null, // Not measured in local mode — requires AI-powered analysis
813
+ avgResponseTime: null, // Not measured in local mode — requires AI-powered analysis
784
814
  },
785
815
  note: 'Install Claude Code CLI for AI-powered optimization suggestions',
786
816
  };
@@ -1042,6 +1072,17 @@ export class WorkerDaemon extends EventEmitter {
1042
1072
  try {
1043
1073
  const logFile = join(this.config.logDir, 'daemon.log');
1044
1074
  appendFileSync(logFile, logMessage + '\n');
1075
+ // Opportunistic rotation: keep the log under 10 MB. When exceeded,
1076
+ // discard the oldest half so recent entries are always retained.
1077
+ const MAX_LOG_BYTES = 10 * 1024 * 1024;
1078
+ if (statSync(logFile).size > MAX_LOG_BYTES) {
1079
+ const content = readFileSync(logFile, 'utf-8');
1080
+ const lines = content.split('\n').filter(Boolean);
1081
+ const trimmed = lines.slice(Math.floor(lines.length / 2)).join('\n') + '\n';
1082
+ const tmp = `${logFile}.${process.pid}.${Date.now()}.tmp`;
1083
+ writeFileSync(tmp, trimmed);
1084
+ renameSync(tmp, logFile);
1085
+ }
1045
1086
  }
1046
1087
  catch {
1047
1088
  // Ignore log write errors
@@ -11,9 +11,6 @@ export declare function detectPII(content: string): PIIDetectionResult;
11
11
  * Redact PII from a string
12
12
  */
13
13
  export declare function redactPII(content: string): string;
14
- /**
15
- * Apply anonymization to CFP document
16
- */
17
14
  export declare function anonymizeCFP(cfp: CFPFormat, level: AnonymizationLevel): {
18
15
  cfp: CFPFormat;
19
16
  transforms: string[];
@@ -35,10 +35,15 @@ const REDACTIONS = {
35
35
  function hash(input) {
36
36
  return crypto.createHash('sha256').update(input).digest('hex');
37
37
  }
38
+ /** Maximum content size for PII scanning/redaction (4 MB). */
39
+ const MAX_SCAN_SIZE = 4 * 1024 * 1024;
38
40
  /**
39
41
  * Detect PII in a string
40
42
  */
41
43
  export function detectPII(content) {
44
+ if (content.length > MAX_SCAN_SIZE) {
45
+ throw new Error(`detectPII: content too large (${content.length} bytes; max ${MAX_SCAN_SIZE})`);
46
+ }
42
47
  const result = {
43
48
  found: false,
44
49
  count: 0,
@@ -85,6 +90,9 @@ function getSeverity(type) {
85
90
  * Redact PII from a string
86
91
  */
87
92
  export function redactPII(content) {
93
+ if (content.length > MAX_SCAN_SIZE) {
94
+ throw new Error(`redactPII: content too large (${content.length} bytes; max ${MAX_SCAN_SIZE})`);
95
+ }
88
96
  let result = content;
89
97
  for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
90
98
  const replacement = REDACTIONS[type];
@@ -100,9 +108,16 @@ export function redactPII(content) {
100
108
  /**
101
109
  * Apply anonymization to CFP document
102
110
  */
111
+ /** Maximum CFP payload size accepted for anonymization (10 MB). */
112
+ const MAX_CFP_ANONYMIZE_SIZE = 10 * 1024 * 1024;
103
113
  export function anonymizeCFP(cfp, level) {
114
+ // Guard before deep clone to prevent OOM on a crafted large object
115
+ const serialized = JSON.stringify(cfp);
116
+ if (serialized.length > MAX_CFP_ANONYMIZE_SIZE) {
117
+ throw new Error(`anonymizeCFP: CFP payload too large (${serialized.length} bytes; max ${MAX_CFP_ANONYMIZE_SIZE})`);
118
+ }
104
119
  const transforms = [];
105
- const anonymized = JSON.parse(JSON.stringify(cfp));
120
+ const anonymized = JSON.parse(serialized);
106
121
  // Level: Minimal
107
122
  if (['minimal', 'standard', 'strict', 'paranoid'].includes(level)) {
108
123
  // Redact author display name
@@ -49,6 +49,14 @@ export async function exportPatterns(cfp, options = {}) {
49
49
  // Write to file
50
50
  const ext = getFileExtension(format);
51
51
  outputPath = output.endsWith(ext) ? output : output + ext;
52
+ // Path traversal guard — output must resolve within the current working
53
+ // directory to prevent callers from writing to arbitrary filesystem paths.
54
+ const projectRoot = path.resolve(process.cwd());
55
+ const resolvedOutputPath = path.resolve(process.cwd(), outputPath);
56
+ if (!resolvedOutputPath.startsWith(projectRoot + path.sep) && resolvedOutputPath !== projectRoot) {
57
+ throw new Error(`Output path must resolve within the project directory: ${projectRoot}`);
58
+ }
59
+ outputPath = resolvedOutputPath;
52
60
  // Ensure directory exists
53
61
  const dir = path.dirname(outputPath);
54
62
  if (!fs.existsSync(dir)) {
@@ -6,6 +6,14 @@
6
6
  * @version 3.0.0
7
7
  */
8
8
  import * as crypto from 'crypto';
9
+ /**
10
+ * Sanitize a user-supplied filename before embedding it inside a multipart
11
+ * Content-Disposition header. Strip all CR/LF characters (MIME header injection)
12
+ * and cap length to prevent oversized headers.
13
+ */
14
+ function sanitizeFileName(name) {
15
+ return name.replace(/[\r\n]/g, '').slice(0, 200);
16
+ }
9
17
  /**
10
18
  * Get web3.storage token from environment or config
11
19
  */
@@ -42,7 +50,7 @@ async function uploadToWeb3Storage(content, options) {
42
50
  'Get a free token at: https://web3.storage');
43
51
  }
44
52
  const endpoint = options.endpoint || 'https://api.web3.storage';
45
- const name = options.name || 'pattern.cfp.json';
53
+ const name = sanitizeFileName(options.name || 'pattern.cfp.json');
46
54
  console.log(`[IPFS] Uploading ${content.length} bytes to web3.storage...`);
47
55
  // Create FormData-like body for upload
48
56
  const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
@@ -91,7 +99,7 @@ async function uploadToPinata(content, options) {
91
99
  throw new Error('Pinata API credentials not found. Set PINATA_API_KEY and PINATA_API_SECRET.\n' +
92
100
  'Get credentials at: https://pinata.cloud');
93
101
  }
94
- const name = options.name || 'pattern.cfp.json';
102
+ const name = sanitizeFileName(options.name || 'pattern.cfp.json');
95
103
  console.log(`[IPFS] Uploading ${content.length} bytes to Pinata...`);
96
104
  const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
97
105
  const metadata = JSON.stringify({ name });
@@ -277,14 +285,36 @@ export async function unpinContent(cid, options = {}) {
277
285
  console.log(`[IPFS] Demo unpinned`);
278
286
  return { success: true };
279
287
  }
288
+ /** Allowlisted IPFS gateway hosts for SSRF prevention */
289
+ const ALLOWED_GATEWAY_HOSTS_UPLOAD = new Set([
290
+ 'w3s.link',
291
+ 'gateway.pinata.cloud',
292
+ 'cloudflare-ipfs.com',
293
+ 'ipfs.io',
294
+ 'dweb.link',
295
+ ]);
296
+ function isAllowedGateway(gateway) {
297
+ try {
298
+ const parsed = new URL(gateway);
299
+ return parsed.protocol === 'https:' && ALLOWED_GATEWAY_HOSTS_UPLOAD.has(parsed.hostname);
300
+ }
301
+ catch {
302
+ return false;
303
+ }
304
+ }
280
305
  /**
281
306
  * Check if content exists on IPFS
282
307
  */
283
308
  export async function checkContent(cid, gateway = 'https://w3s.link') {
309
+ if (!isAllowedGateway(gateway)) {
310
+ console.warn(`[IPFS] Blocked checkContent: gateway not in allowlist: ${gateway}`);
311
+ return { exists: false };
312
+ }
284
313
  console.log(`[IPFS] Checking ${cid}...`);
285
314
  try {
286
315
  const response = await fetch(`${gateway}/ipfs/${cid}`, {
287
316
  method: 'HEAD',
317
+ signal: AbortSignal.timeout(10000),
288
318
  });
289
319
  if (response.ok) {
290
320
  const size = parseInt(response.headers.get('content-length') || '0', 10);
@@ -315,7 +345,7 @@ export function getIPNSURL(name, gateway = 'https://w3s.link') {
315
345
  */
316
346
  async function uploadToLocalIPFS(content, options) {
317
347
  const apiUrl = process.env.IPFS_API_URL || 'http://localhost:5001';
318
- const name = options.name || 'pattern.cfp.json';
348
+ const name = sanitizeFileName(options.name || 'pattern.cfp.json');
319
349
  console.log(`[IPFS] Uploading ${content.length} bytes to ${apiUrl}...`);
320
350
  const boundary = '----IPFSBoundary' + crypto.randomBytes(16).toString('hex');
321
351
  const body = Buffer.concat([
@@ -107,11 +107,17 @@ export function serializeToBuffer(cfp, format) {
107
107
  return Buffer.from(json, 'utf-8');
108
108
  }
109
109
  }
110
+ /** Maximum CFP payload size (10 MB) — prevents OOM on crafted inputs. */
111
+ const MAX_CFP_SIZE = 10 * 1024 * 1024;
110
112
  /**
111
113
  * Deserialize CFP from string/buffer
112
114
  */
113
115
  export function deserializeCFP(data) {
114
116
  const str = typeof data === 'string' ? data : data.toString('utf-8');
117
+ // Guard against OOM before parsing
118
+ if (str.length > MAX_CFP_SIZE) {
119
+ throw new Error(`CFP payload too large (${str.length} bytes; max ${MAX_CFP_SIZE})`);
120
+ }
115
121
  let parsed;
116
122
  try {
117
123
  parsed = JSON.parse(str);
@@ -119,9 +125,9 @@ export function deserializeCFP(data) {
119
125
  catch (e) {
120
126
  throw new Error(`Invalid CFP file: ${e instanceof Error ? e.message : String(e)}`);
121
127
  }
122
- // Validate magic bytes
128
+ // Validate magic bytes — use a fixed message to avoid reflecting arbitrary input
123
129
  if (parsed.magic !== 'CFP1') {
124
- throw new Error(`Invalid CFP format: expected magic 'CFP1', got '${parsed.magic}'`);
130
+ throw new Error('Invalid CFP format: unexpected magic bytes');
125
131
  }
126
132
  return parsed;
127
133
  }
@@ -131,7 +137,7 @@ export function deserializeCFP(data) {
131
137
  export function validateCFP(cfp) {
132
138
  const errors = [];
133
139
  if (cfp.magic !== 'CFP1') {
134
- errors.push(`Invalid magic bytes: ${cfp.magic}`);
140
+ errors.push('Invalid magic bytes');
135
141
  }
136
142
  if (!cfp.version) {
137
143
  errors.push('Missing version');
@@ -105,7 +105,24 @@ export async function uploadToGCS(content, options = {}) {
105
105
  execFileSync('gcloud', uploadArgs, { encoding: 'utf-8', stdio: 'pipe', timeout: 60000 });
106
106
  // Set metadata if provided
107
107
  if (options.metadata && Object.keys(options.metadata).length > 0) {
108
- const metadataJson = JSON.stringify(options.metadata);
108
+ // Cap metadata to prevent unbounded flag values; restrict key charset to
109
+ // alphanumeric/dash/underscore to avoid injection in --custom-metadata=<json>.
110
+ const MAX_META_KEY_LEN = 128;
111
+ const MAX_META_VAL_LEN = 512;
112
+ const MAX_META_ENTRIES = 20;
113
+ const safeMetadata = {};
114
+ let metaCount = 0;
115
+ for (const [k, v] of Object.entries(options.metadata)) {
116
+ if (metaCount >= MAX_META_ENTRIES)
117
+ break;
118
+ if (typeof k !== 'string' || typeof v !== 'string')
119
+ continue;
120
+ if (!/^[a-zA-Z0-9_-]+$/.test(k))
121
+ continue;
122
+ safeMetadata[k.slice(0, MAX_META_KEY_LEN)] = v.slice(0, MAX_META_VAL_LEN);
123
+ metaCount++;
124
+ }
125
+ const metadataJson = JSON.stringify(safeMetadata);
109
126
  try {
110
127
  const metaArgs = ['storage', 'objects', 'update', `gs://${config.bucket}/${objectPath}`, `--custom-metadata=${metadataJson}`];
111
128
  if (config.projectId)
@@ -164,6 +181,15 @@ export async function downloadFromGCS(uri, config) {
164
181
  if (cfg?.projectId)
165
182
  downloadArgs.push(`--project=${cfg.projectId}`);
166
183
  execFileSync('gcloud', downloadArgs, { encoding: 'utf-8', stdio: 'pipe' });
184
+ const MAX_GCS_DOWNLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
185
+ const fileSize = fs.statSync(tempFile).size;
186
+ if (fileSize > MAX_GCS_DOWNLOAD_BYTES) {
187
+ const resolvedTemp2 = path.resolve(tempFile);
188
+ if (resolvedTemp2.startsWith(path.resolve(tempDir)))
189
+ fs.unlinkSync(tempFile);
190
+ console.error(`[GCS] Downloaded file exceeds size limit (${fileSize} > ${MAX_GCS_DOWNLOAD_BYTES} bytes)`);
191
+ return null;
192
+ }
167
193
  const content = fs.readFileSync(tempFile);
168
194
  const resolvedTemp = path.resolve(tempFile);
169
195
  if (resolvedTemp.startsWith(path.resolve(tempDir))) {
@@ -215,9 +241,17 @@ export async function listGCSObjects(prefix, config) {
215
241
  const listArgs = ['storage', 'ls', '-l', uri, '--format=json'];
216
242
  if (cfg.projectId)
217
243
  listArgs.push(`--project=${cfg.projectId}`);
218
- const result = execFileSync('gcloud', listArgs, { encoding: 'utf-8', stdio: 'pipe' });
244
+ const result = execFileSync('gcloud', listArgs, { encoding: 'utf-8', stdio: 'pipe', maxBuffer: 10 * 1024 * 1024 });
245
+ // Guard against gcloud returning a huge JSON payload that could OOM Node.
246
+ const MAX_LIST_BYTES = 10 * 1024 * 1024; // 10 MB
247
+ if (result.length > MAX_LIST_BYTES) {
248
+ console.error(`[GCS] listGCSObjects response too large (${result.length} bytes), truncating`);
249
+ return [];
250
+ }
219
251
  const objects = JSON.parse(result);
220
- return objects.map((obj) => ({
252
+ if (!Array.isArray(objects))
253
+ return [];
254
+ return objects.slice(0, 10_000).map((obj) => ({
221
255
  name: obj.name,
222
256
  size: obj.size || 0,
223
257
  updated: obj.updated || new Date().toISOString(),
@@ -3,6 +3,46 @@
3
3
  * Secure discovery mechanism for finding patterns in decentralized environment
4
4
  */
5
5
  import * as crypto from 'crypto';
6
+ /** Maximum bytes read from any IPFS gateway response to prevent OOM */
7
+ const MAX_DISCOVERY_RESPONSE_BYTES = 10 * 1024 * 1024; // 10 MB
8
+ /**
9
+ * Read a fetch response body with a hard byte cap.
10
+ * Aborts the stream early if the limit is exceeded.
11
+ */
12
+ async function readBodyCapped(response, maxBytes = MAX_DISCOVERY_RESPONSE_BYTES) {
13
+ const lengthHeader = response.headers.get('content-length');
14
+ if (lengthHeader) {
15
+ const declared = parseInt(lengthHeader, 10);
16
+ if (Number.isFinite(declared) && declared > maxBytes) {
17
+ throw new Error(`Response too large: ${declared} bytes (max ${maxBytes})`);
18
+ }
19
+ }
20
+ const reader = response.body?.getReader();
21
+ if (!reader)
22
+ return '';
23
+ const chunks = [];
24
+ let total = 0;
25
+ while (true) {
26
+ const { done, value } = await reader.read();
27
+ if (done)
28
+ break;
29
+ if (value) {
30
+ total += value.byteLength;
31
+ if (total > maxBytes) {
32
+ await reader.cancel();
33
+ throw new Error(`Response too large: exceeded ${maxBytes} bytes`);
34
+ }
35
+ chunks.push(value);
36
+ }
37
+ }
38
+ const combined = new Uint8Array(total);
39
+ let offset = 0;
40
+ for (const c of chunks) {
41
+ combined.set(c, offset);
42
+ offset += c.byteLength;
43
+ }
44
+ return new TextDecoder('utf-8').decode(combined);
45
+ }
6
46
  import { DEFAULT_STORE_CONFIG, } from './registry.js';
7
47
  /**
8
48
  * Pattern Store Discovery Service
@@ -136,7 +176,8 @@ export class PatternDiscovery {
136
176
  signal: AbortSignal.timeout(10000),
137
177
  });
138
178
  if (response.ok) {
139
- const data = await response.json();
179
+ const text = await readBodyCapped(response, 64 * 1024); // IPNS resolve is tiny
180
+ const data = JSON.parse(text);
140
181
  const cid = data.Path?.replace('/ipfs/', '') || '';
141
182
  if (cid) {
142
183
  const resolution = {
@@ -226,7 +267,7 @@ export class PatternDiscovery {
226
267
  signal: AbortSignal.timeout(30000),
227
268
  });
228
269
  if (response.ok) {
229
- const text = await response.text();
270
+ const text = await readBodyCapped(response);
230
271
  try {
231
272
  const registry = JSON.parse(text);
232
273
  console.log(`[Discovery] Fetched registry with ${registry.patterns?.length || 0} patterns`);
@@ -257,7 +298,8 @@ export class PatternDiscovery {
257
298
  signal: AbortSignal.timeout(15000),
258
299
  });
259
300
  if (response.ok) {
260
- const registry = await response.json();
301
+ const altText = await readBodyCapped(response);
302
+ const registry = JSON.parse(altText);
261
303
  console.log(`[Discovery] Fetched registry from ${altGateway}`);
262
304
  return registry;
263
305
  }
@@ -11,6 +11,7 @@ import { DEFAULT_STORE_CONFIG } from './registry.js';
11
11
  * Handles secure download and verification of patterns
12
12
  */
13
13
  const MAX_DOWNLOAD_CACHE = 500;
14
+ const MAX_PATTERN_FILE_BYTES = 50 * 1024 * 1024; // 50 MB — matches client.ts MAX_IPFS_RESPONSE_BYTES
14
15
  const ALLOWED_GATEWAYS = new Set([
15
16
  'https://w3s.link',
16
17
  'https://dweb.link',
@@ -362,6 +363,10 @@ export class PatternDownloader {
362
363
  async importPattern(filePath, strategy = 'merge') {
363
364
  console.log(`[Download] Importing pattern with strategy: ${strategy}`);
364
365
  try {
366
+ if (fs.statSync(filePath).size > MAX_PATTERN_FILE_BYTES) {
367
+ console.error(`[Download] Pattern file exceeds size limit (${MAX_PATTERN_FILE_BYTES} bytes)`);
368
+ return false;
369
+ }
365
370
  const content = fs.readFileSync(filePath, 'utf-8');
366
371
  const cfp = JSON.parse(content);
367
372
  // In production: Import to local pattern store
@@ -137,13 +137,17 @@ export class PatternPublisher {
137
137
  }
138
138
  catch (error) {
139
139
  console.error(`[Publish] Failed:`, error);
140
+ // Sanitize error message: strip control chars and cap length to prevent
141
+ // internal paths / stack traces from leaking into CLI output or logs.
142
+ const rawMsg = error instanceof Error ? error.message : String(error);
143
+ const safeMsg = rawMsg.replace(/[\x00-\x1f\x7f]/g, '?').slice(0, 256);
140
144
  return {
141
145
  success: false,
142
146
  patternId: '',
143
147
  cid: '',
144
148
  registryCid: '',
145
149
  gatewayUrl: '',
146
- message: `Publish failed: ${error}`,
150
+ message: `Publish failed: ${safeMsg}`,
147
151
  };
148
152
  }
149
153
  }
@@ -152,6 +156,14 @@ export class PatternPublisher {
152
156
  */
153
157
  async signContent(content, privateKeyPath) {
154
158
  const ed = await import('@noble/ed25519');
159
+ // Guard: cap key file size to prevent OOM on oversized or malicious files.
160
+ // A valid 32-byte Ed25519 seed encoded as hex is 64 chars; even with a
161
+ // newline and BOM the file should never exceed a few hundred bytes.
162
+ const MAX_KEY_FILE_BYTES = 4 * 1024; // 4 KB — generous upper bound
163
+ const keyFileStat = fs.statSync(privateKeyPath);
164
+ if (keyFileStat.size > MAX_KEY_FILE_BYTES) {
165
+ throw new Error(`Private key file exceeds size limit (${MAX_KEY_FILE_BYTES} bytes)`);
166
+ }
155
167
  const keyHex = fs.readFileSync(privateKeyPath, 'utf-8').trim();
156
168
  // Accept 64-char hex (32-byte seed) directly; otherwise derive via SHA-256
157
169
  const privKeyBytes = keyHex.length === 64
@@ -37,6 +37,14 @@ export declare function removePatternFromRegistry(registry: PatternRegistry, pat
37
37
  export declare function serializeRegistry(registry: PatternRegistry): string;
38
38
  /**
39
39
  * Deserialize registry from JSON
40
+ *
41
+ * Caps the input string length before parsing to prevent OOM on a malicious or
42
+ * oversized registry fetched from IPFS / Pinata. The in-flight body is already
43
+ * capped by readBodyWithLimit (50 MB), but deserializeRegistry is also called
44
+ * with locally-cached data, so we add a 10 MB guard here too.
45
+ *
46
+ * We also reject non-semver and suspiciously long version strings to prevent
47
+ * version fields being used as a side-channel for large-payload injection.
40
48
  */
41
49
  export declare function deserializeRegistry(json: string): PatternRegistry;
42
50
  /**