You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
_Policy satellites are minted with MainNode = themselves, so they list as content on every package cover #2383
Found by MeshWeaver.Plugins' new covers sweep (#706) and measured by that session; root cause located in core.
Symptom
Every package cover's @@("area/Search") contents catalog lists Access Policy (the _Policy satellite), and on packages with children _Entitlements too. On the thirteen packages that ship no nodes of their own (Anthropic, AzureBlob, AzureFoundry, ClaudeCode, Copilot, Import, Indexing, Mail, Mcp, Notifications, OpenAI, Teams, WebSearch) it is the only entry — a public Store cover ending in a "Contents" section whose sole item is an internal security node.
Root cause — the mint, not the listing
Five _Policy nodes read off memex:
Chess mainNode: "Chess" ← partition ROOT NOT listed ✓
RolePlay mainNode: "RolePlay/_Policy" ← itself LISTED
Teams mainNode: "Teams/_Policy" ← itself LISTED
Mail mainNode: "Mail/_Policy" ← itself LISTED
MeshNode.MainNode defaults to the node's own path (MeshNode.cs:96); line 329 states the contract: "Satellite nodes' MainNode is set to their primary node path at creation time."
The catalog's is:main filter is exactly node.MainNode != node.Path (StaticNodeQueryProvider.cs:348/367). The listing is correct. A self-referential _Policy is, by the framework's own definition, a main node.
Six built-in providers mint _Policy as new MeshNode("_Policy", RootNamespace) and never set MainNode: BuiltInAgentProvider:55, BuiltInHarnessProvider:24, BuiltInLanguageModelProvider:284,322, BuiltInSkillProvider:41, DocumentationExtensions:135. They violate the contract on the type they construct. Chess is correct only because the gating reconcile (plugins side) rewrites the field when it runs.
Not an access defect
MeshWeaver.Security contains no reference to MainNode; PartitionAccessPolicy resolves by path ({partition}/_Policy). The AGENTS.md note about a wrong mainNode being silently ignored is about AccessAssignment. publicRead on the affected packages is scoped correctly. Cosmetic — but on the one page whose job is to sell.
Fix shape
Mint satellites with MainNode = <partition root>: a MeshNode.Satellite(id, namespace) factory used at the six sites (and at the Store-package _Policy seeder in plugins). Existing nodes need a one-time repair — a RunOnce logon action or Repair migration, never raw SQL. Not a query-scope default and not a control-level filter: MeshSearchControl has sixteen consumers, several of which list satellites deliberately, and the framework already excludes satellites correctly when the field is right.
Open
Why the gating reconcile writes the correct value while the built-in seed does not — two writers for one field; the seed should be the one that is right.
Deferred behind the CD repoint (#2364); filed so the measurement is not lost.
So for Store packages the gating reconcile is the writer that produces the self-referential shape — not a default falling through, an explicit assignment. My statement above that the reconcile "rewrites the field when it runs" was wrong; Chess's correct mainNode: "Chess" comes from something older or otherwise, still unidentified.
And why nothing converges: the reconcile's PolicyMatches compares content only (publicRead, redirectOnDenied) and never MainNode; both write predicates are built on it. A policy whose content matches is left alone forever, wrong field and all. Fixing the mint sites alone corrects new packages and repairs none of the existing ones — the trap in the "Fix shape" section above.
Two populations, two owners:
Store packages — plugins PluginGate.cs. Fix (plugins session, their file): a pure PolicyIsSatellite(policy, pluginPath) => policy.MainNode == pluginPath; a repair queued as fresh with { MainNode = pluginPath } — the existing node, field corrected, never a re-mint (a mint would overwrite publicRead/redirectOnDenied and could flip gating); predicate fresh is not null && !PolicyIsSatellite(...), false immediately after the write so it converges by construction (this file produced fix(kernel): activity terminal settle — no clobber, no bypass, no pump wedge #223's 14-writes/min storm, so convergence is the whole design). Tests pin: correct mint, one write for a wrong node, zero on the second pass, content preserved.
Built-in partitions (Agent, Harness, LanguageModel, Skill, Doc) — the six core sites above; those are static seeds, so a mint fix does cover them on next boot. Mine, separate PR.
The listing and the security evaluator remain correct in both cases.
Core half up: #2388 — the six built-in mints now set MainNode = <partition root> (static seeds, effective on next deploy, no repair step) plus MeshNode.Satellite(id, mainNode) with a two-direction test. The Store-package half (the explicit self-referential assignment in plugins' PluginGate.cs:936/956 + a converging repair predicate) is owned in MeshWeaver.Plugins.
Core half redone at the root — #2399. Three corrections that affect the plugins half.
1. The mint-site sweep was incomplete, and hand-sweeping was the wrong instrument. There are fifteen_-prefixed sibling mints in core, not six. #2388's grep (new MeshNode("_) cannot match the target-typed spelling new("_Policy", ns), and two writers are raw SQL — so nine were missed: DocumentationNodeProvider (a secondDoc/_Policy mint, distinct from the DocumentationExtensions one that was fixed), RoleNodeType, LicenseNodeType, GlobalSettingsNodeType, AccessControlLayoutArea, InstanceAutoRegistrationService, AssignmentNodeFactory, PostgreSqlAccessControl, SnowflakeAccessControl.
2. 🚨 The framework now derives MainNode for sibling satellites at the write boundary — this probably shrinks the plugins half.MeshExtensions.NormalizeSatelliteMainNode (applied on bothCreateNode and UpsertNode) previously fired only for types flagged IsSatelliteType; it now also fires for a satellite-shaped id (SatelliteTableMapping.IsSiblingSatellite).
Its trigger is MainNode == Path, which cannot distinguish "defaulted" from "explicitly assigned to the same value". So PluginGate.PluginPolicy/OpenPolicy's explicit MainNode = $"{pluginPath}/_Policy" (:936/:956) is now corrected by the framework on write — provided those nodes are written through the node pipeline (meshService.CreateNode / CreateOrUpdateNodeRequest) rather than a storage adapter or raw SQL. Worth checking which path PluginGate actually takes before building the converging repair predicate: if it goes through the pipeline, new and re-written packages self-correct and only never-rewritten rows need the repair.
3. "Not an access defect" was right, but the evidence for it was wrong. The original grep targeted MeshWeaver.Security, and src/MeshWeaver.Security/ does not exist — the security rules live in src/MeshWeaver.Graph/Security/, and they do read MainNode. The correct reason the conclusion holds:
Both policy readers resolve by id = '_Policy' keyed on namespace — rebuild_user_effective_permissions (PostgreSqlSchemaInitializer.cs:1894, :1921) and the in-memory PermissionEvaluator (:443, :764) — never by main_node. So correcting the field cannot change what a policy grants or denies, in either direction.
MainNodeis security-relevant in general — SatelliteAccessRule delegates a satellite's permissions to it and has an explicit degenerate branch that abandons delegation the moment MainNode == Path; grants project at COALESCE(main_node, namespace); the SQL read clause matches n.main_node LIKE prefix || '%' — but PartitionAccessPolicy registers no SatelliteAccessRule and is not an AccessAssignment.
Also no StackOverflow risk from the self-reference: MainNode is a string path, MeshNode holds no object-valued back-reference, GetHashCode deliberately excludes Content, and nothing recurses by following MainNode.
Blast radius of the listing symptom is wider than Contents, though: search_across_schemas hard-filters every union branch on WHERE n.main_node = n.path, and unlike is:main that is not opt-in — so a self-referential _Policy was also returned by mesh-wide search, not just by the cover's catalog.
4. Already-minted core rows repair themselves.PartitionSourceFingerprint frames MainNode into the per-partition hash, so changing a static seed changes the fingerprint and the static-repo importer takes the full re-import path instead of the content-skip. Note this also corrects #2388's "static seeds … no repair step": SkillStaticRepoSource/HarnessStaticRepoSource explicitly include the _Policy in EnumerateSourceNodes, so those rows are durable — it is the fingerprint change, not the staticness, that heals them.
5. Two nodes a naive classifier gets wrong — both real, both now pinned by tests. A partition may legitimately be named with a leading underscore (GlobalSettingsNodeType.SettingsPath is literally _Setting), so the owner must come from the namespace, never a path scan — a path scan resolves _Setting/_Policy to "", the empty prefix that COALESCE(main_node, namespace) projects as a root-scope grant. And Admin/Partition/_Accessdeclares the global root-scope access partition: its id is that partition's NAME, so it is a main node of the catalog listing it. The new guard caught that second one on its first run, on my own predicate.
Activity
rbuergi commentedon Aug 26, 2026
Correction — the causality above is inverted for Store packages, and there are TWO populations
Measured by the plugins session in
MeshWeaver.PluginsStore/Licensing/Source/PluginGate.cs:So for Store packages the gating reconcile is the writer that produces the self-referential shape — not a default falling through, an explicit assignment. My statement above that the reconcile "rewrites the field when it runs" was wrong; Chess's correct
mainNode: "Chess"comes from something older or otherwise, still unidentified.And why nothing converges: the reconcile's
PolicyMatchescompares content only (publicRead,redirectOnDenied) and neverMainNode; both write predicates are built on it. A policy whose content matches is left alone forever, wrong field and all. Fixing the mint sites alone corrects new packages and repairs none of the existing ones — the trap in the "Fix shape" section above.Two populations, two owners:
PluginGate.cs. Fix (plugins session, their file): a purePolicyIsSatellite(policy, pluginPath) => policy.MainNode == pluginPath; a repair queued asfresh with { MainNode = pluginPath }— the existing node, field corrected, never a re-mint (a mint would overwritepublicRead/redirectOnDeniedand could flip gating); predicatefresh is not null && !PolicyIsSatellite(...), false immediately after the write so it converges by construction (this file produced fix(kernel): activity terminal settle — no clobber, no bypass, no pump wedge #223's 14-writes/min storm, so convergence is the whole design). Tests pin: correct mint, one write for a wrong node, zero on the second pass, content preserved.The listing and the security evaluator remain correct in both cases.
rbuergi commentedon Aug 26, 2026
Core half up: #2388 — the six built-in mints now set
MainNode = <partition root>(static seeds, effective on next deploy, no repair step) plusMeshNode.Satellite(id, mainNode)with a two-direction test. The Store-package half (the explicit self-referential assignment in plugins'PluginGate.cs:936/956+ a converging repair predicate) is owned in MeshWeaver.Plugins.Merge pull request #2388 from Systemorph/fix/2383-policy-satellites-c…
rbuergi commentedon Aug 26, 2026
Core half redone at the root — #2399. Three corrections that affect the plugins half.
1. The mint-site sweep was incomplete, and hand-sweeping was the wrong instrument. There are fifteen
_-prefixed sibling mints in core, not six. #2388's grep (new MeshNode("_) cannot match the target-typed spellingnew("_Policy", ns), and two writers are raw SQL — so nine were missed:DocumentationNodeProvider(a secondDoc/_Policymint, distinct from theDocumentationExtensionsone that was fixed),RoleNodeType,LicenseNodeType,GlobalSettingsNodeType,AccessControlLayoutArea,InstanceAutoRegistrationService,AssignmentNodeFactory,PostgreSqlAccessControl,SnowflakeAccessControl.2. 🚨 The framework now derives MainNode for sibling satellites at the write boundary — this probably shrinks the plugins half.
MeshExtensions.NormalizeSatelliteMainNode(applied on bothCreateNodeandUpsertNode) previously fired only for types flaggedIsSatelliteType; it now also fires for a satellite-shaped id (SatelliteTableMapping.IsSiblingSatellite).Its trigger is
MainNode == Path, which cannot distinguish "defaulted" from "explicitly assigned to the same value". SoPluginGate.PluginPolicy/OpenPolicy's explicitMainNode = $"{pluginPath}/_Policy"(:936/:956) is now corrected by the framework on write — provided those nodes are written through the node pipeline (meshService.CreateNode/CreateOrUpdateNodeRequest) rather than a storage adapter or raw SQL. Worth checking which pathPluginGateactually takes before building the converging repair predicate: if it goes through the pipeline, new and re-written packages self-correct and only never-rewritten rows need the repair.3. "Not an access defect" was right, but the evidence for it was wrong. The original grep targeted
MeshWeaver.Security, andsrc/MeshWeaver.Security/does not exist — the security rules live insrc/MeshWeaver.Graph/Security/, and they do readMainNode. The correct reason the conclusion holds:id = '_Policy'keyed onnamespace—rebuild_user_effective_permissions(PostgreSqlSchemaInitializer.cs:1894,:1921) and the in-memoryPermissionEvaluator(:443,:764) — never bymain_node. So correcting the field cannot change what a policy grants or denies, in either direction.MainNodeis security-relevant in general —SatelliteAccessRuledelegates a satellite's permissions to it and has an explicit degenerate branch that abandons delegation the momentMainNode == Path; grants project atCOALESCE(main_node, namespace); the SQL read clause matchesn.main_node LIKE prefix || '%'— butPartitionAccessPolicyregisters noSatelliteAccessRuleand is not anAccessAssignment.Also no StackOverflow risk from the self-reference:
MainNodeis astringpath,MeshNodeholds no object-valued back-reference,GetHashCodedeliberately excludesContent, and nothing recurses by followingMainNode.Blast radius of the listing symptom is wider than Contents, though:
search_across_schemashard-filters every union branch onWHERE n.main_node = n.path, and unlikeis:mainthat is not opt-in — so a self-referential_Policywas also returned by mesh-wide search, not just by the cover's catalog.4. Already-minted core rows repair themselves.
PartitionSourceFingerprintframesMainNodeinto the per-partition hash, so changing a static seed changes the fingerprint and the static-repo importer takes the full re-import path instead of the content-skip. Note this also corrects #2388's "static seeds … no repair step":SkillStaticRepoSource/HarnessStaticRepoSourceexplicitly include the_PolicyinEnumerateSourceNodes, so those rows are durable — it is the fingerprint change, not the staticness, that heals them.5. Two nodes a naive classifier gets wrong — both real, both now pinned by tests. A partition may legitimately be named with a leading underscore (
GlobalSettingsNodeType.SettingsPathis literally_Setting), so the owner must come from the namespace, never a path scan — a path scan resolves_Setting/_Policyto"", the empty prefix thatCOALESCE(main_node, namespace)projects as a root-scope grant. AndAdmin/Partition/_Accessdeclares the global root-scope access partition: its id is that partition's NAME, so it is a main node of the catalog listing it. The new guard caught that second one on its first run, on my own predicate.Sibling satellites derive their MainNode at the write boundary (#2383)
Merge pull request #2399 from Systemorph/fix/2383-satellite-mint-cont…