fix: complete marketplace client remediation
This commit is contained in:
@@ -92,14 +92,15 @@ export class AccountPluginCache {
|
||||
library: MarketplaceLibrarySnapshot | null;
|
||||
resolves: Map<string, MarketplaceResolveSnapshot>;
|
||||
}>();
|
||||
private readonly libraryIntents = new Map<string, number>();
|
||||
|
||||
getLibrary(binding: AccountBinding): MarketplaceLibrarySnapshot | null {
|
||||
const record = this.records.get(bindingId(binding));
|
||||
return record?.library ? clone(record.library) : null;
|
||||
private nextLibraryIntent(key: string): number {
|
||||
const next = (this.libraryIntents.get(key) ?? 0) + 1;
|
||||
this.libraryIntents.set(key, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
setLibrary(binding: AccountBinding, snapshot: MarketplaceLibrarySnapshot): void {
|
||||
const key = bindingId(binding);
|
||||
private setLibrarySnapshot(key: string, binding: AccountBinding, snapshot: MarketplaceLibrarySnapshot): void {
|
||||
const record = this.records.get(key) ?? {
|
||||
binding: { accountKey: binding.accountKey, epoch: binding.epoch },
|
||||
library: null,
|
||||
@@ -109,8 +110,37 @@ export class AccountPluginCache {
|
||||
this.records.set(key, record);
|
||||
}
|
||||
|
||||
markLibraryStale(binding: AccountBinding): MarketplaceLibrarySnapshot | null {
|
||||
getLibrary(binding: AccountBinding): MarketplaceLibrarySnapshot | null {
|
||||
const record = this.records.get(bindingId(binding));
|
||||
return record?.library ? clone(record.library) : null;
|
||||
}
|
||||
|
||||
setLibrary(binding: AccountBinding, snapshot: MarketplaceLibrarySnapshot): void {
|
||||
const key = bindingId(binding);
|
||||
this.nextLibraryIntent(key);
|
||||
this.setLibrarySnapshot(key, binding, snapshot);
|
||||
}
|
||||
|
||||
/** Reserve the commit slot before an async Library read or mutation starts. */
|
||||
beginLibraryIntent(binding: AccountBinding): number {
|
||||
return this.nextLibraryIntent(bindingId(binding));
|
||||
}
|
||||
|
||||
/** Commit only the latest read/mutation intent for this account binding. */
|
||||
commitLibrary(
|
||||
binding: AccountBinding,
|
||||
intent: number,
|
||||
snapshot: MarketplaceLibrarySnapshot,
|
||||
): boolean {
|
||||
const key = bindingId(binding);
|
||||
if (this.libraryIntents.get(key) !== intent) return false;
|
||||
this.setLibrarySnapshot(key, binding, snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
markLibraryStale(binding: AccountBinding, intent?: number): MarketplaceLibrarySnapshot | null {
|
||||
const key = bindingId(binding);
|
||||
if (intent !== undefined && this.libraryIntents.get(key) !== intent) return null;
|
||||
const record = this.records.get(key);
|
||||
if (!record?.library) return null;
|
||||
record.library = { ...record.library, stale: true };
|
||||
@@ -193,12 +223,16 @@ export class AccountPluginCache {
|
||||
clearAccount(binding: AccountBinding): void {
|
||||
assertBinding(binding);
|
||||
for (const [key, record] of this.records.entries()) {
|
||||
if (record.binding.accountKey === binding.accountKey) this.records.delete(key);
|
||||
if (record.binding.accountKey === binding.accountKey) {
|
||||
this.records.delete(key);
|
||||
this.libraryIntents.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateAll(): void {
|
||||
this.records.clear();
|
||||
this.libraryIntents.clear();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
|
||||
@@ -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()];
|
||||
|
||||
@@ -924,6 +924,14 @@ class MarketplaceClientImpl implements MarketplaceClient {
|
||||
|
||||
async readLibrary(): Promise<MarketplaceLibrarySnapshot> {
|
||||
const binding = this.requireBinding();
|
||||
const intent = this.accountCache.beginLibraryIntent(binding);
|
||||
return this.readLibraryForIntent(binding, intent);
|
||||
}
|
||||
|
||||
private async readLibraryForIntent(
|
||||
binding: AccountBinding,
|
||||
intent: number,
|
||||
): Promise<MarketplaceLibrarySnapshot> {
|
||||
const previous = this.accountCache.getLibrary(binding);
|
||||
try {
|
||||
const result = await this.requestJson('/api/plugin-marketplace/v1/library', { auth: 'required' }, parseLibrary);
|
||||
@@ -934,14 +942,16 @@ class MarketplaceClientImpl implements MarketplaceClient {
|
||||
fetchedAt: this.now(),
|
||||
};
|
||||
this.assertBinding(binding);
|
||||
this.accountCache.setLibrary(binding, snapshot);
|
||||
if (!this.accountCache.commitLibrary(binding, intent, snapshot)) {
|
||||
return clone(this.accountCache.getLibrary(binding) ?? snapshot);
|
||||
}
|
||||
return clone(snapshot);
|
||||
} catch (error) {
|
||||
if (!this.bindingMatches(binding)) {
|
||||
fail('marketplace_account_changed', 'Marketplace account changed while the request was active');
|
||||
}
|
||||
if (previous && canServeStale(error)) {
|
||||
const stale = this.accountCache.markLibraryStale(binding);
|
||||
const stale = this.accountCache.markLibraryStale(binding, intent);
|
||||
if (stale) return clone(stale);
|
||||
}
|
||||
throw normalizeError(error);
|
||||
@@ -1073,6 +1083,8 @@ class MarketplaceClientImpl implements MarketplaceClient {
|
||||
private async mutateLibrary(method: 'PUT' | 'DELETE', pluginId: string): Promise<MarketplaceLibrarySnapshot> {
|
||||
const validated = idValue(pluginId, 'pluginId');
|
||||
const binding = this.requireBinding();
|
||||
const intent = this.accountCache.beginLibraryIntent(binding);
|
||||
const previous = this.accountCache.getLibrary(binding);
|
||||
const result = await this.requestJson(
|
||||
`/api/plugin-marketplace/v1/library/${encodeURIComponent(validated)}`,
|
||||
{ auth: 'required', method },
|
||||
@@ -1081,13 +1093,12 @@ class MarketplaceClientImpl implements MarketplaceClient {
|
||||
const entry = result.value!;
|
||||
this.assertBinding(binding);
|
||||
try {
|
||||
return await this.readLibrary();
|
||||
return await this.readLibraryForIntent(binding, intent);
|
||||
} catch (error) {
|
||||
if (!this.bindingMatches(binding)) {
|
||||
fail('marketplace_account_changed', 'Marketplace account changed while the request was active');
|
||||
}
|
||||
if (!canServeStale(error)) throw normalizeError(error);
|
||||
const previous = this.accountCache.getLibrary(binding);
|
||||
if (previous) {
|
||||
const items = [entry, ...previous.items.filter((item) => item.pluginId !== entry.pluginId)];
|
||||
const snapshot: MarketplaceLibrarySnapshot = {
|
||||
@@ -1096,8 +1107,8 @@ class MarketplaceClientImpl implements MarketplaceClient {
|
||||
stale: true,
|
||||
fetchedAt: previous.fetchedAt,
|
||||
};
|
||||
this.accountCache.setLibrary(binding, snapshot);
|
||||
return clone(snapshot);
|
||||
if (this.accountCache.commitLibrary(binding, intent, snapshot)) return clone(snapshot);
|
||||
return clone(this.accountCache.getLibrary(binding) ?? snapshot);
|
||||
}
|
||||
throw normalizeError(error);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import type { CodingPluginDefinition } from '../../shared/coding-plugins';
|
||||
import {
|
||||
buildPluginReleaseDescriptor,
|
||||
compareSemVer,
|
||||
isMakeLoreVersionCompatible,
|
||||
isValidSemVer,
|
||||
type PluginReleaseDescriptor,
|
||||
@@ -70,6 +71,9 @@ const INDEX_RELEASE_KEYS = new Set([
|
||||
'sha256',
|
||||
'size_bytes',
|
||||
'installed_at',
|
||||
'channel',
|
||||
'min_makelore_version',
|
||||
'max_makelore_version',
|
||||
]);
|
||||
const CURRENT_SELECTION_ROOT_KEYS = new Set(['schema_version', 'current']);
|
||||
|
||||
@@ -88,11 +92,16 @@ export interface InstalledReleaseRecord {
|
||||
readonly sha256: string;
|
||||
readonly sizeBytes: number;
|
||||
readonly installedAt: string;
|
||||
/** Channel and verified client range are immutable facts of this install. */
|
||||
readonly channel?: 'stable' | 'beta';
|
||||
readonly minMakeloreVersion?: string;
|
||||
readonly maxMakeloreVersion?: string | null;
|
||||
}
|
||||
|
||||
export interface InstalledRelease extends InstalledReleaseRecord {
|
||||
readonly packageRoot: string;
|
||||
readonly definition: CodingPluginDefinition;
|
||||
readonly unavailableReason?: 'plugin_incompatible_client';
|
||||
}
|
||||
|
||||
export type InstallationStatus = 'installed' | 'kept' | 'removed' | 'unavailable';
|
||||
@@ -102,6 +111,8 @@ export interface InstallationSnapshot {
|
||||
readonly pluginId: string;
|
||||
readonly releaseId?: string;
|
||||
readonly version?: string;
|
||||
/** The immutable channel selected for this device package. */
|
||||
readonly channel?: 'stable' | 'beta';
|
||||
readonly packageRoot?: string;
|
||||
readonly definition?: CodingPluginDefinition;
|
||||
readonly reason?: string;
|
||||
@@ -306,6 +317,20 @@ function parseIndexDocument(value: unknown): IndexDocument {
|
||||
}
|
||||
const installedAt = boundedText(record.installed_at, `release ${index}.installed_at`, 80);
|
||||
if (!Number.isFinite(Date.parse(installedAt))) fail('plugin_store_index_invalid', `invalid release ${index}.installed_at`);
|
||||
const minMakeloreVersion = record.min_makelore_version === undefined
|
||||
? undefined
|
||||
: validVersion(record.min_makelore_version, `release ${index}.min_makelore_version`);
|
||||
const maxMakeloreVersion = record.max_makelore_version === undefined || record.max_makelore_version === null
|
||||
? record.max_makelore_version as string | null | undefined
|
||||
: validVersion(record.max_makelore_version, `release ${index}.max_makelore_version`);
|
||||
if (minMakeloreVersion && maxMakeloreVersion
|
||||
&& compareSemVer(maxMakeloreVersion, minMakeloreVersion) < 0) {
|
||||
fail('plugin_store_index_invalid', `invalid release ${index}.max_makelore_version`);
|
||||
}
|
||||
const channel = record.channel === undefined ? undefined : record.channel;
|
||||
if (channel !== undefined && channel !== 'stable' && channel !== 'beta') {
|
||||
fail('plugin_store_index_invalid', `invalid release ${index}.channel`);
|
||||
}
|
||||
return Object.freeze({
|
||||
pluginId,
|
||||
releaseId,
|
||||
@@ -316,6 +341,9 @@ function parseIndexDocument(value: unknown): IndexDocument {
|
||||
sha256: validSha(record.sha256, `release ${index}.sha256`, 'plugin_store_index_invalid'),
|
||||
sizeBytes: positiveInteger(record.size_bytes, `release ${index}.size_bytes`, DEFAULT_MAX_ARCHIVE_BYTES),
|
||||
installedAt,
|
||||
...(channel === undefined ? {} : { channel }),
|
||||
...(minMakeloreVersion === undefined ? {} : { minMakeloreVersion }),
|
||||
...(maxMakeloreVersion === undefined ? {} : { maxMakeloreVersion }),
|
||||
});
|
||||
});
|
||||
return Object.freeze({ schema_version: INDEX_SCHEMA_VERSION, releases: Object.freeze(releases) });
|
||||
@@ -334,6 +362,9 @@ function serializeIndex(document: IndexDocument): Uint8Array {
|
||||
sha256: record.sha256,
|
||||
size_bytes: record.sizeBytes,
|
||||
installed_at: record.installedAt,
|
||||
...(record.channel === undefined ? {} : { channel: record.channel }),
|
||||
...(record.minMakeloreVersion === undefined ? {} : { min_makelore_version: record.minMakeloreVersion }),
|
||||
...(record.maxMakeloreVersion === undefined ? {} : { max_makelore_version: record.maxMakeloreVersion }),
|
||||
})),
|
||||
})}\n`, 'utf8');
|
||||
}
|
||||
@@ -687,6 +718,7 @@ export class PluginPackageStore {
|
||||
pluginId,
|
||||
releaseId: current.releaseId,
|
||||
version: current.version,
|
||||
...(current.channel === undefined ? {} : { channel: current.channel }),
|
||||
packageRoot: current.packageRoot,
|
||||
definition: current.definition,
|
||||
};
|
||||
@@ -705,7 +737,7 @@ export class PluginPackageStore {
|
||||
const existingRecord = index.releases.find((record) => record.pluginId === pluginId && record.releaseId === item.releaseId);
|
||||
if (existingRecord && existingRecord.sha256 === item.sha256 && await isDirectoryPath(this.releaseDirectory(existingRecord))) {
|
||||
const existing = await this.getInstalledFromIndex(index, pluginId, item.releaseId);
|
||||
if (existing) {
|
||||
if (existing && !existing.unavailableReason) {
|
||||
await this.setCurrentSelection(
|
||||
await this.readCurrentSelection(),
|
||||
pluginId,
|
||||
@@ -716,6 +748,7 @@ export class PluginPackageStore {
|
||||
pluginId,
|
||||
releaseId: existing.releaseId,
|
||||
version: existing.version,
|
||||
...(existing.channel === undefined ? {} : { channel: existing.channel }),
|
||||
packageRoot: existing.packageRoot,
|
||||
definition: existing.definition,
|
||||
};
|
||||
@@ -738,6 +771,7 @@ export class PluginPackageStore {
|
||||
packageRoot,
|
||||
pluginId,
|
||||
grant,
|
||||
channel,
|
||||
binding,
|
||||
});
|
||||
}
|
||||
@@ -781,7 +815,7 @@ export class PluginPackageStore {
|
||||
await writeFile(path.join(extractedPath, ORPHAN_ARCHIVE_FILE), toBuffer(artifact), { flag: 'wx' });
|
||||
await rename(extractedPath, packageRoot);
|
||||
moved = true;
|
||||
const record = this.installedRecord(grant);
|
||||
const record = this.installedRecord(grant, channel);
|
||||
const records = index.releases.filter((candidate) => !(candidate.pluginId === pluginId && candidate.releaseId === grant.releaseId));
|
||||
try {
|
||||
await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: [...records, record] }));
|
||||
@@ -794,6 +828,7 @@ export class PluginPackageStore {
|
||||
pluginId,
|
||||
releaseId: record.releaseId,
|
||||
version: record.version,
|
||||
channel: record.channel,
|
||||
packageRoot,
|
||||
definition,
|
||||
};
|
||||
@@ -817,6 +852,7 @@ export class PluginPackageStore {
|
||||
readonly packageRoot: string;
|
||||
readonly pluginId: string;
|
||||
readonly grant: DownloadGrant;
|
||||
readonly channel: 'stable' | 'beta';
|
||||
readonly binding: AccountBinding;
|
||||
}): Promise<InstallationSnapshot> {
|
||||
const existingRecord = input.index.releases.find((record) => (
|
||||
@@ -832,7 +868,7 @@ export class PluginPackageStore {
|
||||
const descriptor = this.verifyArtifact(artifact, input.grant);
|
||||
const definition = await this.loadDefinition(input.packageRoot, input.grant, descriptor);
|
||||
this.assertBinding(input.binding);
|
||||
const record = this.installedRecord(input.grant);
|
||||
const record = this.installedRecord(input.grant, input.channel);
|
||||
try {
|
||||
await this.writeIndex(this.indexPath, serializeIndex({
|
||||
schema_version: INDEX_SCHEMA_VERSION,
|
||||
@@ -847,6 +883,7 @@ export class PluginPackageStore {
|
||||
pluginId: input.pluginId,
|
||||
releaseId: record.releaseId,
|
||||
version: record.version,
|
||||
channel: record.channel,
|
||||
packageRoot: input.packageRoot,
|
||||
definition,
|
||||
};
|
||||
@@ -879,7 +916,7 @@ export class PluginPackageStore {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private installedRecord(grant: DownloadGrant): InstalledReleaseRecord {
|
||||
private installedRecord(grant: DownloadGrant, channel: 'stable' | 'beta'): InstalledReleaseRecord {
|
||||
return Object.freeze({
|
||||
pluginId: grant.pluginId,
|
||||
releaseId: grant.releaseId,
|
||||
@@ -890,6 +927,9 @@ export class PluginPackageStore {
|
||||
sha256: grant.sha256,
|
||||
sizeBytes: grant.sizeBytes,
|
||||
installedAt: new Date(this.now()).toISOString(),
|
||||
channel,
|
||||
minMakeloreVersion: grant.minMakeloreVersion,
|
||||
maxMakeloreVersion: grant.maxMakeloreVersion,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -908,22 +948,23 @@ export class PluginPackageStore {
|
||||
}
|
||||
return { status: 'removed', pluginId: validated, reason: 'none' };
|
||||
}
|
||||
const latest = records
|
||||
.map((record, index) => ({ record, index }))
|
||||
.sort((left, right) => right.record.installedAt.localeCompare(left.record.installedAt) || right.index - left.index)[0]
|
||||
?.record;
|
||||
if (!latest) return { status: 'removed', pluginId: validated, reason: 'none' };
|
||||
const selected = currentSelection.current[validated]
|
||||
? records.find((record) => record.releaseId === currentSelection.current[validated])
|
||||
: undefined;
|
||||
const reported = selected ?? latest;
|
||||
const protectedIds = new Set([
|
||||
...this.accountCache.referencedReleaseIds(),
|
||||
...(this.activeWorkerReleaseIds() ?? []),
|
||||
...this.activeWorkers,
|
||||
]);
|
||||
const removable = records.filter((record) => !protectedIds.has(record.releaseId));
|
||||
if (removable.length === 0) return { status: 'kept', pluginId: validated, releaseId: reported.releaseId, version: reported.version };
|
||||
const removable = selected && records.length > 1
|
||||
? records.filter((record) => record.releaseId !== selected.releaseId && !protectedIds.has(record.releaseId))
|
||||
: records.filter((record) => !protectedIds.has(record.releaseId));
|
||||
if (removable.length === 0) {
|
||||
const reported = selected ?? records.find((record) => protectedIds.has(record.releaseId));
|
||||
return reported
|
||||
? { status: 'kept', pluginId: validated, releaseId: reported.releaseId, version: reported.version }
|
||||
: { status: 'kept', pluginId: validated, reason: 'current_selection_missing' };
|
||||
}
|
||||
const remaining = index.releases.filter((record) => !removable.includes(record));
|
||||
try {
|
||||
await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: remaining }));
|
||||
@@ -933,15 +974,7 @@ export class PluginPackageStore {
|
||||
const nextCurrent = { ...currentSelection.current };
|
||||
if (nextCurrent[validated] && !remaining.some((record) => (
|
||||
record.pluginId === validated && record.releaseId === nextCurrent[validated]
|
||||
))) {
|
||||
const fallback = remaining
|
||||
.filter((record) => record.pluginId === validated)
|
||||
.map((record, index) => ({ record, index }))
|
||||
.sort((left, right) => right.record.installedAt.localeCompare(left.record.installedAt) || right.index - left.index)[0]
|
||||
?.record;
|
||||
if (fallback) nextCurrent[validated] = fallback.releaseId;
|
||||
else delete nextCurrent[validated];
|
||||
}
|
||||
))) delete nextCurrent[validated];
|
||||
await this.writeCurrentSelection({
|
||||
schema_version: CURRENT_SELECTION_SCHEMA_VERSION,
|
||||
current: Object.freeze(nextCurrent),
|
||||
@@ -952,8 +985,24 @@ export class PluginPackageStore {
|
||||
const currentAfter = nextCurrent[validated]
|
||||
? remaining.find((record) => record.pluginId === validated && record.releaseId === nextCurrent[validated])
|
||||
: undefined;
|
||||
const reportedAfter = currentAfter ?? latest;
|
||||
return { status: 'removed', pluginId: validated, releaseId: reportedAfter.releaseId, version: reportedAfter.version };
|
||||
const selectedAfter = selected && remaining.some((record) => (
|
||||
record.pluginId === validated && record.releaseId === selected.releaseId
|
||||
)) ? selected : undefined;
|
||||
const reportedAfter = currentAfter ?? selectedAfter ?? remaining.find((record) => record.pluginId === validated);
|
||||
if (reportedAfter) {
|
||||
return {
|
||||
status: 'kept',
|
||||
pluginId: validated,
|
||||
releaseId: reportedAfter.releaseId,
|
||||
version: reportedAfter.version,
|
||||
...(reportedAfter.channel === undefined ? {} : { channel: reportedAfter.channel }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'removed',
|
||||
pluginId: validated,
|
||||
...(selected ? { releaseId: selected.releaseId, version: selected.version } : { reason: 'none' }),
|
||||
};
|
||||
}
|
||||
|
||||
private buildDescriptor(grant: DownloadGrant): PluginReleaseDescriptor {
|
||||
@@ -1073,9 +1122,11 @@ export class PluginPackageStore {
|
||||
.filter(({ record }) => record.pluginId === pluginId && (releaseId === undefined || record.releaseId === releaseId))
|
||||
.sort((left, right) => right.record.installedAt.localeCompare(left.record.installedAt) || right.index - left.index)
|
||||
.map(({ record }) => record);
|
||||
const selected = pointedReleaseId === undefined
|
||||
const selected = releaseId !== undefined
|
||||
? records
|
||||
: records.filter((record) => record.releaseId === pointedReleaseId);
|
||||
: pointedReleaseId === undefined
|
||||
? []
|
||||
: records.filter((record) => record.releaseId === pointedReleaseId);
|
||||
for (const record of selected) {
|
||||
const packageRoot = this.releaseDirectory(record);
|
||||
if (!await isDirectoryPath(packageRoot)) continue;
|
||||
@@ -1090,7 +1141,18 @@ export class PluginPackageStore {
|
||||
|| definition.contractVersion !== record.contractVersion || definition.runtimeKind !== record.runtimeKind) {
|
||||
throw new PluginPackageStoreError('plugin_manifest_invalid', 'installed package does not match its immutable index record');
|
||||
}
|
||||
return { ...record, packageRoot, definition };
|
||||
const compatible = record.minMakeloreVersion !== undefined
|
||||
&& isMakeLoreVersionCompatible(
|
||||
this.clientVersion,
|
||||
record.minMakeloreVersion,
|
||||
record.maxMakeloreVersion ?? null,
|
||||
);
|
||||
return {
|
||||
...record,
|
||||
packageRoot,
|
||||
definition,
|
||||
...(compatible ? {} : { unavailableReason: 'plugin_incompatible_client' as const }),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof PluginPackageStoreError) throw error;
|
||||
throw new PluginPackageStoreError('plugin_manifest_invalid', 'installed package manifest is invalid');
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
import type {
|
||||
EffectivePluginResolver,
|
||||
EffectivePluginSnapshot,
|
||||
EffectivePluginSkillSource,
|
||||
} from './effective-resolver';
|
||||
|
||||
const MAX_REQUEST_ID = 128;
|
||||
@@ -391,21 +392,30 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
|
||||
const effectiveSkills = new Set(snapshot.effectiveSkillIds);
|
||||
const pluginIds = new Set<string>(snapshot.runtimePolicies.map(({ pluginId }) => pluginId));
|
||||
const skillRoots = new Set<string>();
|
||||
const sourcesBySkillId = new Map<string, EffectivePluginSkillSource>();
|
||||
for (const source of sources) {
|
||||
if (effectiveSkills.has(source.id)) {
|
||||
pluginIds.add(source.pluginId);
|
||||
skillRoots.add(source.packageRoot);
|
||||
if (effectiveSkills.has(source.id) && !sourcesBySkillId.has(source.id)) {
|
||||
sourcesBySkillId.set(source.id, source);
|
||||
}
|
||||
}
|
||||
const skillEntries = snapshot.skillEntries.map(({ id, entryPath, packageRoot }) => {
|
||||
const source = sourcesBySkillId.get(id);
|
||||
const pairedRoot = packageRoot ?? source?.packageRoot;
|
||||
if (source && pairedRoot) {
|
||||
pluginIds.add(source.pluginId);
|
||||
skillRoots.add(pairedRoot);
|
||||
}
|
||||
return {
|
||||
id,
|
||||
entryPath,
|
||||
...(pairedRoot ? { packageRoot: pairedRoot } : {}),
|
||||
};
|
||||
});
|
||||
return {
|
||||
catalogRevision: this.options.effectiveResolver.getPolicyState().revision,
|
||||
pluginIds: [...pluginIds],
|
||||
effectiveSkillIds: [...snapshot.effectiveSkillIds],
|
||||
skillEntries: snapshot.skillEntries.map(({ id, entryPath, packageRoot }) => ({
|
||||
id,
|
||||
entryPath,
|
||||
...(packageRoot ? { packageRoot } : {}),
|
||||
})),
|
||||
skillEntries,
|
||||
tools: snapshot.toolDefinitions.map((tool) => structuredClone(tool)),
|
||||
effectiveSnapshot: snapshot,
|
||||
skillRoots: [...skillRoots],
|
||||
|
||||
Reference in New Issue
Block a user