CopilotKit's OpenBot Writes the Audit Row Before the Action
An MIT-licensed agent platform where the log entry exists before the browser click does. The identity check that's supposed to protect it ships disabled.
Most agent audit trails are receipts. The agent clicks the button, the click succeeds, something writes a row saying it happened. That ordering is so normal nobody names it, and it has a hole in the middle: any action that dies between the call and the log leaves no trace at all.
CopilotKit's OpenBot inverts it. The README's features section states the rule directly: the gateway "resolves the target from a server-held snapshot, evaluates the policy, writes the audit row, and only then calls the computer. There is no path that acts without the record existing first."
That's one sentence, it's the most interesting thing in the repository, and it has almost nothing to do with the agent.
It also contradicts the project's own tagline, which reads "every action decided before it happens and recorded after." The repo description says after. The feature it describes says before. I'm going with the feature text, because it's specific about ordering and the tagline is a slogan, but somebody should fix the slogan.
What the thing actually is
OpenBot is an MIT-licensed agent platform that runs on your own machine through Docker Compose. Each agent gets a container of its own with its own Chromium, its own browser profile with real logins, and its own /workspace volume, built on demand by a supervisor service. Data lands in your PostgreSQL. No model ships in the box; an administrator supplies the key, which is encrypted at rest.
An agent here is any endpoint speaking AG-UI, the agent-to-user protocol CopilotKit maintains. LangGraph, Mastra, CrewAI, Pydantic AI, and Google ADK agents all connect the same way, and the README makes a point I think is the right one: the governance rides the protocol, not the framework. You don't rewrite the agent to get policy enforcement. You point it at the gateway.
Everything the agent does to a browser, a file, an MCP server, or a UI component goes through that single gateway. There's no side door, at least not by design. The container exposes lower-level token-protected endpoints, and the docs tell you to keep them private and not use them to route around the gateway, which is an honest admission that the side door exists and is held shut by convention.
The three design choices worth stealing
Forget whether you'd ever run this stack. Three decisions in it are portable to whatever you already built.
Record before act. Already covered, and it's the good one. Think about what a receipt-style audit trail looks like during an incident. The agent issued a destructive call, the process was killed mid-flight, and your log shows nothing, because the log entry was going to be written by code that never ran. Record-before-act means the worst case is a row that says an action was permitted and started, with no completion. That's an ugly row. It's also exactly the row you need at 3am. Writing the record first costs you a database round trip on the hot path, and I think that's a trade most teams should take for anything touching a real credential.
Deny beats allow, and a missing rule permits nothing. The policy engine uses CEL, and rules can inspect tool.name, intent, bot.id, actor.id, page.url, page.host, element.*, key, file.*, and mcp.*. The evaluation order is stated: deny is evaluated before allow, a missing policy permits nothing, and a broken rule refuses instead of opening. That last clause is the one most homegrown policy layers get backward. A malformed rule in a typical system logs a warning and falls through to permissive. Here a malformed AGENT_COMPUTER_POLICY JSON blob stops the server from starting at all. Loud and early beats quiet and open.
Unclassified means write. The governed MCP catalogue ships presets for Atlassian, Box, Slack, Salesforce, and ServiceNow, and custom servers have to pass URL checks. Then there's this: any tool not positively classified as a read is treated as a write. If you run MCP servers behind any kind of approval flow, go check your default right now. Most implementations infer read-only from a name or an annotation the server itself supplies, which means the server being governed gets to declare itself harmless.
There's a fourth I like less loudly. When a bot hits a login wall or a 2FA prompt, it asks a person to take the wheel, and the handoff is recorded as computer.help_requested, computer.control_taken, and computer.control_released. While the human is driving, bot actions get refused rather than queued. Refusing instead of queuing avoids the failure where an agent's stale intentions replay after a human has changed the situation. Small choice, correct choice.
Secrets get similar care: the trail records that a secret was requested and how long it was, never what it said.
Put this into practice
You can have this running on a laptop in under an hour, and the fastest useful path is not the demo.
Copy .env.example to .env, get CopilotKit Intelligence credentials with npx --yes copilotkit@latest login, then project select, then license --write. Add an OPENAI_API_KEY. Generate your own KEY_ENCRYPTION_KEY with openssl rand -base64 32, because the one in the example file is public. Then bun install and bash scripts/start.sh, which brings up Docker, applies migrations, starts the API on 3001 and the app on 3010, and health-checks both before printing next steps. You need Bun 1.3+ and Docker.
Now skip the chat demo. Go to /admin/boundaries and add a deny rule for a host you care about. Then ask the bot to do something that touches it, and open /admin/audit. The refusal names the rule that caused it. That loop, rule to refusal to named rule in the trail, is the entire product thesis in about four minutes, and it's the part worth evaluating.
Second thing to try: have it fill out https://httpbin.org/forms/post, then read the audit rows for a single form submission. Count them. That count is what real per-action governance costs in log volume, and it's a number you want before you promise your security team an audit trail for a fleet of agents.
If you're borrowing rather than adopting, the port is small. Find the function in your own stack where a tool call gets dispatched. Move your logging call above the dispatch instead of below it, add a status field, and update the row on completion. Then change your policy default so an unparseable rule refuses. Those two edits give you most of what makes OpenBot interesting, and neither requires Docker.
Where it breaks
The sign-in ships off. OpenBot runs by default with OPENBOT_DEV_NO_AUTH, which the configuration table describes as admitting "every request as one administrator," and adds "How OpenBot runs today." Every design choice above sits on top of an identity check that isn't running. A policy engine that evaluates actor.id against a system where every actor is the same administrator is enforcing a rule about nobody. Google sign-in can be wired up, and it takes four settings set together plus TRUSTED_ORIGINS and INITIAL_ADMIN_EMAILS. Note that TRUSTED_ORIGINS defaults to http://localhost:3000, which is not where start.sh serves the app. That default will cost somebody an afternoon.
The MIT license doesn't get you a working system. The API server refuses to start without seven variables, and four of them point at CopilotKit's hosted service: INTELLIGENCE_API_URL, INTELLIGENCE_GATEWAY_WS_URL, INTELLIGENCE_API_KEY, and COPILOTKIT_LICENSE_TOKEN, alongside DATABASE_URL, KEY_ENCRYPTION_KEY, and MANAGED_AGENT_AG_UI_URL. Durable threads and memory live in CopilotKit Intelligence, over the network. So the audit and policy layers are yours, in your PostgreSQL, and the conversation layer is theirs. Open source with a commercial hard dependency at the center is a legitimate business model, and the README states it plainly. Just don't read "MIT" as "self-hosted."
Nobody has run this in production, because there hasn't been time. The README badge says alpha and the text says "expect rough edges and bugs, and expect things to move." The commit history on main was still in single digits when I read it. The star count, on the other hand, depends entirely on which surface you ask: the repository page served me 4 stars and 0 forks, a third-party index reported 92 stars and 8 forks the same day, and two other renders in the same twenty-four hours returned 274 and 684. I'm reporting the spread instead of picking one, because on a repository this young the number is changing faster than any of these caches. What all of them agree on is that the momentum boards are running well ahead of the code, and momentum boards count mentions rather than deployments.
The gateway is a single point of trust, and single points of trust get attacked. That's worth saying nine days after the August 11 Patch Tuesday, which fixed CVE-2026-62830 in Azure SRE Agent, a missing-authorization flaw rated 9.9 that a low-privileged remote attacker can reach with no user interaction, alongside CVE-2026-59118 in Microsoft Copilot Cowork at 9.3. Both are AI agent products. Both failed at authorization, which is the exact layer a policy gateway is built on. An audit trail that faithfully records everything a broken authorization check permitted is a very complete description of a breach you didn't stop.
What I'd actually do with it
Read docs/architecture.md, run the deny-rule loop, and steal the ordering. That's the honest recommendation. The container-per-agent design is nice and the gVisor option via COMPUTER_RUNTIME=runsc is nicer, but you can get isolation from a dozen projects. What's rarer is a codebase that took a position on when the log entry gets written and then enforced it structurally.
If you're building agent infrastructure this quarter, open your own dispatch path and check which side of the call your logging sits on. You'll know in thirty seconds whether this article was about somebody else's code or yours.
Sources: CopilotKit/openbot on GitHub (README, configuration and architecture tables, MIT license, repository state read August 20, 2026), AG-UI protocol.