Repository navigation
feat(PluginCatalog): one update policy per package — separate from the platform's image policy - #4310
Conversation
…e platform's image policy Maintainer, 2026-09-14: "continuous update is set globally; it should be separate for the platform and for each module." Until now Admin/UpdatePolicy — the record that decides whether the platform IMAGE rolls — also gated every module's unattended landing (PlatformModuleUpdatePolicy → IModuleUpdatePolicy), so pinning the image silently stopped every module from tracking its registry, and following continuous builds could not pin one module. - PackageManifest.UpdatePolicy (Auto | Notify | None, nullable — the zero member must survive WhenWritingDefault) with EffectiveUpdatePolicy reading a legacy record's autoUpdate flag as Auto / Notify. BOTH lanes branch on it: the content reconcile (PackageUpdateReconciler: Auto applies, Notify reminds once, None is silent) and the module lane (RegistryUpdateReconciler passes the record's PolicyDecline into PluginBundleClient.AdoptModule). - PluginCatalogOptions.DefaultUpdatePolicy seeds fresh records (PackageInstaller.SeedUpdatePolicy; AutoUpdateByDefault stays as the legacy mapping); every re-stamp carries the record's own policy forward. - PackageInstaller.SetUpdatePolicy — the catalog card's per-package control for global administrators (Auto · Notify · Pinned), a System write authorized by the card's admin gate; strings en/de. - IModuleUpdatePolicy and the portal's PlatformModuleUpdatePolicy are retired; Admin/UpdatePolicy governs the image roll only (the settings tab says so). - DeploymentContent.ModuleUpdatePolicy → PluginCatalog__DefaultUpdatePolicy in the chart; Doc/Architecture/PluginUpdateOnGreenBuild + ModuleAdoptionPolicy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…__DefaultUpdatePolicy The per-package default the instance seeds onto every install record, from the Hosting/Deployment record's moduleUpdatePolicy; absent renders nothing so the chart's default keeps the legacy AutoUpdateByDefault mapping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved deployment wiring, policy-update integrity, and unattended-policy enforcement issues block approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Separates per-package plugin update policies from the platform image policy, supporting Auto, Notify, and None behavior across content and module updates.
Changes:
- Adds policy modeling, legacy compatibility, defaults, and reconciliation behavior.
- Adds administrator controls, localization, and deployment configuration.
- Retires the platform-wide module policy seam and updates documentation.
File summaries
| File | Summary |
|---|---|
src/MeshWeaver.PluginCatalog/RegistryUpdateReconciler.cs |
Applies package policy to module reconciliation. |
src/MeshWeaver.PluginCatalog/PluginUpdateWatcher.cs |
Uses effective package policy. |
src/MeshWeaver.PluginCatalog/PluginCatalogOptions.cs |
Adds default policy configuration. |
src/MeshWeaver.PluginCatalog/PluginBundleClient.cs |
Supports policy-based module adoption decisions. |
src/MeshWeaver.PluginCatalog/PackageUpdateReconciler.cs |
Applies content update policy decisions. |
src/MeshWeaver.PluginCatalog/PackageInstaller.cs |
Seeds and edits package policies. |
src/MeshWeaver.PluginCatalog/Package.cs |
Defines package policy and legacy mapping. |
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs |
Removes the retired policy interface. |
src/MeshWeaver.PluginCatalog/CatalogLayoutAreas.cs |
Adds administrator policy controls. |
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json |
Adds English UI strings. |
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json |
Adds German UI strings. |
src/MeshWeaver.Documentation/Data/Architecture/PluginUpdateOnGreenBuild.md |
Documents per-package policies. |
src/MeshWeaver.Documentation/Data/Architecture/ModuleAdoptionPolicy.md |
Updates adoption-policy guidance. |
src/MeshWeaver.Deployment.Contract/DeploymentContent.cs |
Adds deployment-level policy input. |
memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateConfiguration.cs |
Removes platform/module coupling. |
memex/Memex.Portal.Shared/SelfUpdate/PlatformModuleUpdatePolicy.cs |
Removes the retired implementation. |
deploy/helm/templates/memex-portal/config.yaml |
Adds the policy configuration key. |
Review details
Suppressed comments (8)
deploy/helm/templates/memex-portal/config.yaml:906
DefaultUpdatePolicyis a nullable enum, but this template always emits an empty string when unset. The .NET configuration binder sees a present invalid enum value rather than an absent/null setting, so a default deployment can fail binding instead of falling back toAutoUpdateByDefault. Omit this key when no policy is configured (or emit only a valid enum name).
PluginCatalog__DefaultUpdatePolicy: "{{ .Values.config.memex_portal.PluginCatalog__DefaultUpdatePolicy | default "" }}"
src/MeshWeaver.Deployment.Contract/DeploymentContent.cs:79
- The new public deployment property adds an English
[Description], but the contract-label appendix inConfiguringAnInstanceFromAspire.mdis the documented source of German labels for this assembly and has noModuleUpdatePolicyrow. Add the German label there (and the in-mesh translation in the paired Plugins change) so this setting is not English-only.
[Description("Default per-module update policy seeded at install: Auto, Notify or None")]
public string? ModuleUpdatePolicy { get; init; }
src/MeshWeaver.Documentation/Data/Architecture/ModuleAdoptionPolicy.md:24
- This migration banner updates one page, but
src/MeshWeaver.Documentation/Data/Architecture/Modules.md:924-936still documentsAdmin/UpdatePolicyandIModuleUpdatePolicyas the active module gate. After this deletion that page tells operators the opposite behavior and points at a removed type; update it in the same documentation change.
> **Who may adopt unattended is a PER-PACKAGE choice** since 2026-09-14 — `updatePolicy` on the
> install record (`Auto` / `Notify` / `None`), seeded from `PluginCatalog:DefaultUpdatePolicy`, and
> independent of the platform's image policy on `Admin/UpdatePolicy`. The rules below say WHAT an
> installation runs once a version is adopted; [Plugin Update on Green Build](@/Doc/Architecture/PluginUpdateOnGreenBuild)
> says whether the adoption happens by itself.
src/MeshWeaver.Documentation/Data/Architecture/PluginUpdateOnGreenBuild.md:388
- This is a user-visible feature, but no
Data/WhatsNew/2026/...entry is included. The portal's What's New feed is built from those nodes and the release convention requires aCategory: Featureentry for a new user-facing capability; add the entry (or explicitly pair it with the dependent change) before considering the feature complete.
## 🚨 One policy PER PACKAGE — separate from the platform's image policy (2026-09-14)
> **Maintainer, 2026-09-14:** *"continuous update is set globally — it should be separate for the
> platform and for each module."* Until that day `Admin/UpdatePolicy` — the record that decides
> whether the platform IMAGE rolls — also gated every module's unattended landing
> (`PlatformModuleUpdatePolicy`, the retired `IModuleUpdatePolicy` seam): a deployment that pinned
> its image to a clean release silently stopped every module from tracking its registry, and one
> that followed continuous builds could not pin a single module. The two are now independent.
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs:26
- This new
SkipPolicycontract conflicts with the shippedDoc/Architecture/Modules.md, which still saysAdmin/UpdatePolicyandIModuleUpdatePolicygate module landing. Since this change removes that interface, update that documentation page in the same change or operators will follow the old platform-coupled behavior.
/// <summary>The package's own update policy declines unattended landing (Notify / None).</summary>
SkipPolicy,
src/MeshWeaver.PluginCatalog/PackageInstaller.cs:577
- This success log is emitted before the write is subscribed and completed, so a rejected or failed write still records "update policy set". Move the
LogInformationcall into the successful write pipeline (for example, afterTake(1)withDo) so the operational record reflects the actual outcome.
logger?.LogInformation(
"Package {Id}: update policy set to {Policy} (was {Previous}).",
packageId, policy, manifest.EffectiveUpdatePolicy);
src/MeshWeaver.PluginCatalog/PackageInstaller.cs:572
Nonehas no representation in the legacyAutoUpdateboolean. WritingAutoUpdate = falsemakes an older reconciler take its Notify branch, and clearingNotifiedModuleVersionhere means a mixed-version rollout or downgrade can still raise a reminder for a package that was explicitly pinned. Either preserve the pinned state with a compatible marker/upgrade ordering, or document and test this intentional loss of down-level semantics instead of calling the flag consistent for older readers.
UpdatePolicy = policy,
AutoUpdate = policy == PackageUpdatePolicy.Auto,
// A new policy is a new question for the reminder path: forget what was
// told under the old one, so a switch back to Notify reminds again.
NotifiedModuleVersion = null,
src/MeshWeaver.PluginCatalog/PackageUpdateReconciler.cs:178
- The new three-way policy decision is not covered by the tests in this change: the existing reconciler fixtures seed only the legacy
AutoUpdateflag. Add real-mesh coverage for Auto applying, Notify producing one reminder, None staying silent, and the module lane declining on the same policy; otherwise content and module policy parity can regress independently.
return policy switch
{
PackageUpdatePolicy.Auto => Apply(hub, meshService, accessService, source, sourceRef, pkg, record,
recordPath, provenance, detail, logger),
PackageUpdatePolicy.Notify => Notify(
- Files reviewed: 18/18 changed files
- Comments generated: 14
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # The per-PACKAGE default policy (Auto | Notify | None), seeded onto every fresh install record — | ||
| # separate from the platform's own image policy (Admin/UpdatePolicy) since 2026-09-14. Empty keeps | ||
| # the legacy AutoUpdateByDefault mapping (true → Auto, false → Notify). | ||
| PluginCatalog__DefaultUpdatePolicy: "{{ .Values.config.memex_portal.PluginCatalog__DefaultUpdatePolicy | default "" }}" |
| /// A package's own record stays the runtime authority once installed. | ||
| /// </summary> | ||
| [Description("Default per-module update policy seeded at install: Auto, Notify or None")] | ||
| public string? ModuleUpdatePolicy { get; init; } |
| var updated = node with | ||
| { | ||
| Content = manifest with | ||
| { | ||
| UpdatePolicy = policy, |
| public IObservable<int> AdoptModule( | ||
| string pluginId, string moduleName, string? packagePath = null, bool unattended = false) | ||
| string pluginId, string moduleName, string? packagePath = null, bool unattended = false, | ||
| string? policyDecline = null) |
| PackageInstaller.SetUpdatePolicy(host.Hub, packageId, policy, logger) | ||
| .Subscribe( | ||
| _ => { }, | ||
| ex => logger?.LogWarning(ex, "Setting update policy {Policy} on {Id} failed.", policy, packageId)); |
| /// <summary> | ||
| /// THE per-package update policy — <see cref="PackageUpdatePolicy.Auto"/> / | ||
| /// <see cref="PackageUpdatePolicy.Notify"/> / <see cref="PackageUpdatePolicy.None"/> — the | ||
| /// sole runtime authority for this package's unattended updates, content AND module, and | ||
| /// independent of the platform's own image policy (<c>Admin/UpdatePolicy</c>). Seeded at | ||
| /// install time from the deployment's <see cref="PluginCatalogOptions.DefaultUpdatePolicy"/> |
| // Internal for the BuildCompletionSubscriptionTest pin (InternalsVisibleTo). | ||
| internal static bool SeedAutoUpdate(PackageManifest? existingRecord, PluginCatalogOptions? options) => | ||
| existingRecord?.AutoUpdate ?? options?.AutoUpdateByDefault ?? false; | ||
| SeedUpdatePolicy(existingRecord, options) == PackageUpdatePolicy.Auto; |
| var policy = record.EffectiveUpdatePolicy; | ||
| logger?.LogInformation( | ||
| "Package update: {Id} has an update ({Old} → {New}; {Detail}); autoUpdate={Auto}. {Provenance}", | ||
| pkg.Id, record.ModuleVersion, pkg.ModuleVersion, detail, record.AutoUpdate, provenance); | ||
| "Package update: {Id} has an update ({Old} → {New}; {Detail}); updatePolicy={Policy}. {Provenance}", | ||
| pkg.Id, record.ModuleVersion, pkg.ModuleVersion, detail, policy, provenance); | ||
|
|
||
| return record.AutoUpdate | ||
| ? Apply(hub, meshService, accessService, source, sourceRef, pkg, record, recordPath, | ||
| provenance, detail, logger) | ||
| : Notify( | ||
| // The PACKAGE's own policy decides — never the platform's image policy (Admin/UpdatePolicy), | ||
| // which governs the image roll alone since 2026-09-14. | ||
| return policy switch | ||
| { | ||
| PackageUpdatePolicy.Auto => Apply(hub, meshService, accessService, source, sourceRef, pkg, record, |
| /// <summary> | ||
| /// The deployment's DEFAULT per-package update policy (<c>PluginCatalog:DefaultUpdatePolicy</c> | ||
| /// = <c>Auto</c> | <c>Notify</c> | <c>None</c>), consulted ONLY at install time: a fresh install | ||
| /// record is seeded with it (<c>PackageInstaller.SeedUpdatePolicy</c>). Unset → the legacy | ||
| /// <see cref="AutoUpdateByDefault"/> decides (<c>true</c> → Auto, else Notify). Separate from | ||
| /// the platform's own image policy on <c>Admin/UpdatePolicy</c> by design: a deployment may | ||
| /// pin its platform and still track every module, or the reverse. | ||
| /// </summary> | ||
| public PackageUpdatePolicy? DefaultUpdatePolicy { get; set; } |
| internal static bool ShouldAutoApply(PackageManifest record) => | ||
| record.EffectiveUpdatePolicy == PackageUpdatePolicy.Auto; |
Test Results (shard 0)262 tests 262 ✅ 1m 55s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 472 tests 1 472 ✅ 2m 48s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)910 tests 910 ✅ 3m 26s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)1 846 tests 1 654 ✅ 4m 32s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 6 files 6 suites 5m 36s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
Test Results (shard 3)1 092 tests 1 090 ✅ 6m 19s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
Test Results 18 files 18 suites 24m 40s ⏱️ Results for commit 8b0a74e. ♻️ This comment has been updated with latest results. |
…; WithModuleUpdatePolicy + its parity row Three gates on the first run, none about the change's substance: - Public surface / cross-repo pair: the retired IModuleUpdatePolicy was DELETED, and a removed public type is binary-breaking for a module compiled against an earlier platform (#2370). It stays as a public, [Obsolete] interface with its one member — nothing registers or consults it — so no TypeRef is orphaned and no counterpart declaration is owed. - RendererParityTest: DeploymentContent.ModuleUpdatePolicy had no fluent transform and no parity-table row. WithModuleUpdatePolicy(policy) is added beside WithUpdatePolicy (the image policy), the ConfiguringAnInstanceFromAspire table gains its row (config key PluginCatalog__DefaultUpdatePolicy), and the fully-populated fixture sets it so the emitted-key check can see it — Deployment.Contract.Test 15/15. - i18n mirror sync: the handover is declared in the PR body (Mirror-sync: Systemorph/MeshWeaver.Plugins#1858). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Maintainer, 2026-09-14: "continuous update is set globally; it should be separate for the platform and for each module."
Until now
Admin/UpdatePolicy— the record that decides whether the platform image rolls — also gated every module's unattended landing (PlatformModuleUpdatePolicy→IModuleUpdatePolicy). Pinning the image silently stopped every module from tracking its registry; following continuous builds could not pin a single module.What changes
PackageManifest.UpdatePolicy=Auto|Notify|None(nullable — the zero member must surviveWhenWritingDefault), withEffectiveUpdatePolicyreading a legacy record'sautoUpdateflag as Auto / Notify. Both lanes branch on it: the content reconcile (PackageUpdateReconciler: Auto applies, Notify reminds once, None is silent) and the module lane (RegistryUpdateReconciler.PolicyDecline→PluginBundleClient.AdoptModule), so a package never lands one half without the other.PluginCatalog:DefaultUpdatePolicyseeds fresh install records (PackageInstaller.SeedUpdatePolicy;AutoUpdateByDefaultstays as the legacy mapping,true→ Auto); every re-stamp carries the record's own policy forward.PackageInstaller.SetUpdatePolicy, a System write authorized by the card's admin gate. Strings en/de.IModuleUpdatePolicyand the portal'sPlatformModuleUpdatePolicy.Admin/UpdatePolicygoverns the image roll only; its settings-tab intro says so.DeploymentContent.ModuleUpdatePolicy→PluginCatalog__DefaultUpdatePolicyin the chart (config.yaml; empty keeps the legacy mapping).Doc/Architecture/PluginUpdateOnGreenBuild(the per-package section and §3),ModuleAdoptionPolicy(a banner).Verified
dotnet build -c Release -warnaserroronMeshWeaver.PluginCatalogandMemex.Portal.Shared: 0 warnings, 0 errors. The catalog's tests live in MeshWeaver.Plugins (src/MeshWeaver.PluginCatalog.Test) — the pinned cases forSeedUpdatePolicy,PolicyDeclineand the three-wayDecidefollow in the Plugins PR once this set seals, together withHelmValuesrenderingmoduleUpdatePolicy.🤖 Generated with Claude Code
Mirror-sync: tracked on Systemorph/MeshWeaver.Plugins#1858 —
npm run sync:i18n -- --ref <merged core sha>in MeshWeaver.Plugins once this lands (5 keys added,ui.mdUpdatePolicyIntroreworded).