How ProjectTrustDialog Works in Pi Web: Trust Status Storage and Security Flow
The ProjectTrustDialog in Pi Web gates execution of user-provided code by checking whether a project requires trust, persisting the user's decision in a JSON-based ProjectTrustStore under ~/.pi/agent, and blocking resource loading until trust is explicitly granted.
Pi Web implements a comprehensive security model where the ProjectTrustDialog serves as the primary interface for user consent before executing potentially unsafe code. This article examines the complete trust flow—from detection and UI presentation to persistent storage—based on the agegr/pi-web source code.
How Project Trust Detection Works
Before displaying the ProjectTrustDialog, Pi Web determines whether a project actually requires trust. This prevents unnecessary interruptions for safe projects.
Detecting Trust-Requiring Resources
The getProjectTrustStatus function in lib/project-trust.ts performs the initial assessment:
// lib/project-trust.ts
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 scans the project directory for:
.pi/extensions— project-specific extensions- Project-specific settings files
.agents/skills— custom agent skills
If none of these gated resources exist, the project is automatically considered trusted without user intervention.
ProjectTrustStore: The Trust Status Storage Mechanism
The ProjectTrustStore class from @earendil-works/pi-coding-agent provides the authoritative storage layer for trust decisions. It persists data as a JSON map under the agent directory.
Storage Location and Format
By default, trust decisions are written to ~/.pi/agent/trust.json. The file contains a simple mapping of project paths to boolean values:
{
"/home/user/projects/my-agent": true,
"/home/user/projects/suspicious-code": false
}
Persisting Trust Decisions
When a user clicks Trust in the dialog, the backend writes the decision via trustProject:
// lib/project-trust.ts
export function trustProject(cwd: string, agentDir: string): ProjectTrustStatus {
const status = getProjectTrustStatus(cwd, agentDir);
if (!status.requiresTrust) return status;
new ProjectTrustStore(agentDir).set(cwd, true);
return { requiresTrust: true, trusted: true };
}
This persistence mechanism ensures trust decisions survive across browser sessions, application restarts, and window reloads.
API Endpoints for Project Trust Status
Pi Web exposes trust status through dedicated Next.js API routes in app/api/project-trust/route.ts.
GET Endpoint: Query Trust Status
// app/api/project-trust/route.ts
export async function GET(req: Request) {
const result = await validateCwd(new URL(req.url).searchParams.get("cwd"));
if ("response" in result) return result.response;
return NextResponse.json(getProjectTrustStatus(result.cwd, getAgentDir()));
}
Returns:
requiresTrust: boolean— whether the project contains gated resourcestrusted: boolean— current trust state fromProjectTrustStore
POST Endpoint: Grant Trust
// app/api/project-trust/route.ts
export async function POST(req: Request) {
const body = await req.json() as { cwd?: unknown };
const result = await validateCwd(body.cwd);
if ("response" in result) return result.response;
const status = trustProject(result.cwd, getAgentDir());
invalidateModelsCache();
await destroyRpcSessionsForCwd(result.cwd);
return NextResponse.json(status);
}
The POST handler performs cleanup after granting trust: it invalidates the models cache and destroys existing RPC sessions for the project, ensuring a fresh security context.
ProjectTrustDialog UI Implementation
The ProjectTrustDialog component in components/ProjectTrustDialog.tsx presents the trust request to users with clear project identification and action buttons.
Dialog Component Structure
// components/ProjectTrustDialog.tsx
export function ProjectTrustDialog({ cwd, busy, error, onCancel, onConfirm }) {
const { t } = useI18n();
return (
<div role="presentation" … onClick={(e)=>!busy && e.target===e.currentTarget && onCancel()}>
<div role="dialog" aria-modal="true" …>
<div>
<div id="project-trust-title">{t("trust.dialogTitle")}</div>
<div>{t("trust.dialogBody")}</div>
<code>{cwd}</code>
{error && <div role="alert">{error}</div>}
</div>
<button onClick={onCancel} disabled={busy}>{t("trust.cancel")}</button>
<button onClick={onConfirm} disabled={busy}>
{busy ? t("trust.trusting") : t("trust.trustProject")}
</button>
</div>
</div>
);
}
The dialog includes:
- Project path display — shows the exact directory being trusted
- Loading states —
busyprop prevents duplicate submissions - Error handling — displays backend validation failures
- Keyboard accessibility — standard dialog roles and modal behavior
Conditional Rendering in AppShell
The AppShell component orchestrates when the ProjectTrustDialog appears:
// components/AppShell.tsx
useEffect(() => {
setProjectTrust(null);
setProjectTrustDialogOpen(false);
if (!projectTrustCwd) return;
fetch(`/api/project-trust?cwd=${encodeURIComponent(projectTrustCwd)}`)
.then(r => r.json())
.then(data => setProjectTrust(data))
.catch(err => console.error(err));
}, [projectTrustCwd]);
// Render
{projectTrustDialogOpen && projectTrustCwd && (
<ProjectTrustDialog
cwd={projectTrustCwd}
busy={projectTrustBusy}
error={projectTrustError}
onCancel={() => setProjectTrustDialogOpen(false)}
onConfirm={() => void handleTrustProject()}
/>
)}
The dialog opens automatically when projectTrust.requiresTrust && !projectTrust.trusted.
Gating Resource Loading with Project Trust
The trust status storage mechanism protects all project-local resource loading through projectTrustReloadOptions.
Trust-Based Resource Gate
// lib/project-trust.ts
export function projectTrustReloadOptions(cwd: string, agentDir: string) {
const status = getProjectTrustStatus(cwd, agentDir);
if (!status.requiresTrust) return undefined;
const trustStore = new ProjectTrustStore(agentDir);
return { resolveProjectTrust: async () => trustStore.get(cwd) === true };
}
Resource loaders like rpc-manager and skills-service call this helper to obtain a resolveProjectTrust promise. They await resolution before:
- Loading extensions
- Initializing RPC sessions
- Executing project-specific skills
This ensures untrusted projects cannot execute arbitrary code even if other security checks are bypassed.
Practical Code Examples
Client-Side Trust Status Hook
import { useEffect, useState } from "react";
function useProjectTrust(cwd: string) {
const [status, setStatus] = useState<ProjectTrustStatus | null>(null);
useEffect(() => {
fetch(`/api/project-trust?cwd=${encodeURIComponent(cwd)}`)
.then(r => r.json())
.then(setStatus)
.catch(console.error);
}, [cwd]);
return status;
}
Programmatic Trust Granting
async function trustProject(cwd: string) {
const resp = await fetch("/api/project-trust", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cwd }),
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error ?? "Failed");
return data; // { requiresTrust: true, trusted: true }
}
Protected Resource Loading
import { projectTrustReloadOptions } from "@/lib/project-trust";
async function loadExtensions(cwd: string) {
const reloadOpts = projectTrustReloadOptions(cwd, getAgentDir());
if (reloadOpts) {
const trusted = await reloadOpts.resolveProjectTrust();
if (!trusted) throw new Error("Project not trusted");
}
// safe to import extensions here …
}
Summary
-
Project trust detection in
lib/project-trust.tsscans for gated resources (extensions, settings, skills) and consultsProjectTrustStoreto determine if user interaction is required. -
Trust status storage mechanism persists decisions as a JSON map at
~/.pi/agent/trust.json, keyed by absolute project path, enabling trust decisions to survive across sessions. -
API endpoints (
GETandPOST /api/project-trust) expose trust operations to the frontend with proper validation and cleanup handling. -
ProjectTrustDialog renders conditionally based on
AppShellstate, presenting the project path and capturing explicit user consent. -
Resource gating through
projectTrustReloadOptionsensures no code execution occurs until trust is obtained from the store, as implemented inrpc-managerand related services.
Frequently Asked Questions
Where is the ProjectTrustDialog defined in the Pi Web codebase?
The ProjectTrustDialog is defined in components/ProjectTrustDialog.tsx (lines 5-42). It accepts cwd, busy, error, onCancel, and onConfirm props, rendering an accessible modal dialog with the project path and trust action buttons.
How does the trust status storage mechanism work across browser restarts?
The ProjectTrustStore class writes to a JSON file at ~/.pi/agent/trust.json by default. This file-based storage survives browser restarts, application updates, and machine reboots because it lives in the user's home directory outside any cache or temporary storage.
What triggers the ProjectTrustDialog to appear?
The dialog appears when three conditions are met: the current working directory (cwd) contains trust-requiring resources (detected by hasTrustRequiringProjectResources), the ProjectTrustStore has no positive entry for that path, and the user navigates to or opens that project in AppShell.
Can trust decisions be revoked programmatically?
The source code in lib/project-trust.ts only exposes set(cwd, true) through the trustProject function. Direct revocation would require calling ProjectTrustStore.set(cwd, false) or manually editing the trust.json file, as no explicit "untrust" API endpoint is implemented in app/api/project-trust/route.ts.
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 →