# How Desktop App Restrictions Sync from the Den Control Plane in OpenWork

> Learn how OpenWork syncs desktop app restrictions using its Den control plane. Discover the policy-based architecture and how features are controlled locally.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-16

---

**OpenWork syncs desktop app restrictions from the Den control plane through a policy-based architecture where the Den "org" service stores JSON policies, the MCP exposes them via `getDesktopPolicies`, and the desktop client calculates an effective policy that controls feature gates locally.**

The OpenWork ecosystem separates policy management from policy enforcement. Rather than embedding restrictions directly in the desktop application, all **Desktop Policies** live in the Den control plane—the multi-tenant "org" service that administrators interact with through the web UI. This architecture ensures centralized governance while allowing real-time restriction updates across distributed desktop clients.

## What Is a Desktop Policy in OpenWork?

A **Desktop Policy** is a JSON document that defines which features a user can access in the OpenWork desktop application. Policies follow the schema defined in [`packages/types/src/den/desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/desktop-policies.ts) and control flags such as `allowCustomProviders`, `allowLocalModels`, or `allowExternalIntegrations`.

Administrators manage two types of policies:

- **Default policy** — applied organization-wide when no specific assignment exists
- **Assigned policies** — targeted to individual members or teams, overriding or extending the default

The desktop application never hard-codes these values. Instead, it treats Den as the single source of truth and computes permissions dynamically at runtime.

## The Five-Step Synchronization Flow

The sync process from Den to desktop follows a predictable pipeline:

| Step | Action | Key Component |
|------|--------|---------------|
| 1 | Admin creates or edits a policy in Den UI | [`desktop-policy-editor-screen.tsx`](https://github.com/different-ai/openwork/blob/main/desktop-policy-editor-screen.tsx) |
| 2 | Den persists policy to database | `desktopPolicy` and `desktopPolicyMember` tables |
| 3 | Desktop app fetches via MCP | `getDesktopPolicies` capability |
| 4 | Client calculates effective policy | `calculateEffectiveDesktopPolicy` |
| 5 | App caches and enforces locally | `desktop-config` store with hourly refresh |

### Step 1: Policy Creation and Storage

When an org owner modifies restrictions through the Den dashboard, the web client sends requests to the REST API at `/v1/desktop-policies`. The route handler in [`ee/apps/den-api/src/routes/org/desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/org/desktop-policies.ts) supports full CRUD operations:

```ts
// From den-api/src/routes/org/desktop-policies.ts
// GET /v1/desktop-policies - list policies
// POST /v1/desktop-policies - create new policy
// PATCH /v1/desktop-policies/:id - update existing policy
// DELETE /v1/desktop-policies/:id - remove policy

```

Policies are stored in two tables: `desktopPolicy` holds the policy documents, while `desktopPolicyMember` tracks which users or teams receive assigned policies.

### Step 2: Desktop Client Discovery via MCP

The OpenWork desktop application communicates with Den through the **Model Context Protocol (MCP)**. During authentication, or when triggered by specific events, the desktop client searches for and invokes the `getDesktopPolicies` capability:

```ts
// Using the OpenWork MCP client from any agent
import { searchCapabilities, executeCapability } from "openwork-mcp";

async function loadDesktopPolicy() {
  // Discover the capability
  const caps = await searchCapabilities("desktop policy");
  if (!caps.includes("getDesktopPolicies")) {
    throw new Error("MCP does not expose desktop-policy tools");
  }

  // Execute the tool
  const result = await executeCapability("getDesktopPolicies");
  if (result.error) {
    throw new Error(`MCP error: ${result.error.message}`);
  }
  // result.body contains { desktopPolicy: … }
  return result.body.desktopPolicy;
}

```

This flow is validated by the end-to-end test in [`evals/flows/desktop-policies-cloud-mcp.flow.ts`](https://github.com/different-ai/openwork/blob/main/evals/flows/desktop-policies-cloud-mcp.flow.ts), which proves capability discovery and policy retrieval work correctly across the network boundary.

### Step 3: Effective Policy Calculation

Raw policies from Den are not applied directly. The desktop client runs `calculateEffectiveDesktopPolicy` to merge multiple policy sources into a single authoritative configuration:

```ts
import {
  calculateEffectiveDesktopPolicy,
  normalizeDesktopPolicyDocument,
} from "@/packages/types/src/den/desktop-policies";

function effectivePolicy(defaultPolicy: unknown, assignedPolicies: unknown[]) {
  const effective = calculateEffectiveDesktopPolicy({
    orgPolicyCount: assignedPolicies.length,
    defaultPolicy,
    assignedPolicies,
  });
  return effective; // => Required<DesktopPolicyValue>
}

```

The algorithm in [`desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/desktop-policies.ts) implements an **"allow-style" Boolean merge** where `true` wins. If any assigned policy grants a permission, the user receives that permission—even if the default policy denies it. This enables flexible, layered access control.

### Step 4: Local Enforcement and Caching

The computed `Required<DesktopPolicyValue>` is stored in the `desktop-config` cache. Desktop components access this cache through hooks like `useDesktopConfig`:

```tsx
import { useDesktopConfig } from "@/hooks/useDesktopConfig";

export function ProviderSelector() {
  const cfg = useDesktopConfig(); // returns the effective policy
  if (!cfg.allowCustomProviders) {
    return null; // UI is hidden because the org disabled custom providers
  }
  return <CustomProviderList />;
}

```

This pattern appears throughout [`desktop-policy-editor-screen.tsx`](https://github.com/different-ai/openwork/blob/main/desktop-policy-editor-screen.tsx) and related components. Feature gates consult the cached policy synchronously, ensuring UI responsiveness.

### Step 5: Automatic Refresh Triggers

The desktop client refreshes its policy cache without user intervention when any of these events occur:

- The active Cloud organization changes
- The Cloud account token changes
- The hourly `desktop-config` background refresh runs

When an admin updates a policy in Den, the change propagates on the next fetch cycle. The UI immediately reflects new restrictions—features disappear or become enabled based on the recalculated effective policy.

## Key Source Files for Desktop Policy Sync

Understanding the complete flow requires familiarity with these files:

| File Path | Purpose |
|-----------|---------|
| [`packages/types/src/den/desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/desktop-policies.ts) | Schema definitions, default values, normalization, and `calculateEffectiveDesktopPolicy` implementation |
| [`ee/apps/den-api/src/routes/org/desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/org/desktop-policies.ts) | REST API endpoints for policy CRUD operations |
| `ee/apps/den-web/app/(den)/dashboard/_components/desktop-policy-editor-screen.tsx` | Admin UI for policy management |
| `packages/docs/cloud/share-with-your-team/desktop-policies.mdx` | User-facing documentation explaining policy controls |
| [`evals/flows/desktop-policies-cloud-mcp.flow.ts`](https://github.com/different-ai/openwork/blob/main/evals/flows/desktop-policies-cloud-mcp.flow.ts) | Integration test validating MCP-based policy synchronization |

## Updating Policies Programmatically

Organizations with custom tooling can modify desktop restrictions directly against the Den API:

```ts
import fetch from "node-fetch";

async function patchDesktopPolicy(policyId: string, patch: any, token: string) {
  const resp = await fetch(
    `https://api.openworklabs.com/v1/desktop-policies/${policyId}`,
    {
      method: "PATCH",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(patch),
    },
  );
  if (!resp.ok) {
    const err = await resp.json();
    throw new Error(`Den error ${resp.status}: ${err.error}`);
  }
  return await resp.json(); // Returns the updated policy
}

```

Changes made through this endpoint become visible to desktop clients on their next policy fetch.

## Summary

- **Den control plane** stores all desktop policies in `desktopPolicy` and `desktopPolicyMember` tables
- **MCP capability** `getDesktopPolicies` exposes organization policies to authenticated desktop clients
- **Effective policy calculation** merges default and assigned policies with an allow-override algorithm
- **Local caching** in `desktop-config` enables fast feature gate checks with hourly refresh
- **Automatic propagation** ensures restriction changes reach clients without manual action

This architecture centralizes governance while maintaining performant, offline-capable enforcement—critical for desktop applications that must respect organizational boundaries regardless of network conditions.

## Frequently Asked Questions

### How quickly do policy changes reach desktop clients?

Policy changes propagate on the next fetch cycle, which occurs when the user switches organizations, refreshes their authentication token, or during the automatic hourly refresh. Most clients receive updates within 60 minutes, though active users will see changes immediately if they trigger a manual refresh.

### Can desktop clients override or ignore Den policies?

No. The desktop application treats Den as the authoritative source. While policies are cached locally for performance, the cache is read-only from the client's perspective. The `calculateEffectiveDesktopPolicy` function runs client-side for latency reasons, but it operates strictly on data retrieved from the MCP—no local overrides exist in the source code.

### What happens if the desktop client cannot reach Den?

The client continues operating with its cached effective policy from the last successful fetch. Feature gates use the stale cache rather than failing open or closed. Once connectivity restores, the next successful `getDesktopPolicies` call refreshes the cache and applies any pending restriction changes.