feat: integrate marketplace plugins with coding runtime

This commit is contained in:
2026-08-28 18:02:51 +08:00
parent 1d64b89499
commit 05917a789a
21 changed files with 1921 additions and 29 deletions

View File

@@ -14,6 +14,10 @@ import type {
PluginCatalogOperation,
PluginPolicyClientState,
} from '../services/plugin-policy-client';
import type {
EffectivePluginResolver,
EffectivePluginSnapshot,
} from './effective-resolver';
const MAX_REQUEST_ID = 128;
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,47}$/u;
@@ -93,6 +97,10 @@ export interface ResolvedWorkerResources {
effectiveSkillIds: readonly string[];
skillEntries: readonly { id: string; entryPath: string }[];
tools: readonly CodingPluginToolDefinition[];
/** The exact Main-owned snapshot used to produce these legacy fields. */
effectiveSnapshot?: EffectivePluginSnapshot;
/** Trusted package roots paired with the same effective snapshot. */
skillRoots?: readonly string[];
}
export interface CodingCapabilityRegistryPort {
@@ -100,7 +108,14 @@ export interface CodingCapabilityRegistryPort {
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
projectId?: string;
}): Promise<ResolvedWorkerResources>;
resolveEffectivePluginSnapshot?(input: {
projectId: string;
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
}): Promise<EffectivePluginSnapshot>;
invoke(input: {
toolName: string;
context: PiProductToolContext;
@@ -124,6 +139,7 @@ export interface CodingCapabilityRegistryOptions {
adapters: readonly CodingPluginAdapter[];
definitions: readonly CodingPluginDefinition[];
getDurableProjectId?: (projectPath: string, localProjectId: string) => Promise<string> | string;
effectiveResolver?: EffectivePluginResolver;
}
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -257,10 +273,14 @@ function definitionValid(definition: CodingPluginDefinition): boolean {
if (!PLUGIN_ID_PATTERN.test(definition.id) || !VERSION_PATTERN.test(definition.version)
|| !Number.isSafeInteger(definition.contractVersion) || definition.contractVersion < 1
|| definition.scope !== 'project' || typeof definition.requiresBackend !== 'boolean') return false;
if (!definition.skills.length || !definition.tools.length) return false;
if (!definition.skills.length
|| (definition.runtimeKind !== 'skill_only' && !definition.tools.length)
|| (definition.runtimeKind === 'skill_only' && (definition.requiresBackend || definition.tools.length > 0))) return false;
const skillIds = new Set<string>();
for (const skill of definition.skills) {
if (!skill.id || skillIds.has(skill.id) || !skill.entryPath || skill.grants.length === 0) return false;
if (!skill.id || skillIds.has(skill.id) || !skill.entryPath
|| (definition.runtimeKind !== 'skill_only' && skill.grants.length === 0)
|| (definition.runtimeKind === 'skill_only' && skill.grants.length > 0)) return false;
skillIds.add(skill.id);
}
const toolNames = new Set<string>();
@@ -358,7 +378,35 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
projectId?: string;
}): Promise<ResolvedWorkerResources> {
if (this.options.effectiveResolver) {
const snapshot = await this.options.effectiveResolver.resolve({
projectId: input.projectId ?? input.projectPath,
projectPath: input.projectPath,
assignedSkillIds: input.assignedSkillIds,
role: input.role,
});
const sources = await this.options.effectiveResolver.getSkillSources();
const effectiveSkills = new Set(snapshot.effectiveSkillIds);
const pluginIds = new Set<string>(snapshot.runtimePolicies.map(({ pluginId }) => pluginId));
const skillRoots = new Set<string>();
for (const source of sources) {
if (effectiveSkills.has(source.id)) {
pluginIds.add(source.pluginId);
skillRoots.add(source.packageRoot);
}
}
return {
catalogRevision: this.options.effectiveResolver.getPolicyState().revision,
pluginIds: [...pluginIds],
effectiveSkillIds: [...snapshot.effectiveSkillIds],
skillEntries: snapshot.skillEntries.map(({ id, entryPath }) => ({ id, entryPath })),
tools: snapshot.toolDefinitions.map((tool) => structuredClone(tool)),
effectiveSnapshot: snapshot,
skillRoots: [...skillRoots],
};
}
const assigned = [...new Set(input.assignedSkillIds)];
const coreIds = new Set<string>(CORE_CODING_SKILL_IDS);
const pluginSkillOwners = new Map<string, CodingPluginDefinition>();
@@ -432,6 +480,26 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
};
}
async resolveEffectivePluginSnapshot(input: {
projectId: string;
projectPath: string;
assignedSkillIds: readonly string[];
role: 'parent' | 'child';
}): Promise<EffectivePluginSnapshot> {
if (this.options.effectiveResolver) return await this.options.effectiveResolver.resolve(input);
const resources = await this.resolveWorkerResources(input);
return Object.freeze({
accountSessionId: 'anonymous',
projectId: input.projectId,
pluginReleaseIds: Object.freeze([]),
effectiveSkillIds: Object.freeze([...resources.effectiveSkillIds]),
skillEntries: Object.freeze(resources.skillEntries.map((entry) => Object.freeze({ ...entry }))),
toolDefinitions: Object.freeze(resources.tools.map((tool) => structuredClone(tool))),
runtimePolicies: Object.freeze([]),
unavailableReasons: Object.freeze([]),
});
}
async invoke(input: {
toolName: string;
context: PiProductToolContext;
@@ -440,10 +508,36 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
value: unknown;
}): Promise<PiProductToolResult> {
const indexed = this.toolsByName.get(input.toolName);
const state = this.options.policyClient.getState();
const validId = requestId(input.context) !== 'invalid-request-id';
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 currentSnapshot = await this.options.effectiveResolver.resolve({
projectId: input.context.projectId,
projectPath: input.context.projectPath,
assignedSkillIds: input.context.effectiveSnapshot.effectiveSkillIds,
role: input.workerRole,
});
if (currentSnapshot.accountSessionId !== input.context.effectiveSnapshot.accountSessionId
|| !currentSnapshot.toolDefinitions.some(({ name }) => name === input.toolName)) {
const disabled = currentSnapshot.unavailableReasons.some((reason) => (
reason.pluginId === definition.id && reason.code === 'project_disabled'
));
return this.resultFailure(
definition,
tool,
input.context,
disabled ? 'plugin_not_enabled' : 'plugin_runtime_stale',
disabled ? 'Plugin is not enabled for this project' : 'Plugin worker resources are stale',
disabled ? 403 : 409,
false,
policyNotStarted('unknown'),
);
}
}
// The effective resolver may refresh a stale policy while checking the
// worker snapshot. Read the post-resolution state for invocation checks.
const state = this.options.policyClient.getState();
const currentEnabled = await this.enabledPluginIds(input.context.projectPath);
const catalogPolicy = state.catalog ? policyOperation(state.catalog, definition, tool) : null;
const baseBilling = catalogPolicy ? policyNotStarted(catalogPolicy.billing.mode) : policyNotStarted('unknown');