llmgw ctl guard judge status lists each judge model the policies ask: in the catalog, as a judge, replicas ready, its last failure to answer, and what is wrong in words. enable serves the missing one through model serve - Llama Guard 3 1B on CPUs, role guard, under the name the policies give it - with --replace for a model of that name that is not a judge; the console has it under Do. GET /api/v1/guard/judges, and the summary's judges, put the problem on ctl status and the console's Overview. A Llama Guard file served from the mirror page is added as a judge, under the name the policies ask, rather than as a model clients use the policies never find. ctl says plainly when the API answers with a page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
145 lines
4.5 KiB
Go
145 lines
4.5 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/guard"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// GuardAdminSource is what the guardrails page reads.
|
|
type GuardAdminSource interface {
|
|
GuardEvents(ctx context.Context, since time.Time, limit int) ([]store.GuardEvent, error)
|
|
GuardCounts(ctx context.Context, since time.Time) ([]store.GuardCount, error)
|
|
GuardAttachments(ctx context.Context) ([]guard.Attachment, error)
|
|
AllProjects(ctx context.Context) ([]store.ProjectPath, error)
|
|
}
|
|
|
|
type guardPageData struct {
|
|
Nav Nav
|
|
Version string
|
|
Window string
|
|
Windows []string
|
|
// Policies are the library, Default those applied to every request.
|
|
Policies []string
|
|
Default []string
|
|
Attachments []guardAttachmentRow
|
|
Counts []guardCountRow
|
|
Events []guardEventRow
|
|
Error string
|
|
}
|
|
|
|
type guardAttachmentRow struct{ Scope, Who, Policy string }
|
|
|
|
type guardCountRow struct {
|
|
Where, Action, Policy, Category string
|
|
Count int64
|
|
}
|
|
|
|
type guardEventRow struct {
|
|
At, RequestID, Where, Model, Action, Policy, Category string
|
|
Status int
|
|
Ms int64
|
|
}
|
|
|
|
var guardWindows = map[string]time.Duration{"24h": 24 * time.Hour, "7d": 7 * 24 * time.Hour, "30d": 30 * 24 * time.Hour}
|
|
|
|
// RegisterAdminGuard mounts /admin/guard: what the guardrails did, where, by
|
|
// which policy — names and counts, never text.
|
|
func (p *AccountPages) RegisterAdminGuard(mux *http.ServeMux, s GuardAdminSource, library func() guard.Config) {
|
|
p.hasGuard, p.guardLibrary = true, library
|
|
mux.HandleFunc("GET /admin/guard", p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) {
|
|
ctx, l := r.Context(), langOf(r)
|
|
lib := library()
|
|
d := guardPageData{Nav: p.navFor(r, admin, "guard"), Version: p.version, Window: r.URL.Query().Get("window"),
|
|
Windows: []string{"24h", "7d", "30d"}, Default: lib.Default}
|
|
since, ok := guardWindows[d.Window]
|
|
if !ok {
|
|
d.Window, since = "7d", guardWindows["7d"]
|
|
}
|
|
for name := range lib.Policies {
|
|
d.Policies = append(d.Policies, name)
|
|
}
|
|
slices.Sort(d.Policies)
|
|
|
|
where := map[string]string{"": l.T("guard.personal")}
|
|
if projects, err := s.AllProjects(ctx); err == nil {
|
|
for _, pp := range projects {
|
|
where[pp.ID] = pp.Path
|
|
}
|
|
}
|
|
name := func(id string) string {
|
|
if n, ok := where[id]; ok {
|
|
return n
|
|
}
|
|
return id
|
|
}
|
|
if att, err := s.GuardAttachments(ctx); err == nil {
|
|
for _, a := range att {
|
|
who := a.ScopeID
|
|
if a.Scope == "project" {
|
|
who = name(a.ScopeID)
|
|
}
|
|
d.Attachments = append(d.Attachments, guardAttachmentRow{Scope: guardScopeWord(l, a.Scope), Who: who, Policy: a.Policy})
|
|
}
|
|
}
|
|
counts, err := s.GuardCounts(ctx, time.Now().Add(-since))
|
|
if err != nil {
|
|
d.Error = l.Err(err)
|
|
}
|
|
for _, c := range counts {
|
|
d.Counts = append(d.Counts, guardCountRow{Where: name(c.ProjectID), Action: guardActionWord(l, c.Action),
|
|
Policy: c.Policy, Category: c.Category, Count: c.Count})
|
|
}
|
|
events, err := s.GuardEvents(ctx, time.Now().Add(-since), 100)
|
|
if err != nil {
|
|
d.Error = l.Err(err)
|
|
}
|
|
for _, e := range events {
|
|
// The day as the reader writes one; the seconds stay, because
|
|
// events a second apart are what this table tells apart.
|
|
at := e.At.UTC()
|
|
d.Events = append(d.Events, guardEventRow{At: l.Date(at) + " " + at.Format("15:04:05"),
|
|
RequestID: e.RequestID, Where: name(e.ProjectID), Model: e.Model, Action: guardActionWord(l, e.Action),
|
|
Policy: e.Policy, Category: e.Category, Status: e.Status, Ms: e.GuardMs})
|
|
}
|
|
noStore(w)
|
|
render(w, r, "admin_guard", d)
|
|
}))
|
|
}
|
|
|
|
// guardScopeWord names the kind of scope a policy is attached to in the
|
|
// reader's language. A kind this page does not know is shown as stored.
|
|
func guardScopeWord(l *Lang, scope string) string {
|
|
switch scope {
|
|
case "org":
|
|
return l.T("guard.scope.org")
|
|
case "team":
|
|
return l.T("guard.scope.team")
|
|
case "project":
|
|
return l.T("guard.scope.project")
|
|
case "user":
|
|
return l.T("guard.scope.user")
|
|
case "key":
|
|
return l.T("guard.scope.key")
|
|
}
|
|
return scope
|
|
}
|
|
|
|
// guardActionWord is what a policy did, in the reader's language. The stored
|
|
// value stays the English word the events are recorded and queried by.
|
|
func guardActionWord(l *Lang, action string) string {
|
|
switch action {
|
|
case "flagged":
|
|
return l.T("guard.action.flagged")
|
|
case "redacted":
|
|
return l.T("guard.action.redacted")
|
|
case "blocked":
|
|
return l.T("guard.action.blocked")
|
|
}
|
|
return action
|
|
}
|