feat: add Pi provider managed resources

This commit is contained in:
2026-08-22 21:48:00 +08:00
parent 81d8ad1b6b
commit 161f3f471b
31 changed files with 1581 additions and 40 deletions

View File

@@ -0,0 +1,102 @@
export interface PiManagedInputRevision {
provider: number;
resources: number;
}
export type PiManagedInputAction =
| { action: 'reuse'; revision: PiManagedInputRevision }
| { action: 'rebuild-before-prompt'; revision: PiManagedInputRevision }
| { action: 'rebuild-after-settled'; revision: PiManagedInputRevision }
| { action: 'defer-until-settled'; revision: PiManagedInputRevision };
interface WorkerRevisionState {
applied: PiManagedInputRevision;
activeRun: PiManagedInputRevision | null;
}
function copyRevision(revision: PiManagedInputRevision): PiManagedInputRevision {
return { provider: revision.provider, resources: revision.resources };
}
function revisionsEqual(left: PiManagedInputRevision, right: PiManagedInputRevision): boolean {
return left.provider === right.provider && left.resources === right.resources;
}
export class PiManagedInputRevisionCoordinator {
private currentRevision: PiManagedInputRevision = { provider: 1, resources: 1 };
private readonly workers = new Map<string, WorkerRevisionState>();
get current(): PiManagedInputRevision {
return copyRevision(this.currentRevision);
}
markProviderStale(): PiManagedInputRevision {
this.currentRevision = {
provider: this.currentRevision.provider + 1,
resources: this.currentRevision.resources,
};
return this.current;
}
markResourcesStale(): PiManagedInputRevision {
this.currentRevision = {
provider: this.currentRevision.provider,
resources: this.currentRevision.resources + 1,
};
return this.current;
}
registerWorker(workerId: string, applied: PiManagedInputRevision = this.currentRevision): void {
if (!workerId.trim()) throw new Error('Worker id is required');
if (this.workers.has(workerId)) throw new Error(`Worker is already registered: ${workerId}`);
this.workers.set(workerId, { applied: copyRevision(applied), activeRun: null });
}
removeWorker(workerId: string): void {
this.workers.delete(workerId);
}
beforePrompt(workerId: string): PiManagedInputAction {
const worker = this.requireWorker(workerId);
if (worker.activeRun) {
return revisionsEqual(worker.applied, this.currentRevision)
? { action: 'reuse', revision: copyRevision(worker.activeRun) }
: { action: 'defer-until-settled', revision: copyRevision(worker.activeRun) };
}
return revisionsEqual(worker.applied, this.currentRevision)
? { action: 'reuse', revision: copyRevision(worker.applied) }
: { action: 'rebuild-before-prompt', revision: this.current };
}
applyCurrentRevision(workerId: string): PiManagedInputRevision {
const worker = this.requireWorker(workerId);
if (worker.activeRun) throw new Error('Cannot rebuild managed input while a run is active');
worker.applied = this.current;
return copyRevision(worker.applied);
}
beginRun(workerId: string): PiManagedInputRevision {
const worker = this.requireWorker(workerId);
if (worker.activeRun) throw new Error('Worker already has an active run');
if (!revisionsEqual(worker.applied, this.currentRevision)) {
throw new Error('Worker managed input is stale');
}
worker.activeRun = copyRevision(worker.applied);
return copyRevision(worker.activeRun);
}
settleRun(workerId: string): PiManagedInputAction {
const worker = this.requireWorker(workerId);
if (!worker.activeRun) throw new Error('Worker has no active run');
worker.activeRun = null;
return revisionsEqual(worker.applied, this.currentRevision)
? { action: 'reuse', revision: copyRevision(worker.applied) }
: { action: 'rebuild-after-settled', revision: this.current };
}
private requireWorker(workerId: string): WorkerRevisionState {
const worker = this.workers.get(workerId);
if (!worker) throw new Error(`Worker is not registered: ${workerId}`);
return worker;
}
}

View File

@@ -0,0 +1,529 @@
import type {
ModelSummary,
ProviderAccount,
ProviderModelEntry,
ProviderProtocol,
ProviderSecret,
} from '../../shared/providers/types';
import {
getProviderBackendConfig,
getProviderDefaultModel,
getProviderDefinition,
} from '../../shared/providers/registry';
import type { ProductModelRef } from '../contracts';
import { atomicWriteJson } from '../../coding-projects/atomic-json';
import {
NIANCODE_USER_MODEL_ACCOUNT_ID,
normalizeImportedUserModelId,
selectUserModelRuntimeAccounts,
} from '../../../shared/user-model-config';
import { getImportedModelProfile } from '../../../shared/imported-model-profile';
const PI_ENV_PREFIX = 'MAKELORE_PI';
const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway';
const WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE = 'works_square_ai_gateway_proxy';
export const PI_PROVIDER_APIS = [
'openai-completions',
'openai-responses',
'anthropic-messages',
'google-generative-ai',
] as const;
export type PiProviderApi = (typeof PI_PROVIDER_APIS)[number];
export interface PiProviderModelDescriptor {
id: string;
name: string;
input: Array<'text' | 'image'>;
reasoning: boolean;
contextWindow?: number;
maxOutputTokens?: number;
compat?: {
thinkingFormat: 'openrouter';
sessionAffinityFormat: 'openrouter';
};
}
export interface PiProviderDescriptor {
accountId: string;
runtimeProviderId: string;
api: PiProviderApi;
baseUrl?: string;
headers: Record<string, string>;
apiKeyEnv?: string;
models: PiProviderModelDescriptor[];
}
export interface PiModelsFile {
providers: Record<string, {
baseUrl?: string;
api: PiProviderApi;
apiKey?: string;
headers?: Record<string, string>;
models: Array<{
id: string;
name: string;
reasoning: boolean;
input: Array<'text' | 'image'>;
contextWindow?: number;
maxTokens?: number;
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
compat?: PiProviderModelDescriptor['compat'];
}>;
}>;
}
export interface PiProviderCatalogSummary {
providerCount: number;
providers: Array<{
runtimeProviderId: string;
api: PiProviderApi;
baseUrl?: string;
modelIds: string[];
imageInputModelIds: string[];
headerNames: string[];
hasCredentialReference: boolean;
}>;
}
export interface PiProviderCatalogResult {
descriptors: PiProviderDescriptor[];
modelsFile: PiModelsFile;
summary: PiProviderCatalogSummary;
}
export interface BuildPiProviderCatalogOptions {
accounts: readonly ProviderAccount[];
modelSummaries?: readonly ModelSummary[];
}
export async function buildPiProviderCatalogFromProviderService(
modelSummaries: readonly ModelSummary[] = [],
): Promise<PiProviderCatalogResult> {
const { getProviderService } = await import('../../services/providers/provider-service');
return buildPiProviderCatalog({
accounts: await getProviderService().listAccounts(),
modelSummaries,
});
}
export interface PiProviderSelection {
accountId: string;
runtimeProviderId: string;
modelId: string;
thinkingLevel: ProductModelRef['thinkingLevel'];
input: Array<'text' | 'image'>;
contextWindow?: number;
maxOutputTokens?: number;
}
export interface BuildPiWorkerCredentialProjectionOptions {
account: ProviderAccount;
descriptor: PiProviderDescriptor;
resolveCredential: (account: ProviderAccount) => Promise<string | null>;
localProxyCredential?: string;
}
export interface PiWorkerCredentialProjection {
env: Record<string, string>;
sensitiveValues: string[];
}
export interface PiWorkerCredentialProjectionSummary {
envKeys: string[];
sensitiveValueCount: number;
}
export class PiProviderConfigError extends Error {
constructor(
public readonly code: 'PROVIDER_INVALID' | 'PROVIDER_AUTH_REQUIRED' | 'MODEL_UNAVAILABLE',
message: string,
) {
super(message);
}
}
export function credentialValueForProviderSecret(secret: ProviderSecret | null): string | null {
if (!secret) return null;
if (secret.type === 'api_key') return secret.apiKey.trim() || null;
if (secret.type === 'oauth') return secret.accessToken.trim() || null;
return secret.apiKey?.trim() || null;
}
export async function resolvePiProviderCredentialFromSecretStore(
account: ProviderAccount,
): Promise<string | null> {
const { getProviderSecret } = await import('../../services/secrets/secret-store');
return credentialValueForProviderSecret(await getProviderSecret(account.id));
}
function accountHex(accountId: string): string {
const normalized = accountId.trim();
if (!normalized) throw new PiProviderConfigError('PROVIDER_INVALID', 'Provider account id is required');
return Buffer.from(normalized, 'utf8').toString('hex');
}
export function resolvePiRuntimeProviderId(accountId: string): string {
return `makelore-account-${accountHex(accountId)}`;
}
function apiKeyEnvForAccount(accountId: string): string {
return `${PI_ENV_PREFIX}_ACCOUNT_${accountHex(accountId).toUpperCase()}_API_KEY`;
}
function headerEnvForAccount(accountId: string, headerName: string): string {
const nameHex = Buffer.from(headerName.toLowerCase(), 'utf8').toString('hex').toUpperCase();
return `${PI_ENV_PREFIX}_ACCOUNT_${accountHex(accountId).toUpperCase()}_HEADER_${nameHex}`;
}
function normalizeBaseUrl(value: string | undefined): string | undefined {
const normalized = value?.trim().replace(/\/+$/, '');
return normalized || undefined;
}
function normalizeWorksGatewayBaseUrl(value: string | undefined): string | undefined {
const normalized = normalizeBaseUrl(value);
if (!normalized) return undefined;
try {
const parsed = new URL(normalized);
const pathname = parsed.pathname.replace(/\/+$/, '');
if (!pathname || pathname === '/') parsed.pathname = '/v1';
return parsed.toString().replace(/\/+$/, '');
} catch {
return normalized;
}
}
function defaultBaseUrlForVendor(vendorId: string): string | undefined {
if (vendorId === 'anthropic') return 'https://api.anthropic.com';
if (vendorId === 'google') return 'https://generativelanguage.googleapis.com/v1beta';
return getProviderBackendConfig(vendorId)?.baseUrl
?? getProviderDefinition(vendorId)?.defaultBaseUrl;
}
function baseUrlForAccount(account: ProviderAccount): string | undefined {
const baseUrl = account.baseUrl
?? account.metadata?.worksSquareOneApiBaseUrl
?? defaultBaseUrlForVendor(account.vendorId);
return account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE
? normalizeWorksGatewayBaseUrl(baseUrl)
: normalizeBaseUrl(baseUrl);
}
function protocolForAccount(account: ProviderAccount): ProviderProtocol | 'google-generative-ai' | undefined {
if (account.apiProtocol) return account.apiProtocol;
const configured = getProviderBackendConfig(account.vendorId)?.api;
if (configured) return configured;
if (account.vendorId === 'anthropic') return 'anthropic-messages';
if (account.vendorId === 'google') return 'google-generative-ai';
if (account.vendorId === 'openai') return 'openai-responses';
if (account.vendorId === 'openrouter') return 'openrouter';
return undefined;
}
function mapProtocol(protocol: ReturnType<typeof protocolForAccount>): {
api: PiProviderApi;
compat?: PiProviderModelDescriptor['compat'];
} {
if (protocol === 'openrouter') {
return {
api: 'openai-completions',
compat: {
thinkingFormat: 'openrouter',
sessionAffinityFormat: 'openrouter',
},
};
}
if (protocol && PI_PROVIDER_APIS.includes(protocol as PiProviderApi)) {
return { api: protocol as PiProviderApi };
}
throw new PiProviderConfigError('PROVIDER_INVALID', 'Provider protocol is not supported by Pi');
}
function normalizeModelId(rawModelId: string | undefined, account: ProviderAccount): string | undefined {
const modelId = rawModelId?.trim();
if (!modelId) return undefined;
const runtimePrefix = `${resolvePiRuntimeProviderId(account.id)}/`;
const unqualified = modelId.startsWith(runtimePrefix)
? modelId.slice(runtimePrefix.length)
: modelId;
return account.id === NIANCODE_USER_MODEL_ACCOUNT_ID
? normalizeImportedUserModelId(unqualified)
: unqualified;
}
function backendModelEntries(account: ProviderAccount): Map<string, ProviderModelEntry> {
return new Map((getProviderBackendConfig(account.vendorId)?.models ?? []).map((model) => [model.id, model]));
}
function modelIdsForAccount(account: ProviderAccount): string[] {
const seen = new Set<string>();
const result: string[] = [];
const configuredModels = getProviderBackendConfig(account.vendorId)?.models ?? [];
for (const rawModelId of [
account.model ?? getProviderDefaultModel(account.vendorId),
...(account.fallbackModels ?? []),
...(account.metadata?.customModels ?? []),
...configuredModels.map((model) => model.id),
]) {
const modelId = normalizeModelId(rawModelId, account);
if (!modelId || seen.has(modelId)) continue;
seen.add(modelId);
result.push(modelId);
}
return result;
}
function finitePositiveInteger(value: unknown): number | undefined {
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
? value
: undefined;
}
function modelDescriptor(
account: ProviderAccount,
modelId: string,
summaries: readonly ModelSummary[],
compat: PiProviderModelDescriptor['compat'],
backendModels: Map<string, ProviderModelEntry>,
): PiProviderModelDescriptor {
const summary = summaries.find((candidate) => (
candidate.id === modelId
&& (candidate.accountId === account.id || (!candidate.accountId && candidate.vendorId === account.vendorId))
));
const backend = backendModels.get(modelId);
const profile = getImportedModelProfile(modelId);
const backendInput = Array.isArray(backend?.input)
? backend.input.filter((input): input is 'text' | 'image' => input === 'text' || input === 'image')
: [];
const supportsImage = Boolean(
summary?.supportsVision
|| profile?.modalities.input.includes('image')
|| backendInput.includes('image'),
);
const contextWindow = finitePositiveInteger(summary?.contextWindow)
?? finitePositiveInteger(profile?.limit?.context)
?? finitePositiveInteger(backend?.contextWindow);
const maxOutputTokens = finitePositiveInteger(profile?.limit?.output)
?? finitePositiveInteger(backend?.maxTokens);
return {
id: modelId,
name: summary?.name || (typeof backend?.name === 'string' && backend.name.trim()) || modelId,
input: supportsImage ? ['text', 'image'] : ['text'],
reasoning: summary?.supportsReasoning === true || backend?.reasoning === true,
...(contextWindow ? { contextWindow } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
...(compat ? { compat } : {}),
};
}
function rawHeadersForAccount(account: ProviderAccount): Record<string, string> {
const headers: Record<string, string> = {};
for (const [name, value] of Object.entries({
...(getProviderBackendConfig(account.vendorId)?.headers ?? {}),
...(account.headers ?? {}),
})) {
const duplicate = Object.keys(headers).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
if (duplicate) delete headers[duplicate];
headers[name] = value;
}
return headers;
}
function headerNamesForAccount(account: ProviderAccount): string[] {
const names = Object.keys(rawHeadersForAccount(account));
if (
account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE
&& !names.some((name) => name.toLowerCase() === 'authorization')
) {
names.push('Authorization');
}
return names.sort((left, right) => left.toLowerCase().localeCompare(right.toLowerCase()));
}
function descriptorForAccount(
account: ProviderAccount,
summaries: readonly ModelSummary[],
): PiProviderDescriptor {
const runtimeProviderId = resolvePiRuntimeProviderId(account.id);
const mapping = mapProtocol(protocolForAccount(account));
const baseUrl = baseUrlForAccount(account);
if (!baseUrl) {
throw new PiProviderConfigError(
'PROVIDER_INVALID',
`Provider account ${account.id} has no runtime base URL`,
);
}
const apiKeyEnv = apiKeyEnvForAccount(account.id);
const models = modelIdsForAccount(account).map((modelId) => modelDescriptor(
account,
modelId,
summaries,
mapping.compat,
backendModelEntries(account),
));
if (models.length === 0) {
throw new PiProviderConfigError(
'PROVIDER_INVALID',
`Provider account ${account.id} has no configured model`,
);
}
const headers = Object.fromEntries(headerNamesForAccount(account).map((headerName) => [
headerName,
`$${headerEnvForAccount(account.id, headerName)}`,
]));
return {
accountId: account.id,
runtimeProviderId,
api: mapping.api,
baseUrl,
headers,
apiKeyEnv,
models,
};
}
function modelsFileForDescriptors(descriptors: readonly PiProviderDescriptor[]): PiModelsFile {
return {
providers: Object.fromEntries(descriptors.map((descriptor) => [
descriptor.runtimeProviderId,
{
...(descriptor.baseUrl ? { baseUrl: descriptor.baseUrl } : {}),
api: descriptor.api,
...(descriptor.apiKeyEnv ? { apiKey: `$${descriptor.apiKeyEnv}` } : {}),
...(Object.keys(descriptor.headers).length > 0 ? { headers: { ...descriptor.headers } } : {}),
models: descriptor.models.map((model) => ({
id: model.id,
name: model.name,
reasoning: model.reasoning,
input: [...model.input],
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
...(model.maxOutputTokens ? { maxTokens: model.maxOutputTokens } : {}),
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
...(model.compat ? { compat: { ...model.compat } } : {}),
})),
},
])),
};
}
export function buildPiProviderCatalog(
options: BuildPiProviderCatalogOptions,
): PiProviderCatalogResult {
const accounts = selectUserModelRuntimeAccounts([...options.accounts]).filter((account) => account.enabled);
const accountIds = new Set<string>();
const descriptors = accounts.map((account) => {
if (accountIds.has(account.id)) {
throw new PiProviderConfigError('PROVIDER_INVALID', `Duplicate Provider account id: ${account.id}`);
}
accountIds.add(account.id);
return descriptorForAccount(account, options.modelSummaries ?? []);
});
return {
descriptors,
modelsFile: modelsFileForDescriptors(descriptors),
summary: {
providerCount: descriptors.length,
providers: descriptors.map((descriptor) => ({
runtimeProviderId: descriptor.runtimeProviderId,
api: descriptor.api,
...(descriptor.baseUrl ? { baseUrl: descriptor.baseUrl } : {}),
modelIds: descriptor.models.map((model) => model.id),
imageInputModelIds: descriptor.models
.filter((model) => model.input.includes('image'))
.map((model) => model.id),
headerNames: Object.keys(descriptor.headers).sort(),
hasCredentialReference: Boolean(descriptor.apiKeyEnv),
})),
},
};
}
export function selectPiProviderModel(
catalog: PiProviderCatalogResult,
modelRef: ProductModelRef,
): PiProviderSelection {
const descriptor = catalog.descriptors.find((candidate) => candidate.accountId === modelRef.accountId);
const model = descriptor?.models.find((candidate) => candidate.id === modelRef.modelId);
if (!descriptor || !model) {
throw new PiProviderConfigError(
'MODEL_UNAVAILABLE',
'The selected Provider account or model is unavailable',
);
}
return {
accountId: descriptor.accountId,
runtimeProviderId: descriptor.runtimeProviderId,
modelId: model.id,
thinkingLevel: modelRef.thinkingLevel,
input: [...model.input],
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
...(model.maxOutputTokens ? { maxOutputTokens: model.maxOutputTokens } : {}),
};
}
export async function writePiProviderCatalog(
filePath: string,
catalog: PiProviderCatalogResult,
modelRef?: ProductModelRef,
): Promise<void> {
if (modelRef) selectPiProviderModel(catalog, modelRef);
await atomicWriteJson(filePath, catalog.modelsFile);
}
function replaceOpenCodeEnvReferences(value: string, credential: string | null): string {
if (!value.includes('{env:')) return value;
if (!credential) {
throw new PiProviderConfigError('PROVIDER_AUTH_REQUIRED', 'Provider credential is unavailable');
}
return value.replace(/\{env:[^}]+\}/g, credential);
}
export async function buildPiWorkerCredentialProjection(
options: BuildPiWorkerCredentialProjectionOptions,
): Promise<PiWorkerCredentialProjection> {
if (options.account.id !== options.descriptor.accountId) {
throw new PiProviderConfigError('PROVIDER_INVALID', 'Provider account does not match descriptor');
}
const useLocalProxy = options.account.metadata?.worksSquareCredentialMode
=== WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE;
const resolved = useLocalProxy
? options.localProxyCredential?.trim() || null
: await options.resolveCredential(options.account);
const credential = resolved?.trim()
|| (options.account.authMode === 'local' ? 'local-provider' : null);
if (!credential) {
throw new PiProviderConfigError('PROVIDER_AUTH_REQUIRED', 'Provider credential is unavailable');
}
const env: Record<string, string> = {};
if (options.descriptor.apiKeyEnv) env[options.descriptor.apiKeyEnv] = credential;
const accountHeaders = rawHeadersForAccount(options.account);
for (const headerName of Object.keys(options.descriptor.headers)) {
const sourceEntry = Object.entries(accountHeaders).find(([name]) => (
name.toLowerCase() === headerName.toLowerCase()
));
let value = sourceEntry?.[1];
if (!value && headerName.toLowerCase() === 'authorization'
&& options.account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE) {
value = `Bearer ${credential}`;
}
if (value === undefined) {
throw new PiProviderConfigError('PROVIDER_INVALID', `Provider header is unavailable: ${headerName}`);
}
env[headerEnvForAccount(options.account.id, headerName)] = replaceOpenCodeEnvReferences(value, credential);
}
return {
env,
sensitiveValues: [...new Set(Object.values(env).filter(Boolean))],
};
}
export function summarizePiWorkerCredentialProjection(
projection: PiWorkerCredentialProjection,
): PiWorkerCredentialProjectionSummary {
return {
envKeys: Object.keys(projection.env).sort(),
sensitiveValueCount: projection.sensitiveValues.length,
};
}

View File

@@ -0,0 +1,44 @@
export interface PiProviderAuthRecoveryOptions<T> {
accountId: string;
operation: (attempt: 0 | 1) => Promise<T>;
isAuthenticationError: (error: unknown) => boolean;
refreshCredential: () => Promise<void>;
reopenWorker: () => Promise<void>;
}
export class PiProviderRefreshCoordinator {
private readonly refreshes = new Map<string, Promise<void>>();
get pendingAccountCount(): number {
return this.refreshes.size;
}
async refreshAccount(accountId: string, refresh: () => Promise<void>): Promise<void> {
const normalizedAccountId = accountId.trim();
if (!normalizedAccountId) throw new Error('Provider account id is required');
let pending = this.refreshes.get(normalizedAccountId);
if (!pending) {
pending = Promise.resolve()
.then(refresh)
.finally(() => {
if (this.refreshes.get(normalizedAccountId) === pending) {
this.refreshes.delete(normalizedAccountId);
}
});
this.refreshes.set(normalizedAccountId, pending);
}
await pending;
}
async withSingleAuthRecovery<T>(options: PiProviderAuthRecoveryOptions<T>): Promise<T> {
try {
return await options.operation(0);
} catch (error) {
if (!options.isAuthenticationError(error)) throw error;
}
await this.refreshAccount(options.accountId, options.refreshCredential);
await options.reopenWorker();
return await options.operation(1);
}
}

View File

@@ -0,0 +1,193 @@
import { mkdir, stat } from 'node:fs/promises';
import path from 'node:path';
import {
BUNDLED_CODING_SKILL_IDS,
type BundledCodingSkillId,
} from '../../../shared/coding-skills';
import { atomicWriteJson, atomicWriteText } from '../../coding-projects/atomic-json';
import type { PiProviderSelection } from './provider-config';
import type { PiManagedInputRevision } from './managed-input-revision';
const MANAGED_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
export interface BundledCodingSkillsPathInput {
isPackaged: boolean;
resourcesPath: string;
appPath: string;
}
export interface PiManagedPaths {
rootDir: string;
configDir: string;
modelsFile: string;
sessionsDir: string;
promptsDir: string;
extensionsDir: string;
logsDir: string;
trashDir: string;
}
export interface MaterializePiAgentResourcesOptions {
userDataDir: string;
projectId: string;
agentId: string;
prompt: string;
skillIds: readonly string[];
bundledSkillsDir: string;
revision: PiManagedInputRevision;
}
export interface PiAgentResourceManifest {
schemaVersion: 1;
projectId: string;
agentId: string;
promptFile: string;
skillIds: BundledCodingSkillId[];
revision: PiManagedInputRevision;
}
export interface PiAgentResourceSnapshot {
paths: PiManagedPaths;
projectSessionsDir: string;
promptPath: string;
manifestPath: string;
skillIds: BundledCodingSkillId[];
skillPaths: string[];
revision: PiManagedInputRevision;
summary: {
projectId: string;
agentId: string;
skillIds: BundledCodingSkillId[];
revision: PiManagedInputRevision;
};
}
function managedSegment(value: string, name: string): string {
const normalized = value.trim();
if (!MANAGED_SEGMENT_PATTERN.test(normalized)) {
throw new Error(`${name} is not a valid managed resource segment`);
}
return normalized;
}
export function resolveBundledCodingSkillsDir(input: BundledCodingSkillsPathInput): string {
return input.isPackaged
? path.join(input.resourcesPath, 'resources', 'coding-skills')
: path.join(input.appPath, 'resources', 'coding-skills');
}
export function getPiManagedPaths(userDataDir: string): PiManagedPaths {
const rootDir = path.join(path.resolve(userDataDir), 'coding-runtime', 'pi');
const configDir = path.join(rootDir, 'config');
return {
rootDir,
configDir,
modelsFile: path.join(configDir, 'models.json'),
sessionsDir: path.join(rootDir, 'sessions'),
promptsDir: path.join(rootDir, 'prompts'),
extensionsDir: path.join(rootDir, 'extensions'),
logsDir: path.join(rootDir, 'logs'),
trashDir: path.join(rootDir, 'trash'),
};
}
export async function ensurePiManagedPaths(userDataDir: string): Promise<PiManagedPaths> {
const paths = getPiManagedPaths(userDataDir);
await Promise.all([
mkdir(paths.configDir, { recursive: true }),
mkdir(paths.sessionsDir, { recursive: true }),
mkdir(paths.promptsDir, { recursive: true }),
mkdir(paths.extensionsDir, { recursive: true }),
mkdir(paths.logsDir, { recursive: true }),
mkdir(paths.trashDir, { recursive: true }),
]);
return paths;
}
function normalizeSkillIds(skillIds: readonly string[]): BundledCodingSkillId[] {
const result: BundledCodingSkillId[] = [];
const seen = new Set<string>();
for (const rawSkillId of skillIds) {
const skillId = rawSkillId.trim();
if (!BUNDLED_CODING_SKILL_IDS.includes(skillId as BundledCodingSkillId)) {
throw new Error(`Unknown bundled coding skill: ${skillId || '(empty)'}`);
}
if (seen.has(skillId)) continue;
seen.add(skillId);
result.push(skillId as BundledCodingSkillId);
}
return result;
}
export async function resolveExplicitCodingSkillPaths(
bundledSkillsDir: string,
skillIds: readonly string[],
): Promise<{ skillIds: BundledCodingSkillId[]; skillPaths: string[] }> {
const normalizedSkillIds = normalizeSkillIds(skillIds);
const root = path.resolve(bundledSkillsDir);
const skillPaths = normalizedSkillIds.map((skillId) => path.join(root, skillId, 'SKILL.md'));
await Promise.all(skillPaths.map(async (skillPath) => {
const metadata = await stat(skillPath);
if (!metadata.isFile()) throw new Error(`Bundled coding skill entry is not a file: ${skillPath}`);
}));
return { skillIds: normalizedSkillIds, skillPaths };
}
export async function materializePiAgentResources(
options: MaterializePiAgentResourcesOptions,
): Promise<PiAgentResourceSnapshot> {
const projectId = managedSegment(options.projectId, 'Project id');
const agentId = managedSegment(options.agentId, 'Agent id');
const paths = await ensurePiManagedPaths(options.userDataDir);
const projectSessionsDir = path.join(paths.sessionsDir, projectId);
const projectPromptsDir = path.join(paths.promptsDir, projectId);
await Promise.all([
mkdir(projectSessionsDir, { recursive: true }),
mkdir(projectPromptsDir, { recursive: true }),
]);
const { skillIds, skillPaths } = await resolveExplicitCodingSkillPaths(
options.bundledSkillsDir,
options.skillIds,
);
const promptPath = path.join(projectPromptsDir, `${agentId}.md`);
const manifestPath = path.join(projectPromptsDir, `${agentId}.manifest.json`);
const manifest: PiAgentResourceManifest = {
schemaVersion: 1,
projectId,
agentId,
promptFile: path.basename(promptPath),
skillIds: [...skillIds],
revision: { ...options.revision },
};
await atomicWriteText(promptPath, options.prompt);
await atomicWriteJson(manifestPath, manifest);
return {
paths,
projectSessionsDir,
promptPath,
manifestPath,
skillIds: [...skillIds],
skillPaths,
revision: { ...options.revision },
summary: {
projectId,
agentId,
skillIds: [...skillIds],
revision: { ...options.revision },
},
};
}
export function buildPiManagedInputArgs(
selection: PiProviderSelection,
resources: PiAgentResourceSnapshot,
): string[] {
const args = [
'--provider', selection.runtimeProviderId,
'--model', selection.modelId,
'--thinking', selection.thinkingLevel,
'--system-prompt', resources.promptPath,
];
for (const skillPath of resources.skillPaths) args.push('--skill', skillPath);
return args;
}

View File

@@ -18,6 +18,27 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 10_000;
const DEFAULT_SHUTDOWN_GRACE_MS = 3_000;
const DEFAULT_DIAGNOSTIC_BYTES = 16_000;
const ANSI_COLOR_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
const PI_INHERITED_ENV_KEYS = [
'APPDATA',
'COMSPEC',
'HOME',
'LANG',
'LC_ALL',
'LD_LIBRARY_PATH',
'LOCALAPPDATA',
'NODE_EXTRA_CA_CERTS',
'PATH',
'PATHEXT',
'SSL_CERT_DIR',
'SSL_CERT_FILE',
'SYSTEMROOT',
'TEMP',
'TMP',
'TMPDIR',
'TZ',
'USERPROFILE',
'WINDIR',
] as const;
export type PiWorkerStopResult = {
mode: 'not-started' | 'stdin-close' | 'forced-tree-kill';
@@ -67,13 +88,50 @@ export function sanitizePiDiagnostic(
.replace(ANSI_COLOR_PATTERN, '')
.replace(/(authorization\s*[:=]\s*(?:bearer\s+)?)[^\s,;]+/gi, '$1[REDACTED]')
.replace(/((?:x-api-key|api[_-]?key|token|secret)\s*[:=]\s*)[^\s,;]+/gi, '$1[REDACTED]');
for (const value of sensitiveValues) {
if (value.length < 4) continue;
const uniqueSensitiveValues = [...new Set(sensitiveValues.filter(Boolean))]
.sort((left, right) => right.length - left.length);
for (const value of uniqueSensitiveValues) {
sanitized = sanitized.split(value).join('[REDACTED]');
}
return sanitized;
}
export function buildPiWorkerEnvironment(
configDir: string,
overlay: NodeJS.ProcessEnv = {},
inherited: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
for (const key of PI_INHERITED_ENV_KEYS) {
const exact = inherited[key];
if (exact !== undefined) {
env[key] = exact;
continue;
}
const matchingKey = Object.keys(inherited).find((candidate) => candidate.toUpperCase() === key);
if (matchingKey && inherited[matchingKey] !== undefined) env[matchingKey] = inherited[matchingKey];
}
return {
...env,
...overlay,
ELECTRON_RUN_AS_NODE: '1',
PI_CODING_AGENT_DIR: configDir,
PI_OFFLINE: '1',
PI_TELEMETRY: '0',
};
}
function assertSensitiveValuesAbsentFromArgs(
args: readonly string[],
sensitiveValues: readonly string[] = [],
): void {
for (const value of sensitiveValues) {
if (value && args.some((argument) => argument.includes(value))) {
throw new Error('Pi worker arguments contain a sensitive value');
}
}
}
function boundedUtf8Tail(source: string, maxBytes: number): string {
const bytes = Buffer.from(source, 'utf8');
if (bytes.length <= maxBytes) return source;
@@ -158,19 +216,14 @@ export class PiWorkerProcess {
async start(): Promise<this> {
if (this.child) throw new Error('Pi worker process already started');
const generation = this.generationValue;
const args = buildPiRpcArgs(this.options.sessionDir, this.options.additionalArgs);
assertSensitiveValuesAbsentFromArgs(args, this.options.sensitiveValues);
const child = spawn(
this.options.executablePath,
[this.options.cliPath, ...buildPiRpcArgs(this.options.sessionDir, this.options.additionalArgs)],
[this.options.cliPath, ...args],
{
cwd: this.options.cwd,
env: {
...process.env,
...this.options.env,
ELECTRON_RUN_AS_NODE: '1',
PI_CODING_AGENT_DIR: this.options.configDir,
PI_OFFLINE: '1',
PI_TELEMETRY: '0',
},
env: buildPiWorkerEnvironment(this.options.configDir, this.options.env),
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
detached: platform() !== 'win32',