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.
- package/.claude/agents/generated/channel-intelligence-director.md +87 -0
- package/.claude/agents/generated/chief-growth-officer.md +88 -0
- package/.claude/agents/generated/content-seo-strategist.md +90 -0
- package/.claude/agents/generated/developer-community-strategist.md +91 -0
- package/.claude/agents/generated/outreach-partnership-strategist.md +90 -0
- package/.claude/agents/generated/social-media-strategist.md +91 -0
- package/.claude/agents/generated/video-visual-strategist.md +90 -0
- package/.claude/commands/mastermind/master.md +1 -1
- package/.claude/helpers/auto-memory-hook.mjs +13 -4
- package/.claude/helpers/control-start.cjs +5 -0
- package/.claude/helpers/event-logger.cjs +114 -0
- package/.claude/helpers/handlers/adr-draft-handler.cjs +19 -5
- package/.claude/helpers/handlers/agent-start-handler.cjs +13 -4
- package/.claude/helpers/handlers/compact-handler.cjs +2 -0
- package/.claude/helpers/handlers/edit-handler.cjs +1 -1
- package/.claude/helpers/handlers/gates-handler.cjs +3 -0
- package/.claude/helpers/handlers/graph-status-handler.cjs +14 -8
- package/.claude/helpers/handlers/loops-status-handler.cjs +5 -2
- package/.claude/helpers/handlers/route-handler.cjs +13 -6
- package/.claude/helpers/handlers/session-handler.cjs +11 -4
- package/.claude/helpers/handlers/session-restore-handler.cjs +21 -11
- package/.claude/helpers/handlers/task-handler.cjs +13 -5
- package/.claude/helpers/intelligence.cjs +7 -2
- package/.claude/helpers/loop-tracker.cjs +15 -3
- package/.claude/helpers/memory.cjs +6 -1
- package/.claude/helpers/router.cjs +5 -2
- package/.claude/helpers/session.cjs +2 -0
- package/.claude/helpers/statusline.cjs +10 -2
- package/.claude/helpers/utils/micro-agents.cjs +20 -4
- package/.claude/scheduled_tasks.lock +1 -1
- package/.claude/settings.json +92 -1
- package/.claude/skills/mastermind/_protocol.md +25 -15
- package/.claude/skills/mastermind/architect.md +3 -3
- package/.claude/skills/mastermind/autodev.md +4 -2
- package/.claude/skills/mastermind/idea.md +10 -0
- package/.claude/skills/mastermind/ops.md +3 -3
- package/.claude/skills/mastermind/runorg.md +153 -86
- package/package.json +20 -3
- package/packages/@monomind/cli/dist/src/agents/registry-builder.js +2 -0
- package/packages/@monomind/cli/dist/src/autopilot-state.js +10 -5
- package/packages/@monomind/cli/dist/src/benchmarks/benchmark-runner.js +13 -0
- package/packages/@monomind/cli/dist/src/benchmarks/metric-evaluators.js +20 -9
- package/packages/@monomind/cli/dist/src/browser/actions.js +10 -3
- package/packages/@monomind/cli/dist/src/browser/browser.js +12 -2
- package/packages/@monomind/cli/dist/src/browser/cdp.js +21 -3
- package/packages/@monomind/cli/dist/src/browser/har.js +27 -5
- package/packages/@monomind/cli/dist/src/commands/agent.js +11 -8
- package/packages/@monomind/cli/dist/src/commands/analyze.js +36 -21
- package/packages/@monomind/cli/dist/src/commands/autopilot.js +12 -4
- package/packages/@monomind/cli/dist/src/commands/benchmark.js +51 -8
- package/packages/@monomind/cli/dist/src/commands/browse.js +5 -2
- package/packages/@monomind/cli/dist/src/commands/claims.js +29 -11
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +25 -5
- package/packages/@monomind/cli/dist/src/commands/config.js +15 -7
- package/packages/@monomind/cli/dist/src/commands/daemon.js +6 -0
- package/packages/@monomind/cli/dist/src/commands/deployment.js +34 -19
- package/packages/@monomind/cli/dist/src/commands/doctor.js +97 -20
- package/packages/@monomind/cli/dist/src/commands/guidance.js +15 -2
- package/packages/@monomind/cli/dist/src/commands/hive-mind.js +37 -14
- package/packages/@monomind/cli/dist/src/commands/hooks.js +42 -25
- package/packages/@monomind/cli/dist/src/commands/init.js +9 -4
- package/packages/@monomind/cli/dist/src/commands/issues.js +29 -26
- package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -5
- package/packages/@monomind/cli/dist/src/commands/memory.js +10 -0
- package/packages/@monomind/cli/dist/src/commands/migrate.js +5 -5
- package/packages/@monomind/cli/dist/src/commands/monograph.js +18 -5
- package/packages/@monomind/cli/dist/src/commands/monovector/backup.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/monovector/benchmark.js +20 -7
- package/packages/@monomind/cli/dist/src/commands/monovector/import.js +15 -0
- package/packages/@monomind/cli/dist/src/commands/monovector/migrate.js +4 -1
- package/packages/@monomind/cli/dist/src/commands/monovector/optimize.js +11 -0
- package/packages/@monomind/cli/dist/src/commands/monovector/setup.js +11 -1
- package/packages/@monomind/cli/dist/src/commands/neural.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/performance.js +20 -7
- package/packages/@monomind/cli/dist/src/commands/platforms.js +90 -8
- package/packages/@monomind/cli/dist/src/commands/plugins.js +12 -5
- package/packages/@monomind/cli/dist/src/commands/process.js +33 -10
- package/packages/@monomind/cli/dist/src/commands/progress.js +5 -3
- package/packages/@monomind/cli/dist/src/commands/providers.js +5 -5
- package/packages/@monomind/cli/dist/src/commands/replay.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/route.js +27 -7
- package/packages/@monomind/cli/dist/src/commands/security.js +4 -0
- package/packages/@monomind/cli/dist/src/commands/session.js +12 -1
- package/packages/@monomind/cli/dist/src/commands/start.js +11 -4
- package/packages/@monomind/cli/dist/src/commands/status.js +7 -4
- package/packages/@monomind/cli/dist/src/commands/swarm.js +27 -13
- package/packages/@monomind/cli/dist/src/commands/task.js +26 -11
- package/packages/@monomind/cli/dist/src/commands/tokens.js +7 -2
- package/packages/@monomind/cli/dist/src/commands/transfer-store.js +36 -22
- package/packages/@monomind/cli/dist/src/commands/update.js +15 -3
- package/packages/@monomind/cli/dist/src/commands/workflow.js +39 -6
- package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +18 -7
- package/packages/@monomind/cli/dist/src/consensus/vote-signer.js +25 -8
- package/packages/@monomind/cli/dist/src/index.js +7 -3
- package/packages/@monomind/cli/dist/src/init/executor.js +14 -11
- package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.js +20 -4
- package/packages/@monomind/cli/dist/src/init/statusline-generator.js +36 -15
- package/packages/@monomind/cli/dist/src/mcp-tools/a2a-tools.js +98 -13
- package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +16 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/analyze-tools.js +80 -17
- package/packages/@monomind/cli/dist/src/mcp-tools/browser-tools.js +84 -22
- package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +35 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +82 -17
- package/packages/@monomind/cli/dist/src/mcp-tools/coordination-tools.js +37 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +49 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +45 -18
- package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +75 -25
- package/packages/@monomind/cli/dist/src/mcp-tools/guidance-tools.js +32 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +91 -20
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +188 -29
- package/packages/@monomind/cli/dist/src/mcp-tools/memory-tools.js +25 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +11 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +148 -26
- package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.js +44 -9
- package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +45 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/progress-tools.js +7 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/request-tracker.js +15 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/security-tools.js +61 -9
- package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +45 -14
- package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +15 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +14 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +52 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +40 -6
- package/packages/@monomind/cli/dist/src/mcp-tools/transfer-tools.js +37 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/workflow-tools.js +29 -6
- package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +26 -10
- package/packages/@monomind/cli/dist/src/memory/intelligence.js +80 -19
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +21 -2
- package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +67 -3
- package/packages/@monomind/cli/dist/src/memory/sona-optimizer.js +14 -4
- package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +43 -7
- package/packages/@monomind/cli/dist/src/monovector/coverage-router.js +8 -4
- package/packages/@monomind/cli/dist/src/monovector/coverage-tools.js +6 -3
- package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +13 -0
- package/packages/@monomind/cli/dist/src/monovector/route-outcomes.d.ts +2 -1
- package/packages/@monomind/cli/dist/src/monovector/route-outcomes.js +46 -4
- package/packages/@monomind/cli/dist/src/plugins/manager.js +8 -3
- package/packages/@monomind/cli/dist/src/plugins/store/discovery.js +46 -2
- package/packages/@monomind/cli/dist/src/plugins/store/search.js +5 -4
- package/packages/@monomind/cli/dist/src/production/circuit-breaker.js +17 -3
- package/packages/@monomind/cli/dist/src/production/error-handler.js +3 -0
- package/packages/@monomind/cli/dist/src/production/monitoring.js +20 -3
- package/packages/@monomind/cli/dist/src/production/rate-limiter.js +13 -4
- package/packages/@monomind/cli/dist/src/production/retry.js +17 -9
- package/packages/@monomind/cli/dist/src/routing/embed-worker.js +6 -2
- package/packages/@monomind/cli/dist/src/routing/embedder.js +0 -0
- package/packages/@monomind/cli/dist/src/routing/llm-caller.js +13 -2
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +18 -3
- package/packages/@monomind/cli/dist/src/services/claim-service.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/services/claim-service.js +8 -0
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +14 -2
- package/packages/@monomind/cli/dist/src/services/headless-worker-executor.js +18 -2
- package/packages/@monomind/cli/dist/src/services/worker-daemon.js +53 -12
- package/packages/@monomind/cli/dist/src/transfer/anonymization/index.d.ts +0 -3
- package/packages/@monomind/cli/dist/src/transfer/anonymization/index.js +16 -1
- package/packages/@monomind/cli/dist/src/transfer/export.js +8 -0
- package/packages/@monomind/cli/dist/src/transfer/ipfs/upload.js +33 -3
- package/packages/@monomind/cli/dist/src/transfer/serialization/cfp.js +9 -3
- package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +37 -3
- package/packages/@monomind/cli/dist/src/transfer/store/discovery.js +45 -3
- package/packages/@monomind/cli/dist/src/transfer/store/download.js +5 -0
- package/packages/@monomind/cli/dist/src/transfer/store/publish.js +13 -1
- package/packages/@monomind/cli/dist/src/transfer/store/registry.d.ts +8 -0
- package/packages/@monomind/cli/dist/src/transfer/store/registry.js +30 -5
- package/packages/@monomind/cli/dist/src/transfer/store/search.js +20 -5
- package/packages/@monomind/cli/dist/src/update/checker.js +59 -7
- package/packages/@monomind/cli/dist/src/update/executor.js +50 -3
- package/packages/@monomind/cli/dist/src/update/index.js +18 -1
- package/packages/@monomind/cli/dist/src/update/rate-limiter.d.ts +6 -0
- package/packages/@monomind/cli/dist/src/update/rate-limiter.js +79 -7
- package/packages/@monomind/cli/dist/src/update/validator.js +52 -1
- package/packages/@monomind/cli/package.json +2 -3
|
@@ -35,7 +35,8 @@ export const coverageRouterTools = [
|
|
|
35
35
|
},
|
|
36
36
|
handler: async (input) => {
|
|
37
37
|
try {
|
|
38
|
-
const
|
|
38
|
+
const rawPath = typeof input.path === 'string' ? input.path.slice(0, 512) : '';
|
|
39
|
+
const result = await coverageRoute(rawPath, { threshold: input.threshold ?? 80 });
|
|
39
40
|
if (!result.found) {
|
|
40
41
|
return text('No coverage report found. Run your test suite with coverage enabled (e.g. `vitest run --coverage`), then retry.');
|
|
41
42
|
}
|
|
@@ -66,7 +67,7 @@ export const coverageRouterTools = [
|
|
|
66
67
|
try {
|
|
67
68
|
const result = await coverageGaps({
|
|
68
69
|
threshold: input.threshold ?? 80,
|
|
69
|
-
path: input.path
|
|
70
|
+
path: typeof input.path === 'string' ? input.path.slice(0, 512) : undefined,
|
|
70
71
|
groupByAgent: true,
|
|
71
72
|
});
|
|
72
73
|
if (!result.found)
|
|
@@ -96,7 +97,9 @@ export const coverageRouterTools = [
|
|
|
96
97
|
},
|
|
97
98
|
handler: async (input) => {
|
|
98
99
|
try {
|
|
99
|
-
const
|
|
100
|
+
const rawPath = typeof input.path === 'string' ? input.path.slice(0, 512) : '.';
|
|
101
|
+
const rawLimit = typeof input.limit === 'number' ? Math.min(Math.max(1, input.limit), 200) : 20;
|
|
102
|
+
const result = await coverageSuggest(rawPath, { threshold: input.threshold ?? 80, limit: rawLimit });
|
|
100
103
|
if (!result.found) {
|
|
101
104
|
return text('No coverage report found. Run your test suite with coverage enabled, then retry.');
|
|
102
105
|
}
|
|
@@ -626,8 +626,11 @@ export function suggestReviewers(files, fileRisks) {
|
|
|
626
626
|
return Array.from(reviewers).slice(0, 5);
|
|
627
627
|
}
|
|
628
628
|
// Analysis result cache
|
|
629
|
+
// FIFO eviction cap matches diffCache. Without a cap, repeated calls to analyzeDiff
|
|
630
|
+
// with unique refs (e.g. HEAD~0...HEAD~N) would grow this Map to GBs.
|
|
629
631
|
const analysisCache = new Map();
|
|
630
632
|
const ANALYSIS_CACHE_TTL_MS = 3000; // 3 seconds
|
|
633
|
+
const ANALYSIS_CACHE_MAX_ENTRIES = 50;
|
|
631
634
|
/**
|
|
632
635
|
* Analyze a diff with full analysis (optimized with caching)
|
|
633
636
|
*/
|
|
@@ -662,6 +665,11 @@ export async function analyzeDiff(options) {
|
|
|
662
665
|
recommendedReviewers,
|
|
663
666
|
};
|
|
664
667
|
// Cache the result
|
|
668
|
+
if (analysisCache.size >= ANALYSIS_CACHE_MAX_ENTRIES) {
|
|
669
|
+
const oldestKey = analysisCache.keys().next().value;
|
|
670
|
+
if (oldestKey !== undefined)
|
|
671
|
+
analysisCache.delete(oldestKey);
|
|
672
|
+
}
|
|
665
673
|
analysisCache.set(ref, { result, timestamp: Date.now() });
|
|
666
674
|
return result;
|
|
667
675
|
}
|
|
@@ -692,6 +700,11 @@ export function analyzeDiffSync(options) {
|
|
|
692
700
|
fileRisks,
|
|
693
701
|
recommendedReviewers,
|
|
694
702
|
};
|
|
703
|
+
if (analysisCache.size >= ANALYSIS_CACHE_MAX_ENTRIES) {
|
|
704
|
+
const oldestKey = analysisCache.keys().next().value;
|
|
705
|
+
if (oldestKey !== undefined)
|
|
706
|
+
analysisCache.delete(oldestKey);
|
|
707
|
+
}
|
|
695
708
|
analysisCache.set(ref, { result, timestamp: Date.now() });
|
|
696
709
|
return result;
|
|
697
710
|
}
|
|
@@ -10,7 +10,8 @@ export interface RouteOutcomeRecord {
|
|
|
10
10
|
measuredSuccess?: boolean;
|
|
11
11
|
quality?: number;
|
|
12
12
|
}
|
|
13
|
-
/** Append a route recommendation (pre-outcome).
|
|
13
|
+
/** Append a route recommendation (pre-outcome). Opportunistically trims the
|
|
14
|
+
* file to MAX_ROUTE_RECORDS lines to prevent unbounded growth. */
|
|
14
15
|
export declare function recordRoute(baseDir: string, rec: RouteOutcomeRecord): Promise<void>;
|
|
15
16
|
/** Join outcome data onto the most recent matching route record by routeId. */
|
|
16
17
|
export declare function joinOutcome(baseDir: string, routeId: string, outcome: {
|
|
@@ -3,16 +3,42 @@
|
|
|
3
3
|
* what actually happened. This is the foundation for routing-accuracy metrics
|
|
4
4
|
* and for giving SONA a real training label.
|
|
5
5
|
*/
|
|
6
|
-
import { promises as fs } from 'node:fs';
|
|
6
|
+
import { promises as fs, statSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
/** Refuse to read files larger than this to prevent OOM. */
|
|
9
|
+
const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
10
|
+
/** Cap string fields stored in each record to prevent file bloat. */
|
|
11
|
+
const MAX_FIELD_LEN = 500;
|
|
8
12
|
function storePath(baseDir) {
|
|
9
13
|
return join(baseDir, 'route-outcomes.jsonl');
|
|
10
14
|
}
|
|
11
|
-
/**
|
|
15
|
+
/** Maximum number of records to keep in route-outcomes.jsonl.
|
|
16
|
+
* computeRoutingAccuracy only ever reads the last 100 records, so anything
|
|
17
|
+
* older is dead weight. Keeping 500 gives a comfortable buffer while bounding
|
|
18
|
+
* the file size and keeping joinOutcome's full-file rewrite cheap. */
|
|
19
|
+
const MAX_ROUTE_RECORDS = 500;
|
|
20
|
+
/** Append a route recommendation (pre-outcome). Opportunistically trims the
|
|
21
|
+
* file to MAX_ROUTE_RECORDS lines to prevent unbounded growth. */
|
|
12
22
|
export async function recordRoute(baseDir, rec) {
|
|
13
23
|
try {
|
|
14
24
|
await fs.mkdir(baseDir, { recursive: true });
|
|
15
|
-
|
|
25
|
+
const path = storePath(baseDir);
|
|
26
|
+
// Cap string fields to prevent individual records from bloating the file.
|
|
27
|
+
const safeRec = {
|
|
28
|
+
...rec,
|
|
29
|
+
routeId: rec.routeId.slice(0, MAX_FIELD_LEN),
|
|
30
|
+
task: rec.task.slice(0, MAX_FIELD_LEN),
|
|
31
|
+
recommendedAgent: rec.recommendedAgent.slice(0, MAX_FIELD_LEN),
|
|
32
|
+
routingMethod: rec.routingMethod.slice(0, 64),
|
|
33
|
+
};
|
|
34
|
+
await fs.appendFile(path, JSON.stringify(safeRec) + '\n', 'utf8');
|
|
35
|
+
// Opportunistic trim: rewrite only when the file exceeds the cap.
|
|
36
|
+
// Avoids an extra stat() on every call by catching the overcount lazily.
|
|
37
|
+
const content = await fs.readFile(path, 'utf8').catch(() => '');
|
|
38
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
39
|
+
if (lines.length > MAX_ROUTE_RECORDS) {
|
|
40
|
+
await fs.writeFile(path, lines.slice(-MAX_ROUTE_RECORDS).join('\n') + '\n', 'utf8');
|
|
41
|
+
}
|
|
16
42
|
}
|
|
17
43
|
catch {
|
|
18
44
|
// Non-fatal — telemetry must never break routing
|
|
@@ -22,6 +48,11 @@ export async function recordRoute(baseDir, rec) {
|
|
|
22
48
|
export async function joinOutcome(baseDir, routeId, outcome) {
|
|
23
49
|
try {
|
|
24
50
|
const path = storePath(baseDir);
|
|
51
|
+
try {
|
|
52
|
+
if (statSync(path).size > MAX_FILE_BYTES)
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
catch { /* file absent */ }
|
|
25
56
|
const content = await fs.readFile(path, 'utf8').catch(() => '');
|
|
26
57
|
if (!content)
|
|
27
58
|
return;
|
|
@@ -52,6 +83,11 @@ export async function joinLatestUnresolved(baseDir, outcome, maxAgeMs = 600_000
|
|
|
52
83
|
) {
|
|
53
84
|
try {
|
|
54
85
|
const path = storePath(baseDir);
|
|
86
|
+
try {
|
|
87
|
+
if (statSync(path).size > MAX_FILE_BYTES)
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
catch { /* file absent */ }
|
|
55
91
|
const content = await fs.readFile(path, 'utf8').catch(() => '');
|
|
56
92
|
if (!content)
|
|
57
93
|
return null;
|
|
@@ -81,7 +117,13 @@ export async function joinLatestUnresolved(baseDir, outcome, maxAgeMs = 600_000
|
|
|
81
117
|
/** Read all outcome records (for metrics). */
|
|
82
118
|
export async function readOutcomes(baseDir) {
|
|
83
119
|
try {
|
|
84
|
-
const
|
|
120
|
+
const p = storePath(baseDir);
|
|
121
|
+
try {
|
|
122
|
+
if (statSync(p).size > MAX_FILE_BYTES)
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
catch { /* file absent */ }
|
|
126
|
+
const content = await fs.readFile(p, 'utf8').catch(() => '');
|
|
85
127
|
if (!content)
|
|
86
128
|
return [];
|
|
87
129
|
return content.trim().split('\n').map(l => {
|
|
@@ -73,7 +73,8 @@ export class PluginManager {
|
|
|
73
73
|
}
|
|
74
74
|
async loadManifest() {
|
|
75
75
|
try {
|
|
76
|
-
|
|
76
|
+
const MAX_MANIFEST_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
77
|
+
if (fs.existsSync(this.config.manifestPath) && fs.statSync(this.config.manifestPath).size <= MAX_MANIFEST_BYTES) {
|
|
77
78
|
const content = fs.readFileSync(this.config.manifestPath, 'utf-8');
|
|
78
79
|
const parsed = JSON.parse(content);
|
|
79
80
|
if (Object.prototype.hasOwnProperty.call(parsed, '__proto__') ||
|
|
@@ -148,7 +149,8 @@ export class PluginManager {
|
|
|
148
149
|
let installedVersion = version || 'latest';
|
|
149
150
|
let commands = [];
|
|
150
151
|
let hooks = [];
|
|
151
|
-
|
|
152
|
+
const MAX_PKG_JSON_BYTES = 1024 * 1024; // 1 MB
|
|
153
|
+
if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).size <= MAX_PKG_JSON_BYTES) {
|
|
152
154
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
153
155
|
installedVersion = pkg.version;
|
|
154
156
|
// Check for monomind plugin metadata
|
|
@@ -205,6 +207,9 @@ export class PluginManager {
|
|
|
205
207
|
if (!fs.existsSync(packageJsonPath)) {
|
|
206
208
|
return { success: false, error: 'No package.json found at path' };
|
|
207
209
|
}
|
|
210
|
+
if (fs.statSync(packageJsonPath).size > 1024 * 1024) {
|
|
211
|
+
return { success: false, error: 'package.json exceeds size limit' };
|
|
212
|
+
}
|
|
208
213
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
209
214
|
const packageName = pkg.name;
|
|
210
215
|
if (!isValidPluginKey(packageName)) {
|
|
@@ -423,7 +428,7 @@ export class PluginManager {
|
|
|
423
428
|
// Update manifest
|
|
424
429
|
const installDir = path.join(this.config.pluginsDir, 'node_modules');
|
|
425
430
|
const packageJsonPath = path.join(installDir, packageName, 'package.json');
|
|
426
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
431
|
+
if (fs.existsSync(packageJsonPath) && fs.statSync(packageJsonPath).size <= 1024 * 1024) {
|
|
427
432
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
428
433
|
existing.version = pkg.version;
|
|
429
434
|
existing.commands = pkg['monomind']?.commands || existing.commands;
|
|
@@ -5,23 +5,67 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as crypto from 'crypto';
|
|
7
7
|
import { resolveIPNS, fetchFromIPFS } from '../../transfer/ipfs/client.js';
|
|
8
|
+
/** Maximum bytes accepted from npm API responses to prevent OOM. */
|
|
9
|
+
const NPM_RESPONSE_MAX_BYTES = 256 * 1024; // 256 KB
|
|
10
|
+
/**
|
|
11
|
+
* Read a fetch Response body up to maxBytes. AbortSignal.timeout() only
|
|
12
|
+
* bounds wall-clock time, NOT response body size; a slow-drip or large
|
|
13
|
+
* response would otherwise buffer unbounded memory here.
|
|
14
|
+
*/
|
|
15
|
+
async function readBoundedJson(res, maxBytes) {
|
|
16
|
+
const lenHdr = res.headers.get('content-length');
|
|
17
|
+
if (lenHdr) {
|
|
18
|
+
const declared = parseInt(lenHdr, 10);
|
|
19
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
20
|
+
throw new Error(`npm response too large: ${declared} bytes`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!res.body)
|
|
24
|
+
return JSON.parse('');
|
|
25
|
+
const reader = res.body.getReader();
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let total = 0;
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done)
|
|
31
|
+
break;
|
|
32
|
+
if (value) {
|
|
33
|
+
total += value.byteLength;
|
|
34
|
+
if (total > maxBytes) {
|
|
35
|
+
await reader.cancel();
|
|
36
|
+
throw new Error(`npm response exceeded ${maxBytes} bytes`);
|
|
37
|
+
}
|
|
38
|
+
chunks.push(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const buf = new Uint8Array(total);
|
|
42
|
+
let off = 0;
|
|
43
|
+
for (const c of chunks) {
|
|
44
|
+
buf.set(c, off);
|
|
45
|
+
off += c.byteLength;
|
|
46
|
+
}
|
|
47
|
+
return JSON.parse(new TextDecoder('utf-8').decode(buf));
|
|
48
|
+
}
|
|
8
49
|
/**
|
|
9
50
|
* Fetch real npm download stats for a package
|
|
10
51
|
*/
|
|
11
52
|
async function fetchNpmStats(packageName) {
|
|
53
|
+
// Validate package name length to avoid constructing huge URLs
|
|
54
|
+
if (!packageName || packageName.length > 214)
|
|
55
|
+
return null;
|
|
12
56
|
try {
|
|
13
57
|
// Fetch last week downloads
|
|
14
58
|
const downloadsUrl = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;
|
|
15
59
|
const downloadsRes = await fetch(downloadsUrl, { signal: AbortSignal.timeout(3000) });
|
|
16
60
|
if (!downloadsRes.ok)
|
|
17
61
|
return null;
|
|
18
|
-
const downloadsData = await downloadsRes
|
|
62
|
+
const downloadsData = await readBoundedJson(downloadsRes, NPM_RESPONSE_MAX_BYTES);
|
|
19
63
|
// Fetch package info for version
|
|
20
64
|
const packageUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
21
65
|
const packageRes = await fetch(packageUrl, { signal: AbortSignal.timeout(3000) });
|
|
22
66
|
let version = 'unknown';
|
|
23
67
|
if (packageRes.ok) {
|
|
24
|
-
const packageData = await packageRes
|
|
68
|
+
const packageData = await readBoundedJson(packageRes, NPM_RESPONSE_MAX_BYTES);
|
|
25
69
|
version = packageData.version || 'unknown';
|
|
26
70
|
}
|
|
27
71
|
return {
|
|
@@ -9,7 +9,7 @@ export function searchPlugins(registry, options = {}) {
|
|
|
9
9
|
let plugins = [...registry.plugins];
|
|
10
10
|
// Text search (name, displayName, description, tags)
|
|
11
11
|
if (options.query) {
|
|
12
|
-
const query = options.query.toLowerCase();
|
|
12
|
+
const query = options.query.slice(0, 256).toLowerCase();
|
|
13
13
|
plugins = plugins.filter(p => p.name.toLowerCase().includes(query) ||
|
|
14
14
|
p.displayName.toLowerCase().includes(query) ||
|
|
15
15
|
p.description.toLowerCase().includes(query) ||
|
|
@@ -88,8 +88,8 @@ export function searchPlugins(registry, options = {}) {
|
|
|
88
88
|
});
|
|
89
89
|
// Pagination
|
|
90
90
|
const total = plugins.length;
|
|
91
|
-
const limit = options.limit || 20;
|
|
92
|
-
const offset = options.offset || 0;
|
|
91
|
+
const limit = Math.min(Math.max(1, options.limit || 20), 200);
|
|
92
|
+
const offset = Math.max(0, Math.min(options.offset || 0, 100_000));
|
|
93
93
|
const page = Math.floor(offset / limit) + 1;
|
|
94
94
|
plugins = plugins.slice(offset, offset + limit);
|
|
95
95
|
return {
|
|
@@ -105,7 +105,8 @@ export function searchPlugins(registry, options = {}) {
|
|
|
105
105
|
* Get search suggestions based on partial query
|
|
106
106
|
*/
|
|
107
107
|
export function getPluginSearchSuggestions(registry, partialQuery, limit = 10) {
|
|
108
|
-
const query = partialQuery.toLowerCase();
|
|
108
|
+
const query = partialQuery.slice(0, 256).toLowerCase();
|
|
109
|
+
limit = Math.min(Math.max(1, limit), 100);
|
|
109
110
|
const suggestions = new Set();
|
|
110
111
|
// Search in plugin names
|
|
111
112
|
for (const plugin of registry.plugins) {
|
|
@@ -105,6 +105,11 @@ export class CircuitBreaker {
|
|
|
105
105
|
this.failures.push(now);
|
|
106
106
|
// Clean old failures outside window
|
|
107
107
|
this.failures = this.failures.filter(t => t > now - this.config.failureWindowMs);
|
|
108
|
+
// Hard cap: prevent unbounded growth under rapid-fire recordFailure() calls
|
|
109
|
+
// before the window filter prunes entries (e.g. clock skew or test hammering).
|
|
110
|
+
if (this.failures.length > 10_000) {
|
|
111
|
+
this.failures = this.failures.slice(-10_000);
|
|
112
|
+
}
|
|
108
113
|
// Check if we should open the circuit
|
|
109
114
|
if (this.state === 'closed' && this.failures.length >= this.config.failureThreshold) {
|
|
110
115
|
this.transitionTo('open');
|
|
@@ -214,17 +219,26 @@ export class CircuitBreaker {
|
|
|
214
219
|
// ============================================================================
|
|
215
220
|
const circuitBreakers = new Map();
|
|
216
221
|
const MAX_CIRCUIT_BREAKERS = 1000;
|
|
222
|
+
const MAX_CIRCUIT_BREAKER_NAME_LEN = 256;
|
|
223
|
+
const FORBIDDEN_CB_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
217
224
|
/**
|
|
218
225
|
* Get or create a circuit breaker by name
|
|
219
226
|
*/
|
|
220
227
|
export function getCircuitBreaker(name, config) {
|
|
221
|
-
|
|
228
|
+
// Sanitize name: cap length and block prototype-pollution keys
|
|
229
|
+
const safeName = typeof name === 'string'
|
|
230
|
+
? name.slice(0, MAX_CIRCUIT_BREAKER_NAME_LEN)
|
|
231
|
+
: 'default';
|
|
232
|
+
if (FORBIDDEN_CB_NAMES.has(safeName)) {
|
|
233
|
+
throw new Error(`Invalid circuit breaker name: ${safeName}`);
|
|
234
|
+
}
|
|
235
|
+
if (!circuitBreakers.has(safeName)) {
|
|
222
236
|
if (circuitBreakers.size >= MAX_CIRCUIT_BREAKERS) {
|
|
223
237
|
throw new Error(`Circuit breaker registry full (max ${MAX_CIRCUIT_BREAKERS})`);
|
|
224
238
|
}
|
|
225
|
-
circuitBreakers.set(
|
|
239
|
+
circuitBreakers.set(safeName, new CircuitBreaker(config));
|
|
226
240
|
}
|
|
227
|
-
return circuitBreakers.get(
|
|
241
|
+
return circuitBreakers.get(safeName);
|
|
228
242
|
}
|
|
229
243
|
/**
|
|
230
244
|
* Get all circuit breaker stats
|
|
@@ -134,6 +134,9 @@ export class ErrorHandler {
|
|
|
134
134
|
return input;
|
|
135
135
|
if (_depth > 20)
|
|
136
136
|
return { '[MAX_DEPTH]': true };
|
|
137
|
+
// Primitives and null cannot be stored in a WeakSet — return them as-is.
|
|
138
|
+
if (input === null || typeof input !== 'object')
|
|
139
|
+
return input;
|
|
137
140
|
if (_seen.has(input))
|
|
138
141
|
return { '[CIRCULAR]': true };
|
|
139
142
|
_seen.add(input);
|
|
@@ -78,11 +78,25 @@ export class MonitoringHooks {
|
|
|
78
78
|
// Apply sampling
|
|
79
79
|
if (Math.random() > this.config.samplingRate)
|
|
80
80
|
return;
|
|
81
|
+
// Cap metric name and label keys/values to prevent DoS
|
|
82
|
+
const safeName = String(name).slice(0, 256);
|
|
83
|
+
const safeLabels = {};
|
|
84
|
+
let labelCount = 0;
|
|
85
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
86
|
+
if (!Object.hasOwn(labels, k))
|
|
87
|
+
continue;
|
|
88
|
+
if (++labelCount > 20)
|
|
89
|
+
break;
|
|
90
|
+
// Block prototype pollution keys
|
|
91
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype')
|
|
92
|
+
continue;
|
|
93
|
+
safeLabels[String(k).slice(0, 64)] = String(v).slice(0, 512);
|
|
94
|
+
}
|
|
81
95
|
const event = {
|
|
82
|
-
name,
|
|
96
|
+
name: safeName,
|
|
83
97
|
type,
|
|
84
98
|
value,
|
|
85
|
-
labels: { ...this.config.globalLabels, ...
|
|
99
|
+
labels: { ...this.config.globalLabels, ...safeLabels },
|
|
86
100
|
timestamp: Date.now(),
|
|
87
101
|
};
|
|
88
102
|
this.metrics.push(event);
|
|
@@ -131,7 +145,8 @@ export class MonitoringHooks {
|
|
|
131
145
|
* Register a health check
|
|
132
146
|
*/
|
|
133
147
|
registerHealthCheck(name, check) {
|
|
134
|
-
|
|
148
|
+
// Cap name length to prevent map-key inflation
|
|
149
|
+
this.healthChecks.set(String(name).slice(0, 128), check);
|
|
135
150
|
}
|
|
136
151
|
/**
|
|
137
152
|
* Run all health checks
|
|
@@ -285,6 +300,8 @@ export class MonitoringHooks {
|
|
|
285
300
|
// Private Methods
|
|
286
301
|
// ============================================================================
|
|
287
302
|
checkAlerts(name, value) {
|
|
303
|
+
if (!Object.hasOwn(this.config.alertThresholds, name))
|
|
304
|
+
return;
|
|
288
305
|
const thresholds = this.config.alertThresholds[name];
|
|
289
306
|
if (!thresholds)
|
|
290
307
|
return;
|
|
@@ -33,6 +33,11 @@ export class RateLimiter {
|
|
|
33
33
|
* Check if a request is allowed
|
|
34
34
|
*/
|
|
35
35
|
check(operation, userId) {
|
|
36
|
+
// Cap operation and userId lengths so bucket keys cannot inflate the Map
|
|
37
|
+
const safeOp = typeof operation === 'string' ? operation.slice(0, 256) : 'unknown';
|
|
38
|
+
const safeUserId = userId ? userId.slice(0, 256) : undefined;
|
|
39
|
+
operation = safeOp;
|
|
40
|
+
userId = safeUserId;
|
|
36
41
|
// Check whitelist
|
|
37
42
|
if (this.config.whitelist.includes(operation)) {
|
|
38
43
|
return { allowed: true, remaining: Infinity, resetAt: 0 };
|
|
@@ -157,10 +162,14 @@ export class RateLimiter {
|
|
|
157
162
|
// Private Methods
|
|
158
163
|
// ============================================================================
|
|
159
164
|
getLimits(operation) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
// Object.hasOwn guards against prototype-pollution: if someone passes
|
|
166
|
+
// '__proto__' as the operation name, falling through to the default is safe.
|
|
167
|
+
return (Object.hasOwn(this.config.operationLimits, operation)
|
|
168
|
+
? this.config.operationLimits[operation]
|
|
169
|
+
: {
|
|
170
|
+
maxRequests: this.config.maxRequests,
|
|
171
|
+
windowMs: this.config.windowMs,
|
|
172
|
+
});
|
|
164
173
|
}
|
|
165
174
|
createBucket() {
|
|
166
175
|
return {
|
|
@@ -59,13 +59,15 @@ function calculateDelay(attempt, config, strategy = 'exponential') {
|
|
|
59
59
|
return Math.round(Math.max(0, delay));
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* Fibonacci helper for fibonacci backoff
|
|
62
|
+
* Fibonacci helper for fibonacci backoff — capped at n=40 to prevent
|
|
63
|
+
* excessively long loops when maxAttempts is set to a large value.
|
|
63
64
|
*/
|
|
64
65
|
function fibonacci(n) {
|
|
65
|
-
|
|
66
|
+
const capped = Math.min(n, 40);
|
|
67
|
+
if (capped <= 1)
|
|
66
68
|
return 1;
|
|
67
69
|
let a = 1, b = 1;
|
|
68
|
-
for (let i = 2; i <=
|
|
70
|
+
for (let i = 2; i <= capped; i++) {
|
|
69
71
|
[a, b] = [b, a + b];
|
|
70
72
|
}
|
|
71
73
|
return b;
|
|
@@ -99,8 +101,12 @@ function sleep(ms) {
|
|
|
99
101
|
*/
|
|
100
102
|
export async function withRetry(fn, config = {}, strategy = 'exponential') {
|
|
101
103
|
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
104
|
+
// Clamp maxAttempts and maxDelayMs to safe ranges to prevent runaway retry loops
|
|
105
|
+
finalConfig.maxAttempts = Math.min(Math.max(1, finalConfig.maxAttempts), 100);
|
|
106
|
+
finalConfig.maxDelayMs = Math.min(Math.max(0, finalConfig.maxDelayMs), 300_000); // 5 min ceiling
|
|
102
107
|
const startTime = Date.now();
|
|
103
108
|
const retryHistory = [];
|
|
109
|
+
const MAX_RETRY_HISTORY = 100;
|
|
104
110
|
let lastError;
|
|
105
111
|
let attempt = 0;
|
|
106
112
|
while (attempt < finalConfig.maxAttempts) {
|
|
@@ -129,12 +135,14 @@ export async function withRetry(fn, config = {}, strategy = 'exponential') {
|
|
|
129
135
|
}
|
|
130
136
|
// Calculate delay
|
|
131
137
|
const delay = calculateDelay(attempt, finalConfig, strategy);
|
|
132
|
-
// Record retry
|
|
133
|
-
retryHistory.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
// Record retry (cap history size and error message length to prevent OOM)
|
|
139
|
+
if (retryHistory.length < MAX_RETRY_HISTORY) {
|
|
140
|
+
retryHistory.push({
|
|
141
|
+
attempt,
|
|
142
|
+
error: lastError.message.slice(0, 512),
|
|
143
|
+
delayMs: delay,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
138
146
|
// Call retry callback
|
|
139
147
|
if (finalConfig.onRetry) {
|
|
140
148
|
finalConfig.onRetry(lastError, attempt, delay);
|
|
@@ -22,12 +22,16 @@ import { createSemanticRouting } from './embedder.js';
|
|
|
22
22
|
// don't land on stdout and corrupt the single JSON result line the parent
|
|
23
23
|
// parses. Must be set before the (lazy) transformers import in embedder.ts.
|
|
24
24
|
process.env.TRANSFORMERS_VERBOSITY ??= 'error';
|
|
25
|
+
/** Max task string length before it's silently truncated — prevents OOM in the embedding model. */
|
|
26
|
+
const MAX_TASK_LENGTH = 2_000;
|
|
25
27
|
async function main() {
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
28
|
+
const rawTask = process.argv[2];
|
|
29
|
+
if (!rawTask) {
|
|
28
30
|
process.stderr.write('embed-worker: missing task argument\n');
|
|
29
31
|
process.exit(2);
|
|
30
32
|
}
|
|
33
|
+
// Cap the task to prevent OOM when an oversized string is passed to the embedding model.
|
|
34
|
+
const task = rawTask.length > MAX_TASK_LENGTH ? rawTask.slice(0, MAX_TASK_LENGTH) : rawTask;
|
|
31
35
|
const { RouteLayer, ALL_ROUTES } = await import('@monomind/routing');
|
|
32
36
|
const semantic = await createSemanticRouting(ALL_ROUTES);
|
|
33
37
|
if (!semantic) {
|
|
Binary file
|
|
@@ -16,6 +16,8 @@ import { spawn, execSync } from 'child_process';
|
|
|
16
16
|
import { tmpdir } from 'os';
|
|
17
17
|
/** Default model for routing fallback — Haiku is fast and cheap for slug classification. */
|
|
18
18
|
const DEFAULT_ROUTING_MODEL = 'haiku';
|
|
19
|
+
/** Runtime-validated set of allowed model aliases. */
|
|
20
|
+
const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
19
21
|
/**
|
|
20
22
|
* Max time to wait for a single classification before giving up.
|
|
21
23
|
* `claude --print` is a full headless session (cold start ~10s, can spike),
|
|
@@ -23,6 +25,8 @@ const DEFAULT_ROUTING_MODEL = 'haiku';
|
|
|
23
25
|
* on timeout rather than blocking the caller.
|
|
24
26
|
*/
|
|
25
27
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
28
|
+
/** Hard upper cap on caller-supplied timeoutMs — prevents disabling the SIGTERM/SIGKILL guard. */
|
|
29
|
+
const MAX_TIMEOUT_MS = 120_000;
|
|
26
30
|
/** Cap captured output so a runaway child can't grow parent memory unbounded. */
|
|
27
31
|
const MAX_OUTPUT = 1024 * 1024; // 1 MB — a slug response is tiny
|
|
28
32
|
let claudeAvailable = null;
|
|
@@ -49,8 +53,15 @@ export function isClaudeCodeAvailable() {
|
|
|
49
53
|
export function createClaudeLLMCaller(options = {}) {
|
|
50
54
|
if (!isClaudeCodeAvailable())
|
|
51
55
|
return null;
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
// Runtime enum guard — TypeScript union is compile-time only; JS callers
|
|
57
|
+
// could otherwise inject an arbitrary string into the --model flag.
|
|
58
|
+
const rawModel = options.model ?? DEFAULT_ROUTING_MODEL;
|
|
59
|
+
const model = ALLOWED_MODELS.has(rawModel) ? rawModel : DEFAULT_ROUTING_MODEL;
|
|
60
|
+
// Cap caller-supplied timeout so a huge value can't disable the SIGTERM/SIGKILL guard.
|
|
61
|
+
const rawTimeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
62
|
+
const timeoutMs = Math.min(typeof rawTimeout === 'number' && Number.isFinite(rawTimeout) && rawTimeout > 0
|
|
63
|
+
? rawTimeout
|
|
64
|
+
: DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
54
65
|
const cwd = options.cwd ?? tmpdir();
|
|
55
66
|
return (prompt) => new Promise((resolve, reject) => {
|
|
56
67
|
const env = { ...process.env };
|
|
@@ -25,12 +25,20 @@ const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'embed-worker.
|
|
|
25
25
|
const WORKER_TIMEOUT_MS = 90_000;
|
|
26
26
|
/** Cap worker stdout so a runaway child can't grow parent memory unbounded. */
|
|
27
27
|
const MAX_WORKER_OUTPUT = 4 * 1024 * 1024; // 4 MB
|
|
28
|
+
/** Cap the task string length to prevent OOM/DoS via oversized argv. */
|
|
29
|
+
const MAX_TASK_LENGTH = 2_000;
|
|
30
|
+
/** Cap stderr reflected in error messages to prevent log-inflation attacks. */
|
|
31
|
+
const MAX_STDERR_IN_ERROR = 500;
|
|
32
|
+
/** Cap allScores array passed to the LLM fallback to prevent downstream OOM. */
|
|
33
|
+
const MAX_ALL_SCORES = 100;
|
|
28
34
|
/** Marker the worker prefixes its result line with (see embed-worker.ts). */
|
|
29
35
|
const RESULT_MARKER = '__ROUTE_RESULT__';
|
|
30
36
|
/** Run the embedding worker for one task. Resolves the RouteResult, or rejects. */
|
|
31
37
|
function runWorker(task) {
|
|
38
|
+
// Cap task length before passing as argv to prevent OOM/DoS via oversized args.
|
|
39
|
+
const safeTask = task.length > MAX_TASK_LENGTH ? task.slice(0, MAX_TASK_LENGTH) : task;
|
|
32
40
|
return new Promise((resolve, reject) => {
|
|
33
|
-
const child = spawn(process.execPath, [WORKER_PATH,
|
|
41
|
+
const child = spawn(process.execPath, [WORKER_PATH, safeTask], {
|
|
34
42
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
43
|
windowsHide: true,
|
|
36
44
|
});
|
|
@@ -75,7 +83,10 @@ function runWorker(task) {
|
|
|
75
83
|
settled = true;
|
|
76
84
|
clearTimeout(timer);
|
|
77
85
|
if (code !== 0) {
|
|
78
|
-
|
|
86
|
+
// Truncate stderr before embedding in Error to prevent log-inflation via
|
|
87
|
+
// a malicious or runaway child writing oversized diagnostic output.
|
|
88
|
+
const stderrSnippet = stderr.trim().slice(0, MAX_STDERR_IN_ERROR);
|
|
89
|
+
reject(new Error(stderrSnippet || `worker exited ${code}`));
|
|
79
90
|
return;
|
|
80
91
|
}
|
|
81
92
|
// Extract the marked result line — model-loader output may have written
|
|
@@ -130,8 +141,12 @@ export async function createConfiguredRouteLayer(opts = {}) {
|
|
|
130
141
|
}
|
|
131
142
|
// 3. Below threshold → Claude fallback in the parent, reusing scores.
|
|
132
143
|
if (llmCaller && Array.isArray(semantic.allScores) && semantic.allScores.length) {
|
|
144
|
+
// Cap allScores to prevent downstream OOM if the worker sends an oversized array.
|
|
145
|
+
const scores = semantic.allScores.length > MAX_ALL_SCORES
|
|
146
|
+
? semantic.allScores.slice(0, MAX_ALL_SCORES)
|
|
147
|
+
: semantic.allScores;
|
|
133
148
|
const fallback = new LLMFallbackRouter({ llmCaller, model: 'haiku' });
|
|
134
|
-
return fallback.classify(taskDescription, ALL_ROUTES,
|
|
149
|
+
return fallback.classify(taskDescription, ALL_ROUTES, scores);
|
|
135
150
|
}
|
|
136
151
|
if (!opts.debug)
|
|
137
152
|
delete semantic.allScores;
|
|
@@ -129,6 +129,7 @@ export declare class ClaimService extends EventEmitter {
|
|
|
129
129
|
private eventLog;
|
|
130
130
|
constructor(projectRoot: string, config?: Partial<ClaimServiceConfig>);
|
|
131
131
|
initialize(): Promise<void>;
|
|
132
|
+
private static readonly MAX_CLAIMS_FILE_BYTES;
|
|
132
133
|
private loadClaims;
|
|
133
134
|
private _saveQueue;
|
|
134
135
|
private saveClaims;
|
|
@@ -57,10 +57,18 @@ export class ClaimService extends EventEmitter {
|
|
|
57
57
|
// Load existing claims
|
|
58
58
|
await this.loadClaims();
|
|
59
59
|
}
|
|
60
|
+
// Hard cap on claims.json: a file larger than this is malformed or
|
|
61
|
+
// adversarially crafted (normal claim files are tiny).
|
|
62
|
+
static MAX_CLAIMS_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
60
63
|
async loadClaims() {
|
|
61
64
|
const claimsFile = path.join(this.storagePath, 'claims.json');
|
|
62
65
|
if (fs.existsSync(claimsFile)) {
|
|
63
66
|
try {
|
|
67
|
+
const fileSize = fs.statSync(claimsFile).size;
|
|
68
|
+
if (fileSize > ClaimService.MAX_CLAIMS_FILE_BYTES) {
|
|
69
|
+
// Oversized file — skip loading rather than OOM-ing the daemon
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
64
72
|
const data = JSON.parse(fs.readFileSync(claimsFile, 'utf-8'));
|
|
65
73
|
for (const claim of data.claims || []) {
|
|
66
74
|
claim.claimedAt = new Date(claim.claimedAt);
|