guard.enabled (unset is on) makes every policy check nothing while false, live: llmgw ctl guard off/on set it in the cluster's settings, and ctl status, ctl guard list and the guardrails page say when they are off. A refusal by a judge names the hazard it saw - judge: S6,Specialized Advice - and the verdict is kept with the request in a new usage column, guard_rule, shown by ctl request, the request log and the guardrails page. llmgw ctl guard judge ask has a judge read a text, past the cache, and shows its raw answer and how it is read; the endpoint is kept out of the audit so no text is recorded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/settings"
|
|
)
|
|
|
|
func newGuardCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
|
|
c := &cobra.Command{
|
|
Use: "guard",
|
|
Short: "Guardrail policies: which apply where",
|
|
Long: "Policies are defined in the cluster's settings under guard.policies\n" +
|
|
"(llmgw ctl config edit; in force everywhere within seconds) — what requests and\n" +
|
|
"answers are checked for (personal data, secrets, prompt injection, patterns,\n" +
|
|
"tools) and what is done: flag, redact or block. guard.default applies to\n" +
|
|
"every request; attach others to an organization, team, project, user or\n" +
|
|
"key, and everything below is judged by them too. Policies narrow: one\n" +
|
|
"attached lower can tighten, never loosen, what applies above it.\n\n" +
|
|
"Targets are written org:Acme, team:Acme/ml, project:Acme/ml/training (or\n" +
|
|
"with IDs), key:KEY-ID or user:USER-ID. An attachment applies at once.",
|
|
}
|
|
c.AddCommand(newGuardJudgeCmd(client, asJSON))
|
|
for _, sw := range []struct {
|
|
use, short string
|
|
on bool
|
|
}{
|
|
{"on", "Turn the guardrails on: every policy checks again", true},
|
|
{"off", "Turn the guardrails off: no request is checked, whatever the policies say", false},
|
|
} {
|
|
var note string
|
|
cmd := &cobra.Command{
|
|
Use: sw.use,
|
|
Short: sw.short,
|
|
Long: "guard.enabled in the cluster's settings: in force on every control plane\n" +
|
|
"within seconds, without a restart. The policies and where they are attached\n" +
|
|
"stay as they are, and come back as they were when turned on again. While\n" +
|
|
"off, llmgw ctl status says so.",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
if note == "" {
|
|
note = "guardrails " + sw.use
|
|
}
|
|
return changeSettings(cmd, client, note, func(doc *settings.Doc) error {
|
|
return doc.SetYAML([]string{"guard", "enabled"}, strconv.FormatBool(sw.on))
|
|
})
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(¬e, "note", "", "why, kept with the settings' version")
|
|
c.AddCommand(cmd)
|
|
}
|
|
c.AddCommand(&cobra.Command{
|
|
Use: "list",
|
|
Short: "The policies, and where each is attached",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
var v adminapi.GuardView
|
|
if err := client().get(cmd.Context(), "/api/v1/guard", &v); err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(v)
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
if !v.Enabled {
|
|
fmt.Fprintln(out, "The guardrails are OFF: no request is checked (llmgw ctl guard on).")
|
|
}
|
|
fmt.Fprintf(out, "Policies: %s\n", strings.Join(v.Policies, ", "))
|
|
def := strings.Join(v.Default, ", ")
|
|
if def == "" {
|
|
def = "none"
|
|
}
|
|
fmt.Fprintf(out, "Applied to every request: %s\n", def)
|
|
if len(v.Attachments) == 0 {
|
|
fmt.Fprintln(out, "Attached nowhere else.")
|
|
return nil
|
|
}
|
|
fmt.Fprintln(out)
|
|
w := tab()
|
|
fmt.Fprintln(w, "SCOPE\tWHO\tPOLICY")
|
|
for _, a := range v.Attachments {
|
|
who := a.ScopeID
|
|
if a.Name != "" {
|
|
who = a.Name + " (" + a.ScopeID + ")"
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\n", a.Scope, who, a.Policy)
|
|
}
|
|
return w.Flush()
|
|
},
|
|
})
|
|
for _, verb := range []struct {
|
|
use, short, method, done string
|
|
}{
|
|
{"attach", "Apply a policy to a scope and everything below it", "POST", "attached to"},
|
|
{"detach", "Stop applying a policy to a scope", "DELETE", "detached from"},
|
|
} {
|
|
c.AddCommand(&cobra.Command{
|
|
Use: verb.use + " TARGET POLICY",
|
|
Short: verb.short,
|
|
Example: " llmgw ctl guard " + verb.use + " project:Acme/finance/payroll finance",
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
body, _ := json.Marshal(map[string]string{"target": args[0], "policy": args[1]})
|
|
if err := client().do(cmd.Context(), verb.method, "/api/v1/guard/attachments", json.RawMessage(body), nil); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%s %s %s\n", args[1], verb.done, args[0])
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
return c
|
|
}
|