Repository navigation
Sync licence: the registry must authorize an INSTANCE to replicate a package, and hand it a short-lived token — per-repo PATs (MW_PLUGINS_REPO_TOKEN) do not scale #1835
Description
The problem
Consumers that need a plugin's content today get a per-repo GitHub PAT. Systemorph/MeshWeaver.Education holds one as the repo secret MW_PLUGINS_REPO_TOKEN (created 2026-07-13, never rotated), used at four actions/checkout sites against Systemorph/MeshWeaver.Plugins on origin/main:
| line | job | scope |
|---|---|---|
| 390 | e2e-install |
full repo |
| 584 | e2e-mesh |
full repo |
| 667 | e2e-journeys |
full repo |
| 805 | publish-bake |
sparse: Edu Store Publish Video |
(Not to be confused with MW_PLUGINS_REPO, the env var holding the filesystem path to that checkout — one letter apart, and a stale value of the latter has already invented a false root cause in an education e2e investigation.)
MeshWeaver.Manufacturing now needs Publish's Deck/Slide types for its Demo/Tour node, and the request that arrived was "issue another PAT, same as Education". That does not scale: one personal credential per (consumer × source repo), each a standing whole-repo grant, none revocable centrally, none expressing which package is needed. Five plugin repos and growing.
The registry exists precisely so this is unnecessary. Doc/Architecture/PluginRegistry: "Onboarding a new installation is point PluginCatalog:RegistryUrl at the registry, not provision it a GitHub App." A consumer holding its own GitHub PAT is that principle inverted.
What already works (do not rebuild)
- Consumer → registry authentication.
mwi_instance key: 32 random bytes, SHA-256-hashed at rest, routed throughMeshWeaverInstanceIndex, resolved byInstanceRegistryAuthenticator.Bearer, plusBasicbecause a NuGet client cannot sendBearer. Kill switch viaMeshWeaverInstance.IsDisabled. - The registry brokering GitHub. The
meshweaver-cloudGitHub App (org-wide,contents:write) is the one credential;/api/pluginsand/api/plugins/filesre-serve, and the credential never leaves. Verified on72cd0a53d: both routes callcaller.Allows(source.Name, package.Id), and/filesanswers 404, not 403, for an ungranted package so it cannot be used as an existence oracle. - A credential-exchange endpoint, already in this shape.
mwr_bootstrap key →POST /api/instances/register→ the instance's ownmwi_returned once.RegistrationKeyalready carriesExpiresAt, revocation and use-counting.
Two things are missing.
Gap 1 — there is no SYNC LICENCE, and it must not be borrowed from the user entitlement
The consumer here is another MeshWeaver instance, and what it needs permission for is sync — replicating a package from the registry into itself. That is its own kind of licence, and the codebase has no representation of it. Three separate concepts, currently two-plus-a-hole:
| subject | right | today | |
|---|---|---|---|
Package.License |
nobody — it is a label | what the copyright holder permits (SPDX id, Apache-2.0) |
✅ exists, correctly nullable ("unspecified" stays unspecified) |
Store entitlement (_Entitlements/{viewer}, PackageEntitlement.Authorize) |
a user | install & use in their own portal / home | ✅ exists |
| sync licence | a MeshWeaverInstance |
pull/replicate the package from the registry | ❌ missing |
PluginGrant is currently standing in for the third row, and it is not a licence — it is a hand-written ACL at Admin/_PluginGrant/{instanceId} with entries and nothing else: no issuer, no term, no expiry, no commercial basis, no renewal. Today's entire live table is three nodes (memex, memex-cloud, client-i), each hand-edited Plugins/*. Every new consumer means an admin editing a node by hand — the part that cannot be done for all plugins.
🚨 Deriving the sync right from the user-purchase entitlement would be wrong, not merely imprecise. They differ on every axis:
- A user's purchase entitles that user, on their portal. It says nothing about an arbitrary instance being allowed to pull the source and hold a copy.
- A sync licence is needed for free packages too —
Plugins/*to a partner's mesh, where no purchase exists at all — so purchase-derivation cannot be the mechanism. - The commercial terms are different in kind: a per-instance replication/redistribution right versus a per-seat use right. They will be priced, termed and renewed differently.
Ask: introduce the sync licence as a first-class type whose subject is a MeshWeaverInstance and whose right is sync, with an issuer, a term and revocation. The registry authorizes against that. PluginGrant either becomes its projection or remains only as an explicit partner/override escape hatch. Preserve the current posture: fail closed, 404-not-403, and DefaultGrants never seeding private or paid sources.
Gap 2 — no short-lived, scoped token
mwi_ is a durable static bearer. MeshWeaverInstance has KeyIssuedAt but no expiry, no refresh, no exchange; re-issuing is a manual Settings action. A consumer that needs registry access holds a long-lived secret forever.
Ask: add the OAuth-style leg — exchange the durable credential for a short-lived access token scoped to the packages the instance's sync licence actually covers, presented per request. This is a further key type on an established pattern, not new machinery: RegistrationKey already has expiry + revocation + use-counting, and InstanceKeys already documents why the prefixes are kept disjoint (mwi_ vs mwr_ vs personal mw_) so no validator can accept another's key by accident.
Why this dissolves the original request
CI is already another MeshWeaver instance — that is the part that makes this fit rather than being a special case. Education's e2e-install / e2e-mesh / e2e-journeys jobs literally boot a disposable mesh (e2e/mesh/up.sh), and the mw-plugin-test gate imports node repos into a fresh SQLite-backed mesh. Those meshes are consumers in exactly the sense meant here. They just get their content from a PAT-checked-out directory handed to them as MW_PLUGINS_REPO instead of by holding a sync licence and pulling.
With both gaps closed, such an instance registers (ephemerally), holds a sync licence for what it is entitled to replicate, exchanges its credential for a short-lived scoped token, and pulls Plugins/Publish via POST /api/plugins/files — which returns {files:[{relativePath, content}]}, exactly the node-repo tree the gate stages into its /repo mount. A one-for-one replacement for actions/checkout. No PAT, no GitHub access, nothing to rotate by hand.
Then MW_PLUGINS_REPO_TOKEN is deleted from MeshWeaver.Education and its four sites migrate. All four — publish-bake is easy to miss and behaves differently: it stages the four Plugins-owned modules so the courses' requires resolve, then deletes Edu/Store/Publish/Video.zip from the bake output so the same packages are not seeded from two sources.
Notes
- Core Bundle route authenticates but never AUTHORIZES: any valid instance key can download every installed package's bundle #1772 (bundle route authenticated but never authorized) closed 2026-08-17. That was
PluginBundleEndpoints, a different route from the two above — but confirm the fix is in the image memex actually runs before relying on per-package scoping anywhere. MeshWeaver.Manufacturingcurrently holds onlyACR_USERNAME/ACR_PASSWORD, so it has no such token today and nothing regresses by not issuing one.- Requested by Roland: consumers should not hold repo credentials — memex.meshweaver.cloud checks the licence and operates the GitHub connection on their behalf; the licence permitting sync is its own kind, and the consumer is another MeshWeaver instance.
Activity
[-]Registry must broker GitHub access against a LICENSE with short-lived tokens — per-repo PATs (MW_PLUGINS_REPO_TOKEN) do not scale[/-][+]Sync licence: the registry must authorize an INSTANCE to replicate a package, and hand it a short-lived token — per-repo PATs (MW_PLUGINS_REPO_TOKEN) do not scale[/+]rbuergi commentedon Aug 18, 2026
@sierragolflima — this is the outcome of your PAT request, and there is something for you to do at the end.
Short version
You were right that MeshWeaver.Education does it with a PAT (
MW_PLUGINS_REPO_TOKEN, created 2026-07-13, fouractions/checkoutsites onorigin/main). That is a precedent nobody sanctioned rather than the standard, and it is being retired — so please don't copy it. You get the same capability through the registry instead, scoped to exactlyPublishrather than to the whole repository.What was built (MeshWeaver#1844)
PluginGrant— an instance's right to replicate a package — now carries the terms it was issued under:ExpiresAt(per entry),IssuedUnderLicense,IssuedVia.SyncLicenseServiceis the one writer, so grants stop being hand-edited nodes.POST /api/instances/token. A registered instance exchanges its durablemwi_key for a short-lived, scopedmwa_token. It carries identity and scope but never authority — the live licence is re-read on every request, so a revocation lands immediately rather than at token expiry — and a token can never mint its successor.enc:-protected, self-provisioning. Nothing to configure.RequiresAcceptanceandLicenseAcceptancehad shipped as node types with no callers at all.Your Manufacturing PR
The gate failure there was never about the PAT — it was two stacked problems, and the logs name both:
The first is names registered nowhere; the second is the right names with the package not mounted. Systemorph/MeshWeaver.Manufacturing#5 now fixes both — the retype, plus
scripts/fetch-registry-package.py, which pullsPublishfrom the registry into the gate's mount. Same result as your checkout step, no repository access.What you need to do (2 minutes)
manufacturing-ci. Themwi_key is shown once; copy it then.MW_REGISTRY_INSTANCE_KEYonSystemorph/MeshWeaver.Manufacturing.The admin half is already done:
Admin/_PluginGrant/manufacturing-cigrants exactlyPlugins/Publishand nothing else. Registration is self-service by design; the grant is not, which is the whole point — and the key is deliberately yours to hold rather than something I mint and paste into a log.If you need another package later, ask for a grant rather than a token — that is the part that scales.
rbuergi commentedon Aug 19, 2026
A second consumer, with the measurement already done: MeshWeaver.Manufacturing
Recording this here because it is exactly the shape this issue describes, and it is currently blocking a red
main.MeshWeaver.Manufacturing's gate needs one package —Publish— because its tour deck is typedPublish/Deck/Publish/Slide. The gate mounts only its own checkout, so today the only way to satisfy it is a GitHub PAT with read on the whole ofMeshWeaver.Plugins. That is the "no smaller thing to ask for" pressure, verbatim.Measured locally with
mw-plugin-teston the pinned image:ManufacturingaloneInstall of 'Manufacturing/Demo/Tour' failed: NodeType 'Publish/Deck' is not registeredManufacturing+Publish+Store[PASS] Manufacturing (86 node(s), 9 type(s))·[PASS] Publish (16 node(s), 2 type(s))So the requirement is precisely "replicate these two packages", not "read this repository" — a licence-sized ask.
Its
mainhas been red since 2026-08-10 (all four open PRs blocked behind the same gate), and I have deliberately NOT provisionedMESHWEAVER_REPO_TOKENthere, since #1844's follow-ups already include retiring the equivalent in MeshWeaver.Education. Issuing a PAT now would mean creating the thing being removed and then removing it. Noted on Systemorph/MeshWeaver.Manufacturing#5.Worth counting as a data point for scope: the consumers wanting exactly this are now Education (four checkout sites) and Manufacturing (one gate, two packages). Manufacturing is the cleaner pilot — a single job, two packages, and a red main that would prove the exchange end to end the moment it works.