fix(plugins): load official hosted plugins from bundled resources
This commit is contained in:
@@ -73,6 +73,7 @@ import {
|
||||
type CodingProjectPluginService,
|
||||
type CodingPluginMarketplaceService,
|
||||
} from './coding-product-services';
|
||||
import { CODE_OWNED_OPTIONAL_BUNDLED_RELEASES } from '../../shared/coding-plugins';
|
||||
|
||||
export interface CodingCompositionPaths {
|
||||
executablePath: string;
|
||||
@@ -289,12 +290,14 @@ export function createCodingComposition(
|
||||
marketplace: marketplaceClient,
|
||||
packageStore,
|
||||
makeloreVersion: options.clientVersion ?? '2.0.0',
|
||||
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
|
||||
});
|
||||
const webSearchAdapter = createWebSearchPluginAdapter({
|
||||
client: new WebSearchClient(),
|
||||
marketplace: marketplaceClient,
|
||||
packageStore,
|
||||
makeloreVersion: options.clientVersion ?? '2.0.0',
|
||||
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
|
||||
});
|
||||
const policyClient = options.policyClient ?? new PluginPolicyClient();
|
||||
const knownPluginIds = new Set(pluginDefinitions.map(({ id }) => id));
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user