fix(plugins): load official hosted plugins from bundled resources

This commit is contained in:
2026-09-01 18:20:24 +08:00
parent 52e2a97a12
commit 1530ac7740
23 changed files with 835 additions and 48 deletions

View File

@@ -5,6 +5,7 @@ import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'
import { PiGameAssetTools } from '../../coding-runtime/pi/extensions/game-assets';
import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store';
import {
type BundledHostedRelease,
MarketplaceHostedAdmissionError,
MarketplaceHostedAdmissionResolver,
} from '../hosted-admission';
@@ -32,6 +33,7 @@ export interface GameResourcePluginAdapterOptions {
readonly marketplace: MarketplacePackageClientPort;
readonly packageStore: Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
readonly makeloreVersion: string;
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
readonly admissionResolver?: MarketplaceHostedAdmissionResolver;
readonly gameAssets?: PiGameAssetTools;
}
@@ -175,10 +177,12 @@ export class GameResourcePluginAdapter implements CodingPluginAdapter {
marketplace: options.marketplace,
packageStore: options.packageStore,
makeloreVersion: options.makeloreVersion,
bundledReleases: options.bundledReleases,
});
}
async inspect(): Promise<PluginBackendProjection> {
if (this.options.bundledReleases?.[PLUGIN_ID]) return { status: 'ready' };
const installed = await this.options.packageStore.getInstalled(PLUGIN_ID).catch(() => null);
return installed ? { status: 'ready' } : { status: 'unconfigured' };
}

View File

@@ -1,6 +1,7 @@
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
import type { CapabilityBillingReceiptV1 } from '../../../shared/data-service';
import {
type BundledHostedRelease,
MarketplaceHostedAdmissionError,
MarketplaceHostedAdmissionResolver,
} from '../hosted-admission';
@@ -62,6 +63,7 @@ export interface WebSearchPluginAdapterOptions {
readonly marketplace?: MarketplacePackageClientPort;
readonly packageStore?: Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
readonly makeloreVersion?: string;
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
readonly admissionResolver?: MarketplaceHostedAdmissionResolver;
}
@@ -230,6 +232,7 @@ export class WebSearchPluginAdapter implements CodingPluginAdapter {
marketplace: options.marketplace,
packageStore: options.packageStore,
makeloreVersion: options.makeloreVersion,
bundledReleases: options.bundledReleases,
});
} else {
throw new TypeError('Web Search adapter requires Marketplace admission dependencies');
@@ -237,6 +240,7 @@ export class WebSearchPluginAdapter implements CodingPluginAdapter {
}
async inspect(_projectPath: string): Promise<PluginBackendProjection> {
if (this.options.bundledReleases?.[PLUGIN_ID]) return { status: 'ready' };
const installed = await this.options.packageStore?.getInstalled(PLUGIN_ID).catch(() => null);
return installed ? { status: 'ready' } : { status: 'unconfigured' };
}

View File

@@ -444,7 +444,7 @@ export class EffectivePluginResolver {
// this seam is exclusively for immutable user-installed package roots.
return Object.freeze(definitions.flatMap(({ definition, installed, unavailableReason }) => (
!installed || unavailableReason || blockedMarketplacePlugins.has(definition.id)
|| definition.acquisitionMode !== 'user_acquired'
|| definition.provenance.source !== 'marketplace'
? []
: definition.skills.map((skill) => ({
id: skill.id,
@@ -464,11 +464,20 @@ export class EffectivePluginResolver {
pluginId: string,
releaseId?: string | null,
): Promise<CodingPluginDefinition | null> {
const records = await this.definitionRecords();
const bundled = records.find(({ definition }) => (
definition.id === pluginId
&& definition.provenance.source === 'bundled'
&& definition.releaseId === releaseId
));
if (bundled) {
return bundled.installed && !bundled.unavailableReason ? bundled.definition : null;
}
if (releaseId && this.options.packageStore) {
const installed = await this.options.packageStore.getInstalledRelease(pluginId, releaseId);
return installed && !installed.unavailableReason ? installed.definition : null;
}
const record = (await this.definitionRecords()).find(({ definition }) => definition.id === pluginId);
const record = records.find(({ definition }) => definition.id === pluginId);
return record?.installed && !record.unavailableReason ? record.definition : null;
}
@@ -491,6 +500,7 @@ export class EffectivePluginResolver {
}
for (const definition of [...records.values()].map(({ definition }) => definition)) {
if (definition.acquisitionMode !== 'user_acquired') continue;
if (definition.provenance.source === 'bundled') continue;
let installed = (this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id);
let unavailableReason: PluginUnavailableReasonCode | undefined;
if (installedIds.has(definition.id)) {

View File

@@ -45,6 +45,13 @@ export interface MarketplaceHostedAdmissionResolverOptions {
readonly packageStore: Pick<PluginPackageStore, 'getInstalledRelease'>;
readonly marketplace: Pick<MarketplacePackageClientPort, 'resolve'>;
readonly makeloreVersion: string;
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
}
export interface BundledHostedRelease {
readonly releaseId: string;
readonly version: string;
readonly channel: 'stable';
}
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -112,6 +119,24 @@ function matchesFrozenRelease(
return item.sha256 !== null && item.sha256 !== undefined && SHA256_PATTERN.test(item.sha256);
}
function matchesBundledFrozenRelease(
item: MarketplaceResolveItem,
bundled: BundledHostedRelease,
plugin: string,
): item is MarketplaceResolveItem & {
readonly releaseId: string;
readonly releaseAdmissionId: string;
} {
return item.pluginId === plugin
&& item.action === 'keep'
&& releaseId(item.releaseId) === bundled.releaseId
&& item.version === bundled.version
&& item.channel === bundled.channel
&& releaseId(item.releaseAdmissionId) !== null
&& item.sha256 == null
&& item.sizeBytes == null;
}
/**
* Resolves the current account admission for the exact Release frozen into a
* parent worker. Marketplace identity and Release freshness are the only
@@ -129,15 +154,22 @@ export class MarketplaceHostedAdmissionResolver {
if (!plugin || !request) throw unavailable('Hosted Plugin worker identity is unavailable');
if (!frozenRelease) throw unavailable('Hosted Plugin worker Release is unavailable');
let installed: Awaited<ReturnType<PluginPackageStore['getInstalledRelease']>>;
try {
installed = await this.options.packageStore.getInstalledRelease(plugin, frozenRelease);
} catch (error) {
throw this.mapError(error);
}
if (!installed || installed.pluginId !== plugin || installed.releaseId !== frozenRelease
|| installed.unavailableReason || !installed.channel) {
throw unavailable('Installed hosted Plugin Release is unavailable');
const bundled = this.options.bundledReleases?.[plugin];
let installed: Awaited<ReturnType<PluginPackageStore['getInstalledRelease']>> = null;
if (bundled) {
if (releaseId(bundled.releaseId) !== frozenRelease || bundled.channel !== 'stable') {
throw unavailable('Bundled hosted Plugin Release is unavailable');
}
} else {
try {
installed = await this.options.packageStore.getInstalledRelease(plugin, frozenRelease);
} catch (error) {
throw this.mapError(error);
}
if (!installed || installed.pluginId !== plugin || installed.releaseId !== frozenRelease
|| installed.unavailableReason || !installed.channel) {
throw unavailable('Installed hosted Plugin Release is unavailable');
}
}
let resolved;
@@ -145,11 +177,11 @@ export class MarketplaceHostedAdmissionResolver {
resolved = await this.options.marketplace.resolve({
resolveRequestId: request,
makeloreVersion: this.options.makeloreVersion,
channel: installed.channel,
installed: [{
pluginId: installed.pluginId,
releaseId: installed.releaseId,
sha256: installed.sha256,
channel: bundled?.channel ?? installed?.channel ?? 'stable',
installed: bundled ? [] : [{
pluginId: installed!.pluginId,
releaseId: installed!.releaseId,
sha256: installed!.sha256,
}],
});
} catch (error) {
@@ -160,7 +192,10 @@ export class MarketplaceHostedAdmissionResolver {
throw stale('Hosted Plugin worker admission is stale');
}
const item = resolved.items.find(({ pluginId: candidate }) => candidate === plugin);
if (!item || !matchesFrozenRelease(item, installed, plugin)) {
const matches = item && (bundled
? matchesBundledFrozenRelease(item, bundled, plugin)
: matchesFrozenRelease(item, installed, plugin));
if (!matches) {
throw stale('Hosted Plugin worker Release admission is stale');
}
return Object.freeze({

View File

@@ -10,10 +10,14 @@ import {
BUNDLED_CODING_PLUGIN_PREVIEW_SURFACES,
BUNDLED_CODING_PLUGIN_SETTINGS_SURFACES,
CODE_OWNED_PLUGIN_PERMISSION_IDS,
GAME_RESOURCE_BUNDLED_RELEASE_ID,
GAME_RESOURCE_PLUGIN_ID,
DATA_SERVICE_CAPABILITY_IDS,
DATA_SERVICE_OPERATION_DEFINITIONS,
DATA_SERVICE_PLUGIN_ID,
DATA_SERVICE_TOOL_NAMES,
WEB_SEARCH_BUNDLED_RELEASE_ID,
WEB_SEARCH_PLUGIN_ID,
type AgentPluginsRootManifest,
type CodingPluginDefinition,
type CodingPluginAcquisitionMode,
@@ -26,14 +30,40 @@ import {
} from '../../shared/coding-plugins';
/**
* P0 deliberately has one statically enumerated package root. Keeping this
* list relative makes it impossible for an environment variable or a project
* The trusted code-owned catalog is statically enumerated. Keeping these
* roots relative makes it impossible for an environment variable or project
* file to add a package to the trusted catalog.
*/
export const BUNDLED_CODING_PLUGIN_ROOTS = Object.freeze([
'data-service',
'game-resource',
'web-search',
] as const);
const BUNDLED_CODING_PLUGIN_METADATA = Object.freeze({
'data-service': Object.freeze({
pluginId: DATA_SERVICE_PLUGIN_ID,
runtimeKind: 'bundled_typed' as const,
acquisitionMode: 'system_included' as const,
releaseId: null,
bundledV2: false,
}),
'game-resource': Object.freeze({
pluginId: GAME_RESOURCE_PLUGIN_ID,
runtimeKind: 'platform_hosted' as const,
acquisitionMode: 'user_acquired' as const,
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
bundledV2: true,
}),
'web-search': Object.freeze({
pluginId: WEB_SEARCH_PLUGIN_ID,
runtimeKind: 'platform_hosted' as const,
acquisitionMode: 'user_acquired' as const,
releaseId: WEB_SEARCH_BUNDLED_RELEASE_ID,
bundledV2: true,
}),
});
const CAPABILITY_MANIFEST_RELATIVE_PATH = 'com.makelore/capability.json';
const PACKAGE_MANIFEST_FILE = 'plugin.json';
const SKILL_ID_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
@@ -160,11 +190,13 @@ export interface CodingPluginManifestParseOptions {
acquisitionMode?: CodingPluginAcquisitionMode;
releaseId?: string | null;
provenance?: CodingPluginPackageProvenance;
/** Only the fixed code-owned package catalog may opt schema 2 into bundled delivery. */
bundledV2?: boolean;
}
export type CodingPluginLoadOptions = Pick<
CodingPluginManifestParseOptions,
'runtimeKind' | 'acquisitionMode' | 'releaseId' | 'provenance'
'runtimeKind' | 'acquisitionMode' | 'releaseId' | 'provenance' | 'bundledV2'
>;
type UnknownRecord = Record<string, unknown>;
@@ -696,9 +728,16 @@ function trustedMetadata(
if (schemaVersion === 1 && releaseId !== null) {
fail(filePath, 'trusted metadata.releaseId', 'bundled schema-1 definitions cannot carry a Release ID');
}
const bundledV2 = schemaVersion === 2 && options.bundledV2 === true;
if (options.bundledV2 && !bundledV2) {
fail(filePath, 'trusted metadata.bundledV2', 'is valid only for schema 2 packages');
}
if (bundledV2 && (runtimeKind !== 'platform_hosted' || releaseId === null)) {
fail(filePath, 'trusted metadata.bundledV2', 'requires a hosted runtime and fixed Release ID');
}
const defaultProvenance: CodingPluginPackageProvenance = {
source: schemaVersion === 1 ? 'bundled' : 'marketplace',
packageRoot: schemaVersion === 1 ? path.basename(packageRoot) : path.resolve(packageRoot),
source: schemaVersion === 1 || bundledV2 ? 'bundled' : 'marketplace',
packageRoot: schemaVersion === 1 || bundledV2 ? path.basename(packageRoot) : path.resolve(packageRoot),
};
const provenance = options.provenance ?? defaultProvenance;
if (provenance.source !== defaultProvenance.source
@@ -1154,13 +1193,15 @@ export async function loadBundledCodingPluginDefinitions(
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
const definitions = await Promise.all(roots.map(async (root, index) => {
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
const metadata = BUNDLED_CODING_PLUGIN_METADATA[expectedRoot];
const definition = await loadCodingPluginDefinition(root, {
runtimeKind: 'bundled_typed',
acquisitionMode: 'system_included',
releaseId: null,
runtimeKind: metadata.runtimeKind,
acquisitionMode: metadata.acquisitionMode,
releaseId: metadata.releaseId,
provenance: { source: 'bundled', packageRoot: expectedRoot },
bundledV2: metadata.bundledV2,
});
if (definition.id !== `makelore.${expectedRoot}`) {
if (definition.id !== metadata.pluginId) {
throw new CodingPluginManifestError(
path.join(root, PACKAGE_MANIFEST_FILE),
'name',
@@ -1179,13 +1220,15 @@ export function loadBundledCodingPluginDefinitionsSync(
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
const definitions = roots.map((root, index) => {
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
const metadata = BUNDLED_CODING_PLUGIN_METADATA[expectedRoot];
const definition = loadCodingPluginDefinitionSync(root, {
runtimeKind: 'bundled_typed',
acquisitionMode: 'system_included',
releaseId: null,
runtimeKind: metadata.runtimeKind,
acquisitionMode: metadata.acquisitionMode,
releaseId: metadata.releaseId,
provenance: { source: 'bundled', packageRoot: expectedRoot },
bundledV2: metadata.bundledV2,
});
if (definition.id !== `makelore.${expectedRoot}`) {
if (definition.id !== metadata.pluginId) {
throw new CodingPluginManifestError(
path.join(root, PACKAGE_MANIFEST_FILE),
'name',