Default trust boundary
By default Desk binds 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
gitandghwith 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
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: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)
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 providerrequestAuthentication: independent operator and machine identity adaptersrequestAccess: owned routes with their operation, identity policy and handler; core authenticates and authorizes before dispatchmiddleware: Connect middleware mounted after core authority admission and before the core/apirouterroutes: legacy v1/apihandlers; v2 declares handlers inrequestAccess.routesupgradeGuard: a central predicate for WebSocket upgradeschannels: providers that replace or wrap parts of the Channels subsystemsetup: 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:
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:
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
SetDESK_PLUGINS to a comma-separated list of module specifiers:
- a
DeskPlugin - a default
DeskPlugin - a factory that returns a
DeskPlugin
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
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. RuntimeDESK_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
- Follow Run Desk securely for the localhost and SSH tunnel model.
- Read API and runtime reference for the routes that plugins can extend.
- Read Troubleshooting and FAQ for startup and connection symptoms.
Request identity and admission
A plugin declaresrequestAuthorityVersion: 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.