fix(plugin): let free users install the free half of a tier pair - #432
Merged
Merged
Conversation
`nself plugin install cron` and `nself plugin install notify` failed for
every operator without a license key:
error installing "cron": plugin "cron" requires a license key
Both slugs are served TWICE by the registry — once free, once pro — and
ResolvePlugin already picks correctly between them by entitlement,
defaulting to free. It never got the chance. installLocked's Step 1 ran
`if isPaidPlugin(name) { checkLicense(...) }` BEFORE fetching the
registry, off the static 59-name paidPlugins allowlist. Tier is not a
property of a name, so that check cannot answer the question for a tier
pair; it saw "cron" in the map and refused.
The two plugins this locked out are exactly the two the published ɳTask
docs advertise as free ("Powered by the free cron and notify plugins, no
pro plugin required").
Fix: move the license check below tier resolution and drive it from the
resolved manifest via isPaidPluginManifest, which reads the registry's
own tier / requires_license fields. This also restores the load order
manager.go documents as frozen (registry fetch -> license check ->
checksum verify).
Gating gets STRICTER, not looser: the static allowlist covered 21 of the
44 licensed-only plugins, so the other 23 — claw-budget, claw-news,
family, mcp and friends — were never license-checked at Step 1 at all.
Registry metadata covers all 44. Verified on a clean isolated HOME
against the live registry: cron and notify install and land tier=free
requires_license=false, while ai, activity-feed, claw-news, mcp and
family are all still refused without a key.
internal/bundle/installer_helpers.go carried the same drift, AND-ing the
name map into a manifest check so a wholly free bundle containing cron or
notify demanded a key. The manifest field alone is authoritative there.
installer_locked.go was exactly at the 300-line cap, so the post-install
reporting tail moves to installer_finish.go as a pure move rather than
raising the budget.
TestInstall_PaidPluginRequiresLicense reached the live registry over the network and only appeared to pass: the license gate ran off the paidPlugins NAME map before any fetch, so the registry response never mattered. With the gate moved below tier resolution it started failing on windows-2022, where the primary registry was unreachable and the GitHub raw fallback — the FREE registry — carries no "ai" entry, so the error became "plugin not found in registry" rather than a license error. Serve the registry from httptest instead, and set USERPROFILE alongside HOME. os.UserHomeDir reads USERPROFILE on Windows, so the old test left the real cache and license dir reachable there; that difference is why this passed locally on macOS and failed only in Windows CI. The test now exercises the path it claims to: fetch -> resolve -> gate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
nself plugin install cronandnself plugin install notifyfailed for every operator without a license key, even though a free variant of each is published withrequires_license: false.Root cause
Both slugs are served twice by the merged registry (free, then pro).
ResolvePluginalready chooses between them by entitlement and explicitly defaults to free — but it never ran.installLockedStep 1 executedif isPaidPlugin(name) { checkLicense(...) }before the registry fetch, using the static 59-namepaidPluginsallowlist. Tier is not a property of a name, so that map cannot answer the question for a tier pair.The two plugins this locked out are the two the published ɳTask docs advertise as free: "Powered by the free
cronandnotifyplugins, no pro plugin required."Fix
Move the check below resolution and drive it from the resolved manifest (
isPaidPluginManifest), which reads the registry's owntier/requires_license. This also restores the ordermanager.godocuments as frozen: registry fetch → license check → checksum verify.This tightens gating rather than loosening it
The static allowlist covered 21 of the 44 licensed-only plugins. The other 23 (claw-budget, claw-news, family, mcp, …) were never license-checked at Step 1 at all. Registry metadata covers all 44.
Verified on a clean isolated
HOMEagainst the live registry:cron,notifytier=free requires_license=falseai,activity-feed(in old allowlist)claw-news,mcp,family(absent from old allowlist)Also
internal/bundle/installer_helpers.goAND-ed the same name map into a manifest check, so a wholly free bundle containing cron/notify demanded a key.installer_locked.gosat exactly on the 300-line cap; the post-install reporting tail moves toinstaller_finish.goas a pure move instead of raising the budget.TestInstallGate_NotGatedBeforeRegistryFetchis a structural guard — confirmed failing against the pre-fix file and passing after.