How the Project-Trust Dialog Integrates with File Access in Pi Web
The Project-Trust Dialog in Pi Web gates access to project-local resources by persisting user trust decisions in the SDK's trust store, which the file-access layer consults before allowing reads from gated paths like .pi/extensions or .agents/skills.
Pi Web implements a security-first architecture that prevents unintentional code execution from untrusted repositories. When a project contains resources requiring explicit permission, the Project-Trust Dialog interacts with the file-access subsystem through a structured trust verification flow. This article examines how the dialog integrates with file permissions across the agegr/pi-web codebase.
Detecting Trust-Required Projects
The trust flow begins with automatic detection of gated resources. The SDK exposes getProjectTrustStatus() in lib/project-trust.ts to evaluate whether the current working directory contains trust-requiring content.
// lib/project-trust.ts – core trust evaluation
export function getProjectTrustStatus(cwd: string, agentDir: string): ProjectTrustStatus {
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 };
}
The helper hasTrustRequiringProjectResources(cwd) scans for specific directory patterns: .pi/extensions, project-specific settings, and .agents/skills. If none exist, the project implicitly passes trust checks.
Surfacing Trust Status to the UI Layer
components/AppShell.tsx retrieves the trust status during component mount and stores it in React state. This state drives conditional UI rendering across the application.
// AppShell.tsx – trust status retrieval on mount (lines 844-854)
fetch(`/api/project-trust?cwd=${encodeURIComponent(projectTrustCwd)}`)
.then(r => r.json())
.then(setProjectTrust);
The projectTrust state object enables security-aware UI decisions. For example, the chat composer hides when projectTrust.requiresTrust && !projectTrust.trusted, preventing interaction with untrusted project resources.
Rendering the Project-Trust Dialog
When trust is required but not yet granted, components/ProjectTrustDialog.tsx renders a modal interface (lines 68-140). The component presents localized strings via t("trust.dialogTitle") and t("trust.dialogBody"), offering explicit Cancel and Trust actions.
// ProjectTrustDialog.tsx – user initiates trust
await fetch("/api/project-trust", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cwd: currentCwd })
});
The POST request routes through app/api/project-trust/route.ts, which delegates to trustProject(cwd, agentDir) in the core library.
Persisting Trust Decisions
The trustProject() function writes the affirmative decision to a ProjectTrustStore instance. This store persists data under the Pi Web agent directory, typically ~/.pi/agent/trust.
// Sequential calls to getProjectTrustStatus return updated state
{ requiresTrust: true, trusted: true } // after user confirmation
Persistent storage ensures trust decisions survive application restarts without requiring repeated user confirmation.
Rebuilding File-Access Permissions After Trust
Once trusted, the client reloads gated resource options through projectTrustReloadOptions() in lib/project-trust.ts. This function returns a resolver that the file-access subsystem integrates into its permission model.
// lib/project-trust.ts – providing trust resolution for file access
export function projectTrustReloadOptions(
cwd: string,
agentDir: string,
): { resolveProjectTrust: () => Promise<boolean> } | undefined {
const status = getProjectTrustStatus(cwd, agentDir);
if (!status.requiresTrust) return undefined;
const trustStore = new ProjectTrustStore(agentDir);
return { resolveProjectTrust: async () => trustStore.get(cwd) === true };
}
lib/file-access.ts consumes this resolver when constructing its allowed roots list. The resolver's return value determines inclusion of project-local paths:
resolveProjectTrust()resolvestrue→ Extensions, settings, and skills directories append to allowed rootsresolveProjectTrust()resolvesfalse→ Project-local roots remain excluded
Enforcing Trust at the Permission Boundary
The final security check occurs in lib/path-security.ts via isFilePathAllowed(). This function validates every file-access request against the constructed allowed-roots set.
Because trust status controls whether project-specific roots populate that set, any read attempt targeting gated directories fails with a permission error until explicit trust is established. This creates a fail-closed security model—deny by default, permit only after explicit user authorization.
Trust-to-Access Integration Flow
- Detection –
getProjectTrustStatus()identifies trust-requiring projects - UI gating –
AppShell.tsxfetches status and conditionally renders components - User prompt –
ProjectTrustDialog.tsxsolicits explicit trust decision - Persistence –
ProjectTrustStorerecords the affirmative choice - Permission reload –
projectTrustReloadOptions()provides updated resolver - Access enforcement –
isFilePathAllowed()permits or denies based on resolved trust
Summary
- Project-trust detection occurs automatically via
hasTrustRequiringProjectResources()inlib/project-trust.ts - UI state management in
components/AppShell.tsxhides functionality until trust is established - Explicit user consent is captured through
components/ProjectTrustDialog.tsxand persisted via POST to/api/project-trust - Permission reconstruction happens through
projectTrustReloadOptions(), which feeds the file-access layer's allowed-roots calculation - Runtime enforcement in
lib/path-security.tsensures no gated file reads succeed without prior trust
Frequently Asked Questions
What triggers the Project-Trust Dialog to appear?
The dialog appears when getProjectTrustStatus() detects trust-gated resources in the current working directory and the project has not yet been trusted. Specifically, presence of .pi/extensions, project-specific settings files, or .agents/skills directories causes requiresTrust to return true, prompting AppShell.tsx to render the modal.
Where does Pi Web store trust decisions?
Trust decisions persist in a ProjectTrustStore located under the agent directory at ~/.pi/agent/trust. This path is derived from the agentDir parameter passed to trustProject() and getProjectTrustStatus(). The store maintains a mapping of working directory paths to boolean trust values.
How does file access change immediately after trusting a project?
After trust is granted, the client calls projectTrustReloadOptions() to obtain a fresh resolveProjectTrust function. The file-access layer rebuilds its allowed-roots set using this resolver, now including project-local directories. Subsequent isFilePathAllowed() checks succeed for paths within .pi/extensions, .agents/skills, and related gated locations.
Can code execute from untrusted projects in Pi Web?
No. The fail-closed design ensures project-specific code paths—extensions, skills, and settings—remain inaccessible until explicit trust is established. The isFilePathAllowed() check in lib/path-security.ts operates as the final barrier, rejecting any file-read attempts targeting roots excluded from the allowed set due to unresolved trust status.
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 →