Plugins
A plugin is not a package — it is a repo of ordinary mesh nodes. MeshWeaver ships dynamic content (node types, whole modules, docs, sample data, AI content) from a git repository of node repos, installed with the framework's existing GitSync and compiled live on the mesh. There is no manifest format and no NuGet — the node is the manifest, and the mesh versions it.
This is deliberately the framework replicated by nothing: it reuses Static Repo Import, Node Type Compilation, and the mesh's node/version machinery.
A plugin is mesh nodes
Compatibility is API-based. A plugin should continue working across platform and dependency versions while the APIs it uses remain compatible. Version/build-identity differences alone are not runtime refusals; missing or incompatible referenced APIs are. Follow the Module Adoption Policy for measured loadability, continuity and eager adoption. Preserve a working generation when a newer one cannot load, and report the actual missing contract. Dependency declarations describe intent; they do not replace measurement. For source NodeTypes, an unusable prebuilt cache entry normally means compilation against the current platform, rather than holding the platform to that entry's build.
A node repo is exactly the on-disk shape the sample partitions use — a *.json per node plus its
Source/ (and Test/) C# — e.g. the Publish plugin and its Slide type:
Publish/index.json the plugin ROOT — nodeType "Store/Plugin", content PluginContent (INSIDE the folder)
Publish/Slide/index.json a NodeType node (Content = NodeTypeDefinition{ configuration })
Publish/Slide/Source/*.cs the content type + layout areas — compiled live
Publish/Slide/Test/*.cs the type's own tests — compiled together with Source
Publish/manifest.lock CI-maintained content fingerprint (per-file sha256 + moduleVersion)
A child NodeType may equally be a sibling Widget.json next to a Widget/Source/ folder (the shape
Edu/ and LinkedIn/ use) — index.json inside the folder and a sibling <Type>.json are both
valid. Only the plugin root must be index.json inside the folder (below).
The plugin folder is the unit of import, so its partition root lives inside it as index.json
(the NodeFileMapper mapping GitSync uses) — a sibling <Plugin>.json outside the folder would not
be part of the partition.
| Concept | Where it lives (node-native) |
|---|---|
| The "manifest" | the plugin root (<Plugin>/index.json, nodeType: "Store/Plugin") — its PluginContent carries the cover, price, entry point and install paths |
| The "kind" (content vs code) | the child's NodeType — a NodeType-with-Source/ vs plain content |
| The "version" | the node's version, mesh-tracked, bumped on every change — nobody hand-bumps a field |
| The "installer" | GitSync — GitHubSyncService.ImportFromGitHub / StaticRepoImporter |
| "what to install" | the registry's per-instance PluginGrant (entitlement) plus the installation's PluginCatalog:InstallByDefault / InstallPreInstalledPackages (what it actually installs) |
The plugin root is a Store/Plugin node
The root <Plugin>/index.json is nodeType: "Store/Plugin" — the Store's generic root type —
carrying a PluginContent (the cover Body, Description, Video/Poster, the OgImage
social preview, EntryPoint, MarketingPath, SetupPath, InstallPaths, price). Card-level
identity (Name, the one-line Description tagline, Icon, Category) stays on the node itself. The
Store/Plugin type owns the install funnel: the anonymous cover, the auto-gated children
(AddPluginGating) and the store card.
🚨
Store/Plugin,PluginContentandAddPluginGatingare not framework code in this repo — they are themselves a dynamic node type, shipped by theStoreplugin inSystemorph/MeshWeaver.Plugins(Store/Plugin/Source/*.cs) and compiled live on the mesh. Grepping this repository for them finds nothing by design. The canonical reference for the repo shape isMeshWeaver.Plugins/AGENTS.md.
That boundary is also why the installer addresses the gating pass's cover grant as a well-known PATH rather than asking the type — see Install Readability for the two doors an install can open, and why waiting on the optional one cost every install 30 seconds.
- Exception — a
Spaceroot is correct only when the root's own content IS a partition-levelNodeTypeDefinitioncompiling a sharedSource/model (e.g.UWDeepfield, inMeshWeaver.Reinsurance); retyping it toStore/Pluginwould kill that compile. - Deprecated — a
PluginManifestroot is the pre-"Store retype" form. A bare manifest root has no home page and reads as broken, so don't author new plugins this way.
Install = GitSync
Installing a plugin is importing its node repo into the mesh. ImportFromGitHub(repo, ref, space, …)
fetches the git folder and parses each file through FileFormatParserRegistry — which handles the
.json node files and the .cs Source, keying each node's type off the parsed nodeType. A
NodeType node + its Source/*.cs land as a NodeType with Code children, and the mesh's first-build
compile makes the type live. No app rebuild, no NuGet.
The version is the node's version, so "update available" is a git diff, not a hand-edited number.
The registry — one credentialed hub, many consumers
GitSync needs credentials for a private plugins repo — and you don't want every installation to hold them. So one MeshWeaver instance (memex) is the registry: it alone holds the source credential, syncs the plugins repo into its mesh, and re-serves plugins over HTTP. Every other installation pulls from the registry, never from git — the credential is encapsulated in the registry, exactly like npm / NuGet (the registry has source access; clients just speak HTTP).
The surface is two endpoints on the registry (PluginRegistryEndpoints), backed by its configured
git sources (the plugins repo). They are not public: a caller presents its registered instance
key (Authorization: Bearer mwi_…), and both the listing and the file fetch are scoped to that
instance's admin-owned PluginGrant. Without a valid key the request is 401 —
PluginCatalog:RequireInstanceKey defaults to true, so a registry that configures nothing refuses
anonymous callers.
| Verb | Returns |
|---|---|
GET /api/plugins |
{ packages:[PackageManifest…] } — the calling instance's granted modules from the configured sources (node-native <Plugin>/index.json Store/Plugin roots by default) |
POST /api/plugins/files {id} |
{ files:[{relativePath, content}…] } — the files that plugin (by id) ships |
A consuming instance reads this through RegistryPackageSource and installs — from the Store
(the PluginCatalog node's Catalog area) on a click, or unattended on first boot via
PluginCatalog:InstallByDefault / InstallPreInstalledPackages. Either way the package's files are
parsed into nodes and upserted locally — a Code package synthesizes its NodeType + Source
Code and compiles live; a Content package imports its folder. The registry ships the
capability, never data instances — and no GitHub credential lives on the consumer at all. Full
reference: Plugin Registry.
Dynamic node types — a module that compiles itself
A NodeType node whose Source/*.cs defines its content type + layout areas is a dynamic node
type: the mesh compiles it with Roslyn on install and serves instances immediately. Proven examples
shipped this way: Publish/Slide, the whole Store (including Store/Plugin itself), and the
education types Edu/Lesson, Edu/Module, Edu/Exercise, Edu/Quiz. Each is gated in the
plugins repo's own CI (Plugin Catalog CI → Compile every NodeType via scripts/compile-check.py,
then Compile + render node repos which imports each repo into a real portal image and runs its
in-node tests) — that compile is never covered by this repository's dotnet build or test suite.
Rules the runtime compile enforces (each load-bearing when migrating a compiled module):
- Explicit usings — implicit/global usings aren't injected; each Source file imports what it uses.
- Public surface only — an
internalhelper in a framework assembly is invisible to the compiled node-type assembly (inline an equivalent). - Identity is the install path — a dynamic type's instances carry the NodeType node's path
(
Edu/Lesson), so logic that matches "my type" derives it from the node, never a hardcoded name. - Read foreign content untyped — a sibling type compiled in another assembly resolves to a
JsonElementhere; read its fields off the JSON rather than referencing the foreign type. Test/*.csis plain C# — the compile references only the runtime + loaded MeshWeaver assemblies, so in-node tests can't use a test framework (xUnit); they assert by throwing.
What is (and isn't) a dynamic node type
A dynamic node type is compiled content + layout areas, per node. That covers a large class — anything self-contained (Slide, the Edu course pages). It cannot, on its own, host:
- cross-hub type registration (registering sibling content types on the mesh hub for polymorphic routing), or
- a control-plane service (e.g. the exercise-attempt validation watcher).
Modules that need those (the exercise fork/validate control plane, AI agent execution) keep their service layer compiled while their content + self-contained types ship as node repos. Turning the service layer itself into git-delivered plugins is a separate capability (a boot-time module loader).
The migration this enables
The same primitive — a node repo in a git source, imported into a running mesh — is how the static node repos (Doc, Agent, Skill, samples) and module content move off the image and onto git delivery: their content exports to the plugins repo (GitSync), and their self-contained types become dynamic node types. Everything from git, versioned by the node, no NuGet.