Skip to content

Built-in _Policy nodes carry their MainNode, so they stop listing as content (#2383) - #2388

Merged
rbuergi merged 2 commits into
mainfrom
fix/2383-policy-satellites-carry-their-main-node
Aug 26, 2026
Merged

rbuergi merged 2 commits into
mainfrom
fix/2383-policy-satellites-carry-their-main-node

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Core half of #2383 (the Store-package half is a converging-predicate change in MeshWeaver.Plugins' PluginGate.cs, owned there).

Six built-in providers mint a partition's _Policy as new MeshNode("_Policy", ns) { … } without setting MainNode. The plain constructor defaults it to the node's own path (MeshNode.cs:96), while line 329 of the same type states the satellite contract. The catalog's is:main is literally MainNode != Path, so a _Policy minted this way is a main node by definition — and "Access Policy" appears under Contents on the Agent, Skill, LanguageModel, Harness and Doc covers. The listing was never wrong; the mint was.

Change

  • each of the six initializers sets MainNode = <partition root> — static seeds, re-registered on boot, so it takes effect on the next deploy with no repair step
  • MeshNode.Satellite(id, mainNode) for new satellites, with a test pinning both that it points at its main node and that the plain constructor does not (the trap this closes)

Not an access defect

MeshWeaver.Security never reads MainNode; PartitionAccessPolicy resolves by path.

Verified

Mesh.Contract, AI, Documentation: 0 errors under -warnaserror; 2/2 tests. What's New: Fix.

…content (#2383)

Six built-in providers mint a partition's _Policy as `new MeshNode("_Policy", ns) { ... }` and
never set MainNode. The plain constructor defaults MainNode to the node's OWN path
(MeshNode.cs:96), while line 329 of the same type states the contract every satellite is
supposed to meet: "Satellite nodes' MainNode is set to their primary node path at creation
time." The catalog's `is:main` predicate is literally `MainNode != Path`, so a _Policy minted
this way is a main node by definition -- and "Access Policy" shows up under Contents on the
Agent, Skill, LanguageModel, Harness and Doc covers. The listing was never wrong; the mint was.

Each of the six initializers now sets `MainNode = <partition root>`. These are static seeds
re-registered on every boot, so the corrected field takes effect on the next deploy with no
repair step. (The Store-package half -- the gating reconcile in MeshWeaver.Plugins, which sets
MainNode = itself EXPLICITLY and never re-checks it -- is a separate converging-predicate
change in that repo; see #2383.)

MeshNode.Satellite(id, mainNode) is added for new satellites, with a test pinning both that it
points at its main node and that the plain constructor does NOT -- the trap this closes.

Not an access defect: MeshWeaver.Security never reads MainNode; PartitionAccessPolicy resolves
by path. Cosmetic, on the pages whose job is to sell.

Verified: Mesh.Contract, AI and Documentation build 0 errors under -warnaserror; 2/2 tests.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a metadata contract violation where built-in partition _Policy nodes were minted with MainNode defaulting to their own path, causing them to be treated as “main” nodes and show up under “Contents” on package covers. It introduces a small factory to mint sibling satellites correctly and updates the built-in _Policy seeds accordingly, with a regression test and a What’s New entry.

Changes:

  • Add MeshNode.Satellite(id, mainNode) to create sibling satellite nodes with MainNode pointing at the primary node.
  • Set MainNode to the partition root for built-in _Policy nodes (AI providers + Documentation seed) so the catalog’s is:main filter excludes them.
  • Add a unit test that pins both the correct satellite behavior and the “plain constructor trap”, plus a What’s New entry.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/MeshWeaver.Data.Test/MeshNodeSatelliteTest.cs Adds regression coverage for MeshNode.Satellite and the default MainNode behavior.
src/MeshWeaver.Mesh.Contract/MeshNode.cs Introduces the MeshNode.Satellite factory and documents the satellite contract.
src/MeshWeaver.Documentation/DocumentationExtensions.cs Fixes the Doc partition _Policy seed to point MainNode at the partition root.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-26-access-policy-no-longer-listed-as-content.md Documents the user-visible behavior change in What’s New format.
src/MeshWeaver.AI/BuiltInSkillProvider.cs Fixes built-in Skill partition _Policy seed MainNode.
src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs Fixes built-in Model Provider + Language Model partition _Policy seed MainNode.
src/MeshWeaver.AI/BuiltInHarnessProvider.cs Fixes built-in Harness partition _Policy seed MainNode.
src/MeshWeaver.AI/BuiltInAgentProvider.cs Fixes built-in Agent partition _Policy seed MainNode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/MeshWeaver.Data.Test/MeshNodeSatelliteTest.cs Outdated
Comment thread src/MeshWeaver.Mesh.Contract/MeshNode.cs Outdated
@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

    9 files  ±0      9 suites  ±0   5m 12s ⏱️ -11s
1 960 tests ±0  1 958 ✅ ±0  2 💤 ±0  0 ❌ ±0 
2 518 runs  +2  2 516 ✅ +2  2 💤 ±0  0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

599 tests  ±0   595 ✅ ±0   13m 1s ⏱️ - 1m 40s
  7 suites ±0     4 💤 ±0 
  7 files   ±0     0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

2 050 tests  +2   1 857 ✅ +2   8m 31s ⏱️ +6s
    9 suites ±0     193 💤 ±0 
    9 files   ±0       0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    9 files  ±0      9 suites  ±0   6m 25s ⏱️ +3s
1 611 tests ±0  1 610 ✅ ±0  1 💤 ±0  0 ❌ ±0 
1 612 runs  ±0  1 611 ✅ ±0  1 💤 ±0  0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 832 tests  ±0   2 729 ✅ ±0   9m 11s ⏱️ -20s
    9 suites ±0     103 💤 ±0 
    9 files   ±0       0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 142 tests  ±0   2 140 ✅ ±0   14m 38s ⏱️ + 2m 34s
   10 suites ±0       2 💤 ±0 
   10 files   ±0       0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    53 files  ±0      53 suites  ±0   57m 1s ⏱️ +33s
11 192 tests +2  10 887 ✅ +2  305 💤 ±0  0 ❌ ±0 
11 753 runs  +4  11 448 ✅ +4  305 💤 ±0  0 ❌ ±0 

Results for commit 60f13dd. ± Comparison against base commit 4ab645f.

♻️ This comment has been updated with latest results.

…ites _Access

Two findings on #2388, both correct:
- MeshNodeSatelliteTest declared namespace MeshWeaver.Mesh.Contract.Test inside
  MeshWeaver.Data.Test, breaking the project convention and namespace-based filters.
- The MeshNode.Satellite doc listed _Access as an example. Access assignments are not
  sibling nodes with id _Access; they live under a _Access/ NAMESPACE (Doc/_Access/{id}),
  so the example would have prompted exactly the wrong usage. Replaced with the distinction.

Rebuilt with -warnaserror; 2/2 tests pass under the corrected namespace.
@rbuergi

rbuergi commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Both correct, both fixed in the follow-up commit: the test now sits in MeshWeaver.Data.Test like its siblings, and the Satellite doc no longer cites _Access — it states the distinction instead (access assignments live under a _Access/ namespace, not as a sibling id, so they are not what the factory is for). Rebuilt with -warnaserror; 2/2 tests under the corrected namespace.

@rbuergi
rbuergi merged commit 8fa9d3b into main Aug 26, 2026
27 checks passed
rbuergi added a commit that referenced this pull request Aug 26, 2026
MeshNode.MainNode self-defaults to the node's own path, and the framework's compensating
normalizer (MeshExtensions.NormalizeSatelliteMainNode, applied on both the CreateNode and
UpsertNode paths) only fired for node types flagged IsSatelliteType -- the ones that also
live in their own satellite TABLE. The `_`-prefixed SIBLINGS that share mesh_nodes --
`_Policy`, `_Provider`, `_GitSync`, `_DefaultInstallLedger` -- were normalised by NOTHING, so
every writer had to remember the field by hand. Fifteen of them exist in core; none did.

`is:main` KEEPS exactly the rows where MainNode == Path (PostgreSqlSqlGenerator emits
`n.main_node = n.path`; StaticNodeQueryProvider skips a node when MainNode != Path), so a
satellite is normally excluded PRECISELY because its MainNode differs from its path -- and one
pointing at itself passes instead, as a main node. search_across_schemas hard-filters every
union branch on the same predicate UNCONDITIONALLY, not only when the caller asks for is:main.
That is why "Access Policy" listed under Contents on the package covers, and why the row also
reached mesh-wide search.

#2388 corrected six of the fifteen by hand. The grep behind it -- `new MeshNode("_` -- cannot
match the target-typed spelling `new("_Policy", ns)`, and two of the writers are raw SQL, so
nine were missed. This change fixes the root rather than repeating that sweep:

  * NormalizeSatelliteMainNode now also fires on a satellite-shaped ID
    (SatelliteTableMapping.IsSiblingSatellite), so CreateNode/UpsertNode derive the field for
    `_Policy` and every future sibling. Flagging those types instead was not an option: the
    flag also RELOCATES the node (CreateLayoutArea files a flagged type under
    `{ns}/_{Type}/{id}`) and routes it to a satellite table, which would hide `{ns}/_Policy`
    from the permission evaluator's `id = '_Policy'` lookup.
  * The nine missed writers are corrected: DocumentationNodeProvider (the SECOND Doc policy
    mint), RoleNodeType, LicenseNodeType, GlobalSettingsNodeType, AccessControlLayoutArea,
    InstanceAutoRegistrationService, AssignmentNodeFactory -- plus PostgreSqlAccessControl and
    SnowflakeAccessControl, which write SQL directly and bypass the handler entirely. Static
    seeds bypass the handler too, which is why the mint fixes are still needed alongside it.
  * A guard sweeps the static seeds the mesh actually serves, so the next writer that forgets
    is caught by a control rather than by a grep.
  * #2388's remark stated the is:main predicate BACKWARDS ("is:main == MainNode != Path") in
    seven places; that is the EXCLUSION test, not the selection test, and it now reads the way
    the code behaves. Caught in review -- the phrasing was inherited from the issue rather than
    checked against StaticNodeQueryProvider.

Two traps the classifier has to avoid, both real nodes in this repo and both pinned by tests:
a PARTITION may legitimately be named with a leading underscore (GlobalSettingsNodeType's
SettingsPath is literally `_Setting`), so the owner is still derived from
OwnerOfSatellitePath over the NAMESPACE rather than by scanning the path -- 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 that
lists it. The guard found that second one on its first run.

Snowflake note: `main_node` and `path` shared one variable there, so the INSERT wrote the
policy's own path into BOTH columns. They are now separate -- `path` is unchanged.

Not an access-control bypass. Both policy readers resolve by `id = '_Policy'` keyed on
namespace -- rebuild_user_effective_permissions (PostgreSqlSchemaInitializer) and the
in-memory PermissionEvaluator -- never by main_node, so what a policy grants or denies is
unaffected. MainNode IS security-relevant in general (SatelliteAccessRule delegates
permissions to it and degrades when it self-references; 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.
The residual harm is disclosure-shaped: an internal governance node listed as content and
returned by mesh-wide search.

No StackOverflow risk: MainNode is a string path, there is no object-valued back-reference on
MeshNode, GetHashCode excludes Content, and nothing recurses by following MainNode.

Already-minted rows: the static seeds are framed into PartitionSourceFingerprint, so changing
them changes each partition's fingerprint and the static-repo importer takes the full
re-import path instead of the content-skip -- the durable rows in the DB-synced partitions
repair themselves on the next deploy, with no migration. Rows written by
AccessControlLayoutArea or the raw-SQL writers before this change keep their old value; they
are cosmetic per the paragraph above, and the Store-package population is owned in
MeshWeaver.Plugins (PluginGate.cs, which assigns the self-reference explicitly).

Verified: Mesh.Contract, Graph, Documentation, AI, PluginCatalog, Hosting.PostgreSql,
Hosting.Snowflake, Hosting.Monolith.TestBase all 0 errors under -c Release -warnaserror.
2722 tests green across Data (390), Security (376), Graph (1626), PathResolution (155),
NodeOperations (76), Documentation (69), Hosting.PostgreSql (26, the container-backed
access-control and permission-projection suites that exercise the changed SQL writer) and
Hosting.Monolith (4). Both directions proven by reverting the fix: without the mint fixes the
sweep fails naming Doc/_Policy, License/_Policy, Role/_Policy and _Setting/_Policy; without
the normalization CreatingASiblingSatellite_DerivesItsMainNode fails with Actual:
"TestData/_Policy". The two negative controls -- ordinary content stays a main node, an
explicit MainNode is preserved -- pass in every configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi deleted the fix/2383-policy-satellites-carry-their-main-node branch August 28, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants