package cli import ( "context" "fmt" "net/http" "strings" "time" tea "github.com/charmbracelet/bubbletea" "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" ) // The guardrails, as the console shows them: the policies server.yaml // defines and what each checks, where they are attached, whether the judge // models they lean on are serving, and what they did in a window. // guardWindows are the windows W steps through. var guardWindows = []string{"24h", "7d", "1h"} type guardState struct { view adminapi.GuardView events adminapi.GuardEventsView err error // window is an index into guardWindows. window int } type consoleGuardMsg struct { view adminapi.GuardView events adminapi.GuardEventsView err error } func (m consoleModel) fetchGuard() tea.Cmd { c, since := m.client, guardWindows[m.guard.window] return func() tea.Msg { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var msg consoleGuardMsg if msg.err = c.get(ctx, "/api/v1/guard", &msg.view); msg.err != nil { return msg } // What they did is a second question; a store without the record // still shows the policies. _ = c.get(ctx, "/api/v1/guard/events?since="+since, &msg.events) return msg } } func (m consoleModel) guardPanel(pw int) (string, string, string) { keys := "→ or Enter open · ↑↓ choose · Q leave" if m.inPanel { keys = "↑↓ attachment · Enter actions · A attach · W window · ← back · Q leave" } g := m.guard var b strings.Builder b.WriteString("\n " + sDim.Render("What requests and answers are checked for, where, and what that did. A to attach a policy.") + "\n\n") if g.err != nil { if strings.Contains(g.err.Error(), "501") || strings.Contains(g.err.Error(), "no guard policies") { b.WriteString(" " + sDim.Render("No policies: define them under guard: in server.yaml (Settings), then restart.")) } else { b.WriteString(" " + sBad.Render(g.err.Error())) } return "Guardrails", b.String(), keys } if len(g.view.Details) == 0 && len(g.view.Policies) == 0 { b.WriteString(" " + sDim.Render("asking the control plane…")) return "Guardrails", b.String(), keys } def := "none" if len(g.view.Default) > 0 { def = strings.Join(g.view.Default, ", ") } b.WriteString(" " + sTitle.Render("POLICIES") + " " + sDim.Render("on every request: "+def) + "\n") for _, p := range g.view.Details { line := fmt.Sprintf(" %-14s %s · fail %s", p.Name, p.Mode, p.Fail) if p.NoStore { line += " · keeps nothing" } if p.Judge != "" { line += " · judge " + p.Judge + " " + m.judgeState(p.Judge) } b.WriteString(line + "\n") for _, c := range p.Checks { b.WriteString(" " + strings.Repeat(" ", 15) + sDim.Render(c) + "\n") } } b.WriteString("\n " + sTitle.Render("ATTACHED") + " " + sDim.Render("besides the default, to part of the tenancy tree, a person or a key") + "\n") if len(g.view.Attachments) == 0 { b.WriteString(" " + sDim.Render("Nowhere. A to attach one.") + "\n") } else { var rows [][]string for _, a := range g.view.Attachments { who := a.Name if who == "" { who = a.ScopeID } rows = append(rows, []string{a.Scope, who, a.Policy}) } b.WriteString(table([]string{"SCOPE", "WHO", "POLICY"}, rows, m.row, pw, m.inPanel) + "\n") } window := guardWindows[g.window] b.WriteString("\n " + sTitle.Render("IN THE LAST "+strings.ToUpper(window)) + " " + sDim.Render("W for another window") + "\n") if len(g.events.Counts) == 0 { b.WriteString(" " + sDim.Render("Nothing flagged, redacted or blocked.") + "\n") } else { var rows [][]string for i, c := range g.events.Counts { if i == 10 { break } rows = append(rows, []string{c.Action, c.Policy, dash(c.Category), dash(c.Project), fmt.Sprint(c.Count)}) } b.WriteString(table([]string{"ACTION", "POLICY", "CATEGORY", "PROJECT", "REQUESTS"}, rows, -1, pw, false) + "\n") } if len(g.events.Events) > 0 { b.WriteString("\n " + sTitle.Render("NEWEST") + "\n") var rows [][]string for i, e := range g.events.Events { if i == 10 { break } rows = append(rows, []string{e.At.Local().Format("01-02 15:04:05"), e.Action, e.Policy, dash(e.Category), e.Model, fmt.Sprint(e.Status), fmt.Sprintf("%dms", e.GuardMs), e.RequestID}) } b.WriteString(table([]string{"AT", "ACTION", "POLICY", "CATEGORY", "MODEL", "STATUS", "GUARD", "REQUEST"}, rows, -1, pw, false)) } return "Guardrails", b.String(), keys } // judgeState is how many of a judge model's replicas are ready: a policy // that fails closed refuses everything while it has none. func (m consoleModel) judgeState(model string) string { for _, md := range m.data.models { if md.Name == model { s := fmt.Sprintf("(%d/%d ready)", md.Ready, md.Desired) if md.Ready == 0 { return sBad.Render(s) } return sOK.Render(s) } } return sBad.Render("(not in the catalog)") } // guardAttachForm attaches a policy. func (m *consoleModel) guardAttachForm() { if len(m.guard.view.Policies) == 0 { return } m.openForm(&formDialog{title: "Attach a guardrail policy", note: "Requests from there are checked by it as well as by the default.", fields: []formField{ choiceF("to", []string{"project", "team", "org", "user", "key"}, "project"), textF("which", "", "ORG/TEAM/PROJECT, ORG/TEAM, ORG, or the ID"), choiceF("policy", m.guard.view.Policies, m.guard.view.Policies[0]), }, submit: func(m *consoleModel, v []string) (tea.Cmd, error) { if strings.TrimSpace(v[1]) == "" { return nil, fmt.Errorf("which %s?", v[0]) } req := adminapi.GuardAttachmentRequest{Target: v[0] + ":" + strings.TrimSpace(v[1]), Policy: v[2]} return m.call("Attach "+v[2], http.MethodPost, "/api/v1/guard/attachments", req, nil), nil }}) } // guardAttachmentMenu is what can be done to one attachment. func (m consoleModel) guardAttachmentMenu(a adminapi.GuardAttachmentView) *menuDialog { who := a.Name if who == "" { who = a.ScopeID } return &menuDialog{title: a.Policy + " on " + a.Scope + " " + who, items: []menuItem{ {"Detach…", func(m *consoleModel) tea.Cmd { m.confirmD = &confirmDialog{title: "Detach " + a.Policy, text: "Requests from " + who + " are then checked by the default policies alone.", yes: func(m *consoleModel) tea.Cmd { req := adminapi.GuardAttachmentRequest{Target: a.Scope + ":" + a.ScopeID, Policy: a.Policy} return m.call("Detach "+a.Policy, http.MethodDelete, "/api/v1/guard/attachments", req, nil) }} return nil }}, }} }