From 2cc52fd1a7528408c20da28fa74762e858543c76 Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Sun, 4 Oct 2026 15:22:07 +0200 Subject: [PATCH] Read an unwritten guard mode as enforce when merging, not as the weakest Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/guard/policy.go | 11 +++++++---- internal/guard/policy_test.go | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/guard/policy.go b/internal/guard/policy.go index d905759d..6189ecf0 100644 --- a/internal/guard/policy.go +++ b/internal/guard/policy.go @@ -189,7 +189,13 @@ func Merge(names []string, ps []Policy) Policy { var allowSets [][]string var prompts []string for _, p := range ps { - out.Mode = stricterOf(out.Mode, p.Mode, []string{"", "off", "shadow", "enforce"}) + // Unwritten is enforce, not the weakest: a policy that says nothing + // of its mode, merged with one in shadow, must still act. + mode := p.Mode + if mode == "" { + mode = "enforce" + } + out.Mode = stricterOf(out.Mode, mode, []string{"", "off", "shadow", "enforce"}) out.Fail = stricterOf(out.Fail, p.Fail, []string{"", "open", "closed"}) out.Stream = stricterOf(out.Stream, p.Stream, []string{"", "sentence", "buffer_all"}) out.NoStore = out.NoStore || p.NoStore @@ -212,9 +218,6 @@ func Merge(names []string, ps []Policy) Policy { out.Tools.MaxCallsPerTurn = minSet(out.Tools.MaxCallsPerTurn, t.MaxCallsPerTurn) out.Tools.ValidateArguments = out.Tools.ValidateArguments || t.ValidateArguments } - if out.Mode == "" && len(ps) > 0 { - out.Mode = "enforce" - } for i, s := range allowSets { if i == 0 { out.Tools.Allow = slices.Clone(s) diff --git a/internal/guard/policy_test.go b/internal/guard/policy_test.go index 9c5441d1..64910228 100644 --- a/internal/guard/policy_test.go +++ b/internal/guard/policy_test.go @@ -37,6 +37,14 @@ func TestPoliciesNarrowAndNeverLoosen(t *testing.T) { 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") }