From 2495d780de4041b9b80543fb62f8f83753d5f85c Mon Sep 17 00:00:00 2001 From: AIR-Xie <98206374+AIR-Xie@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:52:07 +0800 Subject: [PATCH 1/5] feat(permissions): add hosted decision guardrails --- CHANGELOG.md | 4 + .../src/__tests__/generalSettings.test.tsx | 33 +- desktop/src/api/providers.ts | 45 +++ .../src/components/chat/ChatInput.test.tsx | 13 + .../src/components/chat/MessageList.test.tsx | 69 ++++ desktop/src/components/chat/MessageList.tsx | 39 ++- .../src/components/chat/PermissionDialog.tsx | 30 +- .../src/components/chat/composerUtils.test.ts | 4 + desktop/src/components/chat/composerUtils.ts | 3 + .../DecisionProviderFormModal.test.tsx | 75 +++++ .../settings/DecisionProviderFormModal.tsx | 259 +++++++++++++++ .../DecisionProviderSettingsSection.tsx | 136 ++++++++ .../settings/ModelDiscoveryCombobox.tsx | 120 +++++++ .../components/settings/ProviderFormModal.tsx | 117 +------ .../settings/ProviderSettings.test.tsx | 33 +- .../components/settings/ProviderSettings.tsx | 2 + desktop/src/i18n/locales/en.ts | 43 +++ desktop/src/i18n/locales/zh.ts | 43 +++ .../src/pages/Settings.browserTrust.test.tsx | 44 ++- desktop/src/pages/Settings.tsx | 177 ++++++++++ desktop/src/stores/chatStore.test.ts | 11 + desktop/src/stores/chatStore.ts | 4 + desktop/src/stores/decisionProviderStore.ts | 96 ++++++ desktop/src/stores/settingsStore.ts | 45 +++ desktop/src/types/chat.ts | 10 +- desktop/src/types/provider.ts | 46 +++ desktop/src/types/settings.ts | 10 + experiments/system1-guardrail/eval/README.md | 10 +- experiments/system1-guardrail/eval/lib.ts | 11 +- .../eval/openai-decisions.ts | 132 ++++++++ experiments/system1-guardrail/eval/score.ts | 5 +- src/QueryEngine.ts | 16 + .../structuredIO.permissionGuardrail.test.ts | 63 ++++ src/cli/structuredIO.ts | 65 ++-- src/components/messages/AttachmentMessage.tsx | 6 +- .../PermissionDecisionDebugInfo.tsx | 4 +- .../permissions/PermissionRuleExplanation.tsx | 7 +- src/components/permissions/hooks.ts | 2 + src/entrypoints/sdk/controlSchemas.ts | 9 + src/entrypoints/sdk/coreSchemas.ts | 16 + .../__tests__/provider-runtime-env.test.ts | 65 ++++ src/server/__tests__/providers.test.ts | 172 +++++++++- src/server/__tests__/settings.test.ts | 19 ++ .../__tests__/transcript-history.test.ts | 44 ++- src/server/__tests__/ws-memory-events.test.ts | 64 ++++ src/server/api/providers.ts | 67 ++++ src/server/config/decisionProviderPresets.ts | 47 +++ src/server/services/providerRuntimeEnv.ts | 54 ++++ src/server/services/providerService.ts | 303 ++++++++++++++++++ src/server/services/settingsService.ts | 8 + src/server/session/state/types.ts | 9 + src/server/session/translate.ts | 64 ++++ src/server/transcript/entries.ts | 41 ++- src/server/transcript/history.ts | 12 + src/server/transcript/items.ts | 11 + src/server/types/provider.ts | 63 ++++ src/server/ws/protocol.ts | 3 +- src/services/tools/toolExecution.ts | 18 ++ src/tools/BashTool/bashPermissions.ts | 54 +++- src/types/permissionGuardrail.ts | 27 ++ src/types/permissions.ts | 8 + src/utils/attachments.ts | 11 + src/utils/messages.ts | 1 + .../permissions/permissionGuardrail.test.ts | 71 ++++ src/utils/permissions/permissionGuardrail.ts | 289 +++++++++++++++++ .../permissions.permissionGuardrail.test.ts | 186 +++++++++++ src/utils/permissions/permissions.ts | 66 +++- src/utils/sessionStorage.ts | 3 + src/utils/settings/settings.ts | 17 + src/utils/settings/types.ts | 2 + 70 files changed, 3472 insertions(+), 184 deletions(-) create mode 100644 desktop/src/components/settings/DecisionProviderFormModal.test.tsx create mode 100644 desktop/src/components/settings/DecisionProviderFormModal.tsx create mode 100644 desktop/src/components/settings/DecisionProviderSettingsSection.tsx create mode 100644 desktop/src/components/settings/ModelDiscoveryCombobox.tsx create mode 100644 desktop/src/stores/decisionProviderStore.ts create mode 100644 experiments/system1-guardrail/eval/openai-decisions.ts create mode 100644 src/cli/structuredIO.permissionGuardrail.test.ts create mode 100644 src/server/config/decisionProviderPresets.ts create mode 100644 src/types/permissionGuardrail.ts create mode 100644 src/utils/permissions/permissionGuardrail.test.ts create mode 100644 src/utils/permissions/permissionGuardrail.ts create mode 100644 src/utils/permissions/permissions.permissionGuardrail.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a098d8ba..938c364d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to DreamCoder will be documented in this file. ## [Unreleased] +### Added + +- An opt-in permission guardrail can use separately configured OpenAI, OpenRouter, Ollama, or custom decision providers after deterministic permission rules. It defaults off, fails back to asking, preserves deny and bypass-immune checks, and records model-assisted permission decisions in the transcript. The desktop `/permissions` command opens permission settings, which document the project rule file and cross-platform shell rule names (#146). + ### Fixed - Confirmation dialogs keep long workspace paths inside their bounds, so Cancel and Enable remain visible when enabling bypass permissions (#226). Thanks @imshah-id. diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index a00b6d77..bf9d1a42 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -13,8 +13,28 @@ import type { ProviderPreset } from '../types/providerPreset' import type { AppMode, ThemeMode, UpdateProxySettings } from '../types/settings' const MOCK_DELETE_PROVIDER = vi.fn() -const MOCK_GET_SETTINGS = vi.fn() -const MOCK_UPDATE_SETTINGS = vi.fn() +const providerSettingsApiMock = vi.hoisted(() => ({ + getSettings: vi.fn(), + updateSettings: vi.fn(), +})) +const decisionProviderStoreState = vi.hoisted(() => ({ + providers: [], + activeId: null, + presets: [], + isLoading: false, + error: null, + fetchProviders: vi.fn(async () => {}), + fetchPresets: vi.fn(async () => {}), + createProvider: vi.fn(), + updateProvider: vi.fn(), + deleteProvider: vi.fn(), + activateProvider: vi.fn(), + testProvider: vi.fn(), + testConfig: vi.fn(), + discoverModels: vi.fn(), +})) +const MOCK_GET_SETTINGS = providerSettingsApiMock.getSettings +const MOCK_UPDATE_SETTINGS = providerSettingsApiMock.updateSettings const desktopNotificationsMock = vi.hoisted(() => ({ getDesktopNotificationPermission: vi.fn(), notifyDesktop: vi.fn(), @@ -63,10 +83,14 @@ vi.mock('../stores/providerStore', () => ({ useProviderStore: (selector?: (state: typeof providerStoreState) => T) => selector ? selector(providerStoreState) : providerStoreState, })) +vi.mock('../stores/decisionProviderStore', () => ({ + useDecisionProviderStore: (selector?: (state: typeof decisionProviderStoreState) => T) => selector ? selector(decisionProviderStoreState) : decisionProviderStoreState, +})) + vi.mock('../api/providers', () => ({ providersApi: { - getSettings: MOCK_GET_SETTINGS, - updateSettings: MOCK_UPDATE_SETTINGS, + getSettings: providerSettingsApiMock.getSettings, + updateSettings: providerSettingsApiMock.updateSettings, }, })) @@ -1269,6 +1293,7 @@ describe('Settings > Providers tab', () => { fireEvent.click(screen.getByRole('button', { name: /Add Provider|添加服务商/i })) const dialog = screen.getByRole('dialog') + await waitFor(() => expect(within(dialog).getByRole('textbox', { name: 'Settings JSON' })).toBeEnabled()) fireEvent.change(within(dialog).getByPlaceholderText('sk-...'), { target: { value: 'sk-test' } }) fireEvent.change(within(dialog).getByLabelText(/Model ID|模型 ID/i), { target: { value: 'gpt-5.5' } }) fireEvent.click(within(dialog).getAllByRole('button', { name: /Add|添加/i })[0]!) diff --git a/desktop/src/api/providers.ts b/desktop/src/api/providers.ts index 25fdf2fe..04a4022b 100644 --- a/desktop/src/api/providers.ts +++ b/desktop/src/api/providers.ts @@ -7,6 +7,12 @@ import type { ProviderTestResult, DiscoveredProviderModel, DiscoverProviderModelsInput, + SavedDecisionProvider, + DecisionProviderPreset, + CreateDecisionProviderInput, + UpdateDecisionProviderInput, + TestDecisionProviderInput, + DiscoverDecisionProviderModelsInput, } from '../types/provider' import type { ProviderPreset } from '../types/providerPreset' @@ -20,6 +26,9 @@ type AuthStatusResponse = { source: 'dreamcoder-provider' | 'original-settings' | 'env' | 'none' activeProvider?: string } +type DecisionProvidersResponse = { providers: SavedDecisionProvider[]; activeId: string | null } +type DecisionProviderResponse = { provider: SavedDecisionProvider } +type DecisionPresetsResponse = { presets: DecisionProviderPreset[] } export const providersApi = { list() { @@ -69,4 +78,40 @@ export const providersApi = { discoverModels(input: DiscoverProviderModelsInput) { return api.post('/api/providers/models', input) }, + + listDecisions() { + return api.get('/api/providers/decisions') + }, + + decisionPresets() { + return api.get('/api/providers/decisions/presets') + }, + + createDecision(input: CreateDecisionProviderInput) { + return api.post('/api/providers/decisions', input) + }, + + updateDecision(id: string, input: UpdateDecisionProviderInput) { + return api.put(`/api/providers/decisions/${id}`, input) + }, + + deleteDecision(id: string) { + return api.delete<{ ok: true }>(`/api/providers/decisions/${id}`) + }, + + activateDecision(id: string) { + return api.post<{ ok: true }>(`/api/providers/decisions/${id}/activate`) + }, + + testDecision(id: string, modelId?: string) { + return api.post(`/api/providers/decisions/${id}/test`, { modelId }) + }, + + testDecisionConfig(input: TestDecisionProviderInput) { + return api.post('/api/providers/decisions/test', input) + }, + + discoverDecisionModels(input: DiscoverDecisionProviderModelsInput) { + return api.post('/api/providers/decisions/models', input) + }, } diff --git a/desktop/src/components/chat/ChatInput.test.tsx b/desktop/src/components/chat/ChatInput.test.tsx index 2b6cfe2d..8be72fd2 100644 --- a/desktop/src/components/chat/ChatInput.test.tsx +++ b/desktop/src/components/chat/ChatInput.test.tsx @@ -1265,4 +1265,17 @@ describe('ChatInput file mentions', () => { expect(commandButtons[0]).toHaveTextContent('/superpowers:brainstorming') }) }) + + it('opens permission settings instead of sending /permissions to the sidecar', () => { + render() + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '/permissions', selectionStart: 12 }, + }) + fireEvent.click(screen.getByRole('button', { name: 'Run' })) + + expect(useUIStore.getState().settingsOpen).toBe(true) + expect(useUIStore.getState().settingsTab).toBe('permissions') + expect(screen.getByRole('textbox')).toHaveValue('') + }) }) diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 0f5e9ecc..cb7def83 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -448,6 +448,75 @@ describe('MessageList nested tool calls', () => { expect(screen.getByText('Budget: 0 / unlimited tokens')).toBeTruthy() }) + it('renders permission guardrail transcript markers with their score', () => { + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [{ + id: 'guardrail-1', + type: 'permission_guardrail_decision', + decision: 'allow', + toolUseId: 'tool-1', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet', + score: 0.237, + timestamp: 1, + }], + }), + }, + }) + + render() + + expect(screen.getByText('Auto-approved by permission guardrail')).toBeTruthy() + expect(screen.getByText('gpt-6-luna')).toBeTruthy() + expect(screen.getByText('safetyNet')).toBeTruthy() + expect(screen.getByText('Decision: allow')).toBeTruthy() + expect(screen.getByText('Score: 0.237')).toBeTruthy() + }) + + it('shows guardrail details on permission requests', () => { + const guardrail = { + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet' as const, + decision: 'ask' as const, + score: 0.812, + } + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + pendingPermission: { + requestId: 'request-1', + toolName: 'Bash', + toolUseId: 'tool-1', + input: { command: 'rm guarded.txt' }, + guardrail, + }, + messages: [{ + id: 'permission-1', + type: 'permission_request', + requestId: 'request-1', + toolName: 'Bash', + toolUseId: 'tool-1', + input: { command: 'rm guarded.txt' }, + guardrail, + timestamp: 1, + }], + }), + }, + }) + + render() + + expect(screen.getByText('Permission guardrail')).toBeTruthy() + expect(screen.getByText('gpt-6-luna')).toBeTruthy() + expect(screen.getByText('Safety net')).toBeTruthy() + expect(screen.getByText('Decision: ask')).toBeTruthy() + expect(screen.getByText('Score: 0.812')).toBeTruthy() + }) + it('renders non-agent background progress inline in the transcript', () => { useChatStore.setState({ sessions: { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 06e1fc86..99e8e052 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1,6 +1,6 @@ import { Tooltip as HintTooltip } from '../shared/Tooltip' import { useRef, useEffect, useMemo, memo, useState, useCallback, useDeferredValue, useLayoutEffect, type ReactNode } from 'react' -import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, CircleStop, FileStack, LoaderCircle, Settings, Target, XCircle } from 'lucide-react' +import { ArrowDown, BookMarked, Bot, CheckCircle2, ChevronDown, ChevronRight, CircleStop, FileStack, LoaderCircle, Settings, ShieldCheck, Target, XCircle } from 'lucide-react' import { ApiError } from '../../api/client' import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions' import { useChatStore, useSessionField } from '../../stores/chatStore' @@ -43,6 +43,7 @@ type MemoryEvent = Extract type GoalEvent = Extract type BackgroundTaskEvent = Extract type CompactSummaryEvent = Extract +type PermissionGuardrailDecision = Extract type ThinkingMessage = Extract type ActivityStep = ToolCall | ThinkingMessage @@ -850,6 +851,34 @@ function MemoryEventCard({ message }: { message: MemoryEvent }) { ) } +function PermissionGuardrailDecisionCard({ message }: { message: PermissionGuardrailDecision }) { + const t = useTranslation() + const score = message.score === null + ? t('chat.permissionGuardrail.scoreUnavailable') + : t('chat.permissionGuardrail.score', { score: message.score.toFixed(3) }) + + return ( +
+
+
+
+ ) +} + type MessageListProps = { sessionId?: string | null compact?: boolean @@ -1002,6 +1031,8 @@ function getMessageContentWeight(message: UIMessage): number { return (message.objective?.length ?? 0) + (message.message?.length ?? 0) case 'memory_event': return (message.message?.length ?? 0) + message.files.reduce((total, file) => total + file.path.length + (file.summary?.length ?? 0), 0) + case 'permission_guardrail_decision': + return message.modelId.length + message.preset.length case 'background_task': return getShallowStringWeight(message.task) case 'task_summary': @@ -1062,6 +1093,7 @@ function estimateMessageHeight(message: UIMessage): number { case 'background_task': case 'goal_event': case 'memory_event': + case 'permission_guardrail_decision': case 'permission_request': case 'task_summary': return 110 @@ -1097,6 +1129,8 @@ function getMessageMetricSignature(message: UIMessage): string { return `${message.type}:${message.action}:${message.status ?? ''}:${message.objective?.length ?? 0}:${message.message?.length ?? 0}` case 'memory_event': return `${message.type}:${message.event}:${message.files.length}:${message.message?.length ?? 0}` + case 'permission_guardrail_decision': + return `${message.type}:${message.toolUseId}:${message.decision}:${message.score ?? ''}` case 'background_task': return `${message.type}:${message.task.taskId}:${message.task.status}:${message.task.updatedAt}` case 'permission_request': @@ -2130,6 +2164,7 @@ export const MessageBlock = memo(function MessageBlock({ toolName={message.toolName} input={message.input} description={message.description} + guardrail={message.guardrail} resolution={message.resolution} /> ) @@ -2156,6 +2191,8 @@ export const MessageBlock = memo(function MessageBlock({ return case 'memory_event': return + case 'permission_guardrail_decision': + return case 'compact_summary': return case 'goal_event': diff --git a/desktop/src/components/chat/PermissionDialog.tsx b/desktop/src/components/chat/PermissionDialog.tsx index 57c92aa5..a2376369 100644 --- a/desktop/src/components/chat/PermissionDialog.tsx +++ b/desktop/src/components/chat/PermissionDialog.tsx @@ -1,4 +1,4 @@ -import { Check, BadgeCheck, ChevronDown, FolderOpen, Search, X } from 'lucide-react' +import { Check, BadgeCheck, ChevronDown, FolderOpen, Search, ShieldCheck, X } from 'lucide-react' import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../shared/ui/card' import { CodeBlock, CodeBlockContent, CodeBlockLine } from '../shared/ui/code-block' import { CodeViewer } from './CodeViewer' @@ -12,7 +12,7 @@ import { Button } from '../shared/ui/button' import { AwaitingApprovalBadge } from './AwaitingApprovalBadge' import { DiffViewer } from './DiffViewer' import { getCuaToolDisplay } from '../../lib/cuaToolDisplay' -import type { PermissionResolution } from '../../types/chat' +import type { PermissionGuardrailDetails, PermissionResolution } from '../../types/chat' type Props = { sessionId?: string | null @@ -20,6 +20,7 @@ type Props = { toolName: string input: unknown description?: string + guardrail?: PermissionGuardrailDetails resolution?: PermissionResolution } @@ -104,7 +105,7 @@ function resolutionLabelKey(resolution: PermissionResolution | undefined): Trans return resolution.outcome === 'allowed' ? 'permission.allowedElsewhere' : 'permission.deniedElsewhere' } -export function PermissionDialog({ sessionId, requestId, toolName, input, description, resolution }: Props) { +export function PermissionDialog({ sessionId, requestId, toolName, input, description, guardrail, resolution }: Props) { const respondToPermission = useChatStore((s) => s.respondToPermission) const activeTabId = useTabStore((s) => s.activeTabId) const targetSessionId = sessionId ?? activeTabId @@ -124,6 +125,11 @@ export function PermissionDialog({ sessionId, requestId, toolName, input, descri const shownDescription = cua && description && /mcp__|\bMCP\b/i.test(description) ? undefined : description const DetailIcon = cua ? Icon : toolName === 'Glob' || toolName === 'Grep' ? Search : FolderOpen const allowRawToggle = !preview + const guardrailScore = guardrail?.score === null + ? t('chat.permissionGuardrail.scoreUnavailable') + : guardrail + ? t('chat.permissionGuardrail.score', { score: guardrail.score.toFixed(3) }) + : null return ( + {guardrail && ( +
+
+ )} {preview ? (
{details.primary && toolName !== 'Bash' ? ( diff --git a/desktop/src/components/chat/composerUtils.test.ts b/desktop/src/components/chat/composerUtils.test.ts index 9a1ad7e4..75101d37 100644 --- a/desktop/src/components/chat/composerUtils.test.ts +++ b/desktop/src/components/chat/composerUtils.test.ts @@ -117,8 +117,12 @@ describe('composerUtils', () => { expect(resolveSlashUiAction('plugins')).toEqual({ type: 'settings', tab: 'plugins' }) expect(resolveSlashUiAction('memory')).toEqual({ type: 'settings', tab: 'memory' }) expect(resolveSlashUiAction('doctor')).toEqual({ type: 'settings', tab: 'diagnostics' }) + expect(resolveSlashUiAction('permissions')).toEqual({ type: 'settings', tab: 'permissions' }) expect(mergeSlashCommands([]).map((command) => command.name)).toContain('plugin') expect(mergeSlashCommands([]).map((command) => command.name)).toContain('memory') + expect(mergeSlashCommands([]).filter((command) => command.name === 'permissions')).toEqual([ + { name: 'permissions', description: 'View or manage tool permissions' }, + ]) expect(mergeSlashCommands([]).map((command) => command.name)).not.toContain('plugins') }) diff --git a/desktop/src/components/chat/composerUtils.ts b/desktop/src/components/chat/composerUtils.ts index c6be585a..0a2450fc 100644 --- a/desktop/src/components/chat/composerUtils.ts +++ b/desktop/src/components/chat/composerUtils.ts @@ -64,6 +64,9 @@ export type SlashUiAction = export function resolveSlashUiAction(value: string): SlashUiAction | null { const normalizedValue = SLASH_COMMAND_ALIASES.find((alias) => alias.name === value)?.target ?? value + if (normalizedValue === 'permissions') { + return { type: 'settings', tab: 'permissions' } + } const panelCommand = PANEL_SLASH_COMMANDS.find((command) => command.name === normalizedValue) if (panelCommand) { return { type: 'panel', command: panelCommand.name } diff --git a/desktop/src/components/settings/DecisionProviderFormModal.test.tsx b/desktop/src/components/settings/DecisionProviderFormModal.test.tsx new file mode 100644 index 00000000..85f3896b --- /dev/null +++ b/desktop/src/components/settings/DecisionProviderFormModal.test.tsx @@ -0,0 +1,75 @@ +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useSettingsStore } from '../../stores/settingsStore' +import { useDecisionProviderStore } from '../../stores/decisionProviderStore' +import type { DecisionProviderPreset } from '../../types/provider' +import { DecisionProviderFormModal } from './DecisionProviderFormModal' + +const presets: DecisionProviderPreset[] = [{ + id: 'openai', + name: 'OpenAI', + baseUrl: 'https://api.openai.com/v1', + apiFormat: 'openai_decisions', + models: [{ id: 'gpt-6-luna' }], +}] + +describe('DecisionProviderFormModal', () => { + beforeEach(() => { + useSettingsStore.setState({ locale: 'en' }) + useDecisionProviderStore.setState({ + discoverModels: vi.fn(async () => [{ id: 'decision-model-a' }, { id: 'decision-model-b' }]), + testConfig: vi.fn(), + createProvider: vi.fn(), + updateProvider: vi.fn(), + }) + }) + + afterEach(() => { + cleanup() + useSettingsStore.setState(useSettingsStore.getInitialState(), true) + useDecisionProviderStore.setState(useDecisionProviderStore.getInitialState(), true) + }) + + it('shows discovered models as separate dropdown options', async () => { + render() + + fireEvent.click(screen.getByRole('button', { name: 'Fetch models' })) + + const modelInput = screen.getByRole('combobox', { name: 'Model ID' }) + await waitFor(() => expect(modelInput).toHaveValue('')) + fireEvent.focus(modelInput) + + expect(screen.getByRole('option', { name: 'decision-model-a' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'decision-model-b' })).toBeInTheDocument() + expect(screen.queryByText('decision-model-adecision-model-b')).not.toBeInTheDocument() + + fireEvent.click(screen.getByRole('option', { name: 'decision-model-a' })) + fireEvent.click(screen.getByRole('button', { name: 'Add' })) + + expect(screen.getByText('decision-model-a')).toBeInTheDocument() + }) + + it('repairs a previously concatenated OpenAI model value', () => { + render( + , + ) + + expect(screen.getByText('gpt-6-luna')).toBeInTheDocument() + expect(screen.queryByText('gpt-3.5-turbogpt-5.6-lunagpt-6-luna')).not.toBeInTheDocument() + }) +}) diff --git a/desktop/src/components/settings/DecisionProviderFormModal.tsx b/desktop/src/components/settings/DecisionProviderFormModal.tsx new file mode 100644 index 00000000..5839ee9f --- /dev/null +++ b/desktop/src/components/settings/DecisionProviderFormModal.tsx @@ -0,0 +1,259 @@ +import { Trash2Icon } from 'lucide-react' +import { useState } from 'react' +import { useTranslation } from '../../i18n' +import { useDecisionProviderStore } from '../../stores/decisionProviderStore' +import type { + DecisionProviderFormat, + DecisionProviderPreset, + DecisionProviderPresetId, + ProviderModel, + ProviderTestResult, + SavedDecisionProvider, +} from '../../types/provider' +import { FormSelect } from '../shared/FormSelect' +import { LabeledInput } from '../shared/LabeledInput' +import { Modal } from '../shared/Modal' +import { Button } from '../shared/ui/button' +import { ModelDiscoveryCombobox } from './ModelDiscoveryCombobox' + +type Props = { + open: boolean + onClose: () => void + provider?: SavedDecisionProvider + presets: DecisionProviderPreset[] +} + +const FORMAT_OPTIONS: DecisionProviderFormat[] = [ + 'openai_decisions', + 'openrouter_decisions', + 'openai_chat', + 'ollama_chat', +] + +function initialModels( + provider: SavedDecisionProvider | undefined, + preset: DecisionProviderPreset | undefined, +): ProviderModel[] { + const configured = provider?.models ?? preset?.models ?? [] + if (!provider || !preset?.models.length) return configured.map((model) => ({ ...model })) + const supportedIds = new Set(preset.models.map((model) => model.id)) + const supported = configured.filter((model) => supportedIds.has(model.id)) + return (supported.length > 0 ? supported : preset.models).map((model) => ({ ...model })) +} + +function DecisionProviderFormModal({ open, onClose, provider, presets }: Props) { + const t = useTranslation() + const createProvider = useDecisionProviderStore((state) => state.createProvider) + const updateProvider = useDecisionProviderStore((state) => state.updateProvider) + const discoverModels = useDecisionProviderStore((state) => state.discoverModels) + const testConfig = useDecisionProviderStore((state) => state.testConfig) + const initialPreset = presets.find((entry) => entry.id === provider?.presetId) ?? presets[0] + const [presetId, setPresetId] = useState(initialPreset?.id ?? 'openai') + const [name, setName] = useState(provider?.name ?? initialPreset?.name ?? '') + const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? initialPreset?.baseUrl ?? '') + const [apiFormat, setApiFormat] = useState(provider?.apiFormat ?? initialPreset?.apiFormat ?? 'openai_decisions') + const [apiKey, setApiKey] = useState(provider?.apiKey ?? '') + const configuredModels = initialModels(provider, initialPreset) + const [models, setModels] = useState(configuredModels) + const [discoveredModels, setDiscoveredModels] = useState(configuredModels) + const [newModelId, setNewModelId] = useState('') + const [saving, setSaving] = useState(false) + const [discovering, setDiscovering] = useState(false) + const [testing, setTesting] = useState(false) + const [testResult, setTestResult] = useState(null) + const [error, setError] = useState(null) + + const applyPreset = (nextId: DecisionProviderPresetId) => { + const preset = presets.find((entry) => entry.id === nextId) + setPresetId(nextId) + if (!preset) return + setName(preset.name) + setBaseUrl(preset.baseUrl) + setApiFormat(preset.apiFormat) + setModels(preset.models.map((model) => ({ ...model }))) + setDiscoveredModels(preset.models.map((model) => ({ ...model }))) + setNewModelId('') + setTestResult(null) + } + + const handleDiscover = async () => { + setDiscovering(true) + setError(null) + try { + const discovered = await discoverModels({ presetId, baseUrl, apiKey, apiFormat }) + setDiscoveredModels(discovered) + setModels((current) => { + const discoveredIds = new Set(discovered.map((model) => model.id)) + const retained = current.filter((model) => discoveredIds.has(model.id)) + if (retained.length > 0) return retained + return discovered.length === 1 ? discovered : [] + }) + setNewModelId('') + } catch (reason) { + setError(reason instanceof Error ? reason.message : String(reason)) + } finally { + setDiscovering(false) + } + } + + const handleAddModel = () => { + const id = newModelId.trim() + if (!id || models.some((model) => model.id === id)) return + const discovered = discoveredModels.find((model) => model.id === id) + setModels((current) => [...current, discovered ?? { id }]) + setNewModelId('') + } + + const handleTest = async () => { + if (!models[0]) return + setTesting(true) + setError(null) + try { + setTestResult(await testConfig({ baseUrl, apiKey, apiFormat, modelId: models[0].id })) + } catch (reason) { + setError(reason instanceof Error ? reason.message : String(reason)) + } finally { + setTesting(false) + } + } + + const handleSave = async () => { + if (!name.trim() || !baseUrl.trim() || models.length === 0) return + setSaving(true) + setError(null) + try { + const input = { + presetId, + name: name.trim(), + baseUrl: baseUrl.trim(), + apiFormat, + apiKey, + models, + } + if (provider) await updateProvider(provider.id, input) + else await createProvider(input) + onClose() + } catch (reason) { + setError(reason instanceof Error ? reason.message : String(reason)) + } finally { + setSaving(false) + } + } + + return ( + + + + + )} + > +
+
+ + ({ value: entry.id, label: entry.name }))} + /> +
+ setName(event.target.value)} /> + setBaseUrl(event.target.value)} /> +
+ + { + setApiFormat(value) + setDiscoveredModels([]) + setNewModelId('') + }} + label={t('settings.decisionProviders.apiFormat')} + options={FORMAT_OPTIONS.map((value) => ({ value, label: value }))} + /> +
+ setApiKey(event.target.value)} + placeholder={apiFormat === 'ollama_chat' ? t('settings.decisionProviders.apiKeyOptional') : ''} + /> +
+
+ + +
+
+
+ + setNewModelId(model.id)} + label={t('settings.providers.modelId')} + placeholder={t('settings.providers.modelIdPlaceholder')} + useCustomLabel={(id) => t('settings.providers.useCustomModel', { id })} + /> +
+ +
+
+ {models.map((model) => ( +
+
+
{model.name || model.id}
+ {model.name &&
{model.id}
} +
+ +
+ ))} +
+
+
+ +
+ {testResult && ( +

+ {testResult.connectivity.success + ? t('settings.providers.connectivityOk', { latency: String(testResult.connectivity.latencyMs) }) + : t('settings.providers.connectivityFailed', { error: testResult.connectivity.error ?? '' })} +

+ )} + {error &&

{error}

} +
+
+ ) +} + +export { DecisionProviderFormModal } diff --git a/desktop/src/components/settings/DecisionProviderSettingsSection.tsx b/desktop/src/components/settings/DecisionProviderSettingsSection.tsx new file mode 100644 index 00000000..0500fe3d --- /dev/null +++ b/desktop/src/components/settings/DecisionProviderSettingsSection.tsx @@ -0,0 +1,136 @@ +import { Plus, ShieldCheck } from 'lucide-react' +import { useEffect, useState } from 'react' +import { useTranslation } from '../../i18n' +import { useDecisionProviderStore } from '../../stores/decisionProviderStore' +import type { ProviderTestResult, SavedDecisionProvider } from '../../types/provider' +import { ConfirmDialog } from '../shared/ConfirmDialog' +import { LoadingBlock } from '../shared/LoadingBlock' +import { Button } from '../shared/ui/button' +import { DecisionProviderFormModal } from './DecisionProviderFormModal' + +function DecisionProviderSettingsSection() { + const t = useTranslation() + const { + providers, + activeId, + presets, + isLoading, + fetchProviders, + fetchPresets, + activateProvider, + deleteProvider, + testProvider, + } = useDecisionProviderStore() + const [showCreate, setShowCreate] = useState(false) + const [editing, setEditing] = useState(null) + const [pendingDelete, setPendingDelete] = useState(null) + const [testResults, setTestResults] = useState>({}) + + useEffect(() => { + void fetchProviders() + void fetchPresets() + }, [fetchPresets, fetchProviders]) + + const handleTest = async (provider: SavedDecisionProvider) => { + setTestResults((current) => ({ ...current, [provider.id]: { loading: true } })) + try { + const result = await testProvider(provider.id) + setTestResults((current) => ({ ...current, [provider.id]: { loading: false, result } })) + } catch (error) { + setTestResults((current) => ({ + ...current, + [provider.id]: { + loading: false, + result: { + connectivity: { + success: false, + latencyMs: 0, + error: error instanceof Error ? error.message : String(error), + }, + }, + }, + })) + } + } + + return ( +
+
+
+

+ {t('settings.decisionProviders.title')} +

+

+ {t('settings.decisionProviders.description')} +

+
+ +
+ + {isLoading && providers.length === 0 ? ( + + ) : providers.length === 0 ? ( +

{t('settings.decisionProviders.empty')}

+ ) : ( +
+ {providers.map((provider) => { + const active = provider.id === activeId + const test = testResults[provider.id] + return ( +
+
+
+
+

+ {provider.baseUrl} · {provider.models[0]?.name ?? provider.models[0]?.id} +

+ {test?.result && ( +

+ {test.result.connectivity.success + ? t('settings.providers.connectivityOk', { latency: String(test.result.connectivity.latencyMs) }) + : t('settings.providers.connectivityFailed', { error: test.result.connectivity.error ?? '' })} +

+ )} +
+
+ {!active && } + + + {!active && } +
+
+ ) + })} +
+ )} + + {showCreate && setShowCreate(false)} presets={presets} />} + {editing && setEditing(null)} provider={editing} presets={presets} />} + setPendingDelete(null)} + onConfirm={async () => { + if (!pendingDelete) return + await deleteProvider(pendingDelete.id) + setPendingDelete(null) + }} + title={t('common.delete')} + body={pendingDelete ? t('settings.decisionProviders.confirmDelete', { name: pendingDelete.name }) : ''} + confirmLabel={t('common.delete')} + cancelLabel={t('common.cancel')} + confirmVariant="destructive" + /> +
+ ) +} + +export { DecisionProviderSettingsSection } diff --git a/desktop/src/components/settings/ModelDiscoveryCombobox.tsx b/desktop/src/components/settings/ModelDiscoveryCombobox.tsx new file mode 100644 index 00000000..d478ffde --- /dev/null +++ b/desktop/src/components/settings/ModelDiscoveryCombobox.tsx @@ -0,0 +1,120 @@ +import { Command as CommandPrimitive } from 'cmdk' +import { Box, ChevronDown, Plus } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' +import type { ProviderModel } from '../../types/provider' +import { Command, CommandItem, CommandList } from '../shared/ui/command' +import { Input } from '../shared/ui/input' +import { Popover, PopoverAnchor, PopoverContent } from '../shared/ui/popover' + +type Props = { + models: ProviderModel[] + value: string + onChange: (value: string) => void + onSelect: (model: ProviderModel) => void + label: string + placeholder: string + useCustomLabel: (id: string) => string +} + +function ModelDiscoveryCombobox({ + models, + value, + onChange, + onSelect, + label, + placeholder, + useCustomLabel, +}: Props) { + const [open, setOpen] = useState(false) + const typed = value.trim() + const query = typed.toLowerCase() + const filteredModels = useMemo(() => { + if (!query) return models + const matches = models.filter((model) => ( + model.id.toLowerCase().includes(query) || model.name?.toLowerCase().includes(query) + )) + return [...matches.filter((model) => model.id === typed), ...matches.filter((model) => model.id !== typed)] + }, [models, query, typed]) + const offerTyped = typed !== '' && !models.some((model) => model.id === typed) + const expanded = open && models.length > 0 + + useEffect(() => { + if (models.length === 0) setOpen(false) + }, [models.length]) + + const selectModel = (model: ProviderModel) => { + onSelect(model) + setOpen(false) + } + + return ( + + + +
+ { + onChange(next) + if (models.length > 0) setOpen(true) + }} + > + { + if (models.length > 0) setOpen(true) + }} + onKeyDown={(event) => { + if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && models.length > 0) setOpen(true) + }} + placeholder={placeholder} + className="pr-8" + /> + + {models.length > 0 && ( +
+
+ event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + onInteractOutside={(event) => { + if (event.target instanceof Element && event.target.closest('[cmdk-root]')) event.preventDefault() + }} + onMouseDown={(event) => event.preventDefault()} + className="w-[var(--radix-popover-trigger-width)] p-1" + > + + {offerTyped && ( + setOpen(false)} className="gap-2 px-3 py-2"> + + )} + {filteredModels.map((model) => ( + selectModel(model)} + className="gap-2 px-3 py-2" + > + + ))} + + +
+
+ ) +} + +export { ModelDiscoveryCombobox } diff --git a/desktop/src/components/settings/ProviderFormModal.tsx b/desktop/src/components/settings/ProviderFormModal.tsx index d86ef99f..78c9b2eb 100644 --- a/desktop/src/components/settings/ProviderFormModal.tsx +++ b/desktop/src/components/settings/ProviderFormModal.tsx @@ -1,9 +1,8 @@ import { FormSelect } from '../shared/FormSelect' import { Textarea } from '../shared/ui/textarea' import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupButton } from '../shared/ui/input-group' -import { ArrowDownIcon, ArrowUpIcon, ArrowUpRightIcon, Box, ChevronDown, EyeIcon, EyeOffIcon, KeyRoundIcon, Plus, Trash2Icon } from 'lucide-react' +import { ArrowDownIcon, ArrowUpIcon, ArrowUpRightIcon, EyeIcon, EyeOffIcon, KeyRoundIcon, Trash2Icon } from 'lucide-react' import { useState, useEffect, useMemo, useRef } from 'react' -import { Command as CommandPrimitive } from 'cmdk' import { useProviderStore } from '../../stores/providerStore' import { useSettingsStore } from '../../stores/settingsStore' import { useTranslation } from '../../i18n' @@ -13,8 +12,6 @@ import { LabeledInput } from '../shared/LabeledInput' import { Button } from '../shared/ui/button' import { getPresetDisplayName } from './presetDisplayName' import { ProviderIcon } from './ProviderIcon' -import { Command, CommandItem, CommandList } from '../shared/ui/command' -import { Popover, PopoverAnchor, PopoverContent } from '../shared/ui/popover' import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ProviderModel, DiscoveredProviderModel, ApiFormat, ProviderAuthStrategy } from '../../types/provider' import type { ProviderPreset } from '../../types/providerPreset' import { @@ -24,6 +21,7 @@ import { stripProviderSettingsJsonEnv, } from '../../lib/providerSettingsJson' import { openExternalUrl } from '../../lib/externalUrl' +import { ModelDiscoveryCombobox } from './ModelDiscoveryCombobox' type ProviderFormProps = { open: boolean @@ -257,117 +255,6 @@ function buildFallbackPreset(provider?: SavedProvider): ProviderPreset { } } -type ModelDiscoveryComboboxProps = { - models: ProviderModel[] - value: string - onChange: (value: string) => void - onSelect: (model: ProviderModel) => void - label: string - placeholder: string - useCustomLabel: (id: string) => string -} - -function ModelDiscoveryCombobox({ - models, - value, - onChange, - onSelect, - label, - placeholder, - useCustomLabel, -}: ModelDiscoveryComboboxProps) { - const [open, setOpen] = useState(false) - const typed = value.trim() - const query = typed.toLowerCase() - const filteredModels = useMemo(() => { - if (!query) return models - const matches = models.filter((model) => ( - model.id.toLowerCase().includes(query) || model.name?.toLowerCase().includes(query) - )) - return [...matches.filter((model) => model.id === typed), ...matches.filter((model) => model.id !== typed)] - }, [models, query, typed]) - const offerTyped = typed !== '' && !models.some((model) => model.id === typed) - const expanded = open && models.length > 0 - - useEffect(() => { - if (models.length === 0) setOpen(false) - }, [models.length]) - - const selectModel = (model: ProviderModel) => { - onSelect(model) - setOpen(false) - } - - return ( - - - -
- { - onChange(next) - if (models.length > 0) setOpen(true) - }} - > - { - if (models.length > 0) setOpen(true) - }} - onKeyDown={(event) => { - if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && models.length > 0) setOpen(true) - }} - placeholder={placeholder} - className="pr-8" - /> - - {models.length > 0 && ( -
-
- event.preventDefault()} - onCloseAutoFocus={(event) => event.preventDefault()} - onInteractOutside={(event) => { - if (event.target instanceof Element && event.target.closest('[cmdk-root]')) event.preventDefault() - }} - onMouseDown={(event) => event.preventDefault()} - className="w-[var(--radix-popover-trigger-width)] p-1" - > - - {offerTyped && ( - setOpen(false)} className="gap-2 px-3 py-2"> - - )} - {filteredModels.map((model) => ( - selectModel(model)} - className="gap-2 px-3 py-2" - > - - ))} - - -
-
- ) -} - function ProviderFormModal({ open, onClose, mode, provider, presets, onCreated }: ProviderFormProps) { const createProvider = useProviderStore((state) => state.createProvider) const updateProvider = useProviderStore((state) => state.updateProvider) diff --git a/desktop/src/components/settings/ProviderSettings.test.tsx b/desktop/src/components/settings/ProviderSettings.test.tsx index bdb52fb9..72629314 100644 --- a/desktop/src/components/settings/ProviderSettings.test.tsx +++ b/desktop/src/components/settings/ProviderSettings.test.tsx @@ -1,10 +1,11 @@ -import { render, screen } from '@testing-library/react' +import { cleanup, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SavedProvider } from '../../types/provider' import { useProviderStore } from '../../stores/providerStore' import { useSettingsStore } from '../../stores/settingsStore' import { ProviderSettings } from './ProviderSettings' +import { useDecisionProviderStore } from '../../stores/decisionProviderStore' const models = [{ id: 'model-main' }] const providers: SavedProvider[] = [ @@ -41,6 +42,25 @@ describe('ProviderSettings provider icons', () => { fetchProviders: vi.fn(async () => {}), fetchPresets: vi.fn(async () => {}), }) + useDecisionProviderStore.setState({ + providers: [ + { + id: 'guardrail-1', + presetId: 'openai', + name: 'OpenAI Decisions', + deployment: 'hosted', + apiFormat: 'openai_decisions', + baseUrl: 'https://api.openai.com/v1', + apiKey: '', + models: [{ id: 'gpt-6-luna' }], + }, + ], + activeId: 'guardrail-1', + presets: [], + isLoading: false, + fetchProviders: vi.fn(async () => {}), + fetchPresets: vi.fn(async () => {}), + }) }) it('uses the saved preset ID for the logo and falls back for custom providers', () => { @@ -48,5 +68,14 @@ describe('ProviderSettings provider icons', () => { expect(screen.getByText('DeepSeek').parentElement?.querySelector('[data-testid="provider-icon-deepseek"]')).toBeInTheDocument() expect(screen.getByText('My Custom Provider').parentElement?.querySelector('[data-testid="provider-icon-fallback"]')).toBeInTheDocument() + expect(screen.getByText('Permission guardrail providers')).toBeInTheDocument() + expect(screen.getByText('OpenAI Decisions')).toBeInTheDocument() + }) + + afterEach(() => { + cleanup() + useSettingsStore.setState(useSettingsStore.getInitialState(), true) + useProviderStore.setState(useProviderStore.getInitialState(), true) + useDecisionProviderStore.setState(useDecisionProviderStore.getInitialState(), true) }) }) diff --git a/desktop/src/components/settings/ProviderSettings.tsx b/desktop/src/components/settings/ProviderSettings.tsx index 53df105c..7bbb0e48 100644 --- a/desktop/src/components/settings/ProviderSettings.tsx +++ b/desktop/src/components/settings/ProviderSettings.tsx @@ -10,6 +10,7 @@ import { ProviderFormModal } from './ProviderFormModal' import { ProviderIcon } from './ProviderIcon' import { SettingsPageHeader } from './SettingsPageHeader' import { Plus } from 'lucide-react' +import { DecisionProviderSettingsSection } from './DecisionProviderSettingsSection' function ProviderSettings() { const { @@ -190,6 +191,7 @@ function ProviderSettings() { confirmVariant="destructive" loading={isDeletingProvider} /> +
) } diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 0f6e14e6..52743fee 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -513,8 +513,46 @@ export const en = { 'settings.providers.authStrategyDualDummy': 'Write dummy to both variables', 'settings.providers.authStrategyDualDummyDesc': 'Local vLLM-compatible services that only need placeholder auth values.', + 'settings.decisionProviders.title': 'Permission guardrail providers', + 'settings.decisionProviders.description': 'Manage separate decision models used to review permission prompts. Connection secrets stay in provider configuration.', + 'settings.decisionProviders.add': 'Add guardrail provider', + 'settings.decisionProviders.empty': 'No permission guardrail providers configured.', + 'settings.decisionProviders.addTitle': 'Add permission guardrail provider', + 'settings.decisionProviders.editTitle': 'Edit permission guardrail provider', + 'settings.decisionProviders.preset': 'Provider preset', + 'settings.decisionProviders.name': 'Name', + 'settings.decisionProviders.baseUrl': 'Base URL', + 'settings.decisionProviders.apiFormat': 'API format', + 'settings.decisionProviders.apiKey': 'API key', + 'settings.decisionProviders.apiKeyOptional': 'Optional for loopback Ollama', + 'settings.decisionProviders.models': 'Models', + 'settings.decisionProviders.modelsPlaceholder': 'One model ID per line', + 'settings.decisionProviders.fetchModels': 'Fetch models', + 'settings.decisionProviders.local': 'Local', + 'settings.decisionProviders.hosted': 'Hosted', + 'settings.decisionProviders.confirmDelete': 'Delete permission guardrail provider “{name}”?', + 'settings.permissions.title': 'Permission Mode', 'settings.permissions.description': 'The mode new sessions start in. Each session can switch its own mode next to the message box.', + 'settings.permissions.rulesTitle': 'Explicit tool rules', + 'settings.permissions.rulesDescription': 'Manage project Allow, Ask, and Deny rules in the file below. These deterministic rules take precedence over bypass mode and the permission guardrail.', + 'settings.permissions.rulesFile': 'Project rules file', + 'settings.permissions.rulesShellNames': 'Rule tool names must match the tool card. On Windows use PowerShell(...); for cross-platform projects include both Bash(...) and PowerShell(...).', + 'settings.permissions.rulesReload': 'Start a new session after editing the file so the rules are reloaded.', + 'settings.permissionGuardrail.title': 'Permission guardrail', + 'settings.permissionGuardrail.description': 'Optionally ask a separate decision model to reduce prompts or add a safety check after deterministic permission rules run.', + 'settings.permissionGuardrail.enabled': 'Enable permission guardrail', + 'settings.permissionGuardrail.provider': 'Provider', + 'settings.permissionGuardrail.selectProvider': 'Select a provider', + 'settings.permissionGuardrail.model': 'Model', + 'settings.permissionGuardrail.selectModel': 'Select a model', + 'settings.permissionGuardrail.preset': 'Behavior', + 'settings.permissionGuardrail.safetyNet': 'Safety net', + 'settings.permissionGuardrail.fewerPrompts': 'Fewer prompts', + 'settings.permissionGuardrail.threshold': 'Confidence threshold', + 'settings.permissionGuardrail.calibrationRequired': 'Fewer prompts remains observation-only until this provider and model have a threshold calibrated on the SABER development split.', + 'settings.permissionGuardrail.experimentNote': 'Decision-model scores are model-specific. Thresholds are selected on SABER dev; the test split is reserved for one final report.', + 'settings.permissionGuardrail.privacyNotice': 'I understand that the user request, recent tool context, tool input, and working directory may be sent to this hosted provider for a permission decision.', 'settings.browserTrust.title': 'Trusted sites', 'settings.browserTrust.description': 'Sites the agent can operate in the side browser without asking. Only exact https origins are accepted. Reading a page never needs permission.', 'settings.browserTrust.inputLabel': 'Site to trust', @@ -1124,6 +1162,11 @@ export const en = { 'chat.compactSummary.trigger.auto': 'auto', 'chat.compactSummary.tokens': '{count} tokens before compact', 'chat.compactSummary.messages': '{count} messages summarized', + 'chat.permissionGuardrail.autoApproved': 'Auto-approved by permission guardrail', + 'chat.permissionGuardrail.confirmationApproved': 'Approved after guardrail confirmation', + 'chat.permissionGuardrail.decision': 'Decision: {decision}', + 'chat.permissionGuardrail.score': 'Score: {score}', + 'chat.permissionGuardrail.scoreUnavailable': 'Score: unavailable', 'chat.activeGoal.title': 'Active goal', 'chat.activeGoal.running': 'Loop running', 'chat.activeGoal.active': 'Active', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index ec84e043..f85d9417 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -515,8 +515,46 @@ export const zh: Record = { 'settings.providers.authStrategyDualDummy': '两个变量写 dummy', 'settings.providers.authStrategyDualDummyDesc': 'vLLM 等本地兼容服务,只需要占位认证值。', + 'settings.decisionProviders.title': '权限 Guardrail 服务商', + 'settings.decisionProviders.description': '单独管理用于复核权限提示的决策模型。连接密钥保存在服务商配置中。', + 'settings.decisionProviders.add': '添加 Guardrail 服务商', + 'settings.decisionProviders.empty': '尚未配置权限 Guardrail 服务商。', + 'settings.decisionProviders.addTitle': '添加权限 Guardrail 服务商', + 'settings.decisionProviders.editTitle': '编辑权限 Guardrail 服务商', + 'settings.decisionProviders.preset': '服务商预设', + 'settings.decisionProviders.name': '名称', + 'settings.decisionProviders.baseUrl': '基础 URL', + 'settings.decisionProviders.apiFormat': 'API 格式', + 'settings.decisionProviders.apiKey': 'API 密钥', + 'settings.decisionProviders.apiKeyOptional': '回环地址上的 Ollama 可不填', + 'settings.decisionProviders.models': '模型', + 'settings.decisionProviders.modelsPlaceholder': '每行一个模型 ID', + 'settings.decisionProviders.fetchModels': '拉取模型', + 'settings.decisionProviders.local': '本地', + 'settings.decisionProviders.hosted': '托管', + 'settings.decisionProviders.confirmDelete': '删除权限 Guardrail 服务商“{name}”?', + 'settings.permissions.title': '权限模式', 'settings.permissions.description': '新会话开始时使用的模式。每个会话可以在输入框旁切换自己的模式。', + 'settings.permissions.rulesTitle': '显式工具规则', + 'settings.permissions.rulesDescription': '在下方文件中管理项目的 Allow、Ask 和 Deny 规则。这些确定性规则的优先级高于 bypass 模式和权限 Guardrail。', + 'settings.permissions.rulesFile': '项目规则文件', + 'settings.permissions.rulesShellNames': '规则中的工具名必须与工具卡一致。Windows 使用 PowerShell(...);跨平台项目应同时包含 Bash(...) 和 PowerShell(...)。', + 'settings.permissions.rulesReload': '编辑文件后请新建会话,以重新加载规则。', + 'settings.permissionGuardrail.title': '权限 Guardrail', + 'settings.permissionGuardrail.description': '在确定性权限规则执行后,可选用独立决策模型减少提示,或增加一道安全检查。', + 'settings.permissionGuardrail.enabled': '启用权限 Guardrail', + 'settings.permissionGuardrail.provider': '服务商', + 'settings.permissionGuardrail.selectProvider': '选择服务商', + 'settings.permissionGuardrail.model': '模型', + 'settings.permissionGuardrail.selectModel': '选择模型', + 'settings.permissionGuardrail.preset': '行为', + 'settings.permissionGuardrail.safetyNet': '安全网', + 'settings.permissionGuardrail.fewerPrompts': '减少提示', + 'settings.permissionGuardrail.threshold': '置信阈值', + 'settings.permissionGuardrail.calibrationRequired': '在 SABER 开发集上为该服务商和模型校准阈值之前,“减少提示”只进行观察,不会自动批准。', + 'settings.permissionGuardrail.experimentNote': '决策模型分数不能跨模型复用。阈值只在 SABER dev 上选择,test 仅用于一次最终报告。', + 'settings.permissionGuardrail.privacyNotice': '我理解用户请求、近期工具上下文、工具输入和工作目录可能会发送给该托管服务商,用于权限判定。', 'settings.browserTrust.title': '已信任的网站', 'settings.browserTrust.description': 'Agent 在侧边浏览器里操作这些网站时不再询问。只接受精确的 https 来源。读取页面不需要授权。', 'settings.browserTrust.inputLabel': '要信任的网站', @@ -1126,6 +1164,11 @@ export const zh: Record = { 'chat.compactSummary.trigger.auto': '自动', 'chat.compactSummary.tokens': '压缩前 {count} tokens', 'chat.compactSummary.messages': '已摘要 {count} 条消息', + 'chat.permissionGuardrail.autoApproved': '权限护栏已自动批准', + 'chat.permissionGuardrail.confirmationApproved': '权限护栏请求确认后已批准', + 'chat.permissionGuardrail.decision': '判定:{decision}', + 'chat.permissionGuardrail.score': '分数:{score}', + 'chat.permissionGuardrail.scoreUnavailable': '分数:不可用', 'chat.activeGoal.title': '当前目标', 'chat.activeGoal.running': '自循环运行中', 'chat.activeGoal.active': '进行中', diff --git a/desktop/src/pages/Settings.browserTrust.test.tsx b/desktop/src/pages/Settings.browserTrust.test.tsx index c94fb707..2e23588a 100644 --- a/desktop/src/pages/Settings.browserTrust.test.tsx +++ b/desktop/src/pages/Settings.browserTrust.test.tsx @@ -2,23 +2,37 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' -vi.mock('../components/settings/BrowserTrustSettings', () => ({ BrowserTrustSettings: () =>
trusted sites section
})) vi.mock('../api/computerUse', () => ({ computerUseApi: { getConfig: vi.fn().mockResolvedValue({ enabled: false }) } })) import { Settings } from './Settings' import { useSettingsStore } from '../stores/settingsStore' import { useUIStore } from '../stores/uiStore' import { ELECTRON_BRIDGE_KEY } from '../lib/shell/electronBridge' +import { useDecisionProviderStore } from '../stores/decisionProviderStore' function setDesktop(enabled: boolean) { const target = window as unknown as Record - if (enabled) target[ELECTRON_BRIDGE_KEY] = {} + if (enabled) target[ELECTRON_BRIDGE_KEY] = { invoke: vi.fn().mockResolvedValue([]) } else delete target[ELECTRON_BRIDGE_KEY] } describe('Settings permissions tab', () => { beforeEach(() => { - useSettingsStore.setState({ locale: 'en' }) + useSettingsStore.setState({ + locale: 'en', + permissionGuardrail: { + enabled: false, + providerId: null, + modelId: '', + preset: 'safetyNet', + threshold: 0.95, + privacyNoticeVersion: 0, + }, + }) + useDecisionProviderStore.setState({ + providers: [], + fetchProviders: vi.fn(async () => {}), + }) useUIStore.setState({ settingsTab: 'permissions' }) }) @@ -27,18 +41,36 @@ describe('Settings permissions tab', () => { setDesktop(false) useSettingsStore.setState(useSettingsStore.getInitialState(), true) useUIStore.setState(useUIStore.getInitialState(), true) + useDecisionProviderStore.setState(useDecisionProviderStore.getInitialState(), true) }) - it('shows the trusted sites section in the desktop app', () => { + it('shows the trusted sites section in the desktop app', async () => { setDesktop(true) render() - expect(screen.getByText('trusted sites section')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Trusted sites' })).toBeInTheDocument() + expect(await screen.findByText('No trusted sites. The agent asks the first time it operates each site.')).toBeInTheDocument() }) it('hides the trusted sites section outside the desktop app', () => { render() - expect(screen.queryByText('trusted sites section')).not.toBeInTheDocument() + expect(screen.queryByRole('heading', { name: 'Trusted sites' })).not.toBeInTheDocument() + }) + + it('shows the permission guardrail disabled by default', () => { + render() + + expect(screen.getByRole('heading', { name: 'Permission guardrail' })).toBeInTheDocument() + expect(screen.getByRole('switch', { name: 'Enable permission guardrail' })).not.toBeChecked() + }) + + it('shows the project rule configuration path and reload guidance', () => { + render() + + expect(screen.getByRole('heading', { name: 'Explicit tool rules' })).toBeInTheDocument() + expect(screen.getByText('.claude/settings.local.json')).toBeInTheDocument() + expect(screen.getByText(/On Windows use PowerShell/)).toBeInTheDocument() + expect(screen.getByText(/Start a new session after editing/)).toBeInTheDocument() }) }) diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 1844178a..8d67beee 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -43,6 +43,12 @@ import { AboutSettings } from '../components/settings/AboutSettings' import { Button } from '../components/shared/ui/button' import { SearchField } from '../components/shared/SearchField' import { moveRovingFocus } from '../lib/rovingFocus' +import { useDecisionProviderStore } from '../stores/decisionProviderStore' +import type { PermissionGuardrailSettings as PermissionGuardrailSettingsValue } from '../types/settings' +import { FormSelect } from '../components/shared/FormSelect' +import { LabeledInput } from '../components/shared/LabeledInput' +import { Switch } from '../components/shared/ui/switch' +import { Checkbox } from '../components/shared/ui/checkbox' type SettingsNavGroup = { titleKey: TranslationKey | null @@ -259,11 +265,182 @@ function PermissionSettings() { })} +
+

{t('settings.permissions.rulesTitle')}

+

+ {t('settings.permissions.rulesDescription')} +

+
+ {t('settings.permissions.rulesFile')} + + .claude/settings.local.json + +
+

+ {t('settings.permissions.rulesShellNames')} +

+

+ {t('settings.permissions.rulesReload')} +

+
+ + + {isDesktopRuntime() ? : null} ) } +function PermissionGuardrailSettings() { + const t = useTranslation() + const saved = useSettingsStore((state) => state.permissionGuardrail) + const save = useSettingsStore((state) => state.setPermissionGuardrail) + const providers = useDecisionProviderStore((state) => state.providers) + const fetchProviders = useDecisionProviderStore((state) => state.fetchProviders) + const [draft, setDraft] = useState(saved) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + void fetchProviders() + }, [fetchProviders]) + + useEffect(() => { + setDraft(saved) + }, [saved]) + + const provider = providers.find((entry) => entry.id === draft.providerId) ?? null + const privacyAccepted = draft.privacyNoticeVersion >= 1 + const saveDisabled = saving || ( + draft.enabled && ( + !provider || + !draft.modelId || + (provider.deployment === 'hosted' && !privacyAccepted) + ) + ) + + const update = ( + key: K, + value: PermissionGuardrailSettingsValue[K], + ) => setDraft((current) => ({ ...current, [key]: value })) + + return ( +
+
+
+

{t('settings.permissionGuardrail.title')}

+

+ {t('settings.permissionGuardrail.description')} +

+
+ update('enabled', checked)} + aria-label={t('settings.permissionGuardrail.enabled')} + /> +
+ +
+
+ + { + const next = providers.find((entry) => entry.id === providerId) + setDraft((current) => ({ + ...current, + providerId: providerId || null, + modelId: next?.models[0]?.id ?? '', + privacyNoticeVersion: next?.deployment === 'local' ? 1 : 0, + })) + }} + label={t('settings.permissionGuardrail.provider')} + options={[ + { value: '', label: t('settings.permissionGuardrail.selectProvider') }, + ...providers.map((entry) => ({ value: entry.id, label: entry.name })), + ]} + /> +
+
+ + update('modelId', modelId)} + label={t('settings.permissionGuardrail.model')} + options={[ + { value: '', label: t('settings.permissionGuardrail.selectModel') }, + ...(provider?.models ?? []).map((model) => ({ value: model.id, label: model.name ?? model.id })), + ]} + /> +
+
+ + update('preset', preset)} + label={t('settings.permissionGuardrail.preset')} + options={[ + { value: 'safetyNet', label: t('settings.permissionGuardrail.safetyNet') }, + { value: 'fewerPrompts', label: t('settings.permissionGuardrail.fewerPrompts') }, + ]} + /> +
+ update('threshold', Math.min(1, Math.max(0, Number(event.target.value))))} + /> +
+ + {draft.preset === 'fewerPrompts' && ( +

+ {t('settings.permissionGuardrail.calibrationRequired')} +

+ )} + +

+ {t('settings.permissionGuardrail.experimentNote')} +

+ + {provider?.deployment === 'hosted' && ( + + )} + +
+ + {error && {error}} +
+
+ ) +} + function PluginSettings() { const selectedPlugin = usePluginStore((s) => s.selectedPlugin) const t = useTranslation() diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index 4e8831ac..b07e3869 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -2879,6 +2879,13 @@ describe('chatStore permission requests shared across clients', () => { toolName: 'Bash', toolUseId: 'tool-shared-1', input: { command: 'ls' }, + guardrail: { + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet' as const, + decision: 'ask' as const, + score: 0.812, + }, } beforeEach(() => { @@ -2903,6 +2910,10 @@ describe('chatStore permission requests shared across clients', () => { expect(session().messages.filter((message) => message.type === 'permission_request')).toHaveLength(1) expect(notifyDesktopMock).toHaveBeenCalledTimes(1) expect(session().pendingPermission?.requestId).toBe('perm-shared-1') + expect(session().pendingPermission?.guardrail).toEqual(request.guardrail) + expect(session().messages.find((message) => message.type === 'permission_request')).toMatchObject({ + guardrail: request.guardrail, + }) expect(session().chatState).toBe('permission_pending') }) diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 8f99290a..02ee52f7 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -27,6 +27,7 @@ import type { UIAttachment, UIMessage, PermissionResolution, + PermissionGuardrailDetails, ServerMessage, SessionState, TokenUsage, @@ -68,6 +69,7 @@ export type PerSessionState = { toolUseId?: string input: unknown description?: string + guardrail?: PermissionGuardrailDetails } | null tokenUsage: TokenUsage statusVerb: string @@ -1744,6 +1746,7 @@ function pendingPermissionFromMirror(state: SessionState): PerSessionState['pend toolUseId: request.toolUseId, input: request.input, description: request.description, + guardrail: request.guardrail, } } @@ -1775,6 +1778,7 @@ function applyMirror(sessionId: string, next: SessionState): void { toolUseId: request.toolUseId, input: request.input, description: request.description, + guardrail: request.guardrail, timestamp: now, }) } diff --git a/desktop/src/stores/decisionProviderStore.ts b/desktop/src/stores/decisionProviderStore.ts new file mode 100644 index 00000000..1f21877b --- /dev/null +++ b/desktop/src/stores/decisionProviderStore.ts @@ -0,0 +1,96 @@ +import { create } from 'zustand' +import { providersApi } from '../api/providers' +import type { + CreateDecisionProviderInput, + DecisionProviderPreset, + DiscoverDecisionProviderModelsInput, + DiscoveredProviderModel, + ProviderTestResult, + SavedDecisionProvider, + TestDecisionProviderInput, + UpdateDecisionProviderInput, +} from '../types/provider' + +type DecisionProviderStore = { + providers: SavedDecisionProvider[] + activeId: string | null + presets: DecisionProviderPreset[] + isLoading: boolean + error: string | null + fetchProviders: () => Promise + fetchPresets: () => Promise + createProvider: (input: CreateDecisionProviderInput) => Promise + updateProvider: (id: string, input: UpdateDecisionProviderInput) => Promise + deleteProvider: (id: string) => Promise + activateProvider: (id: string) => Promise + testProvider: (id: string, modelId?: string) => Promise + testConfig: (input: TestDecisionProviderInput) => Promise + discoverModels: (input: DiscoverDecisionProviderModelsInput) => Promise +} + +export const useDecisionProviderStore = create((set, get) => ({ + providers: [], + activeId: null, + presets: [], + isLoading: false, + error: null, + + fetchProviders: async () => { + set({ isLoading: true, error: null }) + try { + const { providers, activeId } = await providersApi.listDecisions() + set({ providers, activeId, isLoading: false }) + } catch (error) { + set({ + isLoading: false, + error: error instanceof Error ? error.message : String(error), + }) + } + }, + + fetchPresets: async () => { + try { + const { presets } = await providersApi.decisionPresets() + set({ presets }) + } catch (error) { + set({ error: error instanceof Error ? error.message : String(error) }) + } + }, + + createProvider: async (input) => { + const { provider } = await providersApi.createDecision(input) + await get().fetchProviders() + return provider + }, + + updateProvider: async (id, input) => { + const { provider } = await providersApi.updateDecision(id, input) + await get().fetchProviders() + return provider + }, + + deleteProvider: async (id) => { + await providersApi.deleteDecision(id) + await get().fetchProviders() + }, + + activateProvider: async (id) => { + await providersApi.activateDecision(id) + await get().fetchProviders() + }, + + testProvider: async (id, modelId) => { + const { result } = await providersApi.testDecision(id, modelId) + return result + }, + + testConfig: async (input) => { + const { result } = await providersApi.testDecisionConfig(input) + return result + }, + + discoverModels: async (input) => { + const { models } = await providersApi.discoverDecisionModels(input) + return models + }, +})) diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts index 675dc6bf..9a5b4b27 100644 --- a/desktop/src/stores/settingsStore.ts +++ b/desktop/src/stores/settingsStore.ts @@ -17,6 +17,7 @@ import { type H5AccessDiagnostics, type H5AccessSettings, type NetworkSettings, + type PermissionGuardrailSettings, type DefaultPermissionMode, type EffortLevel, type ModelInfo, @@ -74,6 +75,7 @@ function getStoredOnboardingCompleted(): boolean { type SettingsStore = { permissionMode: DefaultPermissionMode + permissionGuardrail: PermissionGuardrailSettings currentModel: ModelInfo | null effortLevel: EffortLevel thinkingEnabled: boolean @@ -105,6 +107,7 @@ type SettingsStore = { fetchAll: () => Promise fetchH5Access: () => Promise setPermissionMode: (mode: DefaultPermissionMode) => Promise + setPermissionGuardrail: (settings: PermissionGuardrailSettings) => Promise setModel: (modelId: string) => Promise setEffort: (level: EffortLevel) => Promise setThinkingEnabled: (enabled: boolean) => Promise @@ -163,8 +166,38 @@ const DEFAULT_NETWORK_SETTINGS: NetworkSettings = { }, } +export const DEFAULT_PERMISSION_GUARDRAIL_SETTINGS: PermissionGuardrailSettings = { + enabled: false, + providerId: null, + modelId: '', + preset: 'safetyNet', + threshold: 0.95, + privacyNoticeVersion: 0, +} + +function normalizePermissionGuardrailSettings(value: unknown): PermissionGuardrailSettings { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return DEFAULT_PERMISSION_GUARDRAIL_SETTINGS + } + const settings = value as Partial + const threshold = typeof settings.threshold === 'number' && settings.threshold >= 0 && settings.threshold <= 1 + ? settings.threshold + : DEFAULT_PERMISSION_GUARDRAIL_SETTINGS.threshold + return { + enabled: settings.enabled === true, + providerId: typeof settings.providerId === 'string' ? settings.providerId : null, + modelId: typeof settings.modelId === 'string' ? settings.modelId : '', + preset: settings.preset === 'fewerPrompts' ? 'fewerPrompts' : 'safetyNet', + threshold, + privacyNoticeVersion: typeof settings.privacyNoticeVersion === 'number' + ? Math.max(0, Math.floor(settings.privacyNoticeVersion)) + : 0, + } +} + export const useSettingsStore = create((set, get) => ({ permissionMode: 'default', + permissionGuardrail: DEFAULT_PERMISSION_GUARDRAIL_SETTINGS, currentModel: null, effortLevel: 'medium', thinkingEnabled: true, @@ -225,6 +258,7 @@ export const useSettingsStore = create((set, get) => ({ useUIStore.getState().setTheme(theme) set({ permissionMode: mode, + permissionGuardrail: normalizePermissionGuardrailSettings(userSettings.permissionGuardrail), availableModels: modelsRes.models, activeProviderName: modelsRes.provider?.name ?? null, currentModel: model, @@ -274,6 +308,17 @@ export const useSettingsStore = create((set, get) => ({ } }, + setPermissionGuardrail: async (settings) => { + const previous = get().permissionGuardrail + set({ permissionGuardrail: settings }) + try { + await settingsApi.updateUser({ permissionGuardrail: settings }) + } catch (error) { + set({ permissionGuardrail: previous }) + throw error + } + }, + setModel: async (modelId) => { await modelsApi.setCurrent(modelId) const { model } = await modelsApi.getCurrent() diff --git a/desktop/src/types/chat.ts b/desktop/src/types/chat.ts index 8a37858e..354daea2 100644 --- a/desktop/src/types/chat.ts +++ b/desktop/src/types/chat.ts @@ -1,4 +1,9 @@ -import type { TaskNotification, TaskUsage, TranscriptItem } from '@protocol' +import type { + PermissionGuardrailDetails, + TaskNotification, + TaskUsage, + TranscriptItem, +} from '@protocol' export type { AttachmentRef, @@ -15,6 +20,7 @@ export type { TeamMemberStatus, TokenUsage, } from '@protocol' +export type { PermissionGuardrailDetails } import type { ClientKind, GoalEventAction } from '@protocol' export type PermissionResolution = { @@ -107,6 +113,7 @@ export type UIMessage = | SharedItem<'compact_summary'> | SharedItem<'goal_event'> | SharedItem<'memory_event'> + | SharedItem<'permission_guardrail_decision'> | { id: string type: 'permission_request' @@ -115,6 +122,7 @@ export type UIMessage = toolUseId?: string input: unknown description?: string + guardrail?: PermissionGuardrailDetails timestamp: number resolution?: PermissionResolution } diff --git a/desktop/src/types/provider.ts b/desktop/src/types/provider.ts index 2e4b40f7..9247e075 100644 --- a/desktop/src/types/provider.ts +++ b/desktop/src/types/provider.ts @@ -14,6 +14,52 @@ export type ProviderModel = { name?: string } +export type DecisionProviderFormat = + | 'openai_decisions' + | 'openrouter_decisions' + | 'openai_chat' + | 'ollama_chat' + +export type DecisionProviderPresetId = 'openai' | 'openrouter' | 'ollama' | 'custom' + +export type SavedDecisionProvider = { + id: string + presetId: DecisionProviderPresetId + name: string + deployment: 'hosted' | 'local' + apiFormat: DecisionProviderFormat + baseUrl: string + apiKey: string + models: ProviderModel[] +} + +export type DecisionProviderPreset = { + id: DecisionProviderPresetId + name: string + baseUrl: string + apiFormat: DecisionProviderFormat + models: ProviderModel[] +} + +export type CreateDecisionProviderInput = Omit< + SavedDecisionProvider, + 'id' | 'deployment' +> + +export type UpdateDecisionProviderInput = Partial + +export type TestDecisionProviderInput = { + baseUrl: string + apiKey: string + modelId: string + apiFormat: DecisionProviderFormat +} + +export type DiscoverDecisionProviderModelsInput = Omit< + CreateDecisionProviderInput, + 'name' | 'models' +> + export type DiscoveredProviderModel = ProviderModel & { contextWindow?: number } export type ModelContextWindows = Record diff --git a/desktop/src/types/settings.ts b/desktop/src/types/settings.ts index 5ddb438d..95d5ccc9 100644 --- a/desktop/src/types/settings.ts +++ b/desktop/src/types/settings.ts @@ -91,6 +91,7 @@ export type UserSettings = { alwaysThinkingEnabled?: boolean autoCompactPercent?: number permissionMode?: PermissionMode + permissionGuardrail?: PermissionGuardrailSettings theme?: ThemeMode skipWebFetchPreflight?: boolean desktopNotificationsEnabled?: boolean @@ -108,6 +109,15 @@ export type UserSettings = { [key: string]: unknown } +export type PermissionGuardrailSettings = { + enabled: boolean + providerId: string | null + modelId: string + preset: 'fewerPrompts' | 'safetyNet' + threshold: number + privacyNoticeVersion: number +} + export type AppMode = 'default' | 'portable' export type AppModeConfig = { diff --git a/experiments/system1-guardrail/eval/README.md b/experiments/system1-guardrail/eval/README.md index f3156cee..e1afe4cc 100644 --- a/experiments/system1-guardrail/eval/README.md +++ b/experiments/system1-guardrail/eval/README.md @@ -14,6 +14,7 @@ differences come from the model: | [`policy.ts`](policy.ts) | The shared policy, the yes/no phrasings for decision models, and the chat models' system prompt and answer schema. | | [`chat.ts`](chat.ts) | Chat models through OpenRouter: `estimate`, `run` (real time, for small samples), `submit` / `fetch` (Batch API, resumable). | | [`decisions.ts`](decisions.ts) | Decision models through OpenRouter's Decisions endpoint (Jev, d1-ultra, Mercury Decide, Clef, Kev, Solar Decide). | +| [`openai-decisions.ts`](openai-decisions.ts) | OpenAI Decisions API with `gpt-6-luna` and a separate model-specific threshold. | | [`export.ts`](export.ts) | Writes the inputs for a local model; [`../local-models/`](../local-models/) scores them. | | [`score.ts`](score.ts) | AUROC, calibration, false-allow rate and coverage at thresholds set on a dev run, per category and injection, and latency. | | [`lib.ts`](lib.ts) | Which records are scored, run files, and the spending ledger. | @@ -32,6 +33,9 @@ bun experiments/system1-guardrail/eval/chat.ts fetch --run saber-test-claude-hai # decision model, real time bun experiments/system1-guardrail/eval/decisions.ts --model liquid/d1-ultra --split test --view full --budget 1500 +# OpenAI decision model, dev threshold selection first +OPENAI_API_KEY=... bun experiments/system1-guardrail/eval/openai-decisions.ts --split dev --cohort ask --view full --budget 1500 + # local model: export, then score with ../local-models bun experiments/system1-guardrail/eval/export.ts --model laya --split test --view full --budget 1500 uv run --project experiments/system1-guardrail/local-models experiments/system1-guardrail/local-models/run.py --run saber-test-laya-full-b1500-q0 @@ -45,12 +49,16 @@ Run names are printed by each command. `--exact-only` scores only exactly matched harmful labels; `--limit N` takes the same N records for every candidate. +For OpenAI, score the `dev` run first and record the selected threshold. Run +the matching `test` command once for the final report. Do not reuse thresholds +from d1-ultra, Jev, or Claude for `gpt-6-luna`. + ## Files and money - Runs are written to `runs//` (gitignored): `meta.json` (model, view, prompt hash, dataset hashes), `predictions.jsonl` with a probability per record, and `score.json`. -- Every hosted run records its provider-reported cost in `runs/ledger.jsonl`. +- OpenRouter hosted runs record their provider-reported cost in `runs/ledger.jsonl`. A run that would take the total over `--cap` (default $8, counting batches still in flight) is refused. A key or credit limit stops a run instead of writing failures. diff --git a/experiments/system1-guardrail/eval/lib.ts b/experiments/system1-guardrail/eval/lib.ts index 2f17c309..a3b53363 100644 --- a/experiments/system1-guardrail/eval/lib.ts +++ b/experiments/system1-guardrail/eval/lib.ts @@ -9,6 +9,7 @@ export const RUNS_DIR = join(import.meta.dir, 'runs') export const LEDGER = join(RUNS_DIR, 'ledger.jsonl') export type Source = 'saber' | 'private' +export type EvalCohort = 'ask' | 'allow' | 'all' export function dataDir(source: Source): string { return source === 'saber' ? join(DATASET_DIR, 'data', 'saber') : join(DATASET_DIR, 'private') @@ -18,9 +19,15 @@ export function dataDir(source: Source): string { * The records a guardrail is scored on: labeled calls the current rules * would ask about. Everything else the rules already decide. */ -export function evalRecords(source: Source, split: Split, { withContext = true } = {}): ToolCallRecord[] { +export function evalRecords( + source: Source, + split: Split, + { withContext = true, cohort = 'ask' }: { withContext?: boolean; cohort?: EvalCohort } = {}, +): ToolCallRecord[] { return loadRecords(dataDir(source), { withContext }).filter( - r => r.split === split && r.rules_decision === 'ask' && r.label !== null, + r => r.split === split && + (cohort === 'all' || r.rules_decision === cohort) && + r.label !== null, ) } diff --git a/experiments/system1-guardrail/eval/openai-decisions.ts b/experiments/system1-guardrail/eval/openai-decisions.ts new file mode 100644 index 00000000..97effdc6 --- /dev/null +++ b/experiments/system1-guardrail/eval/openai-decisions.ts @@ -0,0 +1,132 @@ +import { existsSync } from 'fs' +import { join } from 'path' +import { parseFlags, sha256, type Split } from '../dataset/lib' +import { renderState, type View, VIEWS } from './input' +import { + datasetHashes, + evalRecords, + type EvalCohort, + type Prediction, + runDir, + writePredictions, + writeRunMeta, +} from './lib' +import { promptHash, QUESTIONS, questionText } from './policy' + +const flags = parseFlags(process.argv.slice(2)) +const model = flags.get('model') ?? 'gpt-6-luna' +const split = (flags.get('split') ?? 'dev') as Split +const view = (flags.get('view') ?? 'full') as View +const cohort = (flags.get('cohort') ?? 'ask') as EvalCohort +const budget = flags.has('budget') && flags.get('budget') !== 'none' + ? Number(flags.get('budget')) + : 1500 +const question = Number(flags.get('question') ?? 1) +const limit = flags.has('limit') ? Number(flags.get('limit')) : null + +if (!VIEWS.includes(view)) throw new Error(`--view must be one of ${VIEWS.join(', ')}`) +if (!['ask', 'allow', 'all'].includes(cohort)) throw new Error('--cohort must be ask, allow or all') +const selectedQuestion = QUESTIONS[question] +if (!selectedQuestion) throw new Error(`--question must be 0..${QUESTIONS.length - 1}`) + +let records = evalRecords('saber', split, { cohort }) +if (limit !== null) { + records = records + .sort((left, right) => sha256(left.id).localeCompare(sha256(right.id))) + .slice(0, limit) +} + +const name = [ + 'saber', + split, + 'openai', + model, + cohort, + view, + `b${budget}`, + `q${question}`, + ...(limit === null ? [] : [`n${limit}`]), + ...(flags.has('tag') ? [flags.get('tag')!] : []), +].join('-') + +if (existsSync(join(runDir(name), 'predictions.jsonl')) && !flags.has('force')) { + console.log(JSON.stringify({ run: name, skipped: 'already done; pass --force to run it again' })) + process.exit(0) +} + +const apiKey = process.env.OPENAI_API_KEY +if (!apiKey) throw new Error('OPENAI_API_KEY is not set') +const instructions = questionText(question) + +writeRunMeta({ + name, + candidate: 'openai-decisions', + model, + source: 'saber', + split, + cohort, + view, + budget_chars: budget, + phrasing: question, + prompt_sha256: promptHash(instructions), + dataset: datasetHashes('saber'), + records: records.length, + created_at: new Date().toISOString(), +}) + +async function decide(input: string): Promise { + const response = await fetch('https://api.openai.com/v1/decisions', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model, + input, + questions: [{ type: 'predicate', name: 'decision', instructions }], + }), + signal: AbortSignal.timeout(30_000), + }) + const text = await response.text() + if (!response.ok) throw new Error(`HTTP ${response.status}`) + const payload = JSON.parse(text) as { answers?: Array<{ name?: string; probability?: number }> } + const probability = payload.answers?.find((answer) => answer.name === 'decision')?.probability + if (typeof probability !== 'number' || probability < 0 || probability > 1) { + throw new Error('Invalid predicate probability') + } + return selectedQuestion.yes === 'ask' ? probability : 1 - probability +} + +const predictions: Prediction[] = new Array(records.length) +let next = 0 +const workers = Array.from({ length: Number(flags.get('concurrency') ?? 8) }, async () => { + while (next < records.length) { + const index = next++ + const record = records[index]! + const startedAt = performance.now() + try { + predictions[index] = { + id: record.id, + p_ask: await decide(renderState(record, view, budget)), + decision: null, + latency_ms: Math.round(performance.now() - startedAt), + } + } catch (error) { + predictions[index] = { + id: record.id, + p_ask: null, + decision: null, + error: error instanceof Error ? error.message : String(error), + } + } + } +}) + +await Promise.all(workers) +writePredictions(name, predictions) +console.log(JSON.stringify({ + run: name, + records: predictions.length, + failed: predictions.filter((prediction) => prediction.p_ask === null).length, +})) diff --git a/experiments/system1-guardrail/eval/score.ts b/experiments/system1-guardrail/eval/score.ts index 011628a8..5314c0d1 100644 --- a/experiments/system1-guardrail/eval/score.ts +++ b/experiments/system1-guardrail/eval/score.ts @@ -17,7 +17,7 @@ import { writeFileSync } from 'fs' import { join } from 'path' import { loadRecords, parseFlags, type Split, type ToolCallRecord } from '../dataset/lib' -import { dataDir, evalRecords, type Prediction, readPredictions, readRunMeta, runDir, type Source } from './lib' +import { dataDir, evalRecords, type EvalCohort, type Prediction, readPredictions, readRunMeta, runDir, type Source } from './lib' const flags = parseFlags(process.argv.slice(2)) @@ -31,7 +31,8 @@ function join_(name: string): Row[] { // otherwise one failure would invent a decision point for a probability-only model. const givesDecisions = all.some(p => p.decision !== null) const exactOnly = flags.has('exact-only') - return evalRecords(meta.source, meta.split, { withContext: false }).flatMap(record => { + const cohort = typeof meta.cohort === 'string' ? meta.cohort as EvalCohort : 'ask' + return evalRecords(meta.source, meta.split, { withContext: false, cohort }).flatMap(record => { if (exactOnly && record.label === 'ask' && record.meta.judge_match !== 'exact') return [] const p = predictions.get(record.id) if (!p) return [] diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index 2e6e7660..60890a6c 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -859,6 +859,22 @@ export class QueryEngine { if (message.attachment.type === 'structured_output') { structuredOutputFromTool = message.attachment.data } + else if ( + message.attachment.type === 'permission_guardrail_decision' + ) { + yield { + type: 'system', + subtype: 'permission_guardrail_decision', + decision: message.attachment.decision, + tool_use_id: message.attachment.toolUseID, + provider_id: message.attachment.providerId, + model_id: message.attachment.modelId, + preset: message.attachment.preset, + score: message.attachment.score, + uuid: message.uuid, + session_id: getSessionId(), + } as SDKMessage + } // Handle max turns reached signal from query.ts else if (message.attachment.type === 'max_turns_reached') { if (persistSession) { diff --git a/src/cli/structuredIO.permissionGuardrail.test.ts b/src/cli/structuredIO.permissionGuardrail.test.ts new file mode 100644 index 00000000..26d2106b --- /dev/null +++ b/src/cli/structuredIO.permissionGuardrail.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from 'bun:test' +import type { PermissionDecision } from '../utils/permissions/PermissionResult.js' +import { + preservePermissionGuardrailReason, + serializePermissionGuardrail, +} from './structuredIO.js' + +describe('structured permission guardrail response', () => { + test('preserves the guardrail decision after desktop approval', () => { + const source: PermissionDecision = { + behavior: 'ask', + message: 'Confirm', + decisionReason: { + type: 'permissionGuardrail', + providerId: 'provider', + modelId: 'model', + preset: 'safetyNet', + decision: 'ask', + score: 0.98, + }, + } + const response: PermissionDecision = { + behavior: 'allow', + updatedInput: {}, + } + + expect(preservePermissionGuardrailReason(source, response)).toEqual({ + ...response, + decisionReason: source.decisionReason, + }) + }) + + test('keeps ordinary permission responses unchanged', () => { + const source: PermissionDecision = { + behavior: 'ask', + message: 'Confirm', + decisionReason: { type: 'mode', mode: 'default' }, + } + const response: PermissionDecision = { + behavior: 'allow', + updatedInput: {}, + } + + expect(preservePermissionGuardrailReason(source, response)).toBe(response) + }) + + test('serializes guardrail details for the permission request', () => { + expect(serializePermissionGuardrail({ + type: 'permissionGuardrail', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet', + decision: 'ask', + score: 0.812, + })).toEqual({ + provider_id: 'provider-1', + model_id: 'gpt-6-luna', + preset: 'safetyNet', + decision: 'ask', + score: 0.812, + }) + }) +}) diff --git a/src/cli/structuredIO.ts b/src/cli/structuredIO.ts index 366b56f5..445e17eb 100644 --- a/src/cli/structuredIO.ts +++ b/src/cli/structuredIO.ts @@ -79,6 +79,7 @@ function serializeDecisionReason( case 'mode': case 'subcommandResults': case 'permissionPromptTool': + case 'permissionGuardrail': return undefined case 'hook': case 'asyncAgent': @@ -90,6 +91,28 @@ function serializeDecisionReason( } } +export function preservePermissionGuardrailReason( + source: PermissionDecision, + response: PermissionDecision, +): PermissionDecision { + return source.decisionReason?.type === 'permissionGuardrail' + ? { ...response, decisionReason: source.decisionReason } + : response +} + +export function serializePermissionGuardrail( + reason: PermissionDecisionReason | undefined, +) { + if (reason?.type !== 'permissionGuardrail') return undefined + return { + provider_id: reason.providerId, + model_id: reason.modelId, + preset: reason.preset, + decision: reason.decision, + score: reason.score, + } +} + function buildRequiresActionDetails( tool: Tool, input: Record, @@ -558,6 +581,16 @@ export class StructuredIO { return mainPermissionResult } + const normalizePermissionResponse = (result: PermissionToolOutput) => { + const decision = permissionPromptToolResultToPermissionDecision( + result, + tool, + input, + toolUseContext, + ) + return preservePermissionGuardrailReason(mainPermissionResult, decision) + } + // Run PermissionRequest hooks in parallel with the SDK permission // prompt. In the terminal CLI, hooks race against the interactive // prompt so that e.g. a hook with --delay 20 doesn't block the UI. @@ -597,6 +630,9 @@ export class StructuredIO { decision_reason: serializeDecisionReason( mainPermissionResult.decisionReason, ), + permission_guardrail: serializePermissionGuardrail( + mainPermissionResult.decisionReason, + ), tool_use_id: toolUseID, agent_id: toolUseContext.agentId, }, @@ -620,33 +656,18 @@ export class StructuredIO { } // Hook passed through (no decision) — wait for the SDK prompt const sdkResult = await sdkPromise - return permissionPromptToolResultToPermissionDecision( - sdkResult.result, - tool, - input, - toolUseContext, - ) + return normalizePermissionResponse(sdkResult.result) } // SDK prompt responded first — use its result (hook still running // in background but its result will be ignored) - return permissionPromptToolResultToPermissionDecision( - winner.result, - tool, - input, - toolUseContext, - ) + return normalizePermissionResponse(winner.result) } catch (error) { - return permissionPromptToolResultToPermissionDecision( - { - behavior: 'deny', - message: `Tool permission request failed: ${error}`, - toolUseID, - }, - tool, - input, - toolUseContext, - ) + return normalizePermissionResponse({ + behavior: 'deny', + message: `Tool permission request failed: ${error}`, + toolUseID, + }) } finally { // Only transition back to 'running' if no other permission prompts // are pending (concurrent tool execution can have multiple in-flight). diff --git a/src/components/messages/AttachmentMessage.tsx b/src/components/messages/AttachmentMessage.tsx index efbb758e..cac5146c 100644 --- a/src/components/messages/AttachmentMessage.tsx +++ b/src/components/messages/AttachmentMessage.tsx @@ -331,6 +331,10 @@ export function AttachmentMessage({ {action} by {attachment.hookEvent} hook ; } + case 'permission_guardrail_decision': + return + {attachment.decision === 'allow' ? 'Allowed' : 'Confirmation requested'} by permission guardrail {attachment.modelId}{attachment.score === null ? ' (unavailable)' : ` (${Math.round(attachment.score * 100)}%)`} + ; case 'task_status': return ; case 'teammate_shutdown_batch': @@ -533,4 +537,4 @@ function Line(t0) { } return t3; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZU1lbW8iLCJBbnNpIiwiQm94IiwiVGV4dCIsIkF0dGFjaG1lbnQiLCJOdWxsUmVuZGVyaW5nQXR0YWNobWVudFR5cGUiLCJ1c2VBcHBTdGF0ZSIsImdldERpc3BsYXlQYXRoIiwiZm9ybWF0RmlsZVNpemUiLCJNZXNzYWdlUmVzcG9uc2UiLCJiYXNlbmFtZSIsInNlcCIsIlVzZXJUZXh0TWVzc2FnZSIsIkRpYWdub3N0aWNzRGlzcGxheSIsImdldENvbnRlbnRUZXh0IiwiVGhlbWUiLCJVc2VySW1hZ2VNZXNzYWdlIiwidG9JbmtDb2xvciIsImpzb25QYXJzZSIsInBsdXJhbCIsImlzRW52VHJ1dGh5IiwiaXNBZ2VudFN3YXJtc0VuYWJsZWQiLCJ0cnlSZW5kZXJQbGFuQXBwcm92YWxNZXNzYWdlIiwiZm9ybWF0VGVhbW1hdGVNZXNzYWdlQ29udGVudCIsIkJMQUNLX0NJUkNMRSIsIlRlYW1tYXRlTWVzc2FnZUNvbnRlbnQiLCJpc1NodXRkb3duQXBwcm92ZWQiLCJDdHJsT1RvRXhwYW5kIiwiRmlsZVBhdGhMaW5rIiwiZmVhdHVyZSIsInVzZVNlbGVjdGVkTWVzc2FnZUJnIiwiUHJvcHMiLCJhZGRNYXJnaW4iLCJhdHRhY2htZW50IiwidmVyYm9zZSIsImlzVHJhbnNjcmlwdE1vZGUiLCJBdHRhY2htZW50TWVzc2FnZSIsIlJlYWN0Tm9kZSIsImJnIiwiaXNEZW1vRW52IiwicHJvY2VzcyIsImVudiIsIklTX0RFTU8iLCJ0eXBlIiwidmlzaWJsZU1lc3NhZ2VzIiwibWVzc2FnZXMiLCJmaWx0ZXIiLCJtc2ciLCJ0ZXh0IiwicGFyc2VkIiwibGVuZ3RoIiwibWFwIiwiaWR4IiwicGFyc2VkTXNnIiwidGFza0lkIiwic3ViamVjdCIsImFzc2lnbmVkQnkiLCJmcm9tIiwicGxhbkFwcHJvdmFsRWxlbWVudCIsImlua0NvbG9yIiwiY29sb3IiLCJmb3JtYXR0ZWRDb250ZW50Iiwic3VtbWFyeSIsInNraWxscyIsIm5hbWVzIiwicyIsInNob3J0SWQiLCJuYW1lIiwiam9pbiIsImZpcnN0SWQiLCJoaW50IiwiZGlzcGxheVBhdGgiLCJjb250ZW50IiwiZmlsZSIsImNlbGxzIiwibnVtTGluZXMiLCJ0cnVuY2F0ZWQiLCJvcmlnaW5hbFNpemUiLCJwYWdlQ291bnQiLCJsaW5lRW5kIiwibGluZVN0YXJ0IiwiaWRlTmFtZSIsIm1lbW9yaWVzIiwibSIsInBhdGgiLCJza2lsbENvdW50Iiwic2tpbGxOYW1lcyIsImlzSW5pdGlhbCIsImFkZGVkVHlwZXMiLCJjb3VudCIsInByb21wdCIsImhhc0ltYWdlcyIsImltYWdlUGFzdGVJZHMiLCJpZCIsInBsYW5GaWxlUGF0aCIsInNlcnZlciIsImhvb2tFdmVudCIsInN0ZGVyciIsImJsb2NraW5nRXJyb3IiLCJ0cmltIiwiaG9va05hbWUiLCJtZXNzYWdlIiwiYWN0aW9uIiwiZGVjaXNpb24iLCJUYXNrU3RhdHVzQXR0YWNobWVudCIsIkV4dHJhY3QiLCJUYXNrU3RhdHVzTWVzc2FnZSIsInQwIiwiJCIsIl9jIiwic3RhdHVzIiwidGFza1R5cGUiLCJ0MSIsIkdlbmVyaWNUYXNrU3RhdHVzIiwic3RhdHVzVGV4dCIsIlN5bWJvbCIsImZvciIsInQyIiwiZGVzY3JpcHRpb24iLCJ0MyIsInQ0IiwiVGVhbW1hdGVUYXNrU3RhdHVzIiwidGFza3MiLCJ0YXNrIiwiaWRlbnRpdHkiLCJhZ2VudENvbG9yIiwiYWdlbnROYW1lIiwidDUiLCJ0NiIsIkxpbmUiLCJkaW1Db2xvciIsImNoaWxkcmVuIiwidW5kZWZpbmVkIl0sInNvdXJjZXMiOlsiQXR0YWNobWVudE1lc3NhZ2UudHN4Il0sInNvdXJjZXNDb250ZW50IjpbIi8vIGJpb21lLWlnbm9yZS1hbGwgYXNzaXN0L3NvdXJjZS9vcmdhbml6ZUltcG9ydHM6IEFOVC1PTkxZIGltcG9ydCBtYXJrZXJzIG11c3Qgbm90IGJlIHJlb3JkZXJlZFxuaW1wb3J0IFJlYWN0LCB7IHVzZU1lbW8gfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEFuc2ksIEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgQXR0YWNobWVudCB9IGZyb20gJ3NyYy91dGlscy9hdHRhY2htZW50cy5qcydcbmltcG9ydCB0eXBlIHsgTnVsbFJlbmRlcmluZ0F0dGFjaG1lbnRUeXBlIH0gZnJvbSAnLi9udWxsUmVuZGVyaW5nQXR0YWNobWVudHMuanMnXG5pbXBvcnQgeyB1c2VBcHBTdGF0ZSB9IGZyb20gJy4uLy4uL3N0YXRlL0FwcFN0YXRlLmpzJ1xuaW1wb3J0IHsgZ2V0RGlzcGxheVBhdGggfSBmcm9tICdzcmMvdXRpbHMvZmlsZS5qcydcbmltcG9ydCB7IGZvcm1hdEZpbGVTaXplIH0gZnJvbSAnc3JjL3V0aWxzL2Zvcm1hdC5qcydcbmltcG9ydCB7IE1lc3NhZ2VSZXNwb25zZSB9IGZyb20gJy4uL01lc3NhZ2VSZXNwb25zZS5qcydcbmltcG9ydCB7IGJhc2VuYW1lLCBzZXAgfSBmcm9tICdwYXRoJ1xuaW1wb3J0IHsgVXNlclRleHRNZXNzYWdlIH0gZnJvbSAnLi9Vc2VyVGV4dE1lc3NhZ2UuanMnXG5pbXBvcnQgeyBEaWFnbm9zdGljc0Rpc3BsYXkgfSBmcm9tICcuLi9EaWFnbm9zdGljc0Rpc3BsYXkuanMnXG5pbXBvcnQgeyBnZXRDb250ZW50VGV4dCB9IGZyb20gJ3NyYy91dGlscy9tZXNzYWdlcy5qcydcbmltcG9ydCB0eXBlIHsgVGhlbWUgfSBmcm9tICdzcmMvdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgeyBVc2VySW1hZ2VNZXNzYWdlIH0gZnJvbSAnLi9Vc2VySW1hZ2VNZXNzYWdlLmpzJ1xuaW1wb3J0IHsgdG9JbmtDb2xvciB9IGZyb20gJy4uLy4uL3V0aWxzL2luay5qcydcbmltcG9ydCB7IGpzb25QYXJzZSB9IGZyb20gJy4uLy4uL3V0aWxzL3Nsb3dPcGVyYXRpb25zLmpzJ1xuaW1wb3J0IHsgcGx1cmFsIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3RyaW5nVXRpbHMuanMnXG5pbXBvcnQgeyBpc0VudlRydXRoeSB9IGZyb20gJy4uLy4uL3V0aWxzL2VudlV0aWxzLmpzJ1xuaW1wb3J0IHsgaXNBZ2VudFN3YXJtc0VuYWJsZWQgfSBmcm9tICcuLi8uLi91dGlscy9hZ2VudFN3YXJtc0VuYWJsZWQuanMnXG5pbXBvcnQge1xuICB0cnlSZW5kZXJQbGFuQXBwcm92YWxNZXNzYWdlLFxuICBmb3JtYXRUZWFtbWF0ZU1lc3NhZ2VDb250ZW50LFxufSBmcm9tICcuL1BsYW5BcHByb3ZhbE1lc3NhZ2UuanMnXG5pbXBvcnQgeyBCTEFDS19DSVJDTEUgfSBmcm9tICcuLi8uLi9jb25zdGFudHMvZmlndXJlcy5qcydcbmltcG9ydCB7IFRlYW1tYXRlTWVzc2FnZUNvbnRlbnQgfSBmcm9tICcuL1VzZXJUZWFtbWF0ZU1lc3NhZ2UuanMnXG5pbXBvcnQgeyBpc1NodXRkb3duQXBwcm92ZWQgfSBmcm9tICcuLi8uLi91dGlscy90ZWFtbWF0ZU1haWxib3guanMnXG5pbXBvcnQgeyBDdHJsT1RvRXhwYW5kIH0gZnJvbSAnLi4vQ3RybE9Ub0V4cGFuZC5qcydcbmltcG9ydCB7IEZpbGVQYXRoTGluayB9IGZyb20gJy4uL0ZpbGVQYXRoTGluay5qcydcbmltcG9ydCB7IGZlYXR1cmUgfSBmcm9tICdidW46YnVuZGxlJ1xuaW1wb3J0IHsgdXNlU2VsZWN0ZWRNZXNzYWdlQmcgfSBmcm9tICcuLi9tZXNzYWdlQWN0aW9ucy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgYWRkTWFyZ2luOiBib29sZWFuXG4gIGF0dGFjaG1lbnQ6IEF0dGFjaG1lbnRcbiAgdmVyYm9zZTogYm9vbGVhblxuICBpc1RyYW5zY3JpcHRNb2RlPzogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gQXR0YWNobWVudE1lc3NhZ2Uoe1xuICBhdHRhY2htZW50LFxuICBhZGRNYXJnaW4sXG4gIHZlcmJvc2UsXG4gIGlzVHJhbnNjcmlwdE1vZGUsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGJnID0gdXNlU2VsZWN0ZWRNZXNzYWdlQmcoKVxuICAvLyBIb2lzdGVkIHRvIG1vdW50LXRpbWUg4oCUIHBlci1tZXNzYWdlIGNvbXBvbmVudCwgcmUtcmVuZGVycyBvbiBldmVyeSBzY3JvbGwuXG4gIGNvbnN0IGlzRGVtb0VudiA9IGZlYXR1cmUoJ0VYUEVSSU1FTlRBTF9TS0lMTF9TRUFSQ0gnKVxuICAgID8gLy8gYmlvbWUtaWdub3JlIGxpbnQvY29ycmVjdG5lc3MvdXNlSG9va0F0VG9wTGV2ZWw6IGZlYXR1cmUoKSBpcyBhIGNvbXBpbGUtdGltZSBjb25zdGFudFxuICAgICAgdXNlTWVtbygoKSA9PiBpc0VudlRydXRoeShwcm9jZXNzLmVudi5JU19ERU1PKSwgW10pXG4gICAgOiBmYWxzZVxuICAvLyBIYW5kbGUgdGVhbW1hdGVfbWFpbGJveCBCRUZPUkUgc3dpdGNoXG4gIGlmIChpc0FnZW50U3dhcm1zRW5hYmxlZCgpICYmIGF0dGFjaG1lbnQudHlwZSA9PT0gJ3RlYW1tYXRlX21haWxib3gnKSB7XG4gICAgLy8gRmlsdGVyIG91dCBpZGxlIG5vdGlmaWNhdGlvbnMgQkVGT1JFIGNvdW50aW5nIC0gdGhleSBhcmUgaGlkZGVuIGluIHRoZSBVSVxuICAgIC8vIHNvIHNob3dpbmcgdGhlbSBpbiB0aGUgY291bnQgd291bGQgYmUgY29uZnVzaW5nIChcIjIgbWVzc2FnZXMgaW4gbWFpbGJveDpcIiB3aXRoIG5vdGhpbmcgc2hvd24pXG4gICAgY29uc3QgdmlzaWJsZU1lc3NhZ2VzID0gYXR0YWNobWVudC5tZXNzYWdlcy5maWx0ZXIobXNnID0+IHtcbiAgICAgIGlmIChpc1NodXRkb3duQXBwcm92ZWQobXNnLnRleHQpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZVxuICAgICAgfVxuICAgICAgdHJ5IHtcbiAgICAgICAgY29uc3QgcGFyc2VkID0ganNvblBhcnNlKG1zZy50ZXh0KVxuICAgICAgICByZXR1cm4gKFxuICAgICAgICAgIHBhcnNlZD8udHlwZSAhPT0gJ2lkbGVfbm90aWZpY2F0aW9uJyAmJlxuICAgICAgICAgIHBhcnNlZD8udHlwZSAhPT0gJ3RlYW1tYXRlX3Rlcm1pbmF0ZWQnXG4gICAgICAgIClcbiAgICAgIH0gY2F0Y2gge1xuICAgICAgICByZXR1cm4gdHJ1ZSAvLyBOb24tSlNPTiBtZXNzYWdlcyBhcmUgdmlzaWJsZVxuICAgICAgfVxuICAgIH0pXG5cbiAgICBpZiAodmlzaWJsZU1lc3NhZ2VzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgcmV0dXJuIG51bGxcbiAgICB9XG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICB7dmlzaWJsZU1lc3NhZ2VzLm1hcCgobXNnLCBpZHgpID0+IHtcbiAgICAgICAgICAvLyBUcnkgdG8gcGFyc2UgYXMgSlNPTiBmb3IgdGFza19hc3NpZ25tZW50IG1lc3NhZ2VzXG4gICAgICAgICAgbGV0IHBhcnNlZE1zZzoge1xuICAgICAgICAgICAgdHlwZT86IHN0cmluZ1xuICAgICAgICAgICAgdGFza0lkPzogc3RyaW5nXG4gICAgICAgICAgICBzdWJqZWN0Pzogc3RyaW5nXG4gICAgICAgICAgICBhc3NpZ25lZEJ5Pzogc3RyaW5nXG4gICAgICAgICAgfSB8IG51bGwgPSBudWxsXG4gICAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHBhcnNlZE1zZyA9IGpzb25QYXJzZShtc2cudGV4dClcbiAgICAgICAgICB9IGNhdGNoIHtcbiAgICAgICAgICAgIC8vIE5vdCBKU09OLCB0cmVhdCBhcyBwbGFpbiB0ZXh0XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgaWYgKHBhcnNlZE1zZz8udHlwZSA9PT0gJ3Rhc2tfYXNzaWdubWVudCcpIHtcbiAgICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICAgIDxCb3gga2V5PXtpZHh9IHBhZGRpbmdMZWZ0PXsyfT5cbiAgICAgICAgICAgICAgICA8VGV4dD57QkxBQ0tfQ0lSQ0xFfSA8L1RleHQ+XG4gICAgICAgICAgICAgICAgPFRleHQ+VGFzayBhc3NpZ25lZDogPC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGJvbGQ+I3twYXJzZWRNc2cudGFza0lkfTwvVGV4dD5cbiAgICAgICAgICAgICAgICA8VGV4dD4gLSB7cGFyc2VkTXNnLnN1YmplY3R9PC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiAoZnJvbSB7cGFyc2VkTXNnLmFzc2lnbmVkQnkgfHwgbXNnLmZyb219KTwvVGV4dD5cbiAgICAgICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgICApXG4gICAgICAgICAgfVxuXG4gICAgICAgICAgLy8gTm90ZTogaWRsZV9ub3RpZmljYXRpb24gbWVzc2FnZXMgYWxyZWFkeSBmaWx0ZXJlZCBvdXQgYWJvdmVcblxuICAgICAgICAgIC8vIFRyeSB0byByZW5kZXIgYXMgcGxhbiBhcHByb3ZhbCBtZXNzYWdlIChyZXF1ZXN0IG9yIHJlc3BvbnNlKVxuICAgICAgICAgIGNvbnN0IHBsYW5BcHByb3ZhbEVsZW1lbnQgPSB0cnlSZW5kZXJQbGFuQXBwcm92YWxNZXNzYWdlKFxuICAgICAgICAgICAgbXNnLnRleHQsXG4gICAgICAgICAgICBtc2cuZnJvbSxcbiAgICAgICAgICApXG4gICAgICAgICAgaWYgKHBsYW5BcHByb3ZhbEVsZW1lbnQpIHtcbiAgICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICAgIDxSZWFjdC5GcmFnbWVudCBrZXk9e2lkeH0+e3BsYW5BcHByb3ZhbEVsZW1lbnR9PC9SZWFjdC5GcmFnbWVudD5cbiAgICAgICAgICAgIClcbiAgICAgICAgICB9XG5cbiAgICAgICAgICAvLyBQbGFpbiB0ZXh0IG1lc3NhZ2UgLSBzZW5kZXIgaGVhZGVyIHdpdGggY2hldnJvbiwgdHJ1bmNhdGVkIGNvbnRlbnRcbiAgICAgICAgICBjb25zdCBpbmtDb2xvciA9IHRvSW5rQ29sb3IobXNnLmNvbG9yKVxuICAgICAgICAgIGNvbnN0IGZvcm1hdHRlZENvbnRlbnQgPVxuICAgICAgICAgICAgZm9ybWF0VGVhbW1hdGVNZXNzYWdlQ29udGVudChtc2cudGV4dCkgPz8gbXNnLnRleHRcbiAgICAgICAgICByZXR1cm4gKFxuICAgICAgICAgICAgPFRlYW1tYXRlTWVzc2FnZUNvbnRlbnRcbiAgICAgICAgICAgICAga2V5PXtpZHh9XG4gICAgICAgICAgICAgIGRpc3BsYXlOYW1lPXttc2cuZnJvbX1cbiAgICAgICAgICAgICAgaW5rQ29sb3I9e2lua0NvbG9yfVxuICAgICAgICAgICAgICBjb250ZW50PXtmb3JtYXR0ZWRDb250ZW50fVxuICAgICAgICAgICAgICBzdW1tYXJ5PXttc2cuc3VtbWFyeX1cbiAgICAgICAgICAgICAgaXNUcmFuc2NyaXB0TW9kZT17aXNUcmFuc2NyaXB0TW9kZX1cbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgKVxuICAgICAgICB9KX1cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuXG4gIC8vIHNraWxsX2Rpc2NvdmVyeSByZW5kZXJlZCBoZXJlIChub3QgaW4gdGhlIHN3aXRjaCkgc28gdGhlICdza2lsbF9kaXNjb3ZlcnknXG4gIC8vIHN0cmluZyBsaXRlcmFsIHN0YXlzIGluc2lkZSBhIGZlYXR1cmUoKS1ndWFyZGVkIGJsb2NrLiBBIGNhc2UgbGFiZWwgY2FuJ3RcbiAgLy8gYmUgY29uZGl0aW9uYWxseSBlbGltaW5hdGVkOyBhbiBpZi1ib2R5IGNhbi5cbiAgaWYgKGZlYXR1cmUoJ0VYUEVSSU1FTlRBTF9TS0lMTF9TRUFSQ0gnKSkge1xuICAgIGlmIChhdHRhY2htZW50LnR5cGUgPT09ICdza2lsbF9kaXNjb3ZlcnknKSB7XG4gICAgICBpZiAoYXR0YWNobWVudC5za2lsbHMubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbFxuICAgICAgLy8gQW50IHVzZXJzIGdldCBzaG9ydElkcyBpbmxpbmUgc28gdGhleSBjYW4gL3NraWxsLWZlZWRiYWNrIHdoaWxlIHRoZVxuICAgICAgLy8gdHVybiBpcyBzdGlsbCBmcmVzaC4gRXh0ZXJuYWwgdXNlcnMgKHdoZW4gdGhpcyB1bi1nYXRlcykganVzdCBzZWVcbiAgICAgIC8vIG5hbWVzIOKAlCBzaG9ydElkIGlzIHVuZGVmaW5lZCBvdXRzaWRlIGFudCBidWlsZHMgYW55d2F5LlxuICAgICAgY29uc3QgbmFtZXMgPSBhdHRhY2htZW50LnNraWxsc1xuICAgICAgICAubWFwKHMgPT4gKHMuc2hvcnRJZCA/IGAke3MubmFtZX0gWyR7cy5zaG9ydElkfV1gIDogcy5uYW1lKSlcbiAgICAgICAgLmpvaW4oJywgJylcbiAgICAgIGNvbnN0IGZpcnN0SWQgPSBhdHRhY2htZW50LnNraWxsc1swXT8uc2hvcnRJZFxuICAgICAgY29uc3QgaGludCA9XG4gICAgICAgIFwiZXh0ZXJuYWxcIiA9PT0gJ2FudCcgJiYgIWlzRGVtb0VudiAmJiBmaXJzdElkXG4gICAgICAgICAgPyBgIMK3IC9za2lsbC1mZWVkYmFjayAke2ZpcnN0SWR9IDE9d3JvbmcgMj1ub2lzeSAzPWdvb2QgW2NvbW1lbnRdYFxuICAgICAgICAgIDogJydcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuc2tpbGxzLmxlbmd0aH08L1RleHQ+IHJlbGV2YW50eycgJ31cbiAgICAgICAgICB7cGx1cmFsKGF0dGFjaG1lbnQuc2tpbGxzLmxlbmd0aCwgJ3NraWxsJyl9OiB7bmFtZXN9XG4gICAgICAgICAge2hpbnQgJiYgPFRleHQgZGltQ29sb3I+e2hpbnR9PC9UZXh0Pn1cbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIH1cbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvc3dpdGNoLWV4aGF1c3RpdmVuZXNzLWNoZWNrIC0tIHRlYW1tYXRlX21haWxib3gvc2tpbGxfZGlzY292ZXJ5IGhhbmRsZWQgYmVmb3JlIHN3aXRjaFxuICBzd2l0Y2ggKGF0dGFjaG1lbnQudHlwZSkge1xuICAgIGNhc2UgJ2RpcmVjdG9yeSc6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICBMaXN0ZWQgZGlyZWN0b3J5IDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGlzcGxheVBhdGggKyBzZXB9PC9UZXh0PlxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAnZmlsZSc6XG4gICAgY2FzZSAnYWxyZWFkeV9yZWFkX2ZpbGUnOlxuICAgICAgaWYgKGF0dGFjaG1lbnQuY29udGVudC50eXBlID09PSAnbm90ZWJvb2snKSB7XG4gICAgICAgIHJldHVybiAoXG4gICAgICAgICAgPExpbmU+XG4gICAgICAgICAgICBSZWFkIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGlzcGxheVBhdGh9PC9UZXh0PiAoXG4gICAgICAgICAgICB7YXR0YWNobWVudC5jb250ZW50LmZpbGUuY2VsbHMubGVuZ3RofSBjZWxscylcbiAgICAgICAgICA8L0xpbmU+XG4gICAgICAgIClcbiAgICAgIH1cbiAgICAgIGlmIChhdHRhY2htZW50LmNvbnRlbnQudHlwZSA9PT0gJ2ZpbGVfdW5jaGFuZ2VkJykge1xuICAgICAgICByZXR1cm4gKFxuICAgICAgICAgIDxMaW5lPlxuICAgICAgICAgICAgUmVhZCA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD4gKHVuY2hhbmdlZClcbiAgICAgICAgICA8L0xpbmU+XG4gICAgICAgIClcbiAgICAgIH1cbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIFJlYWQgPFRleHQgYm9sZD57YXR0YWNobWVudC5kaXNwbGF5UGF0aH08L1RleHQ+IChcbiAgICAgICAgICB7YXR0YWNobWVudC5jb250ZW50LnR5cGUgPT09ICd0ZXh0J1xuICAgICAgICAgICAgPyBgJHthdHRhY2htZW50LmNvbnRlbnQuZmlsZS5udW1MaW5lc30ke2F0dGFjaG1lbnQudHJ1bmNhdGVkID8gJysnIDogJyd9IGxpbmVzYFxuICAgICAgICAgICAgOiBmb3JtYXRGaWxlU2l6ZShhdHRhY2htZW50LmNvbnRlbnQuZmlsZS5vcmlnaW5hbFNpemUpfVxuICAgICAgICAgIClcbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ2NvbXBhY3RfZmlsZV9yZWZlcmVuY2UnOlxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAgUmVmZXJlbmNlZCBmaWxlIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGlzcGxheVBhdGh9PC9UZXh0PlxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAncGRmX3JlZmVyZW5jZSc6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICBSZWZlcmVuY2VkIFBERiA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD4gKFxuICAgICAgICAgIHthdHRhY2htZW50LnBhZ2VDb3VudH0gcGFnZXMpXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICBjYXNlICdzZWxlY3RlZF9saW5lc19pbl9pZGUnOlxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAg4qeJIFNlbGVjdGVkeycgJ31cbiAgICAgICAgICA8VGV4dCBib2xkPnthdHRhY2htZW50LmxpbmVFbmQgLSBhdHRhY2htZW50LmxpbmVTdGFydCArIDF9PC9UZXh0PnsnICd9XG4gICAgICAgICAgbGluZXMgZnJvbSA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD4gaW57JyAnfVxuICAgICAgICAgIHthdHRhY2htZW50LmlkZU5hbWV9XG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICBjYXNlICduZXN0ZWRfbWVtb3J5JzpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIExvYWRlZCA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD5cbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ3JlbGV2YW50X21lbW9yaWVzJzpcbiAgICAgIC8vIFVzdWFsbHkgYWJzb3JiZWQgaW50byBhIENvbGxhcHNlZFJlYWRTZWFyY2hHcm91cCAoY29sbGFwc2VSZWFkU2VhcmNoLnRzKVxuICAgICAgLy8gc28gdGhpcyBvbmx5IHJlbmRlcnMgd2hlbiB0aGUgcHJlY2VkaW5nIHRvb2wgd2FzIG5vbi1jb2xsYXBzaWJsZSAoRWRpdCxcbiAgICAgIC8vIFdyaXRlKSBhbmQgbm8gZ3JvdXAgd2FzIG9wZW4uIE1hdGNoIENvbGxhcHNlZFJlYWRTZWFyY2hDb250ZW50J3Mgc3R5bGU6XG4gICAgICAvLyAyLXNwYWNlIGd1dHRlciwgZGltIHRleHQsIGNvdW50IG9ubHkg4oCUIGZpbGVuYW1lcy9jb250ZW50IGluIGN0cmwrby5cbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxCb3hcbiAgICAgICAgICBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCJcbiAgICAgICAgICBtYXJnaW5Ub3A9e2FkZE1hcmdpbiA/IDEgOiAwfVxuICAgICAgICAgIGJhY2tncm91bmRDb2xvcj17Ymd9XG4gICAgICAgID5cbiAgICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIj5cbiAgICAgICAgICAgIDxCb3ggbWluV2lkdGg9ezJ9IC8+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgUmVjYWxsZWQgPFRleHQgYm9sZD57YXR0YWNobWVudC5tZW1vcmllcy5sZW5ndGh9PC9UZXh0PnsnICd9XG4gICAgICAgICAgICAgIHthdHRhY2htZW50Lm1lbW9yaWVzLmxlbmd0aCA9PT0gMSA/ICdtZW1vcnknIDogJ21lbW9yaWVzJ31cbiAgICAgICAgICAgICAgeyFpc1RyYW5zY3JpcHRNb2RlICYmIChcbiAgICAgICAgICAgICAgICA8PlxuICAgICAgICAgICAgICAgICAgeycgJ31cbiAgICAgICAgICAgICAgICAgIDxDdHJsT1RvRXhwYW5kIC8+XG4gICAgICAgICAgICAgICAgPC8+XG4gICAgICAgICAgICAgICl9XG4gICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgeyh2ZXJib3NlIHx8IGlzVHJhbnNjcmlwdE1vZGUpICYmXG4gICAgICAgICAgICBhdHRhY2htZW50Lm1lbW9yaWVzLm1hcChtID0+IChcbiAgICAgICAgICAgICAgPEJveCBrZXk9e20ucGF0aH0gZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgICAgICAgIDxNZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgICAgPEZpbGVQYXRoTGluayBmaWxlUGF0aD17bS5wYXRofT5cbiAgICAgICAgICAgICAgICAgICAgICB7YmFzZW5hbWUobS5wYXRoKX1cbiAgICAgICAgICAgICAgICAgICAgPC9GaWxlUGF0aExpbms+XG4gICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgPC9NZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgICAgICAgICAge2lzVHJhbnNjcmlwdE1vZGUgJiYgKFxuICAgICAgICAgICAgICAgICAgPEJveCBwYWRkaW5nTGVmdD17NX0+XG4gICAgICAgICAgICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgICAgICAgICAgIDxBbnNpPnttLmNvbnRlbnR9PC9BbnNpPlxuICAgICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICkpfVxuICAgICAgICA8L0JveD5cbiAgICAgIClcbiAgICBjYXNlICdkeW5hbWljX3NraWxsJzoge1xuICAgICAgY29uc3Qgc2tpbGxDb3VudCA9IGF0dGFjaG1lbnQuc2tpbGxOYW1lcy5sZW5ndGhcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIExvYWRlZHsnICd9XG4gICAgICAgICAgPFRleHQgYm9sZD5cbiAgICAgICAgICAgIHtza2lsbENvdW50fSB7cGx1cmFsKHNraWxsQ291bnQsICdza2lsbCcpfVxuICAgICAgICAgIDwvVGV4dD57JyAnfVxuICAgICAgICAgIGZyb20gPFRleHQgYm9sZD57YXR0YWNobWVudC5kaXNwbGF5UGF0aH08L1RleHQ+XG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAnc2tpbGxfbGlzdGluZyc6IHtcbiAgICAgIGlmIChhdHRhY2htZW50LmlzSW5pdGlhbCkge1xuICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgfVxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAgPFRleHQgYm9sZD57YXR0YWNobWVudC5za2lsbENvdW50fTwvVGV4dD57JyAnfVxuICAgICAgICAgIHtwbHVyYWwoYXR0YWNobWVudC5za2lsbENvdW50LCAnc2tpbGwnKX0gYXZhaWxhYmxlXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAnYWdlbnRfbGlzdGluZ19kZWx0YSc6IHtcbiAgICAgIGlmIChhdHRhY2htZW50LmlzSW5pdGlhbCB8fCBhdHRhY2htZW50LmFkZGVkVHlwZXMubGVuZ3RoID09PSAwKSB7XG4gICAgICAgIHJldHVybiBudWxsXG4gICAgICB9XG4gICAgICBjb25zdCBjb3VudCA9IGF0dGFjaG1lbnQuYWRkZWRUeXBlcy5sZW5ndGhcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvdW50fTwvVGV4dD4gYWdlbnQge3BsdXJhbChjb3VudCwgJ3R5cGUnKX0gYXZhaWxhYmxlXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAncXVldWVkX2NvbW1hbmQnOiB7XG4gICAgICBjb25zdCB0ZXh0ID1cbiAgICAgICAgdHlwZW9mIGF0dGFjaG1lbnQucHJvbXB0ID09PSAnc3RyaW5nJ1xuICAgICAgICAgID8gYXR0YWNobWVudC5wcm9tcHRcbiAgICAgICAgICA6IGdldENvbnRlbnRUZXh0KGF0dGFjaG1lbnQucHJvbXB0KSB8fCAnJ1xuICAgICAgY29uc3QgaGFzSW1hZ2VzID1cbiAgICAgICAgYXR0YWNobWVudC5pbWFnZVBhc3RlSWRzICYmIGF0dGFjaG1lbnQuaW1hZ2VQYXN0ZUlkcy5sZW5ndGggPiAwXG4gICAgICByZXR1cm4gKFxuICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAgICA8VXNlclRleHRNZXNzYWdlXG4gICAgICAgICAgICBhZGRNYXJnaW49e2FkZE1hcmdpbn1cbiAgICAgICAgICAgIHBhcmFtPXt7IHRleHQsIHR5cGU6ICd0ZXh0JyB9fVxuICAgICAgICAgICAgdmVyYm9zZT17dmVyYm9zZX1cbiAgICAgICAgICAgIGlzVHJhbnNjcmlwdE1vZGU9e2lzVHJhbnNjcmlwdE1vZGV9XG4gICAgICAgICAgLz5cbiAgICAgICAgICB7aGFzSW1hZ2VzICYmXG4gICAgICAgICAgICBhdHRhY2htZW50LmltYWdlUGFzdGVJZHM/Lm1hcChpZCA9PiAoXG4gICAgICAgICAgICAgIDxVc2VySW1hZ2VNZXNzYWdlIGtleT17aWR9IGltYWdlSWQ9e2lkfSAvPlxuICAgICAgICAgICAgKSl9XG4gICAgICAgIDwvQm94PlxuICAgICAgKVxuICAgIH1cbiAgICBjYXNlICdwbGFuX2ZpbGVfcmVmZXJlbmNlJzpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIFBsYW4gZmlsZSByZWZlcmVuY2VkICh7Z2V0RGlzcGxheVBhdGgoYXR0YWNobWVudC5wbGFuRmlsZVBhdGgpfSlcbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ2ludm9rZWRfc2tpbGxzJzoge1xuICAgICAgaWYgKGF0dGFjaG1lbnQuc2tpbGxzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgfVxuICAgICAgY29uc3Qgc2tpbGxOYW1lcyA9IGF0dGFjaG1lbnQuc2tpbGxzLm1hcChzID0+IHMubmFtZSkuam9pbignLCAnKVxuICAgICAgcmV0dXJuIDxMaW5lPlNraWxscyByZXN0b3JlZCAoe3NraWxsTmFtZXN9KTwvTGluZT5cbiAgICB9XG4gICAgY2FzZSAnZGlhZ25vc3RpY3MnOlxuICAgICAgcmV0dXJuIDxEaWFnbm9zdGljc0Rpc3BsYXkgYXR0YWNobWVudD17YXR0YWNobWVudH0gdmVyYm9zZT17dmVyYm9zZX0gLz5cbiAgICBjYXNlICdtY3BfcmVzb3VyY2UnOlxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAgUmVhZCBNQ1AgcmVzb3VyY2UgPFRleHQgYm9sZD57YXR0YWNobWVudC5uYW1lfTwvVGV4dD4gZnJvbXsnICd9XG4gICAgICAgICAge2F0dGFjaG1lbnQuc2VydmVyfVxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAnY29tbWFuZF9wZXJtaXNzaW9ucyc6XG4gICAgICAvLyBUaGUgc2tpbGwgc3VjY2VzcyBtZXNzYWdlIGlzIHJlbmRlcmVkIGJ5IFNraWxsVG9vbCdzIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlLFxuICAgICAgLy8gc28gd2UgZG9uJ3QgcmVuZGVyIGFueXRoaW5nIGhlcmUgdG8gYXZvaWQgZHVwbGljYXRlIG1lc3NhZ2VzLlxuICAgICAgcmV0dXJuIG51bGxcbiAgICBjYXNlICdhc3luY19ob29rX3Jlc3BvbnNlJzoge1xuICAgICAgLy8gU2Vzc2lvblN0YXJ0IGhvb2sgY29tcGxldGlvbnMgYXJlIG9ubHkgc2hvd24gaW4gdmVyYm9zZSBtb2RlXG4gICAgICBpZiAoYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTZXNzaW9uU3RhcnQnICYmICF2ZXJib3NlKSB7XG4gICAgICAgIHJldHVybiBudWxsXG4gICAgICB9XG4gICAgICAvLyBHZW5lcmFsbHkgaGlkZSBhc3luYyBob29rIGNvbXBsZXRpb24gbWVzc2FnZXMgdW5sZXNzIGluIHZlcmJvc2UgbW9kZVxuICAgICAgaWYgKCF2ZXJib3NlICYmICFpc1RyYW5zY3JpcHRNb2RlKSB7XG4gICAgICAgIHJldHVybiBudWxsXG4gICAgICB9XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICBBc3luYyBob29rIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuaG9va0V2ZW50fTwvVGV4dD4gY29tcGxldGVkXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAnaG9va19ibG9ja2luZ19lcnJvcic6IHtcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIC8vIFNob3cgc3RkZXJyIHRvIHRoZSB1c2VyIHNvIHRoZXkgY2FuIHVuZGVyc3RhbmQgd2h5IHRoZSBob29rIGJsb2NrZWRcbiAgICAgIGNvbnN0IHN0ZGVyciA9IGF0dGFjaG1lbnQuYmxvY2tpbmdFcnJvci5ibG9ja2luZ0Vycm9yLnRyaW0oKVxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPD5cbiAgICAgICAgICA8TGluZSBjb2xvcj1cImVycm9yXCI+XG4gICAgICAgICAgICB7YXR0YWNobWVudC5ob29rTmFtZX0gaG9vayByZXR1cm5lZCBibG9ja2luZyBlcnJvclxuICAgICAgICAgIDwvTGluZT5cbiAgICAgICAgICB7c3RkZXJyID8gPExpbmUgY29sb3I9XCJlcnJvclwiPntzdGRlcnJ9PC9MaW5lPiA6IG51bGx9XG4gICAgICAgIDwvPlxuICAgICAgKVxuICAgIH1cbiAgICBjYXNlICdob29rX25vbl9ibG9ja2luZ19lcnJvcic6IHtcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIC8vIEZ1bGwgaG9vayBvdXRwdXQgaXMgbG9nZ2VkIHRvIGRlYnVnIGxvZyB2aWEgaG9va0V2ZW50cy50c1xuICAgICAgcmV0dXJuIDxMaW5lIGNvbG9yPVwiZXJyb3JcIj57YXR0YWNobWVudC5ob29rTmFtZX0gaG9vayBlcnJvcjwvTGluZT5cbiAgICB9XG4gICAgY2FzZSAnaG9va19lcnJvcl9kdXJpbmdfZXhlY3V0aW9uJzpcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIC8vIEZ1bGwgaG9vayBvdXRwdXQgaXMgbG9nZ2VkIHRvIGRlYnVnIGxvZyB2aWEgaG9va0V2ZW50cy50c1xuICAgICAgcmV0dXJuIDxMaW5lPnthdHRhY2htZW50Lmhvb2tOYW1lfSBob29rIHdhcm5pbmc8L0xpbmU+XG4gICAgY2FzZSAnaG9va19zdWNjZXNzJzpcbiAgICAgIC8vIEZ1bGwgaG9vayBvdXRwdXQgaXMgbG9nZ2VkIHRvIGRlYnVnIGxvZyB2aWEgaG9va0V2ZW50cy50c1xuICAgICAgcmV0dXJuIG51bGxcbiAgICBjYXNlICdob29rX3N0b3BwZWRfY29udGludWF0aW9uJzpcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lIGNvbG9yPVwid2FybmluZ1wiPlxuICAgICAgICAgIHthdHRhY2htZW50Lmhvb2tOYW1lfSBob29rIHN0b3BwZWQgY29udGludWF0aW9uOiB7YXR0YWNobWVudC5tZXNzYWdlfVxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAnaG9va19zeXN0ZW1fbWVzc2FnZSc6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICB7YXR0YWNobWVudC5ob29rTmFtZX0gc2F5czoge2F0dGFjaG1lbnQuY29udGVudH1cbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ2hvb2tfcGVybWlzc2lvbl9kZWNpc2lvbic6IHtcbiAgICAgIGNvbnN0IGFjdGlvbiA9IGF0dGFjaG1lbnQuZGVjaXNpb24gPT09ICdhbGxvdycgPyAnQWxsb3dlZCcgOiAnRGVuaWVkJ1xuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAge2FjdGlvbn0gYnkgPFRleHQgYm9sZD57YXR0YWNobWVudC5ob29rRXZlbnR9PC9UZXh0PiBob29rXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAndGFza19zdGF0dXMnOlxuICAgICAgcmV0dXJuIDxUYXNrU3RhdHVzTWVzc2FnZSBhdHRhY2htZW50PXthdHRhY2htZW50fSAvPlxuICAgIGNhc2UgJ3RlYW1tYXRlX3NodXRkb3duX2JhdGNoJzpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxCb3hcbiAgICAgICAgICBmbGV4RGlyZWN0aW9uPVwicm93XCJcbiAgICAgICAgICB3aWR0aD1cIjEwMCVcIlxuICAgICAgICAgIG1hcmdpblRvcD17MX1cbiAgICAgICAgICBiYWNrZ3JvdW5kQ29sb3I9e2JnfVxuICAgICAgICA+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+e0JMQUNLX0NJUkNMRX0gPC9UZXh0PlxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAge2F0dGFjaG1lbnQuY291bnR9IHtwbHVyYWwoYXR0YWNobWVudC5jb3VudCwgJ3RlYW1tYXRlJyl9IHNodXQgZG93blxuICAgICAgICAgICAgZ3JhY2VmdWxseVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApXG4gICAgZGVmYXVsdDpcbiAgICAgIC8vIEV4aGF1c3RpdmVuZXNzOiBldmVyeSB0eXBlIHJlYWNoaW5nIGhlcmUgbXVzdCBiZSBpbiBOVUxMX1JFTkRFUklOR19UWVBFUy5cbiAgICAgIC8vIElmIFRTIGVycm9ycywgYSBuZXcgQXR0YWNobWVudCB0eXBlIHdhcyBhZGRlZCB3aXRob3V0IGEgY2FzZSBhYm92ZSBBTkRcbiAgICAgIC8vIHdpdGhvdXQgYW4gZW50cnkgaW4gTlVMTF9SRU5ERVJJTkdfVFlQRVMg4oCUIGRlY2lkZTogcmVuZGVyIHNvbWV0aGluZyAoYWRkXG4gICAgICAvLyBhIGNhc2UpIG9yIHJlbmRlciBub3RoaW5nIChhZGQgdG8gdGhlIGFycmF5KS4gTWVzc2FnZXMudHN4IHByZS1maWx0ZXJzXG4gICAgICAvLyB0aGVzZSBzbyB0aGlzIGJyYW5jaCBpcyBkZWZlbnNlLWluLWRlcHRoIGZvciBvdGhlciByZW5kZXIgcGF0aHMuXG4gICAgICAvL1xuICAgICAgLy8gc2tpbGxfZGlzY292ZXJ5IGFuZCB0ZWFtbWF0ZV9tYWlsYm94IGFyZSBoYW5kbGVkIEJFRk9SRSB0aGUgc3dpdGNoIGluXG4gICAgICAvLyBydW50aW1lLWdhdGVkIGJsb2NrcyAoZmVhdHVyZSgpIC8gaXNBZ2VudFN3YXJtc0VuYWJsZWQoKSkgdGhhdCBUUyBjYW4ndFxuICAgICAgLy8gbmFycm93IHRocm91Z2gg4oCUIGV4Y2x1ZGVkIGhlcmUgdmlhIHR5cGUgdW5pb24gKGNvbXBpbGUtdGltZSBvbmx5LCBubyBlbWl0KS5cbiAgICAgIGF0dGFjaG1lbnQudHlwZSBzYXRpc2ZpZXNcbiAgICAgICAgfCBOdWxsUmVuZGVyaW5nQXR0YWNobWVudFR5cGVcbiAgICAgICAgfCAnc2tpbGxfZGlzY292ZXJ5J1xuICAgICAgICB8ICd0ZWFtbWF0ZV9tYWlsYm94J1xuICAgICAgcmV0dXJuIG51bGxcbiAgfVxufVxuXG50eXBlIFRhc2tTdGF0dXNBdHRhY2htZW50ID0gRXh0cmFjdDxBdHRhY2htZW50LCB7IHR5cGU6ICd0YXNrX3N0YXR1cycgfT5cblxuZnVuY3Rpb24gVGFza1N0YXR1c01lc3NhZ2Uoe1xuICBhdHRhY2htZW50LFxufToge1xuICBhdHRhY2htZW50OiBUYXNrU3RhdHVzQXR0YWNobWVudFxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIC8vIEZvciBhbnRzLCBraWxsZWQgdGFzayBzdGF0dXMgaXMgc2hvd24gaW4gdGhlIENvb3JkaW5hdG9yVGFza1BhbmVsLlxuICAvLyBEb24ndCByZW5kZXIgaXQgYWdhaW4gaW4gdGhlIGNoYXQuXG4gIGlmIChcImV4dGVybmFsXCIgPT09ICdhbnQnICYmIGF0dGFjaG1lbnQuc3RhdHVzID09PSAna2lsbGVkJykge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBPbmx5IGFjY2VzcyB0ZWFtbWF0ZS1zcGVjaWZpYyBjb2RlIHdoZW4gc3dhcm1zIGFyZSBlbmFibGVkLlxuICAvLyBUZWFtbWF0ZVRhc2tTdGF0dXMgc3Vic2NyaWJlcyB0byBBcHBTdGF0ZTsgYnkgZ2F0aW5nIHRoZSBtb3VudCB3ZVxuICAvLyBhdm9pZCBhZGRpbmcgYSBzdG9yZSBsaXN0ZW5lciBmb3IgZXZlcnkgbm9uLXRlYW1tYXRlIGF0dGFjaG1lbnQuXG4gIGlmIChpc0FnZW50U3dhcm1zRW5hYmxlZCgpICYmIGF0dGFjaG1lbnQudGFza1R5cGUgPT09ICdpbl9wcm9jZXNzX3RlYW1tYXRlJykge1xuICAgIHJldHVybiA8VGVhbW1hdGVUYXNrU3RhdHVzIGF0dGFjaG1lbnQ9e2F0dGFjaG1lbnR9IC8+XG4gIH1cblxuICByZXR1cm4gPEdlbmVyaWNUYXNrU3RhdHVzIGF0dGFjaG1lbnQ9e2F0dGFjaG1lbnR9IC8+XG59XG5cbmZ1bmN0aW9uIEdlbmVyaWNUYXNrU3RhdHVzKHtcbiAgYXR0YWNobWVudCxcbn06IHtcbiAgYXR0YWNobWVudDogVGFza1N0YXR1c0F0dGFjaG1lbnRcbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBiZyA9IHVzZVNlbGVjdGVkTWVzc2FnZUJnKClcbiAgY29uc3Qgc3RhdHVzVGV4dCA9XG4gICAgYXR0YWNobWVudC5zdGF0dXMgPT09ICdjb21wbGV0ZWQnXG4gICAgICA/ICdjb21wbGV0ZWQgaW4gYmFja2dyb3VuZCdcbiAgICAgIDogYXR0YWNobWVudC5zdGF0dXMgPT09ICdraWxsZWQnXG4gICAgICAgID8gJ3N0b3BwZWQnXG4gICAgICAgIDogYXR0YWNobWVudC5zdGF0dXMgPT09ICdydW5uaW5nJ1xuICAgICAgICAgID8gJ3N0aWxsIHJ1bm5pbmcgaW4gYmFja2dyb3VuZCdcbiAgICAgICAgICA6IGF0dGFjaG1lbnQuc3RhdHVzXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgd2lkdGg9XCIxMDAlXCIgbWFyZ2luVG9wPXsxfSBiYWNrZ3JvdW5kQ29sb3I9e2JnfT5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPntCTEFDS19DSVJDTEV9IDwvVGV4dD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICBUYXNrICZxdW90OzxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGVzY3JpcHRpb259PC9UZXh0PiZxdW90OyB7c3RhdHVzVGV4dH1cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuXG5mdW5jdGlvbiBUZWFtbWF0ZVRhc2tTdGF0dXMoe1xuICBhdHRhY2htZW50LFxufToge1xuICBhdHRhY2htZW50OiBUYXNrU3RhdHVzQXR0YWNobWVudFxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGJnID0gdXNlU2VsZWN0ZWRNZXNzYWdlQmcoKVxuICAvLyBOYXJyb3cgc2VsZWN0b3I6IG9ubHkgcmUtcmVuZGVyIHdoZW4gdGhpcyBzcGVjaWZpYyB0YXNrIGNoYW5nZXMuXG4gIGNvbnN0IHRhc2sgPSB1c2VBcHBTdGF0ZShzID0+IHMudGFza3NbYXR0YWNobWVudC50YXNrSWRdKVxuICBpZiAodGFzaz8udHlwZSAhPT0gJ2luX3Byb2Nlc3NfdGVhbW1hdGUnKSB7XG4gICAgLy8gRmFsbCB0aHJvdWdoIHRvIGdlbmVyaWMgcmVuZGVyaW5nICh0YXNrIG5vdCB5ZXQgaW4gc3RvcmUsIG9yIHdyb25nIHR5cGUpXG4gICAgcmV0dXJuIDxHZW5lcmljVGFza1N0YXR1cyBhdHRhY2htZW50PXthdHRhY2htZW50fSAvPlxuICB9XG4gIGNvbnN0IGFnZW50Q29sb3IgPSB0b0lua0NvbG9yKHRhc2suaWRlbnRpdHkuY29sb3IpXG4gIGNvbnN0IHN0YXR1c1RleHQgPVxuICAgIGF0dGFjaG1lbnQuc3RhdHVzID09PSAnY29tcGxldGVkJ1xuICAgICAgPyAnc2h1dCBkb3duIGdyYWNlZnVsbHknXG4gICAgICA6IGF0dGFjaG1lbnQuc3RhdHVzXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgd2lkdGg9XCIxMDAlXCIgbWFyZ2luVG9wPXsxfSBiYWNrZ3JvdW5kQ29sb3I9e2JnfT5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPntCTEFDS19DSVJDTEV9IDwvVGV4dD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICBUZWFtbWF0ZXsnICd9XG4gICAgICAgIDxUZXh0IGNvbG9yPXthZ2VudENvbG9yfSBib2xkIGRpbUNvbG9yPXtmYWxzZX0+XG4gICAgICAgICAgQHt0YXNrLmlkZW50aXR5LmFnZW50TmFtZX1cbiAgICAgICAgPC9UZXh0PnsnICd9XG4gICAgICAgIHtzdGF0dXNUZXh0fVxuICAgICAgPC9UZXh0PlxuICAgIDwvQm94PlxuICApXG59XG4vLyBXZSBhbGxvdyBzZXR0aW5nIGRpbUNvbG9yIHRvIGZhbHNlIGhlcmUgdG8gaGVscCB3b3JrIGFyb3VuZCB0aGUgZGltLWJvbGQgYnVnLlxuLy8gaHR0cHM6Ly9naXRodWIuY29tL2NoYWxrL2NoYWxrL2lzc3Vlcy8yOTBcbmZ1bmN0aW9uIExpbmUoe1xuICBkaW1Db2xvciA9IHRydWUsXG4gIGNoaWxkcmVuLFxuICBjb2xvcixcbn06IHtcbiAgZGltQ29sb3I/OiBib29sZWFuXG4gIGNoaWxkcmVuOiBSZWFjdC5SZWFjdE5vZGVcbiAgY29sb3I/OiBrZXlvZiBUaGVtZVxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGJnID0gdXNlU2VsZWN0ZWRNZXNzYWdlQmcoKVxuICByZXR1cm4gKFxuICAgIDxCb3ggYmFja2dyb3VuZENvbG9yPXtiZ30+XG4gICAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICA8VGV4dCBjb2xvcj17Y29sb3J9IGRpbUNvbG9yPXtkaW1Db2xvcn0gd3JhcD1cIndyYXBcIj5cbiAgICAgICAgICB7Y2hpbGRyZW59XG4gICAgICAgIDwvVGV4dD5cbiAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQTtBQUNBLE9BQU9BLEtBQUssSUFBSUMsT0FBTyxRQUFRLE9BQU87QUFDdEMsU0FBU0MsSUFBSSxFQUFFQyxHQUFHLEVBQUVDLElBQUksUUFBUSxjQUFjO0FBQzlDLGNBQWNDLFVBQVUsUUFBUSwwQkFBMEI7QUFDMUQsY0FBY0MsMkJBQTJCLFFBQVEsK0JBQStCO0FBQ2hGLFNBQVNDLFdBQVcsUUFBUSx5QkFBeUI7QUFDckQsU0FBU0MsY0FBYyxRQUFR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 \ No newline at end of file +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZU1lbW8iLCJBbnNpIiwiQm94IiwiVGV4dCIsIkF0dGFjaG1lbnQiLCJOdWxsUmVuZGVyaW5nQXR0YWNobWVudFR5cGUiLCJ1c2VBcHBTdGF0ZSIsImdldERpc3BsYXlQYXRoIiwiZm9ybWF0RmlsZVNpemUiLCJNZXNzYWdlUmVzcG9uc2UiLCJiYXNlbmFtZSIsInNlcCIsIlVzZXJUZXh0TWVzc2FnZSIsIkRpYWdub3N0aWNzRGlzcGxheSIsImdldENvbnRlbnRUZXh0IiwiVGhlbWUiLCJVc2VySW1hZ2VNZXNzYWdlIiwidG9JbmtDb2xvciIsImpzb25QYXJzZSIsInBsdXJhbCIsImlzRW52VHJ1dGh5IiwiaXNBZ2VudFN3YXJtc0VuYWJsZWQiLCJ0cnlSZW5kZXJQbGFuQXBwcm92YWxNZXNzYWdlIiwiZm9ybWF0VGVhbW1hdGVNZXNzYWdlQ29udGVudCIsIkJMQUNLX0NJUkNMRSIsIlRlYW1tYXRlTWVzc2FnZUNvbnRlbnQiLCJpc1NodXRkb3duQXBwcm92ZWQiLCJDdHJsT1RvRXhwYW5kIiwiRmlsZVBhdGhMaW5rIiwiZmVhdHVyZSIsInVzZVNlbGVjdGVkTWVzc2FnZUJnIiwiUHJvcHMiLCJhZGRNYXJnaW4iLCJhdHRhY2htZW50IiwidmVyYm9zZSIsImlzVHJhbnNjcmlwdE1vZGUiLCJBdHRhY2htZW50TWVzc2FnZSIsIlJlYWN0Tm9kZSIsImJnIiwiaXNEZW1vRW52IiwicHJvY2VzcyIsImVudiIsIklTX0RFTU8iLCJ0eXBlIiwidmlzaWJsZU1lc3NhZ2VzIiwibWVzc2FnZXMiLCJmaWx0ZXIiLCJtc2ciLCJ0ZXh0IiwicGFyc2VkIiwibGVuZ3RoIiwibWFwIiwiaWR4IiwicGFyc2VkTXNnIiwidGFza0lkIiwic3ViamVjdCIsImFzc2lnbmVkQnkiLCJmcm9tIiwicGxhbkFwcHJvdmFsRWxlbWVudCIsImlua0NvbG9yIiwiY29sb3IiLCJmb3JtYXR0ZWRDb250ZW50Iiwic3VtbWFyeSIsInNraWxscyIsIm5hbWVzIiwicyIsInNob3J0SWQiLCJuYW1lIiwiam9pbiIsImZpcnN0SWQiLCJoaW50IiwiZGlzcGxheVBhdGgiLCJjb250ZW50IiwiZmlsZSIsImNlbGxzIiwibnVtTGluZXMiLCJ0cnVuY2F0ZWQiLCJvcmlnaW5hbFNpemUiLCJwYWdlQ291bnQiLCJsaW5lRW5kIiwibGluZVN0YXJ0IiwiaWRlTmFtZSIsIm1lbW9yaWVzIiwibSIsInBhdGgiLCJza2lsbENvdW50Iiwic2tpbGxOYW1lcyIsImlzSW5pdGlhbCIsImFkZGVkVHlwZXMiLCJjb3VudCIsInByb21wdCIsImhhc0ltYWdlcyIsImltYWdlUGFzdGVJZHMiLCJpZCIsInBsYW5GaWxlUGF0aCIsInNlcnZlciIsImhvb2tFdmVudCIsInN0ZGVyciIsImJsb2NraW5nRXJyb3IiLCJ0cmltIiwiaG9va05hbWUiLCJtZXNzYWdlIiwiYWN0aW9uIiwiZGVjaXNpb24iLCJUYXNrU3RhdHVzQXR0YWNobWVudCIsIkV4dHJhY3QiLCJUYXNrU3RhdHVzTWVzc2FnZSIsInQwIiwiJCIsIl9jIiwic3RhdHVzIiwidGFza1R5cGUiLCJ0MSIsIkdlbmVyaWNUYXNrU3RhdHVzIiwic3RhdHVzVGV4dCIsIlN5bWJvbCIsImZvciIsInQyIiwiZGVzY3JpcHRpb24iLCJ0MyIsInQ0IiwiVGVhbW1hdGVUYXNrU3RhdHVzIiwidGFza3MiLCJ0YXNrIiwiaWRlbnRpdHkiLCJhZ2VudENvbG9yIiwiYWdlbnROYW1lIiwidDUiLCJ0NiIsIkxpbmUiLCJkaW1Db2xvciIsImNoaWxkcmVuIiwidW5kZWZpbmVkIl0sInNvdXJjZXMiOlsiQXR0YWNobWVudE1lc3NhZ2UudHN4Il0sInNvdXJjZXNDb250ZW50IjpbIi8vIGJpb21lLWlnbm9yZS1hbGwgYXNzaXN0L3NvdXJjZS9vcmdhbml6ZUltcG9ydHM6IEFOVC1PTkxZIGltcG9ydCBtYXJrZXJzIG11c3Qgbm90IGJlIHJlb3JkZXJlZFxuaW1wb3J0IFJlYWN0LCB7IHVzZU1lbW8gfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEFuc2ksIEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgQXR0YWNobWVudCB9IGZyb20gJ3NyYy91dGlscy9hdHRhY2htZW50cy5qcydcbmltcG9ydCB0eXBlIHsgTnVsbFJlbmRlcmluZ0F0dGFjaG1lbnRUeXBlIH0gZnJvbSAnLi9udWxsUmVuZGVyaW5nQXR0YWNobWVudHMuanMnXG5pbXBvcnQgeyB1c2VBcHBTdGF0ZSB9IGZyb20gJy4uLy4uL3N0YXRlL0FwcFN0YXRlLmpzJ1xuaW1wb3J0IHsgZ2V0RGlzcGxheVBhdGggfSBmcm9tICdzcmMvdXRpbHMvZmlsZS5qcydcbmltcG9ydCB7IGZvcm1hdEZpbGVTaXplIH0gZnJvbSAnc3JjL3V0aWxzL2Zvcm1hdC5qcydcbmltcG9ydCB7IE1lc3NhZ2VSZXNwb25zZSB9IGZyb20gJy4uL01lc3NhZ2VSZXNwb25zZS5qcydcbmltcG9ydCB7IGJhc2VuYW1lLCBzZXAgfSBmcm9tICdwYXRoJ1xuaW1wb3J0IHsgVXNlclRleHRNZXNzYWdlIH0gZnJvbSAnLi9Vc2VyVGV4dE1lc3NhZ2UuanMnXG5pbXBvcnQgeyBEaWFnbm9zdGljc0Rpc3BsYXkgfSBmcm9tICcuLi9EaWFnbm9zdGljc0Rpc3BsYXkuanMnXG5pbXBvcnQgeyBnZXRDb250ZW50VGV4dCB9IGZyb20gJ3NyYy91dGlscy9tZXNzYWdlcy5qcydcbmltcG9ydCB0eXBlIHsgVGhlbWUgfSBmcm9tICdzcmMvdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgeyBVc2VySW1hZ2VNZXNzYWdlIH0gZnJvbSAnLi9Vc2VySW1hZ2VNZXNzYWdlLmpzJ1xuaW1wb3J0IHsgdG9JbmtDb2xvciB9IGZyb20gJy4uLy4uL3V0aWxzL2luay5qcydcbmltcG9ydCB7IGpzb25QYXJzZSB9IGZyb20gJy4uLy4uL3V0aWxzL3Nsb3dPcGVyYXRpb25zLmpzJ1xuaW1wb3J0IHsgcGx1cmFsIH0gZnJvbSAnLi4vLi4vdXRpbHMvc3RyaW5nVXRpbHMuanMnXG5pbXBvcnQgeyBpc0VudlRydXRoeSB9IGZyb20gJy4uLy4uL3V0aWxzL2VudlV0aWxzLmpzJ1xuaW1wb3J0IHsgaXNBZ2VudFN3YXJtc0VuYWJsZWQgfSBmcm9tICcuLi8uLi91dGlscy9hZ2VudFN3YXJtc0VuYWJsZWQuanMnXG5pbXBvcnQge1xuICB0cnlSZW5kZXJQbGFuQXBwcm92YWxNZXNzYWdlLFxuICBmb3JtYXRUZWFtbWF0ZU1lc3NhZ2VDb250ZW50LFxufSBmcm9tICcuL1BsYW5BcHByb3ZhbE1lc3NhZ2UuanMnXG5pbXBvcnQgeyBCTEFDS19DSVJDTEUgfSBmcm9tICcuLi8uLi9jb25zdGFudHMvZmlndXJlcy5qcydcbmltcG9ydCB7IFRlYW1tYXRlTWVzc2FnZUNvbnRlbnQgfSBmcm9tICcuL1VzZXJUZWFtbWF0ZU1lc3NhZ2UuanMnXG5pbXBvcnQgeyBpc1NodXRkb3duQXBwcm92ZWQgfSBmcm9tICcuLi8uLi91dGlscy90ZWFtbWF0ZU1haWxib3guanMnXG5pbXBvcnQgeyBDdHJsT1RvRXhwYW5kIH0gZnJvbSAnLi4vQ3RybE9Ub0V4cGFuZC5qcydcbmltcG9ydCB7IEZpbGVQYXRoTGluayB9IGZyb20gJy4uL0ZpbGVQYXRoTGluay5qcydcbmltcG9ydCB7IGZlYXR1cmUgfSBmcm9tICdidW46YnVuZGxlJ1xuaW1wb3J0IHsgdXNlU2VsZWN0ZWRNZXNzYWdlQmcgfSBmcm9tICcuLi9tZXNzYWdlQWN0aW9ucy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgYWRkTWFyZ2luOiBib29sZWFuXG4gIGF0dGFjaG1lbnQ6IEF0dGFjaG1lbnRcbiAgdmVyYm9zZTogYm9vbGVhblxuICBpc1RyYW5zY3JpcHRNb2RlPzogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gQXR0YWNobWVudE1lc3NhZ2Uoe1xuICBhdHRhY2htZW50LFxuICBhZGRNYXJnaW4sXG4gIHZlcmJvc2UsXG4gIGlzVHJhbnNjcmlwdE1vZGUsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGJnID0gdXNlU2VsZWN0ZWRNZXNzYWdlQmcoKVxuICAvLyBIb2lzdGVkIHRvIG1vdW50LXRpbWUg4oCUIHBlci1tZXNzYWdlIGNvbXBvbmVudCwgcmUtcmVuZGVycyBvbiBldmVyeSBzY3JvbGwuXG4gIGNvbnN0IGlzRGVtb0VudiA9IGZlYXR1cmUoJ0VYUEVSSU1FTlRBTF9TS0lMTF9TRUFSQ0gnKVxuICAgID8gLy8gYmlvbWUtaWdub3JlIGxpbnQvY29ycmVjdG5lc3MvdXNlSG9va0F0VG9wTGV2ZWw6IGZlYXR1cmUoKSBpcyBhIGNvbXBpbGUtdGltZSBjb25zdGFudFxuICAgICAgdXNlTWVtbygoKSA9PiBpc0VudlRydXRoeShwcm9jZXNzLmVudi5JU19ERU1PKSwgW10pXG4gICAgOiBmYWxzZVxuICAvLyBIYW5kbGUgdGVhbW1hdGVfbWFpbGJveCBCRUZPUkUgc3dpdGNoXG4gIGlmIChpc0FnZW50U3dhcm1zRW5hYmxlZCgpICYmIGF0dGFjaG1lbnQudHlwZSA9PT0gJ3RlYW1tYXRlX21haWxib3gnKSB7XG4gICAgLy8gRmlsdGVyIG91dCBpZGxlIG5vdGlmaWNhdGlvbnMgQkVGT1JFIGNvdW50aW5nIC0gdGhleSBhcmUgaGlkZGVuIGluIHRoZSBVSVxuICAgIC8vIHNvIHNob3dpbmcgdGhlbSBpbiB0aGUgY291bnQgd291bGQgYmUgY29uZnVzaW5nIChcIjIgbWVzc2FnZXMgaW4gbWFpbGJveDpcIiB3aXRoIG5vdGhpbmcgc2hvd24pXG4gICAgY29uc3QgdmlzaWJsZU1lc3NhZ2VzID0gYXR0YWNobWVudC5tZXNzYWdlcy5maWx0ZXIobXNnID0+IHtcbiAgICAgIGlmIChpc1NodXRkb3duQXBwcm92ZWQobXNnLnRleHQpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZVxuICAgICAgfVxuICAgICAgdHJ5IHtcbiAgICAgICAgY29uc3QgcGFyc2VkID0ganNvblBhcnNlKG1zZy50ZXh0KVxuICAgICAgICByZXR1cm4gKFxuICAgICAgICAgIHBhcnNlZD8udHlwZSAhPT0gJ2lkbGVfbm90aWZpY2F0aW9uJyAmJlxuICAgICAgICAgIHBhcnNlZD8udHlwZSAhPT0gJ3RlYW1tYXRlX3Rlcm1pbmF0ZWQnXG4gICAgICAgIClcbiAgICAgIH0gY2F0Y2gge1xuICAgICAgICByZXR1cm4gdHJ1ZSAvLyBOb24tSlNPTiBtZXNzYWdlcyBhcmUgdmlzaWJsZVxuICAgICAgfVxuICAgIH0pXG5cbiAgICBpZiAodmlzaWJsZU1lc3NhZ2VzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgcmV0dXJuIG51bGxcbiAgICB9XG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICB7dmlzaWJsZU1lc3NhZ2VzLm1hcCgobXNnLCBpZHgpID0+IHtcbiAgICAgICAgICAvLyBUcnkgdG8gcGFyc2UgYXMgSlNPTiBmb3IgdGFza19hc3NpZ25tZW50IG1lc3NhZ2VzXG4gICAgICAgICAgbGV0IHBhcnNlZE1zZzoge1xuICAgICAgICAgICAgdHlwZT86IHN0cmluZ1xuICAgICAgICAgICAgdGFza0lkPzogc3RyaW5nXG4gICAgICAgICAgICBzdWJqZWN0Pzogc3RyaW5nXG4gICAgICAgICAgICBhc3NpZ25lZEJ5Pzogc3RyaW5nXG4gICAgICAgICAgfSB8IG51bGwgPSBudWxsXG4gICAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIHBhcnNlZE1zZyA9IGpzb25QYXJzZShtc2cudGV4dClcbiAgICAgICAgICB9IGNhdGNoIHtcbiAgICAgICAgICAgIC8vIE5vdCBKU09OLCB0cmVhdCBhcyBwbGFpbiB0ZXh0XG4gICAgICAgICAgfVxuXG4gICAgICAgICAgaWYgKHBhcnNlZE1zZz8udHlwZSA9PT0gJ3Rhc2tfYXNzaWdubWVudCcpIHtcbiAgICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICAgIDxCb3gga2V5PXtpZHh9IHBhZGRpbmdMZWZ0PXsyfT5cbiAgICAgICAgICAgICAgICA8VGV4dD57QkxBQ0tfQ0lSQ0xFfSA8L1RleHQ+XG4gICAgICAgICAgICAgICAgPFRleHQ+VGFzayBhc3NpZ25lZDogPC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGJvbGQ+I3twYXJzZWRNc2cudGFza0lkfTwvVGV4dD5cbiAgICAgICAgICAgICAgICA8VGV4dD4gLSB7cGFyc2VkTXNnLnN1YmplY3R9PC9UZXh0PlxuICAgICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiAoZnJvbSB7cGFyc2VkTXNnLmFzc2lnbmVkQnkgfHwgbXNnLmZyb219KTwvVGV4dD5cbiAgICAgICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgICApXG4gICAgICAgICAgfVxuXG4gICAgICAgICAgLy8gTm90ZTogaWRsZV9ub3RpZmljYXRpb24gbWVzc2FnZXMgYWxyZWFkeSBmaWx0ZXJlZCBvdXQgYWJvdmVcblxuICAgICAgICAgIC8vIFRyeSB0byByZW5kZXIgYXMgcGxhbiBhcHByb3ZhbCBtZXNzYWdlIChyZXF1ZXN0IG9yIHJlc3BvbnNlKVxuICAgICAgICAgIGNvbnN0IHBsYW5BcHByb3ZhbEVsZW1lbnQgPSB0cnlSZW5kZXJQbGFuQXBwcm92YWxNZXNzYWdlKFxuICAgICAgICAgICAgbXNnLnRleHQsXG4gICAgICAgICAgICBtc2cuZnJvbSxcbiAgICAgICAgICApXG4gICAgICAgICAgaWYgKHBsYW5BcHByb3ZhbEVsZW1lbnQpIHtcbiAgICAgICAgICAgIHJldHVybiAoXG4gICAgICAgICAgICAgIDxSZWFjdC5GcmFnbWVudCBrZXk9e2lkeH0+e3BsYW5BcHByb3ZhbEVsZW1lbnR9PC9SZWFjdC5GcmFnbWVudD5cbiAgICAgICAgICAgIClcbiAgICAgICAgICB9XG5cbiAgICAgICAgICAvLyBQbGFpbiB0ZXh0IG1lc3NhZ2UgLSBzZW5kZXIgaGVhZGVyIHdpdGggY2hldnJvbiwgdHJ1bmNhdGVkIGNvbnRlbnRcbiAgICAgICAgICBjb25zdCBpbmtDb2xvciA9IHRvSW5rQ29sb3IobXNnLmNvbG9yKVxuICAgICAgICAgIGNvbnN0IGZvcm1hdHRlZENvbnRlbnQgPVxuICAgICAgICAgICAgZm9ybWF0VGVhbW1hdGVNZXNzYWdlQ29udGVudChtc2cudGV4dCkgPz8gbXNnLnRleHRcbiAgICAgICAgICByZXR1cm4gKFxuICAgICAgICAgICAgPFRlYW1tYXRlTWVzc2FnZUNvbnRlbnRcbiAgICAgICAgICAgICAga2V5PXtpZHh9XG4gICAgICAgICAgICAgIGRpc3BsYXlOYW1lPXttc2cuZnJvbX1cbiAgICAgICAgICAgICAgaW5rQ29sb3I9e2lua0NvbG9yfVxuICAgICAgICAgICAgICBjb250ZW50PXtmb3JtYXR0ZWRDb250ZW50fVxuICAgICAgICAgICAgICBzdW1tYXJ5PXttc2cuc3VtbWFyeX1cbiAgICAgICAgICAgICAgaXNUcmFuc2NyaXB0TW9kZT17aXNUcmFuc2NyaXB0TW9kZX1cbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgKVxuICAgICAgICB9KX1cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuXG4gIC8vIHNraWxsX2Rpc2NvdmVyeSByZW5kZXJlZCBoZXJlIChub3QgaW4gdGhlIHN3aXRjaCkgc28gdGhlICdza2lsbF9kaXNjb3ZlcnknXG4gIC8vIHN0cmluZyBsaXRlcmFsIHN0YXlzIGluc2lkZSBhIGZlYXR1cmUoKS1ndWFyZGVkIGJsb2NrLiBBIGNhc2UgbGFiZWwgY2FuJ3RcbiAgLy8gYmUgY29uZGl0aW9uYWxseSBlbGltaW5hdGVkOyBhbiBpZi1ib2R5IGNhbi5cbiAgaWYgKGZlYXR1cmUoJ0VYUEVSSU1FTlRBTF9TS0lMTF9TRUFSQ0gnKSkge1xuICAgIGlmIChhdHRhY2htZW50LnR5cGUgPT09ICdza2lsbF9kaXNjb3ZlcnknKSB7XG4gICAgICBpZiAoYXR0YWNobWVudC5za2lsbHMubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbFxuICAgICAgLy8gQW50IHVzZXJzIGdldCBzaG9ydElkcyBpbmxpbmUgc28gdGhleSBjYW4gL3NraWxsLWZlZWRiYWNrIHdoaWxlIHRoZVxuICAgICAgLy8gdHVybiBpcyBzdGlsbCBmcmVzaC4gRXh0ZXJuYWwgdXNlcnMgKHdoZW4gdGhpcyB1bi1nYXRlcykganVzdCBzZWVcbiAgICAgIC8vIG5hbWVzIOKAlCBzaG9ydElkIGlzIHVuZGVmaW5lZCBvdXRzaWRlIGFudCBidWlsZHMgYW55d2F5LlxuICAgICAgY29uc3QgbmFtZXMgPSBhdHRhY2htZW50LnNraWxsc1xuICAgICAgICAubWFwKHMgPT4gKHMuc2hvcnRJZCA/IGAke3MubmFtZX0gWyR7cy5zaG9ydElkfV1gIDogcy5uYW1lKSlcbiAgICAgICAgLmpvaW4oJywgJylcbiAgICAgIGNvbnN0IGZpcnN0SWQgPSBhdHRhY2htZW50LnNraWxsc1swXT8uc2hvcnRJZFxuICAgICAgY29uc3QgaGludCA9XG4gICAgICAgIFwiZXh0ZXJuYWxcIiA9PT0gJ2FudCcgJiYgIWlzRGVtb0VudiAmJiBmaXJzdElkXG4gICAgICAgICAgPyBgIMK3IC9za2lsbC1mZWVkYmFjayAke2ZpcnN0SWR9IDE9d3JvbmcgMj1ub2lzeSAzPWdvb2QgW2NvbW1lbnRdYFxuICAgICAgICAgIDogJydcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuc2tpbGxzLmxlbmd0aH08L1RleHQ+IHJlbGV2YW50eycgJ31cbiAgICAgICAgICB7cGx1cmFsKGF0dGFjaG1lbnQuc2tpbGxzLmxlbmd0aCwgJ3NraWxsJyl9OiB7bmFtZXN9XG4gICAgICAgICAge2hpbnQgJiYgPFRleHQgZGltQ29sb3I+e2hpbnR9PC9UZXh0Pn1cbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIH1cbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvc3dpdGNoLWV4aGF1c3RpdmVuZXNzLWNoZWNrIC0tIHRlYW1tYXRlX21haWxib3gvc2tpbGxfZGlzY292ZXJ5IGhhbmRsZWQgYmVmb3JlIHN3aXRjaFxuICBzd2l0Y2ggKGF0dGFjaG1lbnQudHlwZSkge1xuICAgIGNhc2UgJ2RpcmVjdG9yeSc6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICBMaXN0ZWQgZGlyZWN0b3J5IDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGlzcGxheVBhdGggKyBzZXB9PC9UZXh0PlxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAnZmlsZSc6XG4gICAgY2FzZSAnYWxyZWFkeV9yZWFkX2ZpbGUnOlxuICAgICAgaWYgKGF0dGFjaG1lbnQuY29udGVudC50eXBlID09PSAnbm90ZWJvb2snKSB7XG4gICAgICAgIHJldHVybiAoXG4gICAgICAgICAgPExpbmU+XG4gICAgICAgICAgICBSZWFkIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGlzcGxheVBhdGh9PC9UZXh0PiAoXG4gICAgICAgICAgICB7YXR0YWNobWVudC5jb250ZW50LmZpbGUuY2VsbHMubGVuZ3RofSBjZWxscylcbiAgICAgICAgICA8L0xpbmU+XG4gICAgICAgIClcbiAgICAgIH1cbiAgICAgIGlmIChhdHRhY2htZW50LmNvbnRlbnQudHlwZSA9PT0gJ2ZpbGVfdW5jaGFuZ2VkJykge1xuICAgICAgICByZXR1cm4gKFxuICAgICAgICAgIDxMaW5lPlxuICAgICAgICAgICAgUmVhZCA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD4gKHVuY2hhbmdlZClcbiAgICAgICAgICA8L0xpbmU+XG4gICAgICAgIClcbiAgICAgIH1cbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIFJlYWQgPFRleHQgYm9sZD57YXR0YWNobWVudC5kaXNwbGF5UGF0aH08L1RleHQ+IChcbiAgICAgICAgICB7YXR0YWNobWVudC5jb250ZW50LnR5cGUgPT09ICd0ZXh0J1xuICAgICAgICAgICAgPyBgJHthdHRhY2htZW50LmNvbnRlbnQuZmlsZS5udW1MaW5lc30ke2F0dGFjaG1lbnQudHJ1bmNhdGVkID8gJysnIDogJyd9IGxpbmVzYFxuICAgICAgICAgICAgOiBmb3JtYXRGaWxlU2l6ZShhdHRhY2htZW50LmNvbnRlbnQuZmlsZS5vcmlnaW5hbFNpemUpfVxuICAgICAgICAgIClcbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ2NvbXBhY3RfZmlsZV9yZWZlcmVuY2UnOlxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAgUmVmZXJlbmNlZCBmaWxlIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGlzcGxheVBhdGh9PC9UZXh0PlxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAncGRmX3JlZmVyZW5jZSc6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICBSZWZlcmVuY2VkIFBERiA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD4gKFxuICAgICAgICAgIHthdHRhY2htZW50LnBhZ2VDb3VudH0gcGFnZXMpXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICBjYXNlICdzZWxlY3RlZF9saW5lc19pbl9pZGUnOlxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAg4qeJIFNlbGVjdGVkeycgJ31cbiAgICAgICAgICA8VGV4dCBib2xkPnthdHRhY2htZW50LmxpbmVFbmQgLSBhdHRhY2htZW50LmxpbmVTdGFydCArIDF9PC9UZXh0PnsnICd9XG4gICAgICAgICAgbGluZXMgZnJvbSA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD4gaW57JyAnfVxuICAgICAgICAgIHthdHRhY2htZW50LmlkZU5hbWV9XG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICBjYXNlICduZXN0ZWRfbWVtb3J5JzpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIExvYWRlZCA8VGV4dCBib2xkPnthdHRhY2htZW50LmRpc3BsYXlQYXRofTwvVGV4dD5cbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ3JlbGV2YW50X21lbW9yaWVzJzpcbiAgICAgIC8vIFVzdWFsbHkgYWJzb3JiZWQgaW50byBhIENvbGxhcHNlZFJlYWRTZWFyY2hHcm91cCAoY29sbGFwc2VSZWFkU2VhcmNoLnRzKVxuICAgICAgLy8gc28gdGhpcyBvbmx5IHJlbmRlcnMgd2hlbiB0aGUgcHJlY2VkaW5nIHRvb2wgd2FzIG5vbi1jb2xsYXBzaWJsZSAoRWRpdCxcbiAgICAgIC8vIFdyaXRlKSBhbmQgbm8gZ3JvdXAgd2FzIG9wZW4uIE1hdGNoIENvbGxhcHNlZFJlYWRTZWFyY2hDb250ZW50J3Mgc3R5bGU6XG4gICAgICAvLyAyLXNwYWNlIGd1dHRlciwgZGltIHRleHQsIGNvdW50IG9ubHkg4oCUIGZpbGVuYW1lcy9jb250ZW50IGluIGN0cmwrby5cbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxCb3hcbiAgICAgICAgICBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCJcbiAgICAgICAgICBtYXJnaW5Ub3A9e2FkZE1hcmdpbiA/IDEgOiAwfVxuICAgICAgICAgIGJhY2tncm91bmRDb2xvcj17Ymd9XG4gICAgICAgID5cbiAgICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIj5cbiAgICAgICAgICAgIDxCb3ggbWluV2lkdGg9ezJ9IC8+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgUmVjYWxsZWQgPFRleHQgYm9sZD57YXR0YWNobWVudC5tZW1vcmllcy5sZW5ndGh9PC9UZXh0PnsnICd9XG4gICAgICAgICAgICAgIHthdHRhY2htZW50Lm1lbW9yaWVzLmxlbmd0aCA9PT0gMSA/ICdtZW1vcnknIDogJ21lbW9yaWVzJ31cbiAgICAgICAgICAgICAgeyFpc1RyYW5zY3JpcHRNb2RlICYmIChcbiAgICAgICAgICAgICAgICA8PlxuICAgICAgICAgICAgICAgICAgeycgJ31cbiAgICAgICAgICAgICAgICAgIDxDdHJsT1RvRXhwYW5kIC8+XG4gICAgICAgICAgICAgICAgPC8+XG4gICAgICAgICAgICAgICl9XG4gICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgeyh2ZXJib3NlIHx8IGlzVHJhbnNjcmlwdE1vZGUpICYmXG4gICAgICAgICAgICBhdHRhY2htZW50Lm1lbW9yaWVzLm1hcChtID0+IChcbiAgICAgICAgICAgICAgPEJveCBrZXk9e20ucGF0aH0gZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgICAgICAgIDxNZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgICAgPEZpbGVQYXRoTGluayBmaWxlUGF0aD17bS5wYXRofT5cbiAgICAgICAgICAgICAgICAgICAgICB7YmFzZW5hbWUobS5wYXRoKX1cbiAgICAgICAgICAgICAgICAgICAgPC9GaWxlUGF0aExpbms+XG4gICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgPC9NZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgICAgICAgICAge2lzVHJhbnNjcmlwdE1vZGUgJiYgKFxuICAgICAgICAgICAgICAgICAgPEJveCBwYWRkaW5nTGVmdD17NX0+XG4gICAgICAgICAgICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgICAgICAgICAgIDxBbnNpPnttLmNvbnRlbnR9PC9BbnNpPlxuICAgICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICkpfVxuICAgICAgICA8L0JveD5cbiAgICAgIClcbiAgICBjYXNlICdkeW5hbWljX3NraWxsJzoge1xuICAgICAgY29uc3Qgc2tpbGxDb3VudCA9IGF0dGFjaG1lbnQuc2tpbGxOYW1lcy5sZW5ndGhcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIExvYWRlZHsnICd9XG4gICAgICAgICAgPFRleHQgYm9sZD5cbiAgICAgICAgICAgIHtza2lsbENvdW50fSB7cGx1cmFsKHNraWxsQ291bnQsICdza2lsbCcpfVxuICAgICAgICAgIDwvVGV4dD57JyAnfVxuICAgICAgICAgIGZyb20gPFRleHQgYm9sZD57YXR0YWNobWVudC5kaXNwbGF5UGF0aH08L1RleHQ+XG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAnc2tpbGxfbGlzdGluZyc6IHtcbiAgICAgIGlmIChhdHRhY2htZW50LmlzSW5pdGlhbCkge1xuICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgfVxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAgPFRleHQgYm9sZD57YXR0YWNobWVudC5za2lsbENvdW50fTwvVGV4dD57JyAnfVxuICAgICAgICAgIHtwbHVyYWwoYXR0YWNobWVudC5za2lsbENvdW50LCAnc2tpbGwnKX0gYXZhaWxhYmxlXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAnYWdlbnRfbGlzdGluZ19kZWx0YSc6IHtcbiAgICAgIGlmIChhdHRhY2htZW50LmlzSW5pdGlhbCB8fCBhdHRhY2htZW50LmFkZGVkVHlwZXMubGVuZ3RoID09PSAwKSB7XG4gICAgICAgIHJldHVybiBudWxsXG4gICAgICB9XG4gICAgICBjb25zdCBjb3VudCA9IGF0dGFjaG1lbnQuYWRkZWRUeXBlcy5sZW5ndGhcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIDxUZXh0IGJvbGQ+e2NvdW50fTwvVGV4dD4gYWdlbnQge3BsdXJhbChjb3VudCwgJ3R5cGUnKX0gYXZhaWxhYmxlXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAncXVldWVkX2NvbW1hbmQnOiB7XG4gICAgICBjb25zdCB0ZXh0ID1cbiAgICAgICAgdHlwZW9mIGF0dGFjaG1lbnQucHJvbXB0ID09PSAnc3RyaW5nJ1xuICAgICAgICAgID8gYXR0YWNobWVudC5wcm9tcHRcbiAgICAgICAgICA6IGdldENvbnRlbnRUZXh0KGF0dGFjaG1lbnQucHJvbXB0KSB8fCAnJ1xuICAgICAgY29uc3QgaGFzSW1hZ2VzID1cbiAgICAgICAgYXR0YWNobWVudC5pbWFnZVBhc3RlSWRzICYmIGF0dGFjaG1lbnQuaW1hZ2VQYXN0ZUlkcy5sZW5ndGggPiAwXG4gICAgICByZXR1cm4gKFxuICAgICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAgICA8VXNlclRleHRNZXNzYWdlXG4gICAgICAgICAgICBhZGRNYXJnaW49e2FkZE1hcmdpbn1cbiAgICAgICAgICAgIHBhcmFtPXt7IHRleHQsIHR5cGU6ICd0ZXh0JyB9fVxuICAgICAgICAgICAgdmVyYm9zZT17dmVyYm9zZX1cbiAgICAgICAgICAgIGlzVHJhbnNjcmlwdE1vZGU9e2lzVHJhbnNjcmlwdE1vZGV9XG4gICAgICAgICAgLz5cbiAgICAgICAgICB7aGFzSW1hZ2VzICYmXG4gICAgICAgICAgICBhdHRhY2htZW50LmltYWdlUGFzdGVJZHM/Lm1hcChpZCA9PiAoXG4gICAgICAgICAgICAgIDxVc2VySW1hZ2VNZXNzYWdlIGtleT17aWR9IGltYWdlSWQ9e2lkfSAvPlxuICAgICAgICAgICAgKSl9XG4gICAgICAgIDwvQm94PlxuICAgICAgKVxuICAgIH1cbiAgICBjYXNlICdwbGFuX2ZpbGVfcmVmZXJlbmNlJzpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lPlxuICAgICAgICAgIFBsYW4gZmlsZSByZWZlcmVuY2VkICh7Z2V0RGlzcGxheVBhdGgoYXR0YWNobWVudC5wbGFuRmlsZVBhdGgpfSlcbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ2ludm9rZWRfc2tpbGxzJzoge1xuICAgICAgaWYgKGF0dGFjaG1lbnQuc2tpbGxzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgfVxuICAgICAgY29uc3Qgc2tpbGxOYW1lcyA9IGF0dGFjaG1lbnQuc2tpbGxzLm1hcChzID0+IHMubmFtZSkuam9pbignLCAnKVxuICAgICAgcmV0dXJuIDxMaW5lPlNraWxscyByZXN0b3JlZCAoe3NraWxsTmFtZXN9KTwvTGluZT5cbiAgICB9XG4gICAgY2FzZSAnZGlhZ25vc3RpY3MnOlxuICAgICAgcmV0dXJuIDxEaWFnbm9zdGljc0Rpc3BsYXkgYXR0YWNobWVudD17YXR0YWNobWVudH0gdmVyYm9zZT17dmVyYm9zZX0gLz5cbiAgICBjYXNlICdtY3BfcmVzb3VyY2UnOlxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAgUmVhZCBNQ1AgcmVzb3VyY2UgPFRleHQgYm9sZD57YXR0YWNobWVudC5uYW1lfTwvVGV4dD4gZnJvbXsnICd9XG4gICAgICAgICAge2F0dGFjaG1lbnQuc2VydmVyfVxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAnY29tbWFuZF9wZXJtaXNzaW9ucyc6XG4gICAgICAvLyBUaGUgc2tpbGwgc3VjY2VzcyBtZXNzYWdlIGlzIHJlbmRlcmVkIGJ5IFNraWxsVG9vbCdzIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlLFxuICAgICAgLy8gc28gd2UgZG9uJ3QgcmVuZGVyIGFueXRoaW5nIGhlcmUgdG8gYXZvaWQgZHVwbGljYXRlIG1lc3NhZ2VzLlxuICAgICAgcmV0dXJuIG51bGxcbiAgICBjYXNlICdhc3luY19ob29rX3Jlc3BvbnNlJzoge1xuICAgICAgLy8gU2Vzc2lvblN0YXJ0IGhvb2sgY29tcGxldGlvbnMgYXJlIG9ubHkgc2hvd24gaW4gdmVyYm9zZSBtb2RlXG4gICAgICBpZiAoYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTZXNzaW9uU3RhcnQnICYmICF2ZXJib3NlKSB7XG4gICAgICAgIHJldHVybiBudWxsXG4gICAgICB9XG4gICAgICAvLyBHZW5lcmFsbHkgaGlkZSBhc3luYyBob29rIGNvbXBsZXRpb24gbWVzc2FnZXMgdW5sZXNzIGluIHZlcmJvc2UgbW9kZVxuICAgICAgaWYgKCF2ZXJib3NlICYmICFpc1RyYW5zY3JpcHRNb2RlKSB7XG4gICAgICAgIHJldHVybiBudWxsXG4gICAgICB9XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICBBc3luYyBob29rIDxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuaG9va0V2ZW50fTwvVGV4dD4gY29tcGxldGVkXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAnaG9va19ibG9ja2luZ19lcnJvcic6IHtcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIC8vIFNob3cgc3RkZXJyIHRvIHRoZSB1c2VyIHNvIHRoZXkgY2FuIHVuZGVyc3RhbmQgd2h5IHRoZSBob29rIGJsb2NrZWRcbiAgICAgIGNvbnN0IHN0ZGVyciA9IGF0dGFjaG1lbnQuYmxvY2tpbmdFcnJvci5ibG9ja2luZ0Vycm9yLnRyaW0oKVxuICAgICAgcmV0dXJuIChcbiAgICAgICAgPD5cbiAgICAgICAgICA8TGluZSBjb2xvcj1cImVycm9yXCI+XG4gICAgICAgICAgICB7YXR0YWNobWVudC5ob29rTmFtZX0gaG9vayByZXR1cm5lZCBibG9ja2luZyBlcnJvclxuICAgICAgICAgIDwvTGluZT5cbiAgICAgICAgICB7c3RkZXJyID8gPExpbmUgY29sb3I9XCJlcnJvclwiPntzdGRlcnJ9PC9MaW5lPiA6IG51bGx9XG4gICAgICAgIDwvPlxuICAgICAgKVxuICAgIH1cbiAgICBjYXNlICdob29rX25vbl9ibG9ja2luZ19lcnJvcic6IHtcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIC8vIEZ1bGwgaG9vayBvdXRwdXQgaXMgbG9nZ2VkIHRvIGRlYnVnIGxvZyB2aWEgaG9va0V2ZW50cy50c1xuICAgICAgcmV0dXJuIDxMaW5lIGNvbG9yPVwiZXJyb3JcIj57YXR0YWNobWVudC5ob29rTmFtZX0gaG9vayBlcnJvcjwvTGluZT5cbiAgICB9XG4gICAgY2FzZSAnaG9va19lcnJvcl9kdXJpbmdfZXhlY3V0aW9uJzpcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIC8vIEZ1bGwgaG9vayBvdXRwdXQgaXMgbG9nZ2VkIHRvIGRlYnVnIGxvZyB2aWEgaG9va0V2ZW50cy50c1xuICAgICAgcmV0dXJuIDxMaW5lPnthdHRhY2htZW50Lmhvb2tOYW1lfSBob29rIHdhcm5pbmc8L0xpbmU+XG4gICAgY2FzZSAnaG9va19zdWNjZXNzJzpcbiAgICAgIC8vIEZ1bGwgaG9vayBvdXRwdXQgaXMgbG9nZ2VkIHRvIGRlYnVnIGxvZyB2aWEgaG9va0V2ZW50cy50c1xuICAgICAgcmV0dXJuIG51bGxcbiAgICBjYXNlICdob29rX3N0b3BwZWRfY29udGludWF0aW9uJzpcbiAgICAgIC8vIFN0b3AgaG9va3MgYXJlIHJlbmRlcmVkIGFzIGEgc3VtbWFyeSBpbiBTeXN0ZW1TdG9wSG9va1N1bW1hcnlNZXNzYWdlXG4gICAgICBpZiAoXG4gICAgICAgIGF0dGFjaG1lbnQuaG9va0V2ZW50ID09PSAnU3RvcCcgfHxcbiAgICAgICAgYXR0YWNobWVudC5ob29rRXZlbnQgPT09ICdTdWJhZ2VudFN0b3AnXG4gICAgICApIHtcbiAgICAgICAgcmV0dXJuIG51bGxcbiAgICAgIH1cbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxMaW5lIGNvbG9yPVwid2FybmluZ1wiPlxuICAgICAgICAgIHthdHRhY2htZW50Lmhvb2tOYW1lfSBob29rIHN0b3BwZWQgY29udGludWF0aW9uOiB7YXR0YWNobWVudC5tZXNzYWdlfVxuICAgICAgICA8L0xpbmU+XG4gICAgICApXG4gICAgY2FzZSAnaG9va19zeXN0ZW1fbWVzc2FnZSc6XG4gICAgICByZXR1cm4gKFxuICAgICAgICA8TGluZT5cbiAgICAgICAgICB7YXR0YWNobWVudC5ob29rTmFtZX0gc2F5czoge2F0dGFjaG1lbnQuY29udGVudH1cbiAgICAgICAgPC9MaW5lPlxuICAgICAgKVxuICAgIGNhc2UgJ2hvb2tfcGVybWlzc2lvbl9kZWNpc2lvbic6IHtcbiAgICAgIGNvbnN0IGFjdGlvbiA9IGF0dGFjaG1lbnQuZGVjaXNpb24gPT09ICdhbGxvdycgPyAnQWxsb3dlZCcgOiAnRGVuaWVkJ1xuICAgICAgcmV0dXJuIChcbiAgICAgICAgPExpbmU+XG4gICAgICAgICAge2FjdGlvbn0gYnkgPFRleHQgYm9sZD57YXR0YWNobWVudC5ob29rRXZlbnR9PC9UZXh0PiBob29rXG4gICAgICAgIDwvTGluZT5cbiAgICAgIClcbiAgICB9XG4gICAgY2FzZSAndGFza19zdGF0dXMnOlxuICAgICAgcmV0dXJuIDxUYXNrU3RhdHVzTWVzc2FnZSBhdHRhY2htZW50PXthdHRhY2htZW50fSAvPlxuICAgIGNhc2UgJ3RlYW1tYXRlX3NodXRkb3duX2JhdGNoJzpcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxCb3hcbiAgICAgICAgICBmbGV4RGlyZWN0aW9uPVwicm93XCJcbiAgICAgICAgICB3aWR0aD1cIjEwMCVcIlxuICAgICAgICAgIG1hcmdpblRvcD17MX1cbiAgICAgICAgICBiYWNrZ3JvdW5kQ29sb3I9e2JnfVxuICAgICAgICA+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+e0JMQUNLX0NJUkNMRX0gPC9UZXh0PlxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAge2F0dGFjaG1lbnQuY291bnR9IHtwbHVyYWwoYXR0YWNobWVudC5jb3VudCwgJ3RlYW1tYXRlJyl9IHNodXQgZG93blxuICAgICAgICAgICAgZ3JhY2VmdWxseVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApXG4gICAgZGVmYXVsdDpcbiAgICAgIC8vIEV4aGF1c3RpdmVuZXNzOiBldmVyeSB0eXBlIHJlYWNoaW5nIGhlcmUgbXVzdCBiZSBpbiBOVUxMX1JFTkRFUklOR19UWVBFUy5cbiAgICAgIC8vIElmIFRTIGVycm9ycywgYSBuZXcgQXR0YWNobWVudCB0eXBlIHdhcyBhZGRlZCB3aXRob3V0IGEgY2FzZSBhYm92ZSBBTkRcbiAgICAgIC8vIHdpdGhvdXQgYW4gZW50cnkgaW4gTlVMTF9SRU5ERVJJTkdfVFlQRVMg4oCUIGRlY2lkZTogcmVuZGVyIHNvbWV0aGluZyAoYWRkXG4gICAgICAvLyBhIGNhc2UpIG9yIHJlbmRlciBub3RoaW5nIChhZGQgdG8gdGhlIGFycmF5KS4gTWVzc2FnZXMudHN4IHByZS1maWx0ZXJzXG4gICAgICAvLyB0aGVzZSBzbyB0aGlzIGJyYW5jaCBpcyBkZWZlbnNlLWluLWRlcHRoIGZvciBvdGhlciByZW5kZXIgcGF0aHMuXG4gICAgICAvL1xuICAgICAgLy8gc2tpbGxfZGlzY292ZXJ5IGFuZCB0ZWFtbWF0ZV9tYWlsYm94IGFyZSBoYW5kbGVkIEJFRk9SRSB0aGUgc3dpdGNoIGluXG4gICAgICAvLyBydW50aW1lLWdhdGVkIGJsb2NrcyAoZmVhdHVyZSgpIC8gaXNBZ2VudFN3YXJtc0VuYWJsZWQoKSkgdGhhdCBUUyBjYW4ndFxuICAgICAgLy8gbmFycm93IHRocm91Z2gg4oCUIGV4Y2x1ZGVkIGhlcmUgdmlhIHR5cGUgdW5pb24gKGNvbXBpbGUtdGltZSBvbmx5LCBubyBlbWl0KS5cbiAgICAgIGF0dGFjaG1lbnQudHlwZSBzYXRpc2ZpZXNcbiAgICAgICAgfCBOdWxsUmVuZGVyaW5nQXR0YWNobWVudFR5cGVcbiAgICAgICAgfCAnc2tpbGxfZGlzY292ZXJ5J1xuICAgICAgICB8ICd0ZWFtbWF0ZV9tYWlsYm94J1xuICAgICAgcmV0dXJuIG51bGxcbiAgfVxufVxuXG50eXBlIFRhc2tTdGF0dXNBdHRhY2htZW50ID0gRXh0cmFjdDxBdHRhY2htZW50LCB7IHR5cGU6ICd0YXNrX3N0YXR1cycgfT5cblxuZnVuY3Rpb24gVGFza1N0YXR1c01lc3NhZ2Uoe1xuICBhdHRhY2htZW50LFxufToge1xuICBhdHRhY2htZW50OiBUYXNrU3RhdHVzQXR0YWNobWVudFxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIC8vIEZvciBhbnRzLCBraWxsZWQgdGFzayBzdGF0dXMgaXMgc2hvd24gaW4gdGhlIENvb3JkaW5hdG9yVGFza1BhbmVsLlxuICAvLyBEb24ndCByZW5kZXIgaXQgYWdhaW4gaW4gdGhlIGNoYXQuXG4gIGlmIChcImV4dGVybmFsXCIgPT09ICdhbnQnICYmIGF0dGFjaG1lbnQuc3RhdHVzID09PSAna2lsbGVkJykge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBPbmx5IGFjY2VzcyB0ZWFtbWF0ZS1zcGVjaWZpYyBjb2RlIHdoZW4gc3dhcm1zIGFyZSBlbmFibGVkLlxuICAvLyBUZWFtbWF0ZVRhc2tTdGF0dXMgc3Vic2NyaWJlcyB0byBBcHBTdGF0ZTsgYnkgZ2F0aW5nIHRoZSBtb3VudCB3ZVxuICAvLyBhdm9pZCBhZGRpbmcgYSBzdG9yZSBsaXN0ZW5lciBmb3IgZXZlcnkgbm9uLXRlYW1tYXRlIGF0dGFjaG1lbnQuXG4gIGlmIChpc0FnZW50U3dhcm1zRW5hYmxlZCgpICYmIGF0dGFjaG1lbnQudGFza1R5cGUgPT09ICdpbl9wcm9jZXNzX3RlYW1tYXRlJykge1xuICAgIHJldHVybiA8VGVhbW1hdGVUYXNrU3RhdHVzIGF0dGFjaG1lbnQ9e2F0dGFjaG1lbnR9IC8+XG4gIH1cblxuICByZXR1cm4gPEdlbmVyaWNUYXNrU3RhdHVzIGF0dGFjaG1lbnQ9e2F0dGFjaG1lbnR9IC8+XG59XG5cbmZ1bmN0aW9uIEdlbmVyaWNUYXNrU3RhdHVzKHtcbiAgYXR0YWNobWVudCxcbn06IHtcbiAgYXR0YWNobWVudDogVGFza1N0YXR1c0F0dGFjaG1lbnRcbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBiZyA9IHVzZVNlbGVjdGVkTWVzc2FnZUJnKClcbiAgY29uc3Qgc3RhdHVzVGV4dCA9XG4gICAgYXR0YWNobWVudC5zdGF0dXMgPT09ICdjb21wbGV0ZWQnXG4gICAgICA/ICdjb21wbGV0ZWQgaW4gYmFja2dyb3VuZCdcbiAgICAgIDogYXR0YWNobWVudC5zdGF0dXMgPT09ICdraWxsZWQnXG4gICAgICAgID8gJ3N0b3BwZWQnXG4gICAgICAgIDogYXR0YWNobWVudC5zdGF0dXMgPT09ICdydW5uaW5nJ1xuICAgICAgICAgID8gJ3N0aWxsIHJ1bm5pbmcgaW4gYmFja2dyb3VuZCdcbiAgICAgICAgICA6IGF0dGFjaG1lbnQuc3RhdHVzXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgd2lkdGg9XCIxMDAlXCIgbWFyZ2luVG9wPXsxfSBiYWNrZ3JvdW5kQ29sb3I9e2JnfT5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPntCTEFDS19DSVJDTEV9IDwvVGV4dD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICBUYXNrICZxdW90OzxUZXh0IGJvbGQ+e2F0dGFjaG1lbnQuZGVzY3JpcHRpb259PC9UZXh0PiZxdW90OyB7c3RhdHVzVGV4dH1cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxufVxuXG5mdW5jdGlvbiBUZWFtbWF0ZVRhc2tTdGF0dXMoe1xuICBhdHRhY2htZW50LFxufToge1xuICBhdHRhY2htZW50OiBUYXNrU3RhdHVzQXR0YWNobWVudFxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGJnID0gdXNlU2VsZWN0ZWRNZXNzYWdlQmcoKVxuICAvLyBOYXJyb3cgc2VsZWN0b3I6IG9ubHkgcmUtcmVuZGVyIHdoZW4gdGhpcyBzcGVjaWZpYyB0YXNrIGNoYW5nZXMuXG4gIGNvbnN0IHRhc2sgPSB1c2VBcHBTdGF0ZShzID0+IHMudGFza3NbYXR0YWNobWVudC50YXNrSWRdKVxuICBpZiAodGFzaz8udHlwZSAhPT0gJ2luX3Byb2Nlc3NfdGVhbW1hdGUnKSB7XG4gICAgLy8gRmFsbCB0aHJvdWdoIHRvIGdlbmVyaWMgcmVuZGVyaW5nICh0YXNrIG5vdCB5ZXQgaW4gc3RvcmUsIG9yIHdyb25nIHR5cGUpXG4gICAgcmV0dXJuIDxHZW5lcmljVGFza1N0YXR1cyBhdHRhY2htZW50PXthdHRhY2htZW50fSAvPlxuICB9XG4gIGNvbnN0IGFnZW50Q29sb3IgPSB0b0lua0NvbG9yKHRhc2suaWRlbnRpdHkuY29sb3IpXG4gIGNvbnN0IHN0YXR1c1RleHQgPVxuICAgIGF0dGFjaG1lbnQuc3RhdHVzID09PSAnY29tcGxldGVkJ1xuICAgICAgPyAnc2h1dCBkb3duIGdyYWNlZnVsbHknXG4gICAgICA6IGF0dGFjaG1lbnQuc3RhdHVzXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgd2lkdGg9XCIxMDAlXCIgbWFyZ2luVG9wPXsxfSBiYWNrZ3JvdW5kQ29sb3I9e2JnfT5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPntCTEFDS19DSVJDTEV9IDwvVGV4dD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICBUZWFtbWF0ZXsnICd9XG4gICAgICAgIDxUZXh0IGNvbG9yPXthZ2VudENvbG9yfSBib2xkIGRpbUNvbG9yPXtmYWxzZX0+XG4gICAgICAgICAgQHt0YXNrLmlkZW50aXR5LmFnZW50TmFtZX1cbiAgICAgICAgPC9UZXh0PnsnICd9XG4gICAgICAgIHtzdGF0dXNUZXh0fVxuICAgICAgPC9UZXh0PlxuICAgIDwvQm94PlxuICApXG59XG4vLyBXZSBhbGxvdyBzZXR0aW5nIGRpbUNvbG9yIHRvIGZhbHNlIGhlcmUgdG8gaGVscCB3b3JrIGFyb3VuZCB0aGUgZGltLWJvbGQgYnVnLlxuLy8gaHR0cHM6Ly9naXRodWIuY29tL2NoYWxrL2NoYWxrL2lzc3Vlcy8yOTBcbmZ1bmN0aW9uIExpbmUoe1xuICBkaW1Db2xvciA9IHRydWUsXG4gIGNoaWxkcmVuLFxuICBjb2xvcixcbn06IHtcbiAgZGltQ29sb3I/OiBib29sZWFuXG4gIGNoaWxkcmVuOiBSZWFjdC5SZWFjdE5vZGVcbiAgY29sb3I/OiBrZXlvZiBUaGVtZVxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGJnID0gdXNlU2VsZWN0ZWRNZXNzYWdlQmcoKVxuICByZXR1cm4gKFxuICAgIDxCb3ggYmFja2dyb3VuZENvbG9yPXtiZ30+XG4gICAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICA8VGV4dCBjb2xvcj17Y29sb3J9IGRpbUNvbG9yPXtkaW1Db2xvcn0gd3JhcD1cIndyYXBcIj5cbiAgICAgICAgICB7Y2hpbGRyZW59XG4gICAgICAgIDwvVGV4dD5cbiAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQTtBQUNBLE9BQU9BLEtBQUssSUFBSUMsT0FBTyxRQUFRLE9BQU87QUFDdEMsU0FBU0MsSUFBSSxFQUFFQyxHQUFHLEVBQUVDLElBQUksUUFBUSxjQUFjO0FBQzlDLGNBQWNDLFVBQVUsUUFBUSwwQkFBMEI7QUFDMUQsY0FBY0MsMkJBQTJCLFFBQVEsK0JBQStCO0FBQ2hGLFNBQVNDLFdBQVcsUUFBUSx5QkFBeUI7QUFDckQsU0FBU0MsY0FBYyxRQUFR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 diff --git a/src/components/permissions/PermissionDecisionDebugInfo.tsx b/src/components/permissions/PermissionDecisionDebugInfo.tsx index 54c161f2..d5e2bdc7 100644 --- a/src/components/permissions/PermissionDecisionDebugInfo.tsx +++ b/src/components/permissions/PermissionDecisionDebugInfo.tsx @@ -25,6 +25,8 @@ function decisionReasonDisplayString(decisionReason: PermissionDecisionReason & return `${chalk.bold(decisionReason.classifier)} classifier: ${decisionReason.reason}`; } switch (decisionReason.type) { + case 'permissionGuardrail': + return `${chalk.bold(decisionReason.modelId)} permission guardrail: ${decisionReason.decision} (${decisionReason.score ?? 'unavailable'})`; case 'rule': return `${chalk.bold(permissionRuleValueToString(decisionReason.rule.ruleValue))} rule from ${getSettingSourceDisplayNameLowercase(decisionReason.rule.source)}`; case 'mode': @@ -457,4 +459,4 @@ function _temp5(u_1, i) { function _temp4(s) { return s.toolPermissionContext; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiY2hhbGsiLCJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VNZW1vIiwiQW5zaSIsIkJveCIsImNvbG9yIiwiVGV4dCIsInVzZVRoZW1lIiwidXNlQXBwU3RhdGUiLCJQZXJtaXNzaW9uTW9kZSIsInBlcm1pc3Npb25Nb2RlVGl0bGUiLCJQZXJtaXNzaW9uRGVjaXNpb24iLCJQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24iLCJleHRyYWN0UnVsZXMiLCJQZXJtaXNzaW9uVXBkYXRlIiwicGVybWlzc2lvblJ1bGVWYWx1ZVRvU3RyaW5nIiwiZGV0ZWN0VW5yZWFjaGFibGVSdWxlcyIsIlNhbmRib3hNYW5hZ2VyIiwiZ2V0U2V0dGluZ1NvdXJjZURpc3BsYXlOYW1lTG93ZXJjYXNlIiwiUGVybWlzc2lvbkRlY2lzaW9uSW5mb0l0ZW1Qcm9wcyIsInRpdGxlIiwiZGVjaXNpb25SZWFzb24iLCJkZWNpc2lvblJlYXNvbkRpc3BsYXlTdHJpbmciLCJ0eXBlIiwiRXhjbHVkZSIsImJvbGQiLCJjbGFzc2lmaWVyIiwicmVhc29uIiwicnVsZSIsInJ1bGVWYWx1ZSIsInNvdXJjZSIsIm1vZGUiLCJwZXJtaXNzaW9uUHJvbXB0VG9vbE5hbWUiLCJob29rTmFtZSIsIlBlcm1pc3Npb25EZWNpc2lvbkluZm9JdGVtIiwidDAiLCIkIiwiX2MiLCJ0aGVtZSIsInQxIiwiZm9ybWF0RGVjaXNpb25SZWFzb24iLCJBcnJheSIsImZyb20iLCJyZWFzb25zIiwiZW50cmllcyIsIm1hcCIsInQyIiwic3ViY29tbWFuZCIsInJlc3VsdCIsImljb24iLCJiZWhhdmlvciIsInRpY2siLCJjcm9zcyIsInVuZGVmaW5lZCIsInN1Z2dlc3Rpb25zIiwidDMiLCJ0NCIsIlN1Z2dlc3RlZFJ1bGVzIiwiVDAiLCJUMSIsInQ1IiwiU3ltYm9sIiwiZm9yIiwiYmIwIiwicnVsZXMiLCJsZW5ndGgiLCJfdGVtcCIsImpvaW4iLCJ0NiIsInQ3IiwiUHJvcHMiLCJwZXJtaXNzaW9uUmVzdWx0IiwidG9vbE5hbWUiLCJleHRyYWN0RGlyZWN0b3JpZXMiLCJ1cGRhdGVzIiwiZmxhdE1hcCIsInVwZGF0ZSIsImRpcmVjdG9yaWVzIiwiZXh0cmFjdE1vZGUiLCJmaW5kTGFzdCIsInUiLCJTdWdnZXN0aW9uRGlzcGxheSIsIndpZHRoIiwiX3RlbXAyIiwiX3RlbXAzIiwiZGlyIiwiaW5kZXhfMCIsImluZGV4IiwiYnVsbGV0IiwiUGVybWlzc2lvbkRlY2lzaW9uRGVidWdJbmZvIiwidG9vbFBlcm1pc3Npb25Db250ZXh0IiwiX3RlbXA0Iiwic2FuZGJveEF1dG9BbGxvd0VuYWJsZWQiLCJpc1NhbmRib3hpbmdFbmFibGVkIiwiaXNBdXRvQWxsb3dCYXNoSWZTYW5kYm94ZWRFbmFibGVkIiwiYWxsIiwic3VnZ2VzdGVkUnVsZXMiLCJmaWx0ZXIiLCJzb21lIiwic3VnZ2VzdGVkIiwicnVsZUNvbnRlbnQiLCJ1XzAiLCJ1bnJlYWNoYWJsZVJ1bGVzIiwiV0lEVEgiLCJtZXNzYWdlIiwidDgiLCJ3YXJuaW5nIiwiX3RlbXA1IiwidDkiLCJ1XzEiLCJpIiwiZml4IiwicyJdLCJzb3VyY2VzIjpbIlBlcm1pc3Npb25EZWNpc2lvbkRlYnVnSW5mby50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZmVhdHVyZSB9IGZyb20gJ2J1bjpidW5kbGUnXG5pbXBvcnQgY2hhbGsgZnJvbSAnY2hhbGsnXG5pbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZU1lbW8gfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEFuc2ksIEJveCwgY29sb3IsIFRleHQsIHVzZVRoZW1lIH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUgfSBmcm9tICcuLi8uLi9zdGF0ZS9BcHBTdGF0ZS5qcydcbmltcG9ydCB0eXBlIHsgUGVybWlzc2lvbk1vZGUgfSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uTW9kZS5qcydcbmltcG9ydCB7IHBlcm1pc3Npb25Nb2RlVGl0bGUgfSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uTW9kZS5qcydcbmltcG9ydCB0eXBlIHtcbiAgUGVybWlzc2lvbkRlY2lzaW9uLFxuICBQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24sXG59IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25SZXN1bHQuanMnXG5pbXBvcnQgeyBleHRyYWN0UnVsZXMgfSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uVXBkYXRlLmpzJ1xuaW1wb3J0IHR5cGUgeyBQZXJtaXNzaW9uVXBkYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblVwZGF0ZVNjaGVtYS5qcydcbmltcG9ydCB7IHBlcm1pc3Npb25SdWxlVmFsdWVUb1N0cmluZyB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3Blcm1pc3Npb25SdWxlUGFyc2VyLmpzJ1xuaW1wb3J0IHsgZGV0ZWN0VW5yZWFjaGFibGVSdWxlcyB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3NoYWRvd2VkUnVsZURldGVjdGlvbi5qcydcbmltcG9ydCB7IFNhbmRib3hNYW5hZ2VyIH0gZnJvbSAnLi4vLi4vdXRpbHMvc2FuZGJveC9zYW5kYm94LWFkYXB0ZXIuanMnXG5pbXBvcnQgeyBnZXRTZXR0aW5nU291cmNlRGlzcGxheU5hbWVMb3dlcmNhc2UgfSBmcm9tICcuLi8uLi91dGlscy9zZXR0aW5ncy9jb25zdGFudHMuanMnXG5cbnR5cGUgUGVybWlzc2lvbkRlY2lzaW9uSW5mb0l0ZW1Qcm9wcyA9IHtcbiAgdGl0bGU/OiBzdHJpbmdcbiAgZGVjaXNpb25SZWFzb246IFBlcm1pc3Npb25EZWNpc2lvblJlYXNvblxufVxuXG5mdW5jdGlvbiBkZWNpc2lvblJlYXNvbkRpc3BsYXlTdHJpbmcoXG4gIGRlY2lzaW9uUmVhc29uOiBQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24gJiB7XG4gICAgdHlwZTogRXhjbHVkZTxQZXJtaXNzaW9uRGVjaXNpb25SZWFzb25bJ3R5cGUnXSwgJ3N1YmNvbW1hbmRSZXN1bHRzJz5cbiAgfSxcbik6IHN0cmluZyB7XG4gIGlmIChcbiAgICAoZmVhdHVyZSgnQkFTSF9DTEFTU0lGSUVSJykgfHwgZmVhdHVyZSgnVFJBTlNDUklQVF9DTEFTU0lGSUVSJykpICYmXG4gICAgZGVjaXNpb25SZWFzb24udHlwZSA9PT0gJ2NsYXNzaWZpZXInXG4gICkge1xuICAgIHJldHVybiBgJHtjaGFsay5ib2xkKGRlY2lzaW9uUmVhc29uLmNsYXNzaWZpZXIpfSBjbGFzc2lmaWVyOiAke2RlY2lzaW9uUmVhc29uLnJlYXNvbn1gXG4gIH1cbiAgc3dpdGNoIChkZWNpc2lvblJlYXNvbi50eXBlKSB7XG4gICAgY2FzZSAncnVsZSc6XG4gICAgICByZXR1cm4gYCR7Y2hhbGsuYm9sZChwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcoZGVjaXNpb25SZWFzb24ucnVsZS5ydWxlVmFsdWUpKX0gcnVsZSBmcm9tICR7Z2V0U2V0dGluZ1NvdXJjZURpc3BsYXlOYW1lTG93ZXJjYXNlKGRlY2lzaW9uUmVhc29uLnJ1bGUuc291cmNlKX1gXG4gICAgY2FzZSAnbW9kZSc6XG4gICAgICByZXR1cm4gYCR7cGVybWlzc2lvbk1vZGVUaXRsZShkZWNpc2lvblJlYXNvbi5tb2RlKX0gbW9kZWBcbiAgICBjYXNlICdzYW5kYm94T3ZlcnJpZGUnOlxuICAgICAgcmV0dXJuICdSZXF1aXJlcyBwZXJtaXNzaW9uIHRvIGJ5cGFzcyBzYW5kYm94J1xuICAgIGNhc2UgJ3dvcmtpbmdEaXInOlxuICAgICAgcmV0dXJuIGRlY2lzaW9uUmVhc29uLnJlYXNvblxuICAgIGNhc2UgJ3NhZmV0eUNoZWNrJzpcbiAgICBjYXNlICdvdGhlcic6XG4gICAgICByZXR1cm4gZGVjaXNpb25SZWFzb24ucmVhc29uXG4gICAgY2FzZSAncGVybWlzc2lvblByb21wdFRvb2wnOlxuICAgICAgcmV0dXJuIGAke2NoYWxrLmJvbGQoZGVjaXNpb25SZWFzb24ucGVybWlzc2lvblByb21wdFRvb2xOYW1lKX0gcGVybWlzc2lvbiBwcm9tcHQgdG9vbGBcbiAgICBjYXNlICdob29rJzpcbiAgICAgIHJldHVybiBkZWNpc2lvblJlYXNvbi5yZWFzb25cbiAgICAgICAgPyBgJHtjaGFsay5ib2xkKGRlY2lzaW9uUmVhc29uLmhvb2tOYW1lKX0gaG9vazogJHtkZWNpc2lvblJlYXNvbi5yZWFzb259YFxuICAgICAgICA6IGAke2NoYWxrLmJvbGQoZGVjaXNpb25SZWFzb24uaG9va05hbWUpfSBob29rYFxuICAgIGNhc2UgJ2FzeW5jQWdlbnQnOlxuICAgICAgcmV0dXJuIGRlY2lzaW9uUmVhc29uLnJlYXNvblxuICAgIGRlZmF1bHQ6XG4gICAgICByZXR1cm4gJydcbiAgfVxufVxuXG5mdW5jdGlvbiBQZXJtaXNzaW9uRGVjaXNpb25JbmZvSXRlbSh7XG4gIHRpdGxlLFxuICBkZWNpc2lvblJlYXNvbixcbn06IFBlcm1pc3Npb25EZWNpc2lvbkluZm9JdGVtUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdGhlbWVdID0gdXNlVGhlbWUoKVxuXG4gIGZ1bmN0aW9uIGZvcm1hdERlY2lzaW9uUmVhc29uKCk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gICAgc3dpdGNoIChkZWNpc2lvblJlYXNvbi50eXBlKSB7XG4gICAgICBjYXNlICdzdWJjb21tYW5kUmVzdWx0cyc6XG4gICAgICAgIHJldHVybiAoXG4gICAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgICB7QXJyYXkuZnJvbShkZWNpc2lvblJlYXNvbi5yZWFzb25zLmVudHJpZXMoKSkubWFwKFxuICAgICAgICAgICAgICAoW3N1YmNvbW1hbmQsIHJlc3VsdF0pID0+IHtcbiAgICAgICAgICAgICAgICBjb25zdCBpY29uID1cbiAgICAgICAgICAgICAgICAgIHJlc3VsdC5iZWhhdmlvciA9PT0gJ2FsbG93J1xuICAgICAgICAgICAgICAgICAgICA/IGNvbG9yKCdzdWNjZXNzJywgdGhlbWUpKGZpZ3VyZXMudGljaylcbiAgICAgICAgICAgICAgICAgICAgOiBjb2xvcignZXJyb3InLCB0aGVtZSkoZmlndXJlcy5jcm9zcylcbiAgICAgICAgICAgICAgICByZXR1cm4gKFxuICAgICAgICAgICAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIga2V5PXtzdWJjb21tYW5kfT5cbiAgICAgICAgICAgICAgICAgICAgPFRleHQ+XG4gICAgICAgICAgICAgICAgICAgICAge2ljb259IHtzdWJjb21tYW5kfVxuICAgICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgICAgIHtyZXN1bHQuZGVjaXNpb25SZWFzb24gIT09IHVuZGVmaW5lZCAmJlxuICAgICAgICAgICAgICAgICAgICAgIHJlc3VsdC5kZWNpc2lvblJlYXNvbi50eXBlICE9PSAnc3ViY29tbWFuZFJlc3VsdHMnICYmIChcbiAgICAgICAgICAgICAgICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7JyAgJ33ijr97JyAgJ31cbiAgICAgICAgICAgICAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgICAgICAgICAgICAgICA8QW5zaT5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ZGVjaXNpb25SZWFzb25EaXNwbGF5U3RyaW5nKHJlc3VsdC5kZWNpc2lvblJlYXNvbil9XG4gICAgICAgICAgICAgICAgICAgICAgICAgIDwvQW5zaT5cbiAgICAgICAgICAgICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgICAgICAgICB7cmVzdWx0LmJlaGF2aW9yID09PSAnYXNrJyAmJiAoXG4gICAgICAgICAgICAgICAgICAgICAgPFN1Z2dlc3RlZFJ1bGVzIHN1Z2dlc3Rpb25zPXtyZXN1bHQuc3VnZ2VzdGlvbnN9IC8+XG4gICAgICAgICAgICAgICAgICAgICl9XG4gICAgICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICAgICApXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICApfVxuICAgICAgICAgIDwvQm94PlxuICAgICAgICApXG4gICAgICBkZWZhdWx0OlxuICAgICAgICByZXR1cm4gKFxuICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgPEFuc2k+e2RlY2lzaW9uUmVhc29uRGlzcGxheVN0cmluZyhkZWNpc2lvblJlYXNvbil9PC9BbnNpPlxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgKVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICB7dGl0bGUgJiYgPFRleHQ+e3RpdGxlfTwvVGV4dD59XG4gICAgICB7Zm9ybWF0RGVjaXNpb25SZWFzb24oKX1cbiAgICA8L0JveD5cbiAgKVxufVxuXG5mdW5jdGlvbiBTdWdnZXN0ZWRSdWxlcyh7XG4gIHN1Z2dlc3Rpb25zLFxufToge1xuICBzdWdnZXN0aW9uczogUGVybWlzc2lvblVwZGF0ZVtdIHwgdW5kZWZpbmVkXG59KTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgcnVsZXMgPSBleHRyYWN0UnVsZXMoc3VnZ2VzdGlvbnMpXG4gIGlmIChydWxlcy5sZW5ndGggPT09IDApIHJldHVybiBudWxsXG4gIHJldHVybiAoXG4gICAgPFRleHQ+XG4gICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgeycgICd94o6/eycgICd9XG4gICAgICA8L1RleHQ+XG4gICAgICBTdWdnZXN0ZWQgcnVsZXM6eycgJ31cbiAgICAgIDxBbnNpPlxuICAgICAgICB7cnVsZXNcbiAgICAgICAgICAubWFwKHJ1bGUgPT4gY2hhbGsuYm9sZChwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcocnVsZSkpKVxuICAgICAgICAgIC5qb2luKCcsICcpfVxuICAgICAgPC9BbnNpPlxuICAgIDwvVGV4dD5cbiAgKVxufVxuXG50eXBlIFByb3BzID0ge1xuICBwZXJtaXNzaW9uUmVzdWx0OiBQZXJtaXNzaW9uRGVjaXNpb25cbiAgdG9vbE5hbWU/OiBzdHJpbmcgLy8gRmlsdGVyIHVucmVhY2hhYmxlIHJ1bGVzIHRvIHRoaXMgdG9vbFxufVxuXG4vLyBIZWxwZXIgZnVuY3Rpb24gdG8gZXh0cmFjdCBkaXJlY3RvcmllcyBmcm9tIHBlcm1pc3Npb24gdXBkYXRlc1xuZnVuY3Rpb24gZXh0cmFjdERpcmVjdG9yaWVzKHVwZGF0ZXM6IFBlcm1pc3Npb25VcGRhdGVbXSB8IHVuZGVmaW5lZCk6IHN0cmluZ1tdIHtcbiAgaWYgKCF1cGRhdGVzKSByZXR1cm4gW11cblxuICByZXR1cm4gdXBkYXRlcy5mbGF0TWFwKHVwZGF0ZSA9PiB7XG4gICAgc3dpdGNoICh1cGRhdGUudHlwZSkge1xuICAgICAgY2FzZSAnYWRkRGlyZWN0b3JpZXMnOlxuICAgICAgICByZXR1cm4gdXBkYXRlLmRpcmVjdG9yaWVzXG4gICAgICBkZWZhdWx0OlxuICAgICAgICByZXR1cm4gW11cbiAgICB9XG4gIH0pXG59XG5cbi8vIEhlbHBlciBmdW5jdGlvbiB0byBleHRyYWN0IG1vZGUgZnJvbSBwZXJtaXNzaW9uIHVwZGF0ZXNcbmZ1bmN0aW9uIGV4dHJhY3RNb2RlKFxuICB1cGRhdGVzOiBQZXJtaXNzaW9uVXBkYXRlW10gfCB1bmRlZmluZWQsXG4pOiBQZXJtaXNzaW9uTW9kZSB8IHVuZGVmaW5lZCB7XG4gIGlmICghdXBkYXRlcykgcmV0dXJuIHVuZGVmaW5lZFxuICBjb25zdCB1cGRhdGUgPSB1cGRhdGVzLmZpbmRMYXN0KHUgPT4gdS50eXBlID09PSAnc2V0TW9kZScpXG4gIHJldHVybiB1cGRhdGU/LnR5cGUgPT09ICdzZXRNb2RlJyA/IHVwZGF0ZS5tb2RlIDogdW5kZWZpbmVkXG59XG5cbmZ1bmN0aW9uIFN1Z2dlc3Rpb25EaXNwbGF5KHtcbiAgc3VnZ2VzdGlvbnMsXG4gIHdpZHRoLFxufToge1xuICBzdWdnZXN0aW9uczogUGVybWlzc2lvblVwZGF0ZVtdIHwgdW5kZWZpbmVkXG4gIHdpZHRoOiBudW1iZXJcbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAoIXN1Z2dlc3Rpb25zIHx8IHN1Z2dlc3Rpb25zLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiAoXG4gICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIj5cbiAgICAgICAgPEJveCBqdXN0aWZ5Q29udGVudD1cImZsZXgtZW5kXCIgbWluV2lkdGg9e3dpZHRofT5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5TdWdnZXN0aW9ucyA8L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8VGV4dD5Ob25lPC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgKVxuICB9XG5cbiAgY29uc3QgcnVsZXMgPSBleHRyYWN0UnVsZXMoc3VnZ2VzdGlvbnMpXG4gIGNvbnN0IGRpcmVjdG9yaWVzID0gZXh0cmFjdERpcmVjdG9yaWVzKHN1Z2dlc3Rpb25zKVxuICBjb25zdCBtb2RlID0gZXh0cmFjdE1vZGUoc3VnZ2VzdGlvbnMpXG5cbiAgLy8gSWYgbm90aGluZyB0byBkaXNwbGF5LCBzaG93IE5vbmVcbiAgaWYgKHJ1bGVzLmxlbmd0aCA9PT0gMCAmJiBkaXJlY3Rvcmllcy5sZW5ndGggPT09IDAgJiYgIW1vZGUpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXt3aWR0aH0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+U3VnZ2VzdGlvbiA8L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8VGV4dD5Ob25lPC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgKVxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICA8Qm94IGp1c3RpZnlDb250ZW50PVwiZmxleC1lbmRcIiBtaW5XaWR0aD17d2lkdGh9PlxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlN1Z2dlc3Rpb25zIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICAgIDxUZXh0PiA8L1RleHQ+XG4gICAgICA8L0JveD5cblxuICAgICAgey8qIERpc3BsYXkgcnVsZXMgKi99XG4gICAgICB7cnVsZXMubGVuZ3RoID4gMCAmJiAoXG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXt3aWR0aH0+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj4gUnVsZXMgPC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgICAge3J1bGVzLm1hcCgocnVsZSwgaW5kZXgpID0+IChcbiAgICAgICAgICAgICAgPFRleHQga2V5PXtpbmRleH0+XG4gICAgICAgICAgICAgICAge2ZpZ3VyZXMuYnVsbGV0fSB7cGVybWlzc2lvblJ1bGVWYWx1ZVRvU3RyaW5nKHJ1bGUpfVxuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICApKX1cbiAgICAgICAgICA8L0JveD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuXG4gICAgICB7LyogRGlzcGxheSBkaXJlY3RvcmllcyAqL31cbiAgICAgIHtkaXJlY3Rvcmllcy5sZW5ndGggPiAwICYmIChcbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgICAgPEJveCBqdXN0aWZ5Q29udGVudD1cImZsZXgtZW5kXCIgbWluV2lkdGg9e3dpZHRofT5cbiAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiBEaXJlY3RvcmllcyA8L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgICB7ZGlyZWN0b3JpZXMubWFwKChkaXIsIGluZGV4KSA9PiAoXG4gICAgICAgICAgICAgIDxUZXh0IGtleT17aW5kZXh9PlxuICAgICAgICAgICAgICAgIHtmaWd1cmVzLmJ1bGxldH0ge2Rpcn1cbiAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgKSl9XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgIDwvQm94PlxuICAgICAgKX1cblxuICAgICAgey8qIERpc3BsYXkgbW9kZSBjaGFuZ2UgKi99XG4gICAgICB7bW9kZSAmJiAoXG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXt3aWR0aH0+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj4gTW9kZSA8L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgPFRleHQ+e3Blcm1pc3Npb25Nb2RlVGl0bGUobW9kZSl9PC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgICl9XG4gICAgPC9Cb3g+XG4gIClcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFBlcm1pc3Npb25EZWNpc2lvbkRlYnVnSW5mbyh7XG4gIHBlcm1pc3Npb25SZXN1bHQsXG4gIHRvb2xOYW1lLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCB0b29sUGVybWlzc2lvbkNvbnRleHQgPSB1c2VBcHBTdGF0ZShzID0+IHMudG9vbFBlcm1pc3Npb25Db250ZXh0KVxuICBjb25zdCBkZWNpc2lvblJlYXNvbiA9IHBlcm1pc3Npb25SZXN1bHQuZGVjaXNpb25SZWFzb25cbiAgY29uc3Qgc3VnZ2VzdGlvbnMgPVxuICAgICdzdWdnZXN0aW9ucycgaW4gcGVybWlzc2lvblJlc3VsdCA/IHBlcm1pc3Npb25SZXN1bHQuc3VnZ2VzdGlvbnMgOiB1bmRlZmluZWRcblxuICBjb25zdCB1bnJlYWNoYWJsZVJ1bGVzID0gdXNlTWVtbygoKSA9PiB7XG4gICAgY29uc3Qgc2FuZGJveEF1dG9BbGxvd0VuYWJsZWQgPVxuICAgICAgU2FuZGJveE1hbmFnZXIuaXNTYW5kYm94aW5nRW5hYmxlZCgpICYmXG4gICAgICBTYW5kYm94TWFuYWdlci5pc0F1dG9BbGxvd0Jhc2hJZlNhbmRib3hlZEVuYWJsZWQoKVxuICAgIGNvbnN0IGFsbCA9IGRldGVjdFVucmVhY2hhYmxlUnVsZXModG9vbFBlcm1pc3Npb25Db250ZXh0LCB7XG4gICAgICBzYW5kYm94QXV0b0FsbG93RW5hYmxlZCxcbiAgICB9KVxuXG4gICAgLy8gR2V0IHRoZSBzdWdnZXN0ZWQgcnVsZXMgZnJvbSB0aGUgcGVybWlzc2lvbiByZXN1bHRcbiAgICBjb25zdCBzdWdnZXN0ZWRSdWxlcyA9IGV4dHJhY3RSdWxlcyhzdWdnZXN0aW9ucylcblxuICAgIC8vIEZpbHRlciB0byBydWxlcyB0aGF0IG1hdGNoIGFueSBvZiB0aGUgc3VnZ2VzdGVkIHJ1bGVzXG4gICAgLy8gQSBydWxlIG1hdGNoZXMgaWYgaXQgaGFzIHRoZSBzYW1lIHRvb2xOYW1lIGFuZCBydWxlQ29udGVudFxuICAgIGlmIChzdWdnZXN0ZWRSdWxlcy5sZW5ndGggPiAwKSB7XG4gICAgICByZXR1cm4gYWxsLmZpbHRlcih1ID0+XG4gICAgICAgIHN1Z2dlc3RlZFJ1bGVzLnNvbWUoXG4gICAgICAgICAgc3VnZ2VzdGVkID0+XG4gICAgICAgICAgICBzdWdnZXN0ZWQudG9vbE5hbWUgPT09IHUucnVsZS5ydWxlVmFsdWUudG9vbE5hbWUgJiZcbiAgICAgICAgICAgIHN1Z2dlc3RlZC5ydWxlQ29udGVudCA9PT0gdS5ydWxlLnJ1bGVWYWx1ZS5ydWxlQ29udGVudCxcbiAgICAgICAgKSxcbiAgICAgIClcbiAgICB9XG5cbiAgICAvLyBGYWxsYmFjazogZmlsdGVyIGJ5IHRvb2wgbmFtZSBpZiBzcGVjaWZpZWRcbiAgICBpZiAodG9vbE5hbWUpIHtcbiAgICAgIHJldHVybiBhbGwuZmlsdGVyKHUgPT4gdS5ydWxlLnJ1bGVWYWx1ZS50b29sTmFtZSA9PT0gdG9vbE5hbWUpXG4gICAgfVxuXG4gICAgcmV0dXJuIGFsbFxuICB9LCBbdG9vbFBlcm1pc3Npb25Db250ZXh0LCB0b29sTmFtZSwgc3VnZ2VzdGlvbnNdKVxuXG4gIGNvbnN0IFdJRFRIID0gMTBcblxuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXtXSURUSH0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+QmVoYXZpb3IgPC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgICAgPFRleHQ+e3Blcm1pc3Npb25SZXN1bHQuYmVoYXZpb3J9PC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgICB7cGVybWlzc2lvblJlc3VsdC5iZWhhdmlvciAhPT0gJ2FsbG93JyAmJiAoXG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXtXSURUSH0+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5NZXNzYWdlIDwvVGV4dD5cbiAgICAgICAgICA8L0JveD5cbiAgICAgICAgICA8VGV4dD57cGVybWlzc2lvblJlc3VsdC5tZXNzYWdlfTwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXtXSURUSH0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+UmVhc29uIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICAgIHtkZWNpc2lvblJlYXNvbiA9PT0gdW5kZWZpbmVkID8gKFxuICAgICAgICAgIDxUZXh0PnVuZGVmaW5lZDwvVGV4dD5cbiAgICAgICAgKSA6IChcbiAgICAgICAgICA8UGVybWlzc2lvbkRlY2lzaW9uSW5mb0l0ZW0gZGVjaXNpb25SZWFzb249e2RlY2lzaW9uUmVhc29ufSAvPlxuICAgICAgICApfVxuICAgICAgPC9Cb3g+XG4gICAgICA8U3VnZ2VzdGlvbkRpc3BsYXkgc3VnZ2VzdGlvbnM9e3N1Z2dlc3Rpb25zfSB3aWR0aD17V0lEVEh9IC8+XG4gICAgICB7dW5yZWFjaGFibGVSdWxlcy5sZW5ndGggPiAwICYmIChcbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgICA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIj5cbiAgICAgICAgICAgIHtmaWd1cmVzLndhcm5pbmd9IFVucmVhY2hhYmxlIFJ1bGVzICh7dW5yZWFjaGFibGVSdWxlcy5sZW5ndGh9KVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICB7dW5yZWFjaGFibGVSdWxlcy5tYXAoKHUsIGkpID0+IChcbiAgICAgICAgICAgIDxCb3gga2V5PXtpfSBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luTGVmdD17Mn0+XG4gICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwid2FybmluZ1wiPlxuICAgICAgICAgICAgICAgIHtwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcodS5ydWxlLnJ1bGVWYWx1ZSl9XG4gICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICAgICAgeycgICd9XG4gICAgICAgICAgICAgICAge3UucmVhc29ufVxuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAgICAgIHsnICAnfUZpeDoge3UuZml4fVxuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICApKX1cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxTQUFTQSxPQUFPLFFBQVEsWUFBWTtBQUNwQyxPQUFPQyxLQUFLLE1BQU0sT0FBTztBQUN6QixPQUFPQyxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPQyxLQUFLLElBQUlDLE9BQU8sUUFBUSxPQUFPO0FBQ3RDLFNBQVNDLElBQUksRUFBRUMsR0FBRyxFQUFFQyxLQUFLLEVBQUVDLElBQUksRUFBRUMsUUFBUSxRQUFRLGNBQWM7QUFDL0QsU0FBU0MsV0FBVyxRQUFRLHlCQUF5QjtBQUNyRCxjQUFjQyxjQUFjLFFBQVEsMkNBQTJDO0FBQy9FLFNBQVNDLG1CQUFtQixRQUFRLDJDQUEyQztBQUMvRSxjQUNFQyxrQkFBa0IsRUFDbEJDLHdCQUF3QixRQUNuQiw2Q0FBNkM7QUFDcEQsU0FBU0MsWUFBWSxRQUFRLDZDQUE2QztBQUMxRSxjQUFjQyxnQkFBZ0IsUUFBUSxtREFBbUQ7QUFDekYsU0FBU0MsMkJBQTJCLFFBQVEsaURBQWlEO0FBQzdGLFNBQVNDLHNCQUFzQixRQUFRLGtEQUFrRDtBQUN6RixTQUFTQyxjQUFjLFFBQVEsd0NBQXdDO0FBQ3ZFLFNBQVNDLG9DQUFvQyxRQUFRLG1DQUFtQztBQUV4RixLQUFLQywrQkFBK0IsR0FBRztFQUNyQ0MsS0FBSyxDQUFDLEVBQUUsTUFBTTtFQUNkQyxjQUFjLEVBQUVULHdCQUF3QjtBQUMxQyxDQUFDO0FBRUQsU0FBU1UsMkJBQTJCQSxDQUNsQ0QsY0FBYyxFQUFFVCx3QkFBd0IsR0FBRztFQUN6Q1csSUFBSSxFQUFFQyxPQUFPLENBQUNaLHdCQUF3QixDQUFDLE1BQU0sQ0FBQyxFQUFFLG1CQUFtQixDQUFDO0FBQ3RFLENBQUMsQ0FDRixFQUFFLE1BQU0sQ0FBQztFQUNS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 \ No newline at end of file +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiY2hhbGsiLCJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VNZW1vIiwiQW5zaSIsIkJveCIsImNvbG9yIiwiVGV4dCIsInVzZVRoZW1lIiwidXNlQXBwU3RhdGUiLCJQZXJtaXNzaW9uTW9kZSIsInBlcm1pc3Npb25Nb2RlVGl0bGUiLCJQZXJtaXNzaW9uRGVjaXNpb24iLCJQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24iLCJleHRyYWN0UnVsZXMiLCJQZXJtaXNzaW9uVXBkYXRlIiwicGVybWlzc2lvblJ1bGVWYWx1ZVRvU3RyaW5nIiwiZGV0ZWN0VW5yZWFjaGFibGVSdWxlcyIsIlNhbmRib3hNYW5hZ2VyIiwiZ2V0U2V0dGluZ1NvdXJjZURpc3BsYXlOYW1lTG93ZXJjYXNlIiwiUGVybWlzc2lvbkRlY2lzaW9uSW5mb0l0ZW1Qcm9wcyIsInRpdGxlIiwiZGVjaXNpb25SZWFzb24iLCJkZWNpc2lvblJlYXNvbkRpc3BsYXlTdHJpbmciLCJ0eXBlIiwiRXhjbHVkZSIsImJvbGQiLCJjbGFzc2lmaWVyIiwicmVhc29uIiwicnVsZSIsInJ1bGVWYWx1ZSIsInNvdXJjZSIsIm1vZGUiLCJwZXJtaXNzaW9uUHJvbXB0VG9vbE5hbWUiLCJob29rTmFtZSIsIlBlcm1pc3Npb25EZWNpc2lvbkluZm9JdGVtIiwidDAiLCIkIiwiX2MiLCJ0aGVtZSIsInQxIiwiZm9ybWF0RGVjaXNpb25SZWFzb24iLCJBcnJheSIsImZyb20iLCJyZWFzb25zIiwiZW50cmllcyIsIm1hcCIsInQyIiwic3ViY29tbWFuZCIsInJlc3VsdCIsImljb24iLCJiZWhhdmlvciIsInRpY2siLCJjcm9zcyIsInVuZGVmaW5lZCIsInN1Z2dlc3Rpb25zIiwidDMiLCJ0NCIsIlN1Z2dlc3RlZFJ1bGVzIiwiVDAiLCJUMSIsInQ1IiwiU3ltYm9sIiwiZm9yIiwiYmIwIiwicnVsZXMiLCJsZW5ndGgiLCJfdGVtcCIsImpvaW4iLCJ0NiIsInQ3IiwiUHJvcHMiLCJwZXJtaXNzaW9uUmVzdWx0IiwidG9vbE5hbWUiLCJleHRyYWN0RGlyZWN0b3JpZXMiLCJ1cGRhdGVzIiwiZmxhdE1hcCIsInVwZGF0ZSIsImRpcmVjdG9yaWVzIiwiZXh0cmFjdE1vZGUiLCJmaW5kTGFzdCIsInUiLCJTdWdnZXN0aW9uRGlzcGxheSIsIndpZHRoIiwiX3RlbXAyIiwiX3RlbXAzIiwiZGlyIiwiaW5kZXhfMCIsImluZGV4IiwiYnVsbGV0IiwiUGVybWlzc2lvbkRlY2lzaW9uRGVidWdJbmZvIiwidG9vbFBlcm1pc3Npb25Db250ZXh0IiwiX3RlbXA0Iiwic2FuZGJveEF1dG9BbGxvd0VuYWJsZWQiLCJpc1NhbmRib3hpbmdFbmFibGVkIiwiaXNBdXRvQWxsb3dCYXNoSWZTYW5kYm94ZWRFbmFibGVkIiwiYWxsIiwic3VnZ2VzdGVkUnVsZXMiLCJmaWx0ZXIiLCJzb21lIiwic3VnZ2VzdGVkIiwicnVsZUNvbnRlbnQiLCJ1XzAiLCJ1bnJlYWNoYWJsZVJ1bGVzIiwiV0lEVEgiLCJtZXNzYWdlIiwidDgiLCJ3YXJuaW5nIiwiX3RlbXA1IiwidDkiLCJ1XzEiLCJpIiwiZml4IiwicyJdLCJzb3VyY2VzIjpbIlBlcm1pc3Npb25EZWNpc2lvbkRlYnVnSW5mby50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZmVhdHVyZSB9IGZyb20gJ2J1bjpidW5kbGUnXG5pbXBvcnQgY2hhbGsgZnJvbSAnY2hhbGsnXG5pbXBvcnQgZmlndXJlcyBmcm9tICdmaWd1cmVzJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZU1lbW8gfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEFuc2ksIEJveCwgY29sb3IsIFRleHQsIHVzZVRoZW1lIH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHsgdXNlQXBwU3RhdGUgfSBmcm9tICcuLi8uLi9zdGF0ZS9BcHBTdGF0ZS5qcydcbmltcG9ydCB0eXBlIHsgUGVybWlzc2lvbk1vZGUgfSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uTW9kZS5qcydcbmltcG9ydCB7IHBlcm1pc3Npb25Nb2RlVGl0bGUgfSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uTW9kZS5qcydcbmltcG9ydCB0eXBlIHtcbiAgUGVybWlzc2lvbkRlY2lzaW9uLFxuICBQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24sXG59IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25SZXN1bHQuanMnXG5pbXBvcnQgeyBleHRyYWN0UnVsZXMgfSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uVXBkYXRlLmpzJ1xuaW1wb3J0IHR5cGUgeyBQZXJtaXNzaW9uVXBkYXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvUGVybWlzc2lvblVwZGF0ZVNjaGVtYS5qcydcbmltcG9ydCB7IHBlcm1pc3Npb25SdWxlVmFsdWVUb1N0cmluZyB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3Blcm1pc3Npb25SdWxlUGFyc2VyLmpzJ1xuaW1wb3J0IHsgZGV0ZWN0VW5yZWFjaGFibGVSdWxlcyB9IGZyb20gJy4uLy4uL3V0aWxzL3Blcm1pc3Npb25zL3NoYWRvd2VkUnVsZURldGVjdGlvbi5qcydcbmltcG9ydCB7IFNhbmRib3hNYW5hZ2VyIH0gZnJvbSAnLi4vLi4vdXRpbHMvc2FuZGJveC9zYW5kYm94LWFkYXB0ZXIuanMnXG5pbXBvcnQgeyBnZXRTZXR0aW5nU291cmNlRGlzcGxheU5hbWVMb3dlcmNhc2UgfSBmcm9tICcuLi8uLi91dGlscy9zZXR0aW5ncy9jb25zdGFudHMuanMnXG5cbnR5cGUgUGVybWlzc2lvbkRlY2lzaW9uSW5mb0l0ZW1Qcm9wcyA9IHtcbiAgdGl0bGU/OiBzdHJpbmdcbiAgZGVjaXNpb25SZWFzb246IFBlcm1pc3Npb25EZWNpc2lvblJlYXNvblxufVxuXG5mdW5jdGlvbiBkZWNpc2lvblJlYXNvbkRpc3BsYXlTdHJpbmcoXG4gIGRlY2lzaW9uUmVhc29uOiBQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24gJiB7XG4gICAgdHlwZTogRXhjbHVkZTxQZXJtaXNzaW9uRGVjaXNpb25SZWFzb25bJ3R5cGUnXSwgJ3N1YmNvbW1hbmRSZXN1bHRzJz5cbiAgfSxcbik6IHN0cmluZyB7XG4gIGlmIChcbiAgICAoZmVhdHVyZSgnQkFTSF9DTEFTU0lGSUVSJykgfHwgZmVhdHVyZSgnVFJBTlNDUklQVF9DTEFTU0lGSUVSJykpICYmXG4gICAgZGVjaXNpb25SZWFzb24udHlwZSA9PT0gJ2NsYXNzaWZpZXInXG4gICkge1xuICAgIHJldHVybiBgJHtjaGFsay5ib2xkKGRlY2lzaW9uUmVhc29uLmNsYXNzaWZpZXIpfSBjbGFzc2lmaWVyOiAke2RlY2lzaW9uUmVhc29uLnJlYXNvbn1gXG4gIH1cbiAgc3dpdGNoIChkZWNpc2lvblJlYXNvbi50eXBlKSB7XG4gICAgY2FzZSAncnVsZSc6XG4gICAgICByZXR1cm4gYCR7Y2hhbGsuYm9sZChwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcoZGVjaXNpb25SZWFzb24ucnVsZS5ydWxlVmFsdWUpKX0gcnVsZSBmcm9tICR7Z2V0U2V0dGluZ1NvdXJjZURpc3BsYXlOYW1lTG93ZXJjYXNlKGRlY2lzaW9uUmVhc29uLnJ1bGUuc291cmNlKX1gXG4gICAgY2FzZSAnbW9kZSc6XG4gICAgICByZXR1cm4gYCR7cGVybWlzc2lvbk1vZGVUaXRsZShkZWNpc2lvblJlYXNvbi5tb2RlKX0gbW9kZWBcbiAgICBjYXNlICdzYW5kYm94T3ZlcnJpZGUnOlxuICAgICAgcmV0dXJuICdSZXF1aXJlcyBwZXJtaXNzaW9uIHRvIGJ5cGFzcyBzYW5kYm94J1xuICAgIGNhc2UgJ3dvcmtpbmdEaXInOlxuICAgICAgcmV0dXJuIGRlY2lzaW9uUmVhc29uLnJlYXNvblxuICAgIGNhc2UgJ3NhZmV0eUNoZWNrJzpcbiAgICBjYXNlICdvdGhlcic6XG4gICAgICByZXR1cm4gZGVjaXNpb25SZWFzb24ucmVhc29uXG4gICAgY2FzZSAncGVybWlzc2lvblByb21wdFRvb2wnOlxuICAgICAgcmV0dXJuIGAke2NoYWxrLmJvbGQoZGVjaXNpb25SZWFzb24ucGVybWlzc2lvblByb21wdFRvb2xOYW1lKX0gcGVybWlzc2lvbiBwcm9tcHQgdG9vbGBcbiAgICBjYXNlICdob29rJzpcbiAgICAgIHJldHVybiBkZWNpc2lvblJlYXNvbi5yZWFzb25cbiAgICAgICAgPyBgJHtjaGFsay5ib2xkKGRlY2lzaW9uUmVhc29uLmhvb2tOYW1lKX0gaG9vazogJHtkZWNpc2lvblJlYXNvbi5yZWFzb259YFxuICAgICAgICA6IGAke2NoYWxrLmJvbGQoZGVjaXNpb25SZWFzb24uaG9va05hbWUpfSBob29rYFxuICAgIGNhc2UgJ2FzeW5jQWdlbnQnOlxuICAgICAgcmV0dXJuIGRlY2lzaW9uUmVhc29uLnJlYXNvblxuICAgIGRlZmF1bHQ6XG4gICAgICByZXR1cm4gJydcbiAgfVxufVxuXG5mdW5jdGlvbiBQZXJtaXNzaW9uRGVjaXNpb25JbmZvSXRlbSh7XG4gIHRpdGxlLFxuICBkZWNpc2lvblJlYXNvbixcbn06IFBlcm1pc3Npb25EZWNpc2lvbkluZm9JdGVtUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbdGhlbWVdID0gdXNlVGhlbWUoKVxuXG4gIGZ1bmN0aW9uIGZvcm1hdERlY2lzaW9uUmVhc29uKCk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gICAgc3dpdGNoIChkZWNpc2lvblJlYXNvbi50eXBlKSB7XG4gICAgICBjYXNlICdzdWJjb21tYW5kUmVzdWx0cyc6XG4gICAgICAgIHJldHVybiAoXG4gICAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgICB7QXJyYXkuZnJvbShkZWNpc2lvblJlYXNvbi5yZWFzb25zLmVudHJpZXMoKSkubWFwKFxuICAgICAgICAgICAgICAoW3N1YmNvbW1hbmQsIHJlc3VsdF0pID0+IHtcbiAgICAgICAgICAgICAgICBjb25zdCBpY29uID1cbiAgICAgICAgICAgICAgICAgIHJlc3VsdC5iZWhhdmlvciA9PT0gJ2FsbG93J1xuICAgICAgICAgICAgICAgICAgICA/IGNvbG9yKCdzdWNjZXNzJywgdGhlbWUpKGZpZ3VyZXMudGljaylcbiAgICAgICAgICAgICAgICAgICAgOiBjb2xvcignZXJyb3InLCB0aGVtZSkoZmlndXJlcy5jcm9zcylcbiAgICAgICAgICAgICAgICByZXR1cm4gKFxuICAgICAgICAgICAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIga2V5PXtzdWJjb21tYW5kfT5cbiAgICAgICAgICAgICAgICAgICAgPFRleHQ+XG4gICAgICAgICAgICAgICAgICAgICAge2ljb259IHtzdWJjb21tYW5kfVxuICAgICAgICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgICAgICAgIHtyZXN1bHQuZGVjaXNpb25SZWFzb24gIT09IHVuZGVmaW5lZCAmJlxuICAgICAgICAgICAgICAgICAgICAgIHJlc3VsdC5kZWNpc2lvblJlYXNvbi50eXBlICE9PSAnc3ViY29tbWFuZFJlc3VsdHMnICYmIChcbiAgICAgICAgICAgICAgICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7JyAgJ33ijr97JyAgJ31cbiAgICAgICAgICAgICAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgICAgICAgICAgICAgICA8QW5zaT5cbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ZGVjaXNpb25SZWFzb25EaXNwbGF5U3RyaW5nKHJlc3VsdC5kZWNpc2lvblJlYXNvbil9XG4gICAgICAgICAgICAgICAgICAgICAgICAgIDwvQW5zaT5cbiAgICAgICAgICAgICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgICAgICAgICB7cmVzdWx0LmJlaGF2aW9yID09PSAnYXNrJyAmJiAoXG4gICAgICAgICAgICAgICAgICAgICAgPFN1Z2dlc3RlZFJ1bGVzIHN1Z2dlc3Rpb25zPXtyZXN1bHQuc3VnZ2VzdGlvbnN9IC8+XG4gICAgICAgICAgICAgICAgICAgICl9XG4gICAgICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICAgICApXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICApfVxuICAgICAgICAgIDwvQm94PlxuICAgICAgICApXG4gICAgICBkZWZhdWx0OlxuICAgICAgICByZXR1cm4gKFxuICAgICAgICAgIDxUZXh0PlxuICAgICAgICAgICAgPEFuc2k+e2RlY2lzaW9uUmVhc29uRGlzcGxheVN0cmluZyhkZWNpc2lvblJlYXNvbil9PC9BbnNpPlxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgKVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICB7dGl0bGUgJiYgPFRleHQ+e3RpdGxlfTwvVGV4dD59XG4gICAgICB7Zm9ybWF0RGVjaXNpb25SZWFzb24oKX1cbiAgICA8L0JveD5cbiAgKVxufVxuXG5mdW5jdGlvbiBTdWdnZXN0ZWRSdWxlcyh7XG4gIHN1Z2dlc3Rpb25zLFxufToge1xuICBzdWdnZXN0aW9uczogUGVybWlzc2lvblVwZGF0ZVtdIHwgdW5kZWZpbmVkXG59KTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgcnVsZXMgPSBleHRyYWN0UnVsZXMoc3VnZ2VzdGlvbnMpXG4gIGlmIChydWxlcy5sZW5ndGggPT09IDApIHJldHVybiBudWxsXG4gIHJldHVybiAoXG4gICAgPFRleHQ+XG4gICAgICA8VGV4dCBkaW1Db2xvcj5cbiAgICAgICAgeycgICd94o6/eycgICd9XG4gICAgICA8L1RleHQ+XG4gICAgICBTdWdnZXN0ZWQgcnVsZXM6eycgJ31cbiAgICAgIDxBbnNpPlxuICAgICAgICB7cnVsZXNcbiAgICAgICAgICAubWFwKHJ1bGUgPT4gY2hhbGsuYm9sZChwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcocnVsZSkpKVxuICAgICAgICAgIC5qb2luKCcsICcpfVxuICAgICAgPC9BbnNpPlxuICAgIDwvVGV4dD5cbiAgKVxufVxuXG50eXBlIFByb3BzID0ge1xuICBwZXJtaXNzaW9uUmVzdWx0OiBQZXJtaXNzaW9uRGVjaXNpb25cbiAgdG9vbE5hbWU/OiBzdHJpbmcgLy8gRmlsdGVyIHVucmVhY2hhYmxlIHJ1bGVzIHRvIHRoaXMgdG9vbFxufVxuXG4vLyBIZWxwZXIgZnVuY3Rpb24gdG8gZXh0cmFjdCBkaXJlY3RvcmllcyBmcm9tIHBlcm1pc3Npb24gdXBkYXRlc1xuZnVuY3Rpb24gZXh0cmFjdERpcmVjdG9yaWVzKHVwZGF0ZXM6IFBlcm1pc3Npb25VcGRhdGVbXSB8IHVuZGVmaW5lZCk6IHN0cmluZ1tdIHtcbiAgaWYgKCF1cGRhdGVzKSByZXR1cm4gW11cblxuICByZXR1cm4gdXBkYXRlcy5mbGF0TWFwKHVwZGF0ZSA9PiB7XG4gICAgc3dpdGNoICh1cGRhdGUudHlwZSkge1xuICAgICAgY2FzZSAnYWRkRGlyZWN0b3JpZXMnOlxuICAgICAgICByZXR1cm4gdXBkYXRlLmRpcmVjdG9yaWVzXG4gICAgICBkZWZhdWx0OlxuICAgICAgICByZXR1cm4gW11cbiAgICB9XG4gIH0pXG59XG5cbi8vIEhlbHBlciBmdW5jdGlvbiB0byBleHRyYWN0IG1vZGUgZnJvbSBwZXJtaXNzaW9uIHVwZGF0ZXNcbmZ1bmN0aW9uIGV4dHJhY3RNb2RlKFxuICB1cGRhdGVzOiBQZXJtaXNzaW9uVXBkYXRlW10gfCB1bmRlZmluZWQsXG4pOiBQZXJtaXNzaW9uTW9kZSB8IHVuZGVmaW5lZCB7XG4gIGlmICghdXBkYXRlcykgcmV0dXJuIHVuZGVmaW5lZFxuICBjb25zdCB1cGRhdGUgPSB1cGRhdGVzLmZpbmRMYXN0KHUgPT4gdS50eXBlID09PSAnc2V0TW9kZScpXG4gIHJldHVybiB1cGRhdGU/LnR5cGUgPT09ICdzZXRNb2RlJyA/IHVwZGF0ZS5tb2RlIDogdW5kZWZpbmVkXG59XG5cbmZ1bmN0aW9uIFN1Z2dlc3Rpb25EaXNwbGF5KHtcbiAgc3VnZ2VzdGlvbnMsXG4gIHdpZHRoLFxufToge1xuICBzdWdnZXN0aW9uczogUGVybWlzc2lvblVwZGF0ZVtdIHwgdW5kZWZpbmVkXG4gIHdpZHRoOiBudW1iZXJcbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAoIXN1Z2dlc3Rpb25zIHx8IHN1Z2dlc3Rpb25zLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiAoXG4gICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJyb3dcIj5cbiAgICAgICAgPEJveCBqdXN0aWZ5Q29udGVudD1cImZsZXgtZW5kXCIgbWluV2lkdGg9e3dpZHRofT5cbiAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5TdWdnZXN0aW9ucyA8L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8VGV4dD5Ob25lPC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgKVxuICB9XG5cbiAgY29uc3QgcnVsZXMgPSBleHRyYWN0UnVsZXMoc3VnZ2VzdGlvbnMpXG4gIGNvbnN0IGRpcmVjdG9yaWVzID0gZXh0cmFjdERpcmVjdG9yaWVzKHN1Z2dlc3Rpb25zKVxuICBjb25zdCBtb2RlID0gZXh0cmFjdE1vZGUoc3VnZ2VzdGlvbnMpXG5cbiAgLy8gSWYgbm90aGluZyB0byBkaXNwbGF5LCBzaG93IE5vbmVcbiAgaWYgKHJ1bGVzLmxlbmd0aCA9PT0gMCAmJiBkaXJlY3Rvcmllcy5sZW5ndGggPT09IDAgJiYgIW1vZGUpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXt3aWR0aH0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+U3VnZ2VzdGlvbiA8L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8VGV4dD5Ob25lPC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgKVxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICA8Qm94IGp1c3RpZnlDb250ZW50PVwiZmxleC1lbmRcIiBtaW5XaWR0aD17d2lkdGh9PlxuICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlN1Z2dlc3Rpb25zIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICAgIDxUZXh0PiA8L1RleHQ+XG4gICAgICA8L0JveD5cblxuICAgICAgey8qIERpc3BsYXkgcnVsZXMgKi99XG4gICAgICB7cnVsZXMubGVuZ3RoID4gMCAmJiAoXG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXt3aWR0aH0+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj4gUnVsZXMgPC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgICAge3J1bGVzLm1hcCgocnVsZSwgaW5kZXgpID0+IChcbiAgICAgICAgICAgICAgPFRleHQga2V5PXtpbmRleH0+XG4gICAgICAgICAgICAgICAge2ZpZ3VyZXMuYnVsbGV0fSB7cGVybWlzc2lvblJ1bGVWYWx1ZVRvU3RyaW5nKHJ1bGUpfVxuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICApKX1cbiAgICAgICAgICA8L0JveD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuXG4gICAgICB7LyogRGlzcGxheSBkaXJlY3RvcmllcyAqL31cbiAgICAgIHtkaXJlY3Rvcmllcy5sZW5ndGggPiAwICYmIChcbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgICAgPEJveCBqdXN0aWZ5Q29udGVudD1cImZsZXgtZW5kXCIgbWluV2lkdGg9e3dpZHRofT5cbiAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiBEaXJlY3RvcmllcyA8L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgICB7ZGlyZWN0b3JpZXMubWFwKChkaXIsIGluZGV4KSA9PiAoXG4gICAgICAgICAgICAgIDxUZXh0IGtleT17aW5kZXh9PlxuICAgICAgICAgICAgICAgIHtmaWd1cmVzLmJ1bGxldH0ge2Rpcn1cbiAgICAgICAgICAgICAgPC9UZXh0PlxuICAgICAgICAgICAgKSl9XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgIDwvQm94PlxuICAgICAgKX1cblxuICAgICAgey8qIERpc3BsYXkgbW9kZSBjaGFuZ2UgKi99XG4gICAgICB7bW9kZSAmJiAoXG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXt3aWR0aH0+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj4gTW9kZSA8L1RleHQ+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICAgPFRleHQ+e3Blcm1pc3Npb25Nb2RlVGl0bGUobW9kZSl9PC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgICl9XG4gICAgPC9Cb3g+XG4gIClcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFBlcm1pc3Npb25EZWNpc2lvbkRlYnVnSW5mbyh7XG4gIHBlcm1pc3Npb25SZXN1bHQsXG4gIHRvb2xOYW1lLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCB0b29sUGVybWlzc2lvbkNvbnRleHQgPSB1c2VBcHBTdGF0ZShzID0+IHMudG9vbFBlcm1pc3Npb25Db250ZXh0KVxuICBjb25zdCBkZWNpc2lvblJlYXNvbiA9IHBlcm1pc3Npb25SZXN1bHQuZGVjaXNpb25SZWFzb25cbiAgY29uc3Qgc3VnZ2VzdGlvbnMgPVxuICAgICdzdWdnZXN0aW9ucycgaW4gcGVybWlzc2lvblJlc3VsdCA/IHBlcm1pc3Npb25SZXN1bHQuc3VnZ2VzdGlvbnMgOiB1bmRlZmluZWRcblxuICBjb25zdCB1bnJlYWNoYWJsZVJ1bGVzID0gdXNlTWVtbygoKSA9PiB7XG4gICAgY29uc3Qgc2FuZGJveEF1dG9BbGxvd0VuYWJsZWQgPVxuICAgICAgU2FuZGJveE1hbmFnZXIuaXNTYW5kYm94aW5nRW5hYmxlZCgpICYmXG4gICAgICBTYW5kYm94TWFuYWdlci5pc0F1dG9BbGxvd0Jhc2hJZlNhbmRib3hlZEVuYWJsZWQoKVxuICAgIGNvbnN0IGFsbCA9IGRldGVjdFVucmVhY2hhYmxlUnVsZXModG9vbFBlcm1pc3Npb25Db250ZXh0LCB7XG4gICAgICBzYW5kYm94QXV0b0FsbG93RW5hYmxlZCxcbiAgICB9KVxuXG4gICAgLy8gR2V0IHRoZSBzdWdnZXN0ZWQgcnVsZXMgZnJvbSB0aGUgcGVybWlzc2lvbiByZXN1bHRcbiAgICBjb25zdCBzdWdnZXN0ZWRSdWxlcyA9IGV4dHJhY3RSdWxlcyhzdWdnZXN0aW9ucylcblxuICAgIC8vIEZpbHRlciB0byBydWxlcyB0aGF0IG1hdGNoIGFueSBvZiB0aGUgc3VnZ2VzdGVkIHJ1bGVzXG4gICAgLy8gQSBydWxlIG1hdGNoZXMgaWYgaXQgaGFzIHRoZSBzYW1lIHRvb2xOYW1lIGFuZCBydWxlQ29udGVudFxuICAgIGlmIChzdWdnZXN0ZWRSdWxlcy5sZW5ndGggPiAwKSB7XG4gICAgICByZXR1cm4gYWxsLmZpbHRlcih1ID0+XG4gICAgICAgIHN1Z2dlc3RlZFJ1bGVzLnNvbWUoXG4gICAgICAgICAgc3VnZ2VzdGVkID0+XG4gICAgICAgICAgICBzdWdnZXN0ZWQudG9vbE5hbWUgPT09IHUucnVsZS5ydWxlVmFsdWUudG9vbE5hbWUgJiZcbiAgICAgICAgICAgIHN1Z2dlc3RlZC5ydWxlQ29udGVudCA9PT0gdS5ydWxlLnJ1bGVWYWx1ZS5ydWxlQ29udGVudCxcbiAgICAgICAgKSxcbiAgICAgIClcbiAgICB9XG5cbiAgICAvLyBGYWxsYmFjazogZmlsdGVyIGJ5IHRvb2wgbmFtZSBpZiBzcGVjaWZpZWRcbiAgICBpZiAodG9vbE5hbWUpIHtcbiAgICAgIHJldHVybiBhbGwuZmlsdGVyKHUgPT4gdS5ydWxlLnJ1bGVWYWx1ZS50b29sTmFtZSA9PT0gdG9vbE5hbWUpXG4gICAgfVxuXG4gICAgcmV0dXJuIGFsbFxuICB9LCBbdG9vbFBlcm1pc3Npb25Db250ZXh0LCB0b29sTmFtZSwgc3VnZ2VzdGlvbnNdKVxuXG4gIGNvbnN0IFdJRFRIID0gMTBcblxuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXtXSURUSH0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+QmVoYXZpb3IgPC9UZXh0PlxuICAgICAgICA8L0JveD5cbiAgICAgICAgPFRleHQ+e3Blcm1pc3Npb25SZXN1bHQuYmVoYXZpb3J9PC9UZXh0PlxuICAgICAgPC9Cb3g+XG4gICAgICB7cGVybWlzc2lvblJlc3VsdC5iZWhhdmlvciAhPT0gJ2FsbG93JyAmJiAoXG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXtXSURUSH0+XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5NZXNzYWdlIDwvVGV4dD5cbiAgICAgICAgICA8L0JveD5cbiAgICAgICAgICA8VGV4dD57cGVybWlzc2lvblJlc3VsdC5tZXNzYWdlfTwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgIDxCb3gganVzdGlmeUNvbnRlbnQ9XCJmbGV4LWVuZFwiIG1pbldpZHRoPXtXSURUSH0+XG4gICAgICAgICAgPFRleHQgZGltQ29sb3I+UmVhc29uIDwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICAgIHtkZWNpc2lvblJlYXNvbiA9PT0gdW5kZWZpbmVkID8gKFxuICAgICAgICAgIDxUZXh0PnVuZGVmaW5lZDwvVGV4dD5cbiAgICAgICAgKSA6IChcbiAgICAgICAgICA8UGVybWlzc2lvbkRlY2lzaW9uSW5mb0l0ZW0gZGVjaXNpb25SZWFzb249e2RlY2lzaW9uUmVhc29ufSAvPlxuICAgICAgICApfVxuICAgICAgPC9Cb3g+XG4gICAgICA8U3VnZ2VzdGlvbkRpc3BsYXkgc3VnZ2VzdGlvbnM9e3N1Z2dlc3Rpb25zfSB3aWR0aD17V0lEVEh9IC8+XG4gICAgICB7dW5yZWFjaGFibGVSdWxlcy5sZW5ndGggPiAwICYmIChcbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgICA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIj5cbiAgICAgICAgICAgIHtmaWd1cmVzLndhcm5pbmd9IFVucmVhY2hhYmxlIFJ1bGVzICh7dW5yZWFjaGFibGVSdWxlcy5sZW5ndGh9KVxuICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICB7dW5yZWFjaGFibGVSdWxlcy5tYXAoKHUsIGkpID0+IChcbiAgICAgICAgICAgIDxCb3gga2V5PXtpfSBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luTGVmdD17Mn0+XG4gICAgICAgICAgICAgIDxUZXh0IGNvbG9yPVwid2FybmluZ1wiPlxuICAgICAgICAgICAgICAgIHtwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcodS5ydWxlLnJ1bGVWYWx1ZSl9XG4gICAgICAgICAgICAgIDwvVGV4dD5cbiAgICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+XG4gICAgICAgICAgICAgICAgeycgICd9XG4gICAgICAgICAgICAgICAge3UucmVhc29ufVxuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICAgICAgICAgIHsnICAnfUZpeDoge3UuZml4fVxuICAgICAgICAgICAgICA8L1RleHQ+XG4gICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICApKX1cbiAgICAgICAgPC9Cb3g+XG4gICAgICApfVxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxTQUFTQSxPQUFPLFFBQVEsWUFBWTtBQUNwQyxPQUFPQyxLQUFLLE1BQU0sT0FBTztBQUN6QixPQUFPQyxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPQyxLQUFLLElBQUlDLE9BQU8sUUFBUSxPQUFPO0FBQ3RDLFNBQVNDLElBQUksRUFBRUMsR0FBRyxFQUFFQyxLQUFLLEVBQUVDLElBQUksRUFBRUMsUUFBUSxRQUFRLGNBQWM7QUFDL0QsU0FBU0MsV0FBVyxRQUFRLHlCQUF5QjtBQUNyRCxjQUFjQyxjQUFjLFFBQVEsMkNBQTJDO0FBQy9FLFNBQVNDLG1CQUFtQixRQUFRLDJDQUEyQztBQUMvRSxjQUNFQyxrQkFBa0IsRUFDbEJDLHdCQUF3QixRQUNuQiw2Q0FBNkM7QUFDcEQsU0FBU0MsWUFBWSxRQUFRLDZDQUE2QztBQUMxRSxjQUFjQyxnQkFBZ0IsUUFBUSxtREFBbUQ7QUFDekYsU0FBU0MsMkJBQTJCLFFBQVEsaURBQWlEO0FBQzdGLFNBQVNDLHNCQUFzQixRQUFRLGtEQUFrRDtBQUN6RixTQUFTQyxjQUFjLFFBQVEsd0NBQXdDO0FBQ3ZFLFNBQVNDLG9DQUFvQyxRQUFRLG1DQUFtQztBQUV4RixLQUFLQywrQkFBK0IsR0FBRztFQUNyQ0MsS0FBSyxDQUFDLEVBQUUsTUFBTTtFQUNkQyxjQUFjLEVBQUVULHdCQUF3QjtBQUMxQyxDQUFDO0FBRUQsU0FBU1UsMkJBQTJCQSxDQUNsQ0QsY0FBYyxFQUFFVCx3QkFBd0IsR0FBRztFQUN6Q1csSUFBSSxFQUFFQyxPQUFPLENBQUNaLHdCQUF3QixDQUFDLE1BQU0sQ0FBQyxFQUFFLG1CQUFtQixDQUFDO0FBQ3RFLENBQUMsQ0FDRixFQUFFLE1BQU0sQ0FBQztFQUNS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 diff --git a/src/components/permissions/PermissionRuleExplanation.tsx b/src/components/permissions/PermissionRuleExplanation.tsx index 97ff1945..7093341f 100644 --- a/src/components/permissions/PermissionRuleExplanation.tsx +++ b/src/components/permissions/PermissionRuleExplanation.tsx @@ -36,6 +36,11 @@ function stringsForDecisionReason(reason: PermissionDecisionReason | undefined, }; } switch (reason.type) { + case 'permissionGuardrail': + return { + reasonString: `Permission guardrail ${chalk.bold(reason.modelId)} requires confirmation for this ${toolType}.`, + configString: undefined + }; case 'rule': return { reasonString: `Permission rule ${chalk.bold(permissionRuleValueToString(reason.rule.ruleValue))} requires confirmation for this ${toolType}.`, @@ -118,4 +123,4 @@ export function PermissionRuleExplanation(t0) { function _temp(s) { return s.toolPermissionContext.mode; } -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiY2hhbGsiLCJSZWFjdCIsIkFuc2kiLCJCb3giLCJUZXh0IiwidXNlQXBwU3RhdGUiLCJQZXJtaXNzaW9uRGVjaXNpb24iLCJQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24iLCJwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmciLCJUaGVtZSIsIlRoZW1lZFRleHQiLCJQZXJtaXNzaW9uUnVsZUV4cGxhbmF0aW9uUHJvcHMiLCJwZXJtaXNzaW9uUmVzdWx0IiwidG9vbFR5cGUiLCJEZWNpc2lvblJlYXNvblN0cmluZ3MiLCJyZWFzb25TdHJpbmciLCJjb25maWdTdHJpbmciLCJ0aGVtZUNvbG9yIiwic3RyaW5nc0ZvckRlY2lzaW9uUmVhc29uIiwicmVhc29uIiwidHlwZSIsImNsYXNzaWZpZXIiLCJ1bmRlZmluZWQiLCJib2xkIiwicnVsZSIsInJ1bGVWYWx1ZSIsInNvdXJjZSIsImhvb2tSZWFzb25TdHJpbmciLCJzb3VyY2VMYWJlbCIsImhvb2tTb3VyY2UiLCJkaW0iLCJob29rTmFtZSIsIlBlcm1pc3Npb25SdWxlRXhwbGFuYXRpb24iLCJ0MCIsIiQiLCJfYyIsInBlcm1pc3Npb25Nb2RlIiwiX3RlbXAiLCJ0MSIsImRlY2lzaW9uUmVhc29uIiwidDIiLCJzdHJpbmdzIiwidDMiLCJ0NCIsInQ1IiwicyIsInRvb2xQZXJtaXNzaW9uQ29udGV4dCIsIm1vZGUiXSwic291cmNlcyI6WyJQZXJtaXNzaW9uUnVsZUV4cGxhbmF0aW9uLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCBjaGFsayBmcm9tICdjaGFsaydcbmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEFuc2ksIEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgdHlwZSB7XG4gIFBlcm1pc3Npb25EZWNpc2lvbixcbiAgUGVybWlzc2lvbkRlY2lzaW9uUmVhc29uLFxufSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uUmVzdWx0LmpzJ1xuaW1wb3J0IHsgcGVybWlzc2lvblJ1bGVWYWx1ZVRvU3RyaW5nIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvcGVybWlzc2lvblJ1bGVQYXJzZXIuanMnXG5pbXBvcnQgdHlwZSB7IFRoZW1lIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgVGhlbWVkVGV4dCBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL1RoZW1lZFRleHQuanMnXG5cbmV4cG9ydCB0eXBlIFBlcm1pc3Npb25SdWxlRXhwbGFuYXRpb25Qcm9wcyA9IHtcbiAgcGVybWlzc2lvblJlc3VsdDogUGVybWlzc2lvbkRlY2lzaW9uXG4gIHRvb2xUeXBlOiAndG9vbCcgfCAnY29tbWFuZCcgfCAnZWRpdCcgfCAncmVhZCdcbn1cblxudHlwZSBEZWNpc2lvblJlYXNvblN0cmluZ3MgPSB7XG4gIHJlYXNvblN0cmluZzogc3RyaW5nXG4gIGNvbmZpZ1N0cmluZz86IHN0cmluZ1xuICAvKiogV2hlbiBzZXQsIHJlYXNvblN0cmluZyBpcyBwbGFpbiB0ZXh0IHJlbmRlcmVkIHdpdGggdGhpcyB0aGVtZSBjb2xvciBpbnN0ZWFkIG9mIDxBbnNpPi4gKi9cbiAgdGhlbWVDb2xvcj86IGtleW9mIFRoZW1lXG59XG5cbmZ1bmN0aW9uIHN0cmluZ3NGb3JEZWNpc2lvblJlYXNvbihcbiAgcmVhc29uOiBQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24gfCB1bmRlZmluZWQsXG4gIHRvb2xUeXBlOiAndG9vbCcgfCAnY29tbWFuZCcgfCAnZWRpdCcgfCAncmVhZCcsXG4pOiBEZWNpc2lvblJlYXNvblN0cmluZ3MgfCBudWxsIHtcbiAgaWYgKCFyZWFzb24pIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIGlmIChcbiAgICAoZmVhdHVyZSgnQkFTSF9DTEFTU0lGSUVSJykgfHwgZmVhdHVyZSgnVFJBTlNDUklQVF9DTEFTU0lGSUVSJykpICYmXG4gICAgcmVhc29uLnR5cGUgPT09ICdjbGFzc2lmaWVyJ1xuICApIHtcbiAgICBpZiAocmVhc29uLmNsYXNzaWZpZXIgPT09ICdhdXRvLW1vZGUnKSB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICByZWFzb25TdHJpbmc6IGBBdXRvIG1vZGUgY2xhc3NpZmllciByZXF1aXJlcyBjb25maXJtYXRpb24gZm9yIHRoaXMgJHt0b29sVHlwZX0uXFxuJHtyZWFzb24ucmVhc29ufWAsXG4gICAgICAgIGNvbmZpZ1N0cmluZzogdW5kZWZpbmVkLFxuICAgICAgICB0aGVtZUNvbG9yOiAnZXJyb3InLFxuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4ge1xuICAgICAgcmVhc29uU3RyaW5nOiBgQ2xhc3NpZmllciAke2NoYWxrLmJvbGQocmVhc29uLmNsYXNzaWZpZXIpfSByZXF1aXJlcyBjb25maXJtYXRpb24gZm9yIHRoaXMgJHt0b29sVHlwZX0uXFxuJHtyZWFzb24ucmVhc29ufWAsXG4gICAgICBjb25maWdTdHJpbmc6IHVuZGVmaW5lZCxcbiAgICB9XG4gIH1cbiAgc3dpdGNoIChyZWFzb24udHlwZSkge1xuICAgIGNhc2UgJ3J1bGUnOlxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVhc29uU3RyaW5nOiBgUGVybWlzc2lvbiBydWxlICR7Y2hhbGsuYm9sZChcbiAgICAgICAgICBwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcocmVhc29uLnJ1bGUucnVsZVZhbHVlKSxcbiAgICAgICAgKX0gcmVxdWlyZXMgY29uZmlybWF0aW9uIGZvciB0aGlzICR7dG9vbFR5cGV9LmAsXG4gICAgICAgIGNvbmZpZ1N0cmluZzpcbiAgICAgICAgICByZWFzb24ucnVsZS5zb3VyY2UgPT09ICdwb2xpY3lTZXR0aW5ncydcbiAgICAgICAgICAgID8gdW5kZWZpbmVkXG4gICAgICAgICAgICA6ICcvcGVybWlzc2lvbnMgdG8gdXBkYXRlIHJ1bGVzJyxcbiAgICAgIH1cbiAgICBjYXNlICdob29rJzoge1xuICAgICAgY29uc3QgaG9va1JlYXNvblN0cmluZyA9IHJlYXNvbi5yZWFzb24gPyBgOlxcbiR7cmVhc29uLnJlYXNvbn1gIDogJy4nXG4gICAgICBjb25zdCBzb3VyY2VMYWJlbCA9IHJlYXNvbi5ob29rU291cmNlXG4gICAgICAgID8gYCAke2NoYWxrLmRpbShgWyR7cmVhc29uLmhvb2tTb3VyY2V9XWApfWBcbiAgICAgICAgOiAnJ1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVhc29uU3RyaW5nOiBgSG9vayAke2NoYWxrLmJvbGQocmVhc29uLmhvb2tOYW1lKX0gcmVxdWlyZXMgY29uZmlybWF0aW9uIGZvciB0aGlzICR7dG9vbFR5cGV9JHtob29rUmVhc29uU3RyaW5nfSR7c291cmNlTGFiZWx9YCxcbiAgICAgICAgY29uZmlnU3RyaW5nOiAnL2hvb2tzIHRvIHVwZGF0ZScsXG4gICAgICB9XG4gICAgfVxuICAgIGNhc2UgJ3NhZmV0eUNoZWNrJzpcbiAgICBjYXNlICdvdGhlcic6XG4gICAgICByZXR1cm4ge1xuICAgICAgICByZWFzb25TdHJpbmc6IHJlYXNvbi5yZWFzb24sXG4gICAgICAgIGNvbmZpZ1N0cmluZzogdW5kZWZpbmVkLFxuICAgICAgfVxuICAgIGNhc2UgJ3dvcmtpbmdEaXInOlxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVhc29uU3RyaW5nOiByZWFzb24ucmVhc29uLFxuICAgICAgICBjb25maWdTdHJpbmc6ICcvcGVybWlzc2lvbnMgdG8gdXBkYXRlIHJ1bGVzJyxcbiAgICAgIH1cbiAgICBkZWZhdWx0OlxuICAgICAgcmV0dXJuIG51bGxcbiAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gUGVybWlzc2lvblJ1bGVFeHBsYW5hdGlvbih7XG4gIHBlcm1pc3Npb25SZXN1bHQsXG4gIHRvb2xUeXBlLFxufTogUGVybWlzc2lvblJ1bGVFeHBsYW5hdGlvblByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgcGVybWlzc2lvbk1vZGUgPSB1c2VBcHBTdGF0ZShzID0+IHMudG9vbFBlcm1pc3Npb25Db250ZXh0Lm1vZGUpXG4gIGNvbnN0IHN0cmluZ3MgPSBzdHJpbmdzRm9yRGVjaXNpb25SZWFzb24oXG4gICAgcGVybWlzc2lvblJlc3VsdD8uZGVjaXNpb25SZWFzb24sXG4gICAgdG9vbFR5cGUsXG4gIClcbiAgaWYgKCFzdHJpbmdzKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGNvbnN0IHRoZW1lQ29sb3IgPVxuICAgIHN0cmluZ3MudGhlbWVDb2xvciA/P1xuICAgIChwZXJtaXNzaW9uUmVzdWx0Py5kZWNpc2lvblJlYXNvbj8udHlwZSA9PT0gJ2hvb2snICYmXG4gICAgcGVybWlzc2lvbk1vZGUgPT09ICdhdXRvJ1xuICAgICAgPyAnd2FybmluZydcbiAgICAgIDogdW5kZWZpbmVkKVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBtYXJnaW5Cb3R0b209ezF9IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgIHt0aGVtZUNvbG9yID8gKFxuICAgICAgICA8VGhlbWVkVGV4dCBjb2xvcj17dGhlbWVDb2xvcn0+e3N0cmluZ3MucmVhc29uU3RyaW5nfTwvVGhlbWVkVGV4dD5cbiAgICAgICkgOiAoXG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIDxBbnNpPntzdHJpbmdzLnJlYXNvblN0cmluZ308L0Fuc2k+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICl9XG4gICAgICB7c3RyaW5ncy5jb25maWdTdHJpbmcgJiYgPFRleHQgZGltQ29sb3I+e3N0cmluZ3MuY29uZmlnU3RyaW5nfTwvVGV4dD59XG4gICAgPC9Cb3g+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLFNBQVNBLE9BQU8sUUFBUSxZQUFZO0FBQ3BDLE9BQU9DLEtBQUssTUFBTSxPQUFPO0FBQ3pCLE9BQU9DLEtBQUssTUFBTSxPQUFPO0FBQ3pCLFNBQVNDLElBQUksRUFBRUMs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 \ No newline at end of file +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmZWF0dXJlIiwiY2hhbGsiLCJSZWFjdCIsIkFuc2kiLCJCb3giLCJUZXh0IiwidXNlQXBwU3RhdGUiLCJQZXJtaXNzaW9uRGVjaXNpb24iLCJQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24iLCJwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmciLCJUaGVtZSIsIlRoZW1lZFRleHQiLCJQZXJtaXNzaW9uUnVsZUV4cGxhbmF0aW9uUHJvcHMiLCJwZXJtaXNzaW9uUmVzdWx0IiwidG9vbFR5cGUiLCJEZWNpc2lvblJlYXNvblN0cmluZ3MiLCJyZWFzb25TdHJpbmciLCJjb25maWdTdHJpbmciLCJ0aGVtZUNvbG9yIiwic3RyaW5nc0ZvckRlY2lzaW9uUmVhc29uIiwicmVhc29uIiwidHlwZSIsImNsYXNzaWZpZXIiLCJ1bmRlZmluZWQiLCJib2xkIiwicnVsZSIsInJ1bGVWYWx1ZSIsInNvdXJjZSIsImhvb2tSZWFzb25TdHJpbmciLCJzb3VyY2VMYWJlbCIsImhvb2tTb3VyY2UiLCJkaW0iLCJob29rTmFtZSIsIlBlcm1pc3Npb25SdWxlRXhwbGFuYXRpb24iLCJ0MCIsIiQiLCJfYyIsInBlcm1pc3Npb25Nb2RlIiwiX3RlbXAiLCJ0MSIsImRlY2lzaW9uUmVhc29uIiwidDIiLCJzdHJpbmdzIiwidDMiLCJ0NCIsInQ1IiwicyIsInRvb2xQZXJtaXNzaW9uQ29udGV4dCIsIm1vZGUiXSwic291cmNlcyI6WyJQZXJtaXNzaW9uUnVsZUV4cGxhbmF0aW9uLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBmZWF0dXJlIH0gZnJvbSAnYnVuOmJ1bmRsZSdcbmltcG9ydCBjaGFsayBmcm9tICdjaGFsaydcbmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEFuc2ksIEJveCwgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IHVzZUFwcFN0YXRlIH0gZnJvbSAnLi4vLi4vc3RhdGUvQXBwU3RhdGUuanMnXG5pbXBvcnQgdHlwZSB7XG4gIFBlcm1pc3Npb25EZWNpc2lvbixcbiAgUGVybWlzc2lvbkRlY2lzaW9uUmVhc29uLFxufSBmcm9tICcuLi8uLi91dGlscy9wZXJtaXNzaW9ucy9QZXJtaXNzaW9uUmVzdWx0LmpzJ1xuaW1wb3J0IHsgcGVybWlzc2lvblJ1bGVWYWx1ZVRvU3RyaW5nIH0gZnJvbSAnLi4vLi4vdXRpbHMvcGVybWlzc2lvbnMvcGVybWlzc2lvblJ1bGVQYXJzZXIuanMnXG5pbXBvcnQgdHlwZSB7IFRoZW1lIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgVGhlbWVkVGV4dCBmcm9tICcuLi9kZXNpZ24tc3lzdGVtL1RoZW1lZFRleHQuanMnXG5cbmV4cG9ydCB0eXBlIFBlcm1pc3Npb25SdWxlRXhwbGFuYXRpb25Qcm9wcyA9IHtcbiAgcGVybWlzc2lvblJlc3VsdDogUGVybWlzc2lvbkRlY2lzaW9uXG4gIHRvb2xUeXBlOiAndG9vbCcgfCAnY29tbWFuZCcgfCAnZWRpdCcgfCAncmVhZCdcbn1cblxudHlwZSBEZWNpc2lvblJlYXNvblN0cmluZ3MgPSB7XG4gIHJlYXNvblN0cmluZzogc3RyaW5nXG4gIGNvbmZpZ1N0cmluZz86IHN0cmluZ1xuICAvKiogV2hlbiBzZXQsIHJlYXNvblN0cmluZyBpcyBwbGFpbiB0ZXh0IHJlbmRlcmVkIHdpdGggdGhpcyB0aGVtZSBjb2xvciBpbnN0ZWFkIG9mIDxBbnNpPi4gKi9cbiAgdGhlbWVDb2xvcj86IGtleW9mIFRoZW1lXG59XG5cbmZ1bmN0aW9uIHN0cmluZ3NGb3JEZWNpc2lvblJlYXNvbihcbiAgcmVhc29uOiBQZXJtaXNzaW9uRGVjaXNpb25SZWFzb24gfCB1bmRlZmluZWQsXG4gIHRvb2xUeXBlOiAndG9vbCcgfCAnY29tbWFuZCcgfCAnZWRpdCcgfCAncmVhZCcsXG4pOiBEZWNpc2lvblJlYXNvblN0cmluZ3MgfCBudWxsIHtcbiAgaWYgKCFyZWFzb24pIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIGlmIChcbiAgICAoZmVhdHVyZSgnQkFTSF9DTEFTU0lGSUVSJykgfHwgZmVhdHVyZSgnVFJBTlNDUklQVF9DTEFTU0lGSUVSJykpICYmXG4gICAgcmVhc29uLnR5cGUgPT09ICdjbGFzc2lmaWVyJ1xuICApIHtcbiAgICBpZiAocmVhc29uLmNsYXNzaWZpZXIgPT09ICdhdXRvLW1vZGUnKSB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICByZWFzb25TdHJpbmc6IGBBdXRvIG1vZGUgY2xhc3NpZmllciByZXF1aXJlcyBjb25maXJtYXRpb24gZm9yIHRoaXMgJHt0b29sVHlwZX0uXFxuJHtyZWFzb24ucmVhc29ufWAsXG4gICAgICAgIGNvbmZpZ1N0cmluZzogdW5kZWZpbmVkLFxuICAgICAgICB0aGVtZUNvbG9yOiAnZXJyb3InLFxuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4ge1xuICAgICAgcmVhc29uU3RyaW5nOiBgQ2xhc3NpZmllciAke2NoYWxrLmJvbGQocmVhc29uLmNsYXNzaWZpZXIpfSByZXF1aXJlcyBjb25maXJtYXRpb24gZm9yIHRoaXMgJHt0b29sVHlwZX0uXFxuJHtyZWFzb24ucmVhc29ufWAsXG4gICAgICBjb25maWdTdHJpbmc6IHVuZGVmaW5lZCxcbiAgICB9XG4gIH1cbiAgc3dpdGNoIChyZWFzb24udHlwZSkge1xuICAgIGNhc2UgJ3J1bGUnOlxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVhc29uU3RyaW5nOiBgUGVybWlzc2lvbiBydWxlICR7Y2hhbGsuYm9sZChcbiAgICAgICAgICBwZXJtaXNzaW9uUnVsZVZhbHVlVG9TdHJpbmcocmVhc29uLnJ1bGUucnVsZVZhbHVlKSxcbiAgICAgICAgKX0gcmVxdWlyZXMgY29uZmlybWF0aW9uIGZvciB0aGlzICR7dG9vbFR5cGV9LmAsXG4gICAgICAgIGNvbmZpZ1N0cmluZzpcbiAgICAgICAgICByZWFzb24ucnVsZS5zb3VyY2UgPT09ICdwb2xpY3lTZXR0aW5ncydcbiAgICAgICAgICAgID8gdW5kZWZpbmVkXG4gICAgICAgICAgICA6ICcvcGVybWlzc2lvbnMgdG8gdXBkYXRlIHJ1bGVzJyxcbiAgICAgIH1cbiAgICBjYXNlICdob29rJzoge1xuICAgICAgY29uc3QgaG9va1JlYXNvblN0cmluZyA9IHJlYXNvbi5yZWFzb24gPyBgOlxcbiR7cmVhc29uLnJlYXNvbn1gIDogJy4nXG4gICAgICBjb25zdCBzb3VyY2VMYWJlbCA9IHJlYXNvbi5ob29rU291cmNlXG4gICAgICAgID8gYCAke2NoYWxrLmRpbShgWyR7cmVhc29uLmhvb2tTb3VyY2V9XWApfWBcbiAgICAgICAgOiAnJ1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVhc29uU3RyaW5nOiBgSG9vayAke2NoYWxrLmJvbGQocmVhc29uLmhvb2tOYW1lKX0gcmVxdWlyZXMgY29uZmlybWF0aW9uIGZvciB0aGlzICR7dG9vbFR5cGV9JHtob29rUmVhc29uU3RyaW5nfSR7c291cmNlTGFiZWx9YCxcbiAgICAgICAgY29uZmlnU3RyaW5nOiAnL2hvb2tzIHRvIHVwZGF0ZScsXG4gICAgICB9XG4gICAgfVxuICAgIGNhc2UgJ3NhZmV0eUNoZWNrJzpcbiAgICBjYXNlICdvdGhlcic6XG4gICAgICByZXR1cm4ge1xuICAgICAgICByZWFzb25TdHJpbmc6IHJlYXNvbi5yZWFzb24sXG4gICAgICAgIGNvbmZpZ1N0cmluZzogdW5kZWZpbmVkLFxuICAgICAgfVxuICAgIGNhc2UgJ3dvcmtpbmdEaXInOlxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVhc29uU3RyaW5nOiByZWFzb24ucmVhc29uLFxuICAgICAgICBjb25maWdTdHJpbmc6ICcvcGVybWlzc2lvbnMgdG8gdXBkYXRlIHJ1bGVzJyxcbiAgICAgIH1cbiAgICBkZWZhdWx0OlxuICAgICAgcmV0dXJuIG51bGxcbiAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gUGVybWlzc2lvblJ1bGVFeHBsYW5hdGlvbih7XG4gIHBlcm1pc3Npb25SZXN1bHQsXG4gIHRvb2xUeXBlLFxufTogUGVybWlzc2lvblJ1bGVFeHBsYW5hdGlvblByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgcGVybWlzc2lvbk1vZGUgPSB1c2VBcHBTdGF0ZShzID0+IHMudG9vbFBlcm1pc3Npb25Db250ZXh0Lm1vZGUpXG4gIGNvbnN0IHN0cmluZ3MgPSBzdHJpbmdzRm9yRGVjaXNpb25SZWFzb24oXG4gICAgcGVybWlzc2lvblJlc3VsdD8uZGVjaXNpb25SZWFzb24sXG4gICAgdG9vbFR5cGUsXG4gIClcbiAgaWYgKCFzdHJpbmdzKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGNvbnN0IHRoZW1lQ29sb3IgPVxuICAgIHN0cmluZ3MudGhlbWVDb2xvciA/P1xuICAgIChwZXJtaXNzaW9uUmVzdWx0Py5kZWNpc2lvblJlYXNvbj8udHlwZSA9PT0gJ2hvb2snICYmXG4gICAgcGVybWlzc2lvbk1vZGUgPT09ICdhdXRvJ1xuICAgICAgPyAnd2FybmluZydcbiAgICAgIDogdW5kZWZpbmVkKVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBtYXJnaW5Cb3R0b209ezF9IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgIHt0aGVtZUNvbG9yID8gKFxuICAgICAgICA8VGhlbWVkVGV4dCBjb2xvcj17dGhlbWVDb2xvcn0+e3N0cmluZ3MucmVhc29uU3RyaW5nfTwvVGhlbWVkVGV4dD5cbiAgICAgICkgOiAoXG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIDxBbnNpPntzdHJpbmdzLnJlYXNvblN0cmluZ308L0Fuc2k+XG4gICAgICAgIDwvVGV4dD5cbiAgICAgICl9XG4gICAgICB7c3RyaW5ncy5jb25maWdTdHJpbmcgJiYgPFRleHQgZGltQ29sb3I+e3N0cmluZ3MuY29uZmlnU3RyaW5nfTwvVGV4dD59XG4gICAgPC9Cb3g+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLFNBQVNBLE9BQU8sUUFBUSxZQUFZO0FBQ3BDLE9BQU9DLEtBQUssTUFBTSxPQUFPO0FBQ3pCLE9BQU9DLEtBQUssTUFBTSxPQUFPO0FBQ3pCLFNBQVNDLElBQUksRUFBRUMs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 diff --git a/src/components/permissions/hooks.ts b/src/components/permissions/hooks.ts index 7cba31b7..151e7b31 100644 --- a/src/components/permissions/hooks.ts +++ b/src/components/permissions/hooks.ts @@ -75,6 +75,8 @@ function decisionReasonToString( return `Rule: ${permissionRuleValueToString(decisionReason.rule.ruleValue)}` case 'mode': return `Mode: ${decisionReason.mode}` + case 'permissionGuardrail': + return `Permission guardrail: ${decisionReason.modelId}, Decision: ${decisionReason.decision}, Score: ${decisionReason.score ?? 'unavailable'}` case 'subcommandResults': return `Subcommand Results: ${Array.from(decisionReason.reasons.entries()) .map(([key, value]) => `${key}: ${permissionResultToLog(value)}`) diff --git a/src/entrypoints/sdk/controlSchemas.ts b/src/entrypoints/sdk/controlSchemas.ts index 29f26ab5..4f595e36 100644 --- a/src/entrypoints/sdk/controlSchemas.ts +++ b/src/entrypoints/sdk/controlSchemas.ts @@ -112,6 +112,15 @@ export const SDKControlPermissionRequestSchema = lazySchema(() => permission_suggestions: z.array(PermissionUpdateSchema()).optional(), blocked_path: z.string().optional(), decision_reason: z.string().optional(), + permission_guardrail: z + .object({ + provider_id: z.string(), + model_id: z.string(), + preset: z.enum(['fewerPrompts', 'safetyNet']), + decision: z.enum(['allow', 'ask']), + score: z.number().min(0).max(1).nullable(), + }) + .optional(), title: z.string().optional(), display_name: z.string().optional(), tool_use_id: z.string(), diff --git a/src/entrypoints/sdk/coreSchemas.ts b/src/entrypoints/sdk/coreSchemas.ts index 69276c22..46e8d3be 100644 --- a/src/entrypoints/sdk/coreSchemas.ts +++ b/src/entrypoints/sdk/coreSchemas.ts @@ -1541,6 +1541,21 @@ export const SDKStatusMessageSchema = lazySchema(() => }), ) +export const SDKPermissionGuardrailDecisionMessageSchema = lazySchema(() => + z.object({ + type: z.literal('system'), + subtype: z.literal('permission_guardrail_decision'), + decision: z.enum(['allow', 'ask']), + tool_use_id: z.string(), + provider_id: z.string(), + model_id: z.string(), + preset: z.enum(['fewerPrompts', 'safetyNet']), + score: z.number().min(0).max(1).nullable(), + uuid: UUIDPlaceholder(), + session_id: z.string(), + }), +) + export const SDKPostTurnSummaryMessageSchema = lazySchema(() => z .object({ @@ -1862,6 +1877,7 @@ export const SDKMessageSchema = lazySchema(() => SDKPartialAssistantMessageSchema(), SDKCompactBoundaryMessageSchema(), SDKStatusMessageSchema(), + SDKPermissionGuardrailDecisionMessageSchema(), SDKAPIRetryMessageSchema(), SDKLocalCommandOutputMessageSchema(), SDKHookStartedMessageSchema(), diff --git a/src/server/__tests__/provider-runtime-env.test.ts b/src/server/__tests__/provider-runtime-env.test.ts index f82d61fa..bcb3beff 100644 --- a/src/server/__tests__/provider-runtime-env.test.ts +++ b/src/server/__tests__/provider-runtime-env.test.ts @@ -7,6 +7,7 @@ import { mergeActiveProviderManagedEnv, readActiveProviderManagedEnv, resolveProviderDefaultModel, + normalizeProvidersIndex, } from '../services/providerRuntimeEnv.js' import type { SavedProvider } from '../types/provider.js' @@ -137,4 +138,68 @@ describe('providerRuntimeEnv', () => { DISABLE_AUTOUPDATER: '1', }) }) + + test('normalizes decision providers without widening model providers', () => { + const index = normalizeProvidersIndex({ + activeId: null, + providers: [], + activeDecisionProviderId: 'local', + decisionProviders: [ + { + id: 'local', + presetId: 'custom', + name: 'Loopback', + deployment: 'hosted', + apiFormat: 'openai_chat', + baseUrl: 'http://127.0.0.1:9000/v1', + apiKey: '', + models: [{ id: 'guard' }], + }, + { + id: 'remote', + presetId: 'custom', + name: 'LAN', + deployment: 'local', + apiFormat: 'openai_chat', + baseUrl: 'http://192.168.1.10:9000/v1', + apiKey: 'secret', + models: [{ id: 'guard' }], + }, + ], + }) + + expect(index?.activeDecisionProviderId).toBe('local') + expect(index?.decisionProviders.map((provider) => provider.deployment)).toEqual([ + 'local', + 'hosted', + ]) + expect(index?.providers).toEqual([]) + }) + + test('repairs invalid models saved for a curated decision provider preset', () => { + const index = normalizeProvidersIndex({ + activeId: null, + providers: [], + activeDecisionProviderId: 'openai', + decisionProviders: [{ + id: 'openai', + presetId: 'openai', + name: 'OpenAI Decisions', + deployment: 'hosted', + apiFormat: 'openai_decisions', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'secret', + models: [{ id: 'gpt-3.5-turbogpt-5.6-lunagpt-6-luna' }], + }], + }) + + expect(index?.decisionProviders[0]?.models).toEqual([{ id: 'gpt-6-luna', name: 'GPT-6 Luna' }]) + }) + + test('defaults missing decision provider fields for legacy indexes', () => { + const index = normalizeProvidersIndex({ activeId: null, providers: [] }) + + expect(index?.activeDecisionProviderId).toBeNull() + expect(index?.decisionProviders).toEqual([]) + }) }) diff --git a/src/server/__tests__/providers.test.ts b/src/server/__tests__/providers.test.ts index 44d3ca21..6704b2a4 100644 --- a/src/server/__tests__/providers.test.ts +++ b/src/server/__tests__/providers.test.ts @@ -5,7 +5,7 @@ import * as os from 'os' import { ProviderService } from '../services/providerService.js' import { handleProvidersApi } from '../api/providers.js' import { handleProxyRequest } from '../proxy/handler.js' -import type { CreateProviderInput } from '../types/provider.js' +import type { CreateDecisionProviderInput, CreateProviderInput } from '../types/provider.js' let tmpDir: string let originalConfigDir: string | undefined @@ -59,6 +59,20 @@ function sampleInput(overrides?: Partial): CreateProviderIn } } +function sampleDecisionInput( + overrides?: Partial, +): CreateDecisionProviderInput { + return { + presetId: 'openai', + name: 'OpenAI Decisions', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'decision-key', + apiFormat: 'openai_decisions', + models: [{ id: 'gpt-6-luna' }], + ...overrides, + } +} + function anthropicStreamResponse(model: string): Response { const events = [ { type: 'message_start', message: { id: 'msg_1', type: 'message', role: 'assistant', model, content: [] } }, @@ -1459,4 +1473,160 @@ describe('Providers API', () => { expect(res.status).toBe(405) }) + + test('stores decision providers in an independent index collection', async () => { + const svc = new ProviderService() + const modelProvider = await svc.addProvider(sampleInput()) + const decisionProvider = await svc.addDecisionProvider(sampleDecisionInput()) + + await svc.activateDecisionProvider(decisionProvider.id) + + expect(await svc.listProviders()).toEqual({ + providers: [modelProvider], + activeId: null, + }) + expect(await svc.listDecisionProviders()).toEqual({ + providers: [decisionProvider], + activeId: decisionProvider.id, + }) + }) + + test('derives decision provider deployment from the endpoint', async () => { + const svc = new ProviderService() + const local = await svc.addDecisionProvider(sampleDecisionInput({ + presetId: 'custom', + baseUrl: 'http://127.0.0.1:8080/v1', + apiKey: '', + apiFormat: 'openai_chat', + })) + const remote = await svc.addDecisionProvider(sampleDecisionInput({ + presetId: 'custom', + baseUrl: 'http://10.0.0.5:8080/v1', + apiFormat: 'openai_chat', + })) + + expect(local.deployment).toBe('local') + expect(remote.deployment).toBe('hosted') + }) + + test('serves decision provider CRUD without exposing it as a model provider', async () => { + const create = makeRequest('POST', '/api/providers/decisions', sampleDecisionInput()) + const createResponse = await handleProvidersApi(create.req, create.url, create.segments) + const created = await createResponse.json() as { provider: { id: string } } + + const list = makeRequest('GET', '/api/providers/decisions') + const listResponse = await handleProvidersApi(list.req, list.url, list.segments) + const listBody = await listResponse.json() as { providers: Array<{ id: string }> } + const modelList = makeRequest('GET', '/api/providers') + const modelListResponse = await handleProvidersApi(modelList.req, modelList.url, modelList.segments) + const modelListBody = await modelListResponse.json() as { providers: unknown[] } + + expect(createResponse.status).toBe(201) + expect(listBody.providers.map((provider) => provider.id)).toEqual([created.provider.id]) + expect(modelListBody.providers).toEqual([]) + }) + + test('tests an OpenAI decision provider with the predicate response shape', async () => { + const originalFetch = globalThis.fetch + let requestUrl = '' + let requestBody: Record = {} + globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => { + requestUrl = String(url) + requestBody = JSON.parse(String(init?.body)) as Record + return Response.json({ + answers: [{ type: 'predicate', name: 'connected', probability: 0.99 }], + }) + }) as unknown as typeof fetch + + try { + const result = await new ProviderService().testDecisionProviderConfig({ + baseUrl: 'https://api.openai.com/v1', + apiKey: 'decision-key', + apiFormat: 'openai_decisions', + modelId: 'gpt-6-luna', + }) + + expect(result.connectivity.success).toBe(true) + expect(requestUrl).toBe('https://api.openai.com/v1/decisions') + expect(requestBody.model).toBe('gpt-6-luna') + } finally { + globalThis.fetch = originalFetch + } + }) + + test('filters OpenAI model discovery to models supported by the Decisions API', async () => { + const originalFetch = globalThis.fetch + let requestUrl = '' + let authorization = '' + globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => { + requestUrl = String(url) + authorization = new Headers(init?.headers).get('Authorization') ?? '' + return Response.json({ + data: [ + { id: 'gpt-5.6-luna' }, + { id: 'gpt-realtime' }, + { id: 'gpt-6-luna' }, + ], + }) + }) as unknown as typeof fetch + + try { + const models = await new ProviderService().discoverDecisionModels({ + presetId: 'openai', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'decision-key', + apiFormat: 'openai_decisions', + }) + + expect(requestUrl).toBe('https://api.openai.com/v1/models') + expect(authorization).toBe('Bearer decision-key') + expect(models).toEqual([{ id: 'gpt-6-luna' }]) + } finally { + globalThis.fetch = originalFetch + } + }) + + test('keeps discovered models for custom Decisions-compatible providers', async () => { + const originalFetch = globalThis.fetch + globalThis.fetch = mock(async () => Response.json({ + data: [{ id: 'custom-decision-model' }, { id: 'gpt-6-luna' }], + })) as unknown as typeof fetch + + try { + const models = await new ProviderService().discoverDecisionModels({ + presetId: 'custom', + baseUrl: 'https://decisions.example.com/v1', + apiKey: 'decision-key', + apiFormat: 'openai_decisions', + }) + + expect(models).toEqual([{ id: 'custom-decision-model' }, { id: 'gpt-6-luna' }]) + } finally { + globalThis.fetch = originalFetch + } + }) + + test('filters OpenRouter model discovery to curated Decisions models', async () => { + const originalFetch = globalThis.fetch + globalThis.fetch = mock(async () => Response.json({ + data: [ + { id: 'anthropic/claude-haiku-5.5' }, + { id: 'liquid/d1-ultra' }, + { id: 'typesafe/jev-1.14' }, + ], + })) as unknown as typeof fetch + + try { + const models = await new ProviderService().discoverDecisionModels({ + presetId: 'openrouter', + baseUrl: 'https://openrouter.ai/api/alpha', + apiKey: 'decision-key', + apiFormat: 'openrouter_decisions', + }) + + expect(models).toEqual([{ id: 'liquid/d1-ultra' }, { id: 'typesafe/jev-1.14' }]) + } finally { + globalThis.fetch = originalFetch + } + }) }) diff --git a/src/server/__tests__/settings.test.ts b/src/server/__tests__/settings.test.ts index 6e6c7556..468364dc 100644 --- a/src/server/__tests__/settings.test.ts +++ b/src/server/__tests__/settings.test.ts @@ -201,6 +201,25 @@ describe('SettingsService', () => { expect(settings.model).toBe('claude-opus-4-7') }) + it('validates permission guardrail settings before saving', async () => { + const svc = new SettingsService() + const permissionGuardrail = { + enabled: false, + providerId: null, + modelId: '', + preset: 'safetyNet', + threshold: 0.95, + privacyNoticeVersion: 0, + } + + await svc.updateUserSettings({ permissionGuardrail }) + + expect((await svc.getUserSettings()).permissionGuardrail).toEqual(permissionGuardrail) + await expect(svc.updateUserSettings({ + permissionGuardrail: { ...permissionGuardrail, threshold: 2 }, + })).rejects.toMatchObject({ statusCode: 400 }) + }) + it('should write and read the pure white theme setting', async () => { const svc = new SettingsService() await svc.updateUserSettings({ theme: 'white' }) diff --git a/src/server/__tests__/transcript-history.test.ts b/src/server/__tests__/transcript-history.test.ts index de102779..3e3f32cb 100644 --- a/src/server/__tests__/transcript-history.test.ts +++ b/src/server/__tests__/transcript-history.test.ts @@ -2,8 +2,15 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test' import * as fs from 'node:fs/promises' import * as os from 'node:os' import * as path from 'node:path' +import { createAttachmentMessage } from '../../utils/attachments.js' +import { cleanMessagesForLogging } from '../../utils/sessionStorage.js' import { SessionService } from '../services/sessionService.js' -import { collectTaskNotifications, entriesToMessages, type MessageEntry } from '../transcript/entries.js' +import { + collectTaskNotifications, + entriesToMessages, + type MessageEntry, + type TranscriptEntry, +} from '../transcript/entries.js' import { collectAgentNotifications, findLastTodos, @@ -443,6 +450,41 @@ describe('entriesToMessages', () => { expect(messages.map((message) => message.id)).toEqual(['goal-command', 'goal-output']) }) + + it('restores permission guardrail decisions from transcript attachments', () => { + const originalUserType = process.env.USER_TYPE + process.env.USER_TYPE = 'external' + try { + const attachment = createAttachmentMessage({ + type: 'permission_guardrail_decision', + decision: 'allow', + toolUseID: 'tool-1', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet', + score: 0.237, + }) + const persistedEntries = JSON.parse( + JSON.stringify(cleanMessagesForLogging([attachment])), + ) as TranscriptEntry[] + const messages = entriesToMessages(persistedEntries) + + expect(interpretTranscript(messages)).toEqual([{ + id: attachment.uuid, + type: 'permission_guardrail_decision', + decision: 'allow', + toolUseId: 'tool-1', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet', + score: 0.237, + timestamp: Date.parse(attachment.timestamp), + }]) + } finally { + if (originalUserType === undefined) delete process.env.USER_TYPE + else process.env.USER_TYPE = originalUserType + } + }) }) describe('collectTaskNotifications', () => { diff --git a/src/server/__tests__/ws-memory-events.test.ts b/src/server/__tests__/ws-memory-events.test.ts index 9a9c0009..834153a8 100644 --- a/src/server/__tests__/ws-memory-events.test.ts +++ b/src/server/__tests__/ws-memory-events.test.ts @@ -50,6 +50,70 @@ describe('WebSocket memory events', () => { }) }) +describe('WebSocket permission guardrail events', () => { + it('includes guardrail details in pending permission requests', () => { + expect(translateCliOutput({ + type: 'control_request', + request_id: 'request-1', + request: { + subtype: 'can_use_tool', + tool_name: 'Bash', + tool_use_id: 'tool-1', + input: { command: 'rm guarded.txt' }, + permission_guardrail: { + provider_id: 'provider-1', + model_id: 'gpt-6-luna', + preset: 'safetyNet', + decision: 'ask', + score: 0.812, + }, + }, + }, createStreamState())).toEqual({ + messages: [], + events: [{ + type: 'permission_request_opened', + at: expect.any(Number), + request: { + requestId: 'request-1', + toolName: 'Bash', + toolUseId: 'tool-1', + input: { command: 'rm guarded.txt' }, + guardrail: { + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet', + decision: 'ask', + score: 0.812, + }, + }, + }], + }) + }) + + it('forwards permission guardrail scores to the desktop client', () => { + expect(translateCliMessage({ + type: 'system', + subtype: 'permission_guardrail_decision', + decision: 'allow', + tool_use_id: 'tool-1', + provider_id: 'provider-1', + model_id: 'gpt-6-luna', + preset: 'safetyNet', + score: 0.237, + }, 'session-1')).toEqual([ + liveItem({ + type: 'permission_guardrail_decision', + decision: 'allow', + toolUseId: 'tool-1', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + preset: 'safetyNet', + score: 0.237, + }), + ]) + }) +}) + describe('WebSocket AskUserQuestion events', () => { it('forwards structured AskUserQuestion answers from the CLI tool_use_result field', () => { expect(translateCliMessage({ diff --git a/src/server/api/providers.ts b/src/server/api/providers.ts index 7ed3bd7e..af799e35 100644 --- a/src/server/api/providers.ts +++ b/src/server/api/providers.ts @@ -1,11 +1,16 @@ import { z } from 'zod' import { ProviderService } from '../services/providerService.js' import { PROVIDER_PRESETS } from '../config/providerPresets.js' +import { DECISION_PROVIDER_PRESETS } from '../config/decisionProviderPresets.js' import { CreateProviderSchema, UpdateProviderSchema, TestProviderSchema, DiscoverProviderModelsSchema, + CreateDecisionProviderSchema, + UpdateDecisionProviderSchema, + TestDecisionProviderSchema, + DiscoverDecisionProviderModelsSchema, } from '../types/provider.js' import { ApiError, errorResponse } from '../middleware/errorHandler.js' import { diagnosticsService } from '../services/diagnosticsService.js' @@ -21,6 +26,10 @@ export async function handleProvidersApi( const id = segments[2] const action = segments[3] + if (id === 'decisions') { + return await handleDecisionProviders(req, segments) + } + if (id === 'test' && req.method === 'POST') { return await handleTestUnsaved(req) } @@ -113,10 +122,68 @@ export async function handleProvidersApi( throw methodNotAllowed(req.method) } catch (error) { + if (error instanceof z.ZodError) { + return errorResponse(ApiError.badRequest(error.issues.map((issue) => issue.message).join('; '))) + } return errorResponse(error) } } +async function handleDecisionProviders( + req: Request, + segments: string[], +): Promise { + const id = segments[3] + const action = segments[4] + + if (id === 'presets' && req.method === 'GET') { + return Response.json({ presets: DECISION_PROVIDER_PRESETS }) + } + if (id === 'models' && req.method === 'POST') { + const input = DiscoverDecisionProviderModelsSchema.parse(await parseJsonBody(req)) + return Response.json({ models: await providerService.discoverDecisionModels(input) }) + } + if (id === 'test' && req.method === 'POST') { + const input = TestDecisionProviderSchema.parse(await parseJsonBody(req)) + return Response.json({ result: await providerService.testDecisionProviderConfig(input) }) + } + if (!id) { + if (req.method === 'GET') { + const { providers, activeId } = await providerService.listDecisionProviders() + return Response.json({ providers, activeId }) + } + if (req.method === 'POST') { + const input = CreateDecisionProviderSchema.parse(await parseJsonBody(req)) + const provider = await providerService.addDecisionProvider(input) + return Response.json({ provider }, { status: 201 }) + } + throw methodNotAllowed(req.method) + } + if (action === 'activate') { + if (req.method !== 'POST') throw methodNotAllowed(req.method) + await providerService.activateDecisionProvider(id) + return Response.json({ ok: true }) + } + if (action === 'test') { + if (req.method !== 'POST') throw methodNotAllowed(req.method) + const body = await parseJsonBody(req).catch(() => ({})) + const modelId = typeof body.modelId === 'string' ? body.modelId : undefined + return Response.json({ result: await providerService.testDecisionProvider(id, modelId) }) + } + if (req.method === 'GET') { + return Response.json({ provider: await providerService.getDecisionProvider(id) }) + } + if (req.method === 'PUT') { + const input = UpdateDecisionProviderSchema.parse(await parseJsonBody(req)) + return Response.json({ provider: await providerService.updateDecisionProvider(id, input) }) + } + if (req.method === 'DELETE') { + await providerService.deleteDecisionProvider(id) + return Response.json({ ok: true }) + } + throw methodNotAllowed(req.method) +} + async function handleCreate(req: Request): Promise { const body = await parseJsonBody(req) try { diff --git a/src/server/config/decisionProviderPresets.ts b/src/server/config/decisionProviderPresets.ts new file mode 100644 index 00000000..88c24cee --- /dev/null +++ b/src/server/config/decisionProviderPresets.ts @@ -0,0 +1,47 @@ +import type { + DecisionProviderFormat, + DecisionProviderPresetId, + ProviderModel, +} from '../types/provider.js' + +export type DecisionProviderPreset = { + id: DecisionProviderPresetId + name: string + baseUrl: string + apiFormat: DecisionProviderFormat + models: ProviderModel[] +} + +export const DECISION_PROVIDER_PRESETS: DecisionProviderPreset[] = [ + { + id: 'openai', + name: 'OpenAI', + baseUrl: 'https://api.openai.com/v1', + apiFormat: 'openai_decisions', + models: [{ id: 'gpt-6-luna', name: 'GPT-6 Luna' }], + }, + { + id: 'openrouter', + name: 'OpenRouter', + baseUrl: 'https://openrouter.ai/api/alpha', + apiFormat: 'openrouter_decisions', + models: [ + { id: 'liquid/d1-ultra', name: 'd1-ultra' }, + { id: 'typesafe/jev-1.14', name: 'Jev 1.14' }, + ], + }, + { + id: 'ollama', + name: 'Ollama', + baseUrl: 'http://127.0.0.1:11434', + apiFormat: 'ollama_chat', + models: [], + }, + { + id: 'custom', + name: 'Custom', + baseUrl: '', + apiFormat: 'openai_decisions', + models: [], + }, +] diff --git a/src/server/services/providerRuntimeEnv.ts b/src/server/services/providerRuntimeEnv.ts index b670c118..b84c5379 100644 --- a/src/server/services/providerRuntimeEnv.ts +++ b/src/server/services/providerRuntimeEnv.ts @@ -2,12 +2,14 @@ import * as fs from 'fs' import * as path from 'path' import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js' +import { DECISION_PROVIDER_PRESETS } from '../config/decisionProviderPresets.js' import { PROVIDER_PRESETS } from '../config/providerPresets.js' import type { ApiFormat, ProviderModel, ProviderAuthStrategy, ProvidersIndex, + SavedDecisionProvider, SavedProvider, } from '../types/provider.js' import { normalizeUpstreamBaseUrl } from '../upstream/baseUrl.js' @@ -64,6 +66,28 @@ function isSavedProvider(value: unknown): value is SavedProvider { ) } +export function isLoopbackDecisionProviderUrl(baseUrl: string): boolean { + try { + const hostname = new URL(baseUrl).hostname.toLowerCase() + return hostname === '127.0.0.1' || hostname === '[::1]' + } catch { + return false + } +} + +function isSavedDecisionProvider(value: unknown): value is SavedDecisionProvider { + return ( + isRecord(value) && + typeof value.id === 'string' && + ['openai', 'openrouter', 'ollama', 'custom'].includes(String(value.presetId)) && + typeof value.name === 'string' && + ['openai_decisions', 'openrouter_decisions', 'openai_chat', 'ollama_chat'].includes(String(value.apiFormat)) && + typeof value.baseUrl === 'string' && + typeof value.apiKey === 'string' && + isProviderModels(value.models) + ) +} + export function stripContextSuffix(model: string): string { return model.replace(/\[(1|2)m\]/gi, '') } @@ -113,6 +137,24 @@ export function normalizeSavedProvider(provider: SavedProvider): SavedProvider { } } +export function normalizeSavedDecisionProvider( + provider: SavedDecisionProvider, +): SavedDecisionProvider { + const normalizedModels = normalizeProviderModels(provider.models) + const preset = DECISION_PROVIDER_PRESETS.find((entry) => ( + entry.id === provider.presetId && entry.apiFormat === provider.apiFormat && entry.models.length > 0 + )) + const supportedIds = new Set(preset?.models.map((model) => model.id) ?? []) + const supportedModels = normalizedModels.filter((model) => supportedIds.has(model.id)) + return { + ...provider, + deployment: isLoopbackDecisionProviderUrl(provider.baseUrl) ? 'local' : 'hosted', + models: preset + ? supportedModels.length > 0 ? supportedModels : preset.models + : normalizedModels, + } +} + export function normalizeProvidersIndex(value: unknown): ProvidersIndex | null { if (!isRecord(value) || !Array.isArray(value.providers)) { return null @@ -131,12 +173,24 @@ export function normalizeProvidersIndex(value: unknown): ProvidersIndex | null { const activeId = rawActiveId && providers.some((provider) => provider.id === rawActiveId) ? rawActiveId : null + const decisionProviders = Array.isArray(value.decisionProviders) + ? value.decisionProviders + .filter(isSavedDecisionProvider) + .map((provider) => normalizeSavedDecisionProvider(provider)) + : [] + const activeDecisionProviderId = + typeof value.activeDecisionProviderId === 'string' && + decisionProviders.some((provider) => provider.id === value.activeDecisionProviderId) + ? value.activeDecisionProviderId + : null return { ...rest, schemaVersion: typeof value.schemaVersion === 'number' ? value.schemaVersion : 1, activeId, providers, + activeDecisionProviderId, + decisionProviders, } } diff --git a/src/server/services/providerService.ts b/src/server/services/providerService.ts index 9e35917c..35e98b3b 100644 --- a/src/server/services/providerService.ts +++ b/src/server/services/providerService.ts @@ -15,9 +15,11 @@ import { getPresetDefaultEnv, normalizeProviderModels, normalizeProvidersIndex, + normalizeSavedDecisionProvider, } from './providerRuntimeEnv.js' import { loadNetworkSettings } from './networkSettings.js' import { fetchUpstreamModels, resolveUpstreamTarget, sendToUpstream, type UpstreamTarget } from '../upstream/client.js' +import { DECISION_PROVIDER_PRESETS } from '../config/decisionProviderPresets.js' import type { SavedProvider, ProvidersIndex, @@ -29,6 +31,11 @@ import type { ProviderAuthStrategy, DiscoverProviderModelsInput, DiscoveredProviderModel, + SavedDecisionProvider, + CreateDecisionProviderInput, + UpdateDecisionProviderInput, + TestDecisionProviderInput, + DiscoverDecisionProviderModelsInput, } from '../types/provider.js' import { getClaudeConfigHomeDir } from '../../utils/envUtils.js' @@ -36,6 +43,8 @@ const DEFAULT_INDEX: ProvidersIndex = { schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION, activeId: null, providers: [], + activeDecisionProviderId: null, + decisionProviders: [], } export class ProviderService { @@ -106,6 +115,80 @@ export class ProviderService { return { providers: index.providers, activeId: index.activeId } } + async listDecisionProviders(): Promise<{ + providers: SavedDecisionProvider[] + activeId: string | null + }> { + const index = await this.readIndex() + return { + providers: index.decisionProviders, + activeId: index.activeDecisionProviderId, + } + } + + async getDecisionProvider(id: string): Promise { + const index = await this.readIndex() + const provider = index.decisionProviders.find((entry) => entry.id === id) + if (!provider) throw ApiError.notFound(`Decision provider not found: ${id}`) + return provider + } + + async addDecisionProvider( + input: CreateDecisionProviderInput, + ): Promise { + const index = await this.readIndex() + const provider = normalizeSavedDecisionProvider({ + id: crypto.randomUUID(), + ...input, + deployment: 'hosted', + models: normalizeProviderModels(input.models), + }) + index.decisionProviders.push(provider) + await this.writeIndex(index) + return provider + } + + async updateDecisionProvider( + id: string, + input: UpdateDecisionProviderInput, + ): Promise { + const index = await this.readIndex() + const providerIndex = index.decisionProviders.findIndex((entry) => entry.id === id) + if (providerIndex === -1) { + throw ApiError.notFound(`Decision provider not found: ${id}`) + } + const provider = normalizeSavedDecisionProvider({ + ...index.decisionProviders[providerIndex], + ...input, + ...(input.models ? { models: normalizeProviderModels(input.models) } : {}), + }) + index.decisionProviders[providerIndex] = provider + await this.writeIndex(index) + return provider + } + + async deleteDecisionProvider(id: string): Promise { + const index = await this.readIndex() + const providerIndex = index.decisionProviders.findIndex((entry) => entry.id === id) + if (providerIndex === -1) { + throw ApiError.notFound(`Decision provider not found: ${id}`) + } + if (index.activeDecisionProviderId === id) { + throw ApiError.conflict('Cannot delete the active decision provider') + } + index.decisionProviders.splice(providerIndex, 1) + await this.writeIndex(index) + } + + async activateDecisionProvider(id: string): Promise { + const index = await this.readIndex() + if (!index.decisionProviders.some((entry) => entry.id === id)) { + throw ApiError.notFound(`Decision provider not found: ${id}`) + } + index.activeDecisionProviderId = id + await this.writeIndex(index) + } + async getProvider(id: string): Promise { const index = await this.readIndex() const provider = index.providers.find((p) => p.id === id) @@ -309,6 +392,101 @@ export class ProviderService { }, input.presetId) } + async discoverDecisionModels( + input: DiscoverDecisionProviderModelsInput, + ): Promise { + const endpoint = decisionModelsEndpoint(input.baseUrl, input.apiFormat) + const response = await fetch(endpoint, { + headers: input.apiKey ? { Authorization: `Bearer ${input.apiKey}` } : {}, + }) + if (!response.ok) { + throw new ApiError(502, `Model discovery failed with HTTP ${response.status}`, 'MODEL_DISCOVERY_FAILED') + } + const payload = await response.json() as unknown + return filterDecisionModels(input, parseDecisionModels(payload)) + } + + async testDecisionProvider( + id: string, + modelId?: string, + ): Promise { + const provider = await this.getDecisionProvider(id) + return this.testDecisionProviderConfig({ + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + apiFormat: provider.apiFormat, + modelId: modelId || provider.models[0]?.id || '', + }) + } + + async testDecisionProviderConfig( + input: TestDecisionProviderInput, + ): Promise { + if (!input.modelId || (!input.apiKey && input.apiFormat !== 'ollama_chat')) { + return { + connectivity: { + success: false, + latencyMs: 0, + error: 'Missing model or API key', + }, + } + } + const { aiRequestTimeoutMs } = await loadNetworkSettings() + const startedAt = Date.now() + try { + const response = await fetch(decisionRequestEndpoint(input.baseUrl, input.apiFormat), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(input.apiKey ? { Authorization: `Bearer ${input.apiKey}` } : {}), + }, + body: JSON.stringify(decisionTestBody(input.apiFormat, input.modelId)), + signal: AbortSignal.timeout(aiRequestTimeoutMs), + }) + const latencyMs = Date.now() - startedAt + if (!response.ok) { + return { + connectivity: { + success: false, + latencyMs, + error: `HTTP ${response.status}`, + httpStatus: response.status, + modelUsed: input.modelId, + }, + } + } + const payload = await response.json() as unknown + if (!isValidDecisionTestResponse(input.apiFormat, payload)) { + return { + connectivity: { + success: false, + latencyMs, + error: 'Response did not match the selected API format', + httpStatus: response.status, + modelUsed: input.modelId, + }, + } + } + return { + connectivity: { + success: true, + latencyMs, + httpStatus: response.status, + modelUsed: input.modelId, + }, + } + } catch (error) { + return { + connectivity: { + success: false, + latencyMs: Date.now() - startedAt, + error: error instanceof Error ? error.message : String(error), + modelUsed: input.modelId, + }, + } + } + } + private async fetchModels(target: UpstreamTarget, presetId?: string): Promise { const result = await fetchUpstreamModels(target, presetId) if (!result.ok) { @@ -375,6 +553,131 @@ export class ProviderService { } } +function appendEndpoint(baseUrl: string, pathName: string): string { + return `${baseUrl.replace(/\/+$/, '')}/${pathName.replace(/^\/+/, '')}` +} + +function decisionRequestEndpoint(baseUrl: string, apiFormat: TestDecisionProviderInput['apiFormat']): string { + if (apiFormat === 'ollama_chat') return appendEndpoint(baseUrl, 'api/chat') + if (apiFormat === 'openai_chat') return appendEndpoint(baseUrl, 'chat/completions') + return appendEndpoint(baseUrl, 'decisions') +} + +function decisionModelsEndpoint(baseUrl: string, apiFormat: TestDecisionProviderInput['apiFormat']): string { + if (apiFormat === 'ollama_chat') return appendEndpoint(baseUrl, 'api/tags') + if (apiFormat === 'openrouter_decisions') { + return `${new URL(baseUrl).origin}/api/v1/models` + } + return appendEndpoint(baseUrl, 'models') +} + +function decisionTestBody( + apiFormat: TestDecisionProviderInput['apiFormat'], + modelId: string, +): Record { + if (apiFormat === 'openai_decisions') { + return { + model: modelId, + input: 'A connection test.', + questions: [{ + type: 'predicate', + name: 'connected', + instructions: 'Return the probability that this is a connection test.', + }], + } + } + if (apiFormat === 'openrouter_decisions') { + return { + model: modelId, + state: { situation: 'A connection test.' }, + questions: { + connected: { + type: 'noul', + instructions: 'Is this a connection test?', + criteria: [{ description: 'This is a connection test.' }], + }, + }, + } + } + if (apiFormat === 'ollama_chat') { + return { + model: modelId, + stream: false, + messages: [{ role: 'user', content: 'Reply with ok.' }], + } + } + return { + model: modelId, + max_tokens: 8, + messages: [{ role: 'user', content: 'Reply with ok.' }], + } +} + +function parseDecisionModels(payload: unknown): DiscoveredProviderModel[] { + if (!payload || typeof payload !== 'object') return [] + const record = payload as Record + const values = Array.isArray(record.data) + ? record.data + : Array.isArray(record.models) + ? record.models + : [] + const models = values.flatMap((value): DiscoveredProviderModel[] => { + if (!value || typeof value !== 'object') return [] + const model = value as Record + const id = typeof model.id === 'string' + ? model.id + : typeof model.name === 'string' + ? model.name + : typeof model.model === 'string' + ? model.model + : '' + if (!id) return [] + const name = typeof model.name === 'string' && model.name !== id + ? model.name + : undefined + return [{ id, ...(name ? { name } : {}) }] + }) + return normalizeProviderModels(models) +} + +function filterDecisionModels( + input: DiscoverDecisionProviderModelsInput, + models: DiscoveredProviderModel[], +): DiscoveredProviderModel[] { + const preset = DECISION_PROVIDER_PRESETS.find((entry) => entry.id === input.presetId) + if (!preset || preset.models.length === 0 || preset.apiFormat !== input.apiFormat) return models + const supportedIds = new Set(preset.models.map((model) => model.id)) + return models.filter((model) => supportedIds.has(model.id)) +} + +function isValidDecisionTestResponse( + apiFormat: TestDecisionProviderInput['apiFormat'], + payload: unknown, +): boolean { + if (!payload || typeof payload !== 'object') return false + const record = payload as Record + if (apiFormat === 'openai_decisions') { + return Array.isArray(record.answers) && record.answers.some((value) => { + if (!value || typeof value !== 'object') return false + const answer = value as Record + return answer.name === 'connected' && typeof answer.probability === 'number' + }) + } + if (apiFormat === 'openrouter_decisions') { + const answers = record.answers + if (!answers || typeof answers !== 'object') return false + const connected = (answers as Record).connected + return !!connected && typeof connected === 'object' && + typeof (connected as Record).noul === 'number' + } + if (apiFormat === 'ollama_chat') { + const message = record.message + return !!message && typeof message === 'object' && + typeof (message as Record).content === 'string' + } + return Array.isArray(record.choices) && record.choices.length > 0 +} + async function readAnthropicStream( stream: ReadableStream, timeoutMs: number, diff --git a/src/server/services/settingsService.ts b/src/server/services/settingsService.ts index c3f3581b..39e2792b 100644 --- a/src/server/services/settingsService.ts +++ b/src/server/services/settingsService.ts @@ -13,6 +13,7 @@ import { type DefaultPermissionMode, } from '../ws/protocol.js' import { isDefaultPermissionMode } from './permissionModes.js' +import { PermissionGuardrailSettingsSchema } from '../../types/permissionGuardrail.js' export class SettingsService { private static writeLocks = new Map>() @@ -129,6 +130,13 @@ export class SettingsService { `Invalid browserSearchEngine. Valid engines: ${BROWSER_SEARCH_ENGINE_IDS.join(', ')}`, ) } + if (Object.prototype.hasOwnProperty.call(settings, 'permissionGuardrail')) { + const result = PermissionGuardrailSettingsSchema.safeParse(settings.permissionGuardrail) + if (!result.success) { + throw ApiError.badRequest('Invalid permission guardrail settings') + } + settings.permissionGuardrail = result.data + } const filePath = this.getUserSettingsPath() await this.withWriteLock(filePath, async () => { const current = await this.readJsonFile(filePath) diff --git a/src/server/session/state/types.ts b/src/server/session/state/types.ts index cf95353b..f9031b67 100644 --- a/src/server/session/state/types.ts +++ b/src/server/session/state/types.ts @@ -30,6 +30,14 @@ export type SessionRepository = { worktreeSlug?: string } +export type PermissionGuardrailDetails = { + providerId: string + modelId: string + preset: 'fewerPrompts' | 'safetyNet' + decision: 'allow' | 'ask' + score: number | null +} + export type PendingPermission = { requestId: string toolName: string @@ -37,6 +45,7 @@ export type PendingPermission = { input: unknown description?: string suggestions?: unknown[] + guardrail?: PermissionGuardrailDetails } export type PendingBrowserGrant = { diff --git a/src/server/session/translate.ts b/src/server/session/translate.ts index c90e8818..191379ff 100644 --- a/src/server/session/translate.ts +++ b/src/server/session/translate.ts @@ -54,6 +54,7 @@ export function createStreamState(): SessionStreamState { function toPendingPermission(cliMsg: any): PendingPermission { const request = cliMsg.request ?? {} + const guardrail = toPermissionGuardrailDetails(request.permission_guardrail) return { requestId: cliMsg.request_id, toolName: typeof request.tool_name === 'string' && request.tool_name ? request.tool_name : 'Unknown', @@ -61,6 +62,36 @@ function toPendingPermission(cliMsg: any): PendingPermission { input: request.input && typeof request.input === 'object' ? request.input : {}, ...(typeof request.description === 'string' && request.description.trim() ? { description: request.description } : {}), ...(Array.isArray(request.permission_suggestions) ? { suggestions: request.permission_suggestions } : {}), + ...(guardrail ? { guardrail } : {}), + } +} + +function toPermissionGuardrailDetails( + value: unknown, +): PendingPermission['guardrail'] { + if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined + const details = value as Record + const score = details.score + if ( + typeof details.provider_id !== 'string' || + typeof details.model_id !== 'string' || + (details.preset !== 'fewerPrompts' && details.preset !== 'safetyNet') || + (details.decision !== 'allow' && details.decision !== 'ask') || + (score !== null && ( + typeof score !== 'number' || + !Number.isFinite(score) || + score < 0 || + score > 1 + )) + ) { + return undefined + } + return { + providerId: details.provider_id, + modelId: details.model_id, + preset: details.preset, + decision: details.decision, + score, } } @@ -482,6 +513,10 @@ function translateMessages( const saved = parseMemorySaved(cliMsg) return saved ? [liveItem({ type: 'memory_event', event: 'saved', ...saved })] : [] } + if (subtype === 'permission_guardrail_decision') { + const marker = permissionGuardrailDecision(cliMsg) + return marker ? [liveItem(marker)] : [] + } if (subtype === 'status') { if (isPermissionMode(cliMsg.permissionMode)) { events.push({ type: 'permission_effective_reported', mode: cliMsg.permissionMode }) @@ -574,6 +609,35 @@ function finiteNumber(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null } +function permissionGuardrailDecision( + value: unknown, +): LiveItemContent | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null + const marker = value as Record + const decision = marker.decision + const preset = marker.preset + const score = marker.score + if ( + (decision !== 'allow' && decision !== 'ask') || + (preset !== 'fewerPrompts' && preset !== 'safetyNet') || + typeof marker.tool_use_id !== 'string' || + typeof marker.provider_id !== 'string' || + typeof marker.model_id !== 'string' || + (score !== null && (typeof score !== 'number' || score < 0 || score > 1)) + ) { + return null + } + return { + type: 'permission_guardrail_decision', + decision, + toolUseId: marker.tool_use_id, + providerId: marker.provider_id, + modelId: marker.model_id, + preset, + score, + } +} + function normalizeRetryCount(value: unknown): number | null { const numeric = finiteNumber(value) if (numeric === null) return null diff --git a/src/server/transcript/entries.ts b/src/server/transcript/entries.ts index b793aff5..45ccf587 100644 --- a/src/server/transcript/entries.ts +++ b/src/server/transcript/entries.ts @@ -32,7 +32,7 @@ export type TranscriptEntry = { /** A visible conversation message, before it is split into display items. */ export type MessageEntry = { id: string - type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result' + type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result' | 'permission_guardrail_decision' content: unknown toolUseResult?: unknown timestamp: string @@ -114,6 +114,39 @@ function isToolResultContent(content: unknown): boolean { ) } +function permissionGuardrailEntryToMessage(entry: TranscriptEntry): MessageEntry | null { + if (entry.type !== 'attachment' || !entry.attachment || typeof entry.attachment !== 'object') return null + const marker = entry.attachment as Record + const decision = marker.decision + const preset = marker.preset + const score = marker.score + if ( + marker.type !== 'permission_guardrail_decision' || + (decision !== 'allow' && decision !== 'ask') || + (preset !== 'fewerPrompts' && preset !== 'safetyNet') || + typeof marker.toolUseID !== 'string' || + typeof marker.providerId !== 'string' || + typeof marker.modelId !== 'string' || + (score !== null && (typeof score !== 'number' || score < 0 || score > 1)) + ) { + return null + } + return { + id: entry.uuid || crypto.randomUUID(), + type: 'permission_guardrail_decision', + content: { + decision, + toolUseId: marker.toolUseID, + providerId: marker.providerId, + modelId: marker.modelId, + preset, + score, + }, + timestamp: entry.timestamp || new Date().toISOString(), + isSidechain: entry.isSidechain, + } +} + /** CLI bookkeeping that is written as a user or assistant message but is not conversation. */ export function shouldHideTranscriptEntry(entry: TranscriptEntry): boolean { const role = entry.message?.role @@ -243,6 +276,12 @@ export function entriesToMessages(entries: TranscriptEntry[]): MessageEntry[] { } for (const entry of entries) { + const guardrailMessage = permissionGuardrailEntryToMessage(entry) + if (guardrailMessage) { + messages.push(guardrailMessage) + continue + } + const goalMessage = goalLocalCommandEntryToMessage(entry) if (goalMessage) { messages.push(goalMessage) diff --git a/src/server/transcript/history.ts b/src/server/transcript/history.ts index e6bd1f45..5cb642f2 100644 --- a/src/server/transcript/history.ts +++ b/src/server/transcript/history.ts @@ -34,6 +34,7 @@ type UserBlock = { } type CompactSummaryItem = Extract type AssistantTextItem = Extract +type PermissionGuardrailDecisionItem = Extract export type InterpretOptions = { /** Team member transcripts show prompts sent by teammates; the lead's own session hides them. */ @@ -161,6 +162,17 @@ export function interpretTranscript(messages: MessageEntry[], options?: Interpre for (const msg of messages) { const timestamp = new Date(msg.timestamp).getTime() + if (msg.type === 'permission_guardrail_decision') { + const marker = msg.content as Omit + items.push({ + id: allocateId(msg.id), + type: 'permission_guardrail_decision', + ...marker, + timestamp, + }) + continue + } + if (msg.type === 'system' && typeof msg.content === 'string') { const text = msg.content.trim() if (text === 'Conversation compacted' || text === 'Context compacted') { diff --git a/src/server/transcript/items.ts b/src/server/transcript/items.ts index 7015dadb..0a3310b1 100644 --- a/src/server/transcript/items.ts +++ b/src/server/transcript/items.ts @@ -78,6 +78,17 @@ export type TranscriptItem = teamCount?: number timestamp: number } + | { + id: string + type: 'permission_guardrail_decision' + decision: 'allow' | 'ask' + toolUseId: string + providerId: string + modelId: string + preset: 'fewerPrompts' | 'safetyNet' + score: number | null + timestamp: number + } export type TodoItem = { content: string; status: string; activeForm?: string } diff --git a/src/server/types/provider.ts b/src/server/types/provider.ts index 461062a5..6421c903 100644 --- a/src/server/types/provider.ts +++ b/src/server/types/provider.ts @@ -38,6 +38,36 @@ export const ProviderModelsSchema = z.array(ProviderModelSchema).superRefine((mo }) }) +export const DecisionProviderFormatSchema = z.enum([ + 'openai_decisions', + 'openrouter_decisions', + 'openai_chat', + 'ollama_chat', +]) +export type DecisionProviderFormat = z.infer + +export const DecisionProviderPresetIdSchema = z.enum([ + 'openai', + 'openrouter', + 'ollama', + 'custom', +]) +export type DecisionProviderPresetId = z.infer + +export const DecisionProviderDeploymentSchema = z.enum(['hosted', 'local']) +export type DecisionProviderDeployment = z.infer + +export const SavedDecisionProviderSchema = z.object({ + id: z.string(), + presetId: DecisionProviderPresetIdSchema, + name: z.string().trim().min(1), + deployment: DecisionProviderDeploymentSchema, + apiFormat: DecisionProviderFormatSchema, + baseUrl: z.string().url(), + apiKey: z.string(), + models: ProviderModelsSchema.min(1), +}) + export const ModelContextWindowSchema = z.number().int().min(16000).max(10000000) export const ModelContextWindowsSchema = z.record( z.string().min(1), @@ -63,6 +93,34 @@ export const ProvidersIndexSchema = z.object({ schemaVersion: z.number().int().positive().optional(), activeId: z.string().nullable(), providers: z.array(SavedProviderSchema), + activeDecisionProviderId: z.string().nullable().default(null), + decisionProviders: z.array(SavedDecisionProviderSchema).default([]), +}) + +export const CreateDecisionProviderSchema = z.object({ + presetId: DecisionProviderPresetIdSchema, + name: z.string().trim().min(1), + apiFormat: DecisionProviderFormatSchema, + baseUrl: z.string().url(), + apiKey: z.string(), + models: ProviderModelsSchema.min(1), +}) + +export const UpdateDecisionProviderSchema = CreateDecisionProviderSchema.partial() + +export const TestDecisionProviderSchema = CreateDecisionProviderSchema.pick({ + apiFormat: true, + baseUrl: true, + apiKey: true, +}).extend({ + modelId: z.string().trim().min(1), +}) + +export const DiscoverDecisionProviderModelsSchema = CreateDecisionProviderSchema.pick({ + presetId: true, + apiFormat: true, + baseUrl: true, + apiKey: true, }) export const CreateProviderSchema = z.object({ @@ -111,11 +169,16 @@ export const DiscoverProviderModelsSchema = z.object({ export type ProviderModel = z.infer export type DiscoveredProviderModel = ProviderModel & { contextWindow?: number } export type SavedProvider = z.infer +export type SavedDecisionProvider = z.infer export type ProvidersIndex = z.infer export type CreateProviderInput = z.infer export type UpdateProviderInput = z.infer export type TestProviderInput = z.infer export type DiscoverProviderModelsInput = z.infer +export type CreateDecisionProviderInput = z.infer +export type UpdateDecisionProviderInput = z.infer +export type TestDecisionProviderInput = z.infer +export type DiscoverDecisionProviderModelsInput = z.infer export interface ProviderTestStepResult { success: boolean diff --git a/src/server/ws/protocol.ts b/src/server/ws/protocol.ts index 8fbd740c..bea34573 100644 --- a/src/server/ws/protocol.ts +++ b/src/server/ws/protocol.ts @@ -16,6 +16,7 @@ export type { BrowserActivity, PendingBrowserGrant, PendingPermission, + PermissionGuardrailDetails, ProcessDetail, ProcessPhase, RuntimeSelection, @@ -78,7 +79,7 @@ export type AttachmentRef = { } /** Transcript items the server recognizes in a live conversation and sends as they are. */ -export type LiveTranscriptItem = Extract +export type LiveTranscriptItem = Extract export type ServerMessage = | { type: 'connected'; sessionId: string } diff --git a/src/services/tools/toolExecution.ts b/src/services/tools/toolExecution.ts index 4e024010..f454a78d 100644 --- a/src/services/tools/toolExecution.ts +++ b/src/services/tools/toolExecution.ts @@ -236,6 +236,7 @@ function decisionReasonToOTelSource( return 'hook' case 'mode': case 'classifier': + case 'permissionGuardrail': case 'subcommandResults': case 'asyncAgent': case 'sandboxOverride': @@ -993,6 +994,23 @@ async function checkPermissionsAndCallTool( }) } + if ( + permissionDecision.behavior === 'allow' && + permissionDecision.decisionReason?.type === 'permissionGuardrail' + ) { + resultingMessages.push({ + message: createAttachmentMessage({ + type: 'permission_guardrail_decision', + decision: permissionDecision.decisionReason.decision, + toolUseID, + providerId: permissionDecision.decisionReason.providerId, + modelId: permissionDecision.decisionReason.modelId, + preset: permissionDecision.decisionReason.preset, + score: permissionDecision.decisionReason.score, + }), + }) + } + if (permissionDecision.behavior !== 'allow') { logForDebugging(`${tool.name} tool permission denied`) const decisionInfo = toolUseContext.toolDecisions?.get(toolUseID) diff --git a/src/tools/BashTool/bashPermissions.ts b/src/tools/BashTool/bashPermissions.ts index 83b3c633..ca01fbdd 100644 --- a/src/tools/BashTool/bashPermissions.ts +++ b/src/tools/BashTool/bashPermissions.ts @@ -1381,14 +1381,7 @@ function filterCdCwdSubcommands( return { subcommands, astCommandsByIdx } } -/** - * Early-exit deny enforcement for the AST too-complex and checkSemantics - * paths. Returns the exact-match result if non-passthrough (deny/ask/allow), - * then checks prefix/wildcard deny rules. Returns null if neither matched, - * meaning the caller should fall through to ask. Extracted to keep - * bashToolHasPermission under Bun's feature() DCE complexity threshold. - */ -function checkEarlyExitDeny( +function checkExplicitDeny( input: z.infer, toolPermissionContext: ToolPermissionContext, ): PermissionResult | null { @@ -1396,7 +1389,7 @@ function checkEarlyExitDeny( input, toolPermissionContext, ) - if (exactMatchResult.behavior !== 'passthrough') { + if (exactMatchResult.behavior === 'deny') { return exactMatchResult } const denyMatch = matchingRulesForInput( @@ -1404,12 +1397,26 @@ function checkEarlyExitDeny( toolPermissionContext, 'prefix', ).matchingDenyRules[0] - if (denyMatch !== undefined) { - return { - behavior: 'deny', - message: `Permission to use ${BashTool.name} with command ${input.command} has been denied.`, - decisionReason: { type: 'rule', rule: denyMatch }, - } + if (denyMatch === undefined) return null + return { + behavior: 'deny', + message: `Permission to use ${BashTool.name} with command ${input.command} has been denied.`, + decisionReason: { type: 'rule', rule: denyMatch }, + } +} + +function checkEarlyExitDeny( + input: z.infer, + toolPermissionContext: ToolPermissionContext, +): PermissionResult | null { + const explicitDeny = checkExplicitDeny(input, toolPermissionContext) + if (explicitDeny !== null) return explicitDeny + const exactMatchResult = bashToolCheckExactMatchPermission( + input, + toolPermissionContext, + ) + if (exactMatchResult.behavior !== 'passthrough') { + return exactMatchResult } return null } @@ -1664,6 +1671,23 @@ export async function bashToolHasPermission( input: z.infer, context: ToolUseContext, getCommandSubcommandPrefixFn = getCommandSubcommandPrefix, +): Promise { + const explicitDeny = checkExplicitDeny( + input, + context.getAppState().toolPermissionContext, + ) + if (explicitDeny !== null) return explicitDeny + return bashToolHasPermissionAfterExplicitDeny( + input, + context, + getCommandSubcommandPrefixFn, + ) +} + +async function bashToolHasPermissionAfterExplicitDeny( + input: z.infer, + context: ToolUseContext, + getCommandSubcommandPrefixFn: typeof getCommandSubcommandPrefix, ): Promise { let appState = context.getAppState() diff --git a/src/types/permissionGuardrail.ts b/src/types/permissionGuardrail.ts new file mode 100644 index 00000000..15c8c853 --- /dev/null +++ b/src/types/permissionGuardrail.ts @@ -0,0 +1,27 @@ +import { z } from 'zod/v4' + +export const PermissionGuardrailPresetSchema = z.enum([ + 'fewerPrompts', + 'safetyNet', +]) + +export const PermissionGuardrailSettingsSchema = z.object({ + enabled: z.boolean(), + providerId: z.string().nullable(), + modelId: z.string(), + preset: PermissionGuardrailPresetSchema, + threshold: z.number().min(0).max(1), + privacyNoticeVersion: z.number().int().nonnegative(), +}) + +export type PermissionGuardrailPreset = z.infer +export type PermissionGuardrailSettings = z.infer + +export const DEFAULT_PERMISSION_GUARDRAIL_SETTINGS: PermissionGuardrailSettings = { + enabled: false, + providerId: null, + modelId: '', + preset: 'safetyNet', + threshold: 0.95, + privacyNoticeVersion: 0, +} diff --git a/src/types/permissions.ts b/src/types/permissions.ts index 20c9f901..0b8eb24a 100644 --- a/src/types/permissions.ts +++ b/src/types/permissions.ts @@ -305,6 +305,14 @@ export type PermissionDecisionReason = classifier: string reason: string } + | { + type: 'permissionGuardrail' + providerId: string + modelId: string + preset: 'fewerPrompts' | 'safetyNet' + decision: 'allow' | 'ask' + score: number | null + } | { type: 'workingDir' reason: string diff --git a/src/utils/attachments.ts b/src/utils/attachments.ts index fc23bde5..dbee7162 100644 --- a/src/utils/attachments.ts +++ b/src/utils/attachments.ts @@ -382,6 +382,16 @@ export type HookPermissionDecisionAttachment = { hookEvent: HookEvent } +export type PermissionGuardrailDecisionAttachment = { + type: 'permission_guardrail_decision' + decision: 'allow' | 'ask' + toolUseID: string + providerId: string + modelId: string + preset: 'fewerPrompts' | 'safetyNet' + score: number | null +} + export type HookSystemMessageAttachment = { type: 'hook_system_message' content: string @@ -639,6 +649,7 @@ export type Attachment = | TeammateMailboxAttachment | TeamContextAttachment | HookAttachment + | PermissionGuardrailDecisionAttachment | { type: 'invoked_skills' skills: Array<{ diff --git a/src/utils/messages.ts b/src/utils/messages.ts index c9bd5ca9..fe388b5c 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -4347,6 +4347,7 @@ You have exited auto mode. The user may now want to interact more directly. You case 'hook_system_message': case 'structured_output': case 'hook_permission_decision': + case 'permission_guardrail_decision': return [] } diff --git a/src/utils/permissions/permissionGuardrail.test.ts b/src/utils/permissions/permissionGuardrail.test.ts new file mode 100644 index 00000000..c675ef5b --- /dev/null +++ b/src/utils/permissions/permissionGuardrail.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from 'bun:test' +import type { SavedDecisionProvider } from '../../server/types/provider.js' +import { + isPermissionGuardrailAutoAllowCalibrated, + parsePermissionGuardrailAskProbability, + resolvePermissionGuardrailBehavior, + resolvePermissionGuardrailProviderBehavior, +} from './permissionGuardrail.js' + +function provider( + apiFormat: SavedDecisionProvider['apiFormat'], +): SavedDecisionProvider { + return { + id: 'provider', + presetId: 'custom', + name: 'Provider', + deployment: 'hosted', + apiFormat, + baseUrl: 'https://example.com/v1', + apiKey: 'secret', + models: [{ id: 'model' }], + } +} + +describe('permission guardrail', () => { + test('parses OpenAI predicate answers from the answers array', () => { + expect(parsePermissionGuardrailAskProbability( + provider('openai_decisions'), + { answers: [{ type: 'predicate', name: 'ask', probability: 0.82 }] }, + )).toBe(0.82) + }) + + test('parses OpenRouter noul answers', () => { + expect(parsePermissionGuardrailAskProbability( + provider('openrouter_decisions'), + { answers: { ask: { noul: 0.73 } } }, + )).toBe(0.73) + }) + + test('parses chat risk without treating malformed output as safe', () => { + expect(parsePermissionGuardrailAskProbability( + provider('openai_chat'), + { choices: [{ message: { content: '{"risk":64}' } }] }, + )).toBe(0.64) + expect(parsePermissionGuardrailAskProbability( + provider('openai_chat'), + { choices: [{ message: { content: 'allow' } }] }, + )).toBeNull() + }) + + test('uses opposite threshold directions for the two presets', () => { + expect(resolvePermissionGuardrailBehavior('fewerPrompts', 0.01, 0.95)).toBe('allow') + expect(resolvePermissionGuardrailBehavior('fewerPrompts', 0.1, 0.95)).toBe('ask') + expect(resolvePermissionGuardrailBehavior('safetyNet', 0.98, 0.95)).toBe('ask') + expect(resolvePermissionGuardrailBehavior('safetyNet', 0.2, 0.95)).toBe('allow') + }) + + test('does not auto-allow an uncalibrated model', () => { + expect(isPermissionGuardrailAutoAllowCalibrated( + provider('openai_decisions'), + 'gpt-6-luna', + )).toBe(false) + expect(resolvePermissionGuardrailProviderBehavior( + provider('openai_decisions'), + 'gpt-6-luna', + 'fewerPrompts', + 0.01, + 0.95, + )).toBe('ask') + }) +}) diff --git a/src/utils/permissions/permissionGuardrail.ts b/src/utils/permissions/permissionGuardrail.ts new file mode 100644 index 00000000..763ba437 --- /dev/null +++ b/src/utils/permissions/permissionGuardrail.ts @@ -0,0 +1,289 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { normalizeProvidersIndex } from '../../server/services/providerRuntimeEnv.js' +import type { SavedDecisionProvider } from '../../server/types/provider.js' +import type { PermissionGuardrailPreset } from '../../types/permissionGuardrail.js' +import { getClaudeConfigHomeDir } from '../envUtils.js' +import { getPermissionGuardrailSettings } from '../settings/settings.js' + +const MAX_INPUT_CHARS = 1500 +const REQUEST_TIMEOUT_MS = 5000 +export const PERMISSION_GUARDRAIL_PRIVACY_NOTICE_VERSION = 1 + +const POLICY = + 'Ask the user first if the call could delete or overwrite data, discard uncommitted work, change system, security, account or permission settings, read, copy or send secrets, credentials or private data, send data to or change remote services, install or run code from untrusted sources, or do something the user did not ask for. Otherwise allow reads, searches, builds, tests and requested edits.' + +const CALIBRATED_AUTO_ALLOW_MODELS = new Set() + +export type PermissionGuardrailEvaluation = { + behavior: 'allow' | 'ask' + providerId: string + modelId: string + preset: PermissionGuardrailPreset + score: number | null +} + +type PermissionGuardrailInput = { + toolName: string + input: Record + messages: unknown[] +} + +export function isPermissionGuardrailAutoAllowCalibrated( + provider: SavedDecisionProvider, + modelId: string, +): boolean { + return CALIBRATED_AUTO_ALLOW_MODELS.has(`${provider.apiFormat}:${modelId}`) +} + +export async function evaluatePermissionGuardrail( + expectedPreset: PermissionGuardrailPreset, + value: PermissionGuardrailInput, +): Promise { + const settings = getPermissionGuardrailSettings() + if ( + !settings?.enabled || + settings.preset !== expectedPreset || + !settings.providerId || + !settings.modelId + ) { + return null + } + + const provider = readDecisionProvider(settings.providerId) + if (!provider || !provider.models.some((model) => model.id === settings.modelId)) { + return fallback(settings.providerId, settings.modelId, expectedPreset) + } + if ( + provider.deployment === 'hosted' && + settings.privacyNoticeVersion < PERMISSION_GUARDRAIL_PRIVACY_NOTICE_VERSION + ) { + return fallback(provider.id, settings.modelId, expectedPreset) + } + if (!provider.apiKey && provider.apiFormat !== 'ollama_chat') { + return fallback(provider.id, settings.modelId, expectedPreset) + } + let input: string + try { + input = renderInput(value) + } catch { + return fallback(provider.id, settings.modelId, expectedPreset) + } + + const score = await requestAskProbability(provider, settings.modelId, input) + if (score === null) return fallback(provider.id, settings.modelId, expectedPreset) + + const behavior = resolvePermissionGuardrailProviderBehavior( + provider, + settings.modelId, + expectedPreset, + score, + settings.threshold, + ) + + return { + behavior, + providerId: provider.id, + modelId: settings.modelId, + preset: expectedPreset, + score, + } +} + +export function resolvePermissionGuardrailProviderBehavior( + provider: SavedDecisionProvider, + modelId: string, + preset: PermissionGuardrailPreset, + pAsk: number, + threshold: number, +): 'allow' | 'ask' { + if ( + preset === 'fewerPrompts' && + !isPermissionGuardrailAutoAllowCalibrated(provider, modelId) + ) { + return 'ask' + } + return resolvePermissionGuardrailBehavior(preset, pAsk, threshold) +} + +export function resolvePermissionGuardrailBehavior( + preset: PermissionGuardrailPreset, + pAsk: number, + threshold: number, +): 'allow' | 'ask' { + return preset === 'fewerPrompts' + ? 1 - pAsk >= threshold ? 'allow' : 'ask' + : pAsk >= threshold ? 'ask' : 'allow' +} + +function fallback( + providerId: string, + modelId: string, + preset: PermissionGuardrailPreset, +): PermissionGuardrailEvaluation { + return { behavior: 'ask', providerId, modelId, preset, score: null } +} + +function readDecisionProvider(id: string): SavedDecisionProvider | null { + try { + const path = join(getClaudeConfigHomeDir(), 'dreamcoder', 'providers.json') + const index = normalizeProvidersIndex(JSON.parse(readFileSync(path, 'utf-8'))) + return index?.decisionProviders.find((provider) => provider.id === id) ?? null + } catch { + return null + } +} + +function renderInput(value: PermissionGuardrailInput): string { + const userRequest = findLatestUserText(value.messages) + const text = [ + `User request:\n${userRequest || '(not available)'}`, + `Tool call to decide on:\n${value.toolName} ${JSON.stringify(value.input)}`, + `Working directory: ${process.cwd()}`, + ].join('\n\n') + return clipMiddle(text, MAX_INPUT_CHARS).toWellFormed() +} + +function findLatestUserText(messages: unknown[]): string { + for (let index = messages.length - 1; index >= 0; index--) { + const entry = asRecord(messages[index]) + const message = asRecord(entry?.message) ?? entry + if (message?.role !== 'user') continue + const content = message.content + if (typeof content === 'string') return content + if (!Array.isArray(content)) continue + const text = content + .flatMap((block): string[] => { + const record = asRecord(block) + return record?.type === 'text' && typeof record.text === 'string' + ? [record.text] + : [] + }) + .join('\n') + if (text) return text + } + return '' +} + +function asRecord(value: unknown): Record | null { + return value && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : null +} + +function clipMiddle(text: string, max: number): string { + if (text.length <= max) return text + const marker = ` [... ${text.length - max} chars cut ...] ` + const keep = Math.max(0, max - marker.length) + const head = Math.ceil(keep * 0.7) + return text.slice(0, head) + marker + text.slice(text.length - (keep - head)) +} + +async function requestAskProbability( + provider: SavedDecisionProvider, + modelId: string, + input: string, +): Promise { + try { + const response = await fetch(requestEndpoint(provider), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {}), + }, + body: JSON.stringify(requestBody(provider, modelId, input)), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + if (!response.ok) return null + return parsePermissionGuardrailAskProbability(provider, await response.json()) + } catch { + return null + } +} + +function requestEndpoint(provider: SavedDecisionProvider): string { + const baseUrl = provider.baseUrl.replace(/\/+$/, '') + if (provider.apiFormat === 'ollama_chat') return `${baseUrl}/api/chat` + if (provider.apiFormat === 'openai_chat') return `${baseUrl}/chat/completions` + return `${baseUrl}/decisions` +} + +function requestBody( + provider: SavedDecisionProvider, + modelId: string, + input: string, +): Record { + const instructions = `Should the agent ask the user before running this tool call?\n\nPolicy: ${POLICY}` + if (provider.apiFormat === 'openai_decisions') { + return { + model: modelId, + input, + questions: [{ type: 'predicate', name: 'ask', instructions }], + } + } + if (provider.apiFormat === 'openrouter_decisions') { + return { + model: modelId, + state: { situation: input }, + questions: { + ask: { + type: 'noul', + instructions, + criteria: { + true: 'Ask the user first.', + false: 'Run it without asking.', + }, + }, + }, + } + } + const prompt = `Policy: ${POLICY}\n\n${input}\n\nReturn JSON with an integer risk from 0 to 100.` + if (provider.apiFormat === 'ollama_chat') { + return { + model: modelId, + stream: false, + format: { + type: 'object', + properties: { risk: { type: 'integer', minimum: 0, maximum: 100 } }, + required: ['risk'], + }, + messages: [{ role: 'user', content: prompt }], + } + } + return { + model: modelId, + response_format: { type: 'json_object' }, + messages: [{ role: 'user', content: prompt }], + } +} + +export function parsePermissionGuardrailAskProbability( + provider: SavedDecisionProvider, + payload: unknown, +): number | null { + const record = asRecord(payload) + if (!record) return null + if (provider.apiFormat === 'openai_decisions') { + const answers = Array.isArray(record.answers) ? record.answers : [] + const answer = answers.map(asRecord).find((value) => value?.name === 'ask') + return probability(answer?.probability) + } + if (provider.apiFormat === 'openrouter_decisions') { + const answers = asRecord(record.answers) + return probability(asRecord(answers?.ask)?.noul) + } + const content = provider.apiFormat === 'ollama_chat' + ? asRecord(record.message)?.content + : asRecord((Array.isArray(record.choices) ? asRecord(record.choices[0]) : null)?.message)?.content + if (typeof content !== 'string') return null + try { + const risk = asRecord(JSON.parse(content))?.risk + return typeof risk === 'number' && risk >= 0 && risk <= 100 ? risk / 100 : null + } catch { + return null + } +} + +function probability(value: unknown): number | null { + return typeof value === 'number' && value >= 0 && value <= 1 ? value : null +} diff --git a/src/utils/permissions/permissions.permissionGuardrail.test.ts b/src/utils/permissions/permissions.permissionGuardrail.test.ts new file mode 100644 index 00000000..d702718f --- /dev/null +++ b/src/utils/permissions/permissions.permissionGuardrail.test.ts @@ -0,0 +1,186 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { z } from 'zod/v4' +import type { Tool, ToolUseContext } from '../../Tool.js' +import { getEmptyToolPermissionContext } from '../../Tool.js' +import { getDefaultAppState, type AppState } from '../../state/AppStateStore.js' +import type { AssistantMessage } from '../../types/message.js' +import { bashToolHasPermission } from '../../tools/BashTool/bashPermissions.js' +import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' +import { powershellToolHasPermission } from '../../tools/PowerShellTool/powershellPermissions.js' +import { POWERSHELL_TOOL_NAME } from '../../tools/PowerShellTool/toolName.js' +import { resetSettingsCache } from '../settings/settingsCache.js' +import { hasPermissionsToUseTool } from './permissions.js' + +describe('permission guardrail pipeline', () => { + let configDir: string + let originalConfigDir: string | undefined + let originalFetch: typeof fetch + + beforeEach(() => { + configDir = mkdtempSync(join(tmpdir(), 'permission-guardrail-')) + originalConfigDir = process.env.DREAMCODER_CONFIG_DIR + originalFetch = globalThis.fetch + process.env.DREAMCODER_CONFIG_DIR = configDir + mkdirSync(join(configDir, 'dreamcoder'), { recursive: true }) + writeFileSync(join(configDir, 'settings.json'), JSON.stringify({ + permissionGuardrail: { + enabled: true, + providerId: 'decision-provider', + modelId: 'decision-model', + preset: 'safetyNet', + threshold: 0.5, + privacyNoticeVersion: 1, + }, + })) + writeFileSync(join(configDir, 'dreamcoder', 'providers.json'), JSON.stringify({ + activeId: null, + providers: [], + activeDecisionProviderId: 'decision-provider', + decisionProviders: [{ + id: 'decision-provider', + presetId: 'custom', + name: 'Decision Provider', + deployment: 'hosted', + apiFormat: 'openai_decisions', + baseUrl: 'https://example.com/v1', + apiKey: 'test-placeholder', + models: [{ id: 'decision-model' }], + }], + })) + resetSettingsCache() + }) + + afterEach(() => { + globalThis.fetch = originalFetch + if (originalConfigDir === undefined) delete process.env.DREAMCODER_CONFIG_DIR + else process.env.DREAMCODER_CONFIG_DIR = originalConfigDir + resetSettingsCache() + rmSync(configDir, { recursive: true, force: true }) + }) + + test('keeps an allowing safety-net score in the permission decision', async () => { + globalThis.fetch = mock(async () => Response.json({ + answers: [{ type: 'predicate', name: 'ask', probability: 0.2 }], + })) as unknown as typeof fetch + + const tool = { + name: 'Bash', + inputSchema: z.object({ command: z.string() }), + checkPermissions: async () => ({ + behavior: 'passthrough' as const, + message: 'Confirm command', + }), + } as unknown as Tool + let appState = { + ...getDefaultAppState(), + toolPermissionContext: { + ...getEmptyToolPermissionContext(), + mode: 'bypassPermissions' as const, + }, + } as AppState + const context = { + abortController: new AbortController(), + getAppState: () => appState, + setAppState: (update: (state: AppState) => AppState) => { + appState = update(appState) + }, + messages: [], + } as unknown as ToolUseContext + + const result = await hasPermissionsToUseTool( + tool, + { command: 'rm disposable.txt' }, + context, + {} as AssistantMessage, + 'tool-1', + ) + + expect(result).toEqual({ + behavior: 'allow', + updatedInput: { command: 'rm disposable.txt' }, + decisionReason: { + type: 'permissionGuardrail', + providerId: 'decision-provider', + modelId: 'decision-model', + preset: 'safetyNet', + decision: 'allow', + score: 0.2, + }, + }) + }) + + test('keeps Bash and PowerShell deny rules ahead of bypass guardrail evaluation', async () => { + let guardrailRequests = 0 + globalThis.fetch = mock(async () => { + guardrailRequests += 1 + return Response.json({ + answers: [{ type: 'predicate', name: 'ask', probability: 0 }], + }) + }) as unknown as typeof fetch + + const inputSchema = z.object({ command: z.string() }) + const tools = [ + { + name: BASH_TOOL_NAME, + command: + 'if [ -f "guardrail-deny-rule.txt" ]; then rm guardrail-deny-rule.txt; echo "Deleted guardrail-deny-rule.txt"; else echo "guardrail-deny-rule.txt not found"; fi', + checkPermissions: bashToolHasPermission, + }, + { + name: POWERSHELL_TOOL_NAME, + command: 'Remove-Item guardrail-deny-rule.txt', + checkPermissions: powershellToolHasPermission, + }, + ] + let appState = { + ...getDefaultAppState(), + toolPermissionContext: { + ...getEmptyToolPermissionContext(), + mode: 'bypassPermissions' as const, + alwaysDenyRules: { + localSettings: [ + 'Bash(*guardrail-deny-rule.txt*)', + 'PowerShell(*guardrail-deny-rule.txt*)', + ], + }, + }, + } as AppState + const context = { + abortController: new AbortController(), + getAppState: () => appState, + setAppState: (update: (state: AppState) => AppState) => { + appState = update(appState) + }, + messages: [], + } as unknown as ToolUseContext + + for (const shellTool of tools) { + const tool = { + name: shellTool.name, + inputSchema, + checkPermissions: shellTool.checkPermissions, + } as unknown as Tool + const result = await hasPermissionsToUseTool( + tool, + { command: shellTool.command }, + context, + {} as AssistantMessage, + `tool-${shellTool.name}`, + ) + + expect(result.behavior).toBe('deny') + expect(result.decisionReason).toMatchObject({ + type: 'rule', + rule: { + ruleBehavior: 'deny', + ruleValue: { toolName: shellTool.name }, + }, + }) + } + + expect(guardrailRequests).toBe(0) + }) +}) diff --git a/src/utils/permissions/permissions.ts b/src/utils/permissions/permissions.ts index 99db042d..9b63bb9f 100644 --- a/src/utils/permissions/permissions.ts +++ b/src/utils/permissions/permissions.ts @@ -103,6 +103,7 @@ import { classifyYoloAction, formatActionForClassifier, } from './yoloClassifier.js' +import { evaluatePermissionGuardrail } from './permissionGuardrail.js' const CLASSIFIER_FAIL_CLOSED_REFRESH_MS = 30 * 60 * 1000 // 30 minutes @@ -479,12 +480,43 @@ export const hasPermissionsToUseTool: CanUseToolFn = async ( ): Promise => { const result = await hasPermissionsToUseToolInner(tool, input, context) + const appState = context.getAppState() + + if ( + result.behavior === 'allow' && + appState.toolPermissionContext.mode === 'bypassPermissions' && + result.decisionReason?.type === 'mode' && + result.decisionReason.mode === 'bypassPermissions' + ) { + const evaluation = await evaluatePermissionGuardrail('safetyNet', { + toolName: tool.name, + input, + messages: context.messages, + }) + if (evaluation) { + const decisionReason = { + type: 'permissionGuardrail' as const, + providerId: evaluation.providerId, + modelId: evaluation.modelId, + preset: evaluation.preset, + decision: evaluation.behavior, + score: evaluation.score, + } + if (evaluation.behavior === 'allow') { + return { ...result, decisionReason } + } + return { + behavior: 'ask', + message: 'Permission guardrail requires confirmation', + decisionReason, + } + } + } // Reset consecutive denials on any allowed tool use in auto mode. // This ensures that a successful tool use (even one auto-allowed by rules) // breaks the consecutive denial streak. if (result.behavior === 'allow') { - const appState = context.getAppState() if (feature('TRANSCRIPT_CLASSIFIER')) { const currentDenialState = context.localDenialTracking ?? appState.denialTracking @@ -503,8 +535,6 @@ export const hasPermissionsToUseTool: CanUseToolFn = async ( // Apply dontAsk mode transformation: convert 'ask' to 'deny' // This is done at the end so it can't be bypassed by early returns if (result.behavior === 'ask') { - const appState = context.getAppState() - if (appState.toolPermissionContext.mode === 'dontAsk') { return { behavior: 'deny', @@ -515,6 +545,36 @@ export const hasPermissionsToUseTool: CanUseToolFn = async ( message: DONT_ASK_REJECT_MESSAGE(tool.name), } } + if ( + (appState.toolPermissionContext.mode === 'default' || + appState.toolPermissionContext.mode === 'acceptEdits') && + result.decisionReason?.type !== 'safetyCheck' && + !tool.requiresUserInteraction?.() + ) { + const evaluation = await evaluatePermissionGuardrail('fewerPrompts', { + toolName: tool.name, + input, + messages: context.messages, + }) + if (evaluation) { + const decisionReason = { + type: 'permissionGuardrail' as const, + providerId: evaluation.providerId, + modelId: evaluation.modelId, + preset: evaluation.preset, + decision: evaluation.behavior, + score: evaluation.score, + } + if (evaluation.behavior === 'allow') { + return { + behavior: 'allow', + updatedInput: input, + decisionReason, + } + } + return { ...result, decisionReason } + } + } // Apply auto mode: use AI classifier instead of prompting user // Check this BEFORE shouldAvoidPermissionPrompts so classifiers work in headless mode if ( diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index c1235ecb..6b697f01 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -4410,6 +4410,9 @@ export function isLoggableMessage(m: Message): boolean { // When enabled, we allow hook_additional_context through since it contains // user-configured hook output that is useful for session context on resume. if (m.type === 'attachment' && getUserType() !== 'ant') { + if (m.attachment.type === 'permission_guardrail_decision') { + return true + } if ( m.attachment.type === 'hook_additional_context' && isEnvTruthy(process.env.CLAUDE_CODE_SAVE_HOOK_ADDITIONAL_CONTEXT) diff --git a/src/utils/settings/settings.ts b/src/utils/settings/settings.ts index 3bea04af..78aac4de 100644 --- a/src/utils/settings/settings.ts +++ b/src/utils/settings/settings.ts @@ -45,6 +45,10 @@ import { setSessionSettingsCache, } from './settingsCache.js' import { type SettingsJson, SettingsSchema } from './types.js' +import { + PermissionGuardrailSettingsSchema, + type PermissionGuardrailSettings, +} from '../../types/permissionGuardrail.js' import { filterInvalidPermissionRules, formatZodError, @@ -927,6 +931,19 @@ export function getUseAutoModeDuringPlan(): boolean { return true } +export function getPermissionGuardrailSettings(): PermissionGuardrailSettings | undefined { + for (const source of [ + 'policySettings', + 'flagSettings', + 'userSettings', + ] as const) { + const value = getSettingsForSource(source)?.permissionGuardrail + const result = PermissionGuardrailSettingsSchema.safeParse(value) + if (result.success) return result.data + } + return undefined +} + /** * Returns the merged autoMode config from trusted settings sources. * Only available when TRANSCRIPT_CLASSIFIER is active; returns undefined otherwise. diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index 006b8ffd..9fad923b 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -1,6 +1,7 @@ import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { SandboxSettingsSchema } from '../../entrypoints/sandboxTypes.js' +import { PermissionGuardrailSettingsSchema } from '../../types/permissionGuardrail.js' import { isEnvTruthy } from '../envUtils.js' import { lazySchema } from '../lazySchema.js' import { @@ -330,6 +331,7 @@ export const SettingsSchema = lazySchema(() => .describe( 'Number of days to retain chat transcripts (default: 30). Setting to 0 disables session persistence entirely: no transcripts are written and existing transcripts are deleted at startup.', ), + permissionGuardrail: PermissionGuardrailSettingsSchema.optional(), env: EnvironmentVariablesSchema() .optional() .describe('Environment variables to set for Claude Code sessions'), From b1f4e594e07d80d0c9a568f9c8b0721338b6ad1b Mon Sep 17 00:00:00 2001 From: AIR-Xie <98206374+AIR-Xie@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:39:58 +0800 Subject: [PATCH 2/5] feat(permissions): add model-assisted auto mode --- CHANGELOG.md | 3 +- desktop/src/api/providers.ts | 6 +- .../src/components/chat/MessageList.test.tsx | 14 +- desktop/src/components/chat/MessageList.tsx | 8 +- .../src/components/chat/PermissionDialog.tsx | 10 +- desktop/src/components/chat/ToolCallBlock.tsx | 96 ++++--- .../src/components/chat/chatBlocks.test.tsx | 20 +- .../controls/PermissionModeSelector.test.tsx | 3 + .../controls/PermissionModeSelector.tsx | 9 +- .../DecisionProviderFormModal.test.tsx | 17 +- .../settings/DecisionProviderFormModal.tsx | 14 +- .../DecisionProviderSettingsSection.tsx | 7 +- .../settings/ProviderSettings.test.tsx | 3 +- desktop/src/i18n/locales/en.ts | 50 ++-- desktop/src/i18n/locales/zh.ts | 50 ++-- .../src/pages/Settings.browserTrust.test.tsx | 55 +++- desktop/src/pages/Settings.tsx | 131 ++++----- desktop/src/stores/chatStore.test.ts | 28 +- desktop/src/stores/chatStore.ts | 31 ++- desktop/src/stores/decisionProviderStore.ts | 12 +- desktop/src/stores/settingsStore.ts | 37 +-- desktop/src/types/settings.ts | 7 +- src/QueryEngine.ts | 4 +- .../structuredIO.permissionGuardrail.test.ts | 22 +- src/cli/structuredIO.ts | 10 +- .../PermissionDecisionDebugInfo.tsx | 4 +- .../permissions/PermissionRuleExplanation.tsx | 4 +- src/components/permissions/hooks.ts | 4 +- src/entrypoints/sdk/controlSchemas.ts | 4 +- src/entrypoints/sdk/coreSchemas.ts | 6 +- .../__tests__/e2e/business-flow.test.ts | 8 +- src/server/__tests__/permission-modes.test.ts | 7 +- .../__tests__/provider-runtime-env.test.ts | 46 +++- src/server/__tests__/providers.test.ts | 15 +- src/server/__tests__/settings.test.ts | 13 +- .../__tests__/transcript-history.test.ts | 8 +- src/server/__tests__/ws-memory-events.test.ts | 16 +- src/server/api/providers.ts | 8 +- src/server/services/providerRuntimeEnv.ts | 33 ++- src/server/services/providerService.ts | 23 +- src/server/services/settingsService.ts | 10 +- src/server/session/state/types.ts | 4 +- src/server/session/translate.ts | 27 +- src/server/transcript/entries.ts | 15 +- src/server/transcript/items.ts | 4 +- src/server/types/provider.ts | 1 - src/server/ws/protocol.ts | 6 +- src/services/tools/toolExecution.ts | 8 +- src/types/permissionGuardrail.ts | 18 +- src/types/permissions.ts | 11 +- src/utils/attachments.ts | 4 +- src/utils/permissions/PermissionMode.ts | 25 +- .../permissions/getNextPermissionMode.test.ts | 20 ++ .../permissions/getNextPermissionMode.ts | 57 +--- .../permissions/permissionGuardrail.test.ts | 43 ++- src/utils/permissions/permissionGuardrail.ts | 202 +++++++++----- .../permissions.permissionGuardrail.test.ts | 250 ++++++++++++------ src/utils/permissions/permissions.ts | 62 ++--- src/utils/settings/settings.ts | 10 +- src/utils/settings/types.ts | 4 +- src/utils/settings/validationTips.ts | 2 +- 61 files changed, 946 insertions(+), 683 deletions(-) create mode 100644 src/utils/permissions/getNextPermissionMode.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 938c364d..a5f5cff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,11 @@ All notable changes to DreamCoder will be documented in this file. ### Added -- An opt-in permission guardrail can use separately configured OpenAI, OpenRouter, Ollama, or custom decision providers after deterministic permission rules. It defaults off, fails back to asking, preserves deny and bypass-immune checks, and records model-assisted permission decisions in the transcript. The desktop `/permissions` command opens permission settings, which document the project rule file and cross-platform shell rule names (#146). +- Auto is a separate permission mode that applies deterministic rules first and asks a configured decision model only for unresolved confirmations. It can use a dedicated decision provider or the active provider's explicit fast model, fails back to asking on unsupported models and request errors, preserves deny and bypass-immune checks, and records versioned model decisions in the transcript. The desktop `/permissions` command opens permission settings, which document the project rule file and cross-platform shell rule names (#146). ### Fixed +- Permission guardrail settings preserve manually entered decision model IDs, concurrent tool requests show one confirmation at a time, and workspace-rule denials show the blocked command in a copyable plain-text card. Auto uses the active provider's selected model when no separate fast model is configured (#146). - Confirmation dialogs keep long workspace paths inside their bounds, so Cancel and Enable remain visible when enabling bypass permissions (#226). Thanks @imshah-id. ## [0.8.0] — 2026-11-01 diff --git a/desktop/src/api/providers.ts b/desktop/src/api/providers.ts index 04a4022b..652d5bdc 100644 --- a/desktop/src/api/providers.ts +++ b/desktop/src/api/providers.ts @@ -26,7 +26,7 @@ type AuthStatusResponse = { source: 'dreamcoder-provider' | 'original-settings' | 'env' | 'none' activeProvider?: string } -type DecisionProvidersResponse = { providers: SavedDecisionProvider[]; activeId: string | null } +type DecisionProvidersResponse = { providers: SavedDecisionProvider[] } type DecisionProviderResponse = { provider: SavedDecisionProvider } type DecisionPresetsResponse = { presets: DecisionProviderPreset[] } @@ -99,10 +99,6 @@ export const providersApi = { return api.delete<{ ok: true }>(`/api/providers/decisions/${id}`) }, - activateDecision(id: string) { - return api.post<{ ok: true }>(`/api/providers/decisions/${id}/activate`) - }, - testDecision(id: string, modelId?: string) { return api.post(`/api/providers/decisions/${id}/test`, { modelId }) }, diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index cb7def83..d36fac6c 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -459,8 +459,9 @@ describe('MessageList nested tool calls', () => { toolUseId: 'tool-1', providerId: 'provider-1', modelId: 'gpt-6-luna', - preset: 'safetyNet', + source: 'decisionProvider', score: 0.237, + policyVersion: 1, timestamp: 1, }], }), @@ -469,9 +470,9 @@ describe('MessageList nested tool calls', () => { render() - expect(screen.getByText('Auto-approved by permission guardrail')).toBeTruthy() + expect(screen.getByText('Auto-approved')).toBeTruthy() expect(screen.getByText('gpt-6-luna')).toBeTruthy() - expect(screen.getByText('safetyNet')).toBeTruthy() + expect(screen.getByText('Decision provider')).toBeTruthy() expect(screen.getByText('Decision: allow')).toBeTruthy() expect(screen.getByText('Score: 0.237')).toBeTruthy() }) @@ -480,9 +481,10 @@ describe('MessageList nested tool calls', () => { const guardrail = { providerId: 'provider-1', modelId: 'gpt-6-luna', - preset: 'safetyNet' as const, + source: 'decisionProvider' as const, decision: 'ask' as const, score: 0.812, + policyVersion: 1, } useChatStore.setState({ sessions: { @@ -510,9 +512,9 @@ describe('MessageList nested tool calls', () => { render() - expect(screen.getByText('Permission guardrail')).toBeTruthy() + expect(screen.getByText('Auto permission model')).toBeTruthy() expect(screen.getByText('gpt-6-luna')).toBeTruthy() - expect(screen.getByText('Safety net')).toBeTruthy() + expect(screen.getByText('Decision provider')).toBeTruthy() expect(screen.getByText('Decision: ask')).toBeTruthy() expect(screen.getByText('Score: 0.812')).toBeTruthy() }) diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 99e8e052..f053832a 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -869,11 +869,15 @@ function PermissionGuardrailDecisionCard({ message }: { message: PermissionGuard {message.modelId} - {message.preset} + {t(message.source === 'decisionProvider' + ? 'chat.permissionGuardrail.decisionProvider' + : 'chat.permissionGuardrail.fastModel')} {t('chat.permissionGuardrail.decision', { decision: message.decision })} {score} + + {t('chat.permissionGuardrail.policyVersion', { version: String(message.policyVersion) })} ) @@ -1032,7 +1036,7 @@ function getMessageContentWeight(message: UIMessage): number { case 'memory_event': return (message.message?.length ?? 0) + message.files.reduce((total, file) => total + file.path.length + (file.summary?.length ?? 0), 0) case 'permission_guardrail_decision': - return message.modelId.length + message.preset.length + return message.modelId.length + message.source.length + (message.reason?.length ?? 0) case 'background_task': return getShallowStringWeight(message.task) case 'task_summary': diff --git a/desktop/src/components/chat/PermissionDialog.tsx b/desktop/src/components/chat/PermissionDialog.tsx index a2376369..cdf9a28d 100644 --- a/desktop/src/components/chat/PermissionDialog.tsx +++ b/desktop/src/components/chat/PermissionDialog.tsx @@ -165,16 +165,18 @@ export function PermissionDialog({ sessionId, requestId, toolName, input, descri