admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / frontend / src / playbook / GuardrailPanel.tsx
7746 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | import { useState } from 'react' import type { ApprovalLevel } from '../api/types' function TagList({ values, onChange, placeholder, accentClass, }: { values: string[] onChange: (next: string[]) => void placeholder: string accentClass: string }) { const [draft, setDraft] = useState('') const commit = () => { const value = draft.trim() if (value && !values.includes(value)) { onChange([...values, value]) } setDraft('') } return ( <div> <div className="flex flex-wrap gap-1.5 mb-2"> {values.map((v) => ( <span key={v} className={`inline-flex items-center gap-1 text-[10px] font-mono px-2 py-0.5 rounded border ${accentClass}`} > {v} <button onClick={() => onChange(values.filter((x) => x !== v))} className="hover:text-accent-red"> × </button> </span> ))} {values.length === 0 && <span className="text-[10px] text-cortex-muted italic">None configured</span>} </div> <input value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault() commit() } }} onBlur={commit} placeholder={placeholder} className="w-full bg-cortex-panel2 border border-cortex-border rounded px-2 py-1.5 text-xs focus:outline-none focus:border-accent-blue" /> </div> ) } /** Review-eligible baseline categories a global admin may acknowledge on a * playbook, mirroring app/ai/guardrails.py REVIEW_ELIGIBLE_BASELINE. */ const ACKNOWLEDGEABLE_CATEGORIES: { id: string; label: string; hint: string }[] = [ { id: 'host_power', label: 'Host power control', hint: 'reboot / shutdown / halt / poweroff. Blocked by default; acknowledging routes it to mandatory human review instead.', }, ] export function GuardrailPanel({ allowedTargetOs, onAllowedTargetOsChange, approvalLevel, onApprovalLevelChange, forbiddenCommands, onForbiddenCommandsChange, canSetAutoApprove, acknowledgedDangerousCommands, onAcknowledgedDangerousCommandsChange, canAcknowledge, }: { allowedTargetOs: string[] onAllowedTargetOsChange: (v: string[]) => void approvalLevel: ApprovalLevel onApprovalLevelChange: (v: ApprovalLevel) => void forbiddenCommands: string[] onForbiddenCommandsChange: (v: string[]) => void canSetAutoApprove: boolean acknowledgedDangerousCommands: string[] onAcknowledgedDangerousCommandsChange: (v: string[]) => void canAcknowledge: boolean }) { const toggleAck = (id: string, on: boolean) => { if (on) { if (!acknowledgedDangerousCommands.includes(id)) onAcknowledgedDangerousCommandsChange([...acknowledgedDangerousCommands, id]) } else { onAcknowledgedDangerousCommandsChange(acknowledgedDangerousCommands.filter((x) => x !== id)) } } return ( <aside className="w-72 shrink-0 border-l border-cortex-border bg-cortex-panel overflow-y-auto p-4 space-y-6"> <div> <div className="text-[10px] text-accent-blue uppercase tracking-widest font-display mb-3">Guardrails</div> <div className="text-[9px] text-cortex-muted leading-tight"> Enforced server-side before any command reaches the executor - the graph content can never bypass these. </div> </div> <div> <div className="text-[10px] text-chrome uppercase tracking-wider mb-2">Allowed Target OS</div> <TagList values={allowedTargetOs} onChange={onAllowedTargetOsChange} placeholder="e.g. linux, windows" accentClass="border-accent-blue/30 bg-accent-blue/10 text-accent-blue" /> <div className="text-[9px] text-cortex-muted mt-1.5 leading-tight"> Empty = no restriction. Matches the linked asset's OS as a case-insensitive substring. </div> </div> <div> <div className="text-[10px] text-chrome uppercase tracking-wider mb-2">Required Approval Level</div> <label className="flex items-start gap-2 mb-2 cursor-pointer"> <input type="radio" name="approval_level" checked={approvalLevel === 'human_in_the_loop'} onChange={() => onApprovalLevelChange('human_in_the_loop')} className="mt-0.5" /> <span className="text-xs text-chrome"> Human-in-the-Loop <span className="block text-[9px] text-cortex-muted">Always requires an explicit Approve click.</span> </span> </label> <label className={`flex items-start gap-2 cursor-pointer ${canSetAutoApprove ? '' : 'opacity-50'}`}> <input type="radio" name="approval_level" checked={approvalLevel === 'auto_approve'} disabled={!canSetAutoApprove} onChange={() => onApprovalLevelChange('auto_approve')} className="mt-0.5" /> <span className="text-xs text-chrome"> Auto-Approve <span className="block text-[9px] text-cortex-muted"> {canSetAutoApprove ? 'Executes immediately once selected. Still posts the proposal and outcome to Actions Taken.' : 'Only a global admin can set this, and only when the deployment has AUTO_APPROVE enabled.'} </span> </span> </label> </div> <div> <div className="text-[10px] text-chrome uppercase tracking-wider mb-2">Forbidden Command Strings</div> <TagList values={forbiddenCommands} onChange={onForbiddenCommandsChange} placeholder="e.g. deploy_hook" accentClass="border-accent-red/30 bg-accent-red/10 text-accent-red" /> <div className="text-[9px] text-cortex-muted mt-1.5 leading-tight"> Checked in addition to a hardcoded baseline (rm -rf, mkfs, shutdown, ...) that no tenant config can weaken. </div> </div> <div> <div className="text-[10px] text-accent-orange uppercase tracking-wider mb-2"> Dangerous Command Review </div> <div className="text-[9px] text-cortex-muted mb-2 leading-tight"> Some baseline commands are blocked outright. A few reversible-but-disruptive ones can instead be <span className="text-chrome"> acknowledged</span> here, which lets this playbook reach a human for review rather than being auto-blocked. It can never run unattended, and destructive commands (rm -rf, mkfs, dd to a device) can never be acknowledged. </div> <div className="space-y-2"> {ACKNOWLEDGEABLE_CATEGORIES.map((cat) => ( <label key={cat.id} className={`flex items-start gap-2 ${canAcknowledge ? 'cursor-pointer' : 'opacity-50 cursor-not-allowed'}`} > <input type="checkbox" checked={acknowledgedDangerousCommands.includes(cat.id)} disabled={!canAcknowledge} onChange={(e) => toggleAck(cat.id, e.target.checked)} className="mt-0.5" /> <span className="text-xs text-chrome"> {cat.label} <span className="block text-[9px] text-cortex-muted leading-tight"> {canAcknowledge ? cat.hint : 'Only a global admin can acknowledge a dangerous-command category.'} </span> </span> </label> ))} </div> </div> </aside> ) } |