Repository navigation
feat(modules): bundle build + Store funnel + auto-update — #1664 Slices B+C - #1673
Conversation
…es B+C
Slice B — package shape + bundle build:
- Bundle format gains a MODULE variant inside the SAME bundle the NodeType
lane already ships: meshweaver/modules/<file> beside meshweaver/assemblies/,
one manifest naming both (module: { assemblyName, assemblies[] }). One
reader handles both (BundleReader.Read / ReadModule, manifest-driven; the
module side is all-or-nothing — a partial closure loads and faults at
first use, so it never lands).
- PackageManifest.Module — the compiled-module declaration (entry-assembly
name), authored as content.module on the plugin root, read by
NodeRepoPackageSource, carried onto the install record by the ordinary
record stamp.
- meshweaver-plugin-build module-pack: packs a built module's closure into a
bundle keyed to the MVID of the MeshWeaver.Graph.dll in the build output
(read, never guessed). Explicit closure (--with), never a folder scrape.
Invocable from any node repo's CI (PackAsTool).
Slice C — funnel + landing:
- PluginBundleEndpoints serves the module section from the registry's own
modules/<name>/ tree (ModuleBundleSource — refuses uninstalled and
framework-stale landings); the index stamps each bundle's module only when
the bytes are servable. Same instance-key auth, fail-closed.
- PluginBundleClient.AdoptModule: index → ModuleUpdateDecision → download →
MVID gate (PrebuiltAssemblySeeder.DeclineReason, checked at index,
manifest, and placement) → ModuleLandingService.LandModule
(restart-as-activation via the existing PendingRestart signal). Never
fails an install — every refusal is a logged zero.
- The install branch rides the ONE orchestrator
(CatalogLayoutAreas.InstallOrUpdate) via a Bundles handle on
RegistryPackageSource, so the catalog click, the content auto-update
apply, and the boot default install land a declared module identically.
Auto-update (rides the EXISTING policy surface — no new knob):
- RegistryUpdateReconciler gains a module pass: newer-for-the-running-MVID
lands, same-version skips without a download, foreign-MVID skips
silently-with-log; never rolls back unattended. Landed version recorded on
the activation entry (ModuleActivationEntry.Version).
- Gate = Admin/UpdatePolicy via IModuleUpdatePolicy (memex bridges it in
PlatformModuleUpdatePolicy, registered by AddSelfUpdate). Continuous — the
platform default, and what an absent policy reads as — lands unattended;
Stable/None decline UPGRADES only (first landings complete an install, and
the image-roll heal keeps an installed module working — both exempt).
Docs: Modules.md bundle-lane section + auto-update policy paragraph;
PluginPackaging.md module variant; What's New feature entry.
Tests: bundle round-trip incl. mixed bundle (BundleReaderTest), pure
decision pins (ModuleUpdateDecisionTest), serve rules
(ModuleBundleSourceTest), fetch/land MVID refusals + declaration flow
(ModuleFunnelTest). Suites green: PluginCatalog.Test 340, Graph.Test 1194,
Memex.Portal.Shared.Test 366, doc integrity.
Part of #1664 (Slice D remains).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements #1664 Slice B+C by extending the existing plugin bundle transport to optionally carry a compiled module payload, wiring module landing into the install + boot reconcile funnels, and adding an unattended update gate tied to the existing Admin/UpdatePolicy surface (Memex).
Changes:
- Extend bundle format to support a “module lane” (
meshweaver/modules/*) alongside the existing NodeType assembly lane, with manifest-driven reading/writing and mixed-bundle support. - Route module-declaring packages through install/update + boot reconcile to land module bytes into
modules/<name>/and record activation/version for “already landed” decisions. - Add a
module-packbuild tool command, policy wiring (IModuleUpdatePolicy), API endpoint support, and tests/docs to pin behavior.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.PluginCatalog.Test/ModuleUpdateDecisionTest.cs | Adds pure decision tests covering module reconcile actions (land/skip/policy/rollback rules). |
| test/MeshWeaver.PluginCatalog.Test/ModuleFunnelTest.cs | Adds end-to-end funnel tests for module landing and manifest/record propagation. |
| test/MeshWeaver.PluginCatalog.Test/ModuleBundleSourceTest.cs | Adds tests for registry-side “servable module bytes” rules from modules/ tree. |
| test/MeshWeaver.Graph.Test/BundleReaderTest.cs | Extends bundle reader tests to cover module bundles and mixed bundles. |
| src/MeshWeaver.PluginCatalog/RegistryUpdateReconciler.cs | Adds a post-content module reconcile pass using a shared bundle client per registry. |
| src/MeshWeaver.PluginCatalog/RegistryPackageSource.cs | Adds optional Bundles handle so install orchestrator can land modules from registry sources. |
| src/MeshWeaver.PluginCatalog/PluginBundleClient.cs | Adds module adoption/landing flow (AdoptModule + LandFromBundle) keyed off bundle index + MVID gate + policy. |
| src/MeshWeaver.PluginCatalog/Package.cs | Adds PackageManifest.Module declaration field to route packages through the module funnel. |
| src/MeshWeaver.PluginCatalog/NodeRepoPackageSource.cs | Reads content.module from root node JSON into the package manifest. |
| src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs | Introduces the pure module auto-update decision function + verdict/action types. |
| src/MeshWeaver.PluginCatalog/ModuleLandingService.cs | Extends landing to record version; adds pooled activation read helper and exposes base directory. |
| src/MeshWeaver.PluginCatalog/ModuleBundleSource.cs | Adds registry-side module closure collection rules from modules/<name>/ gated by activation + framework MVID. |
| src/MeshWeaver.PluginCatalog/ModuleActivation.cs | Adds ModuleActivationEntry.Version for “already landed” checks. |
| src/MeshWeaver.PluginCatalog/InstanceAutoRegistrationService.cs | Shares a single bundle client between default-install, registry source, and adopt paths. |
| src/MeshWeaver.PluginCatalog/CatalogLayoutAreas.cs | Wires module landing into the main install/update orchestrator for registry sources. |
| src/MeshWeaver.Plugin.Packaging/NuGetPackageWriter.cs | Adds distinct meshweaver/modules/ folder support and helper for module entry paths. |
| src/MeshWeaver.Plugin.Packaging/BundleReader.cs | Adds manifest support for module section and a ReadModule extractor with all-or-nothing closure semantics. |
| src/MeshWeaver.Plugin.Build/Program.cs | Adds CLI dispatch for module-pack. |
| src/MeshWeaver.Plugin.Build/ModulePackCommand.cs | Implements module-pack to produce module bundles keyed to the framework MVID read from build output. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-16-modules-install-and-update-from-the-store.md | Adds What’s New entry documenting module install + update behavior. |
| src/MeshWeaver.Documentation/Data/Architecture/PluginPackaging.md | Documents the bundle/module variant layout and CI packaging invocation. |
| src/MeshWeaver.Documentation/Data/Architecture/Modules.md | Documents Store module delivery via bundles and the module reconcile/policy behavior. |
| memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateConfiguration.cs | Registers the Memex policy provider implementation for module unattended landing. |
| memex/Memex.Portal.Shared/SelfUpdate/PlatformModuleUpdatePolicy.cs | Implements IModuleUpdatePolicy over Admin/UpdatePolicy with fail-closed behavior on read errors. |
| memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs | Extends bundle index + bundle assembly to stamp/serve module payloads when servable on the registry instance. |
Suppressed comments (1)
test/MeshWeaver.PluginCatalog.Test/ModuleFunnelTest.cs:196
- Add Dispose() to ensure landingRoot is deleted even when a test fails before reaching the explicit cleanup statements.
Directory.Delete(landingRoot, recursive: true);
}
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 4)1 601 tests 1 595 ✅ 7m 30s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)919 tests 918 ✅ 11m 18s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 371 tests 1 370 ✅ 5m 51s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 11 files 11 suites 6m 4s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)2 438 tests 2 434 ✅ 8m 31s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)2 148 tests 2 045 ✅ 6m 50s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
Test Results 65 files 65 suites 46m 6s ⏱️ Results for commit 2b0f05e. ♻️ This comment has been updated with latest results. |
|
CI red root-caused before rerun: shard 0's 🤖 Generated with Claude Code |
…D equality
Maintainer design correction from the dependency analysis: MVID equality is
BAKE semantics (NodeType assemblies compiled against exact refs) wrongly
applied to ordinary modules. Modules are plain .NET assemblies binding by
SIMPLE NAME; their contract is API compatibility. MVID-equality would force
rebundling every module on every CI build and forbid ex-post Store installs
across platform versions ('install GRPC ex post through the Store').
- NEW ModulePlatformFloor: the ONE module platform gate — running platform
(MeshWeaver.Graph's informational version, +sha stripped) must satisfy the
module's declared minMeshVersion, SemVer-compared (NuGetVersionComparer);
absent floor = no constraint; declared floor + unknown running version
declines (never land on faith). Applied at the index, the manifest,
placement (ModuleLandingService — the MVID refusal moved here), the serve
side (ModuleBundleSource), and boot (ModuleActivationBoot; wired in
ConfigureMemexMesh). The built-against MVID stays RECORDED everywhere
(bundle manifest, activation entry, landing log, index) as DIAGNOSTIC
metadata — never a refusal. NodeType-bake bundles keep the strict MVID
gate (PrebuiltAssemblySeeder.DeclineReason) unchanged.
- PackageManifest.MinMeshVersion (authored content.minMeshVersion, peeked by
NodeRepoPackageSource, stamped onto the record) flows to the bundle index
entry, the manifest's module section (BundleReader.ModuleRef), and the
activation entry — so a consumer skips an uninstallable bundle without a
download and boot re-checks the floor after a platform rollback.
- ModuleUpdateDecision: SkipForeignFramework → SkipPlatformBelowFloor;
up-to-date keys on version alone (landed modules keep loading across
platform builds — the image-roll re-land case no longer exists, so the
policy exemption is first-landing only); MVID is no longer an input.
- module-pack: --min-mesh-version records the floor; --graph-dll becomes
optional (MVID recorded as diagnostics when found, warned when not).
- Tests: ModulePlatformFloorTest (pure gate pins incl. SemVer-not-string and
the unknown-running-version refusal); the foreign-MVID pins rewritten as
floor pins; NEW ex-post pins — a bundle from another platform build with a
satisfied floor LANDS/SERVES (client, landing, decision, serve rules);
boot-union floor-skip + no-floor-loads. Docs (Modules.md,
PluginPackaging.md, What's New) updated to the floor semantics.
Suites green (Release): PluginCatalog.Test 347, Graph.Test 1194,
Memex.Portal.Shared.Test 366, doc integrity.
Note: the ONE build-time-coupled module class is IVT consumers (currently
MeshWeaver.Approvals → Graph internals) — those stay image-shipped until
de-internalized.
Part of #1664.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Second commit applies the maintainer's design correction: the module-bundle gate is now a 🤖 Generated with Claude Code |
…lidation, test temp-dir cleanup Copilot review on #1673, all three legitimate: 1) ModuleUpdateDecision: up-to-date used STRING equality while the downgrade check used NuGetVersionComparer — a landed '1.2.0' against a served '1.2' (manifests legitimately carry two-part versions) misread as an update and would re-land on every reconcile, forever. Both checks now go through the ONE comparer; pinned ('1.2' == '1.2.0' ⇒ up-to-date, both directions). 2) module-pack: moduleName/plugin/packageVersion flow into file paths (the entry-DLL probe, the closure entries, the output bundle name) — validate to a safe identifier/semver shape BEFORE composing any path, with a clear exit-2 error naming the offending value; '../evil' can no longer probe outside the module folder or write a bundle somewhere surprising. --min-mesh-version validated to the same version shape. Pinned (ModulePackCommandTest: four injection-shaped rejections + a pack→read round-trip through BundleReader.ReadModule). 3) ModuleFunnelTest: the per-test landing tree leaked whenever an assertion failed (inline end-of-test deletes are skipped the moment an assert throws). Cleanup moved to the lifecycle hook — the base implements IAsyncLifetime and xUnit v3 calls DisposeAsync in preference to IDisposable.Dispose, so the DisposeAsync override (best-effort catch) is the hook that actually runs; inline deletes dropped. Targeted classes green (Release): ModuleUpdateDecision+Floor+Funnel 24/24, BundleReader+ModulePackCommand 17/17. Part of #1664. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements #1664 Slice B (module package shape + bundle build) and Slice C (funnel + landing + auto-update), on top of the merged Slice A landing seam (
ModuleActivation/ModuleLandingService).Shape decisions
One bundle, two lanes. A package's compiled module rides inside the same bundle that already carries its NodeType assemblies —
meshweaver/modules/<file>besidemeshweaver/assemblies/<nodePath>.dll, onemeshweaver/manifest.jsonnaming both (module: { assemblyName, assemblies[] }next toassemblies[]). Same routes, same index, same instance-key auth — the #13 transport decision is honored by extending/api/plugins/bundles, not adding a channel. One reader serves both lanes (BundleReader.Read/BundleReader.ReadModule, both manifest-driven); the module side is deliberately all-or-nothing (a NodeType with missing bytes compiles; a module missing part of its closure loads and faults at first use).The declaration is two fields authors already know.
PackageManifest.Module= the module's entry-assembly name,PackageManifest.MinMeshVersion= its declared platform floor (both authored on the plugin root'sindex.jsoncontent, read byNodeRepoPackageSource, carried onto the install record by the ordinary record stamp). A mixed package — content nodes + compiled module in one Store product, the SocialMedia shape — is exactly these fields on an ordinary node-repo package; card/price/funnel/pre-install are untouched.🚨 The module gate is a
minMeshVersionFLOOR — deliberately NOT MVID equality (maintainer's dependency-analysis correction). MVID equality is bake semantics — a NodeType assembly compiled in-process against exact refs — and applying it to ordinary modules would force rebundling every module on every CI build and forbid ex-post Store installs across platform versions ("install GRPC ex post through the Store"). Modules are plain assemblies binding by simple name; their contract is API compatibility, which the semver floor expresses. The ONE gate is the newModulePlatformFloor.DeclineReason(running platform = MeshWeaver.Graph's informational version, SemVer-compared viaNuGetVersionComparer), applied at the index, the manifest, placement, and boot; an absent floor is no constraint. The bundle keeps recording its built-against MVID as DIAGNOSTIC metadata — logged at landing, surfaced in the index/sidecar — never a refusal. NodeType-bake bundles keep the strict MVID gate (PrebuiltAssemblySeeder.DeclineReason) unchanged. Consequence: landed modules keep loading across ordinary platform updates (no re-land per image roll); the only boot skip is a platform rollback below a module's declared floor.The registry serves what it runs. Module bytes are served from the registry's own
modules/<name>/tree (ModuleBundleSource) — the same philosophy as the NodeType lane ("the inputs ARE the storage") — and a landing the registry's own boot would skip (uninstalled, or a floor its platform no longer satisfies) is refused, so a registry can never fan out assemblies it could not load itself. The index stamps each bundle'smodule+minMeshVersiononly when the bytes are actually servable.The build lane is a plain tool invocation.
meshweaver-plugin-build module-pack <outputDir> --plugin <id> --package-version <v> --min-mesh-version <floor>packs a module bundle recording the floor (the consumer's gate) and, when the restoredMeshWeaver.Graph.dllis at hand, the built-against MVID (diagnostics — optional, warned when absent). Because the gate is the floor, ONE bundle serves every compatible platform build. The closure is an explicit statement (<name>.dll+--with), never a folder scrape, mirroring the modules// rule that for most modules the DLL alone is the closure. Invocable from any node repo's CI (the tool isPackAsTool; the platform'splugin-publish.ymlalready builds/runs it from a checkout).The funnel (Slice C)
CatalogLayoutAreas.InstallOrUpdate), riding aBundleshandle onRegistryPackageSource— so the catalog click, the content auto-update apply, and the boot default install all land a declared module identically: bundle-fetch → floor gate (ModulePlatformFloor.DeclineReason, checked at the index, at the manifest, and a third time at placement) →ModuleLandingService.LandModule(restart-as-activation; the existingPendingRestartsidecar flag is the signal). Nothing here can fail an install — every refusal is a logged zero. Followed the code on the brief's "PackageInstaller binary branch":PackageInstallernever sees a source or a registry credential, so the branch belongs one caller up, in the orchestrator that has both — reported as a deliberate deviation.RegistryUpdateReconcilergains a module pass after its content pass: for installed module-declaring packages it consults the registry's bundle index and applies one pure decision (ModuleUpdateDecision.Decide) — lands newer when the bundle's floor is satisfied here, skips same-version without a download, skips a bundle whose floor exceeds the running platform silently-with-log (it becomes installable after the platform updates, and the same pass lands it then). Never rolls back unattended. The landed version + floor are recorded on the activation entry (ModuleActivationEntry.Version/.MinMeshVersion) so "already landed" costs zero bytes.Policy-surface wiring (no new knob)
The unattended gate is the existing
Admin/UpdatePolicysurface, bridged viaIModuleUpdatePolicy(declared inMeshWeaver.PluginCatalog, implemented inMemex.Portal.Shared.SelfUpdate.PlatformModuleUpdatePolicy, registered byAddSelfUpdatebeside the node type):The plugin lane's existing
AutoUpdate/AutoUpdateByDefaultrecord flag keeps governing the content reconcile exactly as before; a content auto-update that applies carries its module with it through the orchestrator.Deferred (follow-ups, not this repo/PR)
PluginContentrecord inStore/Plugin/Sourcegains the matchingmodulefield beside its existingminMeshVersion(authoring-side mirror ofcontent.module); until then the field is plain JSON on the root and flows fine.module-packand declarescontent.module: MeshWeaver.Socialon its root — the satellite-CI wiring the maintainer scoped out of this PR.InternalsVisibleTointernals binds to build identity, not public API — a semver floor cannot vouch for it. Currently that is exactlyMeshWeaver.Approvals(→MeshWeaver.Graphinternals); such modules stay image-shipped until de-internalized, and only then join the bundle lane.Verification
MeshWeaver.Plugin.Packaging,MeshWeaver.Plugin.Build,MeshWeaver.PluginCatalog,Memex.Portal.Shared+ all touched test projects:-c Release -warnaserror, 0 errors, one project per invocation.BundleReaderTest); the pure floor gate itself incl. SemVer-not-string ordering and never-land-on-unknown-running-version (ModulePlatformFloorTest); floor refusal at fetch (ModuleFunnelTest.ABundleWhoseFloorExceedsThisPlatform_IsRefused_NothingReachesDisk) AND at land (ModuleLandingServiceTest); the ex-post pin the correction demands — a bundle from another platform build with a satisfied floor LANDS, MVID recorded as diagnostics (ModuleFunnelTest.ABundleFromAnotherPlatformBuild_WithItsFloorSatisfied_Lands,ModuleLandingServiceTest.LandModule_ForeignBuiltAgainstMvid_Lands_TheMvidIsDiagnosticOnly,ModuleUpdateDecisionTest.ABundleBuiltAgainstAnOlderPlatform_Lands_WhenItsFloorIsSatisfied,ModuleBundleSourceTest.ALandingBuiltByAnotherPlatformBuild_IsStillServed_TheMvidIsDiagnostic); reconciler lands-newer / skips-same / skips-below-floor / skips-older / policy-gates-upgrades-only / never-reinstalls-uninstalled — all pure (ModuleUpdateDecisionTest); boot union floor-skip + no-floor-loads (ModuleActivationBootTest); installer routing — declaration + floor flow listing → record, module payload routes to landing not node-parse (ModuleFunnelTest); registry serve rules (ModuleBundleSourceTest).MeshWeaver.PluginCatalog.Test347/347 ·MeshWeaver.Graph.Test1194/1194 ·Memex.Portal.Shared.Test366/366 · doc integrity (DocumentationLinkIntegrityTest+WhatsNewEntryIntegrityTest) green after the doc edits.IObservablethrough the sealedIoPools (ModuleLandingService's cap-1 pool, the HTTP pool inPluginBundleClient); no bare async in hub-reachable code.Docs:
Modules.md→ "The bundle lane — modules as Store packages" + the auto-update paragraph naming the policy surface and default;PluginPackaging.md→ "The module variant". What's New: Modules install and update from the Store (Feature).Closes nothing on its own — #1664 remains open for Slice D.
🤖 Generated with Claude Code