PI-Desktop Approval Modes: Tool vs Contract Permissions Explained
PI-Desktop implements two distinct approval families—tool-level requests offering decisions like allow-once and allow-session, and contract-level requests requiring approve or reject decisions paired with permission modes such as read, write, or admin.
PI-Desktop (vastsa/PI-Desktop) is an agent host framework that governs how AI tools and planning contracts receive user authorization. Understanding the approval modes in PI-Desktop is essential for developers integrating custom tools or managing plan proposals, as the system distinguishes between transient tool permissions and persistent contract access levels using a typed broker architecture.
The Two Families of Approval Requests
The codebase separates approval logic into tool-level and contract-level families, each with distinct decision sets and permission requirements defined in packages/shared/src/racp.ts.
Tool-Level Requests
Tool-level approvals govern individual tool executions. When a tool requests permission—for example, to run a shell command or fetch external data—the system creates a RacpApprovalRequest via ApprovalBroker.fromToolPermission.
Users choose from three decision values:
allow-once– Permits the action a single time onlyallow-session– Permits the action for the duration of the current sessiondeny– Blocks the action entirely
Tool requests do not utilize permission modes; access is binary based on the decision selected.
Contract-Level Requests
Contract-level approvals handle plan or goal proposals. These are generated via ApprovalBroker.fromPlanningState and present users with contract approval decisions:
approve– Accepts the proposalreject– Declines the proposal
Unlike tool requests, contract approvals require a permission mode when approved. The available values are exported as RACP_PERMISSION_MODES (e.g., read, write, admin) and are presented to the user through the PlanApprovalBar component in the desktop UI.
The Approval Request Lifecycle
The approval flow follows a strict six-phase pipeline managed by the ApprovalBroker class in packages/agent-host/src/approvals.ts.
Phase 1: Request Creation
The broker instantiates RacpApprovalRequest objects differently depending on the source:
- Tool requests: Created by
ApprovalBroker.fromToolPermission, populatingallowedDecisionsbased on the tool's risk profile and session settings - Plan/goal requests: Created by
ApprovalBroker.fromPlanningState, populating bothallowedDecisions(["approve","reject"]) andallowedPermissionModes(fromRACP_PERMISSION_MODES)
Phase 2: UI Presentation
The PlanApprovalBar component in apps/desktop/src/components/PlanApprovalBar.tsx renders the request to the user. For tool requests, it displays decision buttons. For contract requests, it shows a dropdown labeled "Choose approval mode" containing the permitted permission modes alongside approve/reject options.
Phase 3: User Selection
For tools, the user selects a decision value. For contracts, selecting approve requires choosing a permission mode (e.g., "write"), while reject requires none. The UI constructs a RacpApprovalResponse containing approvalId, decision, and optionally permissionMode.
Phase 4: Resolution and Validation
ApprovalBroker.resolve validates the response against the stored request:
- Verifies the decision exists in
allowedDecisions - Validates the permission mode against
allowedPermissionModes(for contracts) - Checks the request has not expired using
expiresAtMs - Validates the
expectedRevisionto prevent stale approvals
Upon validation, the broker calls the ApprovalPort implementation:
port.resolveToolfor tool decisionsport.resolveContractfor contract decisions (passing the selected permission mode)
Phase 5: Result Recording
The broker records the outcome in its internal results map, removes the pending request, and returns a RacpApprovalResult. The result status is set to resolved, canceled, or expired. Subsequent responses for the same approval ID receive status alreadyResolved.
Phase 6: Expiration and Cancellation
Approvals carry a TTL defined by lifetimeMs. If expiresAtMs passes before resolution, ApprovalBroker automatically settles the request with status expired. When a session ends, cancelForSession closes all pending approvals for that session with status canceled.
Code Implementation Examples
Creating Tool Approval Requests
// Inside the agent host (packages/agent-host/src/approvals.ts)
const toolRequest: ToolPermissionRequest = {
requestId: "t123",
sessionId: "s1",
toolName: "curl",
reason: "fetch external data",
risk: "network",
};
const approval = approvalBroker.fromToolPermission(toolRequest, {
turnId: "turn42",
revision: 3,
lifetimeMs: 5 * 60_000, // 5 minutes TTL
allowSession: false, // Restricts to allow-once or deny only
});
When allowSession is false, approval.allowedDecisions contains ["allow-once","deny"]. When true, it includes "allow-session".
Creating Plan Approval Requests
const planningEvent: PlanningStateEvent = {
sessionId: "s1",
state: "awaiting_approval",
kind: "plan",
proposalId: "p99",
title: "Deploy new feature",
markdown: "...",
version: 2,
};
const approval = approvalBroker.fromPlanningState(planningEvent, {
turnId: "turn42",
revision: 5,
lifetimeMs: 10 * 60_000, // 10 minutes TTL
});
This populates approval.allowedDecisions with ["approve","reject"] and approval.allowedPermissionModes with values exported as RACP_PERMISSION_MODES (e.g., ["read","write","admin"]).
Resolving Approval Responses
const response: RacpApprovalResponse = {
approvalId: "p99",
decision: "approve",
permissionMode: "write",
context: { expectedRevision: 5 },
};
const result = await approvalBroker.resolve(response, principal, currentRevision);
The broker validates the revision, invokes port.resolveContract with the "write" permission mode, and returns a RacpApprovalResult with status resolved.
Core Architecture and Source Files
The approval mode system spans four critical files:
packages/agent-host/src/approvals.ts– Contains theApprovalBrokerclass that creates, tracks, resolves, and settles all approvalspackages/shared/src/racp.ts– Defines the central schema constants:RACP_TOOL_APPROVAL_DECISIONS,RACP_CONTRACT_APPROVAL_DECISIONS, andRACP_PERMISSION_MODESpackages/shared/src/types/plans.ts– Types describing plan/goal proposals, includingPlanApprovalAction,PlanApprovalStatus, andRacpApprovalRequestshapesapps/desktop/src/components/PlanApprovalBar.tsx– React component rendering the approval mode dropdown and decision interface
Summary
- PI-Desktop distinguishes tool-level approvals (
allow-once,allow-session,deny) from contract-level approvals (approve/rejectplus permission modes) - The
ApprovalBrokerclass inpackages/agent-host/src/approvals.tsmanages the complete lifecycle from creation to resolution - Tool requests use
fromToolPermissionand resolve viaport.resolveToolwithout permission modes - Contract requests use
fromPlanningStateand resolve viaport.resolveContractwith mandatory permission modes fromRACP_PERMISSION_MODES - All approvals enforce TTL expiration through
expiresAtMsand support session-based cancellation viacancelForSession
Frequently Asked Questions
What are the available decision values for tool permission requests?
Tool permission requests support three decisions defined in RACP_TOOL_APPROVAL_DECISIONS: allow-once (single execution), allow-session (session-wide access), and deny (blocking the action). The available choices depend on the allowSession parameter passed to ApprovalBroker.fromToolPermission.
How do permission modes work for contract approvals?
When a user approves a contract-level request (plan or goal), they must select a permission mode from the RACP_PERMISSION_MODES array (e.g., read, write, admin). This value is passed through port.resolveContract and determines the access level granted to the approved plan. The available modes are defined centrally in packages/shared/src/racp.ts and exposed via the PlanApprovalBar UI component.
Where is the approval logic implemented in the PI-Desktop codebase?
The core logic resides in packages/agent-host/src/approvals.ts within the ApprovalBroker class. This file handles request creation via fromToolPermission and fromPlanningState, validation in the resolve method, and lifecycle management including expiration and cancellation. The type definitions live in packages/shared/src/types/plans.ts and packages/shared/src/racp.ts.
What happens when an approval request expires?
If the TTL (lifetimeMs) elapses before user action, the ApprovalBroker automatically settles the request with status expired. The broker maintains the expiresAtMs timestamp and removes expired requests from the pending queue. Expired approvals return a RacpApprovalResult with status expired and cannot be resolved afterward.
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 →