/** * @license * Copyright 2026 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useState } from 'react'; import { Box, Text } from 'ink'; import { PolicyDecision, MAX_USER_PRIORITY, type PolicyRule, type EditablePolicyRule, } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; import { theme } from '../semantic-colors.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; import { TextInput } from './ProfileCreateWizard/TextInput.js'; import { useKeypress } from '../hooks/useKeypress.js'; const DECISION_LABELS: Record = { [PolicyDecision.ALLOW]: 'allow', [PolicyDecision.DENY]: 'deny', [PolicyDecision.ASK_USER]: 'ask_user', }; const DECISION_VALUES = [ PolicyDecision.ALLOW, PolicyDecision.DENY, PolicyDecision.ASK_USER, ] as const; const TIER_ORDER = [ { min: 3.0, label: 'Tier 3 (System / Admin) — read-only' }, { min: 2.0, label: 'Tier 2 (User-defined) — editable' }, { min: 1.0, label: 'Tier 1 (Defaults) — read-only' }, { min: 0.0, label: 'Tier 0 (Base)' }, ]; export function tierLabel(priority: number): string { for (const tier of TIER_ORDER) { if (priority >= tier.min) { return tier.label; } } return TIER_ORDER[TIER_ORDER.length - 1].label; } export function formatToolName(rule: { toolName?: string }): string { return rule.toolName !== undefined && rule.toolName.length > 0 ? rule.toolName : '*'; } export function formatDecision(decision: PolicyDecision): string { return DECISION_LABELS[decision]; } export function ruleSummary(rule: EditablePolicyRule): string { const pattern = rule.argsPattern ? ` (pattern: ${rule.argsPattern})` : ''; return `${formatToolName(rule)} \u2192 ${formatDecision(rule.decision)}${pattern} [priority ${rule.priority}]`; } export function engineRuleSummary(rule: PolicyRule): string { const priority = rule.priority ?? 0; const pattern = rule.argsPattern ? ` (pattern: ${rule.argsPattern.source})` : ''; const source = rule.source ? ` [${rule.source}]` : ''; return `${formatToolName(rule)} \u2192 ${formatDecision(rule.decision)}${pattern} [${priority.toFixed(3)}]${source}`; } export function parsePriority(value: string): number | null { const trimmed = value.trim(); if (!/^\d+$/.test(trimmed)) { return null; } const n = Number(trimmed); if (n > MAX_USER_PRIORITY) { return null; } return n; } function groupByTier(rules: readonly PolicyRule[]): Map { const groups = new Map(); for (const rule of rules) { const label = tierLabel(rule.priority ?? 0); const arr = groups.get(label) ?? []; arr.push(rule); groups.set(label, arr); } return groups; } export interface PolicyFormProps { initial: EditablePolicyRule; title: string; onSubmit: (rule: EditablePolicyRule) => void; onCancel: () => void; } function ToolNameStep({ toolName, setToolName, onContinue, }: { toolName: string; setToolName: (v: string) => void; onContinue: () => void; }) { return ( Tool name (or empty for wildcard *): (Enter to continue) ); } function ArgsPatternStep({ argsPattern, setArgsPattern, onContinue, }: { argsPattern: string; setArgsPattern: (v: string) => void; onContinue: () => void; }) { return ( Args regex (optional, matches the JSON tool args): (Enter to continue) ); } function PriorityStep({ priorityStr, setPriorityStr, error, onSubmit, }: { priorityStr: string; setPriorityStr: (v: string) => void; error: string | null; onSubmit: () => void; }) { return ( Priority (0-{MAX_USER_PRIORITY}, higher wins within the tier): { setPriorityStr(v); }} onSubmit={onSubmit} isFocused={true} /> {error !== null && {error}} (Enter to save, Esc to go back) ); } interface PolicyFormState { step: number; toolName: string; decision: PolicyDecision; argsPattern: string; priorityStr: string; error: string | null; } function useFormState( initial: EditablePolicyRule, onSubmit: (rule: EditablePolicyRule) => void, ): PolicyFormState & { setStep: (n: number) => void; setToolName: (v: string) => void; setArgsPattern: (v: string) => void; setPriorityStr: (v: string) => void; setDecision: (d: PolicyDecision) => void; handleDecisionSelect: (d: PolicyDecision) => void; handleSubmit: () => void; } { const [step, setStep] = useState(0); const [toolName, setToolName] = useState(initial.toolName); const [decision, setDecision] = useState(initial.decision); const [argsPattern, setArgsPattern] = useState(initial.argsPattern ?? ''); const [priorityStr, setPriorityStr] = useState(String(initial.priority)); const [error, setError] = useState(null); const handleDecisionSelect = useCallback( (value: PolicyDecision) => { setDecision(value); setStep(2); }, [setDecision, setStep], ); const handleSubmit = useCallback(() => { const priority = parsePriority(priorityStr); if (priority === null) { setError( `Priority must be an integer between 0 and ${MAX_USER_PRIORITY}.`, ); setStep(3); return; } const trimmedPattern = argsPattern.trim(); if (trimmedPattern) { try { new RegExp(trimmedPattern); } catch { setError('Invalid regular expression pattern.'); setStep(2); return; } } onSubmit({ toolName: toolName.trim(), decision, priority, ...(trimmedPattern ? { argsPattern: trimmedPattern } : {}), }); }, [argsPattern, decision, onSubmit, priorityStr, toolName]); return { step, toolName, decision, argsPattern, priorityStr, error, setStep, setToolName, setArgsPattern, setPriorityStr: (v: string) => { setPriorityStr(v); setError(null); }, setDecision, handleDecisionSelect, handleSubmit, }; } export const PolicyForm: React.FC = ({ initial, title, onSubmit, onCancel, }) => { const s = useFormState(initial, onSubmit); const decisionItems: Array> = DECISION_VALUES.map((d) => ({ key: d, label: formatDecision(d), value: d, })); useKeypress( (key) => { if (key.name !== 'escape') return; if (s.step > 0) { s.setStep(s.step - 1); } else { onCancel(); } }, { isActive: true }, ); return ( {title} {s.step === 0 && ( s.setStep(1)} /> )} {s.step === 1 && ( Decision: )} {s.step === 2 && ( s.setStep(3)} /> )} {s.step === 3 && ( )} {s.step > 0 && ( (Esc to go back) )} ); }; export const PolicyStackView: React.FC<{ rules: readonly PolicyRule[] }> = ({ rules, }) => { const groups = groupByTier(rules); return ( {TIER_ORDER.map((tier) => { const tierRules = groups.get(tier.label); if (tierRules === undefined || tierRules.length === 0) { return null; } return ( {tier.label} {tierRules.map((rule, i) => ( {' '} {engineRuleSummary(rule)} ))} ); })} ); }; export interface MenuItem { key: string; label: string; value: string; } export function buildMenuItems( rules: readonly EditablePolicyRule[], ): MenuItem[] { return [ { key: 'add', label: '[+] Add new rule', value: '__add__' }, ...rules.map((r, i) => ({ key: `rule-${i}`, label: ruleSummary(r), value: String(i), })), { key: 'stack', label: '[i] View active stack', value: '__stack__' }, { key: 'close', label: '[x] Close', value: '__close__' }, ]; } export const ACTION_ITEMS: MenuItem[] = [ { key: 'edit', label: 'Edit', value: 'edit' }, { key: 'delete', label: 'Delete', value: 'delete' }, { key: 'duplicate', label: 'Duplicate', value: 'duplicate' }, { key: 'back', label: 'Back', value: 'back' }, ];