# PI-Desktop Approval Modes: Tool vs Contract Permissions Explained

> Understand PI-Desktop approval modes: tool permissions like allow-once and contract permissions such as read, write, or admin. Learn how they work.

- Repository: [Lan/PI-Desktop](https://github.com/vastsa/PI-Desktop)
- Tags: deep-dive
- Published: 2026-09-12

---

**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`](https://github.com/vastsa/PI-Desktop/blob/main/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 only
- **`allow-session`** – Permits the action for the duration of the current session
- **`deny`** – 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 proposal
- **`reject`** – 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`](https://github.com/vastsa/PI-Desktop/blob/main/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`, populating `allowedDecisions` based on the tool's risk profile and session settings
- **Plan/goal requests**: Created by `ApprovalBroker.fromPlanningState`, populating both `allowedDecisions` (`["approve","reject"]`) and `allowedPermissionModes` (from `RACP_PERMISSION_MODES`)

### Phase 2: UI Presentation

The `PlanApprovalBar` component in [`apps/desktop/src/components/PlanApprovalBar.tsx`](https://github.com/vastsa/PI-Desktop/blob/main/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 `expectedRevision` to prevent stale approvals

Upon validation, the broker calls the `ApprovalPort` implementation:
- `port.resolveTool` for tool decisions
- `port.resolveContract` for 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

```typescript
// 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

```typescript
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

```typescript
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`](https://github.com/vastsa/PI-Desktop/blob/main/packages/agent-host/src/approvals.ts)** – Contains the `ApprovalBroker` class that creates, tracks, resolves, and settles all approvals
- **[`packages/shared/src/racp.ts`](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/racp.ts)** – Defines the central schema constants: `RACP_TOOL_APPROVAL_DECISIONS`, `RACP_CONTRACT_APPROVAL_DECISIONS`, and `RACP_PERMISSION_MODES`
- **[`packages/shared/src/types/plans.ts`](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/types/plans.ts)** – Types describing plan/goal proposals, including `PlanApprovalAction`, `PlanApprovalStatus`, and `RacpApprovalRequest` shapes
- **[`apps/desktop/src/components/PlanApprovalBar.tsx`](https://github.com/vastsa/PI-Desktop/blob/main/apps/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`/`reject` plus permission modes)
- The `ApprovalBroker` class in [`packages/agent-host/src/approvals.ts`](https://github.com/vastsa/PI-Desktop/blob/main/packages/agent-host/src/approvals.ts) manages the complete lifecycle from creation to resolution
- Tool requests use `fromToolPermission` and resolve via `port.resolveTool` without permission modes
- Contract requests use `fromPlanningState` and resolve via `port.resolveContract` with mandatory permission modes from `RACP_PERMISSION_MODES`
- All approvals enforce TTL expiration through `expiresAtMs` and support session-based cancellation via `cancelForSession`

## 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`](https://github.com/vastsa/PI-Desktop/blob/main/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`](https://github.com/vastsa/PI-Desktop/blob/main/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`](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/types/plans.ts) and [`packages/shared/src/racp.ts`](https://github.com/vastsa/PI-Desktop/blob/main/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.