diff --git a/.project-docs/30-worklog/tasks/20260901-official-bundled-plugins-client-9d5f3b82.md b/.project-docs/30-worklog/tasks/20260901-official-bundled-plugins-client-9d5f3b82.md new file mode 100644 index 0000000..ff62937 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260901-official-bundled-plugins-client-9d5f3b82.md @@ -0,0 +1,51 @@ +# Task: Use official bundled Plugins without Marketplace Release download + +## Identity + +- Task ID: 20260901-official-bundled-plugins-client-9d5f3b82 +- Mode: Feature +- Branch: codex/20260901-official-bundled-plugins-client-9d5f3b82-official-bundled-plugins +- Worktree: D:\Datas\OthersProjects\makelore-official-bundled-plugins-9d5f3b82 +- Base commit: 52e2a97a12ffaf838c3218795ff33e7fb91940cf +- Owner: codex-root +- Status: In Progress + +## Scope + +- Ship the code-owned Game Resource and Web Search package resources inside MakeLore. +- Materialize acquired bundled optional Plugins without a Package Store download while retaining policy/admission checks. +- Show bundled delivery honestly in Marketplace/My Plugins/Project Plugins and hide only device-package actions. +- Cover manifest, effective resolution, hosted admission, Renderer projection, and artifact reachability with focused tests. + +## Intent And Constraints + +- Preserve free account acquisition/removal, project enablement, Agent assignment, lifecycle invalidation, and Token Point billing. +- Preserve signed artifact download/update behavior for non-bundled Marketplace Plugins. +- Do not add Provider authority or secrets to the client. +- Keep unknown/removed Plugin configuration preservation unchanged. + +## Outcome + +- Game Resource and Web Search are now fixed schema-2 code-owned bundled definitions with deterministic server Release IDs. Their exact package manifests, capability contracts, and Skills ship under `resources/coding-plugins/**` and match the server source bytes. +- Effective resolution still requires the current Account Library entry, project enablement, Agent assignment, policy, and billing readiness, but no longer consults Package Store bytes for these official definitions. Registry definition lookup and hosted Admission resolve the exact bundled Release and reject any server drift. +- My Plugins shows “随 MakeLore 提供” and removes download/update/Beta/device-delete actions for official bundles while retaining Account removal, project enablement, and Agent assignment. Project Plugins no longer reports them as missing a device download. +- Third-party Marketplace package parsing, signature trust, Package Store installation, and lifecycle remain unchanged. + +## Verification + +- Initial RED: five official-bundle assertions failed while 33 adjacent tests passed, proving only Data Service was loaded and hosted Plugins still depended on Package Store. +- Registry-boundary RED: bundled `getInstalledDefinition(releaseId)` returned null; GREEN now returns the fixed bundled definition without `getInstalledRelease`. +- Focused manifest/effective resolver/Admission/resource-loader/UI suite: 57 passed across 6 files; broader adjacent suite: 133 passed. +- Full unit diagnostic before the final registry correction: 214 files ran with 1,781 passed and 2 skipped; two Windows process-spawn tests failed with transient `spawn EBUSY`, then passed 3/3 alone with one worker. Pressure test passed 1/1. +- TypeScript typecheck and scoped ESLint pass. `pnpm run build:vite` passes all Renderer/Main/Preload/utility builds (2,272 Renderer modules). +- Server/client Game Resource and Web Search resource trees are byte-identical under `git diff --no-index`. +- Windows package/artifact verification is pending the final source commit so embedded commit evidence names the exact deliverable. + +## Follow-ups + +- Integrate the sole source commit into the client `main` root without touching its unrelated untracked task record, then build and verify the Windows package. +- The matching Works Square deployment must reach migration `0078`; Provider/pricing readiness remains server-owned. + +## Promotion Candidates + +- None recorded. diff --git a/electron/api/coding-composition.ts b/electron/api/coding-composition.ts index 8d55ac3..7f064d7 100644 --- a/electron/api/coding-composition.ts +++ b/electron/api/coding-composition.ts @@ -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)); diff --git a/electron/coding-plugins/adapters/game-resource.ts b/electron/coding-plugins/adapters/game-resource.ts index fcc8f17..4cbfb37 100644 --- a/electron/coding-plugins/adapters/game-resource.ts +++ b/electron/coding-plugins/adapters/game-resource.ts @@ -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; readonly makeloreVersion: string; + readonly bundledReleases?: Readonly>; 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 { + 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' }; } diff --git a/electron/coding-plugins/adapters/web-search.ts b/electron/coding-plugins/adapters/web-search.ts index 6bae1dc..511c0ce 100644 --- a/electron/coding-plugins/adapters/web-search.ts +++ b/electron/coding-plugins/adapters/web-search.ts @@ -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; readonly makeloreVersion?: string; + readonly bundledReleases?: Readonly>; 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 { + 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' }; } diff --git a/electron/coding-plugins/effective-resolver.ts b/electron/coding-plugins/effective-resolver.ts index 571a177..6c25f9b 100644 --- a/electron/coding-plugins/effective-resolver.ts +++ b/electron/coding-plugins/effective-resolver.ts @@ -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 { + 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)) { diff --git a/electron/coding-plugins/hosted-admission.ts b/electron/coding-plugins/hosted-admission.ts index 83c2845..f3ae3aa 100644 --- a/electron/coding-plugins/hosted-admission.ts +++ b/electron/coding-plugins/hosted-admission.ts @@ -45,6 +45,13 @@ export interface MarketplaceHostedAdmissionResolverOptions { readonly packageStore: Pick; readonly marketplace: Pick; readonly makeloreVersion: string; + readonly bundledReleases?: Readonly>; +} + +export interface BundledHostedRelease { + readonly releaseId: string; + readonly version: string; + readonly channel: 'stable'; } function isRecord(value: unknown): value is Record { @@ -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>; - 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> = 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({ diff --git a/electron/coding-plugins/manifest.ts b/electron/coding-plugins/manifest.ts index 34faef1..e51b023 100644 --- a/electron/coding-plugins/manifest.ts +++ b/electron/coding-plugins/manifest.ts @@ -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; @@ -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', diff --git a/resources/coding-plugins/game-resource/com.makelore/capability.json b/resources/coding-plugins/game-resource/com.makelore/capability.json new file mode 100644 index 0000000..c438a90 --- /dev/null +++ b/resources/coding-plugins/game-resource/com.makelore/capability.json @@ -0,0 +1,288 @@ +{ + "schemaVersion": 2, + "pluginId": "makelore.game-resource", + "contractVersion": 1, + "scope": "project", + "runtime": { + "kind": "platform_hosted", + "protocol": "makelore-hosted.v1" + }, + "skills": [ + { + "id": "game-resource", + "entry": "../skills/game-resource/SKILL.md", + "grants": [ + "game-resource.generate", + "game-resource.library" + ] + } + ], + "tools": [ + { + "name": "game_resource_templates", + "label": "List game resource templates", + "description": "List the currently available hosted templates for pixel-art or HD generation.", + "capabilityId": "game-resource.generate", + "operation": "templates", + "roles": ["parent"], + "mutation": "read", + "projectWriteLease": false, + "permissions": ["hosted.game-resource.templates"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["kind"], + "properties": { + "kind": {"type": "string", "minLength": 2, "maxLength": 5} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "templates"], + "properties": { + "kind": {"type": "string", "minLength": 2, "maxLength": 5}, + "templates": { + "type": "array", + "maxItems": 100, + "items": {"type": "string", "minLength": 1, "maxLength": 200} + } + } + } + }, + { + "name": "game_resource_generate", + "label": "Generate game resource", + "description": "Submit one hosted pixel-art or HD game-resource generation job.", + "capabilityId": "game-resource.generate", + "operation": "generate", + "roles": ["parent"], + "mutation": "write", + "projectWriteLease": false, + "permissions": ["hosted.game-resource.generate"], + "executionMode": "job", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "templateName", "requirement", "confirmed"], + "properties": { + "kind": {"type": "string", "minLength": 2, "maxLength": 5}, + "templateName": {"type": "string", "minLength": 1, "maxLength": 200}, + "templateConfigJson": {"type": "string", "maxLength": 16384}, + "requirement": {"type": "string", "minLength": 1, "maxLength": 12000}, + "aspectRatio": {"type": "string", "minLength": 1, "maxLength": 40}, + "temperature": {"type": "number", "minimum": 0, "maximum": 2}, + "jobName": {"type": "string", "minLength": 1, "maxLength": 200}, + "modelName": {"type": "string", "minLength": 1, "maxLength": 200}, + "resolution": {"type": "string", "minLength": 1, "maxLength": 40}, + "hdRemoveBgMode": {"type": "string", "minLength": 1, "maxLength": 80}, + "threadId": {"type": "string", "minLength": 1, "maxLength": 200}, + "confirmed": {"type": "boolean"}, + "referencePaths": { + "type": "array", + "maxItems": 8, + "items": {"type": "string", "minLength": 1, "maxLength": 1024} + } + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["executionId", "status", "outputCount"], + "properties": { + "executionId": {"type": "string", "minLength": 1, "maxLength": 36}, + "status": {"type": "string", "maxLength": 32}, + "outputCount": {"type": "integer", "minimum": 0, "maximum": 100}, + "pollIntervalSeconds": {"type": "integer", "minimum": 1, "maximum": 300}, + "errorCode": {"type": "string", "minLength": 1, "maxLength": 128} + } + } + }, + { + "name": "game_resource_status", + "label": "Game resource status", + "description": "Read and refresh one hosted game-resource generation job.", + "capabilityId": "game-resource.generate", + "operation": "status", + "roles": ["parent"], + "mutation": "read", + "projectWriteLease": false, + "permissions": ["hosted.game-resource.status"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["executionId"], + "properties": { + "executionId": {"type": "string", "minLength": 1, "maxLength": 36} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["executionId", "status", "outputCount", "generationBilling"], + "properties": { + "executionId": {"type": "string", "minLength": 1, "maxLength": 36}, + "status": {"type": "string", "minLength": 1, "maxLength": 32}, + "outputCount": {"type": "integer", "minimum": 0, "maximum": 100}, + "pollIntervalSeconds": {"type": "integer", "minimum": 1, "maximum": 300}, + "errorCode": {"type": "string", "minLength": 1, "maxLength": 128}, + "generationBilling": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "status", "reserved_points", "usage_amount", "unit"], + "properties": { + "mode": {"type": "string", "minLength": 1, "maxLength": 32}, + "status": {"type": "string", "maxLength": 32}, + "reserved_points": {"type": "string", "maxLength": 32}, + "actual_points": {"type": "string", "maxLength": 32}, + "usage_amount": {"type": "integer", "minimum": 0, "maximum": 1}, + "unit": {"type": "string", "minLength": 1, "maxLength": 32} + } + } + } + } + }, + { + "name": "game_resource_save_output", + "label": "Save generated game resource", + "description": "Save one completed hosted output into a new file inside the current project.", + "capabilityId": "game-resource.library", + "operation": "save_output", + "roles": ["parent"], + "mutation": "write", + "projectWriteLease": true, + "permissions": ["hosted.game-resource.save-output"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["executionId", "relativePath", "confirmed"], + "properties": { + "executionId": {"type": "string", "minLength": 1, "maxLength": 36}, + "outputIndex": {"type": "integer", "minimum": 0, "maximum": 99}, + "relativePath": {"type": "string", "minLength": 1, "maxLength": 1024}, + "confirmed": {"type": "boolean"} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["savedPath", "bytes"], + "properties": { + "savedPath": {"type": "string", "minLength": 1, "maxLength": 1024}, + "bytes": {"type": "integer", "minimum": 1, "maximum": 33554432} + } + } + }, + { + "name": "game_resource_cancel", + "label": "Cancel game resource", + "description": "Cancel one non-terminal hosted generation job.", + "capabilityId": "game-resource.generate", + "operation": "cancel", + "roles": ["parent"], + "mutation": "write", + "projectWriteLease": false, + "permissions": ["hosted.game-resource.cancel"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["executionId"], + "properties": { + "executionId": {"type": "string", "minLength": 1, "maxLength": 36} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["executionId", "status", "outputCount", "generationBilling"], + "properties": { + "executionId": {"type": "string", "minLength": 1, "maxLength": 36}, + "status": {"type": "string", "minLength": 1, "maxLength": 32}, + "outputCount": {"type": "integer", "minimum": 0, "maximum": 100}, + "pollIntervalSeconds": {"type": "integer", "minimum": 1, "maximum": 300}, + "errorCode": {"type": "string", "minLength": 1, "maxLength": 128}, + "generationBilling": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "status", "reserved_points", "usage_amount", "unit"], + "properties": { + "mode": {"type": "string", "minLength": 1, "maxLength": 32}, + "status": {"type": "string", "maxLength": 32}, + "reserved_points": {"type": "string", "maxLength": 32}, + "actual_points": {"type": "string", "maxLength": 32}, + "usage_amount": {"type": "integer", "minimum": 0, "maximum": 1}, + "unit": {"type": "string", "minLength": 1, "maxLength": 32} + } + } + } + } + }, + { + "name": "game_asset_browser", + "label": "Browse game assets", + "description": "Open the project game-asset browser for user selection.", + "capabilityId": "game-resource.library", + "operation": "browse", + "roles": ["parent"], + "mutation": "read", + "projectWriteLease": false, + "permissions": ["hosted.game-resource.browse"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": [], + "properties": { + "invocationId": {"type": "string", "minLength": 1, "maxLength": 200} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["invocationId", "status"], + "properties": { + "invocationId": {"type": "string", "minLength": 1, "maxLength": 200}, + "status": {"type": "string", "minLength": 1, "maxLength": 32} + } + } + }, + { + "name": "game_asset_review", + "label": "Review game asset", + "description": "Open a project game asset for user review and decision.", + "capabilityId": "game-resource.library", + "operation": "review", + "roles": ["parent"], + "mutation": "read", + "projectWriteLease": false, + "permissions": ["hosted.game-resource.review"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["candidateIds"], + "properties": { + "candidateIds": { + "type": "array", + "maxItems": 200, + "items": {"type": "string", "minLength": 1, "maxLength": 200} + }, + "invocationId": {"type": "string", "minLength": 1, "maxLength": 200} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["invocationId", "status"], + "properties": { + "invocationId": {"type": "string", "minLength": 1, "maxLength": 200}, + "status": {"type": "string", "minLength": 1, "maxLength": 32} + } + } + } + ] +} diff --git a/resources/coding-plugins/game-resource/plugin.json b/resources/coding-plugins/game-resource/plugin.json new file mode 100644 index 0000000..14907cd --- /dev/null +++ b/resources/coding-plugins/game-resource/plugin.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "makelore.game-resource", + "version": "1.0.0", + "description": "Generate and review game resources through MakeLore's hosted provider", + "author": { + "name": "MakeLore" + }, + "extensions": { + "com.makelore": { + "capabilityManifest": "./com.makelore/capability.json" + } + } +} diff --git a/resources/coding-plugins/game-resource/skills/game-resource/SKILL.md b/resources/coding-plugins/game-resource/skills/game-resource/SKILL.md new file mode 100644 index 0000000..767ed63 --- /dev/null +++ b/resources/coding-plugins/game-resource/skills/game-resource/SKILL.md @@ -0,0 +1,35 @@ +--- +name: game-resource +description: 当用户明确要求为当前 MakeLore 项目生成、查找或审查游戏资源时使用;生成会消耗 Token Point,提交前必须说明并取得用户确认。 +--- + +# MakeLore 游戏资源 + +此 Skill 只使用 `makelore.game-resource` 提供的工具。不要请求、保存或展示 +Meowa 凭据、Provider URL、Provider job ID,也不要直接访问第三方接口。 + +## 生成流程 + +1. 先确认资源用途、像素或高清类型、画面要求、比例和实际需要的参考图;用 + `game_resource_templates` 获取当前可用模板,不要猜模板名。 +2. 明确告诉用户生成按 Token Point 用量计费,并等待用户确认本次具体生成请求。 +3. 确认后只调用一次 `game_resource_generate`。结果不确定、超时或 + `pending_review` 时不得换 logical operation 重试;报告状态并使用原 + `executionId` 查询。 +4. 用 `game_resource_status` 查询同一个 execution,直到 succeeded、failed、 + cancelled 或 pending_review。轮询间隔遵守返回的 `pollIntervalSeconds`。 +5. 只有用户明确要求取消时才调用 `game_resource_cancel`。 +6. succeeded 后,只有用户明确给出项目内目标路径并确认保存时,才调用 + `game_resource_save_output`。该工具只创建新文件,不覆盖现有文件。 + +## 浏览与审查 + +`game_asset_browser` 与 `game_asset_review` 只处理当前项目的本地候选资源和用户 +确认,不会生成资源,也不会产生新的 Token Point 交易。不得把本地路径当作 +Provider 地址或通过生成工具上传未获用户同意的文件。 + +## 完成标准 + +报告必须区分提交、Provider 状态和 Token Point receipt。只引用工具实际返回的 +execution、status、outputCount 与 billing;不要猜测余额、价格、Provider 成本或 +输出路径。没有 settled receipt 时不得声称计费已最终完成。 diff --git a/resources/coding-plugins/web-search/com.makelore/capability.json b/resources/coding-plugins/web-search/com.makelore/capability.json new file mode 100644 index 0000000..ea629c7 --- /dev/null +++ b/resources/coding-plugins/web-search/com.makelore/capability.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": 2, + "pluginId": "makelore.web-search", + "contractVersion": 1, + "scope": "project", + "runtime": { + "kind": "platform_hosted", + "protocol": "makelore-hosted.v1" + }, + "skills": [ + { + "id": "makelore-web-search", + "entry": "../skills/makelore-web-search/SKILL.md", + "grants": ["web-search.search"] + } + ], + "tools": [ + { + "name": "makelore_web_search", + "label": "Search the web", + "description": "Search the current web through MakeLore and return a grounded answer with sources.", + "capabilityId": "web-search.search", + "operation": "search", + "roles": ["parent"], + "mutation": "read", + "projectWriteLease": false, + "permissions": ["hosted.web-search.search"], + "executionMode": "synchronous", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["query", "confirmed"], + "properties": { + "query": {"type": "string", "minLength": 1, "maxLength": 2000}, + "confirmed": {"type": "boolean"} + } + }, + "outputSchema": { + "type": "object", + "additionalProperties": false, + "required": ["answer", "sources", "searchQueries"], + "properties": { + "answer": {"type": "string", "minLength": 1, "maxLength": 16000}, + "sources": { + "type": "array", + "maxItems": 20, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["title", "url"], + "properties": { + "title": {"type": "string", "minLength": 1, "maxLength": 500}, + "url": {"type": "string", "minLength": 1, "maxLength": 2048} + } + } + }, + "searchQueries": { + "type": "array", + "maxItems": 8, + "items": {"type": "string", "minLength": 1, "maxLength": 500} + } + } + } + } + ] +} diff --git a/resources/coding-plugins/web-search/plugin.json b/resources/coding-plugins/web-search/plugin.json new file mode 100644 index 0000000..35095c7 --- /dev/null +++ b/resources/coding-plugins/web-search/plugin.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "makelore.web-search", + "version": "1.0.0", + "description": "Search the current web through MakeLore and return a grounded answer with sources", + "author": { + "name": "MakeLore" + }, + "extensions": { + "com.makelore": { + "capabilityManifest": "./com.makelore/capability.json" + } + } +} diff --git a/resources/coding-plugins/web-search/skills/makelore-web-search/SKILL.md b/resources/coding-plugins/web-search/skills/makelore-web-search/SKILL.md new file mode 100644 index 0000000..7ed0eb9 --- /dev/null +++ b/resources/coding-plugins/web-search/skills/makelore-web-search/SKILL.md @@ -0,0 +1,26 @@ +--- +name: makelore-web-search +description: 当用户需要当前或外部网页信息且现有上下文不足时使用;搜索会消耗 Token Point,调用前必须说明并取得对本次准确查询的确认。 +--- + +# MakeLore 联网搜索 + +此 Skill 只使用 `makelore.web-search` 提供的 `makelore_web_search` 工具。不要请求、 +保存或展示 Provider 凭据、模型、地址或内部请求标识,也不要直接调用第三方接口。 + +## 搜索流程 + +1. 只有问题需要当前或外部网页信息、而现有上下文不足时才搜索;能从已有上下文回答时不要调用。 +2. 先明确将发送的准确查询,说明成功搜索会消耗 Token Point,并等待用户确认。 +3. 确认后只用同一个 logical operation 调用一次 `makelore_web_search`;不得为了自动重试改写查询或生成新的 logical operation。 +4. 只根据工具实际返回的 `answer`、`sources` 和 `searchQueries` 回答;引用应紧邻其支持的事实,不能虚构或补写来源。 + +## 不确定结果 + +- `submission_unknown`:结果不可用且提交状态未知。说明状态,不得自动重试。 +- `receipt_unavailable`:Main 已收到结果但收据不可用。可以使用实际结果,但必须说明收据暂不可用,且不得自动重试。 +- `pending_review`:可以使用工具返回的实际结果,但必须说明 Token Point 正在人工复核,且不得自动重试。 + +## 完成标准 + +区分搜索结果与 Token Point receipt;没有 settled receipt 时不得声称计费已最终完成。 diff --git a/scripts/lib/pi-product-artifact.mjs b/scripts/lib/pi-product-artifact.mjs index d180b71..b596436 100644 --- a/scripts/lib/pi-product-artifact.mjs +++ b/scripts/lib/pi-product-artifact.mjs @@ -621,8 +621,12 @@ export async function verifyBundledCodingPluginResources({ } const sdkAssetPaths = packageFiles.filter((file) => SDK_ASSET_PATH_PATTERN.test(file)); - if (sdkAssetPaths.length === 0) throw new Error(`Packaged coding plugin has no SDK assets: ${packageName}`); - const adapterId = packagedCapability.adapterId; + if (packagedCapability.schemaVersion === 1 && sdkAssetPaths.length === 0) { + throw new Error(`Packaged schema-1 coding plugin has no SDK assets: ${packageName}`); + } + const adapterId = packagedCapability.schemaVersion === 2 + ? packagedCapability.pluginId + : packagedCapability.adapterId; const tools = Array.isArray(packagedCapability.tools) ? packagedCapability.tools : []; const toolNames = tools.map((tool) => tool?.name); if (typeof adapterId !== 'string' || adapterId.length === 0 || tools.length === 0 diff --git a/shared/coding-plugins.ts b/shared/coding-plugins.ts index 73b5a1b..1a20d91 100644 --- a/shared/coding-plugins.ts +++ b/shared/coding-plugins.ts @@ -101,6 +101,28 @@ export const DATA_SERVICE_ADAPTER_ID = 'data-service' as const; export const DATA_SERVICE_PROJECT_SETTINGS_SURFACE = 'data-service' as const; export const DATA_SERVICE_PREVIEW_RUNTIME_SURFACE = 'data-service-v1' as const; +export const GAME_RESOURCE_PLUGIN_ID = 'makelore.game-resource' as const; +export const GAME_RESOURCE_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000105' as const; +export const WEB_SEARCH_PLUGIN_ID = 'makelore.web-search' as const; +export const WEB_SEARCH_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000204' as const; + +export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({ + [GAME_RESOURCE_PLUGIN_ID]: Object.freeze({ + releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID, + version: '1.0.0', + channel: 'stable' as const, + }), + [WEB_SEARCH_PLUGIN_ID]: Object.freeze({ + releaseId: WEB_SEARCH_BUNDLED_RELEASE_ID, + version: '1.0.0', + channel: 'stable' as const, + }), +}); + +export function isCodeOwnedOptionalBundledPluginId(pluginId: string): boolean { + return Object.prototype.hasOwnProperty.call(CODE_OWNED_OPTIONAL_BUNDLED_RELEASES, pluginId); +} + export const DATA_SERVICE_CAPABILITY_IDS = Object.freeze([ 'data-service.control', 'data-service.documents', diff --git a/src/pages/MyPlugins/index.tsx b/src/pages/MyPlugins/index.tsx index 34f35af..f9093a6 100644 --- a/src/pages/MyPlugins/index.tsx +++ b/src/pages/MyPlugins/index.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'; import type { MarketplaceInstallation, MarketplaceLibrarySnapshot } from '@/lib/plugin-marketplace'; import { pluginMarketplaceStore, usePluginMarketplaceStore } from '@/stores/plugin-marketplace'; import { useAuthStore } from '@/stores/auth'; +import { isCodeOwnedOptionalBundledPluginId } from '../../../shared/coding-plugins'; function failureText(reason?: string): string | null { if (!reason) return null; @@ -55,7 +56,7 @@ export function MyPluginsView(props: MyPluginsViewProps) { const visibleError = actionErrorText(props.error); return (
-

我的插件

账号插件库与本机下载分开管理。下载不会自动启用到项目,也不会分配给伙伴。

+

我的插件

官方插件随 MakeLore 提供;其他插件的账号获取与本机下载分开管理。任何插件都不会自动启用到项目或分配给伙伴。

{props.library?.stale ?

正在显示上次可信的插件库;服务恢复前,在线计量或托管能力保持不可用。

: null} {props.state === 'loading' && !props.library ?

正在读取我的插件…

: null} {props.state === 'error' && !props.library ?

无法读取我的插件

{props.error ?? '请稍后重试。'}

: null} @@ -65,7 +66,8 @@ export function MyPluginsView(props: MyPluginsViewProps) { {props.library?.items.map((plugin) => { const installed = props.installations[plugin.pluginId]; const systemIncluded = plugin.acquisition === 'system_included'; - const hasDevicePackage = !systemIncluded && Boolean(installed?.version); + const bundledDelivery = systemIncluded || isCodeOwnedOptionalBundledPluginId(plugin.pluginId); + const hasDevicePackage = !bundledDelivery && Boolean(installed?.version); const removed = plugin.removedAt !== null; const suspended = plugin.runtimeStatus === 'suspended'; const retired = plugin.catalogStatus === 'retired'; @@ -74,17 +76,18 @@ export function MyPluginsView(props: MyPluginsViewProps) { const updateAvailable = Boolean(hasDevicePackage && channelVersion && installed?.version !== channelVersion); const betaChannelUnavailable = Boolean(hasDevicePackage && installedChannel === 'beta' && !plugin.betaVersion); const busy = Object.keys(props.pending).some((key) => key.endsWith(`:${plugin.pluginId}`)); - const failure = systemIncluded ? null : failureText(installed?.reason); + const failure = bundledDelivery ? null : failureText(installed?.reason); return
-

{plugin.title}

{systemIncluded ? 系统内置 : removed ? 已移除 : 已获取}

{plugin.summary}

稳定版 {plugin.stableVersion ?? '不可用'}

{plugin.betaVersion ?

Beta 版 {plugin.betaVersion}

: null}{systemIncluded ?

随 MakeLore 提供

: hasDevicePackage ? <>

设备版本 {installed?.version}

当前频道:{installedChannel === 'beta' ? 'Beta' : '稳定'}

:

尚未下载到设备

}
+

{plugin.title}

{systemIncluded ? 系统内置 : removed ? 已移除 : 已获取}

{plugin.summary}

稳定版 {plugin.stableVersion ?? '不可用'}

{plugin.betaVersion ?

Beta 版 {plugin.betaVersion}

: null}{bundledDelivery ?

随 MakeLore 提供

: hasDevicePackage ? <>

设备版本 {installed?.version}

当前频道:{installedChannel === 'beta' ? 'Beta' : '稳定'}

:

尚未下载到设备

}
{suspended ?

运行已暂停

: null}{retired ?

{removed ? '已退役,移除后不可重新获取' : '已退役;现有账号插件库仍可继续使用受支持版本'}

: null}{betaChannelUnavailable ?

当前 Beta 频道暂无可用版本;不会静默切回稳定版。

: null}{failure ?

{failure}

: null}
{systemIncluded ? null : removed ? + : bundledDelivery ? null : !hasDevicePackage ? : updateAvailable ? : 设备版本已是最新} - {!removed && plugin.acquisition !== 'system_included' && plugin.betaVersion && installedChannel !== 'beta' ? : null} + {!removed && !bundledDelivery && plugin.betaVersion && installedChannel !== 'beta' ? : null} {!removed && plugin.acquisition !== 'system_included' ? : null} {hasDevicePackage ? : null} diff --git a/src/pages/ProjectPlugins/index.tsx b/src/pages/ProjectPlugins/index.tsx index 5997db4..a0a8dc6 100644 --- a/src/pages/ProjectPlugins/index.tsx +++ b/src/pages/ProjectPlugins/index.tsx @@ -11,6 +11,7 @@ import { cn } from '@/lib/utils'; import { codingPluginsStore, useCodingPluginsStore } from '@/stores/coding-plugins'; import { pluginMarketplaceStore, usePluginMarketplaceStore } from '@/stores/plugin-marketplace'; import { useAuthStore } from '@/stores/auth'; +import { isCodeOwnedOptionalBundledPluginId } from '../../../shared/coding-plugins'; import type { MarketplaceInstallation, MarketplaceLibrarySnapshot } from '@/lib/plugin-marketplace'; import { useCodingWorkspaceStore } from '@/stores/coding-workspace'; import type { DataServiceInstanceState } from '../../../shared/data-service'; @@ -93,7 +94,7 @@ export function ProjectPluginsView(props: ViewProps) { {selected.enabled ? : }
{libraryEntry?.removedAt ?

账号插件库已移除;项目配置仍保留此插件 ID,重新免费获取后可恢复。

: null} - {libraryEntry?.acquisitionMode === 'user_acquired' && !libraryEntry.removedAt && !installation ?

插件已免费获取但尚未下载到本机,当前项目配置会继续保留。

: null} + {libraryEntry?.acquisitionMode === 'user_acquired' && !libraryEntry.removedAt && !installation && !isCodeOwnedOptionalBundledPluginId(selected.id) ?

插件已免费获取但尚未下载到本机,当前项目配置会继续保留。

: null}

发布者

MakeLore

包版本

{selected.version}

diff --git a/tests/unit/coding-plugin-effective-resolver.test.ts b/tests/unit/coding-plugin-effective-resolver.test.ts index 02be35a..43f2933 100644 --- a/tests/unit/coding-plugin-effective-resolver.test.ts +++ b/tests/unit/coding-plugin-effective-resolver.test.ts @@ -395,4 +395,58 @@ describe('effective plugin resolver', () => { })).resolves.toMatchObject({ effectiveSkillIds: ['notes'], toolDefinitions: [], runtimePolicies: [] }); expect(refresh).not.toHaveBeenCalled(); }); + + it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes', async () => { + const bundled: CodingPluginDefinition = { + ...serverDefinition, + id: 'makelore.web-search', + releaseId: '00000000-0000-4000-8000-000000000204', + provenance: { source: 'bundled', packageRoot: 'web-search' }, + skills: [{ + id: 'makelore-web-search', + entryPath: 'skills/makelore-web-search/SKILL.md', + grants: ['remote.read'], + }], + }; + const getInstalled = vi.fn(async () => null); + const getInstalledRelease = vi.fn(async () => null); + const policy = currentPolicy(); + policy.catalog.plugins[0] = { ...policy.catalog.plugins[0], plugin_id: bundled.id }; + const effective = createEffectivePluginResolver({ + definitions: [bundled], + getAccountBinding: () => binding, + getLibrary: vi.fn(async () => ({ + ...library(), + items: [{ ...library().items[0], pluginId: bundled.id }], + })), + packageStore: { + readInstalledIndex: vi.fn(async () => []), + getInstalled, + getInstalledRelease, + }, + getEnabledPluginIds: vi.fn(async () => [bundled.id]), + policyClient: { getState: () => policy, refresh: vi.fn() }, + }); + + await expect(effective.resolve({ + projectId: 'project-a', + projectPath: 'C:/project-a', + assignedSkillIds: ['makelore-web-search'], + role: 'parent', + })).resolves.toMatchObject({ + pluginReleaseIds: [bundled.releaseId], + effectiveSkillIds: ['makelore-web-search'], + skillEntries: [{ + id: 'makelore-web-search', + entryPath: 'skills/makelore-web-search/SKILL.md', + }], + toolDefinitions: [{ name: 'remote_read' }], + unavailableReasons: [], + }); + expect(getInstalled).not.toHaveBeenCalled(); + await expect(effective.getSkillSources()).resolves.toEqual([]); + await expect(effective.getInstalledDefinition(bundled.id, bundled.releaseId)) + .resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId }); + expect(getInstalledRelease).not.toHaveBeenCalled(); + }); }); diff --git a/tests/unit/coding-plugin-manifest.test.ts b/tests/unit/coding-plugin-manifest.test.ts index 9b9d372..c567357 100644 --- a/tests/unit/coding-plugin-manifest.test.ts +++ b/tests/unit/coding-plugin-manifest.test.ts @@ -20,6 +20,8 @@ import { } from '../../shared/coding-plugins'; const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service'); +const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource'); +const WEB_SEARCH_ROOT = path.resolve('resources/coding-plugins/web-search'); async function packageManifests(): Promise<{ root: Record; capability: Record }> { return { @@ -32,9 +34,13 @@ describe('bundled coding plugin manifests', () => { it('loads the fixed Data Service package and immutable declarations', async () => { const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins')); const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins')); - expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service']); - expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([PACKAGE_ROOT]); - expect(definitions).toHaveLength(1); + expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource', 'web-search']); + expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([ + PACKAGE_ROOT, + GAME_RESOURCE_ROOT, + WEB_SEARCH_ROOT, + ]); + expect(definitions).toHaveLength(3); expect(definitions[0]).toMatchObject({ id: 'makelore.data-service', adapterId: 'data-service', @@ -42,7 +48,21 @@ describe('bundled coding plugin manifests', () => { skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }], }); expect(definitions[0]?.tools.map(({ name }) => name)).toEqual(DATA_SERVICE_TOOL_NAMES); - expect(definitions).toEqual([DATA_SERVICE_PLUGIN_DEFINITION]); + expect(definitions[0]).toEqual(DATA_SERVICE_PLUGIN_DEFINITION); + expect(definitions.slice(1)).toMatchObject([ + { + id: 'makelore.game-resource', version: '1.0.0', runtimeKind: 'platform_hosted', + acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000105', + provenance: { source: 'bundled', packageRoot: 'game-resource' }, + skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }], + }, + { + id: 'makelore.web-search', version: '1.0.0', runtimeKind: 'platform_hosted', + acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000204', + provenance: { source: 'bundled', packageRoot: 'web-search' }, + skills: [{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' }], + }, + ]); expect(startupDefinitions).toEqual(definitions); expect(Object.isFrozen(startupDefinitions)).toBe(true); expect(Object.isFrozen(startupDefinitions[0]?.operations)).toBe(true); @@ -115,6 +135,8 @@ describe('bundled coding plugin manifests', () => { it('does not infer package roots from arbitrary directories', async () => { expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([ path.resolve('tmp/data-service'), + path.resolve('tmp/game-resource'), + path.resolve('tmp/web-search'), ]); }); diff --git a/tests/unit/hosted-admission.test.ts b/tests/unit/hosted-admission.test.ts index 6154def..e50967e 100644 --- a/tests/unit/hosted-admission.test.ts +++ b/tests/unit/hosted-admission.test.ts @@ -135,4 +135,30 @@ describe('MarketplaceHostedAdmissionResolver', () => { retryable: false, }); }); + + it('resolves a code-owned bundled Release without consulting the device Package Store', async () => { + const getInstalledRelease = vi.fn(async () => null); + const resolve = vi.fn(async () => resolved({ sha256: null, sizeBytes: null })); + const options = { + packageStore: { getInstalledRelease }, + marketplace: { resolve }, + makeloreVersion: '2.0.0', + bundledReleases: { + [PLUGIN_ID]: { releaseId: RELEASE_ID, version: '1.0.0', channel: 'stable' as const }, + }, + } as unknown as ConstructorParameters[0]; + const bundledResolver = new MarketplaceHostedAdmissionResolver(options); + + await expect(bundledResolver.resolve({ + pluginId: PLUGIN_ID, + workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID }, + })).resolves.toEqual({ releaseId: RELEASE_ID, releaseAdmissionId: 'admission-game-1' }); + expect(getInstalledRelease).not.toHaveBeenCalled(); + expect(resolve).toHaveBeenCalledWith({ + resolveRequestId: REQUEST_ID, + makeloreVersion: '2.0.0', + channel: 'stable', + installed: [], + }); + }); }); diff --git a/tests/unit/pi-product-artifact.test.ts b/tests/unit/pi-product-artifact.test.ts index 621c03e..7e92ea8 100644 --- a/tests/unit/pi-product-artifact.test.ts +++ b/tests/unit/pi-product-artifact.test.ts @@ -74,20 +74,24 @@ function matchingMetadata() { }; } -async function bundledResourceFixture() { +async function bundledResourceFixture({ schemaVersion = 1, includeSdkAssets = true } = {}) { const root = await mkdtemp(path.join(tmpdir(), 'makelore-bundled-plugin-')); roots.push(root); const projectRoot = path.join(root, 'project'); const sourceResources = path.join(projectRoot, 'resources'); const pluginRoot = path.join(sourceResources, 'coding-plugins', 'example'); const capability = { + schemaVersion, pluginId: 'example.plugin', adapterId: 'example-adapter', + ...(schemaVersion === 2 + ? { runtime: { kind: 'platform_hosted', protocol: 'makelore-hosted.v1' } } + : {}), skills: [{ id: 'example', entry: '../skills/example/SKILL.md', grants: [] }], tools: [{ name: 'example_read' }], }; await mkdir(path.join(pluginRoot, 'com.makelore'), { recursive: true }); - await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true }); + await mkdir(path.join(pluginRoot, 'skills', 'example'), { recursive: true }); await mkdir(path.join(sourceResources, 'coding-skills', 'agent-browser'), { recursive: true }); await writeFile(path.join(pluginRoot, 'plugin.json'), JSON.stringify({ name: 'example.plugin', @@ -96,8 +100,11 @@ async function bundledResourceFixture() { })); await writeFile(path.join(pluginRoot, 'com.makelore', 'capability.json'), JSON.stringify(capability)); await writeFile(path.join(pluginRoot, 'skills', 'example', 'SKILL.md'), '# Example Skill\n'); - await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n'); - await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n'); + if (includeSdkAssets) { + await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true }); + await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n'); + await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n'); + } await writeFile(path.join(sourceResources, 'coding-skills', 'agent-browser', 'SKILL.md'), '# Browser\n'); const resourcesDirectory = path.join(root, 'packaged'); await cp(sourceResources, path.join(resourcesDirectory, 'resources'), { recursive: true }); @@ -206,6 +213,20 @@ describe('final Pi product artifact verification', () => { }); }); + it('accepts schema-2 hosted plugins whose implementation has no client SDK assets', async () => { + const fixture = await bundledResourceFixture({ schemaVersion: 2, includeSdkAssets: false }); + await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({ + packages: [{ + id: 'example.plugin', + skills: [{ id: 'example', path: 'skills/example/SKILL.md' }], + sdkAssets: [], + adapterId: 'example.plugin', + tools: ['example_read'], + }], + result: 'pass', + }); + }); + it('proves the packaged Marketplace trust, routes, effective snapshot, and Renderer assets', () => { const artifact = Buffer.from(MARKETPLACE_ARTIFACT_TEXT); diff --git a/tests/unit/pi-resource-loader.test.ts b/tests/unit/pi-resource-loader.test.ts index b1b4e94..7696d44 100644 --- a/tests/unit/pi-resource-loader.test.ts +++ b/tests/unit/pi-resource-loader.test.ts @@ -40,6 +40,8 @@ async function fixtureRoot(): Promise<{ mkdir(path.join(skillsDir, 'grilling'), { recursive: true }), mkdir(path.join(skillsDir, 'agent-browser'), { recursive: true }), mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service'), { recursive: true }), + mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource'), { recursive: true }), + mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search'), { recursive: true }), ]); await Promise.all([ writeFile(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill', 'SKILL.md'), 'untrusted', 'utf8'), @@ -51,6 +53,16 @@ async function fixtureRoot(): Promise<{ '---\nname: data-service\n---\n', 'utf8', ), + writeFile( + path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'), + '---\nname: game-resource\n---\n', + 'utf8', + ), + writeFile( + path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'), + '---\nname: makelore-web-search\n---\n', + 'utf8', + ), ]); return { root, userDataDir, projectDir, skillsDir }; } @@ -160,15 +172,21 @@ describe('Pi managed resource loader', () => { projectId: 'project-1', agentId: 'agent-1', prompt: 'plugin prompt', - skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }], + skillEntries: [ + { id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }, + { id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }, + { id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' }, + ], catalogRevision: 13, bundledSkillsDir: fixture.skillsDir, revision: { provider: 1, resources: 1 }, }); - expect(resources.skillIds).toEqual(['data-service']); + expect(resources.skillIds).toEqual(['data-service', 'game-resource', 'makelore-web-search']); expect(resources.skillPaths).toEqual([ path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'), + path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'), + path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'), ]); expect(resources.catalogRevision).toBe(13); }); diff --git a/tests/unit/plugin-marketplace-pages.test.tsx b/tests/unit/plugin-marketplace-pages.test.tsx index 02313d3..2267ae7 100644 --- a/tests/unit/plugin-marketplace-pages.test.tsx +++ b/tests/unit/plugin-marketplace-pages.test.tsx @@ -129,6 +129,29 @@ describe('My Plugins', () => { expect(screen.getByRole('link', { name: '分配给伙伴' })).toBeVisible(); }); + it('shows acquired code-owned bundled Plugins as supplied without device download actions', () => { + const bundled = { + ...library.items[0], + pluginId: 'makelore.web-search', + title: '联网搜索', + stableVersion: '1.0.0', + betaVersion: null, + }; + render(); + + expect(screen.getByText('随 MakeLore 提供')).toBeVisible(); + expect(screen.queryByText('尚未下载到设备')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '下载联网搜索' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '更新联网搜索' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '删除设备上的联网搜索' })).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: '移除联网搜索' })).toBeVisible(); + expect(screen.getByRole('link', { name: '启用到项目' })).toBeVisible(); + }); + it('updates an installed Beta package only through the explicit Beta action', () => { const onUpdate = vi.fn(); const onInstallBeta = vi.fn();