feat(coding): add model tools and device packages

This commit is contained in:
2026-09-02 18:35:26 +08:00
parent 841273b433
commit 664b8823a0
60 changed files with 3847 additions and 2068 deletions

View File

@@ -0,0 +1,224 @@
import { readFile, readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import type {
DevicePackageKind,
DevicePackageSkillEntry,
} from '../../shared/device-packages';
export type {
DevicePackageIndexV1,
DevicePackageKind,
DevicePackageRecordV1,
DevicePackageSkillEntry,
InstallPreviewV1,
} from '../../shared/device-packages';
export type DevicePackageSource =
| Readonly<{ kind: 'npm'; spec: string }>
| Readonly<{ kind: 'git'; spec: string }>
| Readonly<{ kind: 'file'; absolutePath: string }>;
export interface InspectedDevicePackage {
packageId: string;
displayName: string;
resolvedVersion: string | null;
skillEntries: DevicePackageSkillEntry[];
extensionEntries: string[];
ignoredLifecycleScripts: string[];
}
const PACKAGE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
const SKILL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
const LIFECYCLE_SCRIPT_NAMES = Object.freeze([
'preinstall',
'install',
'postinstall',
'prepare',
] as const);
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function nonempty(value: unknown, maximum: number): string | null {
if (typeof value !== 'string') return null;
const normalized = value.trim();
return normalized && normalized.length <= maximum ? normalized : null;
}
function containsControlCharacter(value: string): boolean {
return Array.from(value).some((character) => {
const codePoint = character.codePointAt(0) ?? 0;
return codePoint <= 0x1f || codePoint === 0x7f;
});
}
export function parseDevicePackageSource(value: string): DevicePackageSource {
const source = nonempty(value, 2048);
if (!source || containsControlCharacter(source)) {
throw new Error('Device package source is invalid');
}
if (source.startsWith('npm:')) {
const spec = nonempty(source.slice(4), 1024);
if (!spec || /\s/u.test(spec)) throw new Error('npm package source is invalid');
return { kind: 'npm', spec: `npm:${spec}` };
}
if (source.startsWith('git:') || /^https?:\/\//iu.test(source)
|| /^ssh:\/\//iu.test(source) || /^git@[^:]+:/u.test(source)) {
if (/\s/u.test(source)) throw new Error('Git package source is invalid');
return { kind: 'git', spec: source };
}
if (!path.isAbsolute(source)) throw new Error('Local package source must be an absolute path');
return { kind: 'file', absolutePath: path.resolve(source) };
}
export function safeDevicePackageId(value: string): string {
const normalized = value.trim().toLowerCase()
.replace(/^@/u, '')
.replaceAll('/', '.')
.replace(/[^a-z0-9._-]+/gu, '-')
.replace(/^[^a-z0-9]+|[^a-z0-9._-]+$/gu, '')
.slice(0, 128);
if (!PACKAGE_ID_PATTERN.test(normalized)) throw new Error('Device package id is invalid');
return normalized;
}
export function safeStorageSegment(value: string): string {
const normalized = value.trim().replace(/[^A-Za-z0-9._-]+/gu, '-').slice(0, 128);
if (!normalized || normalized === '.' || normalized === '..') {
throw new Error('Device package version is invalid');
}
return normalized;
}
export function relativeContainedPath(root: string, candidate: string): string {
const resolvedRoot = path.resolve(root);
const resolvedCandidate = path.resolve(candidate);
const relative = path.relative(resolvedRoot, resolvedCandidate);
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error('Device package entry escapes its package root');
}
return relative.split(path.sep).join('/');
}
async function readJson(filePath: string): Promise<Record<string, unknown> | null> {
try {
const value = JSON.parse(await readFile(filePath, 'utf8')) as unknown;
return isRecord(value) ? value : null;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw error;
}
}
async function existsFile(filePath: string): Promise<boolean> {
return await stat(filePath).then((value) => value.isFile()).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return false;
throw error;
});
}
async function skillId(skillPath: string): Promise<string> {
const source = await readFile(skillPath, 'utf8');
const frontmatter = /^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(source)?.[1] ?? '';
const declared = /^name:\s*['"]?([^'"\r\n]+?)['"]?\s*$/imu.exec(frontmatter)?.[1]?.trim();
const fallback = path.basename(path.dirname(skillPath));
const id = declared || fallback;
if (!SKILL_ID_PATTERN.test(id)) throw new Error(`Skill id is invalid: ${id}`);
return id;
}
async function collectSkillFiles(root: string): Promise<string[]> {
const result: string[] = [];
const visit = async (directory: string): Promise<void> => {
if (result.length > 100) throw new Error('Device package contains too many Skills');
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
const candidate = path.join(directory, entry.name);
if (entry.isDirectory()) await visit(candidate);
else if (entry.isFile() && entry.name === 'SKILL.md') result.push(candidate);
}
};
await visit(root);
return result;
}
function manifestEntries(value: unknown, field: string): string[] {
if (value === undefined) return [];
const values = typeof value === 'string' ? [value] : value;
if (!Array.isArray(values) || values.length > 100
|| values.some((entry) => typeof entry !== 'string' || !entry.trim())) {
throw new Error(`${field} is invalid`);
}
return values.map((entry) => (entry as string).trim());
}
async function expandSkillManifestEntry(packageRoot: string, entry: string): Promise<string[]> {
const candidate = path.resolve(packageRoot, entry);
relativeContainedPath(packageRoot, candidate);
const metadata = await stat(candidate).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null;
throw error;
});
if (!metadata) throw new Error(`Skill entry does not exist: ${entry}`);
if (metadata.isFile()) {
if (path.basename(candidate) !== 'SKILL.md') throw new Error(`Skill entry is not SKILL.md: ${entry}`);
return [candidate];
}
if (!metadata.isDirectory()) throw new Error(`Skill entry is invalid: ${entry}`);
return await collectSkillFiles(candidate);
}
export async function inspectDevicePackage(
packageRoot: string,
fallbackName = path.basename(path.resolve(packageRoot)),
): Promise<InspectedDevicePackage> {
const root = path.resolve(packageRoot);
const packageJson = await readJson(path.join(root, 'package.json'));
const codexManifest = await readJson(path.join(root, '.codex-plugin', 'plugin.json'));
const pi = isRecord(packageJson?.pi) ? packageJson.pi : null;
const declaredExtensions = manifestEntries(pi?.extensions, 'pi.extensions');
const declaredSkills = manifestEntries(pi?.skills, 'pi.skills');
const extensionFiles: string[] = [];
for (const entry of declaredExtensions) {
const candidate = path.resolve(root, entry);
relativeContainedPath(root, candidate);
if (!await existsFile(candidate) || !/\.(?:js|ts)$/iu.test(candidate)) {
throw new Error(`Extension entry is invalid: ${entry}`);
}
extensionFiles.push(candidate);
}
const skillFiles: string[] = [];
for (const entry of declaredSkills) skillFiles.push(...await expandSkillManifestEntry(root, entry));
const codexSkills = manifestEntries(codexManifest?.skills, 'plugin.skills');
for (const entry of codexSkills) skillFiles.push(...await expandSkillManifestEntry(root, entry));
if (await existsFile(path.join(root, 'SKILL.md'))) skillFiles.push(path.join(root, 'SKILL.md'));
const uniqueSkills = new Map<string, DevicePackageSkillEntry>();
for (const skillPath of [...new Set(skillFiles.map((entry) => path.resolve(entry)))]) {
const id = await skillId(skillPath);
if (uniqueSkills.has(id)) throw new Error(`Duplicate Skill id: ${id}`);
uniqueSkills.set(id, { id, entryPath: relativeContainedPath(root, skillPath) });
}
const extensionEntries = [...new Set(extensionFiles.map((entry) => relativeContainedPath(root, entry)))];
if (uniqueSkills.size === 0 && extensionEntries.length === 0) {
throw new Error('Package does not expose a Pi extension or Skill');
}
const displayName = nonempty(packageJson?.name, 256)
?? nonempty(codexManifest?.name, 256)
?? fallbackName;
const scripts = isRecord(packageJson?.scripts) ? packageJson.scripts : {};
return {
packageId: safeDevicePackageId(displayName),
displayName,
resolvedVersion: nonempty(packageJson?.version, 128) ?? nonempty(codexManifest?.version, 128),
skillEntries: [...uniqueSkills.values()].sort((left, right) => left.id.localeCompare(right.id)),
extensionEntries: extensionEntries.sort(),
ignoredLifecycleScripts: LIFECYCLE_SCRIPT_NAMES.filter((name) => typeof scripts[name] === 'string'),
};
}
export function devicePackageKind(input: Pick<InspectedDevicePackage, 'skillEntries' | 'extensionEntries'>): DevicePackageKind {
if (input.skillEntries.length > 0 && input.extensionEntries.length > 0) return 'mixed';
return input.extensionEntries.length > 0 ? 'pi-extension' : 'skill-only';
}

View File

@@ -0,0 +1,609 @@
import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { cp, mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
import path from 'node:path';
import { DefaultPackageManager, SettingsManager } from '@earendil-works/pi-coding-agent';
import { atomicWriteJson } from '../coding-projects/atomic-json';
import {
devicePackageKind,
inspectDevicePackage,
parseDevicePackageSource,
relativeContainedPath,
safeStorageSegment,
type DevicePackageIndexV1,
type DevicePackageRecordV1,
type InstallPreviewV1,
} from './device-package-format';
export * from './device-package-format';
const PLAN_TTL_MS = 10 * 60 * 1000;
const INSTALL_OUTPUT_BYTES = 64 * 1024;
const INSTALL_TIMEOUT_MS = 2 * 60 * 1000;
const INDEX_SCHEMA_VERSION = 1;
const EXECUTABLE_WARNING = '此包包含可执行 Pi extension将获得完整桌面权限包括当前进程可用的文件、网络和进程权限。';
export class DevicePackageError extends Error {
constructor(
readonly code:
| 'local_package_source_unsupported'
| 'local_package_not_found'
| 'local_package_manifest_invalid'
| 'local_package_dependency_failed'
| 'local_package_confirmation_required'
| 'local_package_plan_expired'
| 'local_package_install_failed'
| 'local_package_not_installed'
| 'local_package_in_use',
message: string,
) {
super(message);
this.name = 'DevicePackageError';
}
}
export interface DevicePackageInstallInput {
source: string;
executablePath: string;
cliPath: string;
npmCliPath?: string;
agentDir: string;
cwd: string;
env: NodeJS.ProcessEnv;
}
export type DevicePackageInstallRunner = (input: DevicePackageInstallInput) => Promise<void>;
export interface DevicePackageManagerOptions {
rootDir: string;
executablePath?: string;
cliPath?: string;
npmCliPath?: string;
runInstall?: DevicePackageInstallRunner;
now?: () => number;
createId?: () => string;
writeIndex?: (filePath: string, value: DevicePackageIndexV1) => Promise<void>;
onGenerationChanged?(index: DevicePackageIndexV1): Promise<void> | void;
}
interface StagedPlanV1 {
schemaVersion: 1;
requestedByTurnId: string;
preview: InstallPreviewV1;
record: DevicePackageRecordV1;
}
export interface EnabledDevicePackageResources {
generation: number;
packageIds: string[];
packageRefs: Array<{ packageId: string; resolvedVersion: string }>;
skillEntries: Array<{ id: string; entryPath: string; packageRoot: string }>;
extensionPaths: string[];
}
function installEnvironment(agentDir: string): NodeJS.ProcessEnv {
return {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
PI_CODING_AGENT_DIR: agentDir,
PI_TELEMETRY: '0',
GIT_TERMINAL_PROMPT: '0',
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
CI: '1',
npm_config_ignore_scripts: 'true',
npm_config_update_notifier: 'false',
npm_config_audit: 'false',
npm_config_fund: 'false',
};
}
export async function runPiPackageInstall(input: DevicePackageInstallInput): Promise<void> {
await mkdir(input.agentDir, { recursive: true });
await mkdir(input.cwd, { recursive: true });
if (input.npmCliPath) {
await atomicWriteJson(path.join(input.agentDir, 'settings.json'), {
npmCommand: [input.executablePath, input.npmCliPath],
});
}
await new Promise<void>((resolve, reject) => {
execFile(
input.executablePath,
[input.cliPath, 'install', input.source, '--no-approve'],
{
cwd: input.cwd,
env: input.env,
windowsHide: true,
timeout: INSTALL_TIMEOUT_MS,
maxBuffer: INSTALL_OUTPUT_BYTES,
},
(error) => error ? reject(error) : resolve(),
);
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function string(value: unknown, field: string, maximum = 2048): string {
if (typeof value !== 'string' || !value.trim() || value.length > maximum) {
throw new DevicePackageError('local_package_install_failed', `${field} is invalid`);
}
return value;
}
function parseRecord(value: unknown): DevicePackageRecordV1 {
if (!isRecord(value) || value.schemaVersion !== 1 || !isRecord(value.source)
|| !Array.isArray(value.skillEntries) || !Array.isArray(value.extensionEntries)
|| typeof value.enabled !== 'boolean' || typeof value.confirmedExecutableCode !== 'boolean') {
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
const sourceKind = value.source.kind;
if (sourceKind !== 'npm' && sourceKind !== 'git' && sourceKind !== 'file') {
throw new DevicePackageError('local_package_install_failed', 'Device package source is invalid');
}
const kind = value.kind;
if (kind !== 'skill-only' && kind !== 'pi-extension' && kind !== 'mixed') {
throw new DevicePackageError('local_package_install_failed', 'Device package kind is invalid');
}
const skillEntries = value.skillEntries.map((entry) => {
if (!isRecord(entry)) throw new DevicePackageError('local_package_install_failed', 'Skill entry is invalid');
return { id: string(entry.id, 'Skill id', 128), entryPath: string(entry.entryPath, 'Skill path') };
});
return {
schemaVersion: 1,
packageId: string(value.packageId, 'Package id', 128),
displayName: string(value.displayName, 'Display name', 256),
resolvedVersion: string(value.resolvedVersion, 'Resolved version', 128),
source: {
kind: sourceKind,
requested: string(value.source.requested, 'Requested source'),
resolved: string(value.source.resolved, 'Resolved source'),
},
kind,
skillEntries,
extensionEntries: value.extensionEntries.map((entry) => string(entry, 'Extension path')),
enabled: value.enabled,
confirmedExecutableCode: value.confirmedExecutableCode,
installedAt: string(value.installedAt, 'Installed at', 64),
};
}
function parseIndex(value: unknown): DevicePackageIndexV1 {
if (!isRecord(value) || value.schemaVersion !== INDEX_SCHEMA_VERSION
|| !Number.isSafeInteger(value.generation) || (value.generation as number) < 0
|| !Array.isArray(value.packages)) {
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
const packages = value.packages.map(parseRecord);
const ids = new Set<string>();
for (const record of packages) {
if (ids.has(record.packageId)) {
throw new DevicePackageError('local_package_install_failed', 'Device package index contains duplicate ids');
}
ids.add(record.packageId);
}
return { schemaVersion: 1, generation: value.generation as number, packages };
}
function cloneIndex(index: DevicePackageIndexV1): DevicePackageIndexV1 {
return structuredClone(index);
}
async function pathMetadata(target: string): Promise<'file' | 'directory' | null> {
return await stat(target).then((value) => value.isDirectory() ? 'directory' : value.isFile() ? 'file' : null)
.catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null;
throw error;
});
}
async function gitHead(directory: string): Promise<string> {
return await new Promise<string>((resolve, reject) => {
execFile('git', ['rev-parse', 'HEAD'], {
cwd: directory,
windowsHide: true,
timeout: 10_000,
maxBuffer: 4096,
}, (error, stdout) => error ? reject(error) : resolve(stdout.trim()));
});
}
export class DevicePackageManager {
private readonly rootDir: string;
private readonly packagesDir: string;
private readonly stagingDir: string;
private readonly trashDir: string;
private readonly indexPath: string;
private readonly now: () => number;
private readonly createId: () => string;
private readonly runInstall: DevicePackageInstallRunner;
private readonly writeIndex: (filePath: string, value: DevicePackageIndexV1) => Promise<void>;
private readonly activePackages = new Map<string, number>();
private readonly pendingCleanup = new Set<string>();
private operation: Promise<void> = Promise.resolve();
constructor(private readonly options: DevicePackageManagerOptions) {
this.rootDir = path.resolve(options.rootDir);
this.packagesDir = path.join(this.rootDir, 'packages');
this.stagingDir = path.join(this.rootDir, 'staging');
this.trashDir = path.join(this.rootDir, 'trash');
this.indexPath = path.join(this.rootDir, 'index.json');
this.now = options.now ?? Date.now;
this.createId = options.createId ?? randomUUID;
this.runInstall = options.runInstall ?? runPiPackageInstall;
this.writeIndex = options.writeIndex ?? atomicWriteJson;
}
async prepare(sourceValue: string, requestedByTurnId: string): Promise<InstallPreviewV1> {
if (!requestedByTurnId.trim()) {
throw new DevicePackageError('local_package_confirmation_required', 'Request turn id is required');
}
let source;
try {
source = parseDevicePackageSource(sourceValue);
} catch (error) {
throw new DevicePackageError('local_package_source_unsupported', (error as Error).message);
}
const planId = this.createId();
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(planId)) {
throw new DevicePackageError('local_package_install_failed', 'Install plan id is invalid');
}
const planRoot = path.join(this.stagingDir, planId);
const packageRoot = path.join(planRoot, 'package');
await mkdir(planRoot, { recursive: true });
try {
let resolvedSource = sourceValue.trim();
if (source.kind === 'file') {
const metadata = await pathMetadata(source.absolutePath);
if (!metadata) throw new DevicePackageError('local_package_not_found', 'Local package source does not exist');
if (metadata === 'directory') await cp(source.absolutePath, packageRoot, { recursive: true, force: false });
else {
await mkdir(packageRoot, { recursive: true });
await cp(source.absolutePath, path.join(packageRoot, path.basename(source.absolutePath)), { force: false });
}
resolvedSource = source.absolutePath;
} else {
const executablePath = this.options.executablePath;
const cliPath = this.options.cliPath;
if (!executablePath || !cliPath) {
throw new DevicePackageError('local_package_dependency_failed', 'Bundled Pi installer is unavailable');
}
const agentDir = path.join(planRoot, 'pi-agent');
const cwd = path.join(planRoot, 'project');
await this.runInstall({
source: source.spec,
executablePath,
cliPath,
...(this.options.npmCliPath ? { npmCliPath: this.options.npmCliPath } : {}),
agentDir,
cwd,
env: installEnvironment(agentDir),
});
const manager = new DefaultPackageManager({
cwd,
agentDir,
settingsManager: SettingsManager.inMemory({}, { projectTrusted: false }),
});
const installedPath = manager.getInstalledPath(source.spec, 'user');
if (!installedPath) throw new DevicePackageError('local_package_dependency_failed', 'Pi did not install the package');
if (source.kind === 'npm') {
const installRoot = path.join(agentDir, 'npm');
await cp(installRoot, packageRoot, { recursive: true, force: false });
const relativePackage = relativeContainedPath(installRoot, installedPath);
const nested = path.join(packageRoot, ...relativePackage.split('/'));
const inspected = await inspectDevicePackage(nested);
const prefix = relativeContainedPath(packageRoot, nested);
const remapped = {
...inspected,
skillEntries: inspected.skillEntries.map((entry) => ({
...entry, entryPath: `${prefix}/${entry.entryPath}`,
})),
extensionEntries: inspected.extensionEntries.map((entry) => `${prefix}/${entry}`),
};
return await this.finishPrepare({
planId, planRoot, packageRoot, sourceValue, source, requestedByTurnId,
resolvedSource: `npm:${inspected.displayName}@${inspected.resolvedVersion ?? 'unknown'}`,
inspected: remapped,
});
}
await cp(installedPath, packageRoot, { recursive: true, force: false });
const commit = await gitHead(installedPath).catch(() => {
throw new DevicePackageError('local_package_dependency_failed', 'Git package commit cannot be resolved');
});
resolvedSource = `${source.spec.replace(/@[^/@]+$/u, '')}@${commit}`;
}
return await this.finishPrepare({
planId, planRoot, packageRoot, sourceValue, source, requestedByTurnId, resolvedSource,
inspected: await inspectDevicePackage(
packageRoot,
source.kind === 'file'
? path.basename(source.absolutePath, path.extname(source.absolutePath))
: path.basename(packageRoot),
),
});
} catch (error) {
await rm(planRoot, { recursive: true, force: true }).catch(() => undefined);
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_manifest_invalid', (error as Error).message);
}
}
private async finishPrepare(input: {
planId: string;
planRoot: string;
packageRoot: string;
sourceValue: string;
source: ReturnType<typeof parseDevicePackageSource>;
requestedByTurnId: string;
resolvedSource: string;
inspected: Awaited<ReturnType<typeof inspectDevicePackage>>;
}): Promise<InstallPreviewV1> {
const includesExecutableCode = input.inspected.extensionEntries.length > 0;
const resolvedVersion = input.inspected.resolvedVersion
?? `local-${new Date(this.now()).toISOString().replace(/[-:.]/gu, '')}`;
safeStorageSegment(resolvedVersion);
const preview: InstallPreviewV1 = {
schemaVersion: 1,
planId: input.planId,
expiresAt: new Date(this.now() + PLAN_TTL_MS).toISOString(),
requestedSource: input.sourceValue.trim(),
resolvedSource: input.resolvedSource,
packageId: input.inspected.packageId,
displayName: input.inspected.displayName,
resolvedVersion,
kind: devicePackageKind(input.inspected),
skillEntries: input.inspected.skillEntries,
extensionEntries: input.inspected.extensionEntries,
includesExecutableCode,
ignoredLifecycleScripts: input.inspected.ignoredLifecycleScripts,
warnings: includesExecutableCode ? [EXECUTABLE_WARNING] : [],
scope: 'device-parent-workers',
};
const record: DevicePackageRecordV1 = {
schemaVersion: 1,
packageId: preview.packageId,
displayName: preview.displayName,
resolvedVersion,
source: {
kind: input.source.kind,
requested: preview.requestedSource,
resolved: preview.resolvedSource,
},
kind: preview.kind,
skillEntries: preview.skillEntries,
extensionEntries: preview.extensionEntries,
enabled: true,
confirmedExecutableCode: includesExecutableCode,
installedAt: new Date(this.now()).toISOString(),
};
const plan: StagedPlanV1 = {
schemaVersion: 1,
requestedByTurnId: input.requestedByTurnId,
preview,
record,
};
await atomicWriteJson(path.join(input.planRoot, 'plan.json'), plan);
return structuredClone(preview);
}
async commit(planId: string, confirmed: true, confirmationTurnId: string): Promise<DevicePackageIndexV1> {
if (confirmed !== true || !confirmationTurnId.trim()) {
throw new DevicePackageError('local_package_confirmation_required', 'Literal confirmation is required');
}
return await this.withOperation(async () => {
const plan = await this.readPlan(planId);
if (plan.requestedByTurnId === confirmationTurnId) {
throw new DevicePackageError('local_package_confirmation_required', 'Confirm installation in a new user turn');
}
if (Date.parse(plan.preview.expiresAt) <= this.now()) {
throw new DevicePackageError('local_package_plan_expired', 'Install preview has expired');
}
if (plan.preview.includesExecutableCode && !plan.preview.warnings.includes(EXECUTABLE_WARNING)) {
throw new DevicePackageError('local_package_confirmation_required', 'Executable permission warning is missing');
}
const stagedPackage = path.join(this.stagingDir, planId, 'package');
await this.assertRecordEntries(stagedPackage, plan.record);
const index = await this.readIndex();
const existing = index.packages.find(({ packageId }) => packageId === plan.record.packageId);
if (existing && existing.source.requested !== plan.record.source.requested) {
throw new DevicePackageError('local_package_install_failed', 'Package id belongs to another source');
}
const destination = this.packageDirectory(plan.record);
const activeKey = this.packageKey(plan.record);
if (await pathMetadata(destination) && (this.activePackages.get(activeKey) ?? 0) > 0) {
throw new DevicePackageError('local_package_in_use', 'This package version is active in a worker');
}
await mkdir(path.dirname(destination), { recursive: true });
await mkdir(this.trashDir, { recursive: true });
const displaced = await pathMetadata(destination)
? path.join(this.trashDir, `${plan.record.packageId}-${this.createId()}`)
: null;
if (displaced) await rename(destination, displaced);
let moved = false;
try {
await rename(stagedPackage, destination);
moved = true;
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: [...index.packages.filter(({ packageId }) => packageId !== plan.record.packageId), plan.record]
.sort((left, right) => left.packageId.localeCompare(right.packageId)),
};
await this.writeIndex(this.indexPath, next);
await rm(path.join(this.stagingDir, planId), { recursive: true, force: true });
if (existing && existing.resolvedVersion !== plan.record.resolvedVersion) {
await this.cleanupPackage(existing);
}
if (displaced) await rm(displaced, { recursive: true, force: true }).catch(() => undefined);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
} catch (error) {
if (moved) await rm(destination, { recursive: true, force: true }).catch(() => undefined);
if (displaced) await rename(displaced, destination).catch(() => undefined);
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_install_failed', 'Device package commit failed');
}
});
}
async list(): Promise<DevicePackageIndexV1> {
return cloneIndex(await this.readIndex());
}
async setEnabled(packageId: string, enabled: boolean): Promise<DevicePackageIndexV1> {
return await this.withOperation(async () => {
const index = await this.readIndex();
const existing = index.packages.find((record) => record.packageId === packageId);
if (!existing) throw new DevicePackageError('local_package_not_installed', 'Device package is not installed');
if (existing.enabled === enabled) return cloneIndex(index);
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: index.packages.map((record) => record.packageId === packageId
? { ...record, enabled }
: record),
};
await this.writeIndex(this.indexPath, next);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
});
}
async uninstall(packageId: string): Promise<DevicePackageIndexV1> {
return await this.withOperation(async () => {
const index = await this.readIndex();
const existing = index.packages.find((record) => record.packageId === packageId);
if (!existing) return cloneIndex(index);
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: index.packages.filter((record) => record.packageId !== packageId),
};
await this.writeIndex(this.indexPath, next);
await this.cleanupPackage(existing);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
});
}
async resolveEnabledResources(): Promise<EnabledDevicePackageResources> {
const index = await this.readIndex();
const enabled = index.packages.filter(({ enabled }) => enabled);
const skillEntries: EnabledDevicePackageResources['skillEntries'] = [];
const extensionPaths: string[] = [];
for (const record of enabled) {
const packageRoot = this.packageDirectory(record);
await this.assertRecordEntries(packageRoot, record);
for (const entry of record.skillEntries) skillEntries.push({ ...entry, packageRoot });
for (const entry of record.extensionEntries) {
extensionPaths.push(path.join(packageRoot, ...entry.split('/')));
}
}
return {
generation: index.generation,
packageIds: enabled.map(({ packageId }) => packageId),
packageRefs: enabled.map(({ packageId, resolvedVersion }) => ({ packageId, resolvedVersion })),
skillEntries,
extensionPaths,
};
}
registerActiveWorker(refs: readonly { packageId: string; resolvedVersion: string }[]): () => Promise<void> {
const keys = [...new Set(refs.map((record) => this.packageKey(record)))];
for (const key of keys) this.activePackages.set(key, (this.activePackages.get(key) ?? 0) + 1);
let released = false;
return async () => {
if (released) return;
released = true;
for (const key of keys) {
const count = this.activePackages.get(key) ?? 0;
if (count <= 1) this.activePackages.delete(key);
else this.activePackages.set(key, count - 1);
}
for (const target of [...this.pendingCleanup]) {
const [key, packagePath] = target.split('\u0000', 2);
if (!key || !packagePath || (this.activePackages.get(key) ?? 0) > 0) continue;
this.pendingCleanup.delete(target);
await rm(packagePath, { recursive: true, force: true }).catch(() => undefined);
}
};
}
private async readPlan(planId: string): Promise<StagedPlanV1> {
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(planId)) {
throw new DevicePackageError('local_package_plan_expired', 'Install preview is unavailable');
}
try {
const value = JSON.parse(await readFile(path.join(this.stagingDir, planId, 'plan.json'), 'utf8')) as unknown;
if (!isRecord(value) || value.schemaVersion !== 1 || !isRecord(value.preview)) throw new Error();
const record = parseRecord(value.record);
const preview = value.preview as unknown as InstallPreviewV1;
if (preview.schemaVersion !== 1 || preview.planId !== planId
|| !Array.isArray(preview.warnings) || !Array.isArray(preview.skillEntries)
|| !Array.isArray(preview.extensionEntries)) throw new Error();
return {
schemaVersion: 1,
requestedByTurnId: string(value.requestedByTurnId, 'Request turn id', 256),
preview,
record,
};
} catch (error) {
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_plan_expired', 'Install preview is unavailable');
}
}
private async readIndex(): Promise<DevicePackageIndexV1> {
try {
return parseIndex(JSON.parse(await readFile(this.indexPath, 'utf8')) as unknown);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { schemaVersion: 1, generation: 0, packages: [] };
}
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
}
private packageDirectory(record: Pick<DevicePackageRecordV1, 'packageId' | 'resolvedVersion'>): string {
return path.join(this.packagesDir, record.packageId, safeStorageSegment(record.resolvedVersion));
}
private packageKey(record: Pick<DevicePackageRecordV1, 'packageId' | 'resolvedVersion'>): string {
return `${record.packageId}@${record.resolvedVersion}`;
}
private async assertRecordEntries(packageRoot: string, record: DevicePackageRecordV1): Promise<void> {
if (await pathMetadata(packageRoot) !== 'directory') {
throw new DevicePackageError('local_package_install_failed', 'Device package bytes are unavailable');
}
for (const entry of [...record.skillEntries.map(({ entryPath }) => entryPath), ...record.extensionEntries]) {
const candidate = path.resolve(packageRoot, ...entry.split('/'));
try {
relativeContainedPath(packageRoot, candidate);
} catch {
throw new DevicePackageError('local_package_manifest_invalid', 'Device package entry escapes its root');
}
if (await pathMetadata(candidate) !== 'file') {
throw new DevicePackageError('local_package_manifest_invalid', 'Device package entry is unavailable');
}
}
}
private async cleanupPackage(record: DevicePackageRecordV1): Promise<void> {
const packagePath = this.packageDirectory(record);
const key = this.packageKey(record);
if ((this.activePackages.get(key) ?? 0) > 0) {
this.pendingCleanup.add(`${key}\u0000${packagePath}`);
return;
}
await rm(packagePath, { recursive: true, force: true }).catch(() => undefined);
}
private withOperation<T>(operation: () => Promise<T>): Promise<T> {
const result = this.operation.then(operation, operation);
this.operation = result.then(() => undefined, () => undefined);
return result;
}
}

View File

@@ -0,0 +1,173 @@
import type { CodingPluginToolDefinition } from '../../shared/coding-plugins';
import type {
DevicePackageIndexV1,
DevicePackageToolDetailsV1,
DevicePackageToolOperation,
InstallPreviewV1,
} from '../../shared/device-packages';
import type { DevicePackageToolName } from '../../shared/device-packages';
import type { PiProductToolResult } from '../coding-runtime/pi/product-tools';
import type { DevicePackageManager } from './device-package-manager';
export { DEVICE_PACKAGE_TOOL_NAMES } from '../../shared/device-packages';
const EMPTY_OBJECT_SCHEMA = Object.freeze({
type: 'object', additionalProperties: false, properties: {},
});
function tool(
name: DevicePackageToolName,
label: string,
description: string,
operation: DevicePackageToolOperation,
mutation: 'read' | 'write' | 'destructive',
inputSchema: Readonly<Record<string, unknown>>,
): CodingPluginToolDefinition {
return Object.freeze({
name,
label,
description,
capabilityId: 'makelore.device-packages',
operation,
roles: ['parent'],
mutation,
projectWriteLease: false,
permissions: ['device-packages'],
inputSchema,
});
}
export const DEVICE_PACKAGE_TOOL_DEFINITIONS = Object.freeze([
tool(
'local_package_prepare',
'Prepare local package',
'Resolve an npm, Git, or absolute local Pi package/Skill and show an exact installation preview. This does not install it.',
'prepare',
'read',
{
type: 'object', additionalProperties: false, required: ['source'],
properties: { source: { type: 'string', minLength: 1, maxLength: 2048 } },
},
),
tool(
'local_package_commit',
'Install local package',
'Install a prepared device package only after the user confirms in a later message.',
'commit',
'write',
{
type: 'object', additionalProperties: false, required: ['planId', 'confirmed'],
properties: {
planId: { type: 'string', minLength: 1, maxLength: 128 },
confirmed: { const: true },
},
},
),
tool('local_package_list', 'List local packages', 'List packages installed on this device.', 'list', 'read', EMPTY_OBJECT_SCHEMA),
tool(
'local_package_set_enabled',
'Enable or disable local package',
'Enable or disable an installed device package for future parent workers.',
'set_enabled',
'write',
{
type: 'object', additionalProperties: false, required: ['packageId', 'enabled'],
properties: {
packageId: { type: 'string', minLength: 1, maxLength: 128 },
enabled: { type: 'boolean' },
},
},
),
tool(
'local_package_uninstall',
'Remove local package',
'Remove an installed package from this device after explicit confirmation.',
'uninstall',
'destructive',
{
type: 'object', additionalProperties: false, required: ['packageId', 'confirmed'],
properties: {
packageId: { type: 'string', minLength: 1, maxLength: 128 },
confirmed: { const: true },
},
},
),
]);
function record(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Local package tool input is invalid');
return value as Record<string, unknown>;
}
function exact(input: Record<string, unknown>, keys: readonly string[]): void {
const actual = Object.keys(input).sort();
const expected = [...keys].sort();
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
throw new Error('Local package tool input has unexpected fields');
}
}
function boundedString(value: unknown, name: string, maximum: number): string {
if (typeof value !== 'string' || !value.trim() || value.length > maximum) throw new Error(`${name} is invalid`);
return value.trim();
}
function result(
operation: DevicePackageToolOperation,
value: { preview?: InstallPreviewV1; index?: DevicePackageIndexV1 },
): PiProductToolResult {
const details: DevicePackageToolDetailsV1 = {
schema: 'makelore-device-package.v1', operation, success: true, ...value,
};
return { content: [{ type: 'text', text: JSON.stringify(details) }], details };
}
export class DevicePackageTools {
readonly tools = DEVICE_PACKAGE_TOOL_DEFINITIONS;
constructor(private readonly manager: DevicePackageManager) {}
async invoke(toolName: string, turnId: string, value: unknown): Promise<PiProductToolResult> {
const input = record(value);
switch (toolName) {
case 'local_package_prepare': {
exact(input, ['source']);
return result('prepare', {
preview: await this.manager.prepare(boundedString(input.source, 'source', 2048), turnId),
});
}
case 'local_package_commit': {
exact(input, ['planId', 'confirmed']);
return result('commit', {
index: await this.manager.commit(
boundedString(input.planId, 'planId', 128),
input.confirmed as true,
turnId,
),
});
}
case 'local_package_list':
exact(input, []);
return result('list', { index: await this.manager.list() });
case 'local_package_set_enabled': {
exact(input, ['packageId', 'enabled']);
if (typeof input.enabled !== 'boolean') throw new Error('enabled is invalid');
return result('set_enabled', {
index: await this.manager.setEnabled(
boundedString(input.packageId, 'packageId', 128),
input.enabled,
),
});
}
case 'local_package_uninstall': {
exact(input, ['packageId', 'confirmed']);
if (input.confirmed !== true) throw new Error('Literal confirmation is required');
return result('uninstall', {
index: await this.manager.uninstall(boundedString(input.packageId, 'packageId', 128)),
});
}
default:
throw new Error('Local package tool is unavailable');
}
}
}