feat(chaos): add Chaos Mode — multi-model parallel/collaborative execution#6679
feat(chaos): add Chaos Mode — multi-model parallel/collaborative execution#6679Moseyuh333 wants to merge 11 commits into
Conversation
Integrates Skill Collector functionality into OmniRoute: Core Library (src/lib/skills/githubCollector.ts): - GitHub API search with 20+ query strategies - Heuristic scoring (0-1): stars, skill-file signals, keywords, topics - Security scanning for blocked patterns - Category inference & install path resolution MCP Tools (open-sse/mcp-server/tools/githubSkillTools.ts): - omniroute_github_skills_search - omniroute_github_skills_scan - omniroute_github_skills_install REST API (src/app/api/github-skills/): - GET /api/github-skills - search with filters - POST /api/github-skills - install planning Agent Skills Catalog: - Added omni-github-skills entry (API, area: github-skills) - Updated SkillCoverage: API 22→23
…, fix settings JSON parse - Add buildErrorBody import to github-skills API route (was undefined at runtime) - Add 34 unit tests for githubCollector: scoreRepo, scanText, inferCategory, resolveInstallPath, QUERY_STRATEGIES, INSTALL_TARGETS — all passing - Fix settings.ts JSON.parse to handle non-JSON string values gracefully
…se guard + subsystem wiring - MCP registration passes toolDef.scopes so the 3 github_skills tools gate on read:skills / write:skills under scope enforcement (was denied as tool_definition_missing). - API GET error path routes through sanitizeErrorMessage + returns 500 (Hard Rule diegosouzapw#12). - Realign agent-skills catalog/routes/generator/mcp count guards for the new omni-github-skills catalog entry (43→44 total, 22→23 api). Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…detection - Add GET /api/skills/collect/detect — detect installed coding CLIs using OmniRoute's CLI_TOOL_IDS + getCliRuntimeStatus, search GitHub for matching agent skills, auto-match to installed tools. - Add POST /api/skills/collect/install — plan skill installation into detected coding tools (Codex, Claude, Copilot, OpenCode, Hermes...) - Add skills/cli-skill-collector/SKILL.md — 4-step agent workflow (detect → review → install → verify) - Add docker-compose.drive-d.yml — isolated Docker compose for D: drive with port 30129 and data under ./data - Add start.sh — convenience launcher (PORT=30129, C: drive port 30128 untouched) - Add Codex installer support to existing installer system - Update skills/README.md with cli-skill-collector entry (CLI Skills: 21)
- detect: optional chain for nullable repo fields - detect: distribute top skills evenly across unmatched tools - install: remove unused imports (resolveInstallPath, INSTALL_TARGETS) - install: add 9 missing tool paths (cursor, copilot, cline...) - install: fix ~ expansion on Windows (USERPROFILE priority) - install: validate non-empty strings in targets array - SKILL.md: port 30128 → 30129 everywhere - start.sh: non-blocking C drive check with timeout
…ution - New DB column chaos_mode_enabled on api_keys table - API key create/PATCH routes support chaosModeEnabled toggle - Core library src/lib/chaos/chaosConfig.ts for persistent config - API routes: GET/PUT/DELETE /api/chaos/config - Chaos execution POST /api/skills/collect/chaos with key auth - Dashboard page at /dashboard/chaos with full config UI - Sidebar entry in Agentic Features section - Chaos mode toggle in API Key editor permissions panel - i18n keys for chaos config (en.json)
There was a problem hiding this comment.
Code Review
This pull request introduces 'Chaos Mode' for parallel or collaborative multi-model execution, integrates a built-in GitHub Skill Discovery tool, adds the Tencent Yuanbao and Requesty providers, updates Doubao Web to Dola Global, and implements a session-stickiness opt-out toggle. It also resolves several bugs, including Next.js standalone WebSocket daemon startup, pre-spawn port probes, and macOS certificate fingerprint checks. The reviewer identified several critical issues: an authorization bypass vulnerability in classify.ts that exposes /api/v1/ routes publicly; a missing Authorization header in the internal fetch for Chaos Mode; a discontinuous stars bonus calculation; missing array validation for targets in the GitHub skills API; a potential null pointer crash in ComboDefaultsTab.tsx; and an unprocessed trailing stream buffer in yuanbao-web.ts. Additionally, style guide violations were noted regarding the incorrect placement of sync-next-cycle.mjs and start.sh.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function isClassifiedAsPublic(path: string, method: string): boolean { | ||
| const isV1ApiPrefix = (p: string) => | ||
| p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/"); | ||
| const filtered = PUBLIC_API_ROUTE_PREFIXES.filter((p) => p !== "/api/v1/"); | ||
| if (filtered.some((prefix) => path.startsWith(prefix)) && !isV1ApiPrefix(path)) { | ||
| return true; | ||
| } | ||
| return matchesReadonlyPublic(path, method); | ||
| return isPublicApiRoute(path, method); | ||
| } |
There was a problem hiding this comment.
Security Issue: Authorization Bypass for /api/v1/ Routes
By replacing the original isClassifiedAsPublic logic with a direct call to isPublicApiRoute(path, method), /api/v1/ routes (such as /api/v1/chat/completions) are now classified as PUBLIC for authorization purposes.
While /api/v1/ is bypassed from the global requireLogin middleware (so API clients can connect without a browser session), it must not be classified as PUBLIC in the authorization classifier, as it still requires API key validation. Classifying it as PUBLIC here bypasses crucial security and role-based access checks.
Please restore the explicit exclusion of /api/v1/ routes from being classified as public.
| function isClassifiedAsPublic(path: string, method: string): boolean { | |
| const isV1ApiPrefix = (p: string) => | |
| p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/"); | |
| const filtered = PUBLIC_API_ROUTE_PREFIXES.filter((p) => p !== "/api/v1/"); | |
| if (filtered.some((prefix) => path.startsWith(prefix)) && !isV1ApiPrefix(path)) { | |
| return true; | |
| } | |
| return matchesReadonlyPublic(path, method); | |
| return isPublicApiRoute(path, method); | |
| } | |
| function isClassifiedAsPublic(path: string, method: string): boolean { | |
| const isV1ApiPrefix = (p: string) => | |
| p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/"); | |
| if (isV1ApiPrefix(path)) { | |
| return false; | |
| } | |
| return isPublicApiRoute(path, method); | |
| } |
| async function dispatchToModel( | ||
| providerId: string, | ||
| providerName: string, | ||
| modelId: string, | ||
| messages: { role: string; content: string }[], | ||
| signal?: AbortSignal | ||
| ): Promise<ModelResult> { | ||
| const start = performance.now(); | ||
| try { | ||
| // Use provider:model format — OmniRoute's routing resolves this to | ||
| // the provider connection's actual model endpoint. | ||
| const model = modelId || providerId; | ||
|
|
||
| const body = { | ||
| model, | ||
| messages, | ||
| stream: false, | ||
| max_tokens: 4096, | ||
| }; | ||
|
|
||
| const res = await fetch(`${OMNIROUTE_BASE}/v1/chat/completions`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(body), | ||
| signal: signal ?? AbortSignal.timeout(120_000), | ||
| }); |
There was a problem hiding this comment.
Bug: Missing Authorization Header in Internal Fetch
The internal fetch request to ${OMNIROUTE_BASE}/v1/chat/completions does not pass any Authorization header.
On any secured OmniRoute instance (which is standard in production or when REQUIRE_API_KEY is enabled), this request will fail with 401 Unauthorized, completely breaking Chaos Mode.
Please update dispatchToModel to accept the client's bearerToken and forward it in the request headers.
async function dispatchToModel(
providerId: string,
providerName: string,
modelId: string,
messages: { role: string; content: string }[],
bearerToken: string,
signal?: AbortSignal
): Promise<ModelResult> {
const start = performance.now();
try {
const model = modelId || providerId;
const body = {
model,
messages,
stream: false,
max_tokens: 4096,
};
const res = await fetch(`${OMNIROUTE_BASE}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${bearerToken}`
},
body: JSON.stringify(body),
signal: signal ?? AbortSignal.timeout(120_000),
});| // Stars bonus (logarithmic, capped for awesome lists) | ||
| if (isAwesome) stars = Math.min(stars, 1000); | ||
| if (stars > 20000) bonus = Math.min(0.7, stars / 30000); | ||
| else if (stars > 5000) bonus = stars / 15000; | ||
| else if (stars > 1000) bonus = stars / 10000; | ||
| else if (stars > 300) bonus = stars / 8000; | ||
| else if (stars > 100) bonus = stars / 12000; | ||
| else if (stars > 50) bonus = stars / 15000; |
There was a problem hiding this comment.
Bug: Discontinuous and Non-Monotonic Stars Bonus Calculation
The piecewise stars bonus calculation has severe discontinuities at the boundaries:
- A repository with 20,000 stars gets a bonus of
20000 / 15000 = 1.33. - A repository with 20,001 stars gets a bonus of
Math.min(0.7, 20001 / 30000) = 0.66. - A repository with 5,000 stars gets a bonus of
5000 / 10000 = 0.5. - A repository with 5,001 stars gets a bonus of
5001 / 15000 = 0.33.
This means repositories with slightly fewer stars can receive a significantly higher score.
Please replace this piecewise logic with a continuous, monotonic logarithmic formula.
| // Stars bonus (logarithmic, capped for awesome lists) | |
| if (isAwesome) stars = Math.min(stars, 1000); | |
| if (stars > 20000) bonus = Math.min(0.7, stars / 30000); | |
| else if (stars > 5000) bonus = stars / 15000; | |
| else if (stars > 1000) bonus = stars / 10000; | |
| else if (stars > 300) bonus = stars / 8000; | |
| else if (stars > 100) bonus = stars / 12000; | |
| else if (stars > 50) bonus = stars / 15000; | |
| if (stars > 50) { | |
| bonus = Math.min(0.7, Math.log10(stars / 50) / 4); | |
| } |
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const body = await request.json(); | ||
| const { | ||
| repoName, | ||
| targets = ["hermes"], | ||
| description = "", | ||
| } = body as { | ||
| repoName?: string; | ||
| targets?: string[]; | ||
| description?: string; | ||
| }; | ||
|
|
||
| if (!repoName || typeof repoName !== "string") { | ||
| return NextResponse.json(buildErrorBody(400, "repoName is required"), { status: 400 }); | ||
| } | ||
|
|
There was a problem hiding this comment.
Bug: Missing Validation for targets Array
The POST handler destructures targets from the request body and directly calls targets.map(...) without validating that targets is actually an array.
If a client sends a non-array value (e.g., a string or number), the server will crash with a TypeError (resulting in a 500 Internal Server Error) instead of returning a clean 400 Bad Request.
Please add a validation check to ensure targets is an array.
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
repoName,
targets = ["hermes"],
description = "",
} = body as {
repoName?: string;
targets?: string[];
description?: string;
};
if (!repoName || typeof repoName !== "string") {
return NextResponse.json(buildErrorBody(400, "repoName is required"), { status: 400 });
}
if (!Array.isArray(targets)) {
return NextResponse.json(buildErrorBody(400, "targets must be an array"), { status: 400 });
}
const { resolveInstallPath, INSTALL_TARGETS } = await import("@/lib/skills/githubCollector");| disableSessionStickiness: | ||
| settingsData.disableSessionStickiness ?? | ||
| comboData.comboDefaults?.disableSessionStickiness ?? | ||
| prev.disableSessionStickiness, |
There was a problem hiding this comment.
Bug: Potential TypeError on Null settingsData
If settingsRes.ok is false, settingsData is set to null.
However, settingsData.disableSessionStickiness is accessed directly without optional chaining, which will throw a TypeError: Cannot read properties of null (reading 'disableSessionStickiness') and crash the component.
Please use optional chaining to safely access disableSessionStickiness.
| disableSessionStickiness: | |
| settingsData.disableSessionStickiness ?? | |
| comboData.comboDefaults?.disableSessionStickiness ?? | |
| prev.disableSessionStickiness, | |
| disableSessionStickiness: | |
| settingsData?.disableSessionStickiness ?? | |
| comboData.comboDefaults?.disableSessionStickiness ?? | |
| prev.disableSessionStickiness, |
| @@ -0,0 +1,195 @@ | |||
| #!/usr/bin/env node | |||
There was a problem hiding this comment.
Style Guide Violation: Incorrect Script Placement
According to the Repository Style Guide (Section 1: File Placement & Organization), all maintenance, debugging, generation, or experimental scripts must be placed strictly inside one of the following scripts/ subfolders:
build/dev/check/docs/i18n/ad-hoc/
Creating a new scripts/release/ subfolder and placing sync-next-cycle.mjs inside it violates this rule. Please move this script to scripts/ad-hoc/ or another allowed subfolder.
References
- All scripts must be placed strictly inside allowed scripts/ subfolders (build/, dev/, check/, docs/, i18n/, ad-hoc/). (link)
| @@ -0,0 +1,42 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
Style Guide Violation: Loose Script in Project Root
According to the Repository Style Guide (Section 1: File Placement & Organization), loose scripts must never be dumped in the project root (/). The project root must only contain specific allowed configuration, dependency, documentation, and CI/CD files.
Please move start.sh to an allowed location, such as scripts/dev/ or scripts/ad-hoc/.
References
- Loose scripts must never be dumped in the project root. (link)
| async function collectYuanbaoResponse( | ||
| upstream: ReadableStream<Uint8Array>, | ||
| signal: AbortSignal | null | undefined | ||
| ): Promise<{ content: string; reasoning: string }> { | ||
| const decoder = new TextDecoder(); | ||
| const reader = upstream.getReader(); | ||
| let buffer = ""; | ||
| let content = ""; | ||
| let reasoning = ""; | ||
|
|
||
| try { | ||
| while (true) { | ||
| if (signal?.aborted) break; | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| buffer += decoder.decode(value, { stream: true }); | ||
| const lines = buffer.split("\n"); | ||
| buffer = lines.pop() ?? ""; | ||
| for (const line of lines) { | ||
| const event = parseYuanbaoDataLine(line); | ||
| if (!event) continue; | ||
| if (event.type === "think" && event.content) reasoning += event.content; | ||
| else if (event.type === "text" && typeof event.msg === "string") content += event.msg; | ||
| } | ||
| } | ||
| } finally { | ||
| reader.releaseLock(); | ||
| } | ||
|
|
||
| return { content, reasoning }; | ||
| } |
There was a problem hiding this comment.
Bug: Unprocessed Trailing Stream Buffer
In collectYuanbaoResponse, the stream reader splits chunks by \n and pops the last line into buffer to handle partial lines.
However, when the stream ends (done is true), the loop breaks immediately, and any remaining text left in buffer (which represents the final line of the stream if it didn't end with a trailing newline) is completely ignored and never processed.
Please process the remaining buffer after the read loop completes.
async function collectYuanbaoResponse(
upstream: ReadableStream<Uint8Array>,
signal: AbortSignal | null | undefined
): Promise<{ content: string; reasoning: string }> {
const decoder = new TextDecoder();
const reader = upstream.getReader();
let buffer = "";
let content = "";
let reasoning = "";
try {
while (true) {
if (signal?.aborted) break;
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const event = parseYuanbaoDataLine(line);
if (!event) continue;
if (event.type === "think" && event.content) reasoning += event.content;
else if (event.type === "text" && typeof event.msg === "string") content += event.msg;
}
}
if (buffer) {
const event = parseYuanbaoDataLine(buffer);
if (event) {
if (event.type === "think" && event.content) reasoning += event.content;
else if (event.type === "text" && typeof event.msg === "string") content += event.msg;
}
}
} finally {
reader.releaseLock();
}=== Changes === 1. NEW: src/lib/chaos/chaosExecutor.ts — shared execution engine - Removed ~150 lines of duplicate dispatch logic between two API routes - Single executeChaosRun() function used by both endpoints - Added concurrency limit (max 10 parallel requests) - Added proper TypeScript interfaces (ChaosRunInput, ChaosRunResult) - Added error logging throughout 2. FIX: src/app/api/skills/collect/chaos/route.ts - Was MISSING logger import (log.error was undefined at runtime) - Reduced from 388 lines → 142 lines by delegating to shared executor - Added maxTokens support in schema validation 3. REFACTOR: src/app/api/chaos/run/route.ts - Simplified to thin wrapper: auth + validate + delegate to executor - Added maxTokens support 4. ENHANCE: src/lib/chaos/chaosConfig.ts - Added maxTokens config field (256-128k, default 4096) - Persisted per-instance via settings table 5. ENHANCE: UI — ChaosConfigPageClient.tsx - Loads available providers from /api/models for dropdown autocomplete - Added datalist-based provider selector in overrides section - Added Max Tokens configuration input - Added expandable provider list showing all detected providers - Fixed duplicate override detection
…ack to include global config
Chaos Mode
Allow IDE or any OmniRoute API client to initiate multiple models simultaneously.
Each provider contributes one model — models run in parallel or collaborative mode
to complete a single task faster and more effectively.
Changes
chaos_mode_enabledonapi_keystablechaosModeEnabledtogglesrc/lib/chaos/chaosConfig.ts— get/set/reset chaos config (persisted in settings table)GET/PUT/DELETE /api/chaos/config— global chaos mode settingsPOST /api/skills/collect/chaos— requires Bearer token withchaosModeEnabled=true/dashboard/chaos— full config UI (enable/disable, mode switch, timeout, provider overrides, test button)How to use