feat(coding): add plugin capability policy registry
This commit is contained in:
@@ -15,11 +15,18 @@ import type {
|
||||
} from '../../../shared/coding-product-tools';
|
||||
import {
|
||||
DATA_SERVICE_PI_TOOL_NAMES,
|
||||
type DataServiceHostResult,
|
||||
type DataServicePiToolName,
|
||||
type DataServiceToolData,
|
||||
type DataServiceToolDetailsV1,
|
||||
} from '../../../shared/data-service';
|
||||
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';
|
||||
@@ -52,104 +59,6 @@ export function isPiProductToolName(value: unknown): value is PiProductToolName
|
||||
return typeof value === 'string' && PI_PRODUCT_TOOL_NAMES.has(value);
|
||||
}
|
||||
|
||||
const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
|
||||
const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
||||
const MAX_REQUEST_BYTES = 98_304;
|
||||
const MAX_CURSOR_LENGTH = 1_024;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function invalidDataServiceInput(): never {
|
||||
throw new Error('Data Service tool input is invalid');
|
||||
}
|
||||
|
||||
function exactInput(
|
||||
value: unknown,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = [],
|
||||
): Record<string, unknown> {
|
||||
if (!isRecord(value)) return invalidDataServiceInput();
|
||||
const allowed = new Set([...required, ...optional]);
|
||||
if (Object.keys(value).some((key) => !allowed.has(key))
|
||||
|| required.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
|
||||
return invalidDataServiceInput();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function collection(value: unknown): string {
|
||||
if (typeof value !== 'string' || !COLLECTION_PATTERN.test(value)) return invalidDataServiceInput();
|
||||
return value;
|
||||
}
|
||||
|
||||
function documentId(value: unknown): string {
|
||||
if (typeof value !== 'string' || value === '.' || value === '..'
|
||||
|| !DOCUMENT_ID_PATTERN.test(value)) return invalidDataServiceInput();
|
||||
return value;
|
||||
}
|
||||
|
||||
function revision(value: unknown): number | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 1) return invalidDataServiceInput();
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function collections(value: unknown): string[] {
|
||||
if (!Array.isArray(value) || value.length > 20) return invalidDataServiceInput();
|
||||
return value.map(collection);
|
||||
}
|
||||
|
||||
function data(value: unknown): Record<string, unknown> {
|
||||
if (!isRecord(value)) return invalidDataServiceInput();
|
||||
try {
|
||||
const encoded = JSON.stringify(value);
|
||||
if (typeof encoded !== 'string' || Buffer.byteLength(encoded, 'utf8') > MAX_REQUEST_BYTES) {
|
||||
return invalidDataServiceInput();
|
||||
}
|
||||
} catch {
|
||||
return invalidDataServiceInput();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function limit(value: unknown): number | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 100) {
|
||||
return invalidDataServiceInput();
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function cursor(value: unknown): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value !== 'string' || !value || value.length > MAX_CURSOR_LENGTH) {
|
||||
return invalidDataServiceInput();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function confirmation(value: unknown): true {
|
||||
if (value !== true) return invalidDataServiceInput();
|
||||
return true;
|
||||
}
|
||||
|
||||
function dataServiceResult<T extends DataServiceToolData | null>(
|
||||
operation: DataServicePiToolName,
|
||||
result: DataServiceHostResult<T>,
|
||||
): PiProductToolResult {
|
||||
const details: DataServiceToolDetailsV1 = {
|
||||
schema: 'data-service.v1',
|
||||
operation,
|
||||
...result,
|
||||
};
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(details) }],
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
export interface PiProductToolContext {
|
||||
conversationId: string;
|
||||
runId: string;
|
||||
@@ -170,22 +79,31 @@ export interface PiProductToolsOptions {
|
||||
bundledSkillsDir: string;
|
||||
changeTracker?: ConversationChangeTracker;
|
||||
dataService?: DataServiceOperations;
|
||||
capabilityRegistry?: CodingCapabilityRegistry;
|
||||
}
|
||||
|
||||
export class PiProductTools {
|
||||
readonly changeTracker: ConversationChangeTracker;
|
||||
private readonly browser: PiAgentBrowserTool;
|
||||
private readonly gameAssets = new PiGameAssetTools();
|
||||
private dataService: DataServiceOperations | undefined;
|
||||
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.dataService = options.dataService;
|
||||
this.capabilityRegistry = options.capabilityRegistry;
|
||||
this.dataServiceAdapter = options.dataService
|
||||
? createDataServicePluginAdapter(options.dataService)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
configureDataService(dataService: DataServiceOperations): void {
|
||||
this.dataService = dataService;
|
||||
this.dataServiceAdapter = createDataServicePluginAdapter(dataService);
|
||||
}
|
||||
|
||||
configureCapabilityRegistry(registry: CodingCapabilityRegistry): void {
|
||||
this.capabilityRegistry = registry;
|
||||
}
|
||||
|
||||
beginRun(input: { conversationId: string; runId: string; projectPath: string }) {
|
||||
@@ -220,7 +138,7 @@ export class PiProductTools {
|
||||
}
|
||||
|
||||
async execute(
|
||||
toolName: PiProductToolName,
|
||||
toolName: PiProductToolName | string,
|
||||
context: PiProductToolContext,
|
||||
input: unknown,
|
||||
): Promise<PiProductToolResult> {
|
||||
@@ -237,76 +155,38 @@ export class PiProductTools {
|
||||
if (toolName === 'changed_file') {
|
||||
return await reportChangedFiles(this.changeTracker, context, input);
|
||||
}
|
||||
if (toolName === 'data_service_configure') {
|
||||
const body = exactInput(input, ['collections']);
|
||||
const result = await this.requireDataService().configure(
|
||||
{ collections: collections(body.collections) }, context.projectPath,
|
||||
if (this.capabilityRegistry && toolName !== 'runtime_context') {
|
||||
return await this.capabilityRegistry.invoke({
|
||||
toolName,
|
||||
context,
|
||||
workerRole: 'parent',
|
||||
effectiveSkillIds: context.skillIds,
|
||||
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' },
|
||||
);
|
||||
return dataServiceResult(toolName, result);
|
||||
}
|
||||
if (toolName === 'data_service_inspect') {
|
||||
exactInput(input, []);
|
||||
return dataServiceResult(toolName, await this.requireDataService().inspect(context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_list_projects') {
|
||||
exactInput(input, []);
|
||||
return dataServiceResult(toolName, await this.requireDataService().listProjects());
|
||||
}
|
||||
if (toolName === 'data_service_get_document') {
|
||||
const body = exactInput(input, ['collection', 'document_id']);
|
||||
return dataServiceResult(toolName, await this.requireDataService().getDocument({
|
||||
collection: collection(body.collection),
|
||||
document_id: documentId(body.document_id),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_list_documents') {
|
||||
const body = exactInput(input, ['collection'], ['limit', 'cursor']);
|
||||
const requestedLimit = limit(body.limit);
|
||||
const requestedCursor = cursor(body.cursor);
|
||||
return dataServiceResult(toolName, await this.requireDataService().listDocuments({
|
||||
collection: collection(body.collection),
|
||||
...(requestedLimit === undefined ? {} : { limit: requestedLimit }),
|
||||
...(requestedCursor === undefined ? {} : { cursor: requestedCursor }),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_put_document') {
|
||||
const body = exactInput(input, ['collection', 'document_id', 'data'], ['if_revision']);
|
||||
const requestedRevision = revision(body.if_revision);
|
||||
return dataServiceResult(toolName, await this.requireDataService().putDocument({
|
||||
collection: collection(body.collection),
|
||||
document_id: documentId(body.document_id),
|
||||
data: data(body.data),
|
||||
...(requestedRevision === undefined ? {} : { if_revision: requestedRevision }),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_delete_document') {
|
||||
const body = exactInput(input, ['collection', 'document_id', 'confirmed'], ['if_revision']);
|
||||
const requestedRevision = revision(body.if_revision);
|
||||
return dataServiceResult(toolName, await this.requireDataService().deleteDocument({
|
||||
collection: collection(body.collection),
|
||||
document_id: documentId(body.document_id),
|
||||
...(requestedRevision === undefined ? {} : { if_revision: requestedRevision }),
|
||||
confirmed: confirmation(body.confirmed),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_remove_collection') {
|
||||
const body = exactInput(input, ['collection', 'confirmed']);
|
||||
return dataServiceResult(toolName, await this.requireDataService().removeCollection({
|
||||
collection: collection(body.collection),
|
||||
confirmed: confirmation(body.confirmed),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_reset') {
|
||||
const body = exactInput(input, ['confirmed']);
|
||||
return dataServiceResult(toolName, await this.requireDataService().reset({
|
||||
confirmed: confirmation(body.confirmed),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName === 'data_service_remove_project') {
|
||||
const body = exactInput(input, ['confirmed']);
|
||||
return dataServiceResult(toolName, await this.requireDataService().removeProject({
|
||||
confirmed: confirmation(body.confirmed),
|
||||
}, context.projectPath));
|
||||
}
|
||||
if (toolName !== 'runtime_context') throw new Error('Product tool is unavailable');
|
||||
const skills = await this.listSkills(context.skillIds);
|
||||
@@ -321,8 +201,4 @@ export class PiProductTools {
|
||||
};
|
||||
}
|
||||
|
||||
private requireDataService(): DataServiceOperations {
|
||||
if (!this.dataService) throw new Error('Data Service tools are unavailable');
|
||||
return this.dataService;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user