feat(pi): materialize effective plugin worker tools
This commit is contained in:
@@ -13,13 +13,20 @@ import {
|
||||
parsePiSubagentDispatchRequest,
|
||||
type PiSubagentScheduler,
|
||||
} from './subagent';
|
||||
import {
|
||||
isPiProductToolName,
|
||||
type PiProductToolName,
|
||||
type PiProductTools,
|
||||
} from './product-tools';
|
||||
import type { PiProductTools } from './product-tools';
|
||||
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
|
||||
import type { PiSkillEntry } from './resource-loader';
|
||||
|
||||
const MAX_REQUEST_BYTES = 64 * 1024;
|
||||
const PRODUCT_TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,63}$/u;
|
||||
const CORE_PRODUCT_TOOL_NAMES = new Set([
|
||||
'agent_browser',
|
||||
'game_asset_browser',
|
||||
'game_asset_review',
|
||||
'task_state',
|
||||
'changed_file',
|
||||
'runtime_context',
|
||||
]);
|
||||
|
||||
interface WorkerRegistrationRecord {
|
||||
token: string;
|
||||
@@ -28,6 +35,10 @@ interface WorkerRegistrationRecord {
|
||||
projectId: string;
|
||||
projectPath: string | null;
|
||||
skillIds: string[];
|
||||
catalogRevision?: number;
|
||||
allowedToolNames: string[];
|
||||
tools: CodingPluginToolDefinition[];
|
||||
projectWriteLeaseToolNames: string[];
|
||||
role: 'parent' | 'child';
|
||||
contextFile: string;
|
||||
runId: string | null;
|
||||
@@ -39,6 +50,7 @@ export interface PiExtensionWorkerRegistration {
|
||||
extensionPath: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
sensitiveValues: string[];
|
||||
allowedToolNames: readonly string[];
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -47,7 +59,9 @@ export interface RegisterPiExtensionWorkerInput {
|
||||
generation: number;
|
||||
projectId: string;
|
||||
projectPath?: string;
|
||||
skillIds?: readonly string[];
|
||||
skillEntries?: readonly PiSkillEntry[];
|
||||
catalogRevision?: number;
|
||||
tools?: readonly CodingPluginToolDefinition[];
|
||||
extensionsDir: string;
|
||||
role?: 'parent' | 'child';
|
||||
runId?: string;
|
||||
@@ -77,7 +91,7 @@ interface ProductToolBridgeRequest {
|
||||
workerGeneration: number;
|
||||
runId: string;
|
||||
resourceId: string;
|
||||
toolName: PiProductToolName;
|
||||
toolName: string;
|
||||
input: unknown;
|
||||
}
|
||||
|
||||
@@ -122,7 +136,9 @@ function bridgeRequest(value: unknown): value is BridgeRequest {
|
||||
if (!common) return false;
|
||||
if (value.action === 'subagent.dispatch') return 'request' in value;
|
||||
if (value.action === 'product.invoke') {
|
||||
return isPiProductToolName(value.toolName) && 'input' in value;
|
||||
return typeof value.toolName === 'string'
|
||||
&& PRODUCT_TOOL_NAME_PATTERN.test(value.toolName)
|
||||
&& 'input' in value;
|
||||
}
|
||||
if (value.action === 'changes.bash') return true;
|
||||
if (value.action === 'changes.touched') {
|
||||
@@ -201,6 +217,23 @@ export class PiManagedExtensionHost {
|
||||
if (this.productTools && !input.projectPath?.trim()) {
|
||||
throw new Error('Product tools require a worker project path');
|
||||
}
|
||||
const skillEntries = [...new Map(
|
||||
(input.skillEntries ?? [])
|
||||
.filter((entry) => entry.id.trim() && entry.entryPath.trim())
|
||||
.map((entry) => [entry.id.trim(), {
|
||||
id: entry.id.trim(),
|
||||
entryPath: entry.entryPath.trim().replaceAll('\\', '/'),
|
||||
}]),
|
||||
).values()];
|
||||
const tools = role === 'child'
|
||||
? []
|
||||
: [...new Map(
|
||||
(input.tools ?? []).map((tool) => [tool.name, structuredClone(tool)]),
|
||||
).values()];
|
||||
const allowedToolNames = tools.map(({ name }) => name);
|
||||
const projectWriteLeaseToolNames = tools
|
||||
.filter(({ projectWriteLease }) => projectWriteLease)
|
||||
.map(({ name }) => name);
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
const contextFile = path.join(input.extensionsDir, `worker-${randomUUID()}.json`);
|
||||
const record: WorkerRegistrationRecord = {
|
||||
@@ -209,7 +242,11 @@ export class PiManagedExtensionHost {
|
||||
generation: input.generation,
|
||||
projectId: input.projectId,
|
||||
projectPath: input.projectPath?.trim() || null,
|
||||
skillIds: [...new Set((input.skillIds ?? []).map((id) => id.trim()).filter(Boolean))],
|
||||
skillIds: skillEntries.map(({ id }) => id),
|
||||
...(input.catalogRevision === undefined ? {} : { catalogRevision: input.catalogRevision }),
|
||||
allowedToolNames,
|
||||
tools,
|
||||
projectWriteLeaseToolNames,
|
||||
role,
|
||||
contextFile,
|
||||
runId: role === 'child'
|
||||
@@ -230,6 +267,7 @@ export class PiManagedExtensionHost {
|
||||
MAKELORE_PI_WORKER_ROLE: role,
|
||||
},
|
||||
sensitiveValues: [token],
|
||||
allowedToolNames: [...allowedToolNames],
|
||||
dispose: async () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
@@ -370,6 +408,11 @@ export class PiManagedExtensionHost {
|
||||
this.respond(response, 403, { error: 'Child workers cannot invoke parent product tools' });
|
||||
return;
|
||||
}
|
||||
if (!CORE_PRODUCT_TOOL_NAMES.has(value.toolName)
|
||||
&& !record.allowedToolNames.includes(value.toolName)) {
|
||||
this.respond(response, 403, { error: 'Product tool is not enabled for this worker' });
|
||||
return;
|
||||
}
|
||||
if (!this.productTools || !record.projectPath) {
|
||||
this.respond(response, 503, { error: 'Product tools are unavailable' });
|
||||
return;
|
||||
@@ -544,6 +587,11 @@ export class PiManagedExtensionHost {
|
||||
conversationId: record.conversationId,
|
||||
workerGeneration: record.generation,
|
||||
role: record.role,
|
||||
skillIds: record.skillIds,
|
||||
...(record.catalogRevision === undefined ? {} : { catalogRevision: record.catalogRevision }),
|
||||
allowedToolNames: record.allowedToolNames,
|
||||
tools: record.tools,
|
||||
projectWriteLeaseToolNames: record.projectWriteLeaseToolNames,
|
||||
...(record.runId ? { runId: record.runId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user