Repository navigation
Bundle index serves published modules — GitSync-native registries have no install records - #1948
Conversation
…stry has no install records The bundle index and download route keyed exclusively on the registry's own install records. memex-cloud provisions its packages as Spaces and never runs the catalog install, so it has NO records — the bundle feed was permanently empty and every consumer's module reconcile read SkipNoBundle (found live 2026-08-20, the first real remote consumer: systemorph authenticated fine and adopted nothing). - ServableEntries = install records UNION activation-sidecar entries whose PackagePath stamps a source; records win on collision. IsGranted gates both identically, so licensing is unchanged. - The publish route accepts ?packagePath=<source>/<plugin> (validated: two segments, plugin half must match the URL, traversal refused) and LandModule stamps it on the entry. - node-repo-module-pack gains registry-source (default Plugins) and POSTs the packagePath. Re-publishing stamps existing entries via fresh generations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request enables GitSync-native registries without install records to serve published module bundles.
Changes:
- Validates and stamps package source paths during publication.
- Combines installed packages with sidecar entries for bundle serving.
- Propagates registry source metadata through the module-pack workflow.
- Adds package-path validation tests.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Summary | Final review comments |
|---|---|---|
test/MeshWeaver.PluginCatalog.Test/ModulePublishTest.cs |
Tests package-path validation and stamping. | No final comments. |
src/MeshWeaver.PluginCatalog/ModulePublish.cs |
Validates and carries package source metadata. | Critical (1 vote): Publisher-controlled sources are not bound to authorized credentials or configuration. Moderate (3 votes): Case-only package ID mismatches can prevent granted bundles from being served. |
memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs |
Serves sidecar-published modules with grant checks. | Nit (4 votes): Add endpoint coverage for sidecar-only entries and record-collision behavior. |
.github/workflows/node-repo-module-pack.yml |
Sends registry source metadata during publishing. | No final comments. |
Suppressed comments (3)
.github/workflows/node-repo-module-pack.yml:198
- These query values are interpolated without URI encoding. The packer explicitly permits SemVer build metadata such as
1.2.3+build, and a valid operator-supplied source can contain reserved query characters;+,&,#, or?will be decoded as a different value or terminate the query, causing publishes to be refused or stamped incorrectly. Encode both values before constructing the URL.
-X POST "$REGISTRY/api/plugins/bundles/$PACKAGE?version=$VERSION&packagePath=$REGISTRY_SOURCE/$PACKAGE" \
memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs:399
- For install-record entries,
InstalledPackagessetsPackageIdtoPackagingManifest.IdPrefix + m.Id, but this sidecar-only branch uses the short plugin name.BuildResultusesPackageIdfor both the generated nuspec andContent-Disposition, so a publishedSocialMediabundle is emitted asSocialMediainstead of the canonicalMeshWeaver.Plugin.SocialMediapackage. Keep the same package-id projection for both sources.
return new BundleEntry(
segments[1], e.Version!, segments[1],
memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs:394
- The advertised “records win on collision” rule does not hold because deduplication keys the installed records by
PluginId, while the sidecar branch compares the bare package ID (segments[1]).BundleEntry.PackageIdis the shared raw package-ID field on both entries, so an installedSocialMediarecord is not recognized and a second sidecar entry is appended, producing duplicate index items. KeyrecordedbyPackageIdbefore applying the grant filter.
var recorded = records.Select(r => r.PluginId)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var published = activation.Entries
.Where(e => e.Enabled
&& !string.IsNullOrWhiteSpace(e.Version)
&& e.PackagePath?.Split('/') is { Length: 2 } segments
&& !recorded.Contains(segments[1]))
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| var published = activation.Entries | ||
| .Where(e => e.Enabled | ||
| && !string.IsNullOrWhiteSpace(e.Version) | ||
| && e.PackagePath?.Split('/') is { Length: 2 } segments | ||
| && !recorded.Contains(segments[1])) |
| || segments.Any(seg => string.IsNullOrWhiteSpace(seg) | ||
| || seg is "." or ".." | ||
| || seg.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) | ||
| || !string.Equals(segments[1], plugin, StringComparison.OrdinalIgnoreCase)) |
| if (!string.IsNullOrWhiteSpace(packagePath)) | ||
| { | ||
| var segments = packagePath.Split('/'); | ||
| if (segments.Length != 2 | ||
| || segments.Any(seg => string.IsNullOrWhiteSpace(seg) |
Test Results (shard 3) 10 files 10 suites 4m 59s ⏱️ Results for commit aebb0ac. |
Test Results (shard 0)633 tests 530 ✅ 8m 51s ⏱️ Results for commit aebb0ac. |
Test Results (shard 5)1 687 tests 1 685 ✅ 5m 57s ⏱️ Results for commit aebb0ac. |
Test Results (shard 4)1 563 tests 1 560 ✅ 8m 50s ⏱️ Results for commit aebb0ac. |
Test Results (shard 2)3 234 tests 3 042 ✅ 7m 22s ⏱️ Results for commit aebb0ac. |
Test Results (shard 1)1 585 tests 1 581 ✅ 9m 0s ⏱️ Results for commit aebb0ac. |
Test Results 62 files 62 suites 45m 1s ⏱️ Results for commit aebb0ac. |
The bundle index/download keyed only on the registry's install records; memex-cloud (GitSync-provisioned) has none, so its bundle feed was permanently empty — every consumer read
SkipNoBundle(found live: systemorph, the first real remote consumer, authenticated fine and adopted nothing).ServableEntries= install records ∪ sidecar entries whosePackagePathstamps a source (records win on collision);IsGrantedgates both identically, so licensing semantics are unchanged.?packagePath=<source>/<plugin>(validated; traversal refused) →LandModulestamps the entry.node-repo-module-packgainsregistry-source(defaultPlugins) and POSTs it. A re-publish stamps existing entries via fresh generations.26/26 publish tests green (5 new for the path validation).
🤖 Generated with Claude Code