Files
makelore/electron/coding-plugins/package-store.ts

932 lines
38 KiB
TypeScript

import { Buffer } from 'node:buffer';
import { createHash, randomUUID } from 'node:crypto';
import {
lstat,
mkdir,
mkdtemp,
readFile,
rename,
rm,
stat,
writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import AdmZip from 'adm-zip';
import { getDataDir } from '../utils/paths';
import { subscribeWorksSquareSession } from '../services/works-square-session';
import {
loadCodingPluginDefinition,
} from './manifest';
import type { CodingPluginDefinition } from '../../shared/coding-plugins';
import {
buildPluginReleaseDescriptor,
isMakeLoreVersionCompatible,
isValidSemVer,
type PluginReleaseDescriptor,
} from './release-descriptor';
import {
createPluginSignatureVerifier,
type PluginSignatureFailureCode,
type PluginSignatureVerifier,
} from './signature-verifier';
import {
AccountPluginCache,
type AccountBinding,
type MarketplaceLibrarySnapshot,
type MarketplaceResolveItem,
type MarketplaceResolveSnapshot,
} from './account-plugin-cache';
import type {
DownloadGrant,
DownloadRequest,
InstalledReleaseInput,
MarketplaceClient,
ResolveRequest,
} from './marketplace-client';
import type { PluginSigningKeyStore } from './trusted-keys';
const INDEX_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 MAX_PLUGIN_ID = 128;
const MAX_RELEASE_ID = 128;
const MAX_VERSION = 128;
const MAX_CONTRACT_VERSION = 2 ** 31 - 1;
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
const INDEX_ROOT_KEYS = new Set(['schema_version', 'releases']);
const INDEX_RELEASE_KEYS = new Set([
'plugin_id',
'release_id',
'version',
'package_schema_version',
'contract_version',
'runtime_kind',
'sha256',
'size_bytes',
'installed_at',
]);
type UnknownRecord = Record<string, unknown>;
export type { DownloadGrant, MarketplaceResolveSnapshot } from './marketplace-client';
export type ResolveSnapshot = MarketplaceResolveSnapshot;
export interface InstalledReleaseRecord {
readonly pluginId: string;
readonly releaseId: string;
readonly version: string;
readonly packageSchemaVersion: number;
readonly contractVersion: number;
readonly runtimeKind: 'skill_only' | 'platform_hosted';
readonly sha256: string;
readonly sizeBytes: number;
readonly installedAt: string;
}
export interface InstalledRelease extends InstalledReleaseRecord {
readonly packageRoot: string;
readonly definition: CodingPluginDefinition;
}
export type InstallationStatus = 'installed' | 'kept' | 'removed' | 'unavailable';
export interface InstallationSnapshot {
readonly status: InstallationStatus;
readonly pluginId: string;
readonly releaseId?: string;
readonly version?: string;
readonly packageRoot?: string;
readonly definition?: CodingPluginDefinition;
readonly reason?: string;
}
export interface ResolveInstallInput {
readonly pluginId: string;
readonly makeloreVersion: string;
readonly channel?: 'stable' | 'beta';
/** Beta packages are never selected by background/stable sync. */
readonly explicitBeta?: boolean;
readonly resolveRequestId?: string;
readonly resolveRequestDigest?: string;
readonly installed?: InstalledReleaseInput | null;
}
export interface MarketplacePackageClientPort {
resolve(input: ResolveRequest): Promise<MarketplaceResolveSnapshot>;
issueDownload(input: DownloadRequest): Promise<DownloadGrant>;
downloadContent(grant: DownloadGrant): Promise<Uint8Array>;
readLibrary?(): Promise<MarketplaceLibrarySnapshot>;
getCurrentAccountBinding?(): AccountBinding | null;
}
export interface PluginPackageStoreOptions {
/** A test-owned root may be supplied; production derives it from appData. */
readonly rootDir?: string;
readonly marketplace: MarketplacePackageClientPort | MarketplaceClient;
readonly accountCache?: AccountPluginCache;
readonly getAccountBinding?: () => AccountBinding | null;
readonly subscribeSession?: (listener: () => void) => () => void;
readonly clientVersion?: string;
readonly keyStore?: PluginSigningKeyStore | ReadonlyMap<string, Uint8Array | string>;
readonly signatureVerifier?: PluginSignatureVerifier;
readonly now?: () => number;
readonly maxArchiveBytes?: number;
readonly maxExtractedBytes?: number;
readonly maxFiles?: number;
readonly maxFileBytes?: number;
/** Replaced only by focused interruption tests; production is atomic. */
readonly writeIndex?: (filePath: string, bytes: Uint8Array) => Promise<void>;
readonly activeWorkerReleaseIds?: () => readonly string[];
}
export type PluginPackageStoreErrorCode =
| 'plugin_store_index_invalid'
| 'plugin_beta_selection_required'
| 'plugin_release_unavailable'
| 'plugin_account_changed'
| 'plugin_artifact_invalid'
| 'plugin_signature_invalid'
| 'plugin_incompatible_client'
| 'plugin_manifest_invalid'
| 'plugin_runtime_not_supported'
| 'plugin_release_conflict'
| 'plugin_install_failed';
export class PluginPackageStoreError extends Error {
constructor(
readonly code: PluginPackageStoreErrorCode,
message: string = code,
) {
super(message);
this.name = 'PluginPackageStoreError';
}
}
interface IndexDocument {
readonly schema_version: typeof INDEX_SCHEMA_VERSION;
readonly releases: readonly InstalledReleaseRecord[];
}
function isRecord(value: unknown): value is UnknownRecord {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function fail(code: PluginPackageStoreErrorCode, message: string = code): never {
throw new PluginPackageStoreError(code, message);
}
function assertExactKeys(value: UnknownRecord, allowed: ReadonlySet<string>, context: string): void {
for (const key of Object.keys(value)) {
if (!allowed.has(key)) fail('plugin_store_index_invalid', `unknown ${context} field: ${key.slice(0, 128)}`);
}
}
function boundedText(
value: unknown,
field: string,
maximum: number,
code: PluginPackageStoreErrorCode = 'plugin_store_index_invalid',
): string {
if (typeof value !== 'string' || value.length === 0 || value.length > maximum) {
fail(code, `invalid ${field}`);
}
return value;
}
function positiveInteger(value: unknown, field: string, maximum = Number.MAX_SAFE_INTEGER): number {
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
fail('plugin_store_index_invalid', `invalid ${field}`);
}
return value as number;
}
function validPluginId(
value: unknown,
field = 'pluginId',
code: PluginPackageStoreErrorCode = 'plugin_release_unavailable',
): string {
const result = boundedText(value, field, MAX_PLUGIN_ID, code);
if (!PLUGIN_ID_PATTERN.test(result)) fail(code, `invalid ${field}`);
return result;
}
function validReleaseId(
value: unknown,
field = 'releaseId',
code: PluginPackageStoreErrorCode = 'plugin_release_unavailable',
): string {
const result = boundedText(value, field, MAX_RELEASE_ID, code);
if (!RELEASE_ID_PATTERN.test(result)) fail(code, `invalid ${field}`);
return result;
}
function validSha(
value: unknown,
field: string,
code: PluginPackageStoreErrorCode = 'plugin_artifact_invalid',
): string {
const result = boundedText(value, field, 64, code);
if (!SHA256_PATTERN.test(result)) fail(code, `invalid ${field}`);
return result;
}
function validVersion(value: unknown, field: string): string {
const result = boundedText(value, field, MAX_VERSION);
if (!isValidSemVer(result)) fail('plugin_store_index_invalid', `invalid ${field}`);
return result;
}
function assertPositiveOption(value: number | undefined, fallback: number, field: string): number {
const result = value ?? fallback;
if (!Number.isSafeInteger(result) || result < 1) throw new RangeError(`${field} must be a positive safe integer`);
return result;
}
function clone<T>(value: T): T {
return structuredClone(value);
}
function digest(bytes: Uint8Array): string {
return createHash('sha256').update(bytes).digest('hex');
}
function sameBinding(left: AccountBinding | null, right: AccountBinding): boolean {
return left !== null && left.accountKey === right.accountKey && left.epoch === right.epoch;
}
function toBuffer(bytes: Uint8Array): Buffer {
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}
function isNotFound(error: unknown): boolean {
return isRecord(error) && error.code === 'ENOENT';
}
function isDirectoryPath(value: string): Promise<boolean> {
return stat(value).then((entry) => entry.isDirectory()).catch(() => false);
}
function parseIndexDocument(value: unknown): IndexDocument {
const root = isRecord(value) ? value : fail('plugin_store_index_invalid', 'index root must be an object');
assertExactKeys(root, INDEX_ROOT_KEYS, 'index');
if (root.schema_version !== INDEX_SCHEMA_VERSION) fail('plugin_store_index_invalid', 'unsupported index schema');
if (!Array.isArray(root.releases) || root.releases.length > DEFAULT_MAX_FILES) {
fail('plugin_store_index_invalid', 'invalid index releases');
}
const seen = new Set<string>();
const releases = root.releases.map((value, index) => {
const record = isRecord(value) ? value : fail('plugin_store_index_invalid', `invalid release ${index}`);
assertExactKeys(record, INDEX_RELEASE_KEYS, `release ${index}`);
if (record.package_schema_version !== 2) fail('plugin_store_index_invalid', `invalid release ${index} schema`);
const pluginId = validPluginId(record.plugin_id, `release ${index}.plugin_id`, 'plugin_store_index_invalid');
const releaseId = validReleaseId(record.release_id, `release ${index}.release_id`, 'plugin_store_index_invalid');
const key = `${pluginId}\u0000${releaseId}`;
if (seen.has(key)) fail('plugin_store_index_invalid', `duplicate release ${releaseId}`);
seen.add(key);
const runtimeKind = record.runtime_kind;
if (runtimeKind !== 'skill_only') {
fail('plugin_store_index_invalid', `invalid release ${index}.runtime_kind`);
}
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`);
return Object.freeze({
pluginId,
releaseId,
version: validVersion(record.version, `release ${index}.version`),
packageSchemaVersion: 2 as const,
contractVersion: positiveInteger(record.contract_version, `release ${index}.contract_version`, MAX_CONTRACT_VERSION),
runtimeKind,
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,
});
});
return Object.freeze({ schema_version: INDEX_SCHEMA_VERSION, releases: Object.freeze(releases) });
}
function serializeIndex(document: IndexDocument): Uint8Array {
return Buffer.from(`${JSON.stringify({
schema_version: document.schema_version,
releases: document.releases.map((record) => ({
plugin_id: record.pluginId,
release_id: record.releaseId,
version: record.version,
package_schema_version: record.packageSchemaVersion,
contract_version: record.contractVersion,
runtime_kind: record.runtimeKind,
sha256: record.sha256,
size_bytes: record.sizeBytes,
installed_at: record.installedAt,
})),
})}\n`, 'utf8');
}
async function parseJsonFile(filePath: string): Promise<unknown> {
const bytes = await readFile(filePath);
let source: string;
try {
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch {
fail('plugin_store_index_invalid', 'index is not valid UTF-8');
}
try {
return JSON.parse(source) as unknown;
} catch {
fail('plugin_store_index_invalid', 'index is not valid JSON');
}
}
async function atomicWriteIndex(filePath: string, bytes: Uint8Array): Promise<void> {
await mkdir(path.dirname(filePath), { recursive: true });
const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.tmp`);
try {
await writeFile(temporaryPath, toBuffer(bytes), { flag: 'wx' });
await rename(temporaryPath, filePath);
} finally {
await rm(temporaryPath, { force: true }).catch(() => undefined);
}
}
function safeArchiveEntryName(entryName: string, isDirectory: boolean): string[] {
if (entryName.length === 0 || entryName.includes('\\') || entryName.startsWith('/')
|| /^[A-Za-z]:/u.test(entryName) || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(entryName)) {
fail('plugin_artifact_invalid', 'archive contains a non-canonical path');
}
const segments = entryName.split('/');
if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
fail('plugin_artifact_invalid', 'archive contains a path traversal');
}
if (isDirectory && !entryName.endsWith('/')) {
fail('plugin_artifact_invalid', 'archive directory path is not canonical');
}
if (!isDirectory && entryName.endsWith('/')) {
fail('plugin_artifact_invalid', 'archive file path is not canonical');
}
return isDirectory ? segments.slice(0, -1) : segments;
}
function entryIsSymlink(entry: AdmZip.IZipEntry): boolean {
const mode = (entry.attr >>> 16) & 0xffff;
return (mode & 0xf000) === 0xa000;
}
async function extractArchive(
bytes: Uint8Array,
destination: string,
options: {
readonly maxExtractedBytes: number;
readonly maxFiles: number;
readonly maxFileBytes: number;
},
): Promise<void> {
let archive: AdmZip;
try {
archive = new AdmZip(toBuffer(bytes));
} catch {
fail('plugin_artifact_invalid', 'artifact is not a readable ZIP archive');
}
const entries = archive.getEntries();
if (entries.length === 0 || entries.length > options.maxFiles) {
fail('plugin_artifact_invalid', 'artifact file count is outside its bound');
}
const seen = new Set<string>();
let extractedBytes = 0;
for (const entry of entries) {
if (entryIsSymlink(entry)) fail('plugin_artifact_invalid', 'archive symlinks are not supported');
const segments = safeArchiveEntryName(entry.entryName, entry.isDirectory);
const relative = segments.join('/');
if (seen.has(relative)) fail('plugin_artifact_invalid', 'archive contains duplicate paths');
seen.add(relative);
const target = path.resolve(destination, ...segments);
const relativeTarget = path.relative(destination, target);
if (!relativeTarget || relativeTarget === '..' || relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget)) {
fail('plugin_artifact_invalid', 'archive path escapes its staging directory');
}
if (entry.isDirectory) {
await mkdir(target, { recursive: true });
continue;
}
const declaredSize = entry.header.size;
if (!Number.isSafeInteger(declaredSize) || declaredSize < 0 || declaredSize > options.maxFileBytes) {
fail('plugin_artifact_invalid', 'archive file exceeds its bound');
}
extractedBytes += declaredSize;
if (extractedBytes > options.maxExtractedBytes) fail('plugin_artifact_invalid', 'archive extraction exceeds its bound');
let content: Buffer;
try {
content = entry.getData();
} catch {
fail('plugin_artifact_invalid', 'archive entry could not be decompressed');
}
if (content.byteLength !== declaredSize || content.byteLength > options.maxFileBytes) {
fail('plugin_artifact_invalid', 'archive entry size is invalid');
}
await mkdir(path.dirname(target), { recursive: true });
try {
await writeFile(target, content, { flag: 'wx' });
} catch {
fail('plugin_artifact_invalid', 'archive extraction could not create a file');
}
}
}
function mapVerificationFailure(code: PluginSignatureFailureCode): PluginPackageStoreErrorCode {
return code;
}
function validateGrantAgainstResolve(
item: MarketplaceResolveItem,
grant: DownloadGrant,
input: ResolveInstallInput,
now: number,
maxArchiveBytes: number,
): void {
if (!item.releaseId || !item.version || !item.sha256 || !item.sizeBytes || !item.releaseAdmissionId) {
fail('plugin_release_unavailable', 'resolve result is missing download admission metadata');
}
validReleaseId(item.releaseId);
validPluginId(grant.pluginId);
validReleaseId(grant.releaseId);
validReleaseId(grant.releaseAdmissionId);
validSha(item.sha256, 'resolve sha256');
if (!Number.isSafeInteger(item.sizeBytes) || item.sizeBytes < 1 || item.sizeBytes > maxArchiveBytes) {
fail('plugin_artifact_invalid', 'resolve artifact size is outside its bound');
}
if (!Number.isSafeInteger(grant.sizeBytes) || grant.sizeBytes < 1 || grant.sizeBytes > maxArchiveBytes) {
fail('plugin_artifact_invalid', 'download grant size is outside its bound');
}
if (item.pluginId !== input.pluginId || grant.pluginId !== input.pluginId
|| grant.releaseId !== item.releaseId || grant.version !== item.version
|| grant.sha256 !== item.sha256 || grant.sizeBytes !== item.sizeBytes
|| grant.releaseAdmissionId !== item.releaseAdmissionId) {
fail('plugin_release_unavailable', 'download grant does not match the resolve result');
}
if (grant.packageSchemaVersion !== 2) fail('plugin_artifact_invalid', 'downloaded Release must use package schema 2');
const expiresAt = typeof grant.expiresAt === 'string' ? Date.parse(grant.expiresAt) : Number.NaN;
if (!Number.isFinite(expiresAt) || expiresAt <= now) fail('plugin_release_unavailable', 'download admission has expired');
}
function toInstalledInput(record: InstalledReleaseRecord): InstalledReleaseInput {
return { pluginId: record.pluginId, releaseId: record.releaseId, sha256: record.sha256 };
}
/**
* Main-owned device package store. The on-disk index is intentionally
* account-free; account Library/admission state stays in AccountPluginCache.
*/
export class PluginPackageStore {
private readonly rootDir: string;
private readonly packagesDir: string;
private readonly indexPath: string;
private readonly marketplace: MarketplacePackageClientPort;
private readonly accountCache: AccountPluginCache;
private readonly getAccountBindingImpl: () => AccountBinding | null;
private readonly clientVersion: string;
private readonly signatureVerifier: PluginSignatureVerifier;
private readonly now: () => number;
private readonly maxArchiveBytes: number;
private readonly maxExtractedBytes: number;
private readonly maxFiles: number;
private readonly maxFileBytes: number;
private readonly writeIndex: (filePath: string, bytes: Uint8Array) => Promise<void>;
private readonly activeWorkerReleaseIdsImpl: (() => readonly string[]) | null;
private readonly activeWorkers = new Set<string>();
private readonly unsubscribeSession: (() => void) | null;
private operation: Promise<void> = Promise.resolve();
constructor(options: PluginPackageStoreOptions) {
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.marketplace = options.marketplace;
this.accountCache = options.accountCache ?? new AccountPluginCache();
this.getAccountBindingImpl = options.getAccountBinding
?? (() => this.marketplace.getCurrentAccountBinding?.() ?? null);
this.clientVersion = options.clientVersion ?? '1.0.0';
this.signatureVerifier = options.signatureVerifier ?? createPluginSignatureVerifier({
clientVersion: this.clientVersion,
keyStore: options.keyStore,
});
this.now = options.now ?? (() => Date.now());
this.maxArchiveBytes = assertPositiveOption(options.maxArchiveBytes, DEFAULT_MAX_ARCHIVE_BYTES, 'maxArchiveBytes');
this.maxExtractedBytes = assertPositiveOption(options.maxExtractedBytes, DEFAULT_MAX_EXTRACTED_BYTES, 'maxExtractedBytes');
this.maxFiles = assertPositiveOption(options.maxFiles, DEFAULT_MAX_FILES, 'maxFiles');
this.maxFileBytes = assertPositiveOption(options.maxFileBytes, DEFAULT_MAX_FILE_BYTES, 'maxFileBytes');
this.writeIndex = options.writeIndex ?? atomicWriteIndex;
this.activeWorkerReleaseIdsImpl = options.activeWorkerReleaseIds ?? null;
const subscribeSession = options.subscribeSession
?? ((listener: () => void) => subscribeWorksSquareSession(() => listener()));
this.unsubscribeSession = subscribeSession(() => this.accountCache.invalidateAll());
}
get indexFilePath(): string {
return this.indexPath;
}
get packageRoot(): string {
return this.rootDir;
}
dispose(): void {
this.unsubscribeSession?.();
this.accountCache.invalidateAll();
}
async syncLibrary(): Promise<MarketplaceLibrarySnapshot> {
return this.withOperation(async () => {
const binding = this.requireBinding();
if (!this.marketplace.readLibrary) fail('plugin_install_failed', 'Marketplace Library is unavailable');
const snapshot = await this.marketplace.readLibrary();
this.assertBinding(binding);
this.accountCache.setLibrary(binding, snapshot);
return clone(snapshot);
});
}
async resolveAndInstall(input: ResolveInstallInput): Promise<InstallationSnapshot> {
return this.withOperation(() => this.resolveAndInstallLocked(input));
}
async getInstalled(pluginId: string): Promise<InstalledRelease | null> {
const validated = validPluginId(pluginId);
const index = await this.readIndex();
return this.getInstalledFromIndex(index, validated);
}
async removeUnused(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 };
});
}
registerActiveWorker(releaseId: string): void {
this.activeWorkers.add(validReleaseId(releaseId));
}
releaseActiveWorker(releaseId: string): void {
this.activeWorkers.delete(validReleaseId(releaseId));
}
async readInstalledIndex(): Promise<readonly InstalledReleaseRecord[]> {
const index = await this.readIndex();
return clone(index.releases);
}
private async resolveAndInstallLocked(input: ResolveInstallInput): Promise<InstallationSnapshot> {
const pluginId = validPluginId(input.pluginId);
if (input.channel !== undefined && input.channel !== 'stable' && input.channel !== 'beta') {
fail('plugin_release_unavailable', 'invalid Marketplace channel');
}
const channel = input.channel ?? 'stable';
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 installed = input.installed === undefined
? (current ? [toInstalledInput(current)] : [])
: input.installed === null ? [] : [input.installed];
const request: ResolveRequest = {
makeloreVersion: input.makeloreVersion,
channel,
installed,
resolveRequestId: input.resolveRequestId,
resolveRequestDigest: input.resolveRequestDigest,
};
let resolved: MarketplaceResolveSnapshot;
try {
resolved = await this.marketplace.resolve(request);
} catch (error) {
throw this.mapMarketplaceError(error);
}
this.assertBinding(binding);
const item = resolved.items.find((candidate) => candidate.pluginId === pluginId);
if (!item) fail('plugin_release_unavailable', `Plugin ${pluginId} was not included in the resolve result`);
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 === 'keep') {
if (!current) fail('plugin_release_unavailable', 'resolve requested keep without an installed Release');
return {
status: 'kept',
pluginId,
releaseId: current.releaseId,
version: current.version,
packageRoot: current.packageRoot,
definition: current.definition,
};
}
if (resolved.stale) fail('plugin_release_unavailable', 'stale resolve data cannot install a Release');
if (item.action !== 'install' && item.action !== 'update') fail('plugin_release_unavailable', 'unsupported resolve action');
if (!item.releaseId || !item.releaseAdmissionId || !item.version || !item.sha256 || !item.sizeBytes) {
fail('plugin_release_unavailable', 'resolve result is missing Release metadata');
}
const releaseId = validReleaseId(item.releaseId);
const releaseAdmissionId = validReleaseId(item.releaseAdmissionId);
validSha(item.sha256, 'resolve sha256');
if (!Number.isSafeInteger(item.sizeBytes) || item.sizeBytes < 1 || item.sizeBytes > this.maxArchiveBytes) {
fail('plugin_artifact_invalid', 'resolve artifact size is outside its bound');
}
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) {
return {
status: 'kept',
pluginId,
releaseId: existing.releaseId,
version: existing.version,
packageRoot: existing.packageRoot,
definition: existing.definition,
};
}
}
let grant: DownloadGrant;
try {
grant = await this.marketplace.issueDownload({ releaseId, releaseAdmissionId });
this.assertBinding(binding);
validateGrantAgainstResolve(item, grant, { ...input, pluginId }, this.now(), this.maxArchiveBytes);
} catch (error) {
if (error instanceof PluginPackageStoreError) throw error;
throw this.mapMarketplaceError(error);
}
let artifact: Uint8Array;
try {
artifact = await this.marketplace.downloadContent(grant);
this.assertBinding(binding);
} catch (error) {
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);
try {
await mkdir(this.rootDir, { recursive: true });
} catch {
fail('plugin_install_failed', 'package store is unavailable');
}
let staging: string;
try {
staging = await mkdtemp(path.join(this.rootDir, `.download-${randomUUID()}-`));
} catch {
fail('plugin_install_failed', 'package store is unavailable');
}
let moved = false;
try {
const archivePath = path.join(staging, 'package.zip');
const extractedPath = path.join(staging, 'package');
await writeFile(archivePath, toBuffer(artifact), { flag: 'wx' });
await mkdir(extractedPath, { recursive: true });
await extractArchive(artifact, extractedPath, {
maxExtractedBytes: this.maxExtractedBytes,
maxFiles: this.maxFiles,
maxFileBytes: this.maxFileBytes,
});
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 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 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');
}
return {
status: 'installed',
pluginId,
releaseId: record.releaseId,
version: record.version,
packageRoot,
definition,
};
} catch (error) {
if (error instanceof PluginPackageStoreError) throw error;
if (error instanceof Error && error.name === 'CodingPluginManifestError') {
throw new PluginPackageStoreError('plugin_manifest_invalid', 'package manifest is invalid');
}
throw new PluginPackageStoreError('plugin_install_failed', 'package installation failed');
} finally {
if (moved) {
await rm(path.join(staging, 'package.zip'), { force: true }).catch(() => undefined);
}
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
}
}
private buildDescriptor(grant: DownloadGrant): PluginReleaseDescriptor {
try {
return buildPluginReleaseDescriptor({
pluginId: grant.pluginId,
version: grant.version,
packageSchemaVersion: grant.packageSchemaVersion,
contractVersion: grant.contractVersion,
minMakeloreVersion: grant.minMakeloreVersion,
maxMakeloreVersion: grant.maxMakeloreVersion,
artifact: { sha256: grant.sha256, sizeBytes: grant.sizeBytes },
});
} catch (error) {
fail('plugin_artifact_invalid', error instanceof Error ? error.message : 'invalid Release descriptor');
}
}
private async loadDefinition(
packageRoot: string,
grant: DownloadGrant,
descriptor: PluginReleaseDescriptor,
): Promise<CodingPluginDefinition> {
let definition: CodingPluginDefinition;
try {
definition = await loadCodingPluginDefinition(packageRoot, {
runtimeKind: 'skill_only',
acquisitionMode: 'user_acquired',
releaseId: grant.releaseId,
provenance: { source: 'marketplace', packageRoot },
});
} catch (error) {
if (error instanceof PluginPackageStoreError) throw error;
throw new PluginPackageStoreError('plugin_manifest_invalid', 'package manifest is invalid');
}
if (definition.id !== descriptor.pluginId || definition.version !== descriptor.version
|| definition.contractVersion !== descriptor.contractVersion || definition.releaseId !== grant.releaseId
|| definition.runtimeKind !== 'skill_only' || definition.acquisitionMode !== 'user_acquired') {
fail('plugin_manifest_invalid', 'package definition does not match the signed Release');
}
return definition;
}
private requireBinding(): AccountBinding {
const binding = this.getAccountBindingImpl();
if (!binding || typeof binding.accountKey !== 'string' || binding.accountKey.length === 0
|| binding.accountKey.length > 512 || !Number.isSafeInteger(binding.epoch) || binding.epoch < 0) {
fail('plugin_account_changed', 'Marketplace account session is unavailable');
}
return { accountKey: binding.accountKey, epoch: binding.epoch };
}
private assertBinding(binding: AccountBinding): void {
if (!sameBinding(this.getAccountBindingImpl(), binding)) fail('plugin_account_changed', 'Marketplace account changed during package operation');
}
private activeWorkerReleaseIds(): readonly string[] {
return this.activeWorkerReleaseIdsImpl?.() ?? [];
}
private async readIndex(): Promise<IndexDocument> {
try {
return parseIndexDocument(await parseJsonFile(this.indexPath));
} catch (error) {
if (isNotFound(error)) return { schema_version: INDEX_SCHEMA_VERSION, releases: [] };
if (error instanceof PluginPackageStoreError) throw error;
throw new PluginPackageStoreError('plugin_store_index_invalid', 'package index is unavailable');
}
}
private releaseDirectory(record: Pick<InstalledReleaseRecord, 'pluginId' | 'releaseId'>): string {
return path.join(this.packagesDir, record.pluginId, record.releaseId);
}
private async getInstalledFromIndex(
index: IndexDocument,
pluginId: string,
releaseId?: string,
): Promise<InstalledRelease | null> {
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 packageRoot = this.releaseDirectory(record);
if (!await isDirectoryPath(packageRoot)) continue;
try {
const definition = await loadCodingPluginDefinition(packageRoot, {
runtimeKind: record.runtimeKind,
acquisitionMode: 'user_acquired',
releaseId: record.releaseId,
provenance: { source: 'marketplace', packageRoot },
});
if (definition.id !== record.pluginId || definition.version !== record.version
|| 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 };
} catch (error) {
if (error instanceof PluginPackageStoreError) throw error;
throw new PluginPackageStoreError('plugin_manifest_invalid', 'installed package manifest is invalid');
}
}
return null;
}
private mapMarketplaceError(error: unknown): PluginPackageStoreError {
if (error instanceof PluginPackageStoreError) return error;
if (isRecord(error) && error.code === 'marketplace_account_changed') {
return new PluginPackageStoreError('plugin_account_changed', 'Marketplace account changed during package operation');
}
if (isRecord(error) && error.code === 'marketplace_beta_selection_required') {
return new PluginPackageStoreError('plugin_beta_selection_required');
}
return new PluginPackageStoreError('plugin_install_failed', 'Marketplace package operation failed');
}
private async withOperation<T>(operation: () => Promise<T>): Promise<T> {
let release!: () => void;
const previous = this.operation;
this.operation = new Promise<void>((resolve) => { release = resolve; });
await previous;
try {
return await operation();
} finally {
release();
}
}
}
async function pathExists(value: string): Promise<boolean> {
try {
await lstat(value);
return true;
} catch {
return false;
}
}
export async function readPluginPackageIndex(filePath: string): Promise<readonly InstalledReleaseRecord[]> {
try {
const value = parseIndexDocument(await parseJsonFile(filePath));
return clone(value.releases);
} catch (error) {
if (error instanceof PluginPackageStoreError) throw error;
throw new PluginPackageStoreError('plugin_store_index_invalid', 'package index is unavailable');
}
}