Merge remote-tracking branch 'origin/release/v3.8.47' into tmp/implement-prs-6697-b

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-09 19:58:00 -03:00
32 changed files with 1354 additions and 156 deletions

View File

@@ -15,6 +15,7 @@ import { searchGitHubSkills } from "@/lib/skills/githubCollector";
import { matchesSearch } from "@/shared/utils/turkishText";
import { validateBody } from "@/shared/validation/helpers";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
const installSkillSchema = z.object({
repoName: z.string().min(1),
@@ -25,6 +26,9 @@ const installSkillSchema = z.object({
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const { searchParams } = new URL(request.url);
const minStars = parseInt(searchParams.get("minStars") ?? "1", 10);
@@ -64,6 +68,9 @@ export async function GET(request: NextRequest) {
}
export async function POST(request: NextRequest) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const parsed = validateBody(installSkillSchema, await request.json());
if (!parsed.success) {

View File

@@ -3,6 +3,7 @@ import { getAntigravityHeaders } from "@omniroute/open-sse/services/antigravityH
import { parseGeminiModelsList } from "@/lib/providerModels/geminiModelsParser";
import { filterClinepassModels } from "@omniroute/open-sse/services/clinepassModels.ts";
import { normalizeOpenAiLikeModelsResponse } from "./normalizers";
import { extractKimiJwt } from "@/lib/providers/webCookieAuth";
export type ProviderModelsConfigEntry = {
url: string;
@@ -12,6 +13,7 @@ export type ProviderModelsConfigEntry = {
authPrefix?: string;
authQuery?: string;
body?: unknown;
buildHeaders?: (token: string) => Record<string, string>;
parseResponse: (data: any) => any;
};
@@ -60,10 +62,10 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
},
// #3931: qwen-web (cookie provider) was missing here, so its discovery page
// showed nothing (the OAuth fallback above only fires for provider==="qwen").
// `chat.qwen.ai/api/v2/models` is public (no auth header configured/sent);
// `chat.qwen.ai/api/v2/models/` is public (no auth header configured/sent);
// shape `{ data: { data: [{ id, name, owned_by }] } }`, flatter `{ data: [] }` fallback.
"qwen-web": {
url: "https://chat.qwen.ai/api/v2/models",
url: "https://chat.qwen.ai/api/v2/models/",
method: "GET",
headers: { "Content-Type": "application/json" },
parseResponse: (data) => {
@@ -78,18 +80,34 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
},
},
// #5858 follow-up: kimi-web (cookie provider) on the international domain.
// `GetAvailableModels` returns the model list as a plain JSON envelope
// (no Connect framing on either request or response — only the chat
// completion endpoint uses the 5-byte envelope). Auth: Bearer JWT extracted
// from the `kimi-auth` cookie the user pasted. Agent variants
// `GetAvailableModels` returns the model list as a plain JSON envelope.
// Auth mirrors the web app: Bearer JWT plus `Cookie: kimi-auth=<JWT>`.
// Agent variants
// (`k2d6-agent*`) need a different scenario + agent fields this executor
// doesn't shape, so they're filtered out.
"kimi-web": {
url: "https://www.kimi.com/apiv2/kimi.gateway.config.v1.ConfigService/GetAvailableModels",
method: "GET",
headers: { accept: "application/json, text/plain, */*", "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
method: "POST",
headers: { accept: "*/*", "Content-Type": "application/json" },
body: {},
buildHeaders: (token) => {
const jwt = extractKimiJwt(token);
return {
accept: "*/*",
"Content-Type": "application/json",
"connect-protocol-version": "1",
Origin: "https://www.kimi.com",
Referer: "https://www.kimi.com/",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
...(jwt
? {
Authorization: `Bearer ${jwt}`,
Cookie: `kimi-auth=${jwt}`,
}
: {}),
};
},
parseResponse: (data) => {
const list = (data?.availableModels || []) as Array<{
key?: string;

View File

@@ -1,4 +1,5 @@
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
import type { ProviderModelsConfigEntry } from "./discovery/providerModelsConfig";
/**
* Derive a models-discovery config from the provider's registry `modelsUrl`
@@ -8,18 +9,9 @@ import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts
* OpenAI-compatible `/v1/models` endpoint, or `undefined` when the
* registry entry has no `modelsUrl`.
*/
export function deriveConfigFromRegistryModelsUrl(provider: string):
| {
url: string;
method: "GET";
headers: Record<string, string>;
authHeader?: string;
authPrefix?: string;
authQuery?: string;
body?: unknown;
parseResponse: (data: any) => any;
}
| undefined {
export function deriveConfigFromRegistryModelsUrl(
provider: string
): ProviderModelsConfigEntry | undefined {
const entry = getRegistryEntry(provider);
if (typeof entry?.modelsUrl === "string" && entry.modelsUrl.length > 0) {
return {

View File

@@ -1806,8 +1806,8 @@ export async function GET(
}
// Build headers
const headers = { ...config.headers };
if (config.authHeader && !config.authQuery) {
const headers = config.buildHeaders ? config.buildHeaders(token) : { ...config.headers };
if (!config.buildHeaders && config.authHeader && !config.authQuery) {
headers[config.authHeader] = (config.authPrefix || "") + token;
}

View File

@@ -16,7 +16,8 @@ import {
// guard work unchanged. Only the deployment surface differs (Cloudflare Workers
// API instead of Vercel /v13/deployments).
const CLOUDFLARE_API_BASE = process.env.CLOUDFLARE_API_BASE || "https://api.cloudflare.com/client/v4";
const CLOUDFLARE_API_BASE =
process.env.CLOUDFLARE_API_BASE || "https://api.cloudflare.com/client/v4";
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
@@ -52,7 +53,7 @@ export async function POST(request: Request) {
try {
// 1. PUT the Worker script — Cloudflare requires multipart/form-data with
// main_module + a metadata blob describing the upload.
// body_part + a metadata blob describing the upload.
//
// Built as a raw Buffer with an explicit boundary rather than a native
// `FormData` (#6416): in production `globalThis.fetch` is patched with
@@ -63,14 +64,19 @@ export async function POST(request: Request) {
// with `Content-Type: text/plain;charset=UTF-8`, which Cloudflare
// rejects with "Content-Type must be one of: application/javascript,
// text/javascript, multipart/form-data" — the same class of bug fixed
// for image edits in #3273. ES-module semantics come from `main_module`
// in the metadata part below, not the script part's Content-Type
// (Cloudflare rejects "application/javascript+module" outright, #5128).
// for image edits in #3273.
//
// The script part itself must stay `application/javascript` (Cloudflare
// rejects `application/javascript+module`, #5128), but with that MIME the
// uploaded body is parsed as a Service Worker, not an ES module. So the
// metadata must point at the script via `body_part`, not `main_module` —
// otherwise Cloudflare rejects the body with `Unexpected token 'export'`
// when it sees module syntax in a non-module upload (#6496 / #6416).
const workerScriptUrl = `${CLOUDFLARE_API_BASE}/accounts/${accountId}/workers/scripts/${projectName}`;
const { headers: uploadHeaders, body: uploadBody } = buildCloudflareWorkerUploadRequest(
workerScript,
{
main_module: "index.js",
body_part: "index.js",
compatibility_date: "2026-03-20",
observability: { enabled: true },
}

View File

@@ -0,0 +1,164 @@
/**
* GET /api/skills/collect/detect
*
* Detect installed CLI coding tools + search GitHub for matching agent skills.
* Uses OmniRoute's built-in CLI_TOOL_IDS detection (no Skill Collector bridge needed).
*
* Returns: {
* tools: { toolId, installed, runnable, command, reason }[],
* matchedSkills: { toolId, skillName, repo, score, stars }[],
* totalSkills: number
* }
*/
import { NextRequest, NextResponse } from "next/server";
import { getCliRuntimeStatus, CLI_TOOL_IDS } from "@/shared/services/cliRuntime";
import { searchGitHubSkills, type GitHubSkillRepo } from "@/lib/skills/githubCollector";
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
export const dynamic = "force-dynamic";
const CODING_TOOL_KEYWORDS: Record<string, string[]> = {
claude: ["claude", "anthropic", "claude-code"],
codex: ["codex", "openai", "gpt"],
cursor: ["cursor", "cursor-ai"],
copilot: ["copilot", "github-copilot"],
opencode: ["opencode"],
cline: ["cline"],
kilocode: ["kilo", "kilocode"],
hermes: ["hermes", "nous-research"],
"hermes-agent": ["hermes", "hermes-agent"],
openclaw: ["openclaw"],
droid: ["droid", "factory-ai"],
continue: ["continue"],
antigravity: ["antigravity"],
qwen: ["qwen", "alibaba"],
windsurf: ["windsurf"],
devin: ["devin", "cognition"],
};
interface DetectedTool {
installed: boolean;
runnable: boolean;
command: string | null;
reason: string | null;
}
interface MatchedSkill {
toolId: string;
toolName: string;
skillName: string;
repo: string;
htmlUrl: string;
score: number;
stars: number;
description: string;
}
/** Probes every catalog CLI tool in parallel via getCliRuntimeStatus(). */
async function detectInstalledTools(): Promise<Record<string, DetectedTool>> {
const toolIds = CLI_TOOL_IDS as readonly string[];
const detectedTools: Record<string, DetectedTool> = {};
await Promise.allSettled(
toolIds.map(async (toolId) => {
try {
const result = await getCliRuntimeStatus(toolId);
detectedTools[toolId] = {
installed: result.installed,
runnable: result.runnable,
command: result.command ?? null,
reason: result.reason ?? null,
};
} catch {
detectedTools[toolId] = {
installed: false,
runnable: false,
command: null,
reason: "check_failed",
};
}
})
);
return detectedTools;
}
function toMatchedSkill(toolId: string, repo: GitHubSkillRepo): MatchedSkill {
return {
toolId,
toolName: toolId,
skillName: repo.fullName?.split("/").pop() ?? "unknown",
repo: repo.fullName ?? "",
htmlUrl: repo.htmlUrl ?? "",
score: repo.score ?? 0,
stars: repo.stars ?? 0,
description: (repo.description ?? "").slice(0, 200),
};
}
/** For each repo, matches it to the first installed tool whose keywords hit. */
function matchSkillsToTools(repos: GitHubSkillRepo[], installedTools: string[]): MatchedSkill[] {
const matchedSkills: MatchedSkill[] = [];
for (const repo of repos) {
const name = (repo.fullName ?? "").toLowerCase();
const desc = (repo.description ?? "").toLowerCase();
const matchedTool = installedTools.find((toolId) => {
const keywords = CODING_TOOL_KEYWORDS[toolId] ?? [toolId];
return keywords.some((kw) => name.includes(kw) || desc.includes(kw));
});
if (matchedTool) matchedSkills.push(toMatchedSkill(matchedTool, repo));
}
return matchedSkills;
}
/** Fills in tools with zero keyword matches by distributing top-scored skills evenly. */
function distributeUnmatchedSkills(
repos: GitHubSkillRepo[],
matchedSkills: MatchedSkill[],
installedTools: string[]
): MatchedSkill[] {
const toolsWithoutMatches = installedTools.filter(
(id) => !matchedSkills.some((m) => m.toolId === id)
);
if (toolsWithoutMatches.length === 0 || repos.length === 0) return matchedSkills;
const topSkills = repos.filter((r) => (r.score ?? 0) >= 0.4).slice(0, Math.min(10, repos.length));
const distributed = topSkills.map((r, i) =>
toMatchedSkill(toolsWithoutMatches[i % toolsWithoutMatches.length], r)
);
return [...matchedSkills, ...distributed];
}
export async function GET(request: NextRequest) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const detectedTools = await detectInstalledTools();
const installedTools = Object.entries(detectedTools)
.filter(([, v]) => v.installed)
.map(([id]) => id);
const { repos, errors } = await searchGitHubSkills({ minStars: 1, maxResults: 100 });
const directMatches = matchSkillsToTools(repos, installedTools);
const matchedSkills = distributeUnmatchedSkills(repos, directMatches, installedTools);
return NextResponse.json({
tools: detectedTools,
installedToolIds: installedTools,
matchedSkills: matchedSkills.slice(0, 50),
totalSkills: repos.length,
totalMatched: matchedSkills.length,
searchErrors: (errors?.length ?? 0) > 0 ? errors : undefined,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json(buildErrorBody(500, msg), { status: 500 });
}
}

View File

@@ -0,0 +1,130 @@
/**
* POST /api/skills/collect/install
*
* Install a discovered GitHub skill to detected CLI tools.
* Uses OmniRoute's skill registry + CLI tool paths (no Skill Collector bridge).
*
* Body: {
* repoName: string, // GitHub full name (e.g. "user/repo")
* targets: string[], // Tool IDs to install to (e.g. ["codex", "claude"])
* description?: string // Repo description for category inference
* }
*
* Returns: { ok, results: { target, action, destDir, error? }[] }
*/
import { NextResponse } from "next/server";
import { z } from "zod";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { sanitizeErrorMessage, buildErrorBody } from "@omniroute/open-sse/utils/error";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
const installSchema = z.object({
repoName: z.string().min(1, "repoName is required"),
targets: z
.array(z.string().min(1, "target toolId must be non-empty"))
.min(1, "at least one target required")
.max(10, "max 10 targets"),
description: z.string().default(""),
});
const CODING_TOOL_PATHS: Record<string, string> = {
claude: "~/.claude/skills/{category}",
codex: "~/.codex/skills/{category}",
hermes: "~/AppData/Local/hermes/skills/{category}",
opencode: "~/.opencode/skills/{category}",
gemini: "~/.gemini/skills/{category}",
cursor: "~/.cursor/skills/{category}",
copilot: "~/.copilot/skills/{category}",
cline: "~/.cline/skills/{category}",
windsurf: "~/.windsurf/skills/{category}",
devin: "~/.devin/skills/{category}",
antigravity: "~/.antigravity/skills/{category}",
qwen: "~/.qwen/skills/{category}",
kilocode: "~/.kilocode/skills/{category}",
openclaw: "~/.openclaw/skills/{category}",
droid: "~/.droid/skills/{category}",
continue: "~/.continue/skills/{category}",
};
function inferCategory(skillName: string, description: string): string {
const text = `${skillName} ${description}`.toLowerCase();
const mapping: Record<string, string[]> = {
security: ["security", "pentest", "exploit", "malware", "forensics", "vulnerability"],
"data-science": ["data", "analytics", "pandas", "ml", "model", "train"],
devops: ["deploy", "docker", "k8s", "terraform", "ci/cd", "pipeline"],
creative: ["design", "image", "video", "art", "music"],
productivity: ["email", "doc", "slide", "report", "calendar"],
research: ["paper", "arxiv", "academic", "literature"],
"software-development": ["code", "refactor", "test", "lint", "review", "debug"],
media: ["youtube", "transcript", "gif", "video", "audio"],
};
for (const [cat, keywords] of Object.entries(mapping)) {
if (keywords.some((k) => text.includes(k))) return cat;
}
return "imported-github";
}
function expandHome(dir: string): string {
// Home dir resolution: Windows (USERPROFILE) → Unix fallback (HOME)
const home =
typeof process !== "undefined" ? process.env.USERPROFILE || process.env.HOME || "" : "";
return dir.replace(/^~/, home);
}
function resolveDestDir(target: string, skillName: string, description: string): string {
const template = CODING_TOOL_PATHS[target];
if (!template) {
throw new Error(
`Unknown target tool: "${target}". Supported: ${Object.keys(CODING_TOOL_PATHS).join(", ")}`
);
}
const category = inferCategory(skillName, description);
const resolved = template.replace("{category}", category).replace("{name}", skillName);
return expandHome(`${resolved}/${skillName}`);
}
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const rawBody = await request.json();
const validation = validateBody(installSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(buildErrorBody(400, validation.error.message), { status: 400 });
}
const { repoName, targets, description } = validation.data;
const skillName = repoName.split("/").pop() || repoName;
const results = targets.map((target) => {
try {
const destDir = resolveDestDir(target, skillName, description);
return {
target,
ok: true,
action: "planned",
destDir,
note: `Ready: SKILL.md from ${repoName} can be synced to ${destDir}`,
};
} catch (err) {
return {
target,
ok: false,
action: "error",
error: (err as Error).message,
};
}
});
return NextResponse.json({
ok: results.every((r) => r.ok),
repoName,
skillName,
results,
});
} catch (err) {
const msg = sanitizeErrorMessage(err);
return NextResponse.json(buildErrorBody(500, msg), { status: 500 });
}
}

View File

@@ -144,7 +144,7 @@ export async function validateDeepSeekWebProvider({ apiKey }: any) {
}
// qwen-web has no `modelsUrl` in its registry entry, so the generic OpenAI-compatible
// validator used to derive a probe URL of `https://chat.qwen.ai/api/v2/models` (via
// validator used to derive a probe URL of `https://chat.qwen.ai/api/v2/models/` (via
// addModelsSuffix) — a non-existent path that answers with a 307 redirect, which the
// outbound guard blocked and the route then mislabeled as an SSRF block (#3288/#3758).
//

View File

@@ -13,7 +13,12 @@
* - Strips Host + relay control headers before forwarding upstream.
*
* The string template is fed to Cloudflare's PUT /accounts/{id}/workers/scripts/{name}
* API with main_module=index.js (ESM Workers Modules format).
* API as a Service Worker (no ES module export). Cloudflare's multipart upload
* API rejects `application/javascript+module` (#5128C) and treats a plain
* `application/javascript` script part as a Service Worker regardless of any
* `main_module` metadata — `main_module` requires the script to be an actual
* ES module (top-level `export`), which Service Worker syntax is not. The
* `body_part` metadata field is the correct way to point at a non-ESM script.
*
* The OmniRoute variant intentionally diverges from the upstream PR:
* - The upstream worker had NO auth check, leaving the deployed workers.dev URL
@@ -105,51 +110,53 @@ function isPrivateHostname(h) {
return false;
}
export default {
async fetch(request, env, ctx) {
const auth = request.headers.get("x-relay-auth");
if (auth !== "${relayAuth}") {
return new Response("Unauthorized", { status: 401 });
}
const target = request.headers.get("x-relay-target");
if (!target) {
return new Response("missing x-relay-target", { status: 400 });
}
let targetUrl;
try { targetUrl = new URL(target); } catch { return new Response("invalid x-relay-target", { status: 400 }); }
if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
return new Response("forbidden x-relay-target protocol", { status: 403 });
}
if (targetUrl.username || targetUrl.password) {
return new Response("forbidden x-relay-target (embedded credentials)", { status: 403 });
}
if (isPrivateHostname(targetUrl.hostname)) {
return new Response("forbidden x-relay-target (private/loopback host)", { status: 403 });
}
const relayPath = request.headers.get("x-relay-path") || "/";
const headers = new Headers(request.headers);
["x-relay-target", "x-relay-path", "x-relay-auth", "host"].forEach((h) => headers.delete(h));
const init = {
method: request.method,
headers,
};
if (request.method !== "GET" && request.method !== "HEAD") {
init.body = request.body;
init.duplex = "half";
}
try {
const upstream = await fetch(target.replace(/\\/$/, "") + relayPath, init);
return new Response(upstream.body, {
status: upstream.status,
headers: upstream.headers,
});
} catch (error) {
return new Response(JSON.stringify({ error: error && error.message ? error.message : "relay error" }), {
status: 502,
headers: { "content-type": "application/json" },
});
}
},
};
async function handleRelay(request) {
const auth = request.headers.get("x-relay-auth");
if (auth !== "${relayAuth}") {
return new Response("Unauthorized", { status: 401 });
}
const target = request.headers.get("x-relay-target");
if (!target) {
return new Response("missing x-relay-target", { status: 400 });
}
let targetUrl;
try { targetUrl = new URL(target); } catch { return new Response("invalid x-relay-target", { status: 400 }); }
if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
return new Response("forbidden x-relay-target protocol", { status: 403 });
}
if (targetUrl.username || targetUrl.password) {
return new Response("forbidden x-relay-target (embedded credentials)", { status: 403 });
}
if (isPrivateHostname(targetUrl.hostname)) {
return new Response("forbidden x-relay-target (private/loopback host)", { status: 403 });
}
const relayPath = request.headers.get("x-relay-path") || "/";
const headers = new Headers(request.headers);
["x-relay-target", "x-relay-path", "x-relay-auth", "host"].forEach((h) => headers.delete(h));
const init = {
method: request.method,
headers,
};
if (request.method !== "GET" && request.method !== "HEAD") {
init.body = request.body;
init.duplex = "half";
}
try {
const upstream = await fetch(target.replace(/\\\\/$/, "") + relayPath, init);
return new Response(upstream.body, {
status: upstream.status,
headers: upstream.headers,
});
} catch (error) {
return new Response(JSON.stringify({ error: error && error.message ? error.message : "relay error" }), {
status: 502,
headers: { "content-type": "application/json" },
});
}
}
addEventListener("fetch", (event) => {
event.respondWith(handleRelay(event.request));
});
`;
}

View File

@@ -37,7 +37,7 @@ export interface ScanFinding {
export interface SkillInstallResult {
target: string;
ok: boolean;
action: "installed" | "already_up_to_date" | "skipped" | "error";
action: "installed" | "planned" | "already_up_to_date" | "skipped" | "error";
error?: string;
destDir?: string;
}

View File

@@ -43,6 +43,7 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
"/api/headroom/start", // Headroom token-saver proxy lifecycle: spawns headroom-ai python CLI (Hard Rules #15 + #17)
"/api/headroom/stop", // Headroom token-saver proxy lifecycle: sends SIGTERM/SIGKILL to managed PID (Hard Rules #15 + #17)
"/api/oauth/cursor/auto-import", // spawns `execFile("which", ["cursor"])` to verify a local Cursor install before importing creds — RCE-via-tunnel surface (Hard Rules #15 + #17, found by 6A.8 route-guard gate). Specific path only: the rest of /api/oauth/ (browser redirect/callback flows) must stay remote-reachable.
"/api/skills/collect/", // Skill Collector CLI detection: GET .../detect probes getCliRuntimeStatus() per CLI_TOOL_IDS entry, which spawns a child process to check each tool — RCE-via-tunnel surface (Hard Rules #15 + #17, PR #6294 review).
"/api/discovery/", // Discovery tool (opt-in provider scanner): the scan route makes outbound probes to provider endpoints (SSRF-adjacent) and the whole surface is an admin research tool — strict-loopback only, no manage-scope bypass (NOT in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES). See _tasks/features-v3.8.42/gaps/DISCOVERY_TOOL_DESIGN.md.
];

View File

@@ -30,6 +30,7 @@ export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray<string> = [
"/api/tools/traffic-inspector/", // http-proxy listener + system proxy (Hard Rules #15 + #17)
"/api/plugins/", // plugins: load/execute via worker_threads + child_process (Hard Rules #15 + #17)
"/api/local/", // T-12: 1-click local service launchers (Redis today) — must never be whitelistable via manage-scope bypass (Hard Rules #15 + #17)
"/api/skills/collect/", // Skill Collector CLI detection: GET .../detect spawns a child process per CLI_TOOL_IDS entry — must never be whitelistable via manage-scope bypass (Hard Rules #15 + #17, PR #6294 review)
"/api/headroom/start", // spawns headroom-ai python CLI — must never be bypassable (Hard Rules #15 + #17)
"/api/headroom/stop", // kills tracked PID — must never be bypassable (Hard Rules #15 + #17)
];