Added new models for Claude Opus 4.8 and GPT 5.4 Mini.
Disabled Cowork
This commit is contained in:
@@ -144,6 +144,19 @@ export default function ToolDetailClient({ toolId, machineId }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Guard removed/unknown tools (e.g. disabled Cowork) to avoid crash on direct URL.
|
||||
if (!tool) {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-1 sm:px-0">
|
||||
<Link href="/dashboard/cli-tools" className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary w-fit">
|
||||
<span className="material-symbols-outlined text-[18px]">arrow_back</span>
|
||||
Back to CLI Tools
|
||||
</Link>
|
||||
<p className="text-sm text-text-muted">Tool not found or disabled.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-1 sm:px-0">
|
||||
<Link href="/dashboard/cli-tools" className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary w-fit">
|
||||
|
||||
@@ -540,62 +540,19 @@ export default function ProviderLimits() {
|
||||
const visibleConnections = await fetchConnections(page);
|
||||
setConnectionsLoading(false);
|
||||
|
||||
const cache = getQuotaCache();
|
||||
const nextLoading = {};
|
||||
const cachedQuotas = {};
|
||||
const connectionsToFetch = [];
|
||||
let latestCachedAt = null;
|
||||
// Always fetch fresh quota on mount, no cache display
|
||||
setLoading(buildLoadingState(visibleConnections));
|
||||
setErrors((prev) =>
|
||||
filterQuotaStateByConnections(prev, visibleConnections),
|
||||
);
|
||||
setQuotaData((prev) =>
|
||||
filterQuotaStateByConnections(prev, visibleConnections),
|
||||
);
|
||||
|
||||
visibleConnections.forEach((conn) => {
|
||||
const cachedEntry = cache[conn.id];
|
||||
if (cachedEntry) {
|
||||
nextLoading[conn.id] = false;
|
||||
cachedQuotas[conn.id] = {
|
||||
quotas: cachedEntry.quotas,
|
||||
plan: cachedEntry.plan,
|
||||
message: cachedEntry.message,
|
||||
raw: cachedEntry.raw,
|
||||
};
|
||||
if (cachedEntry.cachedAt) {
|
||||
const cachedTime = new Date(cachedEntry.cachedAt);
|
||||
if (!latestCachedAt || cachedTime > latestCachedAt) {
|
||||
latestCachedAt = cachedTime;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nextLoading[conn.id] = true;
|
||||
connectionsToFetch.push(conn);
|
||||
}
|
||||
});
|
||||
|
||||
setLoading(nextLoading);
|
||||
setErrors((prev) => {
|
||||
const nextErrors = filterQuotaStateByConnections(
|
||||
prev,
|
||||
visibleConnections,
|
||||
);
|
||||
visibleConnections.forEach((conn) => {
|
||||
if (cache[conn.id]) {
|
||||
nextErrors[conn.id] = null;
|
||||
}
|
||||
});
|
||||
return nextErrors;
|
||||
});
|
||||
setQuotaData((prev) => ({
|
||||
...filterQuotaStateByConnections(prev, visibleConnections),
|
||||
...cachedQuotas,
|
||||
}));
|
||||
|
||||
if (latestCachedAt) {
|
||||
setLastUpdated(latestCachedAt);
|
||||
}
|
||||
|
||||
if (connectionsToFetch.length > 0) {
|
||||
await Promise.all(
|
||||
connectionsToFetch.map((conn) => fetchQuota(conn.id, conn.provider)),
|
||||
);
|
||||
setLastUpdated(new Date());
|
||||
}
|
||||
await Promise.all(
|
||||
visibleConnections.map((conn) => fetchQuota(conn.id, conn.provider)),
|
||||
);
|
||||
setLastUpdated(new Date());
|
||||
};
|
||||
|
||||
initializeData();
|
||||
|
||||
@@ -311,6 +311,8 @@ 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();
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request, { params }) {
|
||||
// Cowork disabled: MCP stdio bridge spawns arbitrary processes (RCE risk).
|
||||
return NextResponse.json({ error: "Cowork is disabled" }, { status: 403 });
|
||||
const { plugin } = await params;
|
||||
if (!findPlugin(plugin)) {
|
||||
return NextResponse.json({ error: `Unknown plugin: ${plugin}` }, { status: 404 });
|
||||
|
||||
@@ -4,6 +4,8 @@ export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request, { params }) {
|
||||
// Cowork disabled: MCP stdio bridge spawns arbitrary processes (RCE risk).
|
||||
return new Response("Cowork is disabled", { status: 403 });
|
||||
const { plugin } = await params;
|
||||
if (!findPlugin(plugin)) {
|
||||
return new Response(`Unknown plugin: ${plugin}`, { status: 404 });
|
||||
|
||||
@@ -185,6 +185,7 @@ async function _ensureCloudflared() {
|
||||
|
||||
let cloudflaredProcess = null;
|
||||
let unexpectedExitHandler = null;
|
||||
let intentionalKill = false; // suppress unexpected-exit callback during deliberate kill
|
||||
|
||||
/** Register a callback to be called when cloudflared exits unexpectedly after connecting */
|
||||
export function setUnexpectedExitHandler(handler) {
|
||||
@@ -261,6 +262,7 @@ export async function spawnCloudflared(tunnelToken) {
|
||||
return;
|
||||
}
|
||||
// Watchdog (initializeApp) handles recovery — no auto-reconnect here
|
||||
if (intentionalKill) { intentionalKill = false; return; }
|
||||
if (wasConnected && unexpectedExitHandler) unexpectedExitHandler();
|
||||
});
|
||||
});
|
||||
@@ -387,6 +389,7 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (intentionalKill) { intentionalKill = false; cleanup(); return; }
|
||||
if (unexpectedExitHandler) unexpectedExitHandler();
|
||||
cleanup();
|
||||
});
|
||||
@@ -408,6 +411,7 @@ function killCloudflaredByPort(port) {
|
||||
}
|
||||
|
||||
export function killCloudflared(localPort) {
|
||||
intentionalKill = true;
|
||||
if (cloudflaredProcess) {
|
||||
try {
|
||||
cloudflaredProcess.kill();
|
||||
|
||||
@@ -117,14 +117,15 @@ export const CLI_TOOLS = {
|
||||
description: "OpenCode AI Terminal Assistant",
|
||||
configType: "custom",
|
||||
},
|
||||
cowork: {
|
||||
id: "cowork",
|
||||
name: "Claude Cowork",
|
||||
image: "/providers/claude.png",
|
||||
color: "#D97757",
|
||||
description: "Claude Desktop Cowork (third-party inference)",
|
||||
configType: "custom",
|
||||
},
|
||||
// Cowork disabled: spawns arbitrary processes (RCE risk). Hidden from CLI tools UI.
|
||||
// cowork: {
|
||||
// id: "cowork",
|
||||
// name: "Claude Cowork",
|
||||
// image: "/providers/claude.png",
|
||||
// color: "#D97757",
|
||||
// description: "Claude Desktop Cowork (third-party inference)",
|
||||
// configType: "custom",
|
||||
// },
|
||||
hermes: {
|
||||
id: "hermes",
|
||||
name: "Hermes Agent",
|
||||
|
||||
@@ -142,9 +142,14 @@ async function safeRestartTunnel(reason) {
|
||||
if (svc.cancelToken.cancelled) return;
|
||||
if (svc.spawnInProgress) return;
|
||||
|
||||
// Alive check FIRST: probe URLs to decide health (process up but tunnel 530 = dead)
|
||||
let alive = false;
|
||||
if (isCloudflaredRunning()) {
|
||||
const force = FORCE_RESTART_REASONS.test(reason);
|
||||
|
||||
// Watchdog: process alive = trust it (cloudflared self-retries via --retries 99).
|
||||
// Avoids killing a healthy tunnel on transient HTTP probe failures (app busy / slow DNS).
|
||||
if (!force && isCloudflaredRunning()) return;
|
||||
|
||||
// Force reasons (netchange/sleep/online): process may be up but routing stale → probe to confirm
|
||||
if (force && isCloudflaredRunning()) {
|
||||
const state = loadState();
|
||||
const publicUrl = state?.shortId ? `https://r${state.shortId}.abc-tunnel.us` : null;
|
||||
const directUrl = state?.tunnelUrl || null;
|
||||
@@ -153,14 +158,10 @@ async function safeRestartTunnel(reason) {
|
||||
probeCloudflareAlive(publicUrl),
|
||||
probeCloudflareAlive(directUrl),
|
||||
]);
|
||||
alive = publicOk && directOk;
|
||||
if (publicOk && directOk) return;
|
||||
}
|
||||
}
|
||||
if (alive) return;
|
||||
|
||||
// Degraded/dead → cooldown only prevents hammer loop after a recent restart attempt.
|
||||
// Bypass for network transitions (one-shot events) so user recovers fast after wifi change.
|
||||
const force = FORCE_RESTART_REASONS.test(reason);
|
||||
if (!force && Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) {
|
||||
console.log(`[Tunnel] degraded but cooldown active, skip (${reason})`);
|
||||
return;
|
||||
@@ -184,12 +185,8 @@ async function safeRestartTailscale(reason) {
|
||||
if (svc.cancelToken.cancelled) return;
|
||||
if (svc.spawnInProgress) return;
|
||||
|
||||
// Alive check FIRST: daemon up + URL responds = healthy
|
||||
let alive = false;
|
||||
if (isTailscaleRunning() && settings.tailscaleUrl) {
|
||||
alive = await probeTailscaleAlive(settings.tailscaleUrl);
|
||||
}
|
||||
if (alive) return;
|
||||
// Tailscale daemon is OS-level with built-in reconnect; trust it when running
|
||||
if (isTailscaleRunning()) return;
|
||||
|
||||
const force = FORCE_RESTART_REASONS.test(reason);
|
||||
if (!force && Date.now() - svc.lastRestartAt < RESTART_COOLDOWN_MS) {
|
||||
@@ -198,7 +195,7 @@ async function safeRestartTailscale(reason) {
|
||||
}
|
||||
if (!await checkInternet()) return;
|
||||
|
||||
console.log(`[Tailscale] safeRestart (${reason}) — tunnel unreachable${force ? " [force]" : ""}`);
|
||||
console.log(`[Tailscale] safeRestart (${reason}) — daemon not running${force ? " [force]" : ""}`);
|
||||
try {
|
||||
await enableTailscale();
|
||||
svc.lastRestartAt = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user