How the ProjectTrustDialog in pi-web Validates and Manages Trusted Project Directories
The ProjectTrustDialog validates directories through server-side path resolution and allowed-root checks, persists trust decisions via ProjectTrustStore, and gates project-local resources behind a trust resolver to prevent automatic execution of untrusted code.
The pi-web repository implements a security boundary to protect users from automatically executing code found inside untrusted repositories. The ProjectTrustDialog surfaces this protection layer in the UI, ensuring that users must explicitly validate and manage trusted project directories before project-local extensions, settings, or skills can load. This system operates across three tightly coupled layers: a React dialog component, a Next.js API route for validation, and a core trust management library that integrates with the Pi SDK.
The Three-Layer Architecture
The trust validation system spans distinct architectural layers:
- UI Layer (
components/ProjectTrustDialog.tsx): Renders the modal dialog and forwards user decisions to the API. - API Layer (
app/api/project-trust/route.ts): Validates directory paths and coordinates trust persistence. - Core Layer (
lib/project-trust.ts): Detects trust requirements, reads and writes trust flags viaProjectTrustStore, and supplies reload options to the Pi SDK.
Path Validation in the API Layer
The app/api/project-trust/route.ts file exports a validateCwd function that enforces strict rules before any trust decision is processed. The validation flow requires the cwd parameter to be a non‑empty string, resolves the path, confirms it exists as a directory via stat, and verifies it resides within an allowed file root returned by getAllowedFileRoots() (which includes the current worktree and paths matching ~/pi‑cwd‑*).
// src/app/api/project-trust/route.ts
async function validateCwd(value: unknown) {
if (typeof value !== "string" || !value.trim()) {
return { response: NextResponse.json({ error: "cwd required" }, { status: 400 }) };
}
const cwd = resolve(value);
if (!(await stat(cwd)).isDirectory()) {
return { response: NextResponse.json({ error: "cwd must be a directory" }, { status: 400 }) };
}
const allowedRoots = await getAllowedFileRoots();
if (!isExistingFilePathAllowed(cwd, allowedRoots)) {
return { response: NextResponse.json({ error: "cwd not allowed" }, { status: 403 }) };
}
return { cwd };
}
GET Endpoint: Retrieving Trust Status
When the API receives a GET request, it validates the cwd and returns the current status via getProjectTrustStatus, passing the cwd and agentDir from getAgentDir().
// src/app/api/project-trust/route.ts
return NextResponse.json(getProjectTrustStatus(result.cwd, getAgentDir()));
POST Endpoint: Recording Trust Decisions
The POST handler performs additional safety checks before persisting trust. After validation, it ensures the project actually requires trust, verifies no busy RPC session exists for that directory using hasBusyRpcSessionForCwd, persists the decision via trustProject, and clears related caches.
// src/app/api/project-trust/route.ts (POST handler)
const status = trustProject(result.cwd, agentDir);
invalidateModelsCache();
destroyRpcSessionsForCwd(result.cwd);
return NextResponse.json(status);
Core Trust Management in lib/project-trust.ts
This module centralizes the determination of whether a project requires explicit trust and manages the persistent store shared with the pi CLI.
Detecting Trust Requirements
The getProjectTrustStatus function checks if the directory contains trust‑requiring resources using hasTrustRequiringProjectResources. If the project contains no such resources, it automatically returns { requiresTrust: false, trusted: true }. Otherwise, it reads the trust flag from ProjectTrustStore.
// src/lib/project-trust.ts
const requiresTrust = Boolean(cwd) && hasTrustRequiringProjectResources(cwd);
if (!requiresTrust) return { requiresTrust: false, trusted: true };
const trustStore = new ProjectTrustStore(agentDir);
return { requiresTrust: true, trusted: trustStore.get(cwd) === true };
Persisting Trust Decisions
When a user confirms trust, the trustProject function writes true to the store for that directory path and returns the updated status.
// src/lib/project-trust.ts
new ProjectTrustStore(agentDir).set(cwd, true);
Resource Gating via Reload Options
To prevent the Pi SDK from loading untrusted code, projectTrustReloadOptions supplies a resolver function used by the DefaultResourceLoader. This defers loading of project‑local extensions, .pi/settings.json configurations, and .agents/skills until trust is granted.
// src/lib/project-trust.ts
return { resolveProjectTrust: async () => trustStore.get(cwd) === true };
The React Dialog Component
The components/ProjectTrustDialog.tsx file implements the presentation layer, receiving the cwd, a busy state, and callback handlers. When the user clicks Trust, the onConfirm callback triggers a POST request to /api/project-trust. The component displays a loading state via the translation key t("trust.trusting") while the request is in flight.
// src/components/ProjectTrustDialog.tsx
<button onClick={onConfirm} disabled={busy}>
{busy ? t("trust.trusting") : t("trust.trustProject")}
</button>
The surrounding application manages dialog visibility and fetches the initial trust status via GET to determine whether to prompt the user.
Integration with RPC and Skills Services
The trust system integrates with session management to ensure running services respect the trust boundary.
RPC Session Management
When starting a new session, lib/rpc-manager.ts injects the trust reload options into the resource loader configuration. This ensures services spawned for that working directory check trust status before accessing local resources.
// src/lib/rpc-manager.ts
const trustReloadOptions = projectTrustReloadOptions(cwd, agentDir);
const sessionConfig = {
...(trustReloadOptions ? { resourceLoaderReloadOptions: trustReloadOptions } : {})
};
Skills Service Gating
The lib/skills-service.ts file reads the trust status to determine whether project‑local skills can be loaded. It passes projectResourcesLoaded to downstream consumers, which evaluates to true only when the directory is trusted.
// src/lib/skills-service.ts
projectResourcesLoaded: getProjectTrustStatus(cwd, agentDir).trusted,
Summary
- Validation: The API layer validates directory paths using
validateCwdinapp/api/project-trust/route.ts, ensuring directories exist and reside within allowed roots before processing trust requests. - Detection: The core layer in
lib/project-trust.tsuseshasTrustRequiringProjectResourcesto determine if a project requires explicit trust, automatically trusting projects without executable resources. - Persistence: Trust decisions are stored in
ProjectTrustStore, shared with thepiCLI, and survive across application restarts. - Gating: The
projectTrustReloadOptionsresolver prevents the Pi SDK from loading local extensions, settings, and skills until trust is explicitly granted via the dialog. - Integration: The system invalidates model caches with
invalidateModelsCache()and destroys existing RPC sessions viadestroyRpcSessionsForCwd()when trust status changes, ensuring a clean security boundary.
Frequently Asked Questions
How does the ProjectTrustDialog prevent automatic execution of untrusted code?
The dialog acts as the UI entry point for a security gate that blocks the Pi SDK's DefaultResourceLoader. Until the user confirms trust and trustProject writes true to the ProjectTrustStore, the resolveProjectTrust resolver returns false, preventing the loader from accessing project-local extensions, .pi/settings.json, or skills directories. This ensures no code executes until explicit validation occurs.
What validation rules apply when trusting a project directory?
The validateCwd function enforces three strict rules: the input must be a non-empty string, the resolved path must exist as a directory (verified via fs/promises stat), and the directory must be contained within one of the allowed file roots returned by getAllowedFileRoots(). Failure at any step results in a 400 Bad Request or 403 Forbidden response.
What happens to existing RPC sessions when trust is granted?
When the POST endpoint records a new trust decision, it immediately calls destroyRpcSessionsForCwd to terminate any active RPC sessions for that directory, then invalidates the models cache with invalidateModelsCache(). This forces a clean restart of services with the new trust permissions, preventing stale untrusted sessions from continuing to run.
Can a project be trusted if it doesn't contain trust-requiring resources?
No manual trust is required. The getProjectTrustStatus function automatically returns { trusted: true } for projects where hasTrustRequiringProjectResources returns false. The ProjectTrustDialog will not appear for these directories, and attempting to POST a trust decision for such projects will fail validation because the system recognizes that trust is not required.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →