Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
3.2 KiB
Go
73 lines
3.2 KiB
Go
package guard
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func TestPoliciesNarrowAndNeverLoosen(t *testing.T) {
|
|
gateway := Policy{Mode: "enforce", Fail: "open", MaxOutputTokens: 4096, SystemPrompt: "Be careful.",
|
|
Input: Rules{PII: map[string]string{"email": "flag", "secret": "block"}, Injection: "flag"},
|
|
Tools: Tools{Allow: []string{"search", "shell", "fetch"}, Deny: []string{"rm"}}}
|
|
project := Policy{Mode: "off", Fail: "closed", MaxOutputTokens: 1024, NoStore: true, SystemPrompt: "Answer in Turkish.",
|
|
Input: Rules{PII: map[string]string{"email": "redact", "secret": "flag"}, Injection: "block",
|
|
Deny: []Pattern{{Name: "codename", Regex: "falcon", Action: "block"}}},
|
|
Tools: Tools{Allow: []string{"search", "fetch"}, OnDenied: "block"}}
|
|
got := Merge(nil, []Policy{gateway, project})
|
|
|
|
if got.Mode != "enforce" {
|
|
t.Errorf("mode %q: a project cannot switch the gateway's policy off", got.Mode)
|
|
}
|
|
if got.Fail != "closed" || !got.NoStore || got.MaxOutputTokens != 1024 {
|
|
t.Errorf("fail %q no_store %v max %d", got.Fail, got.NoStore, got.MaxOutputTokens)
|
|
}
|
|
if got.Input.PII["email"] != "redact" || got.Input.PII["secret"] != "block" || got.Input.Injection != "block" {
|
|
t.Errorf("input %+v", got.Input)
|
|
}
|
|
if len(got.Input.Deny) != 1 || !slices.Equal(got.Tools.Allow, []string{"search", "fetch"}) ||
|
|
!slices.Equal(got.Tools.Deny, []string{"rm"}) || got.Tools.OnDenied != "block" {
|
|
t.Errorf("deny %v tools %+v", got.Input.Deny, got.Tools)
|
|
}
|
|
if got.SystemPrompt != "Be careful.\n\nAnswer in Turkish." {
|
|
t.Errorf("prompt %q", got.SystemPrompt)
|
|
}
|
|
|
|
// Allow lists that share nothing allow nothing.
|
|
none := Merge(nil, []Policy{{Tools: Tools{Allow: []string{"a"}}}, {Tools: Tools{Allow: []string{"b"}}}})
|
|
if len(none.Tools.Allow) != 1 || none.Tools.Allow[0] == "a" || none.Tools.Allow[0] == "b" {
|
|
t.Errorf("disjoint allow lists: %v", none.Tools.Allow)
|
|
}
|
|
// Found on the PoC: a shadow default merged with an attached policy that
|
|
// did not write its mode left the attached one in shadow too.
|
|
if m := Merge(nil, []Policy{{Mode: "shadow"}, {}}); m.Mode != "enforce" {
|
|
t.Errorf("shadow and an unwritten mode: %q", m.Mode)
|
|
}
|
|
if m := Merge(nil, []Policy{{Mode: "shadow"}}); m.Mode != "shadow" {
|
|
t.Errorf("shadow alone: %q", m.Mode)
|
|
}
|
|
if Merge(nil, nil).active() || !Merge(nil, []Policy{{}}).active() || Merge(nil, []Policy{{Mode: "off"}}).active() {
|
|
t.Error("activeness")
|
|
}
|
|
}
|
|
|
|
func TestALibraryIsCheckedBeforeItIsUsed(t *testing.T) {
|
|
bad := []Config{
|
|
{Default: []string{"nope"}},
|
|
{Policies: map[string]Policy{"p": {Mode: "loud"}}},
|
|
{Policies: map[string]Policy{"p": {Input: Rules{PII: map[string]string{"dna": "block"}}}}},
|
|
{Policies: map[string]Policy{"p": {Input: Rules{PII: map[string]string{"email": "shred"}}}}},
|
|
{Policies: map[string]Policy{"p": {Output: Rules{Deny: []Pattern{{Name: "x", Regex: "(", Action: "block"}}}}}},
|
|
{Policies: map[string]Policy{"p": {Tools: Tools{OnDenied: "explode"}}}},
|
|
}
|
|
for i, c := range bad {
|
|
if c.Validate() == nil {
|
|
t.Errorf("case %d accepted: %+v", i, c)
|
|
}
|
|
}
|
|
good := Config{Default: []string{"p"}, Policies: map[string]Policy{"p": {Mode: "shadow",
|
|
Input: Rules{PII: map[string]string{"card": "redact"}, Deny: []Pattern{{Name: "x", Regex: "a+", Action: "flag"}}}}}}
|
|
if err := good.Validate(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|