feat(plugins): enforce apiVersion; launcher plugins default ON with "none" opt-out
Two gaps from the MossTank shipment review. **apiVersion was declared in every manifest and checked by nothing.** The loader now refuses an unsupported contract BEFORE loading any code from the plugin — checking after the fact is not equivalent, because by then the assembly is in a collectible context and the mismatch surfaces as a type-load or missing-member failure from inside the plugin, which reads like the plugin is broken rather than built for a different host. PluginApi (Current / MinimumSupported) lives in Plugin.Abstractions beside the contract it versions, and the refusal is a distinct PluginApiVersionException so callers can tell "update the client or the plugin" from "this plugin is broken". The tests pin the ordering too: a manifest with a future apiVersion AND a missing dll must fail on the version, a supported one on the dll. **A launcher-launched client loaded no plugins until the user typed ids.** LA5 distinguishes an omitted allow-list (load all) from an explicit empty one (load none); a fresh character profile's list is empty, so it composed to load-none. Direct launches pass null and load everything -- which is why the gap never showed in development: the two launch paths disagreed and the launcher was the one users get. This REVERSES the LA5 default deliberately: "nothing configured" now composes to the omitted list, so plugins are on by default, including ones installed later. The opt-out is kept -- losing it would be a real regression for stripped sessions -- respelled as the literal id "none", and the launcher's plugin box says so. The cross-host shared fixture composes its explicit-load-none case through the new spelling, keeping the reader-side contract tests (App and Headless both preserve an explicit empty list) exactly as they were. Complete Release suite: 14,469 tests pass on the standard hermetic lane filter, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e75be23d1
commit
cd6eefd0ba
8 changed files with 194 additions and 7 deletions
|
|
@ -138,9 +138,7 @@ public static class SessionConfigComposer
|
|||
Character = selector,
|
||||
Policy = policy,
|
||||
Credential = new SessionCredentialDescriptor(),
|
||||
// LA5 distinguishes an omitted allow-list (load all, preserving
|
||||
// the developer flow) from an explicit empty list (load none).
|
||||
Plugins = [.. character.Plugins],
|
||||
Plugins = ComposePluginAllowList(character.Plugins),
|
||||
LoginCommands = character.LoginCommands.Count > 0
|
||||
? [.. character.LoginCommands]
|
||||
: null,
|
||||
|
|
@ -271,6 +269,41 @@ public static class SessionConfigComposer
|
|||
string sessionId) =>
|
||||
Write(ComposeProbe(server, account, install, paths, sessionId));
|
||||
|
||||
/// <summary>
|
||||
/// Maps a character's configured plugin ids to the session config's
|
||||
/// allow-list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// LA5 distinguishes an OMITTED allow-list (load every discovered plugin)
|
||||
/// from an explicit EMPTY one (load none). A brand-new character profile
|
||||
/// starts with an empty list, which meant a client that ships plugins
|
||||
/// loaded none of them until the user typed an id — "ships with the
|
||||
/// client" and "works out of the box" were different things, and the
|
||||
/// difference was invisible: nothing was logged, the panel simply never
|
||||
/// appeared.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// So "nothing configured" now maps to omitted — plugins are on by
|
||||
/// default, including ones installed later. The opt-out is kept, because
|
||||
/// losing it would be a real regression for anyone running a stripped
|
||||
/// session: the literal id <c>none</c> maps to the explicit empty list.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static List<string>? ComposePluginAllowList(IReadOnlyList<string> configured)
|
||||
{
|
||||
if (configured.Count == 0)
|
||||
return null; // default: load all
|
||||
|
||||
if (configured.Count == 1
|
||||
&& string.Equals(configured[0], "none", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return []; // explicit: load none
|
||||
}
|
||||
|
||||
return [.. configured];
|
||||
}
|
||||
|
||||
private static ComposedSessionConfig Write(ComposedSessionConfig composed)
|
||||
{
|
||||
string? directory = Path.GetDirectoryName(composed.ConfigFilePath);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue