DEV Community

Cover image for OpenBot: A Technical Architecture Review of CopilotKit's Governed Agent Runtime
Sanjay Singh
Sanjay Singh

Posted on Originally published at zyvop.com

OpenBot: A Technical Architecture Review of CopilotKit's Governed Agent Runtime

What shipped

CopilotKit released OpenBot's first alpha on August 17, 2026, then shipped three more tagged versions inside the following five days, ending with v0.0.4 on August 22. The project is MIT licensed and lives at github.com/CopilotKit/openbot. The pitch: a Bot works from a browser and a set of files it shares with no other Bot, limited strictly to whatever an administrator has explicitly granted it. A policy check runs against every action before it executes, and the decision, whether allowed or refused, is written to an audit trail afterward.

Where it sits

OpenBot is not CopilotKit's first release. The company stewards AG-UI, an open protocol for agent-to-application communication that OpenBot's own documentation lists as compatible with LangGraph, Mastra, CrewAI, Pydantic AI, and Google ADK, among other frameworks. CopilotKit raised a $27 million Series A in May 2026, led by Glilot Capital, NFX, and SignalFire, to build out AG-UI and an enterprise layer on top of it, according to TechCrunch. OpenBot is the self-hosted, open half of that layer: governance is its main feature rather than something bolted on afterward.

The shape of a deployment

A deployment is eight moving parts, most of them Docker containers: app, the React interface; server, a Hono API that also holds auth, policy, audit, and the coworker roster; agent-computer, one Chromium instance per Bot with its own workspace. agent-bot and agent-langgraph are the two example Bots that ship in the box, and a supervisor creates and tears them down. PostgreSQL with pgvector holds everything else, and CopilotKit Intelligence, an external service, holds threads and memory.

Component Port Role
app 3010 React/Vite interface for channels, chat, and admin pages
server 3001 API, auth, policy, audit, plugins, coworkers, channels
agent-computer 4100 Chromium, workspace, browser profile per Bot
agent-bot 4200 Proof-of-concept AG-UI Bot
agent-langgraph 4201 LangGraph AG-UI Bot
supervisor 4500/4300 Creates, stops, resets per-Bot computers
PostgreSQL + pgvector 5432 Policy, audit, credentials, grants, channels
CopilotKit Intelligence external Durable threads and memory

Source: docs/architecture.md.

The gateway is the whole point

A Bot never reaches a browser, a file, or an MCP server directly. Every attempt loops back through the server first, which works through a fixed sequence before anything happens: resolve what the Bot is pointing at, using the snapshot on file, weigh that against the live policy, log the outcome, and only then hand the request to the computer. A second log entry follows if the computer itself fails to carry the action out, and nowhere in that sequence can an action complete before its own record does.

Each rule is written as a CEL expression that can inspect fields such as tool.name, bot.id, page.host, element.ref, or mcp.tool. Denials are always weighed before permissions, and the engine treats its own malfunctions as a reason to say no: nothing gets through under a missing policy, and a deny rule that breaks still counts as a refusal.

There's a catch worth flagging: the shipped default is deny: [] and allow: ["true"], which permits everything until an administrator writes a restrictive rule at /admin/boundaries. "Fails closed" describes what happens when the policy breaks, not what the box does on day one.

Mermaid Diagram

One computer per Bot

With COMPUTER_SUPERVISOR_URL set, every Bot gets its own container: its own Chromium, its own /workspace, its own logins. Without it, Bots share one computer, which is a meaningfully weaker guarantee. Nothing on the host network can simply guess its way into a Bot's logged-in browser, either: computers only listen on the loopback address, and every request to one has to present a token scoped to that specific container.

Setting COMPUTER_RUNTIME to runsc switches these containers to gVisor on hosts that support it, trading a bit of performance for a stronger sandbox boundary. A set of SPIRE services for workload identity is already defined in the compose file, though the shipped start script leaves them off for now.

Browser navigation is limited to plain http and https, and anything that resembles a cloud metadata address is refused no matter how the rest of the deployment is configured, closing off a well-known trick for getting an agent to leak cloud credentials it was never supposed to touch. The same validation applies to custom agent endpoints, not just page navigation: pointing a coworker at your own AG-UI server runs through those identical checks, and any authorization header attached to it is stored write-only, never returned by the API afterward.

When a Bot has to stop

When a Bot runs into something it shouldn't push through alone, a sign-in page or a two-factor prompt, it stops and flags a person rather than guessing at a way around it. Control then shifts to that same person inside the live-view panel the Bot had been using, and each stage of that handoff becomes its own logged event: computer.help_requested, computer.control_taken, computer.control_released.

For as long as a human holds the controls, anything the Bot itself attempts is turned away outright rather than held for later. Entering a secret gets the same careful handling: what ends up in the audit trail is the fact that one was requested and its length in characters, not the value a person typed in.

Tools are governed twice

MCP connections and skills draw from the same underlying grant table, but who is allowed to create each one differs. Adding an MCP server is an administrator's call alone, since it reaches outside systems using stored credentials, and the built-in catalogue tops out at Atlassian, Box, Slack, Salesforce, and ServiceNow. Anything not explicitly confirmed as read-only, including tools from custom servers, defaults to being treated as a write.

Skills are just instructions, invoked with a slash in the composer. A skill can declare which tools it expects, but declaring one grants nothing: the actual offer is still intersected with whatever that Bot was separately granted.

That distinction mattered for a scaling problem CopilotKit found in practice. A model's accuracy at picking the correct tool drops off sharply once the list of options grows: reliable around ten, shaky by the time it reaches thirty. As of v0.0.3, that ceiling is handled directly: a Bot holding more than a dozen granted tools is now shown only the subset that the skills matching each message actually declare, not its entire grant list.

A new audit event, mcp.tools_discovered, records how many tools were offered out of how many were granted, and why. That answers the harder failure mode of a Bot that silently calls nothing at all and answers from memory instead, which is a worse outcome than calling the wrong tool.

That narrowing had nothing to draw on until v0.0.4, though: a brand-new deployment ships with no skills at all, so on a fresh clone there was nothing to match incoming messages against, and the feature never actually switched on. v0.0.4 added an optional skills.yaml a tenant package can ship, seeding a starting set of skills at boot so the narrowing has something to work with from the very first run.

Answers can be a component, not just prose

Bots can also answer with a rendered component instead of only prose, the same generative-UI pitch CopilotKit makes to enterprises elsewhere: a revenue question comes back as an interactive chart the company itself designed, not a wall of text. Compiled components publish automatically the first time a catalogue sync sees them; ones built by hand in /admin/playground stay drafts until someone explicitly publishes them. Either way, every render call still asks the server whether that component exists, is published, and hasn't been withheld from the calling Bot specifically.

What four days of patches reveal

Two changes between v0.0.1 and v0.0.4 are worth reading closely, because they show what this kind of gateway has to get right that an ordinary CRUD app does not. The first, in v0.0.2, is a deliberate architectural reversal: OpenBot stopped maintaining its own index of company documents. Early on, a Google Drive connector had run as a single privileged service account, impersonating whichever user asked, and copying files it found into a local vector store.

Under that design, two different people asking the same question got an answer shaped by what one shared credential could see, not by what either of them was individually allowed to see. Removing someone's Drive access did nothing to the copy already sitting in OpenBot's own database. The fix was to delete the index rather than patch it: the rebuilt connector holds no local copy of anything, and each person links their own Google account instead of relying on the shared one.

A Bot now reads Drive scoped to whichever person is asking, requesting a fresh access token on every call instead of reusing a cached one. That means revoking someone's access at Google shows up the moment they try again, not whenever some cached copy of their permission finally times out. Keeping a second, separately-permissioned copy of someone else's data turned out to be a bigger liability than the latency it was saving.

The second change, landing in v0.0.4, is a straightforward security bug rather than a design choice. When a Bot wants to click something on a page, it points at that element using a reference saved in an earlier snapshot, and the server has to look that reference up before any policy rule can weigh in. Before v0.0.4, a lookup that failed to resolve was not treated as a reason to stop.

Instead the action proceeded with an empty description of what was being clicked, so a deny rule written against element.name or element.role had nothing to compare against and simply never matched. The one case this slipped through on was a computer container restarting under an old, previously-taken snapshot after a redeploy. The computer's own staleness check only flags a mismatch between its internal counter and the reference, so a reference that was outdated on both sides looked perfectly consistent to it.

Practically, that meant the exact same click, against the exact same policy, could be refused before a redeploy and silently allowed right after one. As of v0.0.4, any reference the server cannot actually resolve is refused outright rather than passed through with a blank element description, and the person driving that Bot is asked to capture a fresh snapshot before continuing.

What "open source" does not quite mean here

The code is MIT licensed, but a working deployment is not fully self-contained. Durable threads and memory live in CopilotKit Intelligence, an external service that requires its own project and a license token obtained through npx copilotkit login, not in the PostgreSQL instance sitting next to everything else. There is also no hosted option yet: the release notes list it as local-only, and the default run mode, OPENBOT_DEV_NO_AUTH, admits every request as a single administrator until Google sign-in is wired up separately.

Running it

A deployment needs Docker, Bun 1.3 or newer, a CopilotKit Intelligence project and license, and a model key: OpenAI for the built-in Bot, or OpenAI, Anthropic, or Google for the LangGraph one. Copy .env.example to .env, then run npx copilotkit login, project select, and license --write to populate the Intelligence credentials, fill in a model key, and generate a KEY_ENCRYPTION_KEY.

bun install followed by bash scripts/start.sh brings up Postgres, the computers, and both servers, reachable at localhost:3010. The README's suggested first test is asking a Bot to open Hacker News and summarize the top story, then checking /admin/audit to see the decision it recorded.

Verdict

Four tagged releases in five days is a sign of a team actively shepherding something rather than one that shipped and walked away, but it is also a sign this is genuinely alpha: the stale-citation fix in v0.0.4 was a real gap in the exact mechanism the whole project is sold on. Teams evaluating agent governance patterns right now have more to learn from reading docs/architecture.md than from running it in anything resembling production this early.

The idea worth taking regardless of whether OpenBot itself gets used is the ordering it enforces: resolve the target, decide against policy, record the decision, only then act, with a refusal that names the exact rule that caused it.


Originally published on ZyVOP

💡 For more articles like this, subscribe to the ZyVOP newsletter!

Top comments (0)