From 462bca68b203366ed1b4ffa8884aae94135f672b Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 28 Jun 2026 15:26:45 -0300 Subject: [PATCH] =?UTF-8?q?feat(compression):=20risk-gate=20pre-pass=20?= =?UTF-8?q?=E2=80=94=20shield=20sensitive=20spans=20from=20lossy=20compres?= =?UTF-8?q?sion=20(#5243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Risk-gate pre-pass — shields sensitive spans (PEM/secret/stack/k8s/migration/legal) from lossy compression via SENTINEL preserveSpans. Default off, fail-open, ReDoS-bounded patterns. strategySelector baseline rebaselined for the wrapper extraction. --- config/quality/file-size-baseline.json | 3 +- open-sse/services/compression/preservation.ts | Bin 6610 -> 7665 bytes .../services/compression/riskGate/index.ts | 3 + .../services/compression/riskGate/riskGate.ts | 136 ++++++++++++++++++ .../compression/riskGate/riskGateStep.ts | 115 +++++++++++++++ .../compression/riskGate/riskPatterns.ts | 61 ++++++++ .../compression/riskGate/strategyWrap.ts | 44 ++++++ .../services/compression/strategySelector.ts | 45 ++++++ open-sse/services/compression/types.ts | 5 + .../dashboard/compression/studio/PlayView.tsx | 6 + .../compression/studio/PlaygroundInput.tsx | 8 +- .../compression/studio/RiskGateBadge.tsx | 15 ++ .../studio/compressionFlowModel.ts | 8 ++ src/app/api/compression/preview/route.ts | 20 ++- src/hooks/usePreviewCompression.ts | 12 +- tests/unit/compression/riskGateDetect.test.ts | 105 ++++++++++++++ .../compression/riskGateIntegration.test.ts | 78 ++++++++++ tests/unit/compression/riskGateStep.test.ts | 61 ++++++++ tests/unit/ui/riskGateBadge.test.tsx | 42 ++++++ 19 files changed, 758 insertions(+), 9 deletions(-) create mode 100644 open-sse/services/compression/riskGate/index.ts create mode 100644 open-sse/services/compression/riskGate/riskGate.ts create mode 100644 open-sse/services/compression/riskGate/riskGateStep.ts create mode 100644 open-sse/services/compression/riskGate/riskPatterns.ts create mode 100644 open-sse/services/compression/riskGate/strategyWrap.ts create mode 100644 src/app/(dashboard)/dashboard/compression/studio/RiskGateBadge.tsx create mode 100644 tests/unit/compression/riskGateDetect.test.ts create mode 100644 tests/unit/compression/riskGateIntegration.test.ts create mode 100644 tests/unit/compression/riskGateStep.test.ts create mode 100644 tests/unit/ui/riskGateBadge.test.tsx diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index b6a8503d39..f6eae773d2 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -161,7 +161,8 @@ "_rebaseline_2026_06_24_task_aware_routing": "Task-aware routing strategy (port PR #2045, OmniRoute #4945): combo.ts 3190->3225 (+35) = one new `else if (strategy === \"task-aware\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC 854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).", - "open-sse/services/compression/strategySelector.ts": 854, + "_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (%hVKyGZBt`ip&tJ(WH_nb5H>&2hZw>QVzLATpR7ayH0K(~cfsS404kdqp| zkjA4kUBTwLry!tAu(&$E{BUu3{uYInDb20U$oW1Tm0p=ygbQg{l+%jmhdWJw=nlvbDB52iCt?#sf zK(}EC9~`6iIj-Cp;+1o9b++b$bX+&(VoGiVX7Lc*s2J6w*i71Dh;@ax``QwT%F%gR zp(UEkEX)Oa8A@lGraq?HGSMMEea2?@wI2K9O|-0;`f`oAS&^!>^4kJdlX0~-): Hit[] { + const hits: Hit[] = []; + for (const { category, regex } of RISK_PATTERNS) { + if (!enabled.has(category)) continue; + regex.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = regex.exec(text)) !== null) { + if (m[0].length === 0) { + regex.lastIndex++; + continue; + } + hits.push({ start: m.index, end: m.index + m[0].length, category }); + } + } + return hits; +} + +/** True when the hit's line starts with a diff marker (`+`/`-`). */ +function inDiffHunk(text: string, start: number): boolean { + const lineStart = text.lastIndexOf("\n", start - 1) + 1; + return DIFF_HUNK_LINE.test(text.slice(lineStart, lineStart + 1)); +} + +/** Structural k8s Secret detector: a YAML doc carrying `kind: Secret` + `data:`. */ +function detectK8sSecret(text: string): Hit[] { + const hits: Hit[] = []; + const kindRe = /^kind:[ \t]*Secret\b/gm; + let m: RegExpExecArray | null; + while ((m = kindRe.exec(text)) !== null) { + // Document boundaries: nearest `---`/start before, nearest `---`/end after. + const prevSep = text.lastIndexOf("\n---", m.index); + const docStart = prevSep === -1 ? 0 : prevSep + 1; + const nextSep = text.indexOf("\n---", m.index); + const docEnd = nextSep === -1 ? text.length : nextSep + 1; + const doc = text.slice(docStart, docEnd); + if (/^\s*(?:data|stringData):/m.test(doc)) { + hits.push({ start: docStart, end: docEnd, category: "k8s_secret" }); + } + } + return hits; +} + +function mergeSpans(spans: RiskSpan[]): RiskSpan[] { + if (spans.length <= 1) return spans; + const sorted = [...spans].sort((a, b) => a.start - b.start); + const out: RiskSpan[] = [sorted[0]]; + for (let i = 1; i < sorted.length; i++) { + const last = out[out.length - 1]; + const cur = sorted[i]; + if (cur.start <= last.end) { + last.end = Math.max(last.end, cur.end); + } else { + out.push(cur); + } + } + return out; +} + +/** + * Detect spans that should be shielded from compression. Pure and fail-open: + * any internal error yields an empty result (never throws). + */ +export function detectRiskSpans(text: string, cfg: RiskGateConfig): RiskSpan[] { + try { + if (!cfg.enabled) return []; + if (!text) return []; + const enabled = new Set( + cfg.categories?.length + ? cfg.categories + : (["stack_trace", "private_key", "secret_assignment", "k8s_secret", "db_migration", "legal"] as RiskCategory[]) + ); + const vcs = isLikelyVcsContext(text); + + const regexHits = collectRegexHits(text, enabled); + const k8sHits = enabled.has("k8s_secret") ? detectK8sSecret(text) : []; + + // db_migration: require >=MIN_DDL hits; drop those inside a diff hunk. + const ddl = regexHits.filter((h) => h.category === "db_migration" && !(vcs && inDiffHunk(text, h.start))); + const ddlPromoted: RiskSpan[] = + ddl.length >= MIN_DDL ? [{ start: ddl[0].start, end: ddl[ddl.length - 1].end, category: "db_migration" }] : []; + + // Guarded categories: secret_assignment, stack_trace, legal. + const guarded = regexHits.filter( + (h) => h.category === "secret_assignment" || h.category === "stack_trace" || h.category === "legal" + ); + // private_key is the only regex-hit self-evident category; k8s_secret and + // db_migration self-promote via their own structural paths below. + const selfEvident = regexHits.filter((h) => h.category === "private_key"); + + // Count corroborating signals (self-evident + promoted-ddl + k8s + guarded). + const signalCount = selfEvident.length + (ddlPromoted.length ? 1 : 0) + k8sHits.length + guarded.length; + const shortSection = !vcs && text.length < SHORT_SECTION; + const guardedPromoted = signalCount >= 2 || shortSection ? guarded : []; + + const promoted: RiskSpan[] = [ + ...selfEvident.map((h) => ({ start: h.start, end: h.end, category: h.category })), + ...k8sHits.map((h) => ({ start: h.start, end: h.end, category: h.category })), + ...ddlPromoted, + ...guardedPromoted.map((h) => ({ start: h.start, end: h.end, category: h.category })), + ]; + + return mergeSpans(promoted); + } catch { + return []; + } +} diff --git a/open-sse/services/compression/riskGate/riskGateStep.ts b/open-sse/services/compression/riskGate/riskGateStep.ts new file mode 100644 index 0000000000..88954337e9 --- /dev/null +++ b/open-sse/services/compression/riskGate/riskGateStep.ts @@ -0,0 +1,115 @@ +import { preserveSpans, restorePreservedBlocks, type PreservedBlock } from "../preservation.ts"; +import { detectRiskSpans, type RiskGateConfig } from "./riskGate.ts"; +import type { RiskCategory } from "./riskPatterns.ts"; + +export interface RiskGateStats { + spansProtected: number; + categories: Partial>; +} + +export interface RiskMaskResult { + maskedBody: Record; + blocks: PreservedBlock[]; + stats: RiskGateStats; +} + +interface TextPart { + type?: string; + text?: string; +} + +/** Mask one content string; returns masked text + blocks + per-category counts. */ +function maskString( + text: string, + cfg: RiskGateConfig, + tally: Partial> +): { masked: string; blocks: PreservedBlock[] } { + const spans = detectRiskSpans(text, cfg); + if (!spans.length) return { masked: text, blocks: [] }; + for (const s of spans) tally[s.category] = (tally[s.category] ?? 0) + 1; + const { text: masked, blocks } = preserveSpans( + text, + spans.map((s) => ({ start: s.start, end: s.end, kind: `risk_${s.category}` })) + ); + return { masked, blocks }; +} + +/** + * Mask risky spans in every message content (string or `{type:"text"}` parts). + * Pure: clones touched messages, leaves the original body unmutated. Fail-open. + */ +export function applyRiskMask(body: Record, cfg: RiskGateConfig): RiskMaskResult { + const tally: Partial> = {}; + const allBlocks: PreservedBlock[] = []; + const messages = body.messages; + if (!Array.isArray(messages)) { + return { maskedBody: body, blocks: [], stats: { spansProtected: 0, categories: {} } }; + } + + let changed = false; + const maskedMessages = messages.map((msg) => { + const m = msg as { role?: unknown; content?: unknown }; + if (typeof m.content === "string") { + const { masked, blocks } = maskString(m.content, cfg, tally); + if (!blocks.length) return msg; + changed = true; + allBlocks.push(...blocks); + return { ...m, content: masked }; + } + if (Array.isArray(m.content)) { + let partChanged = false; + const parts = (m.content as TextPart[]).map((p) => { + if (p && p.type === "text" && typeof p.text === "string") { + const { masked, blocks } = maskString(p.text, cfg, tally); + if (!blocks.length) return p; + partChanged = true; + allBlocks.push(...blocks); + return { ...p, text: masked }; + } + return p; + }); + if (!partChanged) return msg; + changed = true; + return { ...m, content: parts }; + } + return msg; + }); + + const maskedBody = changed ? { ...body, messages: maskedMessages } : body; + return { + maskedBody, + blocks: allBlocks, + stats: { spansProtected: allBlocks.length, categories: tally }, + }; +} + +/** Restore every masked span in the (possibly compressed) body. Fail-open. */ +export function restoreRiskBlocks( + body: Record, + blocks: PreservedBlock[] +): Record { + if (!blocks.length) return body; + const messages = body.messages; + if (!Array.isArray(messages)) return body; + const restoreParts = (content: unknown): unknown => { + if (typeof content === "string") return restorePreservedBlocks(content, blocks); + if (Array.isArray(content)) { + return (content as TextPart[]).map((p) => + p && p.type === "text" && typeof p.text === "string" + ? { ...p, text: restorePreservedBlocks(p.text, blocks) } + : p + ); + } + return content; + }; + return { + ...body, + messages: messages.map((msg) => { + const m = msg as { content?: unknown }; + if (typeof m.content === "string" || Array.isArray(m.content)) { + return { ...m, content: restoreParts(m.content) }; + } + return msg; + }), + }; +} diff --git a/open-sse/services/compression/riskGate/riskPatterns.ts b/open-sse/services/compression/riskGate/riskPatterns.ts new file mode 100644 index 0000000000..759ef34b0e --- /dev/null +++ b/open-sse/services/compression/riskGate/riskPatterns.ts @@ -0,0 +1,61 @@ +/** + * Risk-gate pattern catalog. Every variable-length pattern uses bounded + * quantifiers (`{0,N}`) to prevent catastrophic backtracking (ReDoS) on + * untrusted input. Patterns are ours (not agent-supplied), so safe-regex is not + * required — boundedness is verified by an adversarial-input test. + */ +export type RiskCategory = + | "stack_trace" + | "private_key" + | "secret_assignment" + | "k8s_secret" + | "db_migration" + | "legal"; + +export const MAX_PEM_LEN = 4096; + +export interface RiskPattern { + category: RiskCategory; + regex: RegExp; +} + +/** + * Categories whose single match is strong enough evidence on its own (no + * corroborating second signal / short-section required). + * `k8s_secret` and `db_migration` are promoted structurally in riskGate.ts. + */ +export const SELF_EVIDENT: ReadonlySet = new Set([ + "private_key", + "k8s_secret", + "db_migration", +]); + +export const RISK_PATTERNS: RiskPattern[] = [ + { + category: "private_key", + regex: new RegExp( + `-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----[\\s\\S]{1,${MAX_PEM_LEN}}?-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----`, + "g" + ), + }, + { + category: "secret_assignment", + regex: + /\b(?:api[_-]?key|secret|token|password|passwd|bearer|authorization|client[_-]?secret)\b[ \t]{0,20}[:=][ \t]{0,20}["']?[A-Za-z0-9._\-+/]{8,200}/gi, + }, + { + category: "stack_trace", + regex: + /^\s{0,8}(?:at\s+\S.{0,300}|File ".{1,300}", line \d{1,9}|Traceback \(most recent call last\):|[A-Za-z_.]{1,80}(?:Error|Exception):.{0,300})$/gm, + }, + { + // Single DDL hit; the ">=2 DDL" promotion rule is enforced in riskGate.ts. + category: "db_migration", + regex: /\b(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX|SCHEMA|DATABASE|COLUMN)\b/gi, + }, + { + category: "legal", + regex: + /\bWITHOUT WARRANTY\b|\bPermission is hereby granted\b|^SPDX-License-Identifier:.{0,200}$|\bCopyright \(c\)\b|\bAll rights reserved\b/gim, + }, +]; diff --git a/open-sse/services/compression/riskGate/strategyWrap.ts b/open-sse/services/compression/riskGate/strategyWrap.ts new file mode 100644 index 0000000000..8ad6d9fb95 --- /dev/null +++ b/open-sse/services/compression/riskGate/strategyWrap.ts @@ -0,0 +1,44 @@ +import type { CompressionResult } from "../types.ts"; +import type { CompressionConfig } from "../types.ts"; +import { applyRiskMask, restoreRiskBlocks } from "./riskGateStep.ts"; +import type { RiskGateConfig } from "./riskGate.ts"; + +/** Resolve the effective risk-gate config (explicit option wins over config); enabled-gated. */ +export function resolveRiskGate(options?: { + riskGate?: RiskGateConfig; + config?: CompressionConfig; +}): RiskGateConfig | undefined { + const rg = options?.riskGate ?? options?.config?.riskGate; + return rg?.enabled ? rg : undefined; +} + +function attach( + result: CompressionResult, + mask: ReturnType +): CompressionResult { + if (mask.blocks.length) result.body = restoreRiskBlocks(result.body, mask.blocks); + if (result.stats) result.stats.riskGate = mask.stats; + return result; +} + +/** Outer mask→run→restore wrapper for a sync compression entry point. Byte-identical when gate absent. */ +export function withRiskGate( + body: Record, + riskGate: RiskGateConfig | undefined, + run: (b: Record) => CompressionResult +): CompressionResult { + if (!riskGate) return run(body); + const mask = applyRiskMask(body, riskGate); + return attach(run(mask.maskedBody), mask); +} + +/** Async variant of withRiskGate. */ +export async function withRiskGateAsync( + body: Record, + riskGate: RiskGateConfig | undefined, + run: (b: Record) => Promise +): Promise { + if (!riskGate) return run(body); + const mask = applyRiskMask(body, riskGate); + return attach(await run(mask.maskedBody), mask); +} diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index 03dbcb2227..d47a8d57ac 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -33,6 +33,12 @@ import { } from "./planResolution.ts"; import { resolveAdaptivePlan } from "./adaptiveCompression/resolveAdaptivePlan.ts"; import type { AdaptiveTelemetry } from "./adaptiveCompression/types.ts"; +import type { RiskGateConfig } from "./riskGate/riskGate.ts"; +import { + resolveRiskGate, + withRiskGate, + withRiskGateAsync, +} from "./riskGate/strategyWrap.ts"; // Re-export so existing importers (resolver test + chatCore dynamic import) keep resolving. export { planFromHeader, formatCompressionMeta, buildNamedComboLookup }; @@ -260,6 +266,23 @@ export function applyCompression( * skipped instead of silently dropping the target. Flows through to applyStackedCompression. */ bailout?: BailoutConfig; + /** Risk-gate mask/restore wrapper (opt-in, default off). Read via resolveRiskGate. */ + riskGate?: RiskGateConfig; + } +): CompressionResult { + return withRiskGate(body, resolveRiskGate(options), (b) => runCompression(b, mode, options)); +} + +function runCompression( + body: Record, + mode: CompressionMode, + options?: { + model?: string; + supportsVision?: boolean | null; + config?: CompressionConfig; + principalId?: string; + bailout?: BailoutConfig; + riskGate?: RiskGateConfig; } ): CompressionResult { if (mode === "off") { @@ -556,6 +579,8 @@ interface StackOptions { bailout?: BailoutConfig; /** Opt-in per-step fidelity gate (default disabled). */ fidelityGate?: FidelityGateConfig; + /** Risk-gate mask/restore wrapper (opt-in, default off). Read via resolveRiskGate. */ + riskGate?: RiskGateConfig; /** Authenticated principal id — threaded through to CCR engine for store scoping. */ principalId?: string; /** F3.3: called once per engine as it completes (live per-engine streaming). */ @@ -713,6 +738,16 @@ export function applyStackedCompression( body: Record, pipeline?: Array, options?: StackOptions +): CompressionResult { + return withRiskGate(body, resolveRiskGate(options), (b) => + runStackedCompression(b, pipeline, options) + ); +} + +function runStackedCompression( + body: Record, + pipeline?: Array, + options?: StackOptions ): CompressionResult { const steps = resolveStackSteps(pipeline); registerBuiltinCompressionEngines(); @@ -786,6 +821,16 @@ export async function applyStackedCompressionAsync( body: Record, pipeline?: Array, options?: StackOptions +): Promise { + return withRiskGateAsync(body, resolveRiskGate(options), (b) => + runStackedCompressionAsync(b, pipeline, options) + ); +} + +async function runStackedCompressionAsync( + body: Record, + pipeline?: Array, + options?: StackOptions ): Promise { const steps = resolveStackSteps(pipeline); registerBuiltinCompressionEngines(); diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 0dd2c73cbd..acd20b4617 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -12,6 +12,8 @@ import { ENGINE_IDS } from "./engineCatalog.ts"; import type { ContextBudgetConfig } from "./adaptiveCompression/types.ts"; import type { FidelityGateConfig } from "./fidelityGate.ts"; +import type { RiskGateConfig } from "./riskGate/riskGate.ts"; +import type { RiskGateStats } from "./riskGate/riskGateStep.ts"; // Re-export so consumers that already import from this module (e.g. src/lib/db/compression.ts) // can get ENGINE_IDS without a second bare `@omniroute/open-sse/...engineCatalog.ts` specifier. @@ -144,6 +146,8 @@ export interface CompressionConfig { stackedPipeline?: CompressionPipelineStep[]; /** Opt-in per-step fidelity gate (default disabled). */ fidelityGate?: FidelityGateConfig; + /** Opt-in risk-gate pre-pass: shields sensitive spans from compression (default disabled). */ + riskGate?: RiskGateConfig; cavemanConfig?: CavemanConfig; cavemanOutputMode?: CavemanOutputModeConfig; /** Phase 4A: selected output styles (supersedes cavemanOutputMode via a back-compat shim). */ @@ -200,6 +204,7 @@ export interface CompressionStats { validationWarnings?: string[]; validationErrors?: string[]; fallbackApplied?: boolean; + riskGate?: RiskGateStats; /** * Phase 4 (B): which `ultra` tier actually ran for this request. * "slm" — Tier-B ran and produced the output. diff --git a/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx b/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx index 47dfd23fc5..a0f38ba002 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/PlayView.tsx @@ -5,6 +5,7 @@ import { WaterfallInspector } from "./WaterfallInspector"; import { DiffPane } from "./DiffPane"; import { EncoderComparisonTable } from "./EncoderComparisonTable"; import { PlaygroundInput, LANE_ENGINES } from "./PlaygroundInput"; +import { RiskGateBadge } from "./RiskGateBadge"; export interface PlayViewProps { text: string; onText: (t: string) => void; @@ -45,6 +46,7 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP const [fuzzyDedup, setFuzzyDedup] = useState(false); const [selectedLane, setSelectedLane] = useState(null); const [fidelityGate, setFidelityGate] = useState(false); + const [riskGate, setRiskGate] = useState(false); const { batch, loading, run } = usePreviewCompression(); const messages = [{ role: "user", content: text }]; const toggle = (e: string) => @@ -56,6 +58,7 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP activeEngines: orderByStack(active, laneEngines), fidelityGate, fuzzyDedup, + riskGate, }); const activeDiff = resolveActiveDiff(batch, selectedLane); return ( @@ -72,6 +75,8 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP onToggleFidelity={() => setFidelityGate((v) => !v)} fuzzyDedup={fuzzyDedup} onToggleFuzzy={() => setFuzzyDedup((v) => !v)} + riskGate={riskGate} + onToggleRisk={() => setRiskGate((v) => !v)} />
@@ -81,6 +86,7 @@ export function PlayView({ text, onText, laneEngines = LANE_ENGINES }: PlayViewP Fluxo combinado — {active.join(" → ")} + )}
diff --git a/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx b/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx index e1e894824f..c2ed9ac0bc 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/PlaygroundInput.tsx @@ -1,7 +1,7 @@ "use client"; export const LANE_ENGINES = ["session-dedup", "ccr", "lite", "rtk", "ionizer", "headroom", "caveman", "aggressive", "ultra"] as const; -export interface PlaygroundInputProps { text: string; onText: (t: string) => void; active: string[]; onToggleActive: (engine: string) => void; onRun: () => void; loading: boolean; fidelityGate: boolean; onToggleFidelity: () => void; fuzzyDedup: boolean; onToggleFuzzy: () => void; } -export function PlaygroundInput({ text, onText, active, onToggleActive, onRun, loading, fidelityGate, onToggleFidelity, fuzzyDedup, onToggleFuzzy }: PlaygroundInputProps) { +export interface PlaygroundInputProps { text: string; onText: (t: string) => void; active: string[]; onToggleActive: (engine: string) => void; onRun: () => void; loading: boolean; fidelityGate: boolean; onToggleFidelity: () => void; fuzzyDedup: boolean; onToggleFuzzy: () => void; riskGate: boolean; onToggleRisk: () => void; } +export function PlaygroundInput({ text, onText, active, onToggleActive, onRun, loading, fidelityGate, onToggleFidelity, fuzzyDedup, onToggleFuzzy, riskGate, onToggleRisk }: PlaygroundInputProps) { return (