How Desktop App Restrictions Sync from the Den Control Plane in OpenWork
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 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 |
| 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 supports full CRUD operations:
// 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:
// 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, 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:
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 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:
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 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-configbackground 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 |
Schema definitions, default values, normalization, and calculateEffectiveDesktopPolicy implementation |
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 |
Integration test validating MCP-based policy synchronization |
Updating Policies Programmatically
Organizations with custom tooling can modify desktop restrictions directly against the Den API:
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
desktopPolicyanddesktopPolicyMembertables - MCP capability
getDesktopPoliciesexposes organization policies to authenticated desktop clients - Effective policy calculation merges default and assigned policies with an allow-override algorithm
- Local caching in
desktop-configenables 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.
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 →