Skip to content

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

Closed

Description

@rbuergi

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)

  1. Consumer → registry authentication. mwi_ instance key: 32 random bytes, SHA-256-hashed at rest, routed through MeshWeaverInstanceIndex, resolved by InstanceRegistryAuthenticator. Bearer, plus Basic because a NuGet client cannot send Bearer. Kill switch via MeshWeaverInstance.IsDisabled.
  2. The registry brokering GitHub. The meshweaver-cloud GitHub App (org-wide, contents:write) is the one credential; /api/plugins and /api/plugins/files re-serve, and the credential never leaves. Verified on 72cd0a53d: both routes call caller.Allows(source.Name, package.Id), and /files answers 404, not 403, for an ungranted package so it cannot be used as an existence oracle.
  3. A credential-exchange endpoint, already in this shape. mwr_ bootstrap key → POST /api/instances/register → the instance's own mwi_ returned once. RegistrationKey already carries ExpiresAt, 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.Manufacturing currently holds only ACR_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

  1. changed the title [-]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[/+] on Aug 18, 2026
  2. rbuergi commented on Aug 18, 2026

    @rbuergi
    ContributorAuthor

    @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, four actions/checkout sites on origin/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 exactly Publish rather than to the whole repository.

    What was built (MeshWeaver#1844)

    • The sync licence. A PluginGrant — an instance's right to replicate a package — now carries the terms it was issued under: ExpiresAt (per entry), IssuedUnderLicense, IssuedVia. SyncLicenseService is the one writer, so grants stop being hand-edited nodes.
    • POST /api/instances/token. A registered instance exchanges its durable mwi_ key for a short-lived, scoped mwa_ 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.
    • The signing key is a mesh node, minted once per registry, enc:-protected, self-provisioning. Nothing to configure.
    • Licence acceptance is enforced — RequiresAcceptance and LicenseAcceptance had 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:

    before the retype:  NodeType(s) not registered: Deck, Slide
    after the retype:   NodeType(s) not registered: Publish/Deck, Publish/Slide
    

    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 pulls Publish from the registry into the gate's mount. Same result as your checkout step, no repository access.

    What you need to do (2 minutes)

    1. Register an instance at https://memex.meshweaver.cloud — Settings ▸ Instances — with the id manufacturing-ci. The mwi_ key is shown once; copy it then.
    2. Set it as the repo secret MW_REGISTRY_INSTANCE_KEY on Systemorph/MeshWeaver.Manufacturing.

    The admin half is already done: Admin/_PluginGrant/manufacturing-ci grants exactly Plugins/Publish and 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.

  3. rbuergi commented on Aug 19, 2026

    @rbuergi
    ContributorAuthor

    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 typed Publish/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 of MeshWeaver.Plugins. That is the "no smaller thing to ask for" pressure, verbatim.

    Measured locally with mw-plugin-test on the pinned image:

    mount result
    Manufacturing alone ❌ Install of 'Manufacturing/Demo/Tour' failed: NodeType 'Publish/Deck' is not registered
    Manufacturing + 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 main has been red since 2026-08-10 (all four open PRs blocked behind the same gate), and I have deliberately NOT provisioned MESHWEAVER_REPO_TOKEN there, 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.

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