fix(coding): remediate ML-07 plugin authority

This commit is contained in:
2026-08-27 20:40:58 +08:00
parent 9407c67df2
commit cf13aa7eba
29 changed files with 1008 additions and 214 deletions

View File

@@ -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 });

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
}
}