import * as path from "node:path"; import { DefaultResourceLoader, getAgentDir, ProjectTrustStore, SettingsManager, type AgentSession, type SessionShutdownEvent, } from "@earendil-works/pi-coding-agent"; const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000; /** Tools that headless children must not receive. Everything else stays enabled. */ export const CHILD_EXCLUDED_TOOL_NAMES = [ "subagent_spawn", "subagent_wait", "subagent_cancel", "subagent_check", "subagent_list", "workflow", "ask_user", ] as const; /** Fresh SDK options avoid turning the denylist into an accidental allowlist. */ export function childToolPolicy() { return { excludeTools: [...CHILD_EXCLUDED_TOOL_NAMES] }; } export interface ChildResourceOptions { cwd: string; projectTrusted: boolean; appendSystemPrompt?: string[]; agentDir?: string; } /** Load normal global/package resources and trust-gated project resources. */ export async function createChildResources(options: ChildResourceOptions) { const agentDir = options.agentDir ?? getAgentDir(); const settingsManager = SettingsManager.create(options.cwd, agentDir, { projectTrusted: options.projectTrusted, }); const loader = new DefaultResourceLoader({ cwd: options.cwd, agentDir, settingsManager, ...(options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {}), }); await loader.reload(); return { loader, settingsManager }; } /** * Same-directory children inherit the live parent decision. An alternate cwd * is trusted only when Pi's persisted trust store explicitly trusts it (or a * containing directory); unreadable/invalid trust data fails closed. */ export function resolveStandaloneChildProjectTrust(options: { parentCwd: string; childCwd: string; parentTrusted: boolean; agentDir?: string; }) { if (path.resolve(options.childCwd) === path.resolve(options.parentCwd)) { return options.parentTrusted; } try { const trustStore = new ProjectTrustStore(options.agentDir ?? getAgentDir()); return trustStore.get(options.childCwd) === true; } catch { return false; } } /** Start child extension session hooks/resources in headless print mode. */ export async function bindChildSessionExtensions( session: Pick, ) { await session.bindExtensions({ mode: "print" }); } interface ChildExtensionRunner { hasHandlers(eventType: string): boolean; emit(event: SessionShutdownEvent): Promise; } export interface DisposableChildSession { readonly extensionRunner: ChildExtensionRunner; dispose(): void; } const childShutdowns = new WeakMap>(); function waitBounded(operation: Promise, timeoutMs: number) { let timer: ReturnType | undefined; const timeout = new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); }); return Promise.race([ operation.then( () => undefined, () => undefined, ), timeout, ]) .catch(() => {}) .finally(() => { if (timer) clearTimeout(timer); }); } /** * Emit child session_shutdown once, then dispose once. Hook failures and a * bounded hook deadline never prevent disposal. */ export function shutdownAndDisposeChildSession( session: DisposableChildSession, options: { timeoutMs?: number } = {}, ) { const existing = childShutdowns.get(session); if (existing) return existing; const shutdown = (async () => { try { if (session.extensionRunner.hasHandlers("session_shutdown")) { await waitBounded( session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }), options.timeoutMs ?? CHILD_SHUTDOWN_TIMEOUT_MS, ); } } catch { // Extension runner inspection/emission is best-effort during teardown. } finally { try { session.dispose(); } catch { // Disposal is terminal and must remain idempotent for callers. } } })(); childShutdowns.set(session, shutdown); return shutdown; }