Skip to content

_Policy satellites are minted with MainNode = themselves, so they list as content on every package cover #2383

Closed

Description

@rbuergi

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.

Activity

  1. rbuergi commented on Aug 26, 2026

    @rbuergi
    ContributorAuthor

    Correction — the causality above is inverted for Store packages, and there are TWO populations

    Measured by the plugins session in MeshWeaver.Plugins Store/Licensing/Source/PluginGate.cs:

    public static MeshNode PluginPolicy(string pluginPath) =>   // :932
        new("_Policy", pluginPath) { …, MainNode = $"{pluginPath}/_Policy", … };   // :936  ← explicit
    public static MeshNode OpenPolicy(string pluginPath) =>     // :952
        new("_Policy", pluginPath) { …, MainNode = $"{pluginPath}/_Policy", … };   // :956  ← explicit

    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:

    1. 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.
    2. 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.

  2. rbuergi commented on Aug 26, 2026

    @rbuergi
    ContributorAuthor

    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.

  3. added a commit that references this issue on Aug 26, 2026
  4. rbuergi commented on Aug 26, 2026

    @rbuergi
    ContributorAuthor

    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 second Doc/_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 both CreateNode 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.
    • MainNode is 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/_Access declares 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.

  5. added 2 commits that reference this issue on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions