From d97ad36d491aeb2ec6cf18fad35cb39bf767f55c Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:24:28 -0500 Subject: [PATCH 1/3] Keep the database off the network a Bot's shell is on Compose declared no networks at all, so every service joined one and reached the others by service name. One of those services is agent-computer, which is where a Bot's shell runs, and another is postgres, whose username and password are three lines above it in the same file. So a Bot could open postgres:5432 and authenticate. That is the audit trail, the policy store and the agent tables, from the one container whose whole job is to run what a Bot was asked to do, driven by an operator or by content on a page the Bot was told to read. shell.ts already says what this breaks: isolation is the container's job, a shell can reach whatever the container can reach. The role Compose creates is the instance owner, so the append-only trail was not a defence against this either. The trigger refuses a delete and its owner can drop the trigger. postgres and migrate, the only service that reaches it by name, now have a data network to themselves. Nothing else moves, so the supervisor, SPIRE and the two Bots keep every relationship they had, and a server on the host is unaffected because it reaches the database through the published port rather than the shared network. The per-Bot path already did this correctly, which is what made the gap look like an omission: with COMPUTER_NETWORK unset the supervisor gives each computer no NetworkMode, so it lands on the default bridge away from Compose, with CapDrop ALL and no-new-privileges besides. --- docker-compose.yml | 26 ++++++++++++++++++++++++++ docs/architecture.md | 2 ++ 2 files changed, 28 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index a832ffc0..8f4f8e5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,18 @@ services: postgres: image: pgvector/pgvector:pg17 + # Off the network the Bots are on, because a Bot's computer is on it and a Bot has a shell. + # + # With no `networks:` anywhere, Compose puts every service on one network with DNS by service + # name, so `agent-computer` could open `postgres:5432` and the credentials three lines below are + # in this file. That is the audit trail, the policy store and the agent tables, reachable from + # the one container whose whole job is to run what a Bot asks for. `agent-computer/src/shell.ts` + # says it plainly: isolation is the container's job, a shell can reach whatever the container + # can reach. + # + # `migrate` is the only service that reaches this by name, so the two of them are all that + # belongs here. Everything else that needs the database is outside this file: the API server + # runs on the host in local development and connects through the published port below. environment: POSTGRES_DB: openbot POSTGRES_USER: openbot @@ -14,6 +26,8 @@ services: interval: 5s timeout: 5s retries: 10 + networks: + - data migrate: build: @@ -26,6 +40,8 @@ services: postgres: condition: service_healthy restart: "no" + networks: + - data # The Bot's computer is long-lived so browser sessions remain signed in across turns. agent-computer: @@ -233,6 +249,16 @@ services: timeout: 5s retries: 5 +networks: + # The database and the one service that reaches it by name. Nothing a Bot can reach is on it. + # + # A deployment that runs the API server inside Compose rather than on the host puts that service + # on both this and `default`, which is the one place the two are meant to meet. + data: + # Everything else, which is where `default` already put it. Named here only so that adding a + # service without a `networks:` key keeps landing beside the Bots rather than beside the database. + default: + volumes: postgres-data: agent-workspace: diff --git a/docs/architecture.md b/docs/architecture.md index 9a0759b7..e8dc5a5c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,6 +69,8 @@ startup. `agent-computer` requires `COMPUTER_TOKEN` and permits only `/health` without it. Docker Compose binds it to `127.0.0.1:4100`. +Compose puts it on a different network from PostgreSQL. A Bot has a shell, and a shell reaches whatever its container reaches, so the database is on a `data` network carrying only itself and `migrate`, and everything else is on `default`. A deployment that runs the API server inside Compose rather than on the host joins it to both, which is the one place the two meet. + With `COMPUTER_SUPERVISOR_URL`, each Bot gets its own computer container, workspace volume, and browser profile. Without it, all Bots share `AGENT_COMPUTER_URL`. A command on the computer inherits PATH, locale and terminal names, and the proxy variables, not the rest of the process environment. Userinfo is stripped from a proxy URL. `COMPUTER_SHELL_ENV` names anything else a deployment wants passed. From e03c5fd193e4a1382322e97b2f4860605e95b95f Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:24:28 -0500 Subject: [PATCH 2/3] Note the network split in the changelog Says what an operator has to do if they run the server inside Compose, and that a deployment which has been running this way should look at its trail rather than assume the change is retroactive. --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd611c5..2bea1abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### A Bot's computer is no longer on the same network as the database + +Compose declared no networks, so every service shared one and reached the others by service name. +One of those services is the container a Bot's shell runs in, and another is PostgreSQL, whose +username and password are in the same file. A shell reaches whatever its container reaches, so a Bot +could open `postgres:5432` and authenticate: the audit trail, the policy store and the agent tables, +from the one container whose job is to run what a Bot asks for. The role Compose creates is the +instance owner, so the trail's append-only trigger was no defence either, being something its owner +can drop. + +PostgreSQL and `migrate`, the only service that reaches it by name, are now on a `data` network of +their own. Everything else stays where it was. Nothing changes for a deployment that runs the API +server on the host, which reaches the database through the published port and never used the shared +network for it. **A deployment that runs the server inside Compose has to join that service to both +networks**, which is the one place the two are meant to meet. + +This does not reach back in time. A deployment that has been running with the two on one network +should assume a Bot could have read or written the database, and look at the trail with that in +mind. + ### Name the private addresses an agent may live at Refusing `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` in production closed a hole and took something with From 991b4cdbd5b2825257e8970d09a1ea6534e24db4 Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 24 Aug 2026 10:22:34 -0700 Subject: [PATCH 3/3] Publish the database on loopback, like every other port in the file Taking postgres off the Bots' network removes the name and leaves the address. A container's default gateway is the host, and a port published on every interface answers there: from inside agent-computer, the gateway on 5432 accepted a connection and began authenticating as openbot on openbot, with the password three lines above it in this file. So the reachability this change is about survived it. Every other port here is already bound to 127.0.0.1 with a comment saying why. Driven both ways: the name and the gateway are now both unreachable from the computer container, the container still reaches the internet, migrate still exits 0 across the network split, and the server on the host still connects. --- CHANGELOG.md | 7 +++++++ docker-compose.yml | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bea1abc..3ab6e5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,13 @@ server on the host, which reaches the database through the published port and ne network for it. **A deployment that runs the server inside Compose has to join that service to both networks**, which is the one place the two are meant to meet. +The published port is now on loopback, as every other port in that file already was. Taking the +database off the Bots' network removes the name, not the address: a container's default gateway is +the host, and a port published on every interface answers there. From inside the computer container, +the gateway on `5432` accepted a connection and began authenticating as `openbot` on `openbot`, with +the password in the same file. **A deployment that reached the database from another machine over +this port has to reach it another way**, which is what publishing it on every interface was doing. + This does not reach back in time. A deployment that has been running with the two on one network should assume a Bot could have read or written the database, and look at the trail with that in mind. diff --git a/docker-compose.yml b/docker-compose.yml index 8f4f8e5e..e2e7cfbc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,13 @@ services: POSTGRES_USER: openbot POSTGRES_PASSWORD: openbot ports: - - "${POSTGRES_PORT:-5432}:5432" + # Loopback, like every other port in this file, and for the reason the network split above + # exists. Published on every interface, this is reachable from the Bot's computer at the + # container's default gateway, which is the host: taking `postgres` off that network removes + # the name and leaves the address. Proven from inside `agent-computer`, where the gateway on + # :5432 answered and began authenticating as `openbot` on `openbot`, with the password in + # this file. The server on the host still reaches it, because that is what loopback is. + - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: