Extensions without Get

Maintainer, 2026-11-11, after installing Electronic Signature on a self-hosted instance: "it is absolutely horrific, the install process. lots of blabla get blabla 'start course' wtf?! blabla then somewhere the config. no get, just the settings and finish. no blabla" — and, on the cover: "additional page can do marketing but should never be seen".

The path he had to walk: Settings → Extensions → a shelf card with a long description, which was a link to the package's cover → Get → ▶ Start Course → find the Signing authority tab somewhere else → enter the credentials.

The rule

An in-app extension of the person app (hostedIn: "{user}/Settings") that the viewer is covered for has no acquisition step and no course button. Its settings tab is simply there, and its one action, anywhere it appears, is Open settings. The path is settings tab → choose → fill → done.

What does what

Part Where What
The tab appears without Get PluginContent.OpensByPlan (Store/Core) A PAID person-app extension is plan-gated by construction — no planGated declaration needed. The gating pass writes the tier-group grants (PlanGroups.TierGrant, Store/AppsOnTheInstance §3a), so a viewer whose plan covers the tier reads the package, and the extension's tab (a UiContribution gated requireAddressAccess) shows. An instance plan that covers it opens it to every signed-in viewer, as before. A lapse hides the tab, a renewal restores it. Every former PlanGated read goes through OpensByPlan: PluginGate.SeedGating, PlanAccess.PlanGatedGrantsFrom, Subscriptions.Unlock (mints nothing), the paywall, CoverContract.IsOpenToAll
Free extensions unchanged The free plan has no tier group (validate-repos.py refuses planGated on a free tier for the same reason), so a free extension keeps its one Get. Making that implicit needs a decision on free access — see Open below
Only a course says "Start Course" CoverContract.StartKindOf ONE classification: a person-app extension is Settings; an app: true package is App; a package in the Education category or hosted in Edu/Courses is Course; everything else is App. An unknown category is never a course. The cover's primary button, the catalog card (StoreCatalogLayoutAreas.GetLabel) and the install dialog's way onward (CoverContract.OnwardLabel) all read it. Before, the rule was "an app says Start, everything else is a course"
One action: Open settings PluginLayoutAreas.CoverCtaButtons, InstallLayoutAreas.InstallStepButtons A viewer who has the extension gets exactly one button, Open settings →, to /{viewer}/Settings/{settingsTab} (CoverContract.SettingsHref). No Start, no Get / Repair / Uninstall step, no hero links into the cover. A viewer who does not have it keeps the funnel's one plan affordance
settingsTab PluginContent.SettingsTab The id of the extension's person-app UiContribution node. Absent → /{viewer}/Settings, where the tab sits in the navigation
The cover is never on the path ExtensionShelf.Shelf Shelf cards no longer link to the package's cover (WithDisableNavigation); the action area is the card's only way anywhere. The install dialog, if a legacy path still opens it for an extension, lands on the settings tab (InstallDialog.OnwardHref), never on an entry point or the cover
No blabla ExtensionShelf.PersonAppPage The Extensions tab is the shelf alone: no heading repeating the tab's label, no intro explaining a Get that no longer exists

What this change does NOT do

Open

Tests

PluginCoverTests.StartKind_OnlyACourseIsToldToStartACourse, PluginCoverTests.ExtensionRow_IsOpenSettings_NeverGetOrStartCourse, InstallDialogTests.WhenFinished_AnExtensionOpensItsSettings_NeverStartCourse, StoreCatalogTests.PurchasedCard_OnlyACourseSaysStartCourse, StoreCatalogTests.OpensByPlan_APaidPersonAppExtensionIsPlanGated, and the updated StoreCatalogTests.Extensions_AnEntryIsTheCardAndThePackagesOwnAction.