diff --git a/src/MeshWeaver.AI/BuiltInAgentProvider.cs b/src/MeshWeaver.AI/BuiltInAgentProvider.cs index d746c55e5a..1dd148f916 100644 --- a/src/MeshWeaver.AI/BuiltInAgentProvider.cs +++ b/src/MeshWeaver.AI/BuiltInAgentProvider.cs @@ -54,6 +54,11 @@ public IEnumerable GetStaticNodes() // The write caps keep the built-in agents unmodifiable. yield return new MeshNode("_Policy", RootNamespace) { + // 🚨 A satellite must point at its MAIN node, not at itself (#2383): the plain + // constructor defaults MainNode to the node's own path, which makes _Policy a main + // node by the catalog's definition (is:main == MainNode != Path) and lists "Access + // Policy" as content on every cover. New satellites: MeshNode.Satellite(id, main). + MainNode = RootNamespace, NodeType = "PartitionAccessPolicy", Name = "Access Policy", Content = new PartitionAccessPolicy diff --git a/src/MeshWeaver.AI/BuiltInHarnessProvider.cs b/src/MeshWeaver.AI/BuiltInHarnessProvider.cs index c8931b63af..0983434304 100644 --- a/src/MeshWeaver.AI/BuiltInHarnessProvider.cs +++ b/src/MeshWeaver.AI/BuiltInHarnessProvider.cs @@ -23,6 +23,11 @@ public IEnumerable GetStaticNodes() // World-readable catalog, unmodifiable — same shape as the agent catalog. yield return new MeshNode("_Policy", HarnessNodeType.RootNamespace) { + // 🚨 A satellite must point at its MAIN node, not at itself (#2383): the plain + // constructor defaults MainNode to the node's own path, which makes _Policy a main + // node by the catalog's definition (is:main == MainNode != Path) and lists "Access + // Policy" as content on every cover. New satellites: MeshNode.Satellite(id, main). + MainNode = HarnessNodeType.RootNamespace, NodeType = "PartitionAccessPolicy", Name = "Access Policy", Content = new PartitionAccessPolicy diff --git a/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs b/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs index 045807bfc6..25174b46b5 100644 --- a/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs +++ b/src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs @@ -283,6 +283,11 @@ public IEnumerable GetStaticNodes() // ALWAYS seed the read-only access policy for the catalog partition. yield return new MeshNode("_Policy", ModelProviderNodeType.RootNamespace) { + // 🚨 A satellite must point at its MAIN node, not at itself (#2383): the plain + // constructor defaults MainNode to the node's own path, which makes _Policy a main + // node by the catalog's definition (is:main == MainNode != Path) and lists "Access + // Policy" as content on every cover. New satellites: MeshNode.Satellite(id, main). + MainNode = ModelProviderNodeType.RootNamespace, NodeType = "PartitionAccessPolicy", Name = "Access Policy", Content = new PartitionAccessPolicy @@ -321,6 +326,11 @@ public IEnumerable GetStaticNodes() if (!string.Equals(LanguageModelNodeType.RootNamespace, ModelProviderNodeType.RootNamespace, StringComparison.Ordinal)) yield return new MeshNode("_Policy", LanguageModelNodeType.RootNamespace) { + // 🚨 A satellite must point at its MAIN node, not at itself (#2383): the plain + // constructor defaults MainNode to the node's own path, which makes _Policy a main + // node by the catalog's definition (is:main == MainNode != Path) and lists "Access + // Policy" as content on every cover. New satellites: MeshNode.Satellite(id, main). + MainNode = LanguageModelNodeType.RootNamespace, NodeType = "PartitionAccessPolicy", Name = "Access Policy", Content = new PartitionAccessPolicy diff --git a/src/MeshWeaver.AI/BuiltInSkillProvider.cs b/src/MeshWeaver.AI/BuiltInSkillProvider.cs index 39c4a4ea08..3425028db7 100644 --- a/src/MeshWeaver.AI/BuiltInSkillProvider.cs +++ b/src/MeshWeaver.AI/BuiltInSkillProvider.cs @@ -40,6 +40,11 @@ public IEnumerable GetStaticNodes() // (the Harness wedge, prod 2026-06-15). The write caps keep the built-in skills unmodifiable. yield return new MeshNode("_Policy", SkillNodeType.RootNamespace) { + // 🚨 A satellite must point at its MAIN node, not at itself (#2383): the plain + // constructor defaults MainNode to the node's own path, which makes _Policy a main + // node by the catalog's definition (is:main == MainNode != Path) and lists "Access + // Policy" as content on every cover. New satellites: MeshNode.Satellite(id, main). + MainNode = SkillNodeType.RootNamespace, NodeType = "PartitionAccessPolicy", Name = "Access Policy", Content = new PartitionAccessPolicy diff --git a/src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-26-access-policy-no-longer-listed-as-content.md b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-26-access-policy-no-longer-listed-as-content.md new file mode 100644 index 0000000000..c0d0446ed0 --- /dev/null +++ b/src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-26-access-policy-no-longer-listed-as-content.md @@ -0,0 +1,11 @@ +--- +Name: Access Policy no longer appears as page content +Category: Fix +Description: The built-in Agent, Skill, Model, Harness and Documentation covers stop listing their internal access-policy node under Contents. +Icon: DocumentBulletList +Order: -20260826 +--- + +# Access Policy no longer appears as page content + +The contents section of the built-in Agent, Skill, Model, Harness and Documentation pages no longer shows an "Access Policy" entry. That entry is an internal security setting, not content, and it was appearing because it had been created without saying which page it belongs to. diff --git a/src/MeshWeaver.Documentation/DocumentationExtensions.cs b/src/MeshWeaver.Documentation/DocumentationExtensions.cs index 7caf9c03f5..e05c1fdf07 100644 --- a/src/MeshWeaver.Documentation/DocumentationExtensions.cs +++ b/src/MeshWeaver.Documentation/DocumentationExtensions.cs @@ -134,6 +134,11 @@ public static TBuilder AddDocumentation(this TBuilder builder, builder.AddMeshNodes( new MeshNode("_Policy", DocumentationNodeProvider.RootNamespace) { + // 🚨 A satellite must point at its MAIN node, not at itself (#2383): the plain + // constructor defaults MainNode to the node's own path, which makes _Policy a main + // node by the catalog's definition (is:main == MainNode != Path) and lists "Access + // Policy" as content on every cover. New satellites: MeshNode.Satellite(id, main). + MainNode = DocumentationNodeProvider.RootNamespace, NodeType = "PartitionAccessPolicy", Name = "Documentation Access Policy", Content = new PartitionAccessPolicy diff --git a/src/MeshWeaver.Mesh.Contract/MeshNode.cs b/src/MeshWeaver.Mesh.Contract/MeshNode.cs index 6e48199142..e4945b7338 100644 --- a/src/MeshWeaver.Mesh.Contract/MeshNode.cs +++ b/src/MeshWeaver.Mesh.Contract/MeshNode.cs @@ -95,6 +95,20 @@ public record MeshNode([property: Key] string Id, [property: Editable(false)] st [Editable(false)] public string MainNode { get; init; } = string.IsNullOrEmpty(Namespace) ? Id : $"{Namespace}/{Id}"; + /// + /// A SATELLITE of the node at : created with + /// already pointing at its primary, which is what the doc on promises and + /// what every listing relies on to keep satellites out of a node's contents (the catalog's + /// is:main is literally MainNode != Path). 🚨 Minting a satellite with + /// new MeshNode(id, ns) leaves MainNode defaulting to ITSELF, which makes it a main node + /// by definition — that is how Access Policy came to be listed as content on every + /// package cover (#2383). Use this for _Policy and every other underscore-prefixed + /// SIBLING node. (Access assignments are not siblings — they live under a _Access/ + /// namespace, e.g. Doc/_Access/{id} — so they are not what this factory is for.) + /// + public static MeshNode Satellite(string id, string mainNode) => + new(id, mainNode) { MainNode = mainNode }; + /// /// Single segments as used for matching and addressing. /// Must be a computed property (not a readonly field) so that it reflects diff --git a/test/MeshWeaver.Data.Test/MeshNodeSatelliteTest.cs b/test/MeshWeaver.Data.Test/MeshNodeSatelliteTest.cs new file mode 100644 index 0000000000..ff5d41ebbe --- /dev/null +++ b/test/MeshWeaver.Data.Test/MeshNodeSatelliteTest.cs @@ -0,0 +1,24 @@ +using MeshWeaver.Mesh; +using Xunit; + +namespace MeshWeaver.Data.Test; + +public class MeshNodeSatelliteTest +{ + [Fact] + public void A_satellite_points_at_its_main_node_not_at_itself() + { + var policy = MeshNode.Satellite("_Policy", "Teams"); + Assert.Equal("Teams/_Policy", policy.Path); + Assert.Equal("Teams", policy.MainNode); + Assert.NotEqual(policy.Path, policy.MainNode); // the catalog's is:main predicate + } + + /// The trap this factory exists to close: the plain constructor makes a MAIN node. + [Fact] + public void The_plain_constructor_defaults_MainNode_to_itself() + { + var node = new MeshNode("_Policy", "Teams"); + Assert.Equal(node.Path, node.MainNode); + } +}