Skip to content

Registry module bundles cannot carry native runtime assets: both closure derivations drop runtimeTargets, the bundle format has no native section, and the container evaluator refuses a native-only PackageReference (measured: MeshWeaver.AppleMessages needs libe_sqlite3) #4126

Closed

Description

@rbuergi

Measured 2026-09-12 while moving MeshWeaver.AppleMessages (the iMessage module, MeshWeaver.Plugins) off a system sqlite3 shell-out onto Microsoft.Data.Sqlite — the fleet rule being that a module brings its own dependencies (Doc/WhatsNew/2026-09-01-a-module-brings-its-own-dependencies-again), and the maintainer's addition (2026-09-12, verbatim: "must also be module … and then mounted"): the engine the module brings must travel INSIDE its sealed bundle, native runtime assets included, so a runner that mounts the sealed publication from the platform volume gets a working module with no install.

It cannot, today. The runtime half exists; the bake half does not. Every reference is origin/main at c7262a5.

What works: the runtime resolver (#1728)

src/MeshWeaver.Mesh.Contract/ModuleNativeAssets.cs:131 probes modules/<Name>/runtimes/<rid>/native/<lib> (then the flat folder) from AssemblyLoadContext.Default.ResolvingUnmanagedDll, keyed on the REQUESTING assembly's folder; MeshBuilder.InstallModules subscribes it. The landing service writes a bundle into modules/<generation>/ (src/MeshWeaver.PluginCatalog/ModuleLandingService.cs:907), whose parent is modules, so a runtimes/ tree laid out there WOULD resolve. The in-image closure lane already keeps that tree: memex/MeshModulesPublish.targets:465 (_ClosureRidNative, kept since #1728).

What strips it: the registry bundle, on both build paths

  1. SDK path — src/MeshWeaver.Plugin.Build/DepsClosure.cs:149 takes only runtime entries ending in .dll; :152 DETECTS runtimeTargets and :184 turns them into a WARNING ('{name}' declares native runtimeTargets the bundle does not carry — if the module needs them at runtime, they must ship another way). Nothing ships them another way.
  2. Container path — tools/MeshWeaver.PluginTester/ContainerReferenceSet.cs:307 reads only the runtime section of the image's deps.json; runtimeTargets/native are never read. So PrivateClosure.Derive (tools/MeshWeaver.PluginTester/PrivateClosure.cs:113) reaches SQLitePCLRaw.lib.e_sqlite3 through the bundle's dependency edge, finds no assembly for it and files it under Missing; ProjectBuild.cs:978 logs that as a warning, and EmitPrivateClosure (ProjectBuild.cs:1392) copies rides FLAT beside the module — there is no runtimes/<rid>/native/ layout to copy into.
  3. The bundle format has no native section — src/MeshWeaver.Plugin.Packaging/NuGetPackageWriter.cs:70 (meshweaver/modules/<flat file>, "Module files are flat") and :79 (meshweaver/moduleassets/ for wwwroot/** only); ModulePackCommand.cs:667 writes exactly those two plus content. The landing side mirrors it (ModuleLandingService.cs:907 flat assemblies, :911 wwwroot).
  4. Worse: the container evaluator REFUSES a native-only PackageReference. ContainerReferenceSet.cs:360 Resolve matches a package by the ASSEMBLY the deps.json says it contributes (:364 falls back to <id>.dll); a package that contributes only natives contributes no assembly → Supplied=false → ProjectBuild.cs:951 unresolved → RED: 1 PackageReference(s) the container does not supply: SQLitePCLRaw.lib.e_sqlite3 — although the image carries libe_sqlite3.so in /app (MeshWeaver.Hosting.Sqlite is platform-shipped). So a build: container module cannot even DECLARE the CVE-patched engine (SQLitePCLRaw.lib.e_sqlite3 3.53.3, the GHSA-2m69-gcr7-jv3q pin every SDK build needs under -warnaserror).

No module in MeshWeaver.Plugins carries a native asset in its registry bundle today — the one native family in the fleet (SkiaSharp for MeshWeaver.Markdown.Export) is supplied by the PORTAL HOST (Memex.Portal.Gui.csproj, SkiaSharp.NativeAssets.Linux.NoDependencies), which is the same "the host happens to have it" shape the 2026-09-01 WhatsNew called a defect for managed assemblies.

What the AppleMessages bundle does meanwhile (MeshWeaver.Plugins, fix/imessage-in-process-sqlite)

The module declares Microsoft.Data.Sqlite + SQLitePCLRaw.core/provider.e_sqlite3 3.0.2 exactly as MeshWeaver.Hosting.Sqlite does, and the native pin SQLitePCLRaw.lib.e_sqlite3 3.53.3 under Condition="'$(MSBuildRuntimeType)' != ''" — set under every MSBuild build (tests, SDK path), empty in the container evaluator, so the container path is not reddened by (4). A consumer instance therefore receives the MANAGED layer in the bundle (Microsoft.Data.Sqlite.dll, SQLitePCLRaw.core.dll, SQLitePCLRaw.provider.e_sqlite3.dll, SQLitePCLRaw.batteries_v2.dll) and NO engine; the HOST supplies libe_sqlite3 (LocalMesh and the portal image both ship MeshWeaver.Hosting.Sqlite; the gate tester mw-plugin-test does not — the module says "unsupported host" on Linux before it ever opens the database, so nothing loads it there). That condition and that dependence on the host go away when this lands.

Ask

  • DepsClosure and PrivateClosure/ContainerReferenceSet carry runtimeTargets (native, per RID) alongside runtime assemblies — loadable natives only, the *.a/*.lib exclusion MeshModulesPublish.targets:465 already applies.
  • The bundle gains a native section (meshweaver/modulenatives/runtimes/<rid>/native/<file> or equivalent, manifest-declared like staticAssets), the packer writes it, the landing service lays it out under the module folder, and ModuleNativeAssets then finds it with no further change.
  • ContainerReferenceSet.Resolve treats a package whose only contribution is native (present in /app flat, or as runtimeTargets in the deps.json) as SUPPLIED, so a build: container module can declare its CVE pin.
  • The pack log's "declares native runtimeTargets the bundle does not carry" warning becomes a refusal once the bundle can carry them, so a native dependency can never again be dropped silently.

Refs: MeshWeaver.Plugins#1736 (the runner move that surfaced the sqlite3 dependency), Memex#323, #1728, #1043, MeshWeaver.Plugins#1710 (the iMessage module).

🤖 Filed from Claude Code.
https://claude.ai/code/session_01DqTDZe4Skkn81GuGkwMrXS

Activity

  1. rbuergi commented on Sep 12, 2026

    @rbuergi
    ContributorAuthor

    The module-side change that measured this, with the conditional native pin and the consumer-receipt statement: Systemorph/MeshWeaver.Plugins#1757.

  2. rbuergi commented on Sep 13, 2026

    @rbuergi
    ContributorAuthor

    Adjudicated 2026-09-13 against main (cdb2878bb) — stays OPEN; every claim holds, and it is not a small fix

    Read-only code audit of the eight claims in the body, each re-located on today's main:

    claim verdict where it is now
    runtime resolver probes modules/<Name>/runtimes/<rid>/native/ then flat, keyed on the requesting assembly; MeshBuilder.InstallModules subscribes it holds src/MeshWeaver.Mesh.Contract/ModuleNativeAssets.cs:48,60,131,133; MeshBuilder.cs:195
    landing service lays a bundle out flat under modules/<generation>/ + wwwroot; no native section holds (lines moved by one) src/MeshWeaver.PluginCatalog/ModuleLandingService.cs:902,908,912 — no native/runtimes string in the file
    in-image closure lane keeps runtimes/*/native/** (minus *.a/*.lib) holds memex/MeshModulesPublish.targets:465-471
    SDK path: DepsClosure takes .dll runtime entries only, detects runtimeTargets, emits a WARNING holds src/MeshWeaver.Plugin.Build/DepsClosure.cs:148-151, 152, 182-184
    container path: ContainerReferenceSet reads only runtime; Resolve marks a native-only package Supplied=false holds tools/MeshWeaver.PluginTester/ContainerReferenceSet.cs:307, 360-370
    PrivateClosure files it under Missing; ProjectBuild reds an unresolved PackageReference, warns on Missing, copies rides FLAT holds (warn is :979) PrivateClosure.cs:113; ProjectBuild.cs:951-958, 979, 1392
    bundle format has exactly two flat sections (meshweaver/modules/, meshweaver/moduleassets/) and the packer writes only those holds (ModulePackCommand.cs lives under src/MeshWeaver.Plugin.Build/, not Plugin.Packaging) NuGetPackageWriter.cs:64-79; ModulePackCommand.cs:667-681
    any code path packing runtimes/<rid>/native into a .module.nupkg none exists grep of src/MeshWeaver.Plugin.*, PluginCatalog, PluginTester, .github/scripts, memex/*.targets for `modulenatives

    One more that the body did not state and that makes the gap two-sided: the CONSUMERS filter to flat entries — ServedModuleBytes.cs:204-206 (meshweaver/modules/ with no / in the remainder) and PublishedBundleCatalogue.cs:519-521 — so an entry under meshweaver/modules/runtimes/… written by a hand-built bundle would be silently dropped, not laid out. The bundle section has to be a declared one, as the ask says.

    Commits touching these areas since the issue was written (c7262a5e8..cdb2878bb): only #4094 (adbb963eb, registry-key handling in InstanceAutoRegistrationService/RegistryUpdateReconciler) — no effect on any claim.

    Verdict

    Open, as a feature gap, not as a triage residue. It spans four components that must land together (DepsClosure/PrivateClosure/ContainerReferenceSet carrying runtimeTargets; a manifest-declared native section in the bundle + packer; the landing layout; ContainerReferenceSet.Resolve accepting a native-only package) plus the warning-to-refusal flip, and the packer/landing halves live on two sides of a registry — so it is not small and not in scope for a sweep. The module-side workaround shipped: Plugins#1757 merged 2026-09-12T20:48Z with the conditional native pin and the host-supplied libe_sqlite3 dependence the body describes.

    Conserved: the gap is now stated on Doc/Architecture/Modules → Native assets (the section that says modules can ship natives now says which lane that is true for), in the same PR as this sweep's other records.

    Read-only, code at origin/main cdb2878bb. No build, no mesh, no registry.

    PR: #4152

  3. rbuergi commented on Sep 13, 2026

    @rbuergi
    ContributorAuthor

    Build ticket — parked on Roland's word ("never mind build tickets for now"). Today's state was already recorded above at 07:44Z (all eight claims hold on main, not a small fix); nothing added and no work done.

  4. rbuergi commented on Sep 13, 2026

    @rbuergi
    ContributorAuthor

    Staged, and stage 1 is built: #4239

    The prior pass's verdict ("a four-component feature, not small") holds. Re-derived, it is four
    stages
    , and the reason is not size — each depends on what the one before establishes, and stage 4
    would break a working module if landed early. Stage 1 (derive + carry) is #4239.

    🚨 One thing this issue did not state, and it is the reason the measured case was invisible

    The body says DepsClosure detects runtimeTargets and turns them into a warning. It does — for a
    package that ALSO contributes a managed assembly. The loop short-circuited first:

    if (!nodes.TryGetValue(name, out var node) || node.RuntimeFiles.Count == 0)
        continue;                                    // ← a pure-native package leaves here

    SQLitePCLRaw.lib.e_sqlite3 contributes no managed assembly at all. It was continued before the
    native branch ran, so the warning could not fire for it. Measured on unmodified main (d1556d7a37)
    with a probe written against main's own API:

    a package whose ONLY contribution is native must not vanish without a word — warnings=0, files=0
    

    Zero warnings, zero files. So this was not "warned about and dropped" for the case it was filed on —
    it was dropped in total silence.

    Stage 1 — #4239 (derive + carry)

    • DepsClosure.Result.Natives — NativeAsset(RelativePath, Rid, Package) from assetType: "native"
      entries; the RuntimeFiles.Count == 0 short-circuit is gone.
    • A third manifest-declared bundle section, meshweaver/modulenatives/, path PRESERVED (the path IS
      the contract — ModuleNativeAssets probes <moduleDir>/runtimes/<rid>/native/<lib>). It cannot be
      meshweaver/modules/ (every consumer filters to entries with no /, so a native there is silently
      skipped, and ModulePublish.Validate declines the flat spelling) and it should not be
      meshweaver/moduleassets/ (static WEB assets — conflating a loadable binary with a served file
      makes every future rule about one apply to the other).
    • Three things still dropped, each now named: a native at a layout the loader does not probe, a
      RID-specific MANAGED asset (assetType: "runtime"), and .a/.lib.

    What each remaining stage IS, and what it depends on

    stage what depends on
    2 — landing ModuleLandingService.LandModule/ShelveModule/LandCore take the natives and write them under modules/<generation>/; PluginBundleClient.LandFromBundle passes them. ValidateAssetPath already accepts the shape and the write loop already handles trees — so it is signatures plus GenerationIdOf, which MUST fold them into the hash or two bundles differing only in natives collide on one generation directory. nothing but itself
    3 — registry re-serve ModuleBundleSource.CollectVersion and ServedModuleBytes.FromSealed collect flat files + wwwroot/** only, so a registry re-serving a shelved module drops the section. (2), which defines the layout it re-serves
    4 — the container lane ContainerReferenceSet.cs:307 reads only runtime; :360-370 Resolve marks a native-only package Supplied=false, so a build: container module cannot DECLARE the CVE pin (ProjectBuild.cs:951-958 reds it). Plus PrivateClosure carrying natives and ProjectBuild.EmitPrivateClosure laying them out. independent of (2)/(3) — it is the other closure derivation
    5 — warning → refusal the ask's last bullet. ALL of the above. Today a module needing a native the bundle cannot carry still has the host-supplies-it fallback; refusing now breaks it with no route forward

    🚨 Until stage 2 lands, a native rides the bundle and is not written to disk — carried, declared,
    ignored. That is deliberate and is why #4239 ships no What's New entry: the user-visible capability
    belongs with stage 2, when a module author can actually ship one. MeshWeaver.AppleMessages keeps its
    conditional pin and its host-supplied libe_sqlite3 (Plugins#1757) until then.

    Neither node-repo-module-pack.yml:2046-2051 (--with refuses a path component) nor :2349-2358
    (the jq provenance gate over module.assemblies) is disturbed by stage 1, because the natives are a
    separate manifest section rather than flat closure entries.

    Conserved on Doc/Architecture/Modules → "The registry bundle: derived and CARRIED (#4126, stage 1)",
    replacing the gap statement the 2026-09-13 sweep wrote there.

    Keeping this open for stages 2–5.

    🤖 Generated with Claude Code

  5. added 2 commits that reference this issue on Sep 13, 2026
  6. rbuergi commented on Sep 14, 2026

    @rbuergi
    ContributorAuthor

    The FORMAT question is answered, measured rather than reasoned — #4318

    The one half of this issue that could be settled without touching production code: does the new
    meshweaver/modulenatives/ section (stage 1, #4239) need a FORMAT VERSION?

    🚨 A bundle is read by portals running older images — the registry serves one set of bytes to
    every installation — so "the format gained a section" is only safe if an older reader ignores
    it. Nothing asserted that, and it is not reachable from a producer-side test: the property belongs to
    code that is not in this build.

    Verdict: ADDITIVE. No format version, no manifest schemaVersion, no dual-write.

    Two independent facts, now pinned by ABundleCarryingNativesIsReadUnchangedByAPreNativesReader
    rather than argued:

    why an older reader is unaffected
    the archive the three sections are prefix-disjoint. Every pre-#4126 consumer of the flat folder filters meshweaver/modules/ with no / in the remainder (ServedModuleBytes, PublishedBundleCatalogue); the asset consumer filters meshweaver/moduleassets/. A native is under neither prefix, so an older reader does not skip it, mis-file it or fail on it — it never enumerates it at all
    the manifest BundleReader deserializes with JsonSerializerDefaults.Web, whose UnmappedMemberHandling is Skip. The test deserializes the real packed manifest into PreNativesManifest/PreNativesModule — the shape as it stood before #4126, spelled out locally so adding a property to the real Manifest can never quietly update it — and every pre-existing field survives

    🚨 What WOULD have needed a version, and is exactly why the section is its own: under
    meshweaver/modules/ an older reader's flat filter silently drops the natives while the producer
    believes it shipped them; under meshweaver/moduleassets/ an older reader lays a loadable binary
    into wwwroot
    and serves it over HTTP.

    The controls, each falsified

    Four, and none decorative — all four driven RED:

    sabotage which control fired
    the bundle carries no native Assert.Single on the entry — "it ignored it" and "there was nothing to ignore" are otherwise the same green
    the manifest declares no nativeAssets the Disallow control — it exists to prove the JSON really carries the unmapped member, so the Skip assertion cannot pass over a document with nothing new in it
    the native written inside meshweaver/modules/ the prefix-disjointness assertion
    the native written at a third, undeclared prefix (found in review) the full-path assertion — "somewhere outside the other two" is not the same statement as "at the declared path", and the path IS the contract: ModuleNativeAssets composes its probe from exactly four segments and has no recursive walk

    Incidental, and worth keeping

    The manifest entry is written with a UTF-8 BOM, and Utf8JsonReader skips one over a stream
    but not over a span — reading the entry's bytes directly fails with '0xEF' is an invalid start of a value. BundleReader uses the stream overload and so must anything else. The first draft of this
    test hit it; it is on Doc/Architecture/Modules now.

    The other half of the format question: the container derivation

    Can both closure derivations keep runtimeTargets? DepsClosure (SDK) does, since stage 1.
    ContainerReferenceSet (container) does not — and the reason is a policy line, not a data limit:
    it reads only the runtime section of the image's deps.json, and runtimeTargets sits in the same
    document
    ; the refusal of a native-only PackageReference is Resolve matching a package by the
    assembly it contributes, so a package contributing only natives answers Supplied=false. Nothing
    about deps.json prevents stage 3.

    Staying open

    Stages 2–5 as recorded on this thread and on Doc/Architecture/Modules — landing, registry re-serve,
    the container lane, warning → refusal. Until stage 2 a native still rides the bundle and is not
    written to disk: carried, declared, ignored. MeshWeaver.AppleMessages keeps its conditional pin and
    its host-supplied libe_sqlite3 (Plugins#1757).

    Read-only against production code: this change set adds a test and a doc section and alters no
    src/ behaviour.

    🤖 Generated with Claude Code

  7. rbuergi commented on Sep 14, 2026

    @rbuergi
    ContributorAuthor

    Landed — c0fc2abb60b8a16ecddbc3e99c8fefc009df4c8e (#4318)

    The format half of this issue is settled: the meshweaver/modulenatives/ section added by stage 1
    is ADDITIVE. No format version, no manifest schemaVersion, no dual-write.
    The detail is in the
    comment above; this is the landing note.

    What it changes for stages 2–5: nothing has to be co-ordinated with the fleet's running images.
    A bundle carrying natives is read unchanged by a portal on an older image — pinned now by
    ABundleCarryingNativesIsReadUnchangedByAPreNativesReader, with four anti-vacuity controls each
    driven red, including the one this review round added (the native written at a third, undeclared
    prefix — "somewhere outside the other two" is not the same statement as "at the declared path").

    So stage 2 (landing) can be built and shipped on its own schedule without a compatibility window,
    which was the open question it would otherwise have had to answer first.

    Staying open for stages 2–5, as recorded on this thread and on Doc/Architecture/Modules:
    landing, registry re-serve, the container lane, and warning → refusal. Until stage 2 a native still
    rides the bundle and is not written to disk — carried, declared, ignored — and
    MeshWeaver.AppleMessages keeps its conditional pin and its host-supplied libe_sqlite3
    (Plugins#1757).

    🤖 Generated with Claude Code

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