Files
makelore/electron/coding-runtime/pi/extension-host.ts
brother7 03a9e866d4 Merge branch 'main' of https://git.nianxx.cn/wangxuming/makelore
# Conflicts:
#	electron/coding-runtime/pi/extensions/makelore-runtime.ts
2026-08-31 15:06:37 +08:00

619 lines
22 KiB
TypeScript

import { randomBytes, randomUUID } from 'node:crypto';
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
import { mkdir, rm } from 'node:fs/promises';
import path from 'node:path';
import { atomicWriteJson } from '../../coding-projects/atomic-json';
import { materializeMakelorePiExtension } from './extensions/makelore-runtime';
import {
PiProjectWriteLeaseCoordinator,
type PiProjectWriteLease,
} from './write-lease';
import type { PiGenerationResourceInput } from './worker-pool';
import {
parsePiSubagentDispatchRequest,
type PiSubagentScheduler,
} from './subagent';
import type { PiProductTools } from './product-tools';
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
import type { PiSkillEntry } from './resource-loader';
import type { EffectivePluginSnapshot } from '../../coding-plugins/effective-resolver';
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',
'task_state',
'changed_file',
'runtime_context',
]);
interface WorkerRegistrationRecord {
token: string;
conversationId: string;
generation: number;
projectId: string;
projectPath: string | null;
skillIds: string[];
catalogRevision?: number;
allowedToolNames: string[];
tools: CodingPluginToolDefinition[];
projectWriteLeaseToolNames: string[];
role: 'parent' | 'child';
effectiveSnapshot?: EffectivePluginSnapshot;
contextFile: string;
runId: string | null;
leases: Map<string, PiProjectWriteLease>;
waiters: Map<string, AbortController>;
}
export interface PiExtensionWorkerRegistration {
extensionPath: string;
env: NodeJS.ProcessEnv;
sensitiveValues: string[];
allowedToolNames: readonly string[];
dispose(): Promise<void>;
}
export interface RegisterPiExtensionWorkerInput {
conversationId: string;
generation: number;
projectId: string;
projectPath?: string;
skillEntries?: readonly PiSkillEntry[];
catalogRevision?: number;
tools?: readonly CodingPluginToolDefinition[];
/** Exact Main-owned resolver output used for this worker generation. */
effectiveSnapshot?: EffectivePluginSnapshot;
extensionsDir: string;
role?: 'parent' | 'child';
runId?: string;
}
interface LeaseBridgeRequest {
action: 'lease.acquire' | 'lease.release';
conversationId: string;
workerGeneration: number;
runId: string;
resourceId: string;
leaseId?: string;
}
interface SubagentBridgeRequest {
action: 'subagent.dispatch';
conversationId: string;
workerGeneration: number;
runId: string;
resourceId: string;
request: unknown;
}
interface ProductToolBridgeRequest {
action: 'product.invoke';
conversationId: string;
workerGeneration: number;
runId: string;
resourceId: string;
toolName: string;
input: unknown;
}
interface ChangeRefreshBridgeRequest {
action: 'changes.bash';
conversationId: string;
workerGeneration: number;
runId: string;
resourceId: string;
}
interface ChangeTouchedBridgeRequest {
action: 'changes.touched';
conversationId: string;
workerGeneration: number;
runId: string;
resourceId: string;
paths: string[];
}
type BridgeRequest = LeaseBridgeRequest
| SubagentBridgeRequest
| ProductToolBridgeRequest
| ChangeRefreshBridgeRequest
| ChangeTouchedBridgeRequest;
export interface PiExtensionSubagentBridge {
scheduler: PiSubagentScheduler;
trackGenerationResource?(input: PiGenerationResourceInput): () => void;
}
function recordValue(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function bridgeRequest(value: unknown): value is BridgeRequest {
if (!recordValue(value)) return false;
const common = typeof value.conversationId === 'string'
&& Number.isSafeInteger(value.workerGeneration)
&& typeof value.runId === 'string'
&& typeof value.resourceId === 'string';
if (!common) return false;
if (value.action === 'subagent.dispatch') return 'request' in value;
if (value.action === 'product.invoke') {
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') {
return Array.isArray(value.paths)
&& value.paths.length > 0
&& value.paths.length <= 200
&& value.paths.every((filePath) => typeof filePath === 'string');
}
return (value.action === 'lease.acquire' || value.action === 'lease.release')
&& (value.leaseId === undefined || typeof value.leaseId === 'string');
}
export class PiManagedExtensionHost {
private readonly leases: PiProjectWriteLeaseCoordinator;
private readonly registrations = new Map<string, WorkerRegistrationRecord>();
private readonly runBindings = new Map<string, string>();
private readonly requestFlights = new Set<Promise<void>>();
private readonly extensionMaterializations = new Map<string, Promise<string>>();
private subagentBridge: PiExtensionSubagentBridge | undefined;
private productTools: PiProductTools | undefined;
private server: Server | null = null;
private bridgeUrl: string | null = null;
private startFlight: Promise<string> | null = null;
private closing = false;
constructor(leases = new PiProjectWriteLeaseCoordinator()) {
this.leases = leases;
}
configureSubagents(bridge: PiExtensionSubagentBridge): void {
this.subagentBridge = bridge;
}
configureProductTools(productTools: PiProductTools): void {
this.productTools = productTools;
}
getDiagnostics(): {
registrations: { parent: number; child: number };
writeLeases: { active: number; waiting: number };
bridgeRequests: number;
} {
const records = [...this.registrations.values()];
return {
registrations: {
parent: records.filter(({ role }) => role === 'parent').length,
child: records.filter(({ role }) => role === 'child').length,
},
writeLeases: {
active: records.reduce((total, { leases }) => total + leases.size, 0),
waiting: records.reduce((total, { waiters }) => total + waiters.size, 0),
},
bridgeRequests: this.requestFlights.size,
};
}
async registerWorker(input: RegisterPiExtensionWorkerInput): Promise<PiExtensionWorkerRegistration> {
if (!Number.isSafeInteger(input.generation) || input.generation <= 0) {
throw new Error('Worker generation must be a positive safe integer');
}
const bridgeUrl = await this.start();
await mkdir(input.extensionsDir, { recursive: true });
let materialization = this.extensionMaterializations.get(input.extensionsDir);
if (!materialization) {
materialization = materializeMakelorePiExtension(input.extensionsDir).catch((error) => {
this.extensionMaterializations.delete(input.extensionsDir);
throw error;
});
this.extensionMaterializations.set(input.extensionsDir, materialization);
}
const extensionPath = await materialization;
const role = input.role ?? 'parent';
if (role === 'child' && !input.runId?.trim()) {
throw new Error('Child extension registration requires a parent run id');
}
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('\\', '/'),
...(entry.packageRoot?.trim() ? { packageRoot: path.resolve(entry.packageRoot) } : {}),
}]),
).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 = {
token,
conversationId: input.conversationId,
generation: input.generation,
projectId: input.projectId,
projectPath: input.projectPath?.trim() || null,
skillIds: skillEntries.map(({ id }) => id),
...(input.catalogRevision === undefined ? {} : { catalogRevision: input.catalogRevision }),
allowedToolNames,
tools,
projectWriteLeaseToolNames,
role,
...(input.effectiveSnapshot ? { effectiveSnapshot: input.effectiveSnapshot } : {}),
contextFile,
runId: role === 'child'
? input.runId as string
: this.runBindings.get(input.conversationId) ?? null,
leases: new Map(),
waiters: new Map(),
};
this.registrations.set(token, record);
await this.writeContext(record);
let disposed = false;
return {
extensionPath,
env: {
MAKELORE_PI_BRIDGE_URL: bridgeUrl,
MAKELORE_PI_WORKER_TOKEN: token,
MAKELORE_PI_CONTEXT_FILE: contextFile,
MAKELORE_PI_WORKER_ROLE: role,
...(record.projectPath ? { MAKELORE_PI_PROJECT_PATH: record.projectPath } : {}),
},
sensitiveValues: [token],
allowedToolNames: [...allowedToolNames],
dispose: async () => {
if (disposed) return;
disposed = true;
this.disposeRecord(record);
await rm(contextFile, { force: true });
},
};
}
async bindRun(conversationId: string, generation: number, runId: string): Promise<void> {
const record = this.findWorker(conversationId, generation);
if (!record) throw new Error('Pi extension worker registration is unavailable');
if (this.productTools && record.projectPath) {
await this.productTools.beginRun({ conversationId, runId, projectPath: record.projectPath });
}
this.releaseWorkerResources(record);
this.runBindings.set(conversationId, runId);
record.runId = runId;
await this.writeContext(record);
}
async clearRun(conversationId: string, generation: number, runId?: string): Promise<void> {
const record = this.findWorker(conversationId, generation);
if (!runId || this.runBindings.get(conversationId) === runId) {
this.runBindings.delete(conversationId);
}
if (!record || (runId && record.runId !== runId)) return;
if (this.productTools && record.runId) {
await this.productTools.settleRun(conversationId, record.runId).catch(() => null);
}
this.releaseWorkerResources(record);
record.runId = null;
await this.writeContext(record);
}
async close(): Promise<void> {
const server = this.server;
this.server = null;
this.bridgeUrl = null;
this.startFlight = null;
this.closing = true;
const closed = server ? new Promise<void>((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
}) : Promise.resolve();
for (const record of [...this.registrations.values()]) this.disposeRecord(record);
this.runBindings.clear();
await Promise.allSettled([...this.requestFlights]);
server?.closeIdleConnections();
await closed;
this.closing = false;
}
private start(): Promise<string> {
if (this.bridgeUrl) return Promise.resolve(this.bridgeUrl);
if (this.startFlight) return this.startFlight;
this.startFlight = new Promise<string>((resolve, reject) => {
const server = createServer((request, response) => {
const flight = this.handle(request, response).finally(() => {
this.requestFlights.delete(flight);
});
this.requestFlights.add(flight);
});
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.removeListener('error', reject);
const address = server.address();
if (!address || typeof address === 'string') {
reject(new Error('Pi extension bridge did not bind a loopback port'));
return;
}
this.server = server;
this.closing = false;
this.bridgeUrl = `http://127.0.0.1:${address.port}/v1/worker`;
resolve(this.bridgeUrl);
});
}).finally(() => {
if (!this.bridgeUrl) this.startFlight = null;
});
return this.startFlight;
}
private async handle(request: IncomingMessage, response: ServerResponse): Promise<void> {
if (request.method !== 'POST' || request.url !== '/v1/worker') {
this.respond(response, 404, { error: 'Not found' });
return;
}
const authorization = request.headers.authorization;
const token = authorization?.startsWith('Bearer ') ? authorization.slice(7) : '';
const record = this.registrations.get(token);
if (!record) {
this.respond(response, 401, { error: 'Worker token is invalid' });
return;
}
try {
const value = await this.readBody(request);
if (!bridgeRequest(value)) {
this.respond(response, 400, { error: 'Bridge request is invalid' });
return;
}
if (value.conversationId !== record.conversationId
|| value.workerGeneration !== record.generation
|| value.runId !== record.runId) {
this.respond(response, 409, { error: 'Worker run identity is stale' });
return;
}
if (!value.resourceId.trim()) {
this.respond(response, 400, { error: 'Bridge resource id is required' });
return;
}
if (value.action === 'subagent.dispatch') {
await this.dispatchSubagents(request, response, record, value);
return;
}
if (value.action === 'changes.bash') {
if (!this.productTools) {
this.respond(response, 503, { error: 'Conversation change tracker is unavailable' });
return;
}
await this.productTools.markBash(record.conversationId, value.runId);
this.respond(response, 200, { marked: true });
return;
}
if (value.action === 'changes.touched') {
if (!this.productTools) {
this.respond(response, 503, { error: 'Conversation change tracker is unavailable' });
return;
}
await this.productTools.recordTouchedPaths(
record.conversationId,
value.runId,
value.paths,
);
this.respond(response, 200, { recorded: true });
return;
}
if (value.action === 'product.invoke') {
if (record.role !== 'parent') {
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;
}
const productResult = await this.productTools.execute(value.toolName, {
conversationId: record.conversationId,
runId: value.runId,
resourceId: value.resourceId,
projectId: record.projectId,
projectPath: record.projectPath,
skillIds: record.skillIds,
...(record.effectiveSnapshot ? { effectiveSnapshot: record.effectiveSnapshot } : {}),
}, value.input);
this.respond(response, 200, { result: productResult });
return;
}
if (value.action === 'lease.release') {
const lease = record.leases.get(value.resourceId);
if (!lease || !value.leaseId || lease.id !== value.leaseId) {
this.respond(response, 409, { error: 'Project write lease is stale' });
return;
}
record.leases.delete(value.resourceId);
lease.release();
this.respond(response, 200, { released: true });
return;
}
if (record.leases.has(value.resourceId) || record.waiters.has(value.resourceId)) {
this.respond(response, 409, { error: 'Project write lease resource already exists' });
return;
}
const controller = new AbortController();
record.waiters.set(value.resourceId, controller);
const cancel = () => {
if (!response.writableEnded) controller.abort();
};
request.once('aborted', cancel);
response.once('close', cancel);
try {
const lease = await this.leases.acquire(
record.projectId,
`${record.conversationId}:${record.generation}:${value.resourceId}`,
controller.signal,
);
if (record.runId !== value.runId || this.registrations.get(token) !== record) {
lease.release();
this.respond(response, 409, { error: 'Worker run identity is stale' });
return;
}
record.leases.set(value.resourceId, lease);
this.respond(response, 200, { leaseId: lease.id });
} finally {
request.removeListener('aborted', cancel);
response.removeListener('close', cancel);
record.waiters.delete(value.resourceId);
}
} catch {
if (!response.writableEnded) this.respond(response, 400, { error: 'Bridge request failed' });
}
}
private async dispatchSubagents(
request: IncomingMessage,
response: ServerResponse,
record: WorkerRegistrationRecord,
value: SubagentBridgeRequest,
): Promise<void> {
if (record.role !== 'parent') {
this.respond(response, 403, { error: 'Child workers cannot dispatch subagents' });
return;
}
if (!this.subagentBridge) {
this.respond(response, 503, { error: 'Subagent scheduler is unavailable' });
return;
}
try {
parsePiSubagentDispatchRequest(value.request);
} catch {
this.respond(response, 400, { error: 'Subagent dispatch request is invalid' });
return;
}
if (record.waiters.has(value.resourceId) || record.leases.has(value.resourceId)) {
this.respond(response, 409, { error: 'Bridge resource already exists' });
return;
}
const controller = new AbortController();
record.waiters.set(value.resourceId, controller);
const cancel = () => controller.abort();
request.once('aborted', cancel);
response.once('close', cancel);
let untrack = () => undefined;
try {
untrack = this.subagentBridge.trackGenerationResource?.({
conversationId: record.conversationId,
kind: 'child',
id: value.resourceId,
cancel,
}) ?? untrack;
response.writeHead(200, {
'content-type': 'application/x-ndjson; charset=utf-8',
...(this.closing ? { connection: 'close' } : {}),
});
const result = await this.subagentBridge.scheduler.dispatch({
conversationId: record.conversationId,
workerGeneration: record.generation,
runId: value.runId,
projectId: record.projectId,
request: value.request,
}, {
signal: controller.signal,
onUpdate: (details) => {
if (!response.writableEnded && !response.destroyed) {
response.write(`${JSON.stringify({ details })}\n`);
}
},
});
if (!response.writableEnded && !response.destroyed) {
response.end(`${JSON.stringify({ details: result.details, done: true })}\n`);
}
} catch {
if (!response.headersSent) this.respond(response, 400, { error: 'Subagent dispatch failed' });
else if (!response.writableEnded && !response.destroyed) response.end();
} finally {
request.removeListener('aborted', cancel);
response.removeListener('close', cancel);
record.waiters.delete(value.resourceId);
untrack();
}
}
private readBody(request: IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let bytes = 0;
request.on('data', (chunk: Buffer) => {
bytes += chunk.length;
if (bytes > MAX_REQUEST_BYTES) {
reject(new Error('Bridge request is too large'));
request.destroy();
return;
}
chunks.push(chunk);
});
request.once('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
} catch (error) {
reject(error);
}
});
request.once('error', reject);
});
}
private respond(response: ServerResponse, status: number, body: Record<string, unknown>): void {
if (response.writableEnded) return;
response.writeHead(status, {
'content-type': 'application/json; charset=utf-8',
...(this.closing ? { connection: 'close' } : {}),
});
response.end(JSON.stringify(body));
}
private findWorker(conversationId: string, generation: number): WorkerRegistrationRecord | undefined {
return [...this.registrations.values()].find((record) => (
record.role === 'parent'
&& record.conversationId === conversationId && record.generation === generation
));
}
private async writeContext(record: WorkerRegistrationRecord): Promise<void> {
await atomicWriteJson(record.contextFile, {
conversationId: record.conversationId,
workerGeneration: record.generation,
role: record.role,
skillIds: record.skillIds,
...(record.catalogRevision === undefined ? {} : { catalogRevision: record.catalogRevision }),
...(record.effectiveSnapshot ? { effectivePluginSnapshot: record.effectiveSnapshot } : {}),
allowedToolNames: record.allowedToolNames,
tools: record.tools,
projectWriteLeaseToolNames: record.projectWriteLeaseToolNames,
...(record.runId ? { runId: record.runId } : {}),
});
}
private disposeRecord(record: WorkerRegistrationRecord): void {
if (this.registrations.get(record.token) !== record) return;
this.registrations.delete(record.token);
this.releaseWorkerResources(record);
}
private releaseWorkerResources(record: WorkerRegistrationRecord): void {
for (const controller of record.waiters.values()) controller.abort();
record.waiters.clear();
for (const lease of record.leases.values()) lease.release();
record.leases.clear();
}
}