feat(plugins): move game resources behind marketplace
This commit is contained in:
319
electron/coding-plugins/adapters/game-resource.ts
Normal file
319
electron/coding-plugins/adapters/game-resource.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
|
||||
import { PiGameAssetTools } from '../../coding-runtime/pi/extensions/game-assets';
|
||||
import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store';
|
||||
import {
|
||||
GameResourceClient,
|
||||
GameResourceClientError,
|
||||
type GameResourceGeneration,
|
||||
} from '../../services/game-resource-client';
|
||||
import type {
|
||||
AdapterInvocationResult,
|
||||
CodingPluginAdapter,
|
||||
PluginBackendProjection,
|
||||
TrustedCodingCapabilityContext,
|
||||
} from '../registry';
|
||||
|
||||
const PLUGIN_ID = 'makelore.game-resource';
|
||||
const MAX_REFERENCE_FILE_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_REFERENCE_BYTES = 16 * 1024 * 1024;
|
||||
const EXECUTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
|
||||
|
||||
type Input = Record<string, unknown>;
|
||||
|
||||
export interface GameResourcePluginAdapterOptions {
|
||||
readonly client: GameResourceClient;
|
||||
readonly marketplace: MarketplacePackageClientPort;
|
||||
readonly packageStore: Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
|
||||
readonly makeloreVersion: string;
|
||||
readonly gameAssets?: PiGameAssetTools;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Input {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function success<T>(
|
||||
data: T,
|
||||
status = 200,
|
||||
billing?: Extract<NonNullable<AdapterInvocationResult['billing']>, { mode: 'platform_metered' }>,
|
||||
): AdapterInvocationResult<T> {
|
||||
return {
|
||||
success: true,
|
||||
status,
|
||||
code: null,
|
||||
error: null,
|
||||
retryable: false,
|
||||
payload_schema: 'game-resource.v1',
|
||||
data,
|
||||
...(billing ? { billing } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function failure(
|
||||
code: string,
|
||||
error: string,
|
||||
status: number,
|
||||
retryable: boolean,
|
||||
billing?: Extract<NonNullable<AdapterInvocationResult['billing']>, { mode: 'platform_metered' }>,
|
||||
): AdapterInvocationResult {
|
||||
return {
|
||||
success: false,
|
||||
status,
|
||||
code,
|
||||
error,
|
||||
retryable,
|
||||
payload_schema: 'game-resource.v1',
|
||||
data: null,
|
||||
...(billing ? { billing } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function generationData(value: GameResourceGeneration, includeBilling: boolean) {
|
||||
return {
|
||||
executionId: value.executionId,
|
||||
status: value.status,
|
||||
outputCount: value.outputCount,
|
||||
...(value.pollIntervalSeconds === undefined ? {} : { pollIntervalSeconds: value.pollIntervalSeconds }),
|
||||
...(value.errorCode === undefined ? {} : { errorCode: value.errorCode }),
|
||||
...(includeBilling ? { generationBilling: value.billing } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function clientFailure(error: unknown): AdapterInvocationResult {
|
||||
if (error instanceof GameResourceClientError) {
|
||||
return failure(error.code, error.message, error.status, error.retryable);
|
||||
}
|
||||
return failure(
|
||||
'plugin_backend_unavailable',
|
||||
'Hosted game-resource service is temporarily unavailable',
|
||||
503,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function relativeProjectPath(projectPath: string, candidate: unknown): { absolute: string; relative: string } {
|
||||
if (typeof candidate !== 'string' || !candidate.trim() || candidate.length > 1_024) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Project-relative path is invalid');
|
||||
}
|
||||
const root = path.resolve(projectPath);
|
||||
const absolute = path.resolve(root, candidate);
|
||||
const relative = path.relative(root, absolute);
|
||||
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Project-relative path is invalid');
|
||||
}
|
||||
return { absolute, relative: relative.split(path.sep).join('/') };
|
||||
}
|
||||
|
||||
function mimeType(filePath: string): string {
|
||||
switch (path.extname(filePath).toLowerCase()) {
|
||||
case '.jpg':
|
||||
case '.jpeg': return 'image/jpeg';
|
||||
case '.webp': return 'image/webp';
|
||||
case '.gif': return 'image/gif';
|
||||
default: return 'image/png';
|
||||
}
|
||||
}
|
||||
|
||||
async function references(projectPath: string, value: unknown) {
|
||||
if (value === undefined) return [];
|
||||
if (!Array.isArray(value) || value.length > 8) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Reference paths are invalid');
|
||||
}
|
||||
let total = 0;
|
||||
return await Promise.all(value.map(async (candidate) => {
|
||||
const target = relativeProjectPath(projectPath, candidate);
|
||||
const metadata = await stat(target.absolute).catch(() => null);
|
||||
if (!metadata?.isFile() || metadata.size <= 0 || metadata.size > MAX_REFERENCE_FILE_BYTES) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Reference file is unavailable or too large');
|
||||
}
|
||||
total += metadata.size;
|
||||
if (total > MAX_REFERENCE_BYTES) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Reference files exceed their total bound');
|
||||
}
|
||||
return {
|
||||
name: path.basename(target.absolute).slice(0, 160),
|
||||
mimeType: mimeType(target.absolute),
|
||||
dataBase64: (await readFile(target.absolute)).toString('base64'),
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
function templateConfig(value: unknown): Readonly<Record<string, unknown>> {
|
||||
if (value === undefined || value === '') return {};
|
||||
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > 16_384) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Template config JSON is invalid');
|
||||
}
|
||||
let parsed: unknown;
|
||||
try { parsed = JSON.parse(value) as unknown; } catch {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Template config JSON is invalid');
|
||||
}
|
||||
if (!isRecord(parsed)) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Template config JSON must be an object');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export class GameResourcePluginAdapter implements CodingPluginAdapter {
|
||||
readonly pluginId = PLUGIN_ID;
|
||||
private readonly gameAssets: PiGameAssetTools;
|
||||
|
||||
constructor(private readonly options: GameResourcePluginAdapterOptions) {
|
||||
this.gameAssets = options.gameAssets ?? new PiGameAssetTools();
|
||||
}
|
||||
|
||||
async inspect(): Promise<PluginBackendProjection> {
|
||||
const installed = await this.options.packageStore.getInstalled(PLUGIN_ID).catch(() => null);
|
||||
return installed ? { status: 'ready' } : { status: 'unconfigured' };
|
||||
}
|
||||
|
||||
async invoke(
|
||||
context: TrustedCodingCapabilityContext,
|
||||
tool: CodingPluginToolDefinition,
|
||||
input: unknown,
|
||||
): Promise<AdapterInvocationResult> {
|
||||
if (!isRecord(input)) return failure('plugin_input_invalid', 'Game-resource tool input is invalid', 422, false);
|
||||
try {
|
||||
switch (tool.name) {
|
||||
case 'game_resource_templates': {
|
||||
const kind = input.kind === 'pixel' || input.kind === 'hd' ? input.kind : null;
|
||||
if (!kind) return failure('plugin_input_invalid', 'Game-resource kind is invalid', 422, false);
|
||||
const admission = await this.admission(context);
|
||||
return success({ kind, templates: await this.options.client.templates({ ...admission, kind }) });
|
||||
}
|
||||
case 'game_resource_generate': {
|
||||
if (input.confirmed !== true) {
|
||||
return failure(
|
||||
'confirmation_required',
|
||||
'Explicit Token Point generation confirmation is required',
|
||||
400,
|
||||
false,
|
||||
);
|
||||
}
|
||||
const kind = input.kind === 'pixel' || input.kind === 'hd' ? input.kind : null;
|
||||
if (!kind || typeof input.templateName !== 'string' || typeof input.requirement !== 'string') {
|
||||
return failure('plugin_input_invalid', 'Game-resource generation input is invalid', 422, false);
|
||||
}
|
||||
const admission = await this.admission(context);
|
||||
const generated = await this.options.client.generate({
|
||||
...admission,
|
||||
projectId: context.durableProjectId,
|
||||
logicalOperationId: context.requestId,
|
||||
kind,
|
||||
templateName: input.templateName,
|
||||
templateConfig: templateConfig(input.templateConfigJson),
|
||||
requirement: input.requirement,
|
||||
...(typeof input.aspectRatio === 'string' ? { aspectRatio: input.aspectRatio } : {}),
|
||||
...(typeof input.temperature === 'number' ? { temperature: input.temperature } : {}),
|
||||
...(typeof input.jobName === 'string' ? { jobName: input.jobName } : {}),
|
||||
...(typeof input.modelName === 'string' ? { modelName: input.modelName } : {}),
|
||||
...(typeof input.resolution === 'string' ? { resolution: input.resolution } : {}),
|
||||
...(typeof input.hdRemoveBgMode === 'string' ? { hdRemoveBgMode: input.hdRemoveBgMode } : {}),
|
||||
...(typeof input.threadId === 'string' ? { threadId: input.threadId } : {}),
|
||||
referenceFiles: await references(context.projectPath, input.referencePaths),
|
||||
});
|
||||
if (generated.status === 'failed' || generated.status === 'cancelled') {
|
||||
return failure(
|
||||
generated.errorCode ?? 'game_resource_generation_failed',
|
||||
'Hosted game-resource generation was rejected',
|
||||
422,
|
||||
false,
|
||||
generated.billing,
|
||||
);
|
||||
}
|
||||
return success(
|
||||
generationData(generated, false),
|
||||
generated.status === 'succeeded' ? 200 : 202,
|
||||
generated.billing,
|
||||
);
|
||||
}
|
||||
case 'game_resource_status': {
|
||||
const executionId = this.executionId(input.executionId);
|
||||
const generated = await this.options.client.get(executionId);
|
||||
return success(generationData(generated, true));
|
||||
}
|
||||
case 'game_resource_cancel': {
|
||||
const executionId = this.executionId(input.executionId);
|
||||
const generated = await this.options.client.cancel(executionId);
|
||||
return success(generationData(generated, true));
|
||||
}
|
||||
case 'game_resource_save_output': {
|
||||
if (input.confirmed !== true) return failure('confirmation_required', 'Explicit save confirmation is required', 400, false);
|
||||
const executionId = this.executionId(input.executionId);
|
||||
const outputIndex = input.outputIndex === undefined ? undefined : input.outputIndex;
|
||||
if (outputIndex !== undefined && (!Number.isSafeInteger(outputIndex) || (outputIndex as number) < 0 || (outputIndex as number) > 99)) {
|
||||
return failure('plugin_input_invalid', 'Game-resource output index is invalid', 422, false);
|
||||
}
|
||||
const target = relativeProjectPath(context.projectPath, input.relativePath);
|
||||
const content = await this.options.client.download(executionId, outputIndex as number | undefined);
|
||||
await mkdir(path.dirname(target.absolute), { recursive: true });
|
||||
try {
|
||||
await writeFile(target.absolute, content.bytes, { flag: 'wx' });
|
||||
} catch (error) {
|
||||
if (isRecord(error) && error.code === 'EEXIST') {
|
||||
return failure('game_resource_destination_exists', 'Destination file already exists', 409, false);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return success({ savedPath: target.relative, bytes: content.bytes.byteLength });
|
||||
}
|
||||
case 'game_asset_browser': {
|
||||
const result = await this.gameAssets.browse(context.projectPath, input, context.requestId);
|
||||
const parsed = JSON.parse(result.content[0]?.text ?? '{}') as unknown;
|
||||
return success(parsed);
|
||||
}
|
||||
case 'game_asset_review': {
|
||||
const result = await this.gameAssets.review(context.projectPath, input, context.requestId);
|
||||
return success(result.details);
|
||||
}
|
||||
default:
|
||||
return failure('plugin_contract_unsupported', 'Game-resource operation is unavailable', 503, true);
|
||||
}
|
||||
} catch (error) {
|
||||
return clientFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
private executionId(value: unknown): string {
|
||||
if (typeof value !== 'string' || !EXECUTION_ID.test(value)) {
|
||||
throw new GameResourceClientError('plugin_input_invalid', 422, false, 'Execution ID is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private async admission(context: TrustedCodingCapabilityContext): Promise<{
|
||||
releaseId: string;
|
||||
releaseAdmissionId: string;
|
||||
}> {
|
||||
const installed = context.pluginReleaseId
|
||||
? await this.options.packageStore.getInstalledRelease(PLUGIN_ID, context.pluginReleaseId)
|
||||
: await this.options.packageStore.getInstalled(PLUGIN_ID);
|
||||
if (!installed || !installed.channel) {
|
||||
throw new GameResourceClientError('plugin_release_unavailable', 409, false, 'Installed game-resource Release is unavailable');
|
||||
}
|
||||
const resolved = await this.options.marketplace.resolve({
|
||||
resolveRequestId: context.requestId,
|
||||
makeloreVersion: this.options.makeloreVersion,
|
||||
channel: installed.channel,
|
||||
installed: [{
|
||||
pluginId: installed.pluginId,
|
||||
releaseId: installed.releaseId,
|
||||
sha256: installed.sha256,
|
||||
}],
|
||||
});
|
||||
const item = resolved.items.find(({ pluginId }) => pluginId === PLUGIN_ID);
|
||||
if (!item?.releaseId || !item.releaseAdmissionId || item.releaseId !== installed.releaseId
|
||||
|| (item.action !== 'keep' && item.action !== 'install')) {
|
||||
throw new GameResourceClientError('plugin_runtime_stale', 409, false, 'Game-resource worker Release is stale');
|
||||
}
|
||||
return { releaseId: item.releaseId, releaseAdmissionId: item.releaseAdmissionId };
|
||||
}
|
||||
}
|
||||
|
||||
export function createGameResourcePluginAdapter(
|
||||
options: GameResourcePluginAdapterOptions,
|
||||
): GameResourcePluginAdapter {
|
||||
return new GameResourcePluginAdapter(options);
|
||||
}
|
||||
@@ -31,6 +31,8 @@ export interface SkillEntry {
|
||||
/** A policy row copied from the last verified server catalog. */
|
||||
export interface RuntimePolicy {
|
||||
readonly pluginId: string;
|
||||
readonly pluginVersion: string;
|
||||
readonly releaseId: string | null;
|
||||
readonly contractVersion: number;
|
||||
readonly capabilityId: string;
|
||||
readonly operation: string;
|
||||
@@ -90,6 +92,7 @@ export interface EffectivePluginResolverOptions {
|
||||
readonly packageStore?: {
|
||||
readInstalledIndex(): Promise<readonly { pluginId: string }[]>;
|
||||
getInstalled(pluginId: string): Promise<InstalledRelease | null>;
|
||||
getInstalledRelease(pluginId: string, releaseId: string): Promise<InstalledRelease | null>;
|
||||
};
|
||||
readonly getLibrary?: (binding: AccountBinding) => Promise<MarketplaceLibrarySnapshot | null> | MarketplaceLibrarySnapshot | null;
|
||||
readonly marketplace?: {
|
||||
@@ -393,6 +396,8 @@ export class EffectivePluginResolver {
|
||||
if (!policy) continue;
|
||||
runtimePolicies.push({
|
||||
pluginId: definition.id,
|
||||
pluginVersion: definition.version,
|
||||
releaseId: definition.releaseId,
|
||||
contractVersion: definition.contractVersion,
|
||||
capabilityId: operation.capabilityId,
|
||||
operation: operation.operation,
|
||||
@@ -455,6 +460,18 @@ export class EffectivePluginResolver {
|
||||
return this.options.policyClient?.getState() ?? EMPTY_POLICY_STATE;
|
||||
}
|
||||
|
||||
async getInstalledDefinition(
|
||||
pluginId: string,
|
||||
releaseId?: string | null,
|
||||
): Promise<CodingPluginDefinition | 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);
|
||||
return record?.installed && !record.unavailableReason ? record.definition : null;
|
||||
}
|
||||
|
||||
private async definitionRecords(): Promise<DefinitionRecord[]> {
|
||||
const base = [
|
||||
...(this.options.definitions ?? []),
|
||||
|
||||
@@ -772,7 +772,9 @@ function parseV2Tool(
|
||||
fail(filePath, `tools[${index}].mutation`, 'unknown tool mutation');
|
||||
}
|
||||
const projectWriteLease = bool(tool.projectWriteLease, filePath, `tools[${index}].projectWriteLease`);
|
||||
if (projectWriteLease) fail(filePath, `tools[${index}].projectWriteLease`, 'distributed tools cannot request a project write lease');
|
||||
if (projectWriteLease && tool.mutation === 'read') {
|
||||
fail(filePath, `tools[${index}].projectWriteLease`, 'read-only tools cannot request a project write lease');
|
||||
}
|
||||
const permissions = uniqueStrings(
|
||||
tool.permissions,
|
||||
filePath,
|
||||
@@ -804,7 +806,7 @@ function parseV2Tool(
|
||||
operation,
|
||||
roles: ['parent'],
|
||||
mutation: mutation as PluginToolMutation,
|
||||
projectWriteLease: false,
|
||||
projectWriteLease,
|
||||
permissions,
|
||||
executionMode: tool.executionMode as CodingPluginExecutionMode,
|
||||
inputSchema: freezeDeep(structuredClone(inputSchema)),
|
||||
|
||||
@@ -320,7 +320,7 @@ function parseIndexDocument(value: unknown): IndexDocument {
|
||||
if (seen.has(key)) fail('plugin_store_index_invalid', `duplicate release ${releaseId}`);
|
||||
seen.add(key);
|
||||
const runtimeKind = record.runtime_kind;
|
||||
if (runtimeKind !== 'skill_only') {
|
||||
if (runtimeKind !== 'skill_only' && runtimeKind !== 'platform_hosted') {
|
||||
fail('plugin_store_index_invalid', `invalid release ${index}.runtime_kind`);
|
||||
}
|
||||
const installedAt = boundedText(record.installed_at, `release ${index}.installed_at`, 80);
|
||||
@@ -706,6 +706,16 @@ export class PluginPackageStore {
|
||||
return this.getInstalledFromIndex(index, validated, undefined, current);
|
||||
}
|
||||
|
||||
async getInstalledRelease(pluginId: string, releaseId: string): Promise<InstalledRelease | null> {
|
||||
const validatedPluginId = validPluginId(pluginId);
|
||||
const validatedReleaseId = validReleaseId(releaseId);
|
||||
return this.getInstalledFromIndex(
|
||||
await this.readIndex(),
|
||||
validatedPluginId,
|
||||
validatedReleaseId,
|
||||
);
|
||||
}
|
||||
|
||||
async removeUnused(pluginId: string): Promise<InstallationSnapshot> {
|
||||
const validated = validPluginId(pluginId);
|
||||
return this.withOperation(() => this.removeUnusedLocked(validated, undefined, 'background'));
|
||||
@@ -912,7 +922,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, channel);
|
||||
const record = this.installedRecord(grant, channel, definition.runtimeKind);
|
||||
const records = index.releases.filter((candidate) => !(candidate.pluginId === pluginId && candidate.releaseId === grant.releaseId));
|
||||
this.assertBinding(binding);
|
||||
try {
|
||||
@@ -968,7 +978,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, input.channel);
|
||||
const record = this.installedRecord(input.grant, input.channel, definition.runtimeKind);
|
||||
this.assertBinding(input.binding);
|
||||
try {
|
||||
await this.writeIndex(this.indexPath, serializeIndex({
|
||||
@@ -1019,14 +1029,18 @@ export class PluginPackageStore {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private installedRecord(grant: DownloadGrant, channel: 'stable' | 'beta'): InstalledReleaseRecord {
|
||||
private installedRecord(
|
||||
grant: DownloadGrant,
|
||||
channel: 'stable' | 'beta',
|
||||
runtimeKind: 'skill_only' | 'platform_hosted',
|
||||
): InstalledReleaseRecord {
|
||||
return Object.freeze({
|
||||
pluginId: grant.pluginId,
|
||||
releaseId: grant.releaseId,
|
||||
version: grant.version,
|
||||
packageSchemaVersion: grant.packageSchemaVersion,
|
||||
contractVersion: grant.contractVersion,
|
||||
runtimeKind: 'skill_only',
|
||||
runtimeKind,
|
||||
sha256: grant.sha256,
|
||||
sizeBytes: grant.sizeBytes,
|
||||
installedAt: new Date(this.now()).toISOString(),
|
||||
@@ -1193,7 +1207,6 @@ export class PluginPackageStore {
|
||||
let definition: CodingPluginDefinition;
|
||||
try {
|
||||
definition = await loadCodingPluginDefinition(packageRoot, {
|
||||
runtimeKind: 'skill_only',
|
||||
acquisitionMode: 'user_acquired',
|
||||
releaseId: grant.releaseId,
|
||||
provenance: { source: 'marketplace', packageRoot },
|
||||
@@ -1204,7 +1217,8 @@ export class PluginPackageStore {
|
||||
}
|
||||
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') {
|
||||
|| (definition.runtimeKind !== 'skill_only' && definition.runtimeKind !== 'platform_hosted')
|
||||
|| definition.acquisitionMode !== 'user_acquired') {
|
||||
fail('plugin_manifest_invalid', 'package definition does not match the signed Release');
|
||||
}
|
||||
return definition;
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface TrustedCodingCapabilityContext {
|
||||
durableProjectId: string;
|
||||
workerRole: 'parent' | 'child';
|
||||
effectiveSkillIds: readonly string[];
|
||||
pluginReleaseId?: string;
|
||||
}
|
||||
|
||||
export type PluginBackendProjection =
|
||||
@@ -527,19 +528,50 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
|
||||
effectiveSkillIds: readonly string[];
|
||||
value: unknown;
|
||||
}): Promise<PiProductToolResult> {
|
||||
const indexed = this.toolsByName.get(input.toolName);
|
||||
let indexed = this.toolsByName.get(input.toolName);
|
||||
const validId = requestId(input.context) !== 'invalid-request-id';
|
||||
if (!indexed && this.options.effectiveResolver && input.context.effectiveSnapshot) {
|
||||
const snapshotTool = input.context.effectiveSnapshot.toolDefinitions
|
||||
.find(({ name }) => name === input.toolName);
|
||||
const policy = snapshotTool
|
||||
? input.context.effectiveSnapshot.runtimePolicies.find((candidate) => (
|
||||
candidate.capabilityId === snapshotTool.capabilityId
|
||||
&& candidate.operation === snapshotTool.operation
|
||||
))
|
||||
: undefined;
|
||||
const definition = policy
|
||||
? await this.options.effectiveResolver.getInstalledDefinition(policy.pluginId, policy.releaseId)
|
||||
: null;
|
||||
const tool = definition?.tools.find((candidate) => (
|
||||
candidate.name === input.toolName
|
||||
&& candidate.capabilityId === snapshotTool?.capabilityId
|
||||
&& candidate.operation === snapshotTool.operation
|
||||
));
|
||||
if (definition && tool) indexed = { definition, tool };
|
||||
}
|
||||
if (!indexed) return this.unknownResult(input.context, 'plugin_backend_unavailable', 'Plugin capability is unavailable');
|
||||
const { definition, tool } = indexed;
|
||||
if (this.options.effectiveResolver && input.context.effectiveSnapshot) {
|
||||
const frozenPolicy = input.context.effectiveSnapshot.runtimePolicies.find((candidate) => (
|
||||
candidate.pluginId === definition.id
|
||||
&& candidate.capabilityId === tool.capabilityId
|
||||
&& candidate.operation === tool.operation
|
||||
));
|
||||
const currentSnapshot = await this.options.effectiveResolver.resolve({
|
||||
projectId: input.context.projectId,
|
||||
projectPath: input.context.projectPath,
|
||||
assignedSkillIds: input.context.effectiveSnapshot.effectiveSkillIds,
|
||||
role: input.workerRole,
|
||||
});
|
||||
const currentPolicy = currentSnapshot.runtimePolicies.find((candidate) => (
|
||||
candidate.pluginId === definition.id
|
||||
&& candidate.capabilityId === tool.capabilityId
|
||||
&& candidate.operation === tool.operation
|
||||
));
|
||||
if (currentSnapshot.accountSessionId !== input.context.effectiveSnapshot.accountSessionId
|
||||
|| !currentSnapshot.toolDefinitions.some(({ name }) => name === input.toolName)) {
|
||||
|| !currentSnapshot.toolDefinitions.some(({ name }) => name === input.toolName)
|
||||
|| frozenPolicy?.releaseId !== currentPolicy?.releaseId
|
||||
|| frozenPolicy?.pluginVersion !== currentPolicy?.pluginVersion) {
|
||||
const disabled = currentSnapshot.unavailableReasons.some((reason) => (
|
||||
reason.pluginId === definition.id && reason.code === 'project_disabled'
|
||||
));
|
||||
@@ -599,6 +631,7 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
|
||||
durableProjectId,
|
||||
workerRole: input.workerRole,
|
||||
effectiveSkillIds: [...input.effectiveSkillIds],
|
||||
...(definition.releaseId ? { pluginReleaseId: definition.releaseId } : {}),
|
||||
};
|
||||
let result: AdapterInvocationResult;
|
||||
try {
|
||||
@@ -640,7 +673,7 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
|
||||
code,
|
||||
error,
|
||||
retryable,
|
||||
payload_schema: 'data-service.v1',
|
||||
payload_schema: 'unknown',
|
||||
data: null,
|
||||
}, billing);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user