fix: close marketplace client review findings

This commit is contained in:
2026-08-28 21:01:51 +08:00
parent 8dfa542860
commit 1614f7efc1
25 changed files with 1224 additions and 143 deletions

View File

@@ -46,10 +46,12 @@ import type {
import type { PluginSigningKeyStore } from './trusted-keys';
const INDEX_SCHEMA_VERSION = 1;
const CURRENT_SELECTION_SCHEMA_VERSION = 1;
const DEFAULT_MAX_ARCHIVE_BYTES = 16 * 1024 * 1024;
const DEFAULT_MAX_EXTRACTED_BYTES = 32 * 1024 * 1024;
const DEFAULT_MAX_FILES = 256;
const DEFAULT_MAX_FILE_BYTES = 4 * 1024 * 1024;
const ORPHAN_ARCHIVE_FILE = '.makelore-release.zip';
const MAX_PLUGIN_ID = 128;
const MAX_RELEASE_ID = 128;
const MAX_VERSION = 128;
@@ -69,6 +71,7 @@ const INDEX_RELEASE_KEYS = new Set([
'size_bytes',
'installed_at',
]);
const CURRENT_SELECTION_ROOT_KEYS = new Set(['schema_version', 'current']);
type UnknownRecord = Record<string, unknown>;
@@ -154,6 +157,11 @@ export type PluginPackageStoreErrorCode =
| 'plugin_manifest_invalid'
| 'plugin_runtime_not_supported'
| 'plugin_release_conflict'
| 'plugin_release_not_ready'
| 'plugin_release_yanked'
| 'plugin_runtime_suspended'
| 'plugin_library_required'
| 'plugin_backend_unavailable'
| 'plugin_install_failed';
export class PluginPackageStoreError extends Error {
@@ -171,6 +179,11 @@ interface IndexDocument {
readonly releases: readonly InstalledReleaseRecord[];
}
interface CurrentSelectionDocument {
readonly schema_version: typeof CURRENT_SELECTION_SCHEMA_VERSION;
readonly current: Readonly<Record<string, string>>;
}
function isRecord(value: unknown): value is UnknownRecord {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
@@ -325,6 +338,33 @@ function serializeIndex(document: IndexDocument): Uint8Array {
})}\n`, 'utf8');
}
function parseCurrentSelectionDocument(value: unknown): CurrentSelectionDocument {
const root = isRecord(value) ? value : fail('plugin_store_index_invalid', 'current selection root must be an object');
assertExactKeys(root, CURRENT_SELECTION_ROOT_KEYS, 'current selection');
if (root.schema_version !== CURRENT_SELECTION_SCHEMA_VERSION) {
fail('plugin_store_index_invalid', 'unsupported current selection schema');
}
if (!isRecord(root.current) || Object.keys(root.current).length > DEFAULT_MAX_FILES) {
fail('plugin_store_index_invalid', 'invalid current selection');
}
const current: Record<string, string> = {};
for (const [pluginId, releaseId] of Object.entries(root.current)) {
const validId = validPluginId(pluginId, 'current plugin_id', 'plugin_store_index_invalid');
current[validId] = validReleaseId(releaseId, `current ${validId}.release_id`, 'plugin_store_index_invalid');
}
return Object.freeze({
schema_version: CURRENT_SELECTION_SCHEMA_VERSION,
current: Object.freeze(current),
});
}
function serializeCurrentSelection(document: CurrentSelectionDocument): Uint8Array {
return Buffer.from(`${JSON.stringify({
schema_version: document.schema_version,
current: document.current,
})}\n`, 'utf8');
}
async function parseJsonFile(filePath: string): Promise<unknown> {
const bytes = await readFile(filePath);
let source: string;
@@ -482,6 +522,7 @@ export class PluginPackageStore {
private readonly rootDir: string;
private readonly packagesDir: string;
private readonly indexPath: string;
private readonly currentSelectionPath: string;
private readonly marketplace: MarketplacePackageClientPort;
private readonly accountCache: AccountPluginCache;
private readonly getAccountBindingImpl: () => AccountBinding | null;
@@ -502,6 +543,7 @@ export class PluginPackageStore {
this.rootDir = path.resolve(options.rootDir ?? path.join(getDataDir(), 'coding-plugins'));
this.packagesDir = path.join(this.rootDir, 'packages');
this.indexPath = path.join(this.rootDir, 'index.json');
this.currentSelectionPath = path.join(this.rootDir, 'current.json');
this.marketplace = options.marketplace;
this.accountCache = options.accountCache ?? new AccountPluginCache();
this.getAccountBindingImpl = options.getAccountBinding
@@ -554,37 +596,23 @@ export class PluginPackageStore {
async getInstalled(pluginId: string): Promise<InstalledRelease | null> {
const validated = validPluginId(pluginId);
const index = await this.readIndex();
return this.getInstalledFromIndex(index, validated);
const current = await this.readCurrentSelection();
return this.getInstalledFromIndex(index, validated, undefined, current);
}
async removeUnused(pluginId: string): Promise<InstallationSnapshot> {
const validated = validPluginId(pluginId);
return this.withOperation(() => this.removeUnusedLocked(validated));
}
/** Remove a device package while leaving the account Library projection intact. */
async uninstall(pluginId: string): Promise<InstallationSnapshot> {
const validated = validPluginId(pluginId);
return this.withOperation(async () => {
const index = await this.readIndex();
const records = index.releases.filter((record) => record.pluginId === validated);
if (records.length === 0) 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 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: latest.releaseId, version: latest.version };
const remaining = index.releases.filter((record) => !removable.includes(record));
try {
await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: remaining }));
} catch {
throw new PluginPackageStoreError('plugin_install_failed', 'package index cleanup failed');
}
await Promise.all(removable.map(async (record) => {
await rm(this.releaseDirectory(record), { recursive: true, force: true });
}));
return { status: 'removed', pluginId: validated, releaseId: latest.releaseId, version: latest.version };
const binding = this.requireBinding();
this.accountCache.invalidatePlugin(binding, validated);
this.assertBinding(binding);
return this.removeUnusedLocked(validated);
});
}
@@ -610,7 +638,12 @@ export class PluginPackageStore {
if (channel === 'beta' && input.explicitBeta !== true) fail('plugin_beta_selection_required');
const binding = this.requireBinding();
const index = await this.readIndex();
const current = await this.getInstalledFromIndex(index, pluginId);
const current = await this.getInstalledFromIndex(
index,
pluginId,
undefined,
await this.readCurrentSelection(),
);
const installed = input.installed === undefined
? (current ? [toInstalledInput(current)] : [])
: input.installed === null ? [] : [input.installed];
@@ -633,7 +666,20 @@ export class PluginPackageStore {
if (item.channel !== undefined && item.channel !== null && item.channel !== channel) {
fail('plugin_release_unavailable', 'resolve channel does not match the requested channel');
}
if (item.action === 'unavailable') fail('plugin_release_unavailable', item.reason ?? 'Plugin Release is unavailable');
if (item.action === 'unavailable') {
const reason = item.reason;
if (reason === 'plugin_release_not_ready'
|| reason === 'plugin_release_yanked'
|| reason === 'plugin_runtime_suspended'
|| reason === 'plugin_library_required'
|| reason === 'plugin_incompatible_client'
|| reason === 'plugin_signature_invalid'
|| reason === 'plugin_artifact_invalid'
|| reason === 'plugin_backend_unavailable') {
fail(reason, reason);
}
fail('plugin_release_unavailable', reason ?? 'Plugin Release is unavailable');
}
if (item.action === 'keep') {
if (!current) fail('plugin_release_unavailable', 'resolve requested keep without an installed Release');
return {
@@ -660,6 +706,11 @@ export class PluginPackageStore {
if (existingRecord && existingRecord.sha256 === item.sha256 && await isDirectoryPath(this.releaseDirectory(existingRecord))) {
const existing = await this.getInstalledFromIndex(index, pluginId, item.releaseId);
if (existing) {
await this.setCurrentSelection(
await this.readCurrentSelection(),
pluginId,
existing.releaseId,
);
return {
status: 'kept',
pluginId,
@@ -679,6 +730,17 @@ export class PluginPackageStore {
if (error instanceof PluginPackageStoreError) throw error;
throw this.mapMarketplaceError(error);
}
const packageRoot = this.releaseDirectory({ pluginId, releaseId: grant.releaseId } as InstalledReleaseRecord);
if (await pathExists(packageRoot)) {
return this.recoverOrphanedRelease({
index,
currentSelection: await this.readCurrentSelection(),
packageRoot,
pluginId,
grant,
binding,
});
}
let artifact: Uint8Array;
try {
artifact = await this.marketplace.downloadContent(grant);
@@ -687,29 +749,7 @@ export class PluginPackageStore {
if (error instanceof PluginPackageStoreError) throw error;
throw this.mapMarketplaceError(error);
}
const expiresAt = typeof grant.expiresAt === 'string' ? Date.parse(grant.expiresAt) : Number.NaN;
if (!Number.isFinite(expiresAt) || expiresAt <= this.now()) {
fail('plugin_release_unavailable', 'download admission has expired');
}
if (!(artifact instanceof Uint8Array)) fail('plugin_artifact_invalid', 'downloaded artifact is not binary data');
if (artifact.byteLength !== grant.sizeBytes || artifact.byteLength > this.maxArchiveBytes || digest(artifact) !== grant.sha256) {
fail('plugin_artifact_invalid', 'downloaded artifact does not match its grant');
}
const descriptor = this.buildDescriptor(grant);
if (!isMakeLoreVersionCompatible(
this.clientVersion,
descriptor.minMakeloreVersion,
descriptor.maxMakeloreVersion,
)) {
fail('plugin_incompatible_client', 'Release is incompatible with this MakeLore client');
}
const verification = this.signatureVerifier.verify({
keyId: grant.signingKeyId,
signature: grant.descriptorSignature,
descriptor,
artifact,
});
if (!verification.ok) fail(mapVerificationFailure(verification.code), verification.message);
const descriptor = this.verifyArtifact(artifact, grant);
try {
await mkdir(this.rootDir, { recursive: true });
} catch {
@@ -734,32 +774,21 @@ export class PluginPackageStore {
});
const definition = await this.loadDefinition(extractedPath, grant, descriptor);
this.assertBinding(binding);
const packageRoot = this.releaseDirectory({ pluginId, releaseId: grant.releaseId } as InstalledReleaseRecord);
await mkdir(path.dirname(packageRoot), { recursive: true });
if (await isDirectoryPath(packageRoot) || await pathExists(packageRoot)) {
fail('plugin_release_conflict', 'immutable Release directory already exists');
}
await writeFile(path.join(extractedPath, ORPHAN_ARCHIVE_FILE), toBuffer(artifact), { flag: 'wx' });
await rename(extractedPath, packageRoot);
moved = true;
const record: InstalledReleaseRecord = Object.freeze({
pluginId,
releaseId: grant.releaseId,
version: grant.version,
packageSchemaVersion: grant.packageSchemaVersion,
contractVersion: grant.contractVersion,
runtimeKind: 'skill_only',
sha256: grant.sha256,
sizeBytes: grant.sizeBytes,
installedAt: new Date(this.now()).toISOString(),
});
const record = this.installedRecord(grant);
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] }));
} catch {
await rm(packageRoot, { recursive: true, force: true }).catch(() => undefined);
moved = false;
throw new PluginPackageStoreError('plugin_install_failed', 'package index replacement failed');
}
await this.setCurrentSelection(await this.readCurrentSelection(), pluginId, record.releaseId);
return {
status: 'installed',
pluginId,
@@ -782,6 +811,151 @@ export class PluginPackageStore {
}
}
private async recoverOrphanedRelease(input: {
readonly index: IndexDocument;
readonly currentSelection: CurrentSelectionDocument;
readonly packageRoot: string;
readonly pluginId: string;
readonly grant: DownloadGrant;
readonly binding: AccountBinding;
}): Promise<InstallationSnapshot> {
const existingRecord = input.index.releases.find((record) => (
record.pluginId === input.pluginId && record.releaseId === input.grant.releaseId
));
if (existingRecord) fail('plugin_release_conflict', 'immutable Release directory already exists');
let artifact: Uint8Array;
try {
artifact = await readFile(path.join(input.packageRoot, ORPHAN_ARCHIVE_FILE));
} catch {
fail('plugin_release_conflict', 'immutable Release directory cannot be recovered');
}
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);
try {
await this.writeIndex(this.indexPath, serializeIndex({
schema_version: INDEX_SCHEMA_VERSION,
releases: [...input.index.releases, record],
}));
} catch {
throw new PluginPackageStoreError('plugin_install_failed', 'package index replacement failed');
}
await this.setCurrentSelection(input.currentSelection, input.pluginId, record.releaseId);
return {
status: 'installed',
pluginId: input.pluginId,
releaseId: record.releaseId,
version: record.version,
packageRoot: input.packageRoot,
definition,
};
}
private verifyArtifact(artifact: Uint8Array, grant: DownloadGrant): PluginReleaseDescriptor {
const expiresAt = typeof grant.expiresAt === 'string' ? Date.parse(grant.expiresAt) : Number.NaN;
if (!Number.isFinite(expiresAt) || expiresAt <= this.now()) {
fail('plugin_release_unavailable', 'download admission has expired');
}
if (!(artifact instanceof Uint8Array)) fail('plugin_artifact_invalid', 'downloaded artifact is not binary data');
if (artifact.byteLength !== grant.sizeBytes || artifact.byteLength > this.maxArchiveBytes || digest(artifact) !== grant.sha256) {
fail('plugin_artifact_invalid', 'downloaded artifact does not match its grant');
}
const descriptor = this.buildDescriptor(grant);
if (!isMakeLoreVersionCompatible(
this.clientVersion,
descriptor.minMakeloreVersion,
descriptor.maxMakeloreVersion,
)) {
fail('plugin_incompatible_client', 'Release is incompatible with this MakeLore client');
}
const verification = this.signatureVerifier.verify({
keyId: grant.signingKeyId,
signature: grant.descriptorSignature,
descriptor,
artifact,
});
if (!verification.ok) fail(mapVerificationFailure(verification.code), verification.message);
return descriptor;
}
private installedRecord(grant: DownloadGrant): InstalledReleaseRecord {
return Object.freeze({
pluginId: grant.pluginId,
releaseId: grant.releaseId,
version: grant.version,
packageSchemaVersion: grant.packageSchemaVersion,
contractVersion: grant.contractVersion,
runtimeKind: 'skill_only',
sha256: grant.sha256,
sizeBytes: grant.sizeBytes,
installedAt: new Date(this.now()).toISOString(),
});
}
private async removeUnusedLocked(validated: string): Promise<InstallationSnapshot> {
const index = await this.readIndex();
const currentSelection = await this.readCurrentSelection();
const records = index.releases.filter((record) => record.pluginId === validated);
if (records.length === 0) {
if (currentSelection.current[validated] !== undefined) {
const current = { ...currentSelection.current };
delete current[validated];
await this.writeCurrentSelection({
schema_version: CURRENT_SELECTION_SCHEMA_VERSION,
current: Object.freeze(current),
});
}
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 remaining = index.releases.filter((record) => !removable.includes(record));
try {
await this.writeIndex(this.indexPath, serializeIndex({ schema_version: INDEX_SCHEMA_VERSION, releases: remaining }));
} catch {
throw new PluginPackageStoreError('plugin_install_failed', 'package index cleanup failed');
}
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];
}
await this.writeCurrentSelection({
schema_version: CURRENT_SELECTION_SCHEMA_VERSION,
current: Object.freeze(nextCurrent),
});
await Promise.all(removable.map(async (record) => {
await rm(this.releaseDirectory(record), { recursive: true, force: true });
}));
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 };
}
private buildDescriptor(grant: DownloadGrant): PluginReleaseDescriptor {
try {
return buildPluginReleaseDescriptor({
@@ -850,6 +1024,39 @@ export class PluginPackageStore {
}
}
private async readCurrentSelection(): Promise<CurrentSelectionDocument> {
try {
return parseCurrentSelectionDocument(await parseJsonFile(this.currentSelectionPath));
} catch (error) {
if (isNotFound(error)) {
return { schema_version: CURRENT_SELECTION_SCHEMA_VERSION, current: Object.freeze({}) };
}
if (error instanceof PluginPackageStoreError) throw error;
throw new PluginPackageStoreError('plugin_store_index_invalid', 'current package selection is unavailable');
}
}
private async writeCurrentSelection(document: CurrentSelectionDocument): Promise<void> {
try {
await atomicWriteIndex(this.currentSelectionPath, serializeCurrentSelection(document));
} catch {
throw new PluginPackageStoreError('plugin_install_failed', 'package current selection replacement failed');
}
}
private async setCurrentSelection(
currentSelection: CurrentSelectionDocument,
pluginId: string,
releaseId: string,
): Promise<void> {
if (currentSelection.current[pluginId] === releaseId) return;
const current = { ...currentSelection.current, [pluginId]: releaseId };
await this.writeCurrentSelection({
schema_version: CURRENT_SELECTION_SCHEMA_VERSION,
current: Object.freeze(current),
});
}
private releaseDirectory(record: Pick<InstalledReleaseRecord, 'pluginId' | 'releaseId'>): string {
return path.join(this.packagesDir, record.pluginId, record.releaseId);
}
@@ -858,13 +1065,18 @@ export class PluginPackageStore {
index: IndexDocument,
pluginId: string,
releaseId?: string,
currentSelection?: CurrentSelectionDocument,
): Promise<InstalledRelease | null> {
const pointedReleaseId = releaseId === undefined ? currentSelection?.current[pluginId] : undefined;
const records = index.releases
.map((record, index) => ({ record, index }))
.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);
for (const record of records) {
const selected = pointedReleaseId === undefined
? records
: records.filter((record) => record.releaseId === pointedReleaseId);
for (const record of selected) {
const packageRoot = this.releaseDirectory(record);
if (!await isDirectoryPath(packageRoot)) continue;
try {
@@ -895,6 +1107,24 @@ export class PluginPackageStore {
if (isRecord(error) && error.code === 'marketplace_beta_selection_required') {
return new PluginPackageStoreError('plugin_beta_selection_required');
}
if (isRecord(error) && (
error.code === 'plugin_release_yanked'
|| error.code === 'plugin_release_not_ready'
|| error.code === 'plugin_runtime_suspended'
|| error.code === 'plugin_library_required'
|| error.code === 'plugin_backend_unavailable'
|| error.code === 'plugin_incompatible_client'
|| error.code === 'plugin_signature_invalid'
|| error.code === 'plugin_artifact_invalid'
)) {
return new PluginPackageStoreError(
error.code,
typeof error.message === 'string' ? error.message : error.code,
);
}
if (isRecord(error) && error.code === 'plugin_auth_required') {
return new PluginPackageStoreError('plugin_account_changed', 'Marketplace authentication is required');
}
return new PluginPackageStoreError('plugin_install_failed', 'Marketplace package operation failed');
}