mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat(cowork): re-enable Claude Cowork with preset-only stdio MCP
Restore Cowork feature while closing the RCE vector: drop user-defined stdio commands so only hard-coded preset plugins (browsermcp) may spawn. Custom MCP now accepts remote URL only. Routes stay gated to localhost. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,9 +5,8 @@ import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import crypto from "crypto";
|
||||
import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
|
||||
import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
|
||||
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
||||
import { DATA_DIR } from "@/lib/dataDir";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
|
||||
const APP_PORT = UPDATER_CONFIG.appPort;
|
||||
@@ -180,17 +179,8 @@ const buildCustomEntries = (customPlugins) => {
|
||||
if (!Array.isArray(customPlugins)) return [];
|
||||
const out = [];
|
||||
for (const p of customPlugins) {
|
||||
if (!p?.name) continue;
|
||||
if (p.url) {
|
||||
out.push({ name: p.name, url: p.url, transport: p.transport || "sse", custom: true });
|
||||
} else if (p.command) {
|
||||
out.push({
|
||||
name: p.name,
|
||||
url: `http://localhost:${APP_PORT}/api/mcp/${encodeURIComponent(p.name)}/sse`,
|
||||
transport: "sse",
|
||||
custom: true,
|
||||
});
|
||||
}
|
||||
if (!p?.name || !p?.url) continue;
|
||||
out.push({ name: p.name, url: p.url, transport: p.transport || "sse", custom: true });
|
||||
}
|
||||
return out;
|
||||
};
|
||||
@@ -311,8 +301,6 @@ export async function GET() {
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
// Cowork disabled: spawns arbitrary processes (RCE risk).
|
||||
return NextResponse.json({ error: "Cowork is disabled" }, { status: 403 });
|
||||
try {
|
||||
const { baseUrl, apiKey, models, plugins, localPlugins, customPlugins } = await request.json();
|
||||
|
||||
@@ -327,29 +315,8 @@ export async function POST(request) {
|
||||
// Respect empty array (user toggled all off); fallback to defaults only when undefined.
|
||||
const pluginsArray = Array.isArray(plugins) ? plugins : DEFAULT_PLUGINS;
|
||||
const localPluginNames = Array.isArray(localPlugins) ? localPlugins : [];
|
||||
const customPluginsArray = Array.isArray(customPlugins) ? customPlugins : [];
|
||||
|
||||
// Register custom stdio plugins into bridge + persist for restart survival.
|
||||
if (customPluginsArray.length > 0) {
|
||||
const { registerCustomPlugin } = require("@/lib/mcp/stdioSseBridge");
|
||||
const stdioCustoms = customPluginsArray
|
||||
.filter((p) => p && typeof p.command === "string" && p.command.trim())
|
||||
.filter((p) => ALLOWED_MCP_COMMANDS.has(path.basename(p.command)))
|
||||
.map((p) => ({
|
||||
name: String(p.name || "").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64),
|
||||
command: p.command,
|
||||
args: Array.isArray(p.args) ? p.args.map(String) : [],
|
||||
}))
|
||||
.filter((p) => p.name);
|
||||
for (const p of stdioCustoms) {
|
||||
try { registerCustomPlugin(p); } catch { /* skip invalid */ }
|
||||
}
|
||||
try {
|
||||
const dir = path.join(DATA_DIR, "mcp");
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(path.join(dir, "customPlugins.json"), JSON.stringify(stdioCustoms, null, 2));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// Only URL-based custom plugins allowed (no stdio command spawning).
|
||||
const customPluginsArray = (Array.isArray(customPlugins) ? customPlugins : []).filter((p) => p?.url);
|
||||
|
||||
const bridgeEntries = await injectAuthHeaders(buildLocalBridgeEntries(localPluginNames));
|
||||
const customEntries = await injectAuthHeaders(buildCustomEntries(customPluginsArray));
|
||||
|
||||
Reference in New Issue
Block a user