fix: complete marketplace client remediation

This commit is contained in:
2026-08-28 23:14:41 +08:00
parent 2c3baf6dff
commit 11d0af0166
18 changed files with 1007 additions and 90 deletions

View File

@@ -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()];