import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; import { lstat, readFile, realpath } from "node:fs/promises"; import { join, relative, resolve, sep } from "node:path"; import { assertWorkerPath } from "../security/worker-scope.js"; import type { TaskEnvelope } from "../types.js"; async function scopedProjectRules(cwd: string, scope: TaskEnvelope["scope"]) { if (scope.paths.length === 0) throw new Error("Worker scope requires at least one path"); const rootScope: TaskEnvelope["scope"] = { paths: ["."], excludedPaths: [] }; for (const path of [...scope.paths, ...scope.excludedPaths]) { if (!path.trim()) throw new Error("Worker scope paths must not be empty"); await assertWorkerPath(cwd, path, rootScope); } const root = resolve(cwd); const seen = new Set(); const agentsFiles: Array<{ path: string; content: string }> = []; for (const path of scope.paths) { await assertWorkerPath(cwd, path, scope); const parts = relative(root, resolve(root, path)).split(sep).filter(Boolean); for (let depth = 0; depth <= parts.length; depth += 1) { const candidate = join(root, ...parts.slice(0, depth), "AGENTS.md"); try { const entry = await lstat(candidate); if (!entry.isFile() && !entry.isSymbolicLink()) continue; } catch (error) { if (["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) continue; throw error; } await assertWorkerPath(cwd, candidate, rootScope); const resolved = await realpath(candidate); if (seen.has(resolved)) continue; seen.add(resolved); agentsFiles.push({ path: resolved, content: await readFile(resolved, "utf8") }); } } return agentsFiles; } export const SKILLS_DIRECTORY = join(".ultra", "skills"); const SKILL_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/; const SKILL_MAX_BYTES = 8 * 1024; export async function allowedSkillFiles(cwd: string, scope: TaskEnvelope["scope"], allowList: readonly string[]): Promise> { const files: Array<{ path: string; content: string }> = []; for (const name of allowList) { if (!SKILL_NAME_RE.test(name)) throw new Error(`UltraPi skill name ${name} is not a slug`); const candidate = join(cwd, SKILLS_DIRECTORY, `${name}.md`); await assertWorkerPath(cwd, candidate, { paths: ["."], excludedPaths: [] }); let content: string; try { content = await readFile(candidate, "utf8"); } catch (error) { if (["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) continue; throw error; } if (Buffer.byteLength(content, "utf8") > SKILL_MAX_BYTES) throw new Error(`UltraPi skill ${name} exceeds the size limit`); files.push({ path: await realpath(candidate), content }); } return files; } export async function minimalResourceLoader(cwd: string, agentDir: string, scope: TaskEnvelope["scope"], allowedSkills: readonly string[] = []) { const agentsFiles = [...await scopedProjectRules(cwd, scope), ...await allowedSkillFiles(cwd, scope, allowedSkills)]; const loader = new DefaultResourceLoader({ cwd, agentDir, noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true, agentsFilesOverride: () => ({ agentsFiles }), systemPrompt: "You are a read-only UltraPi worker. Use only the declared task envelope and return its required JSON result. Never delegate, write files, or reveal unrelated context.", }); await loader.reload(); return loader; }