diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 485e2bbd..6cdefb99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v2 with: @@ -31,7 +31,7 @@ jobs: - name: Install ripgrep run: sudo apt-get update && sudo apt-get install -y ripgrep - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock', 'desktop/bun.lock', 'vendor/showai/bun.lock') }} @@ -54,13 +54,13 @@ jobs: run: working-directory: desktop steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.4.2 - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock', 'desktop/bun.lock', 'vendor/showai/bun.lock') }} @@ -82,13 +82,13 @@ jobs: run: working-directory: desktop steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.4.2 - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.bun/install/cache key: bun-${{ runner.os }}-${{ hashFiles('bun.lock', 'desktop/bun.lock', 'vendor/showai/bun.lock') }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffc9df3..33582a99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to DreamCoder will be documented in this file. ### Added +- 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). - **Sessions can start other sessions and message each other.** The agent can start a new session with `StartSession`, either fresh with a brief or as a fork of its own conversation, optionally in a new git worktree; you approve every new session, whatever the permission mode. Any session can message any other with `SendMessage`, across projects: a message wakes the receiver, and a progress report from a child to its parent waits for the parent's next turn instead of interrupting it (a parent that is already working sees it right away). Reports waiting for the parent are listed above its message box. The server stamps who sent each message, so a session cannot pass itself off as you or as another session. `ListSessions` lists sessions, and `RenameSession` lets the agent rename its own session (#239). - **Sessions are nested in the sidebar.** Sessions the agent started, and conversations you forked, appear under the session they came from, with an icon for each kind and a count that collapses or expands them. Messages from another session appear in the conversation as a one-line row with the sender, which expands to the full text (#239). - **Project board.** Each project gets a persistent board of tasks and sub-tasks that you and your sessions share; every worktree of the project uses the same board. Open it from the project in the sidebar. A task records its owner, reviewer and creator, acceptance criteria, priority, dependencies, progress, and the owner's and reviewer's reports. Sessions read and update the board with four tools (`KanbanList`, `KanbanGet`, `KanbanCreate`, `KanbanUpdate`): they split work into sub-tasks, record progress, block a task with a question, submit their work and review it. The board notifies the sessions involved when they are assigned, when work is submitted, blocked, sent back, reset or cancelled, and when you comment. `StartSession` can start a session as a task's owner or reviewer. The board page has a board view and a hierarchy view, opens a task in the side panel, and lists what waits on you under "Needs me": blocked tasks, deliveries to accept and tasks whose owner session is gone. You approve every time a session creates the board, starts a session on a task, cancels a task, changes acceptance criteria it did not set, or makes itself a reviewer; only a task's creator or you choose its reviewer (#242). @@ -18,6 +19,7 @@ All notable changes to DreamCoder will be documented in this file. - Permission prompts that ask every time, such as starting a session or editing files under `.git/`, no longer offer "Allow for this session", which could not take effect (#242). - Tools sent to providers through the OpenAI Responses API are no longer in strict mode, so their optional parameters stay optional (#242). +- 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. - Reading environment files, credential files, and SSH private keys now requires explicit approval through both the Read tool and read-only Bash commands. Recursive directory reads, Grep tool glob filters, and files read from Git history are not covered yet (#235). Thanks @AIR-Xie. - The exact shell no-op `:` is recognized as read-only and no longer requires approval (#235). Thanks @AIR-Xie. 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..652d5bdc 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[] } +type DecisionProviderResponse = { provider: SavedDecisionProvider } +type DecisionPresetsResponse = { presets: DecisionProviderPreset[] } export const providersApi = { list() { @@ -69,4 +78,36 @@ 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}`) + }, + + 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..d36fac6c 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -448,6 +448,77 @@ 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', + source: 'decisionProvider', + score: 0.237, + policyVersion: 1, + timestamp: 1, + }], + }), + }, + }) + + render() + + expect(screen.getByText('Auto-approved')).toBeTruthy() + expect(screen.getByText('gpt-6-luna')).toBeTruthy() + expect(screen.getByText('Decision provider')).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', + source: 'decisionProvider' as const, + decision: 'ask' as const, + score: 0.812, + policyVersion: 1, + } + 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('Auto permission model')).toBeTruthy() + expect(screen.getByText('gpt-6-luna')).toBeTruthy() + expect(screen.getByText('Decision provider')).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 8d949ef1..e6a905ec 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' @@ -45,6 +45,7 @@ type MemoryEvent = Extract type GoalEvent = Extract type BackgroundTaskEvent = Extract type CompactSummaryEvent = Extract +type PermissionGuardrailDecision = Extract type ThinkingMessage = Extract type ActivityStep = ToolCall | ThinkingMessage @@ -852,6 +853,38 @@ 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 @@ -1004,6 +1037,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.source.length + (message.reason?.length ?? 0) case 'background_task': return getShallowStringWeight(message.task) case 'task_summary': @@ -1064,6 +1099,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 @@ -1099,6 +1135,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': @@ -2134,6 +2172,7 @@ export const MessageBlock = memo(function MessageBlock({ toolName={message.toolName} input={message.input} description={message.description} + guardrail={message.guardrail} alwaysAsks={message.alwaysAsks} resolution={message.resolution} /> @@ -2161,6 +2200,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 2f24888d..92c98070 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' @@ -14,7 +14,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 @@ -22,6 +22,7 @@ type Props = { toolName: string input: unknown description?: string + guardrail?: PermissionGuardrailDetails alwaysAsks?: boolean resolution?: PermissionResolution } @@ -202,7 +203,7 @@ function resolutionLabelKey(resolution: PermissionResolution | undefined): Trans return resolution.outcome === 'allowed' ? 'permission.allowedElsewhere' : 'permission.deniedElsewhere' } -export function PermissionDialog({ sessionId, requestId, toolName, input, description, alwaysAsks, resolution }: Props) { +export function PermissionDialog({ sessionId, requestId, toolName, input, description, guardrail, alwaysAsks, resolution }: Props) { const respondToPermission = useChatStore((s) => s.respondToPermission) const activeTabId = useTabStore((s) => s.activeTabId) const targetSessionId = sessionId ?? activeTabId @@ -252,6 +253,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 && rawInput !== '{}' + 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/ToolCallBlock.tsx b/desktop/src/components/chat/ToolCallBlock.tsx index e93fa2f7..0716bf10 100644 --- a/desktop/src/components/chat/ToolCallBlock.tsx +++ b/desktop/src/components/chat/ToolCallBlock.tsx @@ -4,7 +4,7 @@ import { Trace, TraceCollapsible, TraceCollapsibleTrigger, TraceDescription, Tra import { CollapsibleContent } from '../shared/ui/collapsible' import { Tooltip as HintTooltip } from '../shared/Tooltip' import { memo, useMemo, useState } from 'react' -import { AlertCircle } from 'lucide-react' +import { AlertCircle, ShieldX } from 'lucide-react' import { getToolIcon } from './toolIcons' import { CodeViewer } from './CodeViewer' import { DiffViewer } from './DiffViewer' @@ -60,8 +60,11 @@ export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, resu ) const diff = useMemo(() => renderDiff(toolName, obj), [obj, toolName]) const inputView = useMemo(() => (diff || streamingPreview ? null : getInputView(toolName, obj, cua !== null)), [cua, diff, obj, streamingPreview, toolName]) - // Read results are whole files; the transcript shows the call, not the contents. const outputText = toolName === 'Read' || diff ? null : stripAnsi(extractTextContent(result?.content) ?? '').trimEnd() || null + const permissionRuleDenied = result?.isError === true && isPermissionRuleDenial(result.content) + const deniedInput = typeof obj.command === 'string' + ? obj.command + : inputView?.text ?? (Object.keys(obj).length > 0 ? JSON.stringify(obj, null, 2) : '') const expandable = Boolean(diff || streamingPreview || inputView || outputText || resultImages.length > 0) const line = ( @@ -105,42 +108,56 @@ export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, resu ) return ( - - {expandable ? ( - - {line} - {cuaSettingsLink} - -
- - {diff} - {streamingPreview} - {(inputView || outputText) && ( -
- {inputView && } - {outputText && ( - - - {outputText} - - - )} -
- )} + <> + + {expandable ? ( + + {line} + {cuaSettingsLink} + +
+ + {diff} + {streamingPreview} + {(inputView || outputText) && ( +
+ {inputView && } + {outputText && ( + + + {outputText} + + + )} +
+ )} +
+
+
+ ) : ( + <> + {line} + {cuaSettingsLink} + + )} +
+ {permissionRuleDenied && deniedInput ? ( +
+
+
+ +
+ ) : null} + ) }) @@ -157,6 +174,11 @@ type InputView = { const SECTION_PRE_CLASS = 'overflow-auto whitespace-pre-wrap break-words py-2 pl-3 pr-14 font-[var(--font-mono)] text-[12px] leading-[1.45]' +function isPermissionRuleDenial(content: unknown): boolean { + const text = extractTextContent(content) + return typeof text === 'string' && /Permission to use .+ has been denied\./i.test(text) +} + // The most useful input field per tool; anything else stays behind "Show full input". const PRIMARY_INPUT_FIELD: Record = { Read: 'file_path', diff --git a/desktop/src/components/chat/chatBlocks.test.tsx b/desktop/src/components/chat/chatBlocks.test.tsx index a496a544..94ba4b6f 100644 --- a/desktop/src/components/chat/chatBlocks.test.tsx +++ b/desktop/src/components/chat/chatBlocks.test.tsx @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { act, fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { ThinkingBlock } from './ThinkingBlock' import { ToolCallBlock } from './ToolCallBlock' import { PermissionDialog } from './PermissionDialog' @@ -172,6 +172,24 @@ describe('chat blocks', () => { expect(container.textContent).toContain('fatal: unrecognized argument: --no-stat') }) + it('shows a copyable plain-text window for workspace permission denials', async () => { + const { container } = render( + , + ) + + expect(screen.getByRole('status').textContent).toContain('Blocked by workspace permission rule') + await waitFor(() => expect(screen.getByLabelText('text').textContent).toContain('Remove-Item -Force .\\guardrail-hard-deny.txt')) + expect(container.textContent).toContain('.claude/settings.local.json') + expect(screen.getByRole('status').querySelector('[data-slot="code-block"]')).not.toBeNull() + }) + it('expands tool errors so full Computer Use gate messages are readable', () => { const { container } = render( { 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 2b54b7c3..2fecab9f 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/controls/PermissionModeSelector.test.tsx b/desktop/src/components/controls/PermissionModeSelector.test.tsx index 2808b3e0..a7690f10 100644 --- a/desktop/src/components/controls/PermissionModeSelector.test.tsx +++ b/desktop/src/components/controls/PermissionModeSelector.test.tsx @@ -28,12 +28,14 @@ vi.mock('../../i18n', () => ({ 'permMode.askPermDesc': 'Ask before changing files or running commands', 'permMode.autoAcceptDesc': 'Automatically accept edit operations', 'permMode.planModeDesc': 'Plan before executing', + 'permMode.autoDesc': 'Use a model for unresolved confirmations', 'permMode.bypassDesc': 'Run without permission prompts', 'permMode.executionPermissions': 'Execution Permissions', 'permMode.label.default': 'Manual', 'permMode.defaultTag': 'default', 'permMode.label.acceptEdits': 'Accept edits', 'permMode.label.plan': 'Plan', + 'permMode.label.auto': 'Auto', 'permMode.label.bypassPermissions': 'Bypass permissions', 'permMode.label.dontAsk': "Don't ask", 'permMode.enableBypassTitle': 'Enable bypass mode', @@ -179,6 +181,7 @@ describe('PermissionModeSelector', () => { expect.stringContaining('Manual'), expect.stringContaining('Accept edits'), expect.stringContaining('Plan'), + expect.stringContaining('Auto'), expect.stringContaining('Bypass permissions'), ]) expect(screen.getByRole('menuitemradio', { name: /Plan/ })).toHaveAttribute('aria-checked', 'true') diff --git a/desktop/src/components/controls/PermissionModeSelector.tsx b/desktop/src/components/controls/PermissionModeSelector.tsx index dc3d5502..43d9aaf0 100644 --- a/desktop/src/components/controls/PermissionModeSelector.tsx +++ b/desktop/src/components/controls/PermissionModeSelector.tsx @@ -11,7 +11,7 @@ import { isDesktopRuntime } from '../../lib/desktopRuntime' import { MobileBottomSheet } from '../shared/MobileBottomSheet' import { ConfirmDialog } from '../shared/ConfirmDialog' import { Tooltip } from '../shared/Tooltip' -import { Check, ChevronDown, FilePenLine, Folder, NotebookPen, ShieldCheck, TriangleAlert, Zap, type LucideIcon } from 'lucide-react' +import { Bot, Check, ChevronDown, FilePenLine, Folder, NotebookPen, ShieldCheck, TriangleAlert, Zap, type LucideIcon } from 'lucide-react' import { useMenuAction } from '../../hooks/useMenuAction' import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from '../shared/ui/dropdown-menu' import { Button } from '../shared/ui/button' @@ -21,6 +21,7 @@ const MODE_ICONS: Record = { default: ShieldCheck, acceptEdits: FilePenLine, plan: NotebookPen, + auto: Bot, bypassPermissions: Zap, dontAsk: Zap, } @@ -67,6 +68,11 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false, label: t('permMode.label.plan'), description: t('permMode.planModeDesc'), }, + { + value: 'auto', + label: t('permMode.label.auto'), + description: t('permMode.autoDesc'), + }, { value: 'bypassPermissions', label: t('permMode.label.bypassPermissions'), @@ -79,6 +85,7 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false, default: t('permMode.label.default'), acceptEdits: t('permMode.label.acceptEdits'), plan: t('permMode.label.plan'), + auto: t('permMode.label.auto'), bypassPermissions: t('permMode.label.bypassPermissions'), dontAsk: t('permMode.label.dontAsk'), } diff --git a/desktop/src/components/settings/DecisionProviderFormModal.test.tsx b/desktop/src/components/settings/DecisionProviderFormModal.test.tsx new file mode 100644 index 00000000..2e032107 --- /dev/null +++ b/desktop/src/components/settings/DecisionProviderFormModal.test.tsx @@ -0,0 +1,84 @@ +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('keeps a manually configured model even after discovery does not return it', async () => { + useDecisionProviderStore.setState({ + discoverModels: vi.fn(async () => [{ id: 'gpt-6-luna' }]), + }) + render( + , + ) + + expect(screen.getByText('no-auto-decision-model')).toBeInTheDocument() + expect(screen.queryByText('gpt-6-luna')).not.toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'Fetch models' })) + + await waitFor(() => expect(screen.getByRole('button', { name: 'Fetch models' })).toBeEnabled()) + expect(screen.getByText('no-auto-decision-model')).toBeInTheDocument() + expect(screen.queryByText('gpt-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..faeea831 --- /dev/null +++ b/desktop/src/components/settings/DecisionProviderFormModal.tsx @@ -0,0 +1,253 @@ +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 ?? [] + return configured.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) => current.map((model) => ( + discovered.find((candidate) => candidate.id === model.id) ?? model + ))) + 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..be0eb68b --- /dev/null +++ b/desktop/src/components/settings/DecisionProviderSettingsSection.tsx @@ -0,0 +1,131 @@ +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, + presets, + isLoading, + fetchProviders, + fetchPresets, + 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 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 ?? '' })} +

+ )} +
+
+ + + +
+
+ ) + })} +
+ )} + + {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..94ba5770 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,24 @@ 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' }], + }, + ], + 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 +67,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('Decision model 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 aeb98073..e0c63b69 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -486,7 +486,7 @@ export const en = { 'settings.providers.displayNameOptional': 'Optional', 'settings.providers.fastModel': 'Fast model', 'settings.providers.fastModelDefault': 'Use selected model', - 'settings.providers.fastModelDesc': 'Used for quick background work such as title generation.', + 'settings.providers.fastModelDesc': 'Used for quick background work such as title generation and Auto permission decisions.', 'settings.providers.duplicateDisplayName': 'Another model uses this display name.', 'settings.providers.defaultModel': 'Default model', 'settings.providers.moveModelUp': 'Move model up', @@ -519,8 +519,41 @@ 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': 'Decision model providers', + 'settings.decisionProviders.description': 'Manage dedicated models that Auto mode can use for unresolved permission decisions. Connection secrets stay in provider configuration.', + 'settings.decisionProviders.add': 'Add decision provider', + 'settings.decisionProviders.empty': 'No decision model providers configured.', + 'settings.decisionProviders.addTitle': 'Add decision model provider', + 'settings.decisionProviders.editTitle': 'Edit decision model 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 decision model 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.autoPermission.title': 'Auto permission model', + 'settings.autoPermission.description': 'Auto mode applies deterministic rules first, then asks this model only when a tool call would otherwise require confirmation.', + 'settings.autoPermission.provider': 'Decision source', + 'settings.autoPermission.fastModel': 'Active provider fast model', + 'settings.autoPermission.model': 'Model', + 'settings.autoPermission.fastModelReady': 'Auto will use {model} from {provider}.', + 'settings.autoPermission.fastModelMissing': 'No active model provider is available. Choose a decision provider or activate a model provider before saving.', + 'settings.autoPermission.policyNote': 'Model policy and thresholds are fixed by DreamCoder. Errors, timeouts, unsupported models, and invalid responses always fall back to confirmation.', + 'settings.autoPermission.privacyNotice': 'I understand that the user request, recent tool context, tool input, and working directory may be sent to this 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', @@ -1361,6 +1394,14 @@ 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', + 'chat.permissionGuardrail.confirmationApproved': 'Approved after Auto confirmation', + 'chat.permissionGuardrail.decision': 'Decision: {decision}', + 'chat.permissionGuardrail.score': 'Score: {score}', + 'chat.permissionGuardrail.scoreUnavailable': 'Score: unavailable', + 'chat.permissionGuardrail.decisionProvider': 'Decision provider', + 'chat.permissionGuardrail.fastModel': 'Fast model', + 'chat.permissionGuardrail.policyVersion': 'Policy v{version}', 'chat.activeGoal.title': 'Active goal', 'chat.activeGoal.running': 'Loop running', 'chat.activeGoal.active': 'Active', @@ -1563,6 +1604,8 @@ export const en = { 'permission.hideDetails': 'Hide details', 'permission.showFullInput': 'Show full input', 'permission.replacingContent': 'Replacing content in file', + 'permission.ruleDeniedTitle': 'Blocked by workspace permission rule', + 'permission.ruleDeniedDescription': 'This input was blocked by the project permission configuration. Review .claude/settings.local.json if you want DreamCoder to run it.', 'question.needsInput': 'Claude needs your input', @@ -1747,6 +1790,7 @@ export const en = { 'permMode.executionPermissions': 'Execution Permissions', 'permMode.askPermDesc': 'Confirm file edits and higher-risk commands when CLI asks', + 'permMode.autoDesc': 'Use hard rules first, then a model for unresolved confirmations', 'permMode.autoAcceptDesc': 'Claude writes to disk without asking', 'permMode.planModeDesc': 'Architecture & reasoning only, no files', 'permMode.bypassDesc': 'Full tool access for shell and file system', @@ -1762,6 +1806,7 @@ export const en = { 'permMode.defaultTag': 'default', 'permMode.label.acceptEdits': 'Accept edits', 'permMode.label.plan': 'Plan', + 'permMode.label.auto': 'Auto', 'permMode.label.bypassPermissions': 'Bypass permissions', 'permMode.label.dontAsk': "Don't ask", diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 2f51587a..f6abbc78 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -488,7 +488,7 @@ export const zh: Record = { 'settings.providers.displayNameOptional': '可选', 'settings.providers.fastModel': '快速模型', 'settings.providers.fastModelDefault': '使用当前所选模型', - 'settings.providers.fastModelDesc': '用于标题生成等快速后台任务。', + 'settings.providers.fastModelDesc': '用于标题生成和 Auto 权限判定等快速后台任务。', 'settings.providers.duplicateDisplayName': '另一个模型已使用此显示名称。', 'settings.providers.defaultModel': '默认模型', 'settings.providers.moveModelUp': '上移模型', @@ -521,8 +521,41 @@ export const zh: Record = { 'settings.providers.authStrategyDualDummy': '两个变量写 dummy', 'settings.providers.authStrategyDualDummyDesc': 'vLLM 等本地兼容服务,只需要占位认证值。', + 'settings.decisionProviders.title': '决策模型服务商', + 'settings.decisionProviders.description': '管理 Auto 模式处理未决权限判定时可使用的独立模型。连接密钥保存在服务商配置中。', + 'settings.decisionProviders.add': '添加决策服务商', + 'settings.decisionProviders.empty': '尚未配置决策模型服务商。', + 'settings.decisionProviders.addTitle': '添加决策模型服务商', + 'settings.decisionProviders.editTitle': '编辑决策模型服务商', + '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': '删除决策模型服务商“{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.autoPermission.title': 'Auto 权限模型', + 'settings.autoPermission.description': 'Auto 模式会先应用确定性规则,只在工具调用仍需确认时才请求该模型。', + 'settings.autoPermission.provider': '决策来源', + 'settings.autoPermission.fastModel': '当前服务商的快速模型', + 'settings.autoPermission.model': '模型', + 'settings.autoPermission.fastModelReady': 'Auto 将使用 {provider} 的 {model}。', + 'settings.autoPermission.fastModelMissing': '当前没有可用的模型服务商。请先选择决策服务商,或激活一个模型服务商后再保存。', + 'settings.autoPermission.policyNote': '模型策略和阈值由 DreamCoder 固定。错误、超时、不受支持的模型和无法解析的响应都会回退为用户确认。', + 'settings.autoPermission.privacyNotice': '我理解用户请求、近期工具上下文、工具输入和工作目录可能会发送给该服务商,用于权限判定。', 'settings.browserTrust.title': '已信任的网站', 'settings.browserTrust.description': 'Agent 在侧边浏览器里操作这些网站时不再询问。只接受精确的 https 来源。读取页面不需要授权。', 'settings.browserTrust.inputLabel': '要信任的网站', @@ -1363,6 +1396,14 @@ export const zh: Record = { 'chat.compactSummary.trigger.auto': '自动', 'chat.compactSummary.tokens': '压缩前 {count} tokens', 'chat.compactSummary.messages': '已摘要 {count} 条消息', + 'chat.permissionGuardrail.autoApproved': 'Auto 已自动批准', + 'chat.permissionGuardrail.confirmationApproved': 'Auto 请求确认后已批准', + 'chat.permissionGuardrail.decision': '判定:{decision}', + 'chat.permissionGuardrail.score': '分数:{score}', + 'chat.permissionGuardrail.scoreUnavailable': '分数:不可用', + 'chat.permissionGuardrail.decisionProvider': '决策服务商', + 'chat.permissionGuardrail.fastModel': '快速模型', + 'chat.permissionGuardrail.policyVersion': '策略 v{version}', 'chat.activeGoal.title': '当前目标', 'chat.activeGoal.running': '自循环运行中', 'chat.activeGoal.active': '进行中', @@ -1565,6 +1606,8 @@ export const zh: Record = { 'permission.hideDetails': '隐藏详情', 'permission.showFullInput': '显示完整输入', 'permission.replacingContent': '替换文件内容', + 'permission.ruleDeniedTitle': '已被工作区权限规则阻止', + 'permission.ruleDeniedDescription': '该输入被项目权限配置阻止。如果希望 DreamCoder 执行,请检查 .claude/settings.local.json。', 'question.needsInput': 'Claude 需要你的输入', @@ -1749,6 +1792,7 @@ export const zh: Record = { 'permMode.executionPermissions': '执行权限', 'permMode.askPermDesc': 'CLI 请求时确认文件编辑和高风险命令', + 'permMode.autoDesc': '先执行硬规则,再用模型处理未决确认', 'permMode.autoAcceptDesc': 'Claude 无需询问即可写入磁盘', 'permMode.planModeDesc': '仅架构和推理,不操作文件', 'permMode.bypassDesc': '对 Shell 和文件系统的完整工具访问', @@ -1764,6 +1808,7 @@ export const zh: Record = { 'permMode.defaultTag': '默认', 'permMode.label.acceptEdits': 'Accept edits', 'permMode.label.plan': 'Plan', + 'permMode.label.auto': 'Auto', 'permMode.label.bypassPermissions': 'Bypass permissions', 'permMode.label.dontAsk': "Don't ask", diff --git a/desktop/src/pages/Settings.browserTrust.test.tsx b/desktop/src/pages/Settings.browserTrust.test.tsx index c94fb707..5aeaa67d 100644 --- a/desktop/src/pages/Settings.browserTrust.test.tsx +++ b/desktop/src/pages/Settings.browserTrust.test.tsx @@ -2,23 +2,51 @@ 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' +import { useProviderStore } from '../stores/providerStore' 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', + permissionMode: 'default', + autoPermission: { + providerId: null, + modelId: '', + privacyNoticeVersion: 0, + }, + }) + useDecisionProviderStore.setState({ + providers: [], + fetchProviders: vi.fn(async () => {}), + }) + useProviderStore.setState({ + providers: [{ + id: 'provider-1', + presetId: 'openai', + name: 'OpenAI', + apiKey: '', + baseUrl: 'https://api.openai.com/v1', + apiFormat: 'openai_responses', + runtimeKind: 'anthropic_compatible', + models: [{ id: 'gpt-5' }, { id: 'gpt-5-mini' }], + fastModel: 'gpt-5-mini', + }], + activeId: 'provider-1', + fetchProviders: vi.fn(async () => {}), + }) useUIStore.setState({ settingsTab: 'permissions' }) }) @@ -27,18 +55,63 @@ describe('Settings permissions tab', () => { setDesktop(false) useSettingsStore.setState(useSettingsStore.getInitialState(), true) useUIStore.setState(useUIStore.getInitialState(), true) + useDecisionProviderStore.setState(useDecisionProviderStore.getInitialState(), true) + useProviderStore.setState(useProviderStore.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 Auto configuration only when Auto mode is selected', () => { + render() + + expect(screen.queryByRole('heading', { name: 'Auto permission model' })).not.toBeInTheDocument() + + cleanup() + useSettingsStore.setState({ permissionMode: 'auto' }) + render() + + expect(screen.getByRole('heading', { name: 'Auto permission model' })).toBeInTheDocument() + expect(screen.getByText('Auto will use gpt-5-mini from OpenAI.')).toBeInTheDocument() + expect(screen.getByText('Active provider fast model')).toBeInTheDocument() + }) + + it('uses the selected model when the active provider has no separate fast model', () => { + useSettingsStore.setState({ + permissionMode: 'auto', + autoPermission: { + providerId: null, + modelId: '', + privacyNoticeVersion: 1, + }, + }) + useProviderStore.setState((state) => ({ + providers: state.providers.map((provider) => ({ ...provider, fastModel: undefined })), + })) + + render() + + expect(screen.getByText('Auto will use gpt-5 from OpenAI.')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled() + }) + + 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..8101b73d 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -43,6 +43,11 @@ 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 { useProviderStore } from '../stores/providerStore' +import type { AutoPermissionSettings as AutoPermissionSettingsValue } from '../types/settings' +import { FormSelect } from '../components/shared/FormSelect' +import { Checkbox } from '../components/shared/ui/checkbox' type SettingsNavGroup = { titleKey: TranslationKey | null @@ -193,6 +198,7 @@ const PERMISSION_MODES: Array<{ mode: DefaultPermissionMode; Icon: LucideIcon; d { mode: 'default', Icon: ShieldCheck, descKey: 'permMode.askPermDesc' }, { mode: 'acceptEdits', Icon: FilePenLine, descKey: 'permMode.autoAcceptDesc' }, { mode: 'plan', Icon: NotebookPen, descKey: 'permMode.planModeDesc' }, + { mode: 'auto', Icon: Bot, descKey: 'permMode.autoDesc' }, { mode: 'bypassPermissions', Icon: Zap, descKey: 'permMode.bypassDesc' }, ] @@ -259,11 +265,161 @@ 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')} +

+
+ + {permissionMode === 'auto' ? : null} + {isDesktopRuntime() ? : null} ) } +function AutoPermissionSettings() { + const t = useTranslation() + const saved = useSettingsStore((state) => state.autoPermission) + const save = useSettingsStore((state) => state.setAutoPermission) + const decisionProviders = useDecisionProviderStore((state) => state.providers) + const fetchDecisionProviders = useDecisionProviderStore((state) => state.fetchProviders) + const providers = useProviderStore((state) => state.providers) + const activeProviderId = useProviderStore((state) => state.activeId) + const fetchProviders = useProviderStore((state) => state.fetchProviders) + const [draft, setDraft] = useState(saved) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + void fetchDecisionProviders() + void fetchProviders() + }, [fetchDecisionProviders, fetchProviders]) + + useEffect(() => { + setDraft(saved) + }, [saved]) + + const provider = decisionProviders.find((entry) => entry.id === draft.providerId) ?? null + const activeProvider = providers.find((entry) => entry.id === activeProviderId) ?? null + const fastModel = activeProvider?.fastModel ?? activeProvider?.models[0]?.id ?? null + const privacyAccepted = draft.privacyNoticeVersion >= 1 + const usesFastModel = draft.providerId === null + const requiresPrivacy = usesFastModel || provider?.deployment === 'hosted' + const saveDisabled = saving || ( + usesFastModel + ? !activeProvider || !fastModel || !privacyAccepted + : !provider || !draft.modelId || (requiresPrivacy && !privacyAccepted) + ) + + const update = ( + key: K, + value: AutoPermissionSettingsValue[K], + ) => setDraft((current) => ({ ...current, [key]: value })) + + return ( +
+
+

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

+

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

+
+ +
+
+ + { + const next = decisionProviders.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.autoPermission.provider')} + options={[ + { value: '', label: t('settings.autoPermission.fastModel') }, + ...decisionProviders.map((entry) => ({ value: entry.id, label: entry.name })), + ]} + /> +
+ {provider ? ( +
+ + update('modelId', modelId)} + label={t('settings.autoPermission.model')} + options={provider.models.map((model) => ({ value: model.id, label: model.name ?? model.id }))} + /> +
+ ) : null} +
+ + {usesFastModel ? ( +

+ {activeProvider && fastModel + ? t('settings.autoPermission.fastModelReady', { provider: activeProvider.name, model: fastModel }) + : t('settings.autoPermission.fastModelMissing')} +

+ ) : null} + +

+ {t('settings.autoPermission.policyNote')} +

+ + {requiresPrivacy && ( + + )} + +
+ + {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 451f0642..c3ca0300 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -3000,6 +3000,15 @@ describe('chatStore permission requests shared across clients', () => { toolName: 'Bash', toolUseId: 'tool-shared-1', input: { command: 'ls' }, + alwaysAsks: true as const, + guardrail: { + providerId: 'provider-1', + modelId: 'gpt-6-luna', + source: 'decisionProvider' as const, + decision: 'ask' as const, + score: 0.812, + policyVersion: 1, + }, } beforeEach(() => { @@ -3024,9 +3033,39 @@ 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, + alwaysAsks: true, + }) expect(session().chatState).toBe('permission_pending') }) + it('shows concurrent permission requests one at a time', () => { + const queued = { + ...request, + requestId: 'perm-shared-2', + toolUseId: 'tool-shared-2', + input: { command: 'pwd' }, + } + + sendPatch(2, { + pendingPermissions: [request, queued], + turn: { phase: 'awaiting_permission', startedAt: 1, activity: 'tool' }, + }) + + expect(session().messages.filter((message) => message.type === 'permission_request')).toHaveLength(1) + expect(session().pendingPermission?.requestId).toBe('perm-shared-1') + + sendPatch(3, { + pendingPermissions: [queued], + turn: { phase: 'awaiting_permission', startedAt: 1, activity: 'tool' }, + }) + + expect(session().messages.filter((message) => message.type === 'permission_request')).toHaveLength(2) + expect(session().pendingPermission?.requestId).toBe('perm-shared-2') + }) + it('closes the dialog and records who answered when another client replies', () => { openRequest() useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 01f63f0d..e3cabc17 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -28,6 +28,7 @@ import type { UIAttachment, UIMessage, PermissionResolution, + PermissionGuardrailDetails, ServerMessage, SessionState, TokenUsage, @@ -69,6 +70,7 @@ export type PerSessionState = { toolUseId?: string input: unknown description?: string + guardrail?: PermissionGuardrailDetails } | null tokenUsage: TokenUsage statusVerb: string @@ -1786,6 +1788,7 @@ function pendingPermissionFromMirror(state: SessionState): PerSessionState['pend toolUseId: request.toolUseId, input: request.input, description: request.description, + guardrail: request.guardrail, } } @@ -1806,7 +1809,11 @@ function applyMirror(sessionId: string, next: SessionState): void { const previous = session.mirror ?? null const previousRequests = new Set(previous?.pendingPermissions.map((request) => request.requestId) ?? []) const nextRequests = new Set(next.pendingPermissions.map((request) => request.requestId)) - const openedRequests = next.pendingPermissions.filter((request) => !previousRequests.has(request.requestId)) + const previousActiveRequestId = previous?.pendingPermissions[0]?.requestId + const nextActiveRequest = next.pendingPermissions[0] + const activatedRequest = nextActiveRequest?.requestId !== previousActiveRequestId + ? nextActiveRequest + : undefined const closedRequestIds = [...previousRequests].filter((requestId) => !nextRequests.has(requestId)) const wasRunning = previous ? previous.turn.phase !== 'idle' : session.chatState !== 'idle' const isRunning = next.turn.phase !== 'idle' @@ -1818,17 +1825,21 @@ function applyMirror(sessionId: string, next: SessionState): void { let messages = dropQueuedBubbles(current.messages, next.queuedMessages ?? []) let streamingText = current.streamingText - for (const request of openedRequests) { - if (isInlineQuestionTool(request.toolName) || hasPermissionRequestMessage(messages, request.requestId)) continue + if ( + activatedRequest && + !isInlineQuestionTool(activatedRequest.toolName) && + !hasPermissionRequestMessage(messages, activatedRequest.requestId) + ) { messages = appendMessage(messages, { id: nextId(), type: 'permission_request', - requestId: request.requestId, - toolName: request.toolName, - toolUseId: request.toolUseId, - input: request.input, - description: request.description, - ...(request.alwaysAsks ? { alwaysAsks: true as const } : {}), + requestId: activatedRequest.requestId, + toolName: activatedRequest.toolName, + toolUseId: activatedRequest.toolUseId, + input: activatedRequest.input, + description: activatedRequest.description, + guardrail: activatedRequest.guardrail, + ...(activatedRequest.alwaysAsks ? { alwaysAsks: true as const } : {}), timestamp: now, }) } @@ -1875,13 +1886,13 @@ function applyMirror(sessionId: string, next: SessionState): void { } }) - for (const request of openedRequests) { + if (activatedRequest) { notifyDesktop({ - dedupeKey: `permission:${request.requestId}`, + dedupeKey: `permission:${activatedRequest.requestId}`, cooldownScope: 'permission-prompt', requestAttention: true, title: t('notifications.permissionPrompt.title'), - body: t('notifications.permissionPrompt.toolBody', { toolName: request.toolName || 'A tool' }), + body: t('notifications.permissionPrompt.toolBody', { toolName: activatedRequest.toolName || 'A tool' }), target: { type: 'session', sessionId }, }) } diff --git a/desktop/src/stores/decisionProviderStore.ts b/desktop/src/stores/decisionProviderStore.ts new file mode 100644 index 00000000..0ae35ac1 --- /dev/null +++ b/desktop/src/stores/decisionProviderStore.ts @@ -0,0 +1,88 @@ +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[] + 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 + testProvider: (id: string, modelId?: string) => Promise + testConfig: (input: TestDecisionProviderInput) => Promise + discoverModels: (input: DiscoverDecisionProviderModelsInput) => Promise +} + +export const useDecisionProviderStore = create((set, get) => ({ + providers: [], + presets: [], + isLoading: false, + error: null, + + fetchProviders: async () => { + set({ isLoading: true, error: null }) + try { + const { providers } = await providersApi.listDecisions() + set({ providers, 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() + }, + + 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..b3292c4a 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 AutoPermissionSettings, type DefaultPermissionMode, type EffortLevel, type ModelInfo, @@ -74,6 +75,7 @@ function getStoredOnboardingCompleted(): boolean { type SettingsStore = { permissionMode: DefaultPermissionMode + autoPermission: AutoPermissionSettings currentModel: ModelInfo | null effortLevel: EffortLevel thinkingEnabled: boolean @@ -105,6 +107,7 @@ type SettingsStore = { fetchAll: () => Promise fetchH5Access: () => Promise setPermissionMode: (mode: DefaultPermissionMode) => Promise + setAutoPermission: (settings: AutoPermissionSettings) => Promise setModel: (modelId: string) => Promise setEffort: (level: EffortLevel) => Promise setThinkingEnabled: (enabled: boolean) => Promise @@ -163,8 +166,29 @@ const DEFAULT_NETWORK_SETTINGS: NetworkSettings = { }, } +export const DEFAULT_AUTO_PERMISSION_SETTINGS: AutoPermissionSettings = { + providerId: null, + modelId: '', + privacyNoticeVersion: 0, +} + +function normalizeAutoPermissionSettings(value: unknown): AutoPermissionSettings { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return DEFAULT_AUTO_PERMISSION_SETTINGS + } + const settings = value as Partial + return { + providerId: typeof settings.providerId === 'string' ? settings.providerId : null, + modelId: typeof settings.modelId === 'string' ? settings.modelId : '', + privacyNoticeVersion: typeof settings.privacyNoticeVersion === 'number' + ? Math.max(0, Math.floor(settings.privacyNoticeVersion)) + : 0, + } +} + export const useSettingsStore = create((set, get) => ({ permissionMode: 'default', + autoPermission: DEFAULT_AUTO_PERMISSION_SETTINGS, currentModel: null, effortLevel: 'medium', thinkingEnabled: true, @@ -225,6 +249,7 @@ export const useSettingsStore = create((set, get) => ({ useUIStore.getState().setTheme(theme) set({ permissionMode: mode, + autoPermission: normalizeAutoPermissionSettings(userSettings.autoPermission), availableModels: modelsRes.models, activeProviderName: modelsRes.provider?.name ?? null, currentModel: model, @@ -274,6 +299,17 @@ export const useSettingsStore = create((set, get) => ({ } }, + setAutoPermission: async (settings) => { + const previous = get().autoPermission + set({ autoPermission: settings }) + try { + await settingsApi.updateUser({ autoPermission: settings }) + } catch (error) { + set({ autoPermission: 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 8077a527..54ce5c3b 100644 --- a/desktop/src/types/chat.ts +++ b/desktop/src/types/chat.ts @@ -1,4 +1,10 @@ -import type { TaskNotification, TaskUsage, TranscriptItem, UserMessageOrigin } from '@protocol' +import type { + PermissionGuardrailDetails, + TaskNotification, + TaskUsage, + TranscriptItem, + UserMessageOrigin, +} from '@protocol' export type { AttachmentRef, @@ -16,6 +22,7 @@ export type { TokenUsage, UserMessageOrigin, } from '@protocol' +export type { PermissionGuardrailDetails } import type { ClientKind, GoalEventAction } from '@protocol' export type SessionOrigin = Extract @@ -111,6 +118,7 @@ export type UIMessage = | SharedItem<'compact_summary'> | SharedItem<'goal_event'> | SharedItem<'memory_event'> + | SharedItem<'permission_guardrail_decision'> | { id: string type: 'permission_request' @@ -119,6 +127,7 @@ export type UIMessage = toolUseId?: string input: unknown description?: string + guardrail?: PermissionGuardrailDetails alwaysAsks?: true 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..80480e2b 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 + autoPermission?: AutoPermissionSettings theme?: ThemeMode skipWebFetchPreflight?: boolean desktopNotificationsEnabled?: boolean @@ -108,6 +109,12 @@ export type UserSettings = { [key: string]: unknown } +export type AutoPermissionSettings = { + providerId: string | null + modelId: string + privacyNoticeVersion: number +} + export type AppMode = 'default' | 'portable' export type AppModeConfig = { diff --git a/docs/README.md b/docs/README.md index 4d029bb9..e138df01 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ - [computer-use](features/computer-use/tech-design.md): Computer Use through cua-driver, with its [decision records](features/computer-use/adr.md). - [kanban](features/kanban/README.md): a persistent project board shared by people and agent sessions (first version implemented on `feat/kanban`). - [performance](features/performance/optimization-report.md): performance audit report. + - [permission-guardrail](features/permission-guardrail/tech-design.md): model-assisted Auto permission mode behind deterministic permission rules. - [session-links](features/session-links/tech-design.md): sessions that start other sessions and message each other. - [session-state](features/session-state/tech-design.md): session state owned by the server. - [ui-refresh](features/ui-refresh/README.md): the UI refresh design log, mockups and checks. diff --git a/docs/features/permission-guardrail/tech-design.md b/docs/features/permission-guardrail/tech-design.md new file mode 100644 index 00000000..d032aeb4 --- /dev/null +++ b/docs/features/permission-guardrail/tech-design.md @@ -0,0 +1,128 @@ +# Permission guardrail technical design + +## Purpose + +Auto permission mode reduces confirmation prompts without weakening deterministic permission rules. Rules run first. Calls that rules do not allow or deny are evaluated by a configured model, and only calls that the model does not allow are shown to the user. + +The feature is opt-in because the evaluation can send the user request, recent tool context, tool input, and working directory to a hosted provider. + +## Scope + +The first release provides: + +- an `auto` permission mode in the CLI, server protocol, desktop selector, and persisted settings; +- separate decision-provider records under Providers; +- a fallback to the active model provider when no decision provider is selected; +- fail-safe model evaluation after deterministic rules; +- persistent transcript markers for model-assisted decisions; +- desktop settings and permission cards in English and Chinese. + +It does not provide a structured editor for user or project permission rules. Those rules remain in the existing settings files, including project-local `.claude/settings.local.json`. + +## Permission pipeline + +Auto follows this order: + +1. Load the effective user, project, command-line, and session permission rules. +2. Run tool-level and content-specific permission checks. +3. Return any deterministic deny without calling a model. +4. Return any deterministic allow without calling a model. +5. Evaluate the unresolved confirmation with the configured decision source. +6. Allow when a supported policy returns allow; otherwise ask the user. + +Bypass-immune checks remain before model evaluation. The model can return only allow or ask. It cannot deny a call, override a deny rule, or bypass a bypass-immune check. + +Manual, Accept edits, Plan, and Bypass permissions do not call the decision model. Bypass retains its existing meaning of skipping ordinary prompts while still respecting deterministic deny and bypass-immune checks. + +## Configuration ownership + +The sidecar owns provider configuration and Auto settings. + +Decision providers are stored separately from normal model providers in the provider index: + +```ts +type ProvidersIndex = { + activeId: string | null + providers: SavedProvider[] + decisionProviders: SavedDecisionProvider[] +} +``` + +Each decision provider owns its endpoint, API format, deployment classification, credential, and model list. Auto settings reference a decision provider and model, or use `providerId: null` for the active model-provider fallback: + +```ts +type AutoPermissionSettings = { + providerId: string | null + modelId: string + privacyNoticeVersion: number +} +``` + +There is no global active decision provider. The selected source belongs to Auto settings. API keys follow the existing provider persistence path and are not added to session state, WebSocket messages, logs, or transcripts. + +## Decision sources + +### Decision provider + +A decision provider may use OpenAI Decisions, OpenRouter Decisions, OpenAI-compatible chat, or Ollama chat transport. Automatic allow is enabled only when the provider-format and model pair has a policy defined in code. Unsupported pairs return ask. + +The validated policy is `openai_decisions:gpt-6-luna`, with ask threshold `0.5` and policy version `1`. Its predicate estimates whether the user should be asked before the tool call runs. + +Jev, d1-ultra, Claude models, and chat-based local models have not been validated end to end in DreamCoder. Their thresholds must be selected independently on SABER dev data and evaluated once on the test split. They do not inherit the GPT-6 Luna threshold. + +### Active provider fallback + +When Auto has no decision provider, the sidecar uses the active model provider. It selects the provider's explicit `fastModel` when present and otherwise its selected model. The model receives a fixed policy and a forced structured response with `behavior: allow | ask` and an optional reason. + +The fallback does not invent a probability score. Its transcript marker stores `score: null`. A missing active provider, missing model, invalid structured response, request error, or timeout returns ask. + +## Privacy and endpoint classification + +Hosted evaluation requires acceptance of the current privacy notice. Only verified loopback endpoints are treated as local/private. A custom LAN or public URL remains external even when its provider is labeled custom. + +The policy and response options are fixed in code. Tool output is attacker-controlled context and cannot change the deterministic rule result or the fail-safe behavior. + +## Session state and protocol + +The CLI includes model details in the structured permission request only when Auto returns ask. The server validates those details before adding them to `PendingPermission.guardrail`. The existing `alwaysAsks` flag remains independent and may appear on the same request. + +`pendingPermissions` is ordered. The desktop renders only the first request as active, then activates the next request after the first closes. This prevents concurrent tool execution from presenting several actionable cards at once while preserving every server-owned request. + +The model decision marker is emitted as a transcript attachment and translated into a live transcript item containing: + +- provider and model identifiers; +- decision source; +- allow or ask decision; +- score when the transport supplies one; +- policy version; +- optional reason and tool-use identifier. + +Permission markers are loggable for external users and are reconstructed by transcript history after restart. They coexist with queued-command attachments introduced by session links. Messages sent during an active turn continue to use the server-owned queue and do not bypass or replace pending permission state. + +## Desktop behavior + +Providers contains a separate Permission guardrail providers section with connection testing and model discovery. Fetching models refreshes suggestions without overwriting a manually entered model identifier. + +Permissions lists Auto as a fifth mode. Selecting it shows the decision source, model, privacy notice, fixed-policy explanation, and a link to provider configuration. The permission card shows validated guardrail details when the model returns ask. Transcript markers show model-assisted allow and ask outcomes after history reload. + +The desktop `/permissions` command opens Permission settings. Rule help names the project-local settings file and explains that shell rules are tool-specific, including separate `Bash(...)` and `PowerShell(...)` forms. + +## Failure behavior + +The following conditions return ask: + +- missing provider or model; +- missing hosted-provider credentials; +- privacy notice not accepted; +- unsupported provider-format and model policy; +- timeout, network error, or non-success response; +- invalid probability or structured response; +- input serialization failure. + +No failure path becomes allow. + +## Verification + +Automated coverage includes threshold boundaries, errors and malformed results, deterministic Bash and PowerShell denies, bypass-immune checks, mode isolation, provider fallback, structured protocol translation, concurrent request activation, transcript persistence, settings validation, and English and Chinese desktop settings. + +Manual acceptance uses a new session after permission-rule edits and verifies the actual shell tool name displayed by the tool card. OpenAI Decisions with `gpt-6-luna` is the only provider/model combination currently validated end to end. 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 9dcb1861..295ac16c 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -886,6 +886,24 @@ 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, + source: message.attachment.source, + score: message.attachment.score, + policy_version: message.attachment.policyVersion, + reason: message.attachment.reason, + 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..6985177a --- /dev/null +++ b/src/cli/structuredIO.permissionGuardrail.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from 'bun:test' +import type { PermissionDecision } from '../utils/permissions/PermissionResult.js' +import { + preservePermissionGuardrailReason, + serializePermissionGuardrail, +} from './structuredIO.js' + +describe('structured Auto permission response', () => { + test('preserves the model decision after desktop approval', () => { + const source: PermissionDecision = { + behavior: 'ask', + message: 'Confirm', + decisionReason: { + type: 'autoPermission', + providerId: 'provider', + modelId: 'model', + source: 'decisionProvider', + decision: 'ask', + score: 0.98, + policyVersion: 1, + reason: 'Risky write', + }, + } + 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 Auto details for the permission request', () => { + expect(serializePermissionGuardrail({ + type: 'autoPermission', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + source: 'decisionProvider', + decision: 'ask', + score: 0.812, + policyVersion: 1, + reason: 'Sensitive command', + })).toEqual({ + provider_id: 'provider-1', + model_id: 'gpt-6-luna', + source: 'decisionProvider', + decision: 'ask', + score: 0.812, + policy_version: 1, + reason: 'Sensitive command', + }) + }) +}) diff --git a/src/cli/structuredIO.ts b/src/cli/structuredIO.ts index b8b75282..92e4a4ca 100644 --- a/src/cli/structuredIO.ts +++ b/src/cli/structuredIO.ts @@ -80,6 +80,7 @@ function serializeDecisionReason( case 'mode': case 'subcommandResults': case 'permissionPromptTool': + case 'autoPermission': return undefined case 'hook': case 'asyncAgent': @@ -91,6 +92,30 @@ function serializeDecisionReason( } } +export function preservePermissionGuardrailReason( + source: PermissionDecision, + response: PermissionDecision, +): PermissionDecision { + return source.decisionReason?.type === 'autoPermission' + ? { ...response, decisionReason: source.decisionReason } + : response +} + +export function serializePermissionGuardrail( + reason: PermissionDecisionReason | undefined, +) { + if (reason?.type !== 'autoPermission') return undefined + return { + provider_id: reason.providerId, + model_id: reason.modelId, + source: reason.source, + decision: reason.decision, + score: reason.score, + policy_version: reason.policyVersion, + reason: reason.reason, + } +} + function buildRequiresActionDetails( tool: Tool, input: Record, @@ -567,6 +592,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. @@ -606,6 +641,9 @@ export class StructuredIO { decision_reason: serializeDecisionReason( mainPermissionResult.decisionReason, ), + permission_guardrail: serializePermissionGuardrail( + mainPermissionResult.decisionReason, + ), tool_use_id: toolUseID, agent_id: toolUseContext.agentId, ...(mainPermissionResult.decisionReason?.type === 'safetyCheck' ? { always_asks: true } : {}), @@ -630,33 +668,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..20e4d1ac 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 'autoPermission': + return `${chalk.bold(decisionReason.modelId)} Auto permission: ${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..edd4fbe4 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 'autoPermission': + return { + reasonString: `Auto mode ${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+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLFNBQVNBLE9BQU8sUUFBUSxZQUFZO0FBQ3BDLE9BQU9DLEtBQUssTUFBTSxPQUFPO0FBQ3pCLE9BQU9DLEtBQUssTUFBTSxPQUFPO0FBQ3pCLFNBQVNDLElBQUksRUFBRUMsR0FBRyxFQUFFQyxJQUFJLFFBQVEsY0FBYztBQUM5QyxTQUFTQyxXQUFXLFFBQVEseUJBQXlCO0FBQ3JELGNBQ0VDLGtCQUFrQixFQUNsQkMsd0JBQXdCLFFBQ25CLDZDQUE2QztBQUNwRCxTQUFTQywyQkFBMkIsUUFBUSxpREFBaUQ7QUFDN0YsY0FBY0MsS0FBSyxRQUFRLHNCQUFzQjtBQUNqRCxPQUFPQyxVQUFVLE1BQU0sZ0NBQWdDO0FBRXZELE9BQU8sS0FBS0MsOEJBQThCLEdBQUc7RUFDM0NDLGdCQUFnQixFQUFFTixrQkFBa0I7RUFDcENPLFFBQVEsRUFBRSxNQUFNLEdBQUcsU0FBUyxHQUFHLE1BQU0sR0FBRyxNQUFNO0FBQ2hELENBQUM7QUFFRCxLQUFLQyxxQkFBcUIsR0FBRztFQUMzQkMsWUFBWSxFQUFFLE1BQU07RUFDcEJDLFlBQVksQ0FBQyxFQUFFLE1BQU07RUFDckI7RUFDQUMsVUFBVSxDQUFDLEVBQUUsTUFBTVIsS0FBSztBQUMxQixDQUFDO0FBRUQsU0FBU1Msd0JBQXdCQSxDQUMvQkMsTUFBTSxFQUFFWix3QkFBd0IsR0FBRyxTQUFTLEVBQzVDTSxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsR0FBRyxNQUFNLEdBQUcsTUFBTSxDQUMvQyxFQUFFQyxxQkFBcUIsR0FBRyxJQUFJLENBQUM7RUFDOUIsSUFBSSxDQUFDSyxNQUFNLEVBQUU7SUFDWCxPQUFPLElBQUk7RUFDYjtFQUNBLElBQ0UsQ0FBQ3BCLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJQSxPQUFPLENBQUMsdUJBQXVCLENBQUMsS0FDL0RvQixNQUFNLENBQUNDLElBQUksS0FBSyxZQUFZLEVBQzVCO0lBQ0EsSUFBSUQsTUFBTSxDQUFDRSxVQUFVLEtBQUssV0FBVyxFQUFFO01BQ3JDLE9BQU87UUFDTE4sWUFBWSxFQUFFLHVEQUF1REYsUUFBUSxNQUFNTSxNQUFNLENBQUNBLE1BQU0sRUFBRTtRQUNsR0gsWUFBWSxFQUFFTSxTQUFTO1FBQ3ZCTCxVQUFVLEVBQUU7TUFDZCxDQUFDO0lBQ0g7SUFDQSxPQUFPO01BQ0xGLFlBQVksRUFBRSxjQUFjZixLQUFLLENBQUN1QixJQUFJLENBQUNKLE1BQU0sQ0FBQ0UsVUFBVSxDQUFDLG1DQUFtQ1IsUUFBUSxNQUFNTSxNQUFNLENBQUNBLE1BQU0sRUFBRTtNQUN6SEgsWUFBWSxFQUFFTTtJQUNoQixDQUFDO0VBQ0g7RUFDQSxRQUFRSCxNQUFNLENBQUNDLElBQUk7SUFDakIsS0FBSyxNQUFNO01BQ1QsT0FBTztRQUNMTCxZQUFZLEVBQUUsbUJBQW1CZixLQUFLLENBQUN1QixJQUFJLENBQ3pDZiwyQkFBMkIsQ0FBQ1csTUFBTSxDQUFDSyxJQUFJLENBQUNDLFNBQVMsQ0FDbkQsQ0FBQyxtQ0FBbUNaLFFBQVEsR0FBRztRQUMvQ0csWUFBWSxFQUNWRyxNQUFNLENBQUNLLElBQUksQ0FBQ0UsTUFBTSxLQUFLLGdCQUFnQixHQUNuQ0osU0FBUyxHQUNUO01BQ1IsQ0FBQztJQUNILEtBQUssTUFBTTtNQUFFO1FBQ1gsTUFBTUssZ0JBQWdCLEdBQUdSLE1BQU0sQ0FBQ0EsTUFBTSxHQUFHLE1BQU1BLE1BQU0sQ0FBQ0EsTUFBTSxFQUFFLEdBQUcsR0FBRztRQUNwRSxNQUFNUyxXQUFXLEdBQUdULE1BQU0sQ0FBQ1UsVUFBVSxHQUNqQyxJQUFJN0IsS0FBSyxDQUFDOEIsR0FBRyxDQUFDLElBQUlYLE1BQU0sQ0FBQ1UsVUFBVSxHQUFHLENBQUMsRUFBRSxHQUN6QyxFQUFFO1FBQ04sT0FBTztVQUNMZCxZQUFZLEVBQUUsUUFBUWYsS0FBSyxDQUFDdUIsSUFBSSxDQUFDSixNQUFNLENBQUNZLFFBQVEsQ0FBQyxtQ0FBbUNsQixRQUFRLEdBQUdjLGdCQUFnQixHQUFHQyxXQUFXLEVBQUU7VUFDL0haLFlBQVksRUFBRTtRQUNoQixDQUFDO01BQ0g7SUFDQSxLQUFLLGFBQWE7SUFDbEIsS0FBSyxPQUFPO01BQ1YsT0FBTztRQUNMRCxZQUFZLEVBQUVJLE1BQU0sQ0FBQ0EsTUFBTTtRQUMzQkgsWUFBWSxFQUFFTTtNQUNoQixDQUFDO0lBQ0gsS0FBSyxZQUFZO01BQ2YsT0FBTztRQUNMUCxZQUFZLEVBQUVJLE1BQU0sQ0FBQ0EsTUFBTTtRQUMzQkgsWUFBWSxFQUFFO01BQ2hCLENBQUM7SUFDSDtNQUNFLE9BQU8sSUFBSTtFQUNmO0FBQ0Y7QUFFQSxPQUFPLFNBQUFnQiwwQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUFtQztJQUFBdkIsZ0JBQUE7SUFBQUM7RUFBQSxJQUFBb0IsRUFHVDtFQUMvQixNQUFBRyxjQUFBLEdBQXVCL0IsV0FBVyxDQUFDZ0MsS0FBaUMsQ0FBQztFQUVuRSxNQUFBQyxFQUFBLEdBQUExQixnQkFBZ0IsRUFBQTJCLGNBQWdCO0VBQUEsSUFBQUMsRUFBQTtFQUFBLElBQUFOLENBQUEsUUFBQUksRUFBQSxJQUFBSixDQUFBLFFBQUFyQixRQUFBO0lBRGxCMkIsRUFBQSxHQUFBdEIsd0JBQXdCLENBQ3RDb0IsRUFBZ0MsRUFDaEN6QixRQUNGLENBQUM7SUFBQXFCLENBQUEsTUFBQUksRUFBQTtJQUFBSixDQUFBLE1BQUFyQixRQUFBO0lBQUFxQixDQUFBLE1BQUFNLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFOLENBQUE7RUFBQTtFQUhELE1BQUFPLE9BQUEsR0FBZ0JELEVBR2Y7RUFDRCxJQUFJLENBQUNDLE9BQU87SUFBQSxPQUNILElBQUk7RUFBQTtFQUdiLE1BQUF4QixVQUFBLEdBQ0V3QixPQUFPLENBQUF4QixVQUlPLEtBSGJMLGdCQUFnQixFQUFBMkIsY0FBc0IsRUFBQW5CLElBQUEsS0FBSyxNQUNuQixJQUF6QmdCLGNBQWMsS0FBSyxNQUVOLEdBSFosU0FHWSxHQUhaZCxTQUdhO0VBQUEsSUFBQW9CLEVBQUE7RUFBQSxJQUFBUixDQUFBLFFBQUFPLE9BQUEsQ0FBQTFCLFlBQUEsSUFBQW1CLENBQUEsUUFBQWpCLFVBQUE7SUFJWHlCLEVBQUEsR0FBQXpCLFVBQVUsR0FDVCxDQUFDLFVBQVUsQ0FBUUEsS0FBVSxDQUFWQSxXQUFTLENBQUMsQ0FBRyxDQUFBd0IsT0FBTyxDQUFBMUIsWUFBWSxDQUFFLEVBQXBELFVBQVUsQ0FLWixHQUhDLENBQUMsSUFBSSxDQUNILENBQUMsSUFBSSxDQUFFLENBQUEwQixPQUFPLENBQUExQixZQUFZLENBQUUsRUFBM0IsSUFBSSxDQUNQLEVBRkMsSUFBSSxDQUdOO0lBQUFtQixDQUFBLE1BQUFPLE9BQUEsQ0FBQTFCLFlBQUE7SUFBQW1CLENBQUEsTUFBQWpCLFVBQUE7SUFBQWlCLENBQUEsTUFBQVEsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVIsQ0FBQTtFQUFBO0VBQUEsSUFBQVMsRUFBQTtFQUFBLElBQUFULENBQUEsUUFBQU8sT0FBQSxDQUFBekIsWUFBQTtJQUNBMkIsRUFBQSxHQUFBRixPQUFPLENBQUF6QixZQUE2RCxJQUE1QyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQUUsQ0FBQXlCLE9BQU8sQ0FBQXpCLFlBQVksQ0FBRSxFQUFwQyxJQUFJLENBQXVDO0lBQUFrQixDQUFBLE1BQUFPLE9BQUEsQ0FBQXpCLFlBQUE7SUFBQWtCLENBQUEsTUFBQVMsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQVQsQ0FBQTtFQUFBO0VBQUEsSUFBQVUsRUFBQTtFQUFBLElBQUFWLENBQUEsUUFBQVEsRUFBQSxJQUFBUixDQUFBLFFBQUFTLEVBQUE7SUFSdkVDLEVBQUEsSUFBQyxHQUFHLENBQWUsWUFBQyxDQUFELEdBQUMsQ0FBZ0IsYUFBUSxDQUFSLFFBQVEsQ0FDekMsQ0FBQUYsRUFNRCxDQUNDLENBQUFDLEVBQW1FLENBQ3RFLEVBVEMsR0FBRyxDQVNFO0lBQUFULENBQUEsTUFBQVEsRUFBQTtJQUFBUixDQUFBLE1BQUFTLEVBQUE7SUFBQVQsQ0FBQSxPQUFBVSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBVixDQUFBO0VBQUE7RUFBQSxPQVROVSxFQVNNO0FBQUE7QUE5QkgsU0FBQVAsTUFBQVEsQ0FBQTtFQUFBLE9BSW1DQSxDQUFDLENBQUFDLHFCQUFzQixDQUFBQyxJQUFLO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0= diff --git a/src/components/permissions/hooks.ts b/src/components/permissions/hooks.ts index 7cba31b7..bd693fa3 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 'autoPermission': + return `Auto permission: ${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 f694eadd..b24138c2 100644 --- a/src/entrypoints/sdk/controlSchemas.ts +++ b/src/entrypoints/sdk/controlSchemas.ts @@ -112,6 +112,17 @@ 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(), + source: z.enum(['decisionProvider', 'fastModel']), + decision: z.enum(['allow', 'ask']), + score: z.number().min(0).max(1).nullable(), + policy_version: z.number().int().nonnegative(), + reason: z.string().optional(), + }) + .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 9204796d..8aad52ca 100644 --- a/src/entrypoints/sdk/coreSchemas.ts +++ b/src/entrypoints/sdk/coreSchemas.ts @@ -336,7 +336,7 @@ export const PermissionResultSchema = lazySchema(() => export const PermissionModeSchema = lazySchema(() => z - .enum(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk']) + .enum(['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan', 'dontAsk']) .describe( 'Permission mode for controlling how tool executions are handled. ' + "'default' - Standard behavior, prompts for dangerous operations. " + @@ -1600,6 +1600,23 @@ 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(), + source: z.enum(['decisionProvider', 'fastModel']), + score: z.number().min(0).max(1).nullable(), + policy_version: z.number().int().nonnegative(), + reason: z.string().optional(), + uuid: UUIDPlaceholder(), + session_id: z.string(), + }), +) + export const SDKPostTurnSummaryMessageSchema = lazySchema(() => z .object({ @@ -1921,6 +1938,7 @@ export const SDKMessageSchema = lazySchema(() => SDKPartialAssistantMessageSchema(), SDKCompactBoundaryMessageSchema(), SDKStatusMessageSchema(), + SDKPermissionGuardrailDecisionMessageSchema(), SDKAPIRetryMessageSchema(), SDKLocalCommandOutputMessageSchema(), SDKHookStartedMessageSchema(), diff --git a/src/server/__tests__/e2e/business-flow.test.ts b/src/server/__tests__/e2e/business-flow.test.ts index d7bb8a10..fdba49e9 100644 --- a/src/server/__tests__/e2e/business-flow.test.ts +++ b/src/server/__tests__/e2e/business-flow.test.ts @@ -153,7 +153,7 @@ describe('Business Flow: Permission Modes', () => { await fs.rm(tmpDir, { recursive: true, force: true }) }) - const VALID_MODES = ['default', 'acceptEdits', 'plan', 'bypassPermissions'] + const VALID_MODES = ['default', 'acceptEdits', 'plan', 'auto', 'bypassPermissions'] it('should default to "default" mode', async () => { const { data } = await api('GET', '/api/permissions/mode') @@ -176,12 +176,6 @@ describe('Business Flow: Permission Modes', () => { }) } - it('should reject invalid mode "auto"', async () => { - const { status, data } = await api('PUT', '/api/permissions/mode', { mode: 'auto' }) - expect(status).toBe(400) - expect(data.message).toContain('Invalid permission mode') - }) - it('should reject missing mode field', async () => { const { status } = await api('PUT', '/api/permissions/mode', {}) expect(status).toBe(400) diff --git a/src/server/__tests__/permission-modes.test.ts b/src/server/__tests__/permission-modes.test.ts index 8d33ecf1..52b65097 100644 --- a/src/server/__tests__/permission-modes.test.ts +++ b/src/server/__tests__/permission-modes.test.ts @@ -11,11 +11,13 @@ describe('permission modes', () => { expect([...DEFAULT_PERMISSION_MODES].sort()).toEqual(PERMISSION_MODES.filter((mode) => mode !== 'dontAsk').sort()) expect(isDefaultPermissionMode('dontAsk')).toBe(false) expect(isDefaultPermissionMode('plan')).toBe(true) + expect(isDefaultPermissionMode('auto')).toBe(true) }) test('Bypass starts the CLI with --dangerously-skip-permissions, other modes with --permission-mode', () => { expect(permissionModeCliArgs('bypassPermissions')).toEqual(['--dangerously-skip-permissions']) expect(permissionModeCliArgs('plan')).toEqual(['--permission-mode', 'plan']) + expect(permissionModeCliArgs('auto')).toEqual(['--permission-mode', 'auto']) expect(permissionModeCliArgs(undefined)).toEqual(['--permission-mode', 'default']) }) @@ -47,8 +49,9 @@ describe('permission modes', () => { ).toBe('acceptEdits') }) - test('ignores unknown modes and returns null when nothing is recorded', () => { - expect(permissionModeFromTranscriptEntries([{ type: 'permission-mode', permissionMode: 'auto' }])).toBeNull() + test('restores Auto mode and ignores unknown modes', () => { + expect(permissionModeFromTranscriptEntries([{ type: 'permission-mode', permissionMode: 'auto' }])).toBe('auto') + expect(permissionModeFromTranscriptEntries([{ type: 'permission-mode', permissionMode: 'legacy' }])).toBeNull() expect(permissionModeFromTranscriptEntries([])).toBeNull() }) }) diff --git a/src/server/__tests__/provider-runtime-env.test.ts b/src/server/__tests__/provider-runtime-env.test.ts index f82d61fa..b9e9a160 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' @@ -54,6 +55,30 @@ describe('providerRuntimeEnv', () => { ANTHROPIC_SMALL_FAST_MODEL: 'active-main', ANTHROPIC_DEFAULT_SONNET_MODEL: 'active-main', ANTHROPIC_DEFAULT_OPUS_MODEL: 'active-main', + DREAMCODER_ACTIVE_PROVIDER_ID: 'provider-1', + DREAMCODER_FAST_MODEL_ID: 'active-main', + }) + }) + + test('prefers an explicitly configured fast model for Auto permission', async () => { + await writeJson(path.join(tmpDir, 'dreamcoder', 'providers.json'), { + activeId: 'provider-1', + providers: [{ + id: 'provider-1', + presetId: 'custom', + name: 'Active Provider', + apiKey: 'sk-active', + baseUrl: 'https://api.example.com/anthropic', + apiFormat: 'anthropic', + models: [{ id: 'active-main' }, { id: 'active-fast' }], + fastModel: 'active-fast', + }], + }) + + expect(readActiveProviderManagedEnv(tmpDir)).toMatchObject({ + DREAMCODER_ACTIVE_PROVIDER_ID: 'provider-1', + DREAMCODER_FAST_MODEL_ID: 'active-fast', + ANTHROPIC_SMALL_FAST_MODEL: 'active-fast', }) }) @@ -137,4 +162,86 @@ 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).not.toHaveProperty('activeDecisionProviderId') + 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('preserves manually configured decision models outside the preset', () => { + const index = normalizeProvidersIndex({ + activeId: null, + providers: [], + decisionProviders: [{ + id: 'openai', + presetId: 'openai', + name: 'OpenAI Decisions', + deployment: 'hosted', + apiFormat: 'openai_decisions', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'secret', + models: [{ id: 'vendor/gpt-6-luna-tuned' }], + }], + }) + + expect(index?.decisionProviders[0]?.models).toEqual([{ id: 'vendor/gpt-6-luna-tuned' }]) + }) + + test('defaults missing decision provider fields for legacy indexes', () => { + const index = normalizeProvidersIndex({ activeId: null, providers: [] }) + + expect(index?.decisionProviders).toEqual([]) + }) }) diff --git a/src/server/__tests__/providers.test.ts b/src/server/__tests__/providers.test.ts index 44d3ca21..eb56543d 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,169 @@ 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()) + + expect(await svc.listProviders()).toEqual({ + providers: [modelProvider], + activeId: null, + }) + expect(await svc.listDecisionProviders()).toEqual({ + providers: [decisionProvider], + }) + }) + + test('preserves manually configured decision model ids', async () => { + const svc = new ProviderService() + const created = await svc.addDecisionProvider(sampleDecisionInput({ + models: [{ id: 'no-auto-decision-model' }], + })) + + expect(created.models).toEqual([{ id: 'no-auto-decision-model' }]) + expect((await svc.listDecisionProviders()).providers[0]?.models).toEqual([ + { id: 'no-auto-decision-model' }, + ]) + }) + + 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..d1d7164d 100644 --- a/src/server/__tests__/settings.test.ts +++ b/src/server/__tests__/settings.test.ts @@ -201,6 +201,22 @@ describe('SettingsService', () => { expect(settings.model).toBe('claude-opus-4-7') }) + it('validates Auto permission settings before saving', async () => { + const svc = new SettingsService() + const autoPermission = { + providerId: null, + modelId: '', + privacyNoticeVersion: 0, + } + + await svc.updateUserSettings({ autoPermission }) + + expect((await svc.getUserSettings()).autoPermission).toEqual(autoPermission) + await expect(svc.updateUserSettings({ + autoPermission: { ...autoPermission, privacyNoticeVersion: -1 }, + })).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..48d09b9e 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,45 @@ 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', + source: 'decisionProvider', + score: 0.237, + policyVersion: 1, + reason: 'Requested action is low risk', + }) + 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', + source: 'decisionProvider', + score: 0.237, + policyVersion: 1, + reason: 'Requested action is low risk', + 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..9f905335 100644 --- a/src/server/__tests__/ws-memory-events.test.ts +++ b/src/server/__tests__/ws-memory-events.test.ts @@ -50,6 +50,80 @@ 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' }, + always_asks: true, + permission_guardrail: { + provider_id: 'provider-1', + model_id: 'gpt-6-luna', + source: 'decisionProvider', + decision: 'ask', + score: 0.812, + policy_version: 1, + reason: 'Potential deletion', + }, + }, + }, 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' }, + alwaysAsks: true, + guardrail: { + providerId: 'provider-1', + modelId: 'gpt-6-luna', + source: 'decisionProvider', + decision: 'ask', + score: 0.812, + policyVersion: 1, + reason: 'Potential deletion', + }, + }, + }], + }) + }) + + 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', + source: 'decisionProvider', + score: 0.237, + policy_version: 1, + reason: 'Requested action is low risk', + }, 'session-1')).toEqual([ + liveItem({ + type: 'permission_guardrail_decision', + decision: 'allow', + toolUseId: 'tool-1', + providerId: 'provider-1', + modelId: 'gpt-6-luna', + source: 'decisionProvider', + score: 0.237, + policyVersion: 1, + reason: 'Requested action is low risk', + }), + ]) + }) +}) + 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..1545804e 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,62 @@ 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') { + return Response.json(await providerService.listDecisionProviders()) + } + 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 === '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..ebe84ba9 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' @@ -32,6 +34,8 @@ export const MANAGED_PROVIDER_ENV_KEYS = [ ATTRIBUTION_HEADER_ENV_KEY, MODEL_CONTEXT_WINDOWS_ENV_KEY, 'DREAMCODER_OPENAI_OAUTH_PROVIDER', + 'DREAMCODER_ACTIVE_PROVIDER_ID', + 'DREAMCODER_FAST_MODEL_ID', 'OPENAI_CODEX_OAUTH_FILE', ] as const @@ -64,6 +68,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,12 +139,37 @@ 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 legacyModelId = normalizedModels.length === 1 ? normalizedModels[0]!.id : '' + const matchingPresetModels = preset?.models.filter((model) => legacyModelId.includes(model.id)) ?? [] + const concatenatedLegacyModel = matchingPresetModels.length > 0 && + !matchingPresetModels.some((model) => legacyModelId === model.id) && + (matchingPresetModels.length > 1 || (legacyModelId.match(/gpt-/gi)?.length ?? 0) > 1) + return { + ...provider, + deployment: isLoopbackDecisionProviderUrl(provider.baseUrl) ? 'local' : 'hosted', + models: concatenatedLegacyModel || normalizedModels.length === 0 + ? preset?.models ?? [] + : normalizedModels, + } +} + export function normalizeProvidersIndex(value: unknown): ProvidersIndex | null { if (!isRecord(value) || !Array.isArray(value.providers)) { return null } - const { activeProviderId: legacyActiveProviderId, ...rest } = value + const { + activeProviderId: legacyActiveProviderId, + activeDecisionProviderId: _legacyActiveDecisionProviderId, + ...rest + } = value const providers = value.providers .filter(isSavedProvider) .map((provider) => normalizeSavedProvider(provider)) @@ -131,12 +182,17 @@ 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)) + : [] return { ...rest, schemaVersion: typeof value.schemaVersion === 'number' ? value.schemaVersion : 1, activeId, providers, + decisionProviders, } } @@ -217,9 +273,10 @@ export function buildProviderManagedEnv( const models = normalizeProviderModels(provider.models) const selectedModel = options?.modelId?.trim() || models[0]?.id || '' - const fastModel = provider.fastModel && models.some((model) => model.id === provider.fastModel) + const configuredFastModel = provider.fastModel && models.some((model) => model.id === provider.fastModel) ? provider.fastModel - : selectedModel + : undefined + const fastModel = configuredFastModel ?? selectedModel const modelContextWindows = { ...getPresetModelContextWindows(provider.presetId), ...(provider.modelContextWindows ?? {}), @@ -248,6 +305,8 @@ export function buildProviderManagedEnv( ANTHROPIC_SMALL_FAST_MODEL: stripContextSuffix(fastModel), ANTHROPIC_DEFAULT_SONNET_MODEL: stripContextSuffix(selectedModel), ANTHROPIC_DEFAULT_OPUS_MODEL: stripContextSuffix(selectedModel), + DREAMCODER_ACTIVE_PROVIDER_ID: provider.id, + DREAMCODER_FAST_MODEL_ID: stripContextSuffix(fastModel), ...attributionHeaderEnvForModel(selectedModel), } } diff --git a/src/server/services/providerService.ts b/src/server/services/providerService.ts index 9e35917c..2169bcf2 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,7 @@ const DEFAULT_INDEX: ProvidersIndex = { schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION, activeId: null, providers: [], + decisionProviders: [], } export class ProviderService { @@ -106,6 +114,62 @@ export class ProviderService { return { providers: index.providers, activeId: index.activeId } } + async listDecisionProviders(): Promise<{ providers: SavedDecisionProvider[] }> { + const index = await this.readIndex() + return { providers: index.decisionProviders } + } + + 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}`) + } + index.decisionProviders.splice(providerIndex, 1) + 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 +373,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 +534,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..9d8d46ce 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 { AutoPermissionSettingsSchema } 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, 'autoPermission')) { + const result = AutoPermissionSettingsSchema.safeParse(settings.autoPermission) + if (!result.success) { + throw ApiError.badRequest('Invalid Auto permission settings') + } + settings.autoPermission = 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 db9fda05..1aa1d426 100644 --- a/src/server/session/state/types.ts +++ b/src/server/session/state/types.ts @@ -31,6 +31,16 @@ export type SessionRepository = { worktreeSlug?: string } +export type PermissionGuardrailDetails = { + providerId: string + modelId: string + source: 'decisionProvider' | 'fastModel' + decision: 'allow' | 'ask' + score: number | null + policyVersion: number + reason?: string +} + export type PendingPermission = { requestId: string toolName: string @@ -38,6 +48,7 @@ export type PendingPermission = { input: unknown description?: string suggestions?: unknown[] + guardrail?: PermissionGuardrailDetails alwaysAsks?: true } diff --git a/src/server/session/translate.ts b/src/server/session/translate.ts index 2ea19d54..cb4db282 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,10 +62,46 @@ 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 } : {}), ...(request.always_asks === true ? { alwaysAsks: true as const } : {}), } } +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.source !== 'decisionProvider' && details.source !== 'fastModel') || + (details.decision !== 'allow' && details.decision !== 'ask') || + (score !== null && ( + typeof score !== 'number' || + !Number.isFinite(score) || + score < 0 || + score > 1 + )) || + typeof details.policy_version !== 'number' || + !Number.isInteger(details.policy_version) || + details.policy_version < 0 || + (details.reason !== undefined && typeof details.reason !== 'string') + ) { + return undefined + } + return { + providerId: details.provider_id, + modelId: details.model_id, + source: details.source, + decision: details.decision, + score, + policyVersion: details.policy_version, + ...(typeof details.reason === 'string' ? { reason: details.reason } : {}), + } +} + function initSlashCommands(cliMsg: any): SessionSlashCommand[] | null { return Array.isArray(cliMsg.slash_commands) ? normalizeSessionSlashCommands(cliMsg.slash_commands) : null } @@ -486,6 +523,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 }) @@ -578,6 +619,42 @@ 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 source = marker.source + const score = marker.score + const policyVersion = marker.policy_version + if ( + (decision !== 'allow' && decision !== 'ask') || + (source !== 'decisionProvider' && source !== 'fastModel') || + 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)) || + typeof policyVersion !== 'number' || + !Number.isInteger(policyVersion) || + policyVersion < 0 || + (marker.reason !== undefined && typeof marker.reason !== 'string') + ) { + return null + } + return { + type: 'permission_guardrail_decision', + decision, + toolUseId: marker.tool_use_id, + providerId: marker.provider_id, + modelId: marker.model_id, + source, + score, + policyVersion, + ...(typeof marker.reason === 'string' ? { reason: marker.reason } : {}), + } +} + 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 ea3718ce..541665e9 100644 --- a/src/server/transcript/entries.ts +++ b/src/server/transcript/entries.ts @@ -35,7 +35,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 @@ -119,6 +119,46 @@ 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 source = marker.source + const score = marker.score + const policyVersion = marker.policyVersion + if ( + marker.type !== 'permission_guardrail_decision' || + (decision !== 'allow' && decision !== 'ask') || + (source !== 'decisionProvider' && source !== 'fastModel') || + typeof marker.toolUseID !== 'string' || + typeof marker.providerId !== 'string' || + typeof marker.modelId !== 'string' || + (score !== null && (typeof score !== 'number' || score < 0 || score > 1)) || + typeof policyVersion !== 'number' || + !Number.isInteger(policyVersion) || + policyVersion < 0 || + (marker.reason !== undefined && typeof marker.reason !== 'string') + ) { + return null + } + return { + id: entry.uuid || crypto.randomUUID(), + type: 'permission_guardrail_decision', + content: { + decision, + toolUseId: marker.toolUseID, + providerId: marker.providerId, + modelId: marker.modelId, + source, + score, + policyVersion, + ...(typeof marker.reason === 'string' ? { reason: marker.reason } : {}), + }, + 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 @@ -272,6 +312,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 9907087d..111067a7 100644 --- a/src/server/transcript/history.ts +++ b/src/server/transcript/history.ts @@ -35,6 +35,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. */ @@ -163,6 +164,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 b55c6442..6a5695bb 100644 --- a/src/server/transcript/items.ts +++ b/src/server/transcript/items.ts @@ -82,6 +82,19 @@ export type TranscriptItem = teamCount?: number timestamp: number } + | { + id: string + type: 'permission_guardrail_decision' + decision: 'allow' | 'ask' + toolUseId: string + providerId: string + modelId: string + source: 'decisionProvider' | 'fastModel' + score: number | null + policyVersion: number + reason?: string + 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..762e5c82 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,33 @@ export const ProvidersIndexSchema = z.object({ schemaVersion: z.number().int().positive().optional(), activeId: z.string().nullable(), providers: z.array(SavedProviderSchema), + 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 +168,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 d8d0de68..5f1435f8 100644 --- a/src/server/ws/protocol.ts +++ b/src/server/ws/protocol.ts @@ -29,6 +29,7 @@ export type { BrowserActivity, PendingBrowserGrant, PendingPermission, + PermissionGuardrailDetails, ProcessDetail, ProcessPhase, QueuedMessage, @@ -42,15 +43,15 @@ export type { } from '../session/state/types.js' /** The permission modes a client may set; mirrors the CLI's `ExternalPermissionMode`. */ -export type PermissionMode = 'acceptEdits' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan' +export type PermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan' -export const PERMISSION_MODES: readonly PermissionMode[] = ['acceptEdits', 'bypassPermissions', 'default', 'dontAsk', 'plan'] +export const PERMISSION_MODES: readonly PermissionMode[] = ['acceptEdits', 'auto', 'bypassPermissions', 'default', 'dontAsk', 'plan'] export type ClientKind = 'desktop' | 'h5' export type DefaultPermissionMode = Exclude -export const DEFAULT_PERMISSION_MODES: readonly DefaultPermissionMode[] = ['default', 'acceptEdits', 'plan', 'bypassPermissions'] +export const DEFAULT_PERMISSION_MODES: readonly DefaultPermissionMode[] = ['default', 'acceptEdits', 'plan', 'auto', 'bypassPermissions'] export const BROWSER_SEARCH_ENGINE_IDS = ['bing', 'google', 'duckduckgo', 'baidu'] as const @@ -94,7 +95,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..509a89d2 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 'autoPermission': case 'subcommandResults': case 'asyncAgent': case 'sandboxOverride': @@ -993,6 +994,25 @@ async function checkPermissionsAndCallTool( }) } + if ( + permissionDecision.behavior === 'allow' && + permissionDecision.decisionReason?.type === 'autoPermission' + ) { + resultingMessages.push({ + message: createAttachmentMessage({ + type: 'permission_guardrail_decision', + decision: permissionDecision.decisionReason.decision, + toolUseID, + providerId: permissionDecision.decisionReason.providerId, + modelId: permissionDecision.decisionReason.modelId, + source: permissionDecision.decisionReason.source, + score: permissionDecision.decisionReason.score, + policyVersion: permissionDecision.decisionReason.policyVersion, + reason: permissionDecision.decisionReason.reason, + }), + }) + } + 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..6d7a38ea --- /dev/null +++ b/src/types/permissionGuardrail.ts @@ -0,0 +1,15 @@ +import { z } from 'zod/v4' + +export const AutoPermissionSettingsSchema = z.object({ + providerId: z.string().nullable(), + modelId: z.string(), + privacyNoticeVersion: z.number().int().nonnegative(), +}) + +export type AutoPermissionSettings = z.infer + +export const DEFAULT_AUTO_PERMISSION_SETTINGS: AutoPermissionSettings = { + providerId: null, + modelId: '', + privacyNoticeVersion: 0, +} diff --git a/src/types/permissions.ts b/src/types/permissions.ts index 20c9f901..3316388a 100644 --- a/src/types/permissions.ts +++ b/src/types/permissions.ts @@ -6,7 +6,6 @@ * to avoid circular dependencies. */ -import { feature } from 'bun:bundle' import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs' // ============================================================================ @@ -15,6 +14,7 @@ import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs export const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', + 'auto', 'bypassPermissions', 'default', 'dontAsk', @@ -25,14 +25,13 @@ export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number] // Exhaustive mode union for typechecking. The user-addressable runtime set // is INTERNAL_PERMISSION_MODES below. -export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble' +export type InternalPermissionMode = ExternalPermissionMode | 'bubble' export type PermissionMode = InternalPermissionMode // Runtime validation set: modes that are user-addressable (settings.json // defaultMode, --permission-mode CLI flag, conversation recovery). export const INTERNAL_PERMISSION_MODES = [ ...EXTERNAL_PERMISSION_MODES, - ...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)), ] as const satisfies readonly PermissionMode[] export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES @@ -305,6 +304,16 @@ export type PermissionDecisionReason = classifier: string reason: string } + | { + type: 'autoPermission' + providerId: string + modelId: string + source: 'decisionProvider' | 'fastModel' + decision: 'allow' | 'ask' + score: number | null + policyVersion: number + reason?: string + } | { type: 'workingDir' reason: string diff --git a/src/utils/attachments.ts b/src/utils/attachments.ts index 7952c420..00ba944f 100644 --- a/src/utils/attachments.ts +++ b/src/utils/attachments.ts @@ -382,6 +382,18 @@ export type HookPermissionDecisionAttachment = { hookEvent: HookEvent } +export type PermissionGuardrailDecisionAttachment = { + type: 'permission_guardrail_decision' + decision: 'allow' | 'ask' + toolUseID: string + providerId: string + modelId: string + source: 'decisionProvider' | 'fastModel' + score: number | null + policyVersion: number + reason?: string +} + export type HookSystemMessageAttachment = { type: 'hook_system_message' content: string @@ -639,6 +651,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 29e0c35f..e7d85335 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -4352,6 +4352,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/PermissionMode.ts b/src/utils/permissions/PermissionMode.ts index 3e8ee50f..7c6a5147 100644 --- a/src/utils/permissions/PermissionMode.ts +++ b/src/utils/permissions/PermissionMode.ts @@ -1,4 +1,3 @@ -import { feature } from 'bun:bundle' import z from 'zod/v4' import type { ToolPermissionContext } from '../../Tool.js' import { PAUSE_ICON } from '../../constants/figures.js' @@ -78,17 +77,13 @@ const PERMISSION_MODE_CONFIG: Partial< color: 'error', external: 'dontAsk', }, - ...(feature('TRANSCRIPT_CLASSIFIER') - ? { - auto: { - title: 'Auto mode', - shortTitle: 'Auto', - symbol: '⏵⏵', - color: 'warning' as ModeColorKey, - external: 'default' as ExternalPermissionMode, - }, - } - : {}), + auto: { + title: 'Auto mode', + shortTitle: 'Auto', + symbol: '⏵⏵', + color: 'warning', + external: 'auto', + }, } /** @@ -98,11 +93,7 @@ const PERMISSION_MODE_CONFIG: Partial< export function isExternalPermissionMode( mode: PermissionMode, ): mode is ExternalPermissionMode { - // External users can't have auto, so always true for them - if (process.env.USER_TYPE !== 'ant') { - return true - } - return mode !== 'auto' && mode !== 'bubble' + return (EXTERNAL_PERMISSION_MODES as readonly string[]).includes(mode) } function getModeConfig(mode: PermissionMode): PermissionModeConfig { diff --git a/src/utils/permissions/getNextPermissionMode.test.ts b/src/utils/permissions/getNextPermissionMode.test.ts new file mode 100644 index 00000000..4d050fbf --- /dev/null +++ b/src/utils/permissions/getNextPermissionMode.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'bun:test' +import { getEmptyToolPermissionContext } from '../../Tool.js' +import { getNextPermissionMode } from './getNextPermissionMode.js' + +describe('permission mode cycle', () => { + test('places Auto between Plan and Bypass', () => { + const context = getEmptyToolPermissionContext() + expect(getNextPermissionMode({ ...context, mode: 'plan' })).toBe('auto') + expect(getNextPermissionMode({ + ...context, + mode: 'auto', + isBypassPermissionsModeAvailable: true, + })).toBe('bypassPermissions') + expect(getNextPermissionMode({ + ...context, + mode: 'auto', + isBypassPermissionsModeAvailable: false, + })).toBe('default') + }) +}) diff --git a/src/utils/permissions/getNextPermissionMode.ts b/src/utils/permissions/getNextPermissionMode.ts index cbe958e1..1a5baea9 100644 --- a/src/utils/permissions/getNextPermissionMode.ts +++ b/src/utils/permissions/getNextPermissionMode.ts @@ -1,32 +1,6 @@ -import { feature } from 'bun:bundle' import type { ToolPermissionContext } from '../../Tool.js' -import { logForDebugging } from '../debug.js' import type { PermissionMode } from './PermissionMode.js' -import { - getAutoModeUnavailableReason, - isAutoModeGateEnabled, - transitionPermissionMode, -} from './permissionSetup.js' - -// Checks both the cached isAutoModeAvailable (set at startup by -// verifyAutoModeGateAccess) and the live isAutoModeGateEnabled() — these can -// diverge if the circuit breaker or settings change mid-session. The -// live check prevents transitionPermissionMode from throwing -// (permissionSetup.ts:~559), which would silently crash the shift+tab handler -// and leave the user stuck at the current mode. -function canCycleToAuto(ctx: ToolPermissionContext): boolean { - if (feature('TRANSCRIPT_CLASSIFIER')) { - const gateEnabled = isAutoModeGateEnabled() - const can = !!ctx.isAutoModeAvailable && gateEnabled - if (!can) { - logForDebugging( - `[auto-mode] canCycleToAuto=false: ctx.isAutoModeAvailable=${ctx.isAutoModeAvailable} isAutoModeGateEnabled=${gateEnabled} reason=${getAutoModeUnavailableReason()}`, - ) - } - return can - } - return false -} +import { transitionPermissionMode } from './permissionSetup.js' /** * Determines the next permission mode when cycling through modes with Shift+Tab. @@ -37,54 +11,31 @@ export function getNextPermissionMode( ): PermissionMode { switch (toolPermissionContext.mode) { case 'default': - // Ants skip acceptEdits and plan — auto mode replaces them - if (process.env.USER_TYPE === 'ant') { - if (toolPermissionContext.isBypassPermissionsModeAvailable) { - return 'bypassPermissions' - } - if (canCycleToAuto(toolPermissionContext)) { - return 'auto' - } - return 'default' - } return 'acceptEdits' case 'acceptEdits': return 'plan' case 'plan': + return 'auto' + + case 'auto': if (toolPermissionContext.isBypassPermissionsModeAvailable) { return 'bypassPermissions' } - if (canCycleToAuto(toolPermissionContext)) { - return 'auto' - } return 'default' case 'bypassPermissions': - if (canCycleToAuto(toolPermissionContext)) { - return 'auto' - } return 'default' case 'dontAsk': // Not exposed in UI cycle yet, but return default if somehow reached return 'default' - - default: - // Covers auto (when TRANSCRIPT_CLASSIFIER is enabled) and any future modes — always fall back to default return 'default' } } -/** - * Computes the next permission mode and prepares the context for it. - * Handles any context cleanup needed for the target mode (e.g., stripping - * dangerous permissions when entering auto mode). - * - * @returns The next mode and the context to use (with dangerous permissions stripped if needed) - */ export function cyclePermissionMode( toolPermissionContext: ToolPermissionContext, teamContext?: { leadAgentId: string }, diff --git a/src/utils/permissions/permissionGuardrail.test.ts b/src/utils/permissions/permissionGuardrail.test.ts new file mode 100644 index 00000000..aebb00b0 --- /dev/null +++ b/src/utils/permissions/permissionGuardrail.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'bun:test' +import type { SavedDecisionProvider } from '../../server/types/provider.js' +import { + getDecisionPolicy, + parseFastModelDecision, + parsePermissionGuardrailAskProbability, +} from './permissionGuardrail.js' + +function provider( + apiFormat: SavedDecisionProvider['apiFormat'], + modelId = 'model', +): SavedDecisionProvider { + return { + id: 'provider', + presetId: 'custom', + name: 'Provider', + deployment: 'hosted', + apiFormat, + baseUrl: 'https://example.com/v1', + apiKey: 'secret', + models: [{ id: modelId }], + } +} + +describe('Auto permission evaluator', () => { + 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('does not treat malformed chat 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('registers a fixed policy only for the validated OpenAI Decisions model', () => { + expect(getDecisionPolicy(provider('openai_decisions', 'gpt-6-luna'), 'gpt-6-luna')).toEqual({ + threshold: 0.5, + policyVersion: 1, + }) + expect(getDecisionPolicy(provider('openrouter_decisions', 'gpt-6-luna'), 'gpt-6-luna')).toBeNull() + expect(getDecisionPolicy(provider('openai_decisions', 'other'), 'other')).toBeNull() + }) + + test('parses forced fast-model output conservatively', () => { + expect(parseFastModelDecision({ behavior: 'allow', reason: 'Requested read' })).toEqual({ + behavior: 'allow', + reason: 'Requested read', + }) + expect(parseFastModelDecision({ behavior: 'allowish' })).toBeNull() + expect(parseFastModelDecision(null)).toBeNull() + }) +}) diff --git a/src/utils/permissions/permissionGuardrail.ts b/src/utils/permissions/permissionGuardrail.ts new file mode 100644 index 00000000..e966d1bf --- /dev/null +++ b/src/utils/permissions/permissionGuardrail.ts @@ -0,0 +1,361 @@ +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 { getClaudeConfigHomeDir } from '../envUtils.js' +import { sideQuery } from '../sideQuery.js' +import { getAutoPermissionSettings } from '../settings/settings.js' + +const MAX_INPUT_CHARS = 1500 +const REQUEST_TIMEOUT_MS = 5000 +export const AUTO_PERMISSION_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 DECISION_POLICIES = new Map([ + ['openai_decisions:gpt-6-luna', { threshold: 0.5, policyVersion: 1 }], +]) + +const AUTO_PERMISSION_TOOL = { + name: 'auto_permission_decision', + description: 'Decide whether the tool call can run or requires user confirmation', + input_schema: { + type: 'object' as const, + properties: { + behavior: { type: 'string', enum: ['allow', 'ask'] }, + reason: { type: 'string' }, + }, + required: ['behavior', 'reason'], + }, +} + +export type AutoPermissionEvaluation = { + behavior: 'allow' | 'ask' + providerId: string + modelId: string + source: 'decisionProvider' | 'fastModel' + score: number | null + policyVersion: number + reason?: string +} + +type AutoPermissionInput = { + toolName: string + input: Record + messages: unknown[] + signal?: AbortSignal +} + +export function getDecisionPolicy( + provider: SavedDecisionProvider, + modelId: string, +): { threshold: number; policyVersion: number } | null { + return DECISION_POLICIES.get(`${provider.apiFormat}:${modelId}`) ?? null +} + +export async function evaluateAutoPermission( + value: AutoPermissionInput, +): Promise { + const settings = getAutoPermissionSettings() + if (!settings) return null + + if (settings.providerId) { + return evaluateDecisionProvider(settings, value) + } + + const providerId = process.env.DREAMCODER_ACTIVE_PROVIDER_ID?.trim() + const modelId = process.env.DREAMCODER_FAST_MODEL_ID?.trim() + if (!providerId || !modelId) return null + if (settings.privacyNoticeVersion < AUTO_PERMISSION_PRIVACY_NOTICE_VERSION) { + return fallback(providerId, modelId, 'fastModel', 1, 'Privacy confirmation is required') + } + + return evaluateFastModel(providerId, modelId, value) +} + +async function evaluateDecisionProvider( + settings: { providerId: string | null; modelId: string; privacyNoticeVersion: number }, + value: AutoPermissionInput, +): Promise { + const providerId = settings.providerId ?? '' + const provider = readDecisionProvider(providerId) + if (!provider || !provider.models.some((model) => model.id === settings.modelId)) { + return fallback(providerId, settings.modelId, 'decisionProvider', 0, 'Decision provider or model is unavailable') + } + + const policy = getDecisionPolicy(provider, settings.modelId) + if (!policy) { + return fallback(provider.id, settings.modelId, 'decisionProvider', 0, 'The selected model does not have an Auto policy') + } + if ( + provider.deployment === 'hosted' && + settings.privacyNoticeVersion < AUTO_PERMISSION_PRIVACY_NOTICE_VERSION + ) { + return fallback(provider.id, settings.modelId, 'decisionProvider', policy.policyVersion, 'Privacy confirmation is required') + } + if (!provider.apiKey && provider.apiFormat !== 'ollama_chat') { + return fallback(provider.id, settings.modelId, 'decisionProvider', policy.policyVersion, 'API key is missing') + } + + let input: string + try { + input = renderInput(value) + } catch { + return fallback(provider.id, settings.modelId, 'decisionProvider', policy.policyVersion, 'Tool context could not be serialized') + } + + const score = await requestAskProbability(provider, settings.modelId, input, value.signal) + if (score === null) { + return fallback(provider.id, settings.modelId, 'decisionProvider', policy.policyVersion, 'Decision request failed') + } + + return { + behavior: score >= policy.threshold ? 'ask' : 'allow', + providerId: provider.id, + modelId: settings.modelId, + source: 'decisionProvider', + score, + policyVersion: policy.policyVersion, + } +} + +async function evaluateFastModel( + providerId: string, + modelId: string, + value: AutoPermissionInput, +): Promise { + try { + const response = await sideQuery({ + querySource: 'auto_mode_critique', + model: modelId, + system: POLICY, + skipSystemPromptPrefix: true, + messages: [{ role: 'user', content: renderInput(value) }], + tools: [AUTO_PERMISSION_TOOL], + tool_choice: { type: 'tool', name: AUTO_PERMISSION_TOOL.name }, + max_tokens: 128, + maxRetries: 0, + signal: combineSignal(value.signal), + temperature: 0, + thinking: false, + }) + const block = response.content.find((content) => content.type === 'tool_use') + const parsed = parseFastModelDecision(block?.type === 'tool_use' ? block.input : null) + if (!parsed) { + return fallback(providerId, modelId, 'fastModel', 1, 'Decision response could not be parsed') + } + return { + behavior: parsed.behavior, + providerId, + modelId, + source: 'fastModel', + score: null, + policyVersion: 1, + reason: parsed.reason, + } + } catch { + return fallback(providerId, modelId, 'fastModel', 1, 'Decision request failed') + } +} + +export function parseFastModelDecision( + value: unknown, +): { behavior: 'allow' | 'ask'; reason?: string } | null { + const record = asRecord(value) + if (record?.behavior !== 'allow' && record?.behavior !== 'ask') return null + return { + behavior: record.behavior, + ...(typeof record.reason === 'string' && record.reason.trim() + ? { reason: record.reason.trim() } + : {}), + } +} + +function fallback( + providerId: string, + modelId: string, + source: AutoPermissionEvaluation['source'], + policyVersion: number, + reason: string, +): AutoPermissionEvaluation { + return { + behavior: 'ask', + providerId, + modelId, + source, + score: null, + policyVersion, + reason, + } +} + +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: AutoPermissionInput): 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, + signal?: AbortSignal, +): 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: combineSignal(signal), + }) + if (!response.ok) return null + return parsePermissionGuardrailAskProbability(provider, await response.json()) + } catch { + return null + } +} + +function combineSignal(signal?: AbortSignal): AbortSignal { + const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS) + return signal ? AbortSignal.any([signal, timeout]) : timeout +} + +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..62bb3271 --- /dev/null +++ b/src/utils/permissions/permissions.permissionGuardrail.test.ts @@ -0,0 +1,273 @@ +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 { getOriginalCwd, setOriginalCwd } from '../../bootstrap/state.js' +import type { Tool, ToolPermissionContext, ToolUseContext } from '../../Tool.js' +import { getEmptyToolPermissionContext } from '../../Tool.js' +import { getDefaultAppState, type AppState } from '../../state/AppStateStore.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 type { AssistantMessage } from '../../types/message.js' +import type { PermissionMode } from '../../types/permissions.js' +import { resetSettingsCache } from '../settings/settingsCache.js' +import { initializeToolPermissionContext } from './permissionSetup.js' +import { hasPermissionsToUseTool } from './permissions.js' + +describe('Auto permission pipeline', () => { + let configDir: string + let originalConfigDir: string | undefined + let originalCwd: string + let originalFetch: typeof fetch + + beforeEach(() => { + configDir = mkdtempSync(join(tmpdir(), 'auto-permission-')) + originalConfigDir = process.env.DREAMCODER_CONFIG_DIR + originalCwd = getOriginalCwd() + originalFetch = globalThis.fetch + process.env.DREAMCODER_CONFIG_DIR = configDir + mkdirSync(join(configDir, 'dreamcoder'), { recursive: true }) + writeFileSync(join(configDir, 'settings.json'), JSON.stringify({ + autoPermission: { + providerId: 'decision-provider', + modelId: 'gpt-6-luna', + privacyNoticeVersion: 1, + }, + })) + writeFileSync(join(configDir, 'dreamcoder', 'providers.json'), JSON.stringify({ + activeId: null, + providers: [], + decisionProviders: [{ + id: 'decision-provider', + presetId: 'openai', + name: 'OpenAI Decisions', + deployment: 'hosted', + apiFormat: 'openai_decisions', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-placeholder', + models: [{ id: 'gpt-6-luna' }], + }], + })) + resetSettingsCache() + }) + + afterEach(() => { + globalThis.fetch = originalFetch + if (originalConfigDir === undefined) delete process.env.DREAMCODER_CONFIG_DIR + else process.env.DREAMCODER_CONFIG_DIR = originalConfigDir + setOriginalCwd(originalCwd) + resetSettingsCache() + rmSync(configDir, { recursive: true, force: true }) + }) + + function context( + mode: PermissionMode, + denyRules: string[] = [], + loadedContext?: ToolPermissionContext, + ): ToolUseContext { + let appState = { + ...getDefaultAppState(), + toolPermissionContext: loadedContext ?? { + ...getEmptyToolPermissionContext(), + mode, + alwaysDenyRules: denyRules.length > 0 ? { localSettings: denyRules } : {}, + }, + } as AppState + return { + abortController: new AbortController(), + getAppState: () => appState, + setAppState: (update: (state: AppState) => AppState) => { + appState = update(appState) + }, + messages: [], + } as unknown as ToolUseContext + } + + function unresolvedTool(): Tool { + return { + name: BASH_TOOL_NAME, + inputSchema: z.object({ command: z.string() }), + checkPermissions: async () => ({ + behavior: 'passthrough' as const, + message: 'Confirm command', + }), + } as unknown as Tool + } + + test('auto-allows an unresolved prompt below the fixed policy threshold', async () => { + globalThis.fetch = mock(async () => Response.json({ + answers: [{ type: 'predicate', name: 'ask', probability: 0.49 }], + })) as unknown as typeof fetch + + const result = await hasPermissionsToUseTool( + unresolvedTool(), + { command: 'ls' }, + context('auto'), + {} as AssistantMessage, + 'tool-1', + ) + + expect(result).toEqual({ + behavior: 'allow', + updatedInput: { command: 'ls' }, + decisionReason: { + type: 'autoPermission', + providerId: 'decision-provider', + modelId: 'gpt-6-luna', + source: 'decisionProvider', + decision: 'allow', + score: 0.49, + policyVersion: 1, + reason: undefined, + }, + }) + }) + + test('keeps threshold, errors, and malformed responses as confirmation', async () => { + for (const response of [ + Response.json({ answers: [{ type: 'predicate', name: 'ask', probability: 0.5 }] }), + Response.json({ answers: [] }), + new Response('failure', { status: 500 }), + ]) { + globalThis.fetch = mock(async () => response) as unknown as typeof fetch + const result = await hasPermissionsToUseTool( + unresolvedTool(), + { command: 'rm file.txt' }, + context('auto'), + {} as AssistantMessage, + 'tool-ask', + ) + expect(result.behavior).toBe('ask') + expect(result.decisionReason).toMatchObject({ + type: 'autoPermission', + decision: 'ask', + policyVersion: 1, + }) + } + }) + + test('does not invoke a decision model outside Auto mode', async () => { + let requests = 0 + globalThis.fetch = mock(async () => { + requests += 1 + return Response.json({ + answers: [{ type: 'predicate', name: 'ask', probability: 0 }], + }) + }) as unknown as typeof fetch + + for (const mode of ['default', 'acceptEdits', 'plan', 'bypassPermissions'] as const) { + await hasPermissionsToUseTool( + unresolvedTool(), + { command: 'ls' }, + context(mode), + {} as AssistantMessage, + `tool-${mode}`, + ) + } + + expect(requests).toBe(0) + }) + + test.each([ + [BASH_TOOL_NAME, 'rm guardrail-deny-rule.txt', bashToolHasPermission], + [POWERSHELL_TOOL_NAME, 'Remove-Item guardrail-deny-rule.txt', powershellToolHasPermission], + ] as const)('keeps %s deny rules ahead of Auto and bypass', async (name, command, checkPermissions) => { + let requests = 0 + globalThis.fetch = mock(async () => { + requests += 1 + return Response.json({ + answers: [{ type: 'predicate', name: 'ask', probability: 0 }], + }) + }) as unknown as typeof fetch + + const inputSchema = z.object({ command: z.string() }) + const denyRules = [ + 'Bash(*guardrail-deny-rule.txt*)', + 'PowerShell(*guardrail-deny-rule.txt*)', + ] + + for (const mode of ['auto', 'bypassPermissions'] as const) { + const tool = { name, inputSchema, checkPermissions } as unknown as Tool + const result = await hasPermissionsToUseTool( + tool, + { command }, + context(mode, denyRules), + {} as AssistantMessage, + `tool-${mode}-${name}`, + ) + + expect(result.behavior).toBe('deny') + expect(result.decisionReason).toMatchObject({ + type: 'rule', + rule: { + ruleBehavior: 'deny', + ruleValue: { toolName: name }, + }, + }) + } + + expect(requests).toBe(0) + }, 10_000) + + test('loads Bash and PowerShell deny rules from project local settings', async () => { + const projectDir = join(configDir, 'project') + mkdirSync(join(projectDir, '.claude'), { recursive: true }) + writeFileSync(join(projectDir, '.claude', 'settings.local.json'), JSON.stringify({ + permissions: { + deny: [ + 'Bash(*guardrail-hard-deny.txt*)', + 'PowerShell(*guardrail-hard-deny.txt*)', + ], + }, + })) + setOriginalCwd(projectDir) + resetSettingsCache() + + const initialized = await initializeToolPermissionContext({ + allowedToolsCli: [], + disallowedToolsCli: [], + permissionMode: 'auto', + allowDangerouslySkipPermissions: false, + addDirs: [], + }) + const shellTools = [ + { + name: BASH_TOOL_NAME, + command: 'rm guardrail-hard-deny.txt', + checkPermissions: bashToolHasPermission, + }, + { + name: POWERSHELL_TOOL_NAME, + command: 'Remove-Item guardrail-hard-deny.txt', + checkPermissions: powershellToolHasPermission, + }, + ] + + for (const shellTool of shellTools) { + const result = await hasPermissionsToUseTool( + { + name: shellTool.name, + inputSchema: z.object({ command: z.string() }), + checkPermissions: shellTool.checkPermissions, + } as unknown as Tool, + { command: shellTool.command }, + context('auto', [], initialized.toolPermissionContext), + {} as AssistantMessage, + `tool-loaded-${shellTool.name}`, + ) + + expect(result.behavior).toBe('deny') + expect(result.decisionReason).toMatchObject({ + type: 'rule', + rule: { + source: 'localSettings', + ruleBehavior: 'deny', + ruleValue: { toolName: shellTool.name }, + }, + }) + } + }) +}) diff --git a/src/utils/permissions/permissions.ts b/src/utils/permissions/permissions.ts index 24b46cd4..8be4dfa0 100644 --- a/src/utils/permissions/permissions.ts +++ b/src/utils/permissions/permissions.ts @@ -106,6 +106,7 @@ import { classifyYoloAction, formatActionForClassifier, } from './yoloClassifier.js' +import { evaluateAutoPermission } from './permissionGuardrail.js' const CLASSIFIER_FAIL_CLOSED_REFRESH_MS = 30 * 60 * 1000 // 30 minutes @@ -482,12 +483,12 @@ export const hasPermissionsToUseTool: CanUseToolFn = async ( ): Promise => { const result = await hasPermissionsToUseToolInner(tool, input, context) + const appState = context.getAppState() // 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 @@ -506,8 +507,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', @@ -518,13 +517,48 @@ export const hasPermissionsToUseTool: CanUseToolFn = async ( message: DONT_ASK_REJECT_MESSAGE(tool.name), } } + if (appState.toolPermissionContext.mode === 'auto') { + if ( + result.decisionReason?.type === 'safetyCheck' && + !result.decisionReason.classifierApprovable + ) { + return result + } + if (tool.requiresUserInteraction?.()) return result + + const evaluation = await evaluateAutoPermission({ + toolName: tool.name, + input, + messages: context.messages, + signal: context.abortController.signal, + }) + if (evaluation) { + const decisionReason = { + type: 'autoPermission' as const, + providerId: evaluation.providerId, + modelId: evaluation.modelId, + source: evaluation.source, + decision: evaluation.behavior, + score: evaluation.score, + policyVersion: evaluation.policyVersion, + reason: evaluation.reason, + } + 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 ( feature('TRANSCRIPT_CLASSIFIER') && - (appState.toolPermissionContext.mode === 'auto' || - (appState.toolPermissionContext.mode === 'plan' && - (autoModeStateModule?.isAutoModeActive() ?? false))) + appState.toolPermissionContext.mode === 'plan' && + (autoModeStateModule?.isAutoModeActive() ?? false) ) { // Non-classifier-approvable safetyCheck decisions stay immune to ALL // auto-approve paths: the acceptEdits fast-path, the safe-tool allowlist, diff --git a/src/utils/sessionStorage.loggable.test.ts b/src/utils/sessionStorage.loggable.test.ts index ba9e5e08..814c706a 100644 --- a/src/utils/sessionStorage.loggable.test.ts +++ b/src/utils/sessionStorage.loggable.test.ts @@ -10,6 +10,11 @@ describe('isLoggableMessage', () => { expect(isLoggableMessage(attachment('queued_command'))).toBe(true) }) + test('keeps permission guardrail decisions with queued messages', () => { + expect(isLoggableMessage(attachment('permission_guardrail_decision'))).toBe(true) + expect(isLoggableMessage(attachment('queued_command'))).toBe(true) + }) + test('still drops other attachments', () => { expect(isLoggableMessage(attachment('date_change'))).toBe(false) }) diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index 4939f05e..27f49c24 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -4410,6 +4410,7 @@ 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 === 'queued_command') return true if ( m.attachment.type === 'hook_additional_context' && diff --git a/src/utils/settings/settings.ts b/src/utils/settings/settings.ts index 3bea04af..5df4ed1c 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 { + AutoPermissionSettingsSchema, + type AutoPermissionSettings, +} from '../../types/permissionGuardrail.js' import { filterInvalidPermissionRules, formatZodError, @@ -927,6 +931,19 @@ export function getUseAutoModeDuringPlan(): boolean { return true } +export function getAutoPermissionSettings(): AutoPermissionSettings | undefined { + for (const source of [ + 'policySettings', + 'flagSettings', + 'userSettings', + ] as const) { + const value = getSettingsForSource(source)?.autoPermission + const result = AutoPermissionSettingsSchema.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..54d8e950 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 { AutoPermissionSettingsSchema } 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.', ), + autoPermission: AutoPermissionSettingsSchema.optional(), env: EnvironmentVariablesSchema() .optional() .describe('Environment variables to set for Claude Code sessions'), diff --git a/src/utils/settings/validationTips.ts b/src/utils/settings/validationTips.ts index 716fbfb4..6023d76c 100644 --- a/src/utils/settings/validationTips.ts +++ b/src/utils/settings/validationTips.ts @@ -31,7 +31,7 @@ const TIP_MATCHERS: TipMatcher[] = [ ctx.path === 'permissions.defaultMode' && ctx.code === 'invalid_value', tip: { suggestion: - 'Valid modes: "acceptEdits" (ask before file changes), "plan" (analysis only), "bypassPermissions" (auto-accept all), or "default" (standard behavior)', + 'Valid modes: "acceptEdits" (ask before file changes), "plan" (analysis only), "auto" (model-assisted unresolved prompts), "bypassPermissions" (auto-accept all), or "default" (standard behavior)', docLink: `${DOCUMENTATION_BASE}/iam#permission-modes`, }, },