Desk runs with the host user’s operating-system privileges. Request authentication separates operator, managed-agent and machine capabilities; it does not isolate processes sharing the host account. The stock server requires a local operator credential for operator API access. Hosted installations require a configured gateway operator issuer. Managed agent credentials grant only their scoped publication and producer operations; they do not grant shell, filesystem or administrative access. See Channels authentication.

Default trust boundary

By default Desk binds to:
Keep that default. Do not expose Desk directly on a network. If Desk runs on your own remote development box, keep it bound to 127.0.0.1 there and reach it through SSH port forwarding.

What a connected client can do

An authenticated operator can:
  • view and type into agent terminals
  • start, restart, and delete configured sessions
  • operate git and gh with the host user’s credentials
  • read and write files under the active explorer root
  • use notes under ~/.config/desk/notes
  • post and read channel messages
  • upload channel files
  • run the emergency kill switch
Treat the Desk port as equivalent to local operator access for the selected workspace and running agent fleet.

Filesystem boundary

The file API is constrained to the active explorer root. Desk resolves client paths and rejects path escapes. The manifest file is a special trusted file so the UI can open it even when it is outside the explorer root. The notes subsystem uses its own pinned root:
Pick editor roots carefully. The root you choose is the filesystem trust boundary for editor file operations.

Channel uploads

Channel uploads are stored under the channel files directory. Desk serves uploaded files with restricted headers, including a sandboxing content security policy and forced download behavior for active content. Treat uploaded files as untrusted, especially when they were produced by an agent.

Emergency kill switch scope

The kill switch retires configured sessions and performs a host-wide sweep for supported agent processes. It can terminate:
  • every Moor session in the active manifest
  • every explorer terminal opened from the editor tree
  • any remaining host agent CLI process found by ps: every provider in the agent registry (codex, claude, opencode, qwen, kimi, grok)
The process sweep is intentionally broader than the active manifest. Use the control only as an emergency stop.

Plugin extension points

Desk exposes a small backend plugin interface for local embedders and downstream builds. It is an extension surface, not a replacement for built-in user accounts or request authentication. A plugin can provide:
  • channelsAuthentication: a trusted gateway operator authentication provider
  • requestAuthentication: independent operator and machine identity adapters
  • requestAccess: owned routes with their operation, identity policy and handler; core authenticates and authorizes before dispatch
  • middleware: Connect middleware mounted after core authority admission and before the core /api router
  • routes: legacy v1 /api handlers; v2 declares handlers in requestAccess.routes
  • upgradeGuard: a central predicate for WebSocket upgrades
  • channels: providers that replace or wrap parts of the Channels subsystem
  • setup: lifecycle code that runs when Desk installs the plugin

Channels providers

channels takes one provider per Channels port. Each receives a factory for the implementation Desk would otherwise use and returns the one to use instead, so a plugin can wrap rather than reimplement:
The six ports are store (where conversations live, their reactions and stars, and how a finalised message is noticed), files (where attachments live), views (saved view filters), router (who a message is for), delivery (what reaches an agent), and renderer (what an agent sees). Providers apply in plugin order, each wrapping the previous result; the factory is memoised, so calling it twice yields the same instance. A provider that never calls the factory replaces the part outright, and Desk does not build the stock implementation at all — no watcher is started for a store that pushes its own changes. A provider that returns base() unchanged is a no-op, and declaring none leaves Channels exactly as it ships. Every store operation is asynchronous, so an implementation may be backed by anything reachable over a network, not only a local disk. The contracts are in src/server/channels/ports.ts. A Channels provider is the most intrusive thing a plugin can do: it sits on the path that types text into a live agent session. Treat it with the same care as upgradeGuard. Runtime plugin modules can export a plain plugin object:
The typed helper defineDeskPlugin exists at src/server/plugin.ts. Source-backed installs retain the application source and locked dependencies, so local embedders can build against the same release tree.

Runtime plugins

Set DESK_PLUGINS to a comma-separated list of module specifiers:
Each module must export either:
  • a DeskPlugin
  • a default DeskPlugin
  • a factory that returns a DeskPlugin
Unset DESK_PLUGINS selects the built-in local operator authentication.

WebSocket guards

upgradeGuard runs before any WebSocket bridge handles a socket. It is useful for embedded builds that need a local runtime policy, but it does not make the stock Desk server safe to expose publicly. It covers:
  • terminal WebSocket connections
  • filesystem watch sockets
  • LSP sockets
All plugin guards must allow the request. If any guard rejects, Desk closes the upgrade before the subsystem sees it.

Embedded plugins

The private Bun build has a build-time seam for embedding plugins directly into a downstream runtime. Desk’s own runtime embeds no plugins. Runtime DESK_PLUGINS works with both desk serve and desk serve --dev. The private Bun runtime loads runtime plugins first, then appends any embedded plugins supplied by the build.

Error surfaces

Public API routes return JSON errors. Unexpected route failures are collapsed to terse messages instead of exposing stacks. Plugins should follow the same pattern and avoid returning secrets or stack traces.

Next steps

Request identity and admission

A plugin declares requestAuthorityVersion: 2 for independent authentication and owned routes. Core validates names, versions, namespaces, issuers and route patterns before acquiring runtime ownership, constructing services or invoking plugin setup. Plugin settings contain data, never grants or authority policies.
requestAuthentication registers identity adapters independently of routes. Every v2 adapter provides matches(req) and authenticate. Recognition selects one credential profile without granting authority; an invalid selected profile fails without falling back to another identity. Multiple recognized profiles are ambiguous. Operator adapters enter the existing Channels authority checks; local operator credentials and managed-agent proofs remain built-in sources. Only operator adapters select gateway mode. Machine adapters alone do not. A machine adapter receives the requested operation and exact request resource. It returns a service, peer, or workspace principal with a finite expiry and explicit operation/resource grants. Core checks the registered kind and issuer, expiry, grant and route’s identity selector, including subject where specified. A matching grant cannot replace the route owner’s policy. Missing optional issuers leave that alternative unavailable; they do not prevent operator access or expand another provider’s rights. Each /api/<name> namespace has one owner. Core namespaces are reserved, including operator-only APIs. Paths support literals and whole-segment :name parameters; overlapping paths of the same method are rejected. Encoded separators and traversal aliases cannot select privileged routes. Admission and dispatch use the same declaration. A v2 handler is called exclusively; declining the request produces 404, never fallthrough into another plugin’s handler. Middleware runs after admission and may reject a request, but cannot transfer its admission to a different method or URL. Gateway HTML and static resources also receive a verified operator context before downstream middleware. Machine requests carry their verified request principal and a restricted native service principal. RequestOnly, the default, permits no Channels mutation. Routes declare additional native operations explicitly; MessageAppend and MessageEdit are forbidden for machine identities. Peer history intake remains historical, never an attested local operator or agent publication. WebSocket admission still requires operator authority. The lightweight host ports are createPluginRequestAccessRegistry(plugins), createRequestAuthorityMiddleware(authority, registry) and createDeskApiMiddleware(legacyRoutes). The same registry selects admission and binds the v2 handler to the current request. registry.operatorAuthentication contains only the configured operator adapters.

Compatibility

DeskPluginContext.requestAuthority supplies the version declared by that plugin and currentPrincipal() inside an admitted request. Plugins without an explicit version receive v1. V2 plugins check the context version at the start of setup, before consuming bootstrap or starting workers, to refuse older Desk hosts. V1 channelsAuthentication remains an operator-only alias. Its requestAccess: { namespace, authentication, policies } machine adapters remain local to their declaring access object; they are not independent v2 providers. Legacy opaque routes retain their previous dispatch path. A v2 plugin cannot also register legacy routes or the legacy authentication alias. Built-in LSP, GitHub webhook, writer-authority and journal transports use the same registry through v1 declarations. Their adapters retain existing capabilities, HMAC, peer proof, body-integrity and project/workspace scope checks. Their native authority is unchanged. No manifest migration, grant installation, new token format or direct plugin-to-plugin invocation is required.