feat: integrate marketplace plugins with coding runtime
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user