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

@@ -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');