Eight things that hold up on a laptop and stop holding somewhere between one server and a company. They are grouped here because they are one theme rather than eight unrelated bugs: state that a deployment needs to share is kept per process, and data that grows with use has nothing bounding it.
Every claim below was checked against main at f725fb5 rather than inferred. Where a first pass overstated something, the corrected version is what is written here.
Ordered by how badly it breaks, not by how hard it is to fix.
1. A boundary rule only applies on the replica that received it
server/src/computer/policy-store.ts:59,62 and server/src/index.ts:189
An administrator adds a rule saying a Bot may not do something. The replica that happened to serve that request starts obeying it. Every other replica keeps enforcing the rule list it read when it booted.
load() is called once at start-up. get() is () => current, a plain in-memory read, and set() persists the row and then updates current in that one process. There is no LISTEN/NOTIFY and no re-read, so nothing tells the other replicas anything changed.
With N replicas behind a load balancer a new deny rule applies to roughly 1/N of Bot actions, and both the admin screen and the computer.policy_loaded audit row report success. The comment at server/src/index.ts:377 states that "a rule added a moment ago applies to the next call", which is true on one box and not on a fleet.
This is the one worth fixing first, because it is the product's central promise. The pattern is already in the repository: server/src/channels/events.ts:7 uses Postgres LISTEN/NOTIFY for exactly this shape of problem.
2. Nothing bounds how many browsers one computer holds
agent-computer/src/profiles.ts:146,213
The computer starts a Chromium context the first time each Bot is used and keeps it. Contexts are dropped in three places, and all three are explicit: a context whose browser has gone (:190), an operator or API stop (:236), and closeAll at shutdown (:295). There is no idle timeout, no LRU, no cap, and no admission control.
A deployment where every employee has a Bot therefore trends toward one resident browser per employee in a single container, at a few hundred MB each. The container runs out of memory, and because page() relaunches on demand it returns to the same state after restarting. The per-Bot sessions map at agent-computer/src/index.ts:123 has the same shape: an entry per Bot id ever seen, with nothing removing it.
3. The audit trail has no retention and no index for what the UI filters on
server/src/db/schema/core.ts (the auditEvent table) and server/src/audit.ts:262
Every click, keystroke, scroll, tool call, refusal and command writes a permanent row. Nothing in the product deletes, archives or partitions one.
The table has a single index, on createdAt. The audit screen filters by eventType, actorUserId, targetType and targetId, so those queries are sequential scans over what becomes the largest table in the deployment within weeks of real use.
Retention is also a control an enterprise buyer asks about on its own terms, separately from performance. Right now the answer is that rows are kept forever and there is no way to express a policy.
4. Replacing a Bot's key leaves the old key live
server/src/agents/auth-header.ts:62
Editing a Bot's credential calls store.create(...) for the new secret and repoints the Bot at it. The previous credential is not revoked and not deleted, so it stays in the vault, decryptable, and still valid.
The comment at auth-header.ts:15 already notes there is no way to revoke such a credential without editing the agent. Deleting the Bot leaves its key live too.
Rotating a key is the standard response to a suspected leak. Here rotation does not retire the leaked credential, so the honest answer to "is that key still valid" is yes. The credentials table also accumulates one unrevoked secret per edit per Bot, which nothing lists and no revocation path reaches.
5. The actions that change what a Bot can reach are not audited
server/src/agents/routes.ts
The file contains exactly one recordAuditEvent call, at :159, for POST /:agentId/declined. These eight routes mutate state and write nothing:
| route |
line |
POST / create |
236 |
PATCH /:agentId |
252 |
POST /:agentId/duplicate |
272 |
POST /:agentId/hide |
284 |
POST /:agentId/unhide |
297 |
POST /:agentId/callback-token |
318 |
DELETE /:agentId/callback-token |
331 |
DELETE /:agentId |
343 |
There is also no event type in auditEventTypes for any of them, so there is no row being omitted; the vocabulary does not exist.
A Bot's endpoint is where conversation content is sent, and its callback token is a capability handed to third-party infrastructure. The trail records every mouse movement a Bot makes and cannot answer "who pointed this Bot at that host, and when", which is the first question asked in an incident.
6. Looking up one person loads every person
server/src/people/store.ts:42,59,114
list() takes no arguments: no limit, no cursor, no search. It builds one query joining every user to their roles, linked provider accounts and sessions, then returns every person in the deployment.
find(userId) at :114 is implemented as (await list()).find(...), so fetching a single person runs the full aggregate and filters the result in JavaScript. Every role change and every access change calls find, twice.
Sessions are never pruned, so each person accumulates a row per sign-in for the life of the deployment, and the join multiplies sessions by accounts by roles per user before grouping. At tens of thousands of employees, one page view becomes a database-wide problem, and there is no pagination to switch on.
7. The channel list has no limit and channels are never removed
server/src/channels/routes.ts:195-262
Every sidebar render asks for all of a person's channels, one row per channel per participating Bot. Starting a conversation creates a channel, and nothing archives or deletes one.
Somebody who talks to their Bot daily accumulates thousands of channels, so a query that is instant in a demo returns thousands of rows on every page load, for every employee, and grows monotonically.
8. A Bot's profile directory is built from an unvalidated caller-supplied id
agent-computer/src/profiles.ts:153,251 reached from server/src/computer/routes.ts
The Bot id arrives as a URL path segment or an x-openbot-bot-id header, is not checked for shape, and is joined onto a filesystem path that reset then removes recursively. An id containing .. escapes the profiles volume.
agent-computer/src/workspace.ts goes to considerable trouble over exactly this confinement problem for files. profiles.ts does none of it.
Listed for completeness because #30 addresses it and #37 makes the route unreachable for a Bot the caller may not use. Both are worth taking, and they fix different halves: #37 closes the route, #30 closes the computer itself for anything that reaches it directly.
Shape of the fixes
1, 6 and 7 are the same fix applied three times: put shared state where every replica can see it, and put a bound on anything that grows with use. 3 and 4 are retention and revocation, which enterprise buyers ask about as controls rather than as performance. 5 is a vocabulary gap before it is a code gap: the event types do not exist yet.
None of these are regressions. They are the difference between a product that demonstrates well and one that survives being rolled out to a company.
Eight things that hold up on a laptop and stop holding somewhere between one server and a company. They are grouped here because they are one theme rather than eight unrelated bugs: state that a deployment needs to share is kept per process, and data that grows with use has nothing bounding it.
Every claim below was checked against
mainatf725fb5rather than inferred. Where a first pass overstated something, the corrected version is what is written here.Ordered by how badly it breaks, not by how hard it is to fix.
1. A boundary rule only applies on the replica that received it
server/src/computer/policy-store.ts:59,62andserver/src/index.ts:189An administrator adds a rule saying a Bot may not do something. The replica that happened to serve that request starts obeying it. Every other replica keeps enforcing the rule list it read when it booted.
load()is called once at start-up.get()is() => current, a plain in-memory read, andset()persists the row and then updatescurrentin that one process. There is noLISTEN/NOTIFYand no re-read, so nothing tells the other replicas anything changed.With N replicas behind a load balancer a new deny rule applies to roughly 1/N of Bot actions, and both the admin screen and the
computer.policy_loadedaudit row report success. The comment atserver/src/index.ts:377states that "a rule added a moment ago applies to the next call", which is true on one box and not on a fleet.This is the one worth fixing first, because it is the product's central promise. The pattern is already in the repository:
server/src/channels/events.ts:7uses PostgresLISTEN/NOTIFYfor exactly this shape of problem.2. Nothing bounds how many browsers one computer holds
agent-computer/src/profiles.ts:146,213The computer starts a Chromium context the first time each Bot is used and keeps it. Contexts are dropped in three places, and all three are explicit: a context whose browser has gone (
:190), an operator or APIstop(:236), andcloseAllat shutdown (:295). There is no idle timeout, no LRU, no cap, and no admission control.A deployment where every employee has a Bot therefore trends toward one resident browser per employee in a single container, at a few hundred MB each. The container runs out of memory, and because
page()relaunches on demand it returns to the same state after restarting. The per-Botsessionsmap atagent-computer/src/index.ts:123has the same shape: an entry per Bot id ever seen, with nothing removing it.3. The audit trail has no retention and no index for what the UI filters on
server/src/db/schema/core.ts(theauditEventtable) andserver/src/audit.ts:262Every click, keystroke, scroll, tool call, refusal and command writes a permanent row. Nothing in the product deletes, archives or partitions one.
The table has a single index, on
createdAt. The audit screen filters byeventType,actorUserId,targetTypeandtargetId, so those queries are sequential scans over what becomes the largest table in the deployment within weeks of real use.Retention is also a control an enterprise buyer asks about on its own terms, separately from performance. Right now the answer is that rows are kept forever and there is no way to express a policy.
4. Replacing a Bot's key leaves the old key live
server/src/agents/auth-header.ts:62Editing a Bot's credential calls
store.create(...)for the new secret and repoints the Bot at it. The previous credential is not revoked and not deleted, so it stays in the vault, decryptable, and still valid.The comment at
auth-header.ts:15already notes there is no way to revoke such a credential without editing the agent. Deleting the Bot leaves its key live too.Rotating a key is the standard response to a suspected leak. Here rotation does not retire the leaked credential, so the honest answer to "is that key still valid" is yes. The credentials table also accumulates one unrevoked secret per edit per Bot, which nothing lists and no revocation path reaches.
5. The actions that change what a Bot can reach are not audited
server/src/agents/routes.tsThe file contains exactly one
recordAuditEventcall, at:159, forPOST /:agentId/declined. These eight routes mutate state and write nothing:POST /createPATCH /:agentIdPOST /:agentId/duplicatePOST /:agentId/hidePOST /:agentId/unhidePOST /:agentId/callback-tokenDELETE /:agentId/callback-tokenDELETE /:agentIdThere is also no event type in
auditEventTypesfor any of them, so there is no row being omitted; the vocabulary does not exist.A Bot's endpoint is where conversation content is sent, and its callback token is a capability handed to third-party infrastructure. The trail records every mouse movement a Bot makes and cannot answer "who pointed this Bot at that host, and when", which is the first question asked in an incident.
6. Looking up one person loads every person
server/src/people/store.ts:42,59,114list()takes no arguments: no limit, no cursor, no search. It builds one query joining every user to their roles, linked provider accounts and sessions, then returns every person in the deployment.find(userId)at:114is implemented as(await list()).find(...), so fetching a single person runs the full aggregate and filters the result in JavaScript. Every role change and every access change callsfind, twice.Sessions are never pruned, so each person accumulates a row per sign-in for the life of the deployment, and the join multiplies sessions by accounts by roles per user before grouping. At tens of thousands of employees, one page view becomes a database-wide problem, and there is no pagination to switch on.
7. The channel list has no limit and channels are never removed
server/src/channels/routes.ts:195-262Every sidebar render asks for all of a person's channels, one row per channel per participating Bot. Starting a conversation creates a channel, and nothing archives or deletes one.
Somebody who talks to their Bot daily accumulates thousands of channels, so a query that is instant in a demo returns thousands of rows on every page load, for every employee, and grows monotonically.
8. A Bot's profile directory is built from an unvalidated caller-supplied id
agent-computer/src/profiles.ts:153,251reached fromserver/src/computer/routes.tsThe Bot id arrives as a URL path segment or an
x-openbot-bot-idheader, is not checked for shape, and is joined onto a filesystem path thatresetthen removes recursively. An id containing..escapes the profiles volume.agent-computer/src/workspace.tsgoes to considerable trouble over exactly this confinement problem for files.profiles.tsdoes none of it.Listed for completeness because #30 addresses it and #37 makes the route unreachable for a Bot the caller may not use. Both are worth taking, and they fix different halves: #37 closes the route, #30 closes the computer itself for anything that reaches it directly.
Shape of the fixes
1, 6 and 7 are the same fix applied three times: put shared state where every replica can see it, and put a bound on anything that grows with use. 3 and 4 are retention and revocation, which enterprise buyers ask about as controls rather than as performance. 5 is a vocabulary gap before it is a code gap: the event types do not exist yet.
None of these are regressions. They are the difference between a product that demonstrates well and one that survives being rolled out to a company.