fix: complete marketplace client remediation
This commit is contained in:
@@ -43,6 +43,7 @@ export type PluginUnavailableReasonCode =
|
||||
| 'library_unavailable'
|
||||
| 'release_not_installed'
|
||||
| 'release_invalid'
|
||||
| 'client_incompatible'
|
||||
| 'project_disabled'
|
||||
| 'skill_unassigned'
|
||||
| 'runtime_suspended'
|
||||
@@ -119,6 +120,7 @@ export interface EffectivePluginSkillSource {
|
||||
interface DefinitionRecord {
|
||||
readonly definition: CodingPluginDefinition;
|
||||
readonly installed: boolean;
|
||||
readonly unavailableReason?: PluginUnavailableReasonCode;
|
||||
}
|
||||
|
||||
const EMPTY_POLICY_STATE: PluginPolicyClientState = {
|
||||
@@ -210,6 +212,26 @@ function validateBinding(value: AccountBinding | null): AccountBinding | null {
|
||||
return { accountKey: value.accountKey, epoch: value.epoch };
|
||||
}
|
||||
|
||||
/**
|
||||
* A raw Skill ID is the public assignment key. Marketplace packages cannot
|
||||
* shadow a core Skill or an already accepted package owner; the project
|
||||
* assignment is retained, but the later package contributes no resources.
|
||||
*/
|
||||
function marketplaceSkillConflicts(records: readonly DefinitionRecord[]): ReadonlySet<string> {
|
||||
const blocked = new Set<string>();
|
||||
const owned = new Set<string>(CORE_CODING_SKILL_IDS);
|
||||
for (const { definition } of records) {
|
||||
if (definition.provenance.source !== 'marketplace') continue;
|
||||
const conflicts = definition.skills.some(({ id }) => owned.has(id));
|
||||
if (conflicts) {
|
||||
blocked.add(definition.id);
|
||||
continue;
|
||||
}
|
||||
for (const { id } of definition.skills) owned.add(id);
|
||||
}
|
||||
return blocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a worker snapshot from separated Marketplace, Package Store,
|
||||
* project, assignment, and policy state. This module deliberately performs
|
||||
@@ -222,6 +244,7 @@ export class EffectivePluginResolver {
|
||||
|
||||
async resolve(input: EffectivePluginResolverInput): Promise<EffectivePluginSnapshot> {
|
||||
const definitions = await this.definitionRecords();
|
||||
const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions);
|
||||
const assigned = normalizeIds(input.assignedSkillIds);
|
||||
|
||||
const coreIds = new Set<string>(CORE_CODING_SKILL_IDS);
|
||||
@@ -273,7 +296,8 @@ export class EffectivePluginResolver {
|
||||
policyState = this.options.policyClient.getState();
|
||||
}
|
||||
|
||||
for (const { definition, installed } of definitions) {
|
||||
for (const { definition, installed, unavailableReason } of definitions) {
|
||||
if (blockedMarketplacePlugins.has(definition.id)) continue;
|
||||
const selectedSkills = definition.skills.filter(({ id }) => assigned.includes(id));
|
||||
if (selectedSkills.length === 0) {
|
||||
unavailableReasons.push(unavailable(definition.id, 'skill_unassigned', 'Plugin Skill is not assigned'));
|
||||
@@ -284,6 +308,16 @@ export class EffectivePluginResolver {
|
||||
unavailableReasons.push(unavailable(definition.id, 'release_not_installed', 'Plugin Release is not installed'));
|
||||
continue;
|
||||
}
|
||||
if (unavailableReason) {
|
||||
unavailableReasons.push(unavailable(
|
||||
definition.id,
|
||||
unavailableReason,
|
||||
unavailableReason === 'client_incompatible'
|
||||
? 'Plugin Release is incompatible with this MakeLore client'
|
||||
: 'Plugin Release is unavailable',
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (!enabled.has(definition.id)) {
|
||||
unavailableReasons.push(unavailable(definition.id, 'project_disabled', 'Plugin is not enabled for this project'));
|
||||
continue;
|
||||
@@ -386,12 +420,14 @@ export class EffectivePluginResolver {
|
||||
|
||||
async getSkillSources(): Promise<readonly EffectivePluginSkillSource[]> {
|
||||
const definitions = await this.definitionRecords();
|
||||
const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions);
|
||||
// Bundled plugin Skills already have a trusted resource source supplied by
|
||||
// composition. Returning their manifest-relative `packageRoot` here would
|
||||
// replace that source with a cwd-relative path in the product projection;
|
||||
// this seam is exclusively for immutable user-installed package roots.
|
||||
return Object.freeze(definitions.flatMap(({ definition, installed }) => (
|
||||
!installed || definition.acquisitionMode !== 'user_acquired'
|
||||
return Object.freeze(definitions.flatMap(({ definition, installed, unavailableReason }) => (
|
||||
!installed || unavailableReason || blockedMarketplacePlugins.has(definition.id)
|
||||
|| definition.acquisitionMode !== 'user_acquired'
|
||||
? []
|
||||
: definition.skills.map((skill) => ({
|
||||
id: skill.id,
|
||||
@@ -427,19 +463,38 @@ export class EffectivePluginResolver {
|
||||
for (const definition of [...records.values()].map(({ definition }) => definition)) {
|
||||
if (definition.acquisitionMode !== 'user_acquired') continue;
|
||||
let installed = (this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id);
|
||||
if (installedIds.has(definition.id)) installed = Boolean(await this.options.packageStore?.getInstalled(definition.id));
|
||||
if (this.options.getInstalled) installed = Boolean(await this.options.getInstalled(definition.id));
|
||||
let unavailableReason: PluginUnavailableReasonCode | undefined;
|
||||
if (installedIds.has(definition.id)) {
|
||||
const installedRelease = await this.options.packageStore?.getInstalled(definition.id);
|
||||
installed = Boolean(installedRelease);
|
||||
if (installedRelease?.unavailableReason === 'plugin_incompatible_client') {
|
||||
unavailableReason = 'client_incompatible';
|
||||
}
|
||||
}
|
||||
if (this.options.getInstalled) {
|
||||
const installedRelease = await this.options.getInstalled(definition.id);
|
||||
installed = Boolean(installedRelease);
|
||||
if (installedRelease?.unavailableReason === 'plugin_incompatible_client') {
|
||||
unavailableReason = 'client_incompatible';
|
||||
}
|
||||
}
|
||||
if (!this.options.packageStore && !this.options.getInstalled
|
||||
&& !(this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id)) {
|
||||
installed = false;
|
||||
}
|
||||
records.set(definition.id, { definition, installed });
|
||||
records.set(definition.id, { definition, installed, unavailableReason });
|
||||
}
|
||||
if (this.options.packageStore) {
|
||||
for (const pluginId of installedIds) {
|
||||
if (records.has(pluginId)) continue;
|
||||
const installed = await this.options.packageStore.getInstalled(pluginId);
|
||||
if (installed) records.set(pluginId, { definition: installed.definition, installed: true });
|
||||
if (installed) records.set(pluginId, {
|
||||
definition: installed.definition,
|
||||
installed: true,
|
||||
...(installed.unavailableReason === 'plugin_incompatible_client'
|
||||
? { unavailableReason: 'client_incompatible' as const }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...records.values()];
|
||||
|
||||
Reference in New Issue
Block a user