diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f92ef4b3..9b4ec31d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral - **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) - **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) - **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) +- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) ### 🐛 Bug Fixes diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index c5e09dbc1d..67e0c1e43e 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -25,6 +25,8 @@ import { SELF_ACCOUNT_QUOTA_SCOPE, SELF_USAGE_SCOPE } from "@/shared/constants/s import { extractApiErrorMessage } from "@/shared/http/apiErrorMessage"; import { hasProviderQuotaBypassScope } from "@/shared/constants/apiKeyPolicyScopes"; import { UsageLimitSettings } from "./components/UsageLimitSettings"; +import { ChaosModeAccessToggle } from "./components/ChaosModeAccessToggle"; +import { BypassProviderQuotaToggle } from "./components/BypassProviderQuotaToggle"; // Constants for validation const MAX_KEY_NAME_LENGTH = 200; @@ -125,6 +127,7 @@ interface ApiKey { streamDefaultMode?: StreamDefaultMode; disableNonPublicModels?: boolean; allowUsageCommand?: boolean; + chaosModeEnabled?: boolean; usageLimitEnabled?: boolean; dailyUsageLimitUsd?: number | null; weeklyUsageLimitUsd?: number | null; @@ -518,7 +521,8 @@ export default function ApiManagerPageClient() { const res = await fetch(`/api/keys/${encodeURIComponent(key.id)}/devices`); if (!res.ok) return [key.id, 0] as const; const data = await res.json(); - const count = typeof data?.count === "number" && Number.isFinite(data.count) ? data.count : 0; + const count = + typeof data?.count === "number" && Number.isFinite(data.count) ? data.count : 0; return [key.id, count] as const; } catch { return [key.id, 0] as const; @@ -791,7 +795,8 @@ export default function ApiManagerPageClient() { usageLimitEnabled: boolean, dailyUsageLimitUsd: number | null, weeklyUsageLimitUsd: number | null, - blockedModels: string[] + blockedModels: string[], + chaosModeEnabled: boolean ) => { if (!editingKey || !editingKey.id) return; @@ -862,6 +867,7 @@ export default function ApiManagerPageClient() { usageLimitEnabled, dailyUsageLimitUsd, weeklyUsageLimitUsd, + chaosModeEnabled, }), }); @@ -1645,7 +1651,8 @@ const PermissionsModal = memo(function PermissionsModal({ usageLimitEnabled: boolean, dailyUsageLimitUsd: number | null, weeklyUsageLimitUsd: number | null, - blockedModels: string[] + blockedModels: string[], + chaosModeEnabled: boolean ) => void; }) { const t = useTranslations("apiManager"); @@ -1731,6 +1738,7 @@ const PermissionsModal = memo(function PermissionsModal({ const [usageCommandEnabled, setUsageCommandEnabled] = useState( apiKey?.allowUsageCommand === true ); + const [chaosModeEnabled, setChaosModeEnabled] = useState(apiKey?.chaosModeEnabled === true); const [usageLimitEnabled, setUsageLimitEnabled] = useState(apiKey?.usageLimitEnabled === true); const [dailyUsageLimitUsd, setDailyUsageLimitUsd] = useState( typeof apiKey?.dailyUsageLimitUsd === "number" && apiKey.dailyUsageLimitUsd > 0 @@ -1935,7 +1943,8 @@ const PermissionsModal = memo(function PermissionsModal({ usageLimitEnabled, parseUsdLimitInput(dailyUsageLimitUsd), parseUsdLimitInput(weeklyUsageLimitUsd), - blockedModels + blockedModels, + chaosModeEnabled ); }, [ onSave, @@ -1974,6 +1983,7 @@ const PermissionsModal = memo(function PermissionsModal({ parseUsdLimitInput, blockedClaudeCodeFamilies, initialBlockedModels, + chaosModeEnabled, apiKey?.scopes, t, ]); @@ -2563,30 +2573,17 @@ const PermissionsModal = memo(function PermissionsModal({ /> + {/* Chaos Mode Access Toggle */} + setChaosModeEnabled((prev) => !prev)} + /> + {/* Advanced Provider Quota Policy Override */} -
-
-

Bypass provider quota cutoffs

-

- Allows this key to ignore upstream provider/account cutoff policy during routing. API - key USD quotas still apply. -

-
- -
+ setBypassProviderQuotaPolicyEnabled((prev) => !prev)} + /> {/* Disable Non-Public Models Toggle */}
diff --git a/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx b/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx new file mode 100644 index 0000000000..76ccbd25d4 --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/components/BypassProviderQuotaToggle.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +/** + * "Bypass provider quota cutoffs" toggle for the API Key permissions modal. + * Extracted out of ApiManagerPageClient.tsx (frozen god-file — see + * config/quality/file-size-baseline.json) following the same pattern as + * UsageLimitSettings.tsx — pure UI move, no behavior change. + */ +export function BypassProviderQuotaToggle({ + enabled, + onToggle, +}: { + enabled: boolean; + onToggle: () => void; +}) { + const tc = useTranslations("common"); + + return ( +
+
+

Bypass provider quota cutoffs

+

+ Allows this key to ignore upstream provider/account cutoff policy during routing. API key + USD quotas still apply. +

+
+ +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx b/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx new file mode 100644 index 0000000000..30cf2d228e --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/components/ChaosModeAccessToggle.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +/** + * Chaos Mode access toggle for the API Key permissions modal — gates a single + * API key's ability to call the Chaos Mode dispatch endpoints + * (`POST /api/chaos/run`, `POST /api/skills/collect/chaos`) via the + * `chaosModeEnabled` permission. Extracted out of ApiManagerPageClient.tsx + * (frozen god-file — see config/quality/file-size-baseline.json) following the + * same pattern as UsageLimitSettings.tsx. + */ +export function ChaosModeAccessToggle({ + enabled, + onToggle, +}: { + enabled: boolean; + onToggle: () => void; +}) { + const tChaos = useTranslations("chaosConfig"); + const tc = useTranslations("common"); + + return ( +
+
+

{tChaos("keyPermission")}

+

{tChaos("keyPermissionDesc")}

+
+ +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/chaos/ChaosConfigPageClient.tsx b/src/app/(dashboard)/dashboard/chaos/ChaosConfigPageClient.tsx new file mode 100644 index 0000000000..ef74806adc --- /dev/null +++ b/src/app/(dashboard)/dashboard/chaos/ChaosConfigPageClient.tsx @@ -0,0 +1,107 @@ +/** + * /dashboard/chaos — Chaos Mode Configuration Page + * + * Allows users to: + * - Enable/disable chaos mode globally + * - Set default mode (parallel/collaborative) + * - Override provider models for chaos mode + * - Set custom system prompt and max tokens + * - Configure timeout + * - Test chaos mode with a simple task + * + * State + handlers live in useChaosConfigPage.ts and the JSX sections are + * split into ./components/* — this file stays a thin composition/render + * function under the complexity/size ratchet + * (config/quality/complexity-baseline.json). + */ +"use client"; + +import { useChaosConfigPage } from "./useChaosConfigPage"; +import { ChaosModeSelector } from "./components/ChaosModeSelector"; +import { ChaosTestResultsPanel } from "./components/ChaosTestResultsPanel"; +import { ChaosProviderOverridesPanel } from "./components/ChaosProviderOverridesPanel"; +import { ChaosBasicSettingsFields } from "./components/ChaosBasicSettingsFields"; +import { ChaosConfigActionsBar } from "./components/ChaosConfigActionsBar"; +import { ChaosStatusMessage } from "./components/ChaosStatusMessage"; + +export default function ChaosConfigPage() { + const { + t, + config, + setConfig, + availableProviders, + loading, + saving, + testing, + testResult, + message, + saveConfig, + resetConfig, + testChaos, + addOverride, + updateOverride, + removeOverride, + } = useChaosConfigPage(); + + if (loading) { + return ( +
+
{t("loadingProviderModels")}
+
+ ); + } + + return ( +
+ {/* Header */} +
+

{t("pageTitle")}

+

{t("pageSubtitle")}

+
+ + {/* Status Message */} + + + {/* Enable toggle + timeout + max tokens + system prompt */} + setConfig((prev) => ({ ...prev, ...patch }))} + /> + + {/* Default Mode Selector */} + setConfig((prev) => ({ ...prev, defaultMode }))} + label={t("mode")} + parallelLabel={t("modeParallel")} + parallelDesc={t("modeParallelDesc")} + collaborativeLabel={t("modeCollaborative")} + collaborativeDesc={t("modeCollaborativeDesc")} + /> + + + + {/* Test Results */} + {testResult && } + + {/* Provider Overrides */} + +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/chaos/chaosPageTypes.ts b/src/app/(dashboard)/dashboard/chaos/chaosPageTypes.ts new file mode 100644 index 0000000000..66e6006ed3 --- /dev/null +++ b/src/app/(dashboard)/dashboard/chaos/chaosPageTypes.ts @@ -0,0 +1,28 @@ +import type { ChaosProviderOverride } from "./components/ChaosProviderOverridesPanel"; + +export interface ChaosProviderInfo { + id: string; + name: string; + provider: string; + defaultModel: string | null; +} + +export interface ChaosPageConfig { + enabled: boolean; + defaultMode: "parallel" | "collaborative"; + providerOverrides: ChaosProviderOverride[]; + systemPrompt?: string; + timeoutMs: number; + maxTokens: number; +} + +export const DEFAULT_CHAOS_PAGE_CONFIG: ChaosPageConfig = { + enabled: false, + defaultMode: "parallel", + providerOverrides: [], + systemPrompt: "", + timeoutMs: 120_000, + maxTokens: 4096, +}; + +export type ChaosPageMessage = { type: "success" | "error"; text: string } | null; diff --git a/src/app/(dashboard)/dashboard/chaos/components/ChaosBasicSettingsFields.tsx b/src/app/(dashboard)/dashboard/chaos/components/ChaosBasicSettingsFields.tsx new file mode 100644 index 0000000000..33f84d7ac8 --- /dev/null +++ b/src/app/(dashboard)/dashboard/chaos/components/ChaosBasicSettingsFields.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +export interface ChaosBasicSettings { + enabled: boolean; + timeoutMs: number; + maxTokens: number; + systemPrompt?: string; +} + +function ChaosSystemPromptField({ + value, + onChange, +}: { + value: string | undefined; + onChange: (value: string) => void; +}) { + const t = useTranslations("chaosConfig"); + return ( +
+

{t("systemPrompt")}

+

{t("systemPromptDesc")}

+