diff --git a/internal/config/profiles_v3_test.go b/internal/config/profiles_v3_test.go index 80c4464b2..3f1bbe27e 100644 --- a/internal/config/profiles_v3_test.go +++ b/internal/config/profiles_v3_test.go @@ -22,7 +22,7 @@ func TestProfileConfig_LegacyRoundTrip(t *testing.T) { out, err := json.Marshal(p) require.NoError(t, err) - require.JSONEq(t, src, string(out)) + require.Equal(t, src, string(out)) } // TestProfileConfig_V3FieldsParse pins that every v3 field round-trips and diff --git a/internal/profile/glob.go b/internal/profile/glob.go index 250f6178f..1a6e4fa37 100644 --- a/internal/profile/glob.go +++ b/internal/profile/glob.go @@ -19,9 +19,12 @@ type globMatcher struct { // compileGlob compiles a "server:tool" rule pattern into an anchored // matcher. The pattern is assumed already validated (config.ValidateProfiles, // FR-004) — an unparsable pattern here would be a validator bug, not a -// runtime condition, so this never errors. +// runtime condition, so this never errors. The (?s) flag makes '*' (".*") +// match embedded newlines too: a tool identity is upstream-controlled text, +// and without it a deny pattern like "github:delete*" would silently fail +// open on "github:delete\nrepo". func compileGlob(pattern string) *globMatcher { - return &globMatcher{pattern: pattern, re: regexp.MustCompile("^" + globToRegexp(pattern) + "$")} + return &globMatcher{pattern: pattern, re: regexp.MustCompile("(?s)^" + globToRegexp(pattern) + "$")} } // globToRegexp renders pattern as an anchor-free regexp fragment: '*' diff --git a/internal/profile/glob_test.go b/internal/profile/glob_test.go index 7a012468a..60b580075 100644 --- a/internal/profile/glob_test.go +++ b/internal/profile/glob_test.go @@ -22,13 +22,16 @@ func TestGlobMatcher(t *testing.T) { {"anchored: no partial prefix match", "github:list", "github:list_issues", false}, {"anchored: no partial suffix match", "github:issues", "github:list_issues", false}, {"case-sensitive", "github:List_Issues", "github:list_issues", false}, - {"tool name containing a slash", "filesystem:read_text_file", "filesystem:read_text_file", true}, + {"underscored tool name, exact", "filesystem:read_text_file", "filesystem:read_text_file", true}, + {"tool name containing a slash, exact", "ns:sub/erase", "ns:sub/erase", true}, {"tool name containing a slash, glob", "ns:sub/*", "ns:sub/erase", true}, {"tool name containing double underscore (direct-surface alias shape)", "github:list__issues", "github:list__issues", true}, {"tool identity with an embedded colon (namespaced raw name)", "a:ns:erase", "a:ns:erase", true}, {"tool identity with an embedded colon, mismatch", "a:ns:erase", "a:ns:wipe", false}, {"literal dot is not a regex any-char wildcard", "github:v1.0-sync", "github:v1.0-sync", true}, {"literal dot near-miss: dot must match exactly, not any char", "github:v1.0-sync", "github:v1x0-sync", false}, + {"star matches an embedded newline (deny rules must not fail open)", "github:delete*", "github:delete\nrepo", true}, + {"leading star matches across a newline", "github:*repo", "github:delete\nrepo", true}, {"literal hyphen", "github:v1.0-sync", "github:v1.0_sync", false}, } for _, tc := range cases { diff --git a/internal/server/profiles_v3_node_fixture_test.go b/internal/server/profiles_v3_node_fixture_test.go index 548447fc4..077416033 100644 --- a/internal/server/profiles_v3_node_fixture_test.go +++ b/internal/server/profiles_v3_node_fixture_test.go @@ -52,8 +52,11 @@ var wantNodeFixtureTools = map[string]wantNodeFixtureTool{ // nodeFixtureFileTools is which tools each per-server file must contain, in // order (matches enforcementMatrixProfiles' upstream registration order in -// profiles_v3_fixture_test.go, so the two fixtures never drift apart on -// content even though they are read by different runtimes). +// profiles_v3_fixture_test.go). Only the tool names and their order are kept +// in step across the two fixtures; per-tool descriptions and annotation +// shapes intentionally differ (the JSON files use description==name and a +// single hint, the in-process fixture uses descriptive text and sets both +// readOnlyHint and destructiveHint). var nodeFixtureFileTools = map[string][]string{ "github": {"list_issues", "create_issue", "delete_repo", "search_code", "get_secret_scanning_alert"}, "notion": {"update_page"},