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
@@ -68,6 +68,11 @@ export class CdpClient {
68
68
  reject(new Error('CDP not connected'));
69
69
  return;
70
70
  }
71
+ // Cap in-flight commands to prevent unbounded Map growth
72
+ if (this.pendingCommands.size >= 1000) {
73
+ reject(new Error('CDP command queue full (>1000 in-flight commands)'));
74
+ return;
75
+ }
71
76
  const id = this.nextId++;
72
77
  const cmd = { id, method, params };
73
78
  if (sessionId)
@@ -88,7 +93,12 @@ export class CdpClient {
88
93
  if (!this.eventListeners.has(event)) {
89
94
  this.eventListeners.set(event, new Set());
90
95
  }
91
- this.eventListeners.get(event).add(fn);
96
+ const listeners = this.eventListeners.get(event);
97
+ // Cap listeners per event to prevent unbounded Set growth
98
+ if (listeners.size >= 100) {
99
+ throw new Error(`CDP event listener limit reached for event: ${event}`);
100
+ }
101
+ listeners.add(fn);
92
102
  return () => this.eventListeners.get(event)?.delete(fn);
93
103
  }
94
104
  once(event, sessionId) {
@@ -120,16 +130,24 @@ export class CdpClient {
120
130
  return this.connected;
121
131
  }
122
132
  }
133
+ const CDP_RESPONSE_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB
134
+ async function readCdpJson(res) {
135
+ const text = await res.text();
136
+ if (text.length > CDP_RESPONSE_SIZE_LIMIT) {
137
+ throw new Error(`CDP response too large: ${text.length} bytes`);
138
+ }
139
+ return JSON.parse(text);
140
+ }
123
141
  export async function fetchTargets(port) {
124
142
  const res = await fetch(`http://127.0.0.1:${port}/json/list`);
125
143
  if (!res.ok)
126
144
  throw new Error(`Failed to fetch targets: ${res.statusText}`);
127
- return res.json();
145
+ return readCdpJson(res);
128
146
  }
129
147
  export async function fetchNewTarget(port, url) {
130
148
  const res = await fetch(`http://127.0.0.1:${port}/json/new?${url}`);
131
149
  if (!res.ok)
132
150
  throw new Error(`Failed to create target: ${res.statusText}`);
133
- return res.json();
151
+ return readCdpJson(res);
134
152
  }
135
153
  //# sourceMappingURL=cdp.js.map
@@ -1,10 +1,29 @@
1
1
  import { writeFile } from 'fs/promises';
2
- import { join } from 'path';
3
- import { tmpdir } from 'os';
2
+ import { join, resolve, relative, isAbsolute } from 'path';
3
+ import { tmpdir, homedir } from 'os';
4
+ const MAX_CONCURRENT_SESSIONS = 100;
5
+ const MAX_REQUESTS_PER_SESSION = 5_000;
4
6
  const _sessions = new Map();
7
+ /** Validate output path is within cwd or home dir to prevent path traversal. */
8
+ function safeOutputPath(p) {
9
+ const resolved = resolve(p);
10
+ const cwd = process.cwd();
11
+ const home = homedir();
12
+ const relCwd = relative(cwd, resolved);
13
+ const relHome = relative(home, resolved);
14
+ if ((!relCwd.startsWith('..') && !isAbsolute(relCwd)) ||
15
+ (!relHome.startsWith('..') && !isAbsolute(relHome))) {
16
+ return resolved;
17
+ }
18
+ // Reject out-of-scope paths — fall back to tmpdir
19
+ return join(tmpdir(), `monomind-har-${Date.now()}.har`);
20
+ }
5
21
  export async function startHarRecording(client, sessionId) {
6
22
  if (_sessions.has(sessionId))
7
23
  throw new Error('HAR recording already in progress');
24
+ if (_sessions.size >= MAX_CONCURRENT_SESSIONS) {
25
+ throw new Error(`HAR recording limit reached (max ${MAX_CONCURRENT_SESSIONS} concurrent sessions)`);
26
+ }
8
27
  const requests = new Map();
9
28
  const startTime = Date.now();
10
29
  const startWallMs = startTime;
@@ -17,6 +36,9 @@ export async function startHarRecording(client, sessionId) {
17
36
  const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
18
37
  if (sid !== sessionId)
19
38
  return;
39
+ // Cap to prevent unbounded memory growth on high-traffic pages
40
+ if (requests.size >= MAX_REQUESTS_PER_SESSION)
41
+ return;
20
42
  const p = params;
21
43
  requests.set(p.requestId, {
22
44
  id: p.requestId,
@@ -78,9 +100,9 @@ export async function stopHarRecording(client, sessionId, outputPath, captureRes
78
100
  }
79
101
  }
80
102
  const har = buildHar(Array.from(state.requests.values()), state.startTime);
81
- const path = outputPath ?? join(tmpdir(), `monomind-har-${Date.now()}.har`);
82
- await writeFile(path, JSON.stringify(har, null, 2));
83
- return path;
103
+ const safePath = outputPath ? safeOutputPath(outputPath) : join(tmpdir(), `monomind-har-${Date.now()}.har`);
104
+ await writeFile(safePath, JSON.stringify(har, null, 2));
105
+ return safePath;
84
106
  }
85
107
  export function getHarStatus(sessionId) {
86
108
  const state = _sessions.get(sessionId);
@@ -19,7 +19,7 @@ function updateSwarmActivityMetrics(agentCountDelta) {
19
19
  timestamp: new Date().toISOString(),
20
20
  swarm: { active: false, agent_count: 0, coordination_active: false },
21
21
  };
22
- if (fs.existsSync(activityPath)) {
22
+ if (fs.existsSync(activityPath) && fs.statSync(activityPath).size <= 10 * 1024 * 1024) {
23
23
  data = JSON.parse(fs.readFileSync(activityPath, 'utf-8'));
24
24
  }
25
25
  else {
@@ -113,8 +113,8 @@ const spawnCommand = {
113
113
  { command: 'monomind agent spawn -t researcher --task "Research React 19"', description: 'Spawn researcher with task' }
114
114
  ],
115
115
  action: async (ctx) => {
116
- let agentType = ctx.flags.type;
117
- let agentName = ctx.flags.name;
116
+ let agentType = ctx.flags.type?.slice(0, 64) ?? '';
117
+ let agentName = ctx.flags.name?.slice(0, 128) ?? '';
118
118
  // Interactive mode if type not specified
119
119
  if (!agentType && ctx.interactive) {
120
120
  agentType = await select({
@@ -123,7 +123,7 @@ const spawnCommand = {
123
123
  });
124
124
  }
125
125
  // Semantic routing: if --type absent but --task provided, use RouteLayer
126
- const taskDescription = ctx.flags.task;
126
+ const taskDescription = ctx.flags.task?.slice(0, 2048);
127
127
  if (!agentType && taskDescription) {
128
128
  try {
129
129
  // Builds a RouteLayer with a real local embedding model + headless
@@ -453,7 +453,10 @@ const metricsCommand = {
453
453
  const files = readdirSync(agentsDir).filter(f => f.endsWith('.json'));
454
454
  for (const file of files) {
455
455
  try {
456
- const data = JSON.parse(readFileSync(join(agentsDir, file), 'utf-8'));
456
+ const agentFilePath = join(agentsDir, file);
457
+ if (statSync(agentFilePath).size > 512 * 1024)
458
+ continue; // skip files > 512 KB
459
+ const data = JSON.parse(readFileSync(agentFilePath, 'utf-8'));
457
460
  totalAgents++;
458
461
  const agType = data.type || 'unknown';
459
462
  if (!typeCounts[agType])
@@ -475,7 +478,7 @@ const metricsCommand = {
475
478
  }
476
479
  // Read swarm activity for additional state
477
480
  const activityFile = join(swarmDir, 'swarm-activity.json');
478
- if (existsSync(activityFile)) {
481
+ if (existsSync(activityFile) && statSync(activityFile).size <= 10 * 1024 * 1024) {
479
482
  try {
480
483
  const activity = JSON.parse(readFileSync(activityFile, 'utf-8'));
481
484
  if (activity.totalAgents && totalAgents === 0)
@@ -801,9 +804,9 @@ const logsCommand = {
801
804
  { command: 'monomind agent logs -l error --since 1h', description: 'Show errors from last hour' }
802
805
  ],
803
806
  action: async (ctx) => {
804
- const agentId = ctx.args[0] || ctx.flags.id;
807
+ const agentId = ((ctx.args[0] || ctx.flags.id) ?? '').slice(0, 128);
805
808
  const tail = ctx.flags.tail;
806
- const level = ctx.flags.level;
809
+ const level = ctx.flags.level?.slice(0, 32);
807
810
  if (!agentId) {
808
811
  output.printError('Agent ID is required. Use --id or -i');
809
812
  return { success: false, exitCode: 1 };
@@ -28,6 +28,19 @@ async function getASTAnalyzer() {
28
28
  async function getGraphAnalyzer() {
29
29
  return null;
30
30
  }
31
+ /**
32
+ * Write analysis output to a file, constraining the path to the current working
33
+ * directory to prevent path traversal attacks via --output /etc/cron.d/x or
34
+ * similar. Throws if the resolved path escapes cwd.
35
+ */
36
+ async function safeWriteOutputFile(outputFile, data) {
37
+ const projectRoot = path.resolve(process.cwd());
38
+ const fullPath = path.resolve(process.cwd(), outputFile);
39
+ if (!fullPath.startsWith(projectRoot + path.sep) && fullPath !== projectRoot) {
40
+ throw new Error(`Output path must resolve within the project directory: ${projectRoot}`);
41
+ }
42
+ await writeFile(fullPath, data);
43
+ }
31
44
  // Diff subcommand
32
45
  const diffCommand = {
33
46
  name: 'diff',
@@ -557,7 +570,7 @@ const astCommand = {
557
570
  if (formatType === 'json') {
558
571
  const jsonOutput = { files: results, totals };
559
572
  if (outputFile) {
560
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
573
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
561
574
  output.printSuccess(`Results written to ${outputFile}`);
562
575
  }
563
576
  else {
@@ -649,7 +662,7 @@ const astCommand = {
649
662
  }
650
663
  }
651
664
  if (outputFile) {
652
- await writeFile(outputFile, JSON.stringify({ files: results, totals }, null, 2));
665
+ await safeWriteOutputFile(outputFile, JSON.stringify({ files: results, totals }, null, 2));
653
666
  output.printSuccess(`Results written to ${outputFile}`);
654
667
  }
655
668
  return { success: true, data: { files: results, totals } };
@@ -750,7 +763,7 @@ const complexityAstCommand = {
750
763
  if (formatType === 'json') {
751
764
  const jsonOutput = { files: results, summary: { total: results.length, flagged: flaggedCount, avgComplexity, threshold } };
752
765
  if (outputFile) {
753
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
766
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
754
767
  output.printSuccess(`Results written to ${outputFile}`);
755
768
  }
756
769
  else {
@@ -800,7 +813,7 @@ const complexityAstCommand = {
800
813
  output.writeln(output.dim(` ... and ${results.length - 15} more files`));
801
814
  }
802
815
  if (outputFile) {
803
- await writeFile(outputFile, JSON.stringify({ files: results, summary: { total: results.length, flagged: flaggedCount, avgComplexity, threshold } }, null, 2));
816
+ await safeWriteOutputFile(outputFile, JSON.stringify({ files: results, summary: { total: results.length, flagged: flaggedCount, avgComplexity, threshold } }, null, 2));
804
817
  output.printSuccess(`Results written to ${outputFile}`);
805
818
  }
806
819
  return { success: true, data: { files: results, flaggedCount } };
@@ -907,7 +920,7 @@ const symbolsCommand = {
907
920
  symbols.sort((a, b) => a.file.localeCompare(b.file) || a.name.localeCompare(b.name));
908
921
  if (formatType === 'json') {
909
922
  if (outputFile) {
910
- await writeFile(outputFile, JSON.stringify(symbols, null, 2));
923
+ await safeWriteOutputFile(outputFile, JSON.stringify(symbols, null, 2));
911
924
  output.printSuccess(`Results written to ${outputFile}`);
912
925
  }
913
926
  else {
@@ -941,7 +954,7 @@ const symbolsCommand = {
941
954
  output.writeln(output.dim(` ... and ${symbols.length - 30} more symbols`));
942
955
  }
943
956
  if (outputFile) {
944
- await writeFile(outputFile, JSON.stringify(symbols, null, 2));
957
+ await safeWriteOutputFile(outputFile, JSON.stringify(symbols, null, 2));
945
958
  output.printSuccess(`Results written to ${outputFile}`);
946
959
  }
947
960
  return { success: true, data: symbols };
@@ -1043,7 +1056,7 @@ const importsCommand = {
1043
1056
  fileImports: Object.fromEntries(fileImports),
1044
1057
  };
1045
1058
  if (outputFile) {
1046
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1059
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1047
1060
  output.printSuccess(`Results written to ${outputFile}`);
1048
1061
  }
1049
1062
  else {
@@ -1081,7 +1094,7 @@ const importsCommand = {
1081
1094
  output.writeln(output.dim(` ... and ${sortedImports.length - 20} more imports`));
1082
1095
  }
1083
1096
  if (outputFile) {
1084
- await writeFile(outputFile, JSON.stringify({
1097
+ await safeWriteOutputFile(outputFile, JSON.stringify({
1085
1098
  imports: Object.fromEntries(sortedImports),
1086
1099
  fileImports: Object.fromEntries(fileImports),
1087
1100
  }, null, 2));
@@ -1378,7 +1391,8 @@ const boundariesCommand = {
1378
1391
  ],
1379
1392
  action: async (ctx) => {
1380
1393
  const targetDir = ctx.args[0] || ctx.cwd;
1381
- const numPartitions = ctx.flags.partitions || 2;
1394
+ const rawPartitions = ctx.flags.partitions || 2;
1395
+ const numPartitions = Number.isFinite(rawPartitions) ? Math.max(1, Math.min(rawPartitions, 100)) : 2;
1382
1396
  const outputFile = ctx.flags.output;
1383
1397
  const format = ctx.flags.format || 'text';
1384
1398
  output.printInfo(`Analyzing code boundaries in: ${output.highlight(targetDir)}`);
@@ -1406,7 +1420,7 @@ const boundariesCommand = {
1406
1420
  circularDependencies: result.circularDependencies,
1407
1421
  };
1408
1422
  if (outputFile) {
1409
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1423
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1410
1424
  output.printSuccess(`Results written to ${outputFile}`);
1411
1425
  }
1412
1426
  else {
@@ -1420,7 +1434,7 @@ const boundariesCommand = {
1420
1434
  highlightCycles: true,
1421
1435
  });
1422
1436
  if (outputFile) {
1423
- await writeFile(outputFile, dotOutput);
1437
+ await safeWriteOutputFile(outputFile, dotOutput);
1424
1438
  output.printSuccess(`DOT graph written to ${outputFile}`);
1425
1439
  output.writeln(output.dim('Visualize with: dot -Tpng -o graph.png ' + outputFile));
1426
1440
  }
@@ -1480,7 +1494,7 @@ const boundariesCommand = {
1480
1494
  }
1481
1495
  }
1482
1496
  if (outputFile) {
1483
- await writeFile(outputFile, JSON.stringify(result, null, 2));
1497
+ await safeWriteOutputFile(outputFile, JSON.stringify(result, null, 2));
1484
1498
  output.printSuccess(`Full results written to ${outputFile}`);
1485
1499
  }
1486
1500
  return { success: true, data: result };
@@ -1558,7 +1572,7 @@ const modulesCommand = {
1558
1572
  statistics: result.statistics,
1559
1573
  };
1560
1574
  if (outputFile) {
1561
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1575
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1562
1576
  output.printSuccess(`Results written to ${outputFile}`);
1563
1577
  }
1564
1578
  else {
@@ -1573,7 +1587,7 @@ const modulesCommand = {
1573
1587
  highlightCycles: true,
1574
1588
  });
1575
1589
  if (outputFile) {
1576
- await writeFile(outputFile, dotOutput);
1590
+ await safeWriteOutputFile(outputFile, dotOutput);
1577
1591
  output.printSuccess(`DOT graph written to ${outputFile}`);
1578
1592
  output.writeln(output.dim('Visualize with: dot -Tpng -o modules.png ' + outputFile));
1579
1593
  }
@@ -1614,7 +1628,7 @@ const modulesCommand = {
1614
1628
  }
1615
1629
  }
1616
1630
  if (outputFile) {
1617
- await writeFile(outputFile, JSON.stringify(result, null, 2));
1631
+ await safeWriteOutputFile(outputFile, JSON.stringify(result, null, 2));
1618
1632
  output.printSuccess(`Full results written to ${outputFile}`);
1619
1633
  }
1620
1634
  return { success: true, data: result };
@@ -1682,7 +1696,8 @@ const dependenciesCommand = {
1682
1696
  const format = ctx.flags.format || 'text';
1683
1697
  const include = (ctx.flags.include || '.ts,.tsx,.js,.jsx,.mjs,.cjs').split(',');
1684
1698
  const exclude = (ctx.flags.exclude || 'node_modules,dist,build,.git').split(',');
1685
- const maxDepth = ctx.flags.depth || 10;
1699
+ const rawDepth = ctx.flags.depth || 10;
1700
+ const maxDepth = Number.isFinite(rawDepth) ? Math.max(1, Math.min(rawDepth, 50)) : 10;
1686
1701
  output.printInfo(`Building dependency graph for: ${output.highlight(targetDir)}`);
1687
1702
  output.writeln();
1688
1703
  const spinner = output.createSpinner({ text: 'Scanning files...', spinner: 'dots' });
@@ -1711,7 +1726,7 @@ const dependenciesCommand = {
1711
1726
  circularDependencies: circularDeps,
1712
1727
  };
1713
1728
  if (outputFile) {
1714
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1729
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1715
1730
  output.printSuccess(`Graph written to ${outputFile}`);
1716
1731
  }
1717
1732
  else {
@@ -1733,7 +1748,7 @@ const dependenciesCommand = {
1733
1748
  highlightCycles: true,
1734
1749
  });
1735
1750
  if (outputFile) {
1736
- await writeFile(outputFile, dotOutput);
1751
+ await safeWriteOutputFile(outputFile, dotOutput);
1737
1752
  output.printSuccess(`DOT graph written to ${outputFile}`);
1738
1753
  output.writeln(output.dim('Visualize with: dot -Tpng -o deps.png ' + outputFile));
1739
1754
  }
@@ -1791,7 +1806,7 @@ const dependenciesCommand = {
1791
1806
  metadata: graph.metadata,
1792
1807
  circularDependencies: circularDeps,
1793
1808
  };
1794
- await writeFile(outputFile, JSON.stringify(fullOutput, null, 2));
1809
+ await safeWriteOutputFile(outputFile, JSON.stringify(fullOutput, null, 2));
1795
1810
  output.printSuccess(`Full results written to ${outputFile}`);
1796
1811
  }
1797
1812
  return { success: true };
@@ -1865,7 +1880,7 @@ const circularCommand = {
1865
1880
  if (format === 'json') {
1866
1881
  const jsonOutput = { cycles: filtered, total: cycles.length, filtered: filtered.length };
1867
1882
  if (outputFile) {
1868
- await writeFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1883
+ await safeWriteOutputFile(outputFile, JSON.stringify(jsonOutput, null, 2));
1869
1884
  output.printSuccess(`Results written to ${outputFile}`);
1870
1885
  }
1871
1886
  else {
@@ -1909,7 +1924,7 @@ const circularCommand = {
1909
1924
  }
1910
1925
  }
1911
1926
  if (outputFile) {
1912
- await writeFile(outputFile, JSON.stringify({ cycles: filtered }, null, 2));
1927
+ await safeWriteOutputFile(outputFile, JSON.stringify({ cycles: filtered }, null, 2));
1913
1928
  output.printSuccess(`Results written to ${outputFile}`);
1914
1929
  }
1915
1930
  return { success: true, data: { cycles: filtered } };
@@ -151,8 +151,11 @@ const configCommand = {
151
151
  state.maxIterations = validateNumber(maxIter, 1, 1000, state.maxIterations);
152
152
  if (timeout)
153
153
  state.timeoutMinutes = validateNumber(timeout, 1, 1440, state.timeoutMinutes);
154
- if (sources)
155
- state.taskSources = validateTaskSources(sources.split(',').map(s => s.trim()).filter(Boolean));
154
+ if (sources) {
155
+ // Cap total sources string length before splitting to prevent O(n) split on huge input
156
+ const cappedSources = sources.length > 512 ? sources.slice(0, 512) : sources;
157
+ state.taskSources = validateTaskSources(cappedSources.split(',').map(s => s.trim()).filter(Boolean));
158
+ }
156
159
  saveState(state);
157
160
  appendLog({ ts: Date.now(), event: 'config-updated', maxIterations: state.maxIterations, timeoutMinutes: state.timeoutMinutes, taskSources: state.taskSources });
158
161
  output.writeln(`Config updated: maxIterations=${state.maxIterations}, timeout=${state.timeoutMinutes}min, sources=${state.taskSources.join(',')}`);
@@ -248,12 +251,17 @@ const historyCommand = {
248
251
  { name: 'json', type: 'boolean', description: 'Output as JSON' },
249
252
  ],
250
253
  action: async (ctx) => {
251
- const query = (ctx.flags?.query || '');
254
+ const rawQuery = (ctx.flags?.query || '');
252
255
  const limit = validateNumber(ctx.flags?.limit, 1, 100, 10);
253
- if (!query) {
256
+ if (!rawQuery) {
254
257
  output.writeln('Usage: autopilot history --query "search terms" [--limit N]');
255
258
  return { success: false, message: 'Missing --query' };
256
259
  }
260
+ if (rawQuery.length > 1024) {
261
+ output.writeln('Error: --query too long (max 1024 characters)');
262
+ return { success: false, message: 'Query too long' };
263
+ }
264
+ const query = rawQuery;
257
265
  const learning = await tryLoadLearning();
258
266
  if (!learning) {
259
267
  output.writeln('Learning not available. No history to search.');
@@ -5,7 +5,7 @@
5
5
  * @module v1/cli/commands/benchmark
6
6
  */
7
7
  import { output } from '../output.js';
8
- import { writeFileSync, renameSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
8
+ import { writeFileSync, renameSync, readFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
  import { BenchmarkRunner } from '../benchmarks/benchmark-runner.js';
11
11
  // ============================================================================
@@ -63,9 +63,12 @@ const neuralCommand = {
63
63
  { command: 'monomind benchmark neural -d 768 -n 5000', description: 'Higher dimension, more vectors' },
64
64
  ],
65
65
  action: async (ctx) => {
66
- const iterations = parseInt(ctx.flags.iterations || '100', 10);
67
- const dimension = parseInt(ctx.flags.dimension || '384', 10);
68
- const numVectors = parseInt(ctx.flags.vectors || '1000', 10);
66
+ const iterationsRaw = parseInt(ctx.flags.iterations || '100', 10);
67
+ const iterations = Number.isFinite(iterationsRaw) ? Math.max(1, Math.min(iterationsRaw, 10_000)) : 100;
68
+ const dimensionRaw = parseInt(ctx.flags.dimension || '384', 10);
69
+ const dimension = Number.isFinite(dimensionRaw) ? Math.max(1, Math.min(dimensionRaw, 4096)) : 384;
70
+ const numVectorsRaw = parseInt(ctx.flags.vectors || '1000', 10);
71
+ const numVectors = Number.isFinite(numVectorsRaw) ? Math.max(1, Math.min(numVectorsRaw, 100_000)) : 1000;
69
72
  const outputFormat = ctx.flags.output || 'text';
70
73
  output.writeln();
71
74
  output.writeln(output.bold('Neural Operations Benchmark'));
@@ -228,7 +231,8 @@ const memoryCommand = {
228
231
  { command: 'monomind benchmark memory', description: 'Run memory benchmarks' },
229
232
  ],
230
233
  action: async (ctx) => {
231
- const iterations = parseInt(ctx.flags.iterations || '100', 10);
234
+ const iterationsRaw = parseInt(ctx.flags.iterations || '100', 10);
235
+ const iterations = Number.isFinite(iterationsRaw) ? Math.max(1, Math.min(iterationsRaw, 10_000)) : 100;
232
236
  const outputFormat = ctx.flags.output || 'text';
233
237
  output.writeln();
234
238
  output.writeln(output.bold('Memory Operations Benchmark'));
@@ -384,7 +388,15 @@ const allCommand = {
384
388
  if (!existsSync(resultsDir)) {
385
389
  mkdirSync(resultsDir, { recursive: true });
386
390
  }
387
- const savePath = saveFile.startsWith('/') ? saveFile : join(resultsDir, saveFile);
391
+ // Path traversal guard: resolve within resultsDir regardless of whether saveFile is absolute
392
+ const { resolve: resolvePath, basename } = await import('node:path');
393
+ const safeName = basename(saveFile);
394
+ const savePath = resolvePath(resultsDir, safeName);
395
+ const resolvedResultsDir = resolvePath(resultsDir);
396
+ if (!savePath.startsWith(resolvedResultsDir + '/') && savePath !== resolvedResultsDir) {
397
+ output.writeln(output.error(`Save path must be within ${resultsDir}`));
398
+ return { success: false, message: 'Invalid save path' };
399
+ }
388
400
  const saveTmp2 = savePath + '.tmp';
389
401
  writeFileSync(saveTmp2, JSON.stringify({
390
402
  timestamp: new Date().toISOString(),
@@ -416,14 +428,30 @@ const regressionCommand = {
416
428
  { command: 'monomind benchmark regression -b agent-spawn -a output.txt --pin-baseline', description: 'Evaluate and pin results as new baseline' },
417
429
  ],
418
430
  action: async (ctx) => {
419
- const suiteDir = ctx.flags.suite || '.monomind/benchmarks/definitions';
431
+ const suiteDirRaw = ctx.flags.suite || '.monomind/benchmarks/definitions';
420
432
  const benchmarkId = ctx.flags['benchmark-id'];
421
433
  const agentOutputFile = ctx.flags['agent-output'];
422
434
  const pinBaseline = ctx.flags['pin-baseline'] === true;
423
435
  const outputFormat = ctx.flags.output || 'text';
436
+ // Validate benchmarkId to prevent path traversal in baseline file names
437
+ if (benchmarkId !== undefined) {
438
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(benchmarkId)) {
439
+ output.writeln(output.error('Invalid benchmark-id: must contain only alphanumeric, dash, or underscore characters (max 128).'));
440
+ return { success: false, message: 'Invalid benchmark-id' };
441
+ }
442
+ }
424
443
  const runner = new BenchmarkRunner();
425
444
  const baselinesDir = join(process.cwd(), '.monomind', 'benchmarks', 'baselines');
426
- const definitions = runner.loadBenchmarks(join(process.cwd(), suiteDir));
445
+ // Path traversal guard for suiteDir
446
+ const { resolve: resolvePath2 } = await import('node:path');
447
+ const projectRoot = resolvePath2(process.cwd());
448
+ const resolvedSuiteDir = resolvePath2(process.cwd(), suiteDirRaw);
449
+ if (!resolvedSuiteDir.startsWith(projectRoot + '/') && resolvedSuiteDir !== projectRoot) {
450
+ output.writeln(output.error(`Suite directory must be within the project: ${projectRoot}`));
451
+ return { success: false, message: 'Invalid suite directory' };
452
+ }
453
+ const suiteDir = suiteDirRaw;
454
+ const definitions = runner.loadBenchmarks(resolvedSuiteDir);
427
455
  if (definitions.length === 0) {
428
456
  output.writeln(output.dim(`No benchmark definitions found in ${suiteDir}`));
429
457
  output.writeln(output.dim('Create JSON files there to define quality benchmarks.'));
@@ -446,6 +474,15 @@ const regressionCommand = {
446
474
  output.writeln(output.error(`Agent output file not found: ${agentOutputFile}`));
447
475
  return { success: false, message: 'Agent output file not found' };
448
476
  }
477
+ const MAX_AGENT_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB
478
+ try {
479
+ const agentOutputStat = statSync(agentOutputFile);
480
+ if (agentOutputStat.size > MAX_AGENT_OUTPUT_BYTES) {
481
+ output.writeln(output.error(`Agent output file too large: ${agentOutputFile} (max 10 MB)`));
482
+ return { success: false, message: 'Agent output file too large' };
483
+ }
484
+ }
485
+ catch { /* existsSync already passed; ignore stat failure */ }
449
486
  const agentOutput = readFileSync(agentOutputFile, 'utf-8');
450
487
  const targetDefs = benchmarkId
451
488
  ? definitions.filter((d) => d.benchmarkId === benchmarkId)
@@ -473,8 +510,14 @@ const regressionCommand = {
473
510
  output.writeln();
474
511
  }
475
512
  // Baseline comparison
513
+ const MAX_BASELINE_BYTES = 5 * 1024 * 1024; // 5 MB
476
514
  const baselinePath = join(baselinesDir, `${benchmarkId ?? 'all'}.json`);
477
515
  if (existsSync(baselinePath)) {
516
+ const baselineStat = statSync(baselinePath);
517
+ if (baselineStat.size > MAX_BASELINE_BYTES) {
518
+ output.writeln(output.error(`Baseline file too large (max 5 MB)`));
519
+ return { success: false, message: 'Baseline file too large' };
520
+ }
478
521
  const baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
479
522
  const hasRegression = runner.detectRegression(results, baseline);
480
523
  if (hasRegression) {
@@ -214,13 +214,16 @@ const waitCommand = {
214
214
  const { client, sessionId } = await ensureConnected(_port);
215
215
  const browser = await getBrowser();
216
216
  if (ctx.flags.ms) {
217
- await new Promise((r) => setTimeout(r, ctx.flags.ms));
217
+ const rawMs = ctx.flags.ms;
218
+ const waitMs = Number.isFinite(rawMs) ? Math.max(0, Math.min(rawMs, 60_000)) : 0; // cap at 60s
219
+ await new Promise((r) => setTimeout(r, waitMs));
218
220
  output.printSuccess(`Waited ${ctx.flags.ms}ms`);
219
221
  return { success: true };
220
222
  }
221
223
  if (ctx.flags.fn) {
222
224
  const expr = ctx.flags.fn;
223
- const timeout = ctx.flags.timeout ?? 30000;
225
+ const rawTimeout = ctx.flags.timeout ?? 30000;
226
+ const timeout = Number.isFinite(rawTimeout) ? Math.max(100, Math.min(rawTimeout, 300_000)) : 30000; // cap at 5min
224
227
  const interval = 200;
225
228
  const deadline = Date.now() + timeout;
226
229
  while (Date.now() < deadline) {
@@ -35,7 +35,7 @@ function safeParseJson(content) {
35
35
  function loadClaimsConfig() {
36
36
  const configPaths = getClaimsConfigPaths();
37
37
  for (const configPath of configPaths) {
38
- if (fs.existsSync(configPath)) {
38
+ if (fs.existsSync(configPath) && fs.statSync(configPath).size <= 1024 * 1024) {
39
39
  const content = fs.readFileSync(configPath, 'utf-8');
40
40
  return { config: safeParseJson(content), path: configPath };
41
41
  }
@@ -153,13 +153,19 @@ const checkCommand = {
153
153
  { command: 'monomind claims check -c admin:delete -u user123', description: 'Check user permission' },
154
154
  ],
155
155
  action: async (ctx) => {
156
- const claim = ctx.flags.claim;
157
- const user = ctx.flags.user || 'current';
158
- const resource = ctx.flags.resource;
156
+ const claim = (ctx.flags.claim || '').slice(0, 256);
157
+ const user = (ctx.flags.user || 'current').slice(0, 128);
158
+ const resource = (ctx.flags.resource || '').slice(0, 256);
159
159
  if (!claim) {
160
160
  output.printError('Claim is required');
161
161
  return { success: false, exitCode: 1 };
162
162
  }
163
+ // Block prototype-polluting user or resource keys.
164
+ const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
165
+ if (PROTO_KEYS.has(user)) {
166
+ output.printError(`Forbidden user key: "${user}"`);
167
+ return { success: false, exitCode: 1 };
168
+ }
163
169
  output.writeln();
164
170
  output.writeln(output.bold('Claim Check'));
165
171
  output.writeln(output.dim('─'.repeat(40)));
@@ -189,7 +195,7 @@ const checkCommand = {
189
195
  defaultClaims: ['swarm:create', 'swarm:status', 'agent:spawn', 'agent:list', 'memory:read', 'memory:write', 'task:create'],
190
196
  };
191
197
  for (const configPath of claimsConfigPaths) {
192
- if (fs.existsSync(configPath)) {
198
+ if (fs.existsSync(configPath) && fs.statSync(configPath).size <= 1024 * 1024) {
193
199
  const content = fs.readFileSync(configPath, 'utf-8');
194
200
  claimsConfig = { ...claimsConfig, ...safeParseJson(content) };
195
201
  policySource = configPath;
@@ -283,9 +289,9 @@ const grantCommand = {
283
289
  { command: 'monomind claims grant -c agent:spawn -r developer', description: 'Grant to role' },
284
290
  ],
285
291
  action: async (ctx) => {
286
- const claim = ctx.flags.claim;
287
- const user = ctx.flags.user;
288
- const role = ctx.flags.role;
292
+ const claim = (ctx.flags.claim || '').slice(0, 256);
293
+ const user = (ctx.flags.user || '').slice(0, 128);
294
+ const role = (ctx.flags.role || '').slice(0, 64);
289
295
  if (!claim) {
290
296
  output.printError('Claim is required');
291
297
  return { success: false, exitCode: 1 };
@@ -294,6 +300,12 @@ const grantCommand = {
294
300
  output.printError('Either user or role is required');
295
301
  return { success: false, exitCode: 1 };
296
302
  }
303
+ // Block prototype-polluting user or role keys.
304
+ const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
305
+ if ((user && PROTO_KEYS.has(user)) || (role && PROTO_KEYS.has(role))) {
306
+ output.printError('Forbidden user or role key');
307
+ return { success: false, exitCode: 1 };
308
+ }
297
309
  try {
298
310
  const { config, path: configPath } = loadClaimsConfig();
299
311
  if (user) {
@@ -343,9 +355,9 @@ const revokeCommand = {
343
355
  { command: 'monomind claims revoke -c admin:* -r guest', description: 'Revoke from role' },
344
356
  ],
345
357
  action: async (ctx) => {
346
- const claim = ctx.flags.claim;
347
- const user = ctx.flags.user;
348
- const role = ctx.flags.role;
358
+ const claim = (ctx.flags.claim || '').slice(0, 256);
359
+ const user = (ctx.flags.user || '').slice(0, 128);
360
+ const role = (ctx.flags.role || '').slice(0, 64);
349
361
  if (!claim) {
350
362
  output.printError('Claim is required');
351
363
  return { success: false, exitCode: 1 };
@@ -354,6 +366,12 @@ const revokeCommand = {
354
366
  output.printError('Either user or role is required');
355
367
  return { success: false, exitCode: 1 };
356
368
  }
369
+ // Block prototype-polluting user or role keys.
370
+ const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
371
+ if ((user && PROTO_KEYS.has(user)) || (role && PROTO_KEYS.has(role))) {
372
+ output.printError('Forbidden user or role key');
373
+ return { success: false, exitCode: 1 };
374
+ }
357
375
  try {
358
376
  const { config, path: configPath } = loadClaimsConfig();
359
377
  let removed = false;