fix(coding): remediate ML-07 plugin authority
This commit is contained in:
@@ -42,6 +42,10 @@ import {
|
||||
} from '../coding-plugins/registry';
|
||||
import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service';
|
||||
import { PluginPolicyClient } from '../services/plugin-policy-client';
|
||||
import {
|
||||
loadBundledCodingPluginDefinitionsSync,
|
||||
resolveBundledCodingPluginRootPaths,
|
||||
} from '../coding-plugins/manifest';
|
||||
import {
|
||||
createCodingProjectPluginService,
|
||||
type CodingProjectPluginService,
|
||||
@@ -88,6 +92,20 @@ export function createCodingComposition(
|
||||
options: CreateCodingCompositionOptions,
|
||||
): CodingProductComposition {
|
||||
const getLocalProxyCredential = options.getLocalProxyCredential;
|
||||
const bundledPluginsDir = path.join(
|
||||
path.dirname(path.resolve(options.paths.bundledSkillsDir)),
|
||||
'coding-plugins',
|
||||
);
|
||||
const pluginDefinitions = loadBundledCodingPluginDefinitionsSync(bundledPluginsDir);
|
||||
const pluginRoots = resolveBundledCodingPluginRootPaths(bundledPluginsDir);
|
||||
const pluginSkillSources = pluginDefinitions.flatMap((definition, index) => (
|
||||
definition.skills.map((skill) => ({
|
||||
id: skill.id,
|
||||
pluginId: definition.id,
|
||||
directory: path.join(pluginRoots[index] as string, path.dirname(skill.entryPath)),
|
||||
entryPath: path.basename(skill.entryPath),
|
||||
}))
|
||||
));
|
||||
const projectStore = options.projectStore ?? createCodingProjectStore(options.storage);
|
||||
const attachments = new CodingAttachmentStore(
|
||||
path.join(options.paths.userDataDir, 'coding-runtime', 'attachments'),
|
||||
@@ -96,6 +114,7 @@ export function createCodingComposition(
|
||||
browser: options.browser,
|
||||
attachments,
|
||||
bundledSkillsDir: options.paths.bundledSkillsDir,
|
||||
pluginSkillSources,
|
||||
});
|
||||
const extensionHost = new PiManagedExtensionHost();
|
||||
extensionHost.configureProductTools(productTools);
|
||||
@@ -156,6 +175,7 @@ export function createCodingComposition(
|
||||
const dataServiceAdapter = createDataServicePluginAdapter(dataService);
|
||||
const policyClient = new PluginPolicyClient();
|
||||
const projectPlugins = createProjectPluginService({
|
||||
knownPluginIds: pluginDefinitions.map(({ id }) => id),
|
||||
onManagedInputsChanged: async ({ projectPath }) => {
|
||||
runtime?.markResourcesStale();
|
||||
const conversations = await conversationStoreForProject(projectPath).read()
|
||||
@@ -172,6 +192,7 @@ export function createCodingComposition(
|
||||
policyClient,
|
||||
projectPlugins,
|
||||
adapters: [dataServiceAdapter],
|
||||
definitions: pluginDefinitions,
|
||||
getDurableProjectId: async (projectPath, localProjectId) => {
|
||||
const active = await projects.requireActiveRealProjectWithIdentity(projectPath);
|
||||
if (active.project.id !== localProjectId) {
|
||||
@@ -195,6 +216,7 @@ export function createCodingComposition(
|
||||
projectPlugins,
|
||||
policyClient,
|
||||
adapters: [dataServiceAdapter],
|
||||
definitions: pluginDefinitions,
|
||||
});
|
||||
const workerPool = new PiWorkerPool({
|
||||
processBudget,
|
||||
@@ -269,6 +291,7 @@ export function createCodingComposition(
|
||||
const host = createCodingProductHost({
|
||||
projects,
|
||||
productTools,
|
||||
getEnabledPluginIds: (projectPath) => projectPlugins.getEnabledPluginIds(projectPath),
|
||||
listPiCommands: (conversationId) => conversations.listLiveCommands(conversationId),
|
||||
});
|
||||
previewDataSession = createPreviewDataSessionManager({ projects });
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { PiProductTools } from '../coding-runtime/pi/product-tools';
|
||||
import type { DataServiceOperations } from '../services/data-service-client';
|
||||
import type { PreviewDataSessionManager } from '../services/preview-data-session';
|
||||
import {
|
||||
BUNDLED_CODING_PLUGIN_DEFINITIONS,
|
||||
type CodingPluginDefinition,
|
||||
type PluginBillingMode,
|
||||
} from '../../shared/coding-plugins';
|
||||
@@ -97,6 +96,8 @@ export interface CodingPluginProjectProjection {
|
||||
items: Array<{
|
||||
id: string;
|
||||
version: string;
|
||||
contractVersion: number;
|
||||
requiresBackend: boolean;
|
||||
displayName: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
@@ -105,7 +106,17 @@ export interface CodingPluginProjectProjection {
|
||||
skills: Array<{ id: string; assignedAgentIds: string[] }>;
|
||||
capabilities: Array<{
|
||||
id: string;
|
||||
operations: Array<{ id: string; billing: PublicPluginBilling }>;
|
||||
operations: Array<{
|
||||
id: string;
|
||||
billing: PublicPluginBilling;
|
||||
tool: {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
mutation: 'read' | 'write' | 'destructive';
|
||||
permissions: string[];
|
||||
} | null;
|
||||
}>;
|
||||
}>;
|
||||
settingsSurface: string | null;
|
||||
}>;
|
||||
@@ -137,7 +148,7 @@ export interface CreateCodingProjectPluginServiceOptions {
|
||||
projectPlugins: Pick<ProjectPluginService, 'getEnabledPluginIds' | 'setEnabled'>;
|
||||
policyClient: Pick<PluginPolicyClient, 'refresh' | 'getState'>;
|
||||
adapters: readonly CodingPluginAdapter[];
|
||||
definitions?: readonly CodingPluginDefinition[];
|
||||
definitions: readonly CodingPluginDefinition[];
|
||||
}
|
||||
|
||||
function policyOperation(
|
||||
@@ -245,7 +256,7 @@ function effectiveState(input: {
|
||||
export function createCodingProjectPluginService(
|
||||
options: CreateCodingProjectPluginServiceOptions,
|
||||
): CodingProjectPluginService {
|
||||
const definitions = options.definitions ?? BUNDLED_CODING_PLUGIN_DEFINITIONS;
|
||||
const definitions = options.definitions;
|
||||
const adapters = new Map(options.adapters.map((adapter) => [adapter.pluginId, adapter]));
|
||||
|
||||
async function project(localProjectId: string) {
|
||||
@@ -278,13 +289,27 @@ export function createCodingProjectPluginService(
|
||||
const policyAvailable = Boolean(
|
||||
pluginPolicy?.supported_contract_versions.includes(definition.contractVersion),
|
||||
);
|
||||
const capabilityIds = [...new Set(definition.tools.map(({ capabilityId }) => capabilityId))];
|
||||
const capabilityIds = [...new Set(definition.operations.map(({ capabilityId }) => capabilityId))];
|
||||
const capabilities = capabilityIds.flatMap((capabilityId) => {
|
||||
const operations = definition.tools
|
||||
.filter((tool) => tool.capabilityId === capabilityId)
|
||||
.flatMap((tool) => {
|
||||
const joined = policyOperation(policy, definition, capabilityId, tool.operation);
|
||||
return joined ? [{ id: tool.operation, billing: publicBilling(joined.billing, policy) }] : [];
|
||||
const operations = definition.operations
|
||||
.filter((candidate) => candidate.capabilityId === capabilityId)
|
||||
.flatMap((candidate) => {
|
||||
const joined = policyOperation(policy, definition, capabilityId, candidate.operation);
|
||||
if (!joined) return [];
|
||||
const tool = candidate.toolName
|
||||
? definition.tools.find(({ name }) => name === candidate.toolName)
|
||||
: undefined;
|
||||
return [{
|
||||
id: candidate.operation,
|
||||
billing: publicBilling(joined.billing, policy),
|
||||
tool: tool ? {
|
||||
name: tool.name,
|
||||
label: tool.label,
|
||||
description: tool.description,
|
||||
mutation: tool.mutation,
|
||||
permissions: [...tool.permissions],
|
||||
} : null,
|
||||
}];
|
||||
});
|
||||
return operations.length > 0 ? [{ id: capabilityId, operations }] : [];
|
||||
});
|
||||
@@ -292,6 +317,8 @@ export function createCodingProjectPluginService(
|
||||
return {
|
||||
id: definition.id,
|
||||
version: definition.version,
|
||||
contractVersion: definition.contractVersion,
|
||||
requiresBackend: definition.requiresBackend,
|
||||
displayName: definition.displayName,
|
||||
description: definition.description,
|
||||
enabled,
|
||||
@@ -349,6 +376,7 @@ export interface CodingProductHostOptions {
|
||||
projects: Pick<CodingProjectService, 'getActiveProject' | 'findActiveConversation'>;
|
||||
productTools: PiProductTools;
|
||||
files?: CodingProjectFileService;
|
||||
getEnabledPluginIds?(projectPath: string): Promise<readonly string[]>;
|
||||
listPiCommands?(conversationId: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -455,16 +483,23 @@ export function createCodingProductHost(options: CodingProductHostOptions): Codi
|
||||
},
|
||||
async listSkills(agentId) {
|
||||
const project = await activeProject();
|
||||
const enabledPluginIds = options.getEnabledPluginIds
|
||||
? await options.getEnabledPluginIds(project.path)
|
||||
: [];
|
||||
return await options.productTools.listSkills(
|
||||
await selectedSkillIds(project.path, agentId),
|
||||
enabledPluginIds,
|
||||
);
|
||||
},
|
||||
async listCommands(conversationId) {
|
||||
const context = await conversationContext(conversationId);
|
||||
const enabledPluginIds = options.getEnabledPluginIds
|
||||
? await options.getEnabledPluginIds(context.project.path)
|
||||
: [];
|
||||
const piCommands = options.listPiCommands
|
||||
? normalizePiCommands(await options.listPiCommands(conversationId))
|
||||
: [];
|
||||
return await options.productTools.listCommands(context.skillIds, piCommands);
|
||||
return await options.productTools.listCommands(context.skillIds, piCommands, enabledPluginIds);
|
||||
},
|
||||
async getChanges(conversationId) {
|
||||
await conversationContext(conversationId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
BUNDLED_CODING_PLUGIN_SETTINGS_SURFACES,
|
||||
CODE_OWNED_PLUGIN_PERMISSION_IDS,
|
||||
DATA_SERVICE_CAPABILITY_IDS,
|
||||
DATA_SERVICE_OPERATION_DEFINITIONS,
|
||||
DATA_SERVICE_PLUGIN_ID,
|
||||
DATA_SERVICE_TOOL_NAMES,
|
||||
type AgentPluginsRootManifest,
|
||||
@@ -563,6 +565,9 @@ export function parseCodingPluginManifest(
|
||||
requiresBackend,
|
||||
skills,
|
||||
tools,
|
||||
operations: pluginId === DATA_SERVICE_PLUGIN_ID
|
||||
? DATA_SERVICE_OPERATION_DEFINITIONS
|
||||
: tools.map(({ capabilityId, operation, name }) => ({ capabilityId, operation, toolName: name })),
|
||||
surfaces: normalizedSurfaces,
|
||||
});
|
||||
validateDataServiceDefinition(definition, capabilityManifestPath);
|
||||
@@ -613,6 +618,42 @@ export async function loadCodingPluginDefinition(packageRoot: string): Promise<C
|
||||
return definition;
|
||||
}
|
||||
|
||||
export function loadCodingPluginDefinitionSync(packageRoot: string): CodingPluginDefinition {
|
||||
const root = path.resolve(packageRoot);
|
||||
const pluginManifestPath = path.join(root, PACKAGE_MANIFEST_FILE);
|
||||
const rootValue = readJson(readFileSync(pluginManifestPath, 'utf8'), pluginManifestPath);
|
||||
const rootManifest = parseAgentPluginsRootManifest(rootValue, pluginManifestPath);
|
||||
const capabilityManifest = normalizeCandidatePath(
|
||||
root,
|
||||
root,
|
||||
rootManifest.extensions['com.makelore'].capabilityManifest,
|
||||
pluginManifestPath,
|
||||
'extensions.com.makelore.capabilityManifest',
|
||||
);
|
||||
if (capabilityManifest !== CAPABILITY_MANIFEST_RELATIVE_PATH) {
|
||||
fail(pluginManifestPath, 'extensions.com.makelore.capabilityManifest', `must point to ${CAPABILITY_MANIFEST_RELATIVE_PATH}`);
|
||||
}
|
||||
const capabilityManifestPath = path.join(root, capabilityManifest);
|
||||
const capabilityValue = readJson(readFileSync(capabilityManifestPath, 'utf8'), capabilityManifestPath);
|
||||
const definition = parseCodingPluginManifest(rootValue, capabilityValue, {
|
||||
packageRoot: root,
|
||||
rootManifestPath: pluginManifestPath,
|
||||
capabilityManifestPath,
|
||||
});
|
||||
for (const skill of definition.skills) {
|
||||
try {
|
||||
readFileSync(path.join(root, skill.entryPath), 'utf8');
|
||||
} catch (error) {
|
||||
fail(
|
||||
capabilityManifestPath,
|
||||
`skills.${skill.id}.entry`,
|
||||
`Skill entry could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
export function resolveBundledCodingPluginRootPaths(resourcesRoot: string): string[] {
|
||||
const root = path.resolve(resourcesRoot);
|
||||
return BUNDLED_CODING_PLUGIN_ROOTS.map((relativeRoot) => path.join(root, relativeRoot));
|
||||
@@ -620,22 +661,7 @@ export function resolveBundledCodingPluginRootPaths(resourcesRoot: string): stri
|
||||
|
||||
export const resolveBundledPluginRoots = resolveBundledCodingPluginRootPaths;
|
||||
|
||||
export async function loadBundledCodingPluginDefinitions(
|
||||
resourcesRoot: string,
|
||||
): Promise<CodingPluginDefinition[]> {
|
||||
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
||||
const definitions = await Promise.all(roots.map(async (root, index) => {
|
||||
const definition = await loadCodingPluginDefinition(root);
|
||||
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
||||
if (definition.id !== `makelore.${expectedRoot}`) {
|
||||
throw new CodingPluginManifestError(
|
||||
path.join(root, PACKAGE_MANIFEST_FILE),
|
||||
'name',
|
||||
`fixed bundle root ${expectedRoot} contains ${definition.id}`,
|
||||
);
|
||||
}
|
||||
return definition;
|
||||
}));
|
||||
function validateBundledDefinitions(definitions: readonly CodingPluginDefinition[]): void {
|
||||
const pluginIds = new Set<string>();
|
||||
const capabilityIds = new Set<string>();
|
||||
const skillIds = new Set<string>();
|
||||
@@ -657,7 +683,46 @@ export async function loadBundledCodingPluginDefinitions(
|
||||
capabilityIds.add(tool.capabilityId);
|
||||
}
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
export async function loadBundledCodingPluginDefinitions(
|
||||
resourcesRoot: string,
|
||||
): Promise<readonly CodingPluginDefinition[]> {
|
||||
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
||||
const definitions = await Promise.all(roots.map(async (root, index) => {
|
||||
const definition = await loadCodingPluginDefinition(root);
|
||||
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
||||
if (definition.id !== `makelore.${expectedRoot}`) {
|
||||
throw new CodingPluginManifestError(
|
||||
path.join(root, PACKAGE_MANIFEST_FILE),
|
||||
'name',
|
||||
`fixed bundle root ${expectedRoot} contains ${definition.id}`,
|
||||
);
|
||||
}
|
||||
return definition;
|
||||
}));
|
||||
validateBundledDefinitions(definitions);
|
||||
return Object.freeze(definitions);
|
||||
}
|
||||
|
||||
export function loadBundledCodingPluginDefinitionsSync(
|
||||
resourcesRoot: string,
|
||||
): readonly CodingPluginDefinition[] {
|
||||
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
||||
const definitions = roots.map((root, index) => {
|
||||
const definition = loadCodingPluginDefinitionSync(root);
|
||||
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
||||
if (definition.id !== `makelore.${expectedRoot}`) {
|
||||
throw new CodingPluginManifestError(
|
||||
path.join(root, PACKAGE_MANIFEST_FILE),
|
||||
'name',
|
||||
`fixed bundle root ${expectedRoot} contains ${definition.id}`,
|
||||
);
|
||||
}
|
||||
return definition;
|
||||
});
|
||||
validateBundledDefinitions(definitions);
|
||||
return Object.freeze(definitions);
|
||||
}
|
||||
|
||||
export const parseBundledCodingPlugins = loadBundledCodingPluginDefinitions;
|
||||
|
||||
@@ -2,7 +2,6 @@ import path from 'node:path';
|
||||
import { atomicWriteJson, readJsonFile, type JsonFileWriter } from '../coding-projects/atomic-json';
|
||||
import { readCodingProjectConfigV2 } from '../coding-projects/project-config';
|
||||
import {
|
||||
BUNDLED_CODING_PLUGIN_DEFINITIONS,
|
||||
DATA_SERVICE_PLUGIN_ID,
|
||||
} from '../../shared/coding-plugins';
|
||||
|
||||
@@ -98,7 +97,7 @@ function knownPluginIdSet(
|
||||
): Set<string> {
|
||||
const source = typeof value === 'function'
|
||||
? value()
|
||||
: value ?? BUNDLED_CODING_PLUGIN_DEFINITIONS.map(({ id }) => id);
|
||||
: value ?? [DATA_SERVICE_PLUGIN_ID];
|
||||
return new Set(source.map((id) => id.trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
@@ -193,6 +192,7 @@ interface ReadSelectionResult {
|
||||
export class ProjectPluginService {
|
||||
private readonly mutationTails = new Map<string, Promise<unknown>>();
|
||||
private readonly managedInputRevisions = new Map<string, number>();
|
||||
private readonly legacySelections = new Map<string, readonly string[]>();
|
||||
|
||||
constructor(private readonly options: ProjectPluginServiceOptions = {}) {}
|
||||
|
||||
@@ -211,6 +211,7 @@ export class ProjectPluginService {
|
||||
const result = await this.readFile(filePath);
|
||||
const knownIds = knownPluginIdSet(this.options.knownPluginIds);
|
||||
if (result.file) {
|
||||
this.legacySelections.delete(project);
|
||||
const enabledPluginIds = result.file.enabledPluginIds;
|
||||
return {
|
||||
projectPath: project,
|
||||
@@ -225,7 +226,11 @@ export class ProjectPluginService {
|
||||
};
|
||||
}
|
||||
|
||||
const legacyProjectedPluginIds = await this.legacyProjectedPluginIds(project);
|
||||
let legacyProjectedPluginIds = this.legacySelections.get(project);
|
||||
if (!legacyProjectedPluginIds) {
|
||||
legacyProjectedPluginIds = Object.freeze(await this.legacyProjectedPluginIds(project));
|
||||
this.legacySelections.set(project, legacyProjectedPluginIds);
|
||||
}
|
||||
const enabledPluginIds = [...legacyProjectedPluginIds];
|
||||
return {
|
||||
projectPath: project,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BUNDLED_CODING_PLUGIN_DEFINITIONS,
|
||||
type CodingPluginDefinition,
|
||||
type CodingPluginToolDefinition,
|
||||
type PluginBillingMode,
|
||||
@@ -120,7 +119,7 @@ export interface CodingCapabilityRegistryOptions {
|
||||
getEnabledPluginIds(projectPath: string): Promise<readonly string[]>;
|
||||
};
|
||||
adapters: readonly CodingPluginAdapter[];
|
||||
definitions?: readonly CodingPluginDefinition[];
|
||||
definitions: readonly CodingPluginDefinition[];
|
||||
getDurableProjectId?: (projectPath: string, localProjectId: string) => Promise<string> | string;
|
||||
}
|
||||
|
||||
@@ -340,7 +339,7 @@ export class CodingCapabilityRegistryImpl implements CodingCapabilityRegistryPor
|
||||
private readonly adaptersByPluginId: ReadonlyMap<string, CodingPluginAdapter>;
|
||||
|
||||
constructor(private readonly options: CodingCapabilityRegistryOptions) {
|
||||
const definitions = (options.definitions ?? BUNDLED_CODING_PLUGIN_DEFINITIONS).filter(definitionValid);
|
||||
const definitions = options.definitions.filter(definitionValid);
|
||||
this.definitions = Object.freeze([...definitions]);
|
||||
const tools = new Map<string, { definition: CodingPluginDefinition; tool: CodingPluginToolDefinition }>();
|
||||
for (const definition of this.definitions) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
|
||||
import {
|
||||
BUNDLED_CODING_SKILL_IDS,
|
||||
type BundledCodingSkillId,
|
||||
type CodingSkillId,
|
||||
} from '../../shared/coding-skills';
|
||||
import type {
|
||||
ProductCodingCommand,
|
||||
@@ -29,7 +28,9 @@ const COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
||||
|
||||
export interface ProductCodingPluginSkillSource {
|
||||
id: string;
|
||||
pluginId?: string;
|
||||
directory: string;
|
||||
available?: boolean;
|
||||
/** Package-relative Skill entry; defaults to `SKILL.md`. */
|
||||
entryPath?: string;
|
||||
}
|
||||
@@ -47,20 +48,6 @@ function frontmatterScalar(content: string, key: string): string | undefined {
|
||||
return value || undefined;
|
||||
}
|
||||
|
||||
function defaultPluginSkillSources(bundledSkillsDir: string): ProductCodingPluginSkillSource[] {
|
||||
const skillId = DATA_SERVICE_PLUGIN_DEFINITION.skills[0]?.id ?? 'data-service';
|
||||
return [{
|
||||
id: skillId,
|
||||
directory: path.join(
|
||||
path.dirname(path.resolve(bundledSkillsDir)),
|
||||
'coding-plugins',
|
||||
'data-service',
|
||||
'skills',
|
||||
'data-service',
|
||||
),
|
||||
}];
|
||||
}
|
||||
|
||||
function selectedSkillIds(
|
||||
value: readonly string[],
|
||||
allIds: readonly string[],
|
||||
@@ -75,11 +62,8 @@ function selectedSkillIds(
|
||||
return selected;
|
||||
}
|
||||
|
||||
function productSkillId(id: string): BundledCodingSkillId {
|
||||
// ProductCodingSkill predates package-owned Skill ids. The registry is
|
||||
// the trusted projection boundary, so the runtime value may contain a
|
||||
// package Skill while the shared contract is migrated by the caller.
|
||||
return id as BundledCodingSkillId;
|
||||
function productSkillId(id: string): CodingSkillId {
|
||||
return id;
|
||||
}
|
||||
|
||||
async function listSkillEntries(
|
||||
@@ -103,7 +87,7 @@ async function listSkillEntries(
|
||||
export async function listProductCodingSkills(
|
||||
bundledSkillsDir: string,
|
||||
selectedIds: readonly string[] = [],
|
||||
pluginSkillSources: readonly ProductCodingPluginSkillSource[] = defaultPluginSkillSources(bundledSkillsDir),
|
||||
pluginSkillSources: readonly ProductCodingPluginSkillSource[] = [],
|
||||
): Promise<ProductCodingSkill[]> {
|
||||
const pluginIds = pluginSkillSources.map(({ id }) => id);
|
||||
const allIds = [...BUNDLED_CODING_SKILL_IDS, ...pluginIds];
|
||||
@@ -118,7 +102,8 @@ export async function listProductCodingSkills(
|
||||
})),
|
||||
...pluginSkillSources,
|
||||
];
|
||||
return await Promise.all(sources.map(async ({ id, directory, entryPath }) => {
|
||||
const visibleSources = sources.filter(({ id, available }) => available !== false || selected.has(id));
|
||||
return await Promise.all(visibleSources.map(async ({ id, directory, entryPath, available = true }) => {
|
||||
const location = path.resolve(directory);
|
||||
const content = await readFile(path.join(location, entryPath ?? 'SKILL.md'), 'utf8');
|
||||
return {
|
||||
@@ -126,6 +111,8 @@ export async function listProductCodingSkills(
|
||||
name: frontmatterScalar(content, 'name') ?? id,
|
||||
description: frontmatterScalar(content, 'description') ?? '',
|
||||
selected: selected.has(id),
|
||||
available,
|
||||
effective: available && selected.has(id),
|
||||
location,
|
||||
content,
|
||||
entries: await listSkillEntries(location),
|
||||
@@ -153,7 +140,7 @@ export function buildProductCodingCommandCatalog(
|
||||
}
|
||||
for (const skill of skills) {
|
||||
const key = skill.id.toLocaleLowerCase();
|
||||
if (!skill.selected || used.has(key)) continue;
|
||||
if (!skill.effective || used.has(key)) continue;
|
||||
used.add(key);
|
||||
commands.push({
|
||||
name: skill.id,
|
||||
|
||||
@@ -14,20 +14,11 @@ import type {
|
||||
ProductPiCommandInput,
|
||||
} from '../../../shared/coding-product-tools';
|
||||
import {
|
||||
DATA_SERVICE_PI_TOOL_NAMES,
|
||||
type DataServicePiToolName,
|
||||
} from '../../../shared/data-service';
|
||||
type ProductCodingPluginSkillSource,
|
||||
} from '../../coding-projects/skill-registry';
|
||||
import {
|
||||
DATA_SERVICE_PLUGIN_DEFINITION,
|
||||
} from '../../../shared/coding-plugins';
|
||||
import { createDataServicePluginAdapter } from '../../coding-plugins/adapters/data-service';
|
||||
import {
|
||||
buildCapabilityToolResult,
|
||||
type AdapterInvocationResult,
|
||||
type CodingCapabilityRegistry,
|
||||
type TrustedCodingCapabilityContext,
|
||||
} from '../../coding-plugins/registry';
|
||||
import type { DataServiceOperations } from '../../services/data-service-client';
|
||||
import type { KnownToolDetails, RuntimeContextDetailsV1 } from '../contracts';
|
||||
import { PiAgentBrowserTool } from './extensions/agent-browser';
|
||||
import { reportChangedFiles } from './extensions/changed-file';
|
||||
@@ -40,10 +31,7 @@ export type PiProductToolName =
|
||||
| 'game_asset_review'
|
||||
| 'task_state'
|
||||
| 'changed_file'
|
||||
| 'runtime_context'
|
||||
| DataServicePiToolName;
|
||||
|
||||
export { DATA_SERVICE_PI_TOOL_NAMES };
|
||||
| 'runtime_context';
|
||||
|
||||
const PI_PRODUCT_TOOL_NAMES = new Set<string>([
|
||||
'agent_browser',
|
||||
@@ -52,7 +40,6 @@ const PI_PRODUCT_TOOL_NAMES = new Set<string>([
|
||||
'task_state',
|
||||
'changed_file',
|
||||
'runtime_context',
|
||||
...DATA_SERVICE_PI_TOOL_NAMES,
|
||||
]);
|
||||
|
||||
export function isPiProductToolName(value: unknown): value is PiProductToolName {
|
||||
@@ -78,7 +65,7 @@ export interface PiProductToolsOptions {
|
||||
attachments: CodingAttachmentStore;
|
||||
bundledSkillsDir: string;
|
||||
changeTracker?: ConversationChangeTracker;
|
||||
dataService?: DataServiceOperations;
|
||||
pluginSkillSources?: readonly ProductCodingPluginSkillSource[];
|
||||
capabilityRegistry?: CodingCapabilityRegistry;
|
||||
}
|
||||
|
||||
@@ -86,20 +73,12 @@ export class PiProductTools {
|
||||
readonly changeTracker: ConversationChangeTracker;
|
||||
private readonly browser: PiAgentBrowserTool;
|
||||
private readonly gameAssets = new PiGameAssetTools();
|
||||
private dataServiceAdapter: ReturnType<typeof createDataServicePluginAdapter> | undefined;
|
||||
private capabilityRegistry: CodingCapabilityRegistry | undefined;
|
||||
|
||||
constructor(private readonly options: PiProductToolsOptions) {
|
||||
this.changeTracker = options.changeTracker ?? new ConversationChangeTracker();
|
||||
this.browser = new PiAgentBrowserTool(options.browser, options.attachments);
|
||||
this.capabilityRegistry = options.capabilityRegistry;
|
||||
this.dataServiceAdapter = options.dataService
|
||||
? createDataServicePluginAdapter(options.dataService)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
configureDataService(dataService: DataServiceOperations): void {
|
||||
this.dataServiceAdapter = createDataServicePluginAdapter(dataService);
|
||||
}
|
||||
|
||||
configureCapabilityRegistry(registry: CodingCapabilityRegistry): void {
|
||||
@@ -118,15 +97,27 @@ export class PiProductTools {
|
||||
return this.changeTracker.getSnapshot(conversationId);
|
||||
}
|
||||
|
||||
listSkills(skillIds: readonly string[]): Promise<ProductCodingSkill[]> {
|
||||
return listProductCodingSkills(this.options.bundledSkillsDir, skillIds);
|
||||
listSkills(
|
||||
skillIds: readonly string[],
|
||||
availablePluginSkillIds: readonly string[] = [],
|
||||
): Promise<ProductCodingSkill[]> {
|
||||
const available = new Set(availablePluginSkillIds);
|
||||
const sources = (this.options.pluginSkillSources ?? []).map((source) => ({
|
||||
...source,
|
||||
available: available.has(source.pluginId ?? source.id) || available.has(source.id),
|
||||
}));
|
||||
return listProductCodingSkills(this.options.bundledSkillsDir, skillIds, sources);
|
||||
}
|
||||
|
||||
async listCommands(
|
||||
skillIds: readonly string[],
|
||||
piCommands: readonly ProductPiCommandInput[] = [],
|
||||
availablePluginSkillIds: readonly string[] = [],
|
||||
): Promise<ProductCodingCommand[]> {
|
||||
return buildProductCodingCommandCatalog(await this.listSkills(skillIds), piCommands);
|
||||
return buildProductCodingCommandCatalog(
|
||||
await this.listSkills(skillIds, availablePluginSkillIds),
|
||||
piCommands,
|
||||
);
|
||||
}
|
||||
|
||||
async markBash(conversationId: string, runId: string): Promise<void> {
|
||||
@@ -164,32 +155,8 @@ export class PiProductTools {
|
||||
value: input,
|
||||
});
|
||||
}
|
||||
if (DATA_SERVICE_PI_TOOL_NAMES.includes(toolName as DataServicePiToolName)) {
|
||||
const adapter = this.dataServiceAdapter;
|
||||
const definition = DATA_SERVICE_PLUGIN_DEFINITION.tools.find(({ name }) => name === toolName);
|
||||
if (!adapter || !definition) throw new Error('Data Service tools are unavailable');
|
||||
const trustedContext: TrustedCodingCapabilityContext = {
|
||||
conversationId: context.conversationId,
|
||||
runId: context.runId,
|
||||
resourceId: context.resourceId,
|
||||
requestId: `pi:${context.runId}:${context.resourceId}`,
|
||||
localProjectId: context.projectId,
|
||||
projectPath: context.projectPath,
|
||||
durableProjectId: context.projectId,
|
||||
workerRole: 'parent',
|
||||
effectiveSkillIds: [...context.skillIds],
|
||||
};
|
||||
const result: AdapterInvocationResult = await adapter.invoke(trustedContext, definition, input);
|
||||
return buildCapabilityToolResult(
|
||||
DATA_SERVICE_PLUGIN_DEFINITION,
|
||||
definition,
|
||||
context,
|
||||
result,
|
||||
{ mode: 'included', status: 'included' },
|
||||
);
|
||||
}
|
||||
if (toolName !== 'runtime_context') throw new Error('Product tool is unavailable');
|
||||
const skills = await this.listSkills(context.skillIds);
|
||||
const skills = await this.listSkills(context.skillIds, context.skillIds);
|
||||
const details: RuntimeContextDetailsV1 = {
|
||||
schema: 'runtime-context.v1',
|
||||
skills,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WORKS_SQUARE_CONFIG } from '../api/works-config';
|
||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
import { proxyAwareFetch, runWithDeadline } from '../utils/proxy-fetch';
|
||||
|
||||
const MAX_CATALOG_BYTES = 1_310_720;
|
||||
const MAX_CATALOG_VERSION = 64;
|
||||
@@ -7,6 +7,7 @@ const MAX_PLUGIN_ID = 48;
|
||||
const MAX_CAPABILITY_ID = 64;
|
||||
const MAX_OPERATION_ID = 64;
|
||||
const MAX_NOTICE = 160;
|
||||
const DEFAULT_POLICY_REQUEST_TIMEOUT_MS = 10_000;
|
||||
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,47}$/u;
|
||||
const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
|
||||
const OPERATION_ID_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
|
||||
@@ -86,6 +87,7 @@ export interface PluginPolicyClientOptions {
|
||||
fetchImpl?: FetchImplementation;
|
||||
apiBaseUrl?: string;
|
||||
now?: () => number;
|
||||
requestTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export class PluginPolicyCatalogError extends Error {
|
||||
@@ -384,6 +386,7 @@ export class PluginPolicyClient {
|
||||
private readonly fetchImpl: FetchImplementation;
|
||||
private readonly apiBaseUrl: string;
|
||||
private readonly now: () => number;
|
||||
private readonly requestTimeoutMs: number;
|
||||
private state: PluginPolicyClientState = {
|
||||
status: 'unavailable',
|
||||
catalog: null,
|
||||
@@ -396,6 +399,7 @@ export class PluginPolicyClient {
|
||||
this.fetchImpl = options.fetchImpl ?? proxyAwareFetch;
|
||||
this.apiBaseUrl = (options.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/u, '');
|
||||
this.now = options.now ?? (() => Date.now());
|
||||
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_POLICY_REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
getState(): PluginPolicyClientState {
|
||||
@@ -442,24 +446,26 @@ export class PluginPolicyClient {
|
||||
}
|
||||
|
||||
private async fetchCatalog(): Promise<PluginCatalog> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await this.fetchImpl(
|
||||
`${this.apiBaseUrl}/api/plugins/v1/catalog`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
redirect: 'manual',
|
||||
},
|
||||
);
|
||||
return await runWithDeadline(async (signal) => {
|
||||
const response = await this.fetchImpl(
|
||||
`${this.apiBaseUrl}/api/plugins/v1/catalog`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
redirect: 'manual',
|
||||
signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
throw new PluginPolicyCatalogError('catalog request failed');
|
||||
}
|
||||
return await readCatalog(response);
|
||||
}, this.requestTimeoutMs);
|
||||
} catch {
|
||||
throw new PluginPolicyCatalogError('catalog request failed');
|
||||
}
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
throw new PluginPolicyCatalogError('catalog request failed');
|
||||
}
|
||||
return await readCatalog(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user