Bees is an open source desktop app that runs a team of AI agents on your own computer. Earlier posts covered the review loop, where keys live, and team work without a central dispatcher. This one is for people who want to read the code.
Three processes and a webview
Bees is a Tauri app. The Rust side in src-tauri/src is small. lib.rs is about 1,100 lines and local_models.rs about 1,300. It owns the window, the tray, the updater and the installer. Its main job is to start and supervise other processes.
When the window opens, it shows a splash screen (src/entry.ts, under 40 lines) and calls one Tauri command, ensure_dsh_runtime. That command does the following:
- Starts a local Temporal dev server on a random loopback port, backed by a SQLite file in the app data folder. Temporal is the workflow engine that remembers which stage each piece of work is at, so a restart doesn't lose its place.
- Starts a bundled Node 24 runtime running
dsh-runtime/start.mjson a second loopback port, which it remembers between launches, with a fresh random token. - Waits for
/healthz, then points the webview at the runtime's own URL with that token.
From that moment the whole interface is served by the Node process. The Rust side keeps polling /healthz every two seconds. If the runtime stops answering for about 30 seconds, it sends the window back to the splash, which starts a new one. A third process, llama-server from llama.cpp, starts only when you run a local model.
The Node runtime is DSH, short for DeepSeek Harness, an open source agent runtime. DSH gives us model providers, a credential store, sessions, the agent loop, tools, skills, an MCP client and approvals. MCP is the standard way to plug outside tools into an agent. Bees is written as DSH plugins on top of it:
-
dsh-runtime/pluginis the product: the board, schedules, runs, recovery, file boundaries, document search, audit records and the UI shell. The server code is inplugin/lib, the UI is inplugin/client. -
dsh-runtime/plugins/*holds the model connections:local-ai,subscriptions(Claude Code and Codex),free-aiandcustom-ai.
Product data lives in one SQLite file, bees-stage1.db. Tauri passes its path, along with every other folder the runtime needs, as environment variables when it starts Node.
How a run executes
A request becomes a work item in a process. A process is a list of stages, such as Work, Review and Done. Temporal drives the item from stage to stage. Each stage runs as a fresh agent session in its own run folder, with inputs/ and outputs/. The only files a stage carries in are the ones the previous worker stage left. The agent's base instructions live in RUN_PERSONA in dsh-runtime/plugin/lib/agent-runtime.js, which is the biggest server file at about 2,600 lines.
Before every tool call, agent-runtime.js checks the call. Paths outside the run folder are refused until the person approves that exact path. Folders that hold passwords, keys or Bees' own files can't be read at all. Small local models often write to /outputs/report.md with a leading slash, so the refusal tells them the relative path instead of only saying no.
Agents that edit shared files go through a lock tool. docs/file-access.md covers it. The agent acquires every destination path, reads again after the lock is granted, writes, and releases. Each replacement is written to a staging file and renamed over the target, so a crash never leaves a half-written file. Shell commands can still bypass these locks, and the doc says so.
The restart problem
This was the first trade-off we had to make on purpose. Bees can restart in the middle of a tool call. The app might crash, or an update might install. When it comes back, Temporal knows which stage was running, but nobody knows whether the last call finished.
For reading tools that doesn't matter. Running grep twice is fine. For a tool that sends an email or posts a reply, running it again could send it twice. Skipping it could lose it.
So recoveryToolContext in agent-runtime.js sorts the calls. A fixed list, REPEATABLE_TOOLS, holds the calls that are safe to repeat: reads, searches, fetches, and the stage submit, which commits in one transaction. Any other call without a recorded result stops the run with a message asking the owner to check whether the action happened. Once the owner continues, the agent gets the finished calls from the interrupted turn as evidence. It's told to reuse those results and to find out what the unknown call did before trying it again.
The cost is that some runs stop and wait for a person after a crash, even when the call never reached the outside service. We tried auto-retrying first. A run that hit this state once stayed stuck for good, and a retry that guessed wrong would have sent something twice. Asking the owner is slower, but it can't double-send.
Tools and add-ons
DSH provides skills and built-in tools. MCP servers are managed by Bees. Each enabled server is a row in bees-stage1.db, and Capabilities in plugin/lib/capabilities.js mounts one long-lived process for each row. Secrets go in DSH's credential store, never in the database. The servers shipped out of the box are listed in plugin/lib/mcp-catalog.js, and none of them installs without a review screen.
Each agent has an MCP access policy: all, none, or a named list. The browser is the exception to one process per server. Two runs browsing at the same time used to land on each other's pages, so each run now mounts its own browser server, which goes away when the run ends.
Approval is enforced at the tool call too, not only at the end of the stage. We started by checking approval when a stage finished. Then we noticed a bid or an email could go out before anyone had seen it. Now, if a stage needs approval, any MCP call that sends something has to be preceded by an approval recorded after the last outgoing call. This includes any non-GET API request and any browser step. So one approval covers one send, and a later rejection wins. Local-only servers like filesystem, memory and time skip the check. The query reads the run's audit events in SQLite, so the rule survives a restart.
Models: local, subscriptions and keys
The second trade-off is how we use Claude Code and Codex. For Claude Code, we don't hand the CLI our tools or let it run its own loop. plugins/subscriptions/lib/index.js calls the CLI with its own tools turned off and an empty MCP config. It passes a JSON schema that forces every reply into one shape: a tool name, its arguments, and text.
--tools "" --strict-mcp-config --mcp-config {"mcpServers":{}} --json-schema {...}
Bees then runs the tool itself, through the same checks every other model goes through. This means the file fence, the approval guard and the audit trail apply the same way whichever model you pick. The cost is that the CLI never sees real tool schemas, so it sometimes sends "1" instead of 1 or a list as JSON text. The plugin has code to repair those arguments. It's extra work on every turn, but the alternative was a second, weaker set of permission checks inside each CLI.
Local models run on llama.cpp's llama-server, which ships in llama-runtime/. The tricky part is choosing the context window. local_models.rs reads the GGUF file header without loading any weights, to get the model's trained context length and how many bytes of KV cache each token needs. Then it sizes the window so weights and cache fit in a third of system memory. The automatic window stays between 16k and 32k tokens, and a per-model override can ask for more. The comments explain why. Our 3B default model was trained for 262,144 tokens at 88 KiB each. Taken literally, that's 23.6 GB of cache. Below 16k, the roughly 4.5k tokens of instructions and tool schemas Bees sends leave no room for the model to answer.
llama-server runs with --parallel 1, one request at a time. local-inference.js queues requests in front of it, so five agents on a local model take turns instead of hitting timeouts.
What stays local and what syncs
A private organization, the default with no sign-in, never syncs. If a team turns on sync, the optional Bees Cloud server receives organizations, members, process templates, runs, work items, schedules, agent definitions, the titles you type, and a folder's ID and relative path. File contents, credentials, model connections, the search index and run folders stay on the machine. The sync code is in plugin/lib/team-sync.js. The desktop app is fully open source. Bees Cloud lives in a separate closed repository.
What's rough today
-
The UI is one huge file.
dsh-runtime/plugin/client.jsis about 30,000 lines. It's the hardest file in the repo to navigate. -
We patch DSH at install time. We pin DSH to
0.1.7-rc.2, andscripts/install-dsh-runtime.mjsreapplies a few exact-match patches to it. One batches client module builds, which took DSH boot from 22.7 seconds to 2.9 seconds in our measurements. Every DSH upgrade means rechecking those patches.docs/dsh-upgrade-checklist.mdlists the steps. -
The agent's Chrome debugging port has no password.
docs/limitations.mdsays this plainly. Port 9333 has no auth, so another program on the same machine could drive that browser while it runs. We plan to close it. - Mac gets far more use. Windows installers aren't signed yet, and the Linux and Windows builds have seen much less real use.
- 0.1.1 data doesn't carry over to 0.2, because the database changed.
Reading and building it
To be upfront: we don't take outside pull requests right now. We are a small team, and reviewing a PR properly takes longer than making the change. CONTRIBUTING.md explains this. Bug reports, feature requests and forks are all welcome, and a bug report that points at the right file is a big help.
You need Node 24 (pinned in .nvmrc), Rust 1.88+, and CMake on macOS for the first llama-server build.
npm ci
npm run check # typecheck, project checks and build
npm run prepare:dsh # stage Node, llama.cpp and uv for the Rust build
make bees # run the app against the hosted API
Good files to start with:
-
src-tauri/src/lib.rs, atensure_dsh_runtime_blocking, for the whole launch sequence. -
dsh-runtime/plugin/lib/agent-runtime.js, for the run persona, the tool guards and restart recovery. -
dsh-runtime/plugin/lib/capabilities.jsandmcp-catalog.js, for add-ons. -
docs/startup-timing.md, if startup feels slow. Every launch writes[bees-startup]JSON lines todsh-state/startup.log, with no URLs or prompts in them.
Code: https://github.com/Bees-bot/bees-desktop
Download: https://bees.bot/download/
Questions: https://github.com/Bees-bot/bees-desktop/discussions
Top comments (0)