docker-compose.yml declares no networks: stanza, so every service joins one default project
network with DNS by service name. One of those services is the container a Bot's shell runs in, and
another is postgres, whose username and password are on lines 6 and 7 of the same file.
agent-computer/src/shell.ts:262 spawns /bin/bash -c <command>. computer_run_command is a
governed verb (gateway.ts:864) and the shipped default policy is {deny: [], allow: ["true"]}
(policy-store.ts:52), so a fresh deployment permits it. The prompt driving it is either the Bot's
operator or content on a page the Bot was asked to read.
The module states the invariant this breaks in its own words, at shell.ts:18-20:
ISOLATION IS THE CONTAINER'S JOB. A shell can reach whatever the container can reach.
Observed
Compose resolves the topology itself, so this is its answer rather than a reading of the file. Every
service, agent-computer and postgres included:
agent-computer:
networks:
default: null
postgres:
networks:
default: null
networks:
default:
name: openbot_default
From a container on openbot_default, with nothing but the committed credentials and the service
name:
$ PGPASSWORD=openbot psql -h postgres -U openbot -d openbot -tAc "select current_user, version();"
openbot|PostgreSQL 17.11 ...
It is superuser, not an application role
POSTGRES_USER bootstraps the instance owner, so rolsuper is t. That matters most for the audit
trail, which is protected by a trigger rather than by permissions. Against the real schema:
=== delete WITH the append-only trigger in place ===
ERROR: Audit events are append-only
CONTEXT: PL/pgSQL function prevent_audit_event_mutation() line 19 at RAISE
still_there | 1
=== drop the protection as this role ===
DROP TRIGGER
=== delete again ===
DELETE 1
after_tamper | 0
The trigger does its job and the role removes it. So the append-only property does not hold against
anything that reaches the database on this network, and the exposure is not "a Bot can read the
trail", it is "a Bot can edit or erase the record of what Bots did". The policy store and the
agent and profile tables are on the same footing.
Where it applies
Both arrangements, for different reasons.
In the shared-computer setup, the compose agent-computer service is the one scripts/start.sh:165
starts on every run.
In the per-Bot setup that docker-compose.yml:136-138 recommends for real deployments,
COMPUTER_NETWORK is set and supervisor/src/docker.ts:356 attaches every per-Bot computer to that
same network with NetworkMode.
The per-Bot path without COMPUTER_NETWORK is the one that already does this properly, and it is
what makes the gap look like an omission rather than a decision: supervisor/src/docker.ts:327-370
gives each computer no NetworkMode, so it lands on the default bridge and Docker's isolation rules
keep it off the compose network, plus CapDrop: ["ALL"], no-new-privileges and per-Bot volumes,
with a comment for each. The compose agent-computer service gets none of that, and
agent-computer/Dockerfile has no USER, unlike the all-in-one image which chowns to pwuser at
Dockerfile:195.
KEY_ENCRYPTION_KEY is deliberately kept out of the shell environment, so encrypted credential rows
stay encrypted. Everything else in the database is in the clear.
Not proposing a shape
Splitting the computer onto a network that does not carry the data plane is the obvious direction,
but it touches COMPUTER_NETWORK, the supervisor needing to reach the computers it creates, and
SPIRE, and those constraints are yours rather than mine. Happy to implement whichever shape you
want.
Verification notes
Run against a compose postgres brought up from this file, with the real migrations applied. The
reachability and superuser results are from a container attached to openbot_default;
agent-computer's membership of that network is from docker compose config rather than from
building the Playwright image, which is the one link here that is traced and not executed.
Separately, and not the subject of this issue: bringing the stack up failed with
failed to bind host port 0.0.0.0:5432, which is the postgres port being published on every host
interface. That was raised as a follow-up on #194 and again on #195 and does not appear to have been
filed. Same file, different problem, and worth fixing in the same pass if you take this one.
docker-compose.ymldeclares nonetworks:stanza, so every service joins one default projectnetwork with DNS by service name. One of those services is the container a Bot's shell runs in, and
another is
postgres, whose username and password are on lines 6 and 7 of the same file.agent-computer/src/shell.ts:262spawns/bin/bash -c <command>.computer_run_commandis agoverned verb (
gateway.ts:864) and the shipped default policy is{deny: [], allow: ["true"]}(
policy-store.ts:52), so a fresh deployment permits it. The prompt driving it is either the Bot'soperator or content on a page the Bot was asked to read.
The module states the invariant this breaks in its own words, at
shell.ts:18-20:Observed
Compose resolves the topology itself, so this is its answer rather than a reading of the file. Every
service,
agent-computerandpostgresincluded:From a container on
openbot_default, with nothing but the committed credentials and the servicename:
It is superuser, not an application role
POSTGRES_USERbootstraps the instance owner, sorolsuperist. That matters most for the audittrail, which is protected by a trigger rather than by permissions. Against the real schema:
The trigger does its job and the role removes it. So the append-only property does not hold against
anything that reaches the database on this network, and the exposure is not "a Bot can read the
trail", it is "a Bot can edit or erase the record of what Bots did". The policy store and the
agent and profile tables are on the same footing.
Where it applies
Both arrangements, for different reasons.
In the shared-computer setup, the compose
agent-computerservice is the onescripts/start.sh:165starts on every run.
In the per-Bot setup that
docker-compose.yml:136-138recommends for real deployments,COMPUTER_NETWORKis set andsupervisor/src/docker.ts:356attaches every per-Bot computer to thatsame network with
NetworkMode.The per-Bot path without
COMPUTER_NETWORKis the one that already does this properly, and it iswhat makes the gap look like an omission rather than a decision:
supervisor/src/docker.ts:327-370gives each computer no
NetworkMode, so it lands on the default bridge and Docker's isolation ruleskeep it off the compose network, plus
CapDrop: ["ALL"],no-new-privilegesand per-Bot volumes,with a comment for each. The compose
agent-computerservice gets none of that, andagent-computer/Dockerfilehas noUSER, unlike the all-in-one image which chowns topwuseratDockerfile:195.KEY_ENCRYPTION_KEYis deliberately kept out of the shell environment, so encrypted credential rowsstay encrypted. Everything else in the database is in the clear.
Not proposing a shape
Splitting the computer onto a network that does not carry the data plane is the obvious direction,
but it touches
COMPUTER_NETWORK, the supervisor needing to reach the computers it creates, andSPIRE, and those constraints are yours rather than mine. Happy to implement whichever shape you
want.
Verification notes
Run against a compose
postgresbrought up from this file, with the real migrations applied. Thereachability and superuser results are from a container attached to
openbot_default;agent-computer's membership of that network is fromdocker compose configrather than frombuilding the Playwright image, which is the one link here that is traced and not executed.
Separately, and not the subject of this issue: bringing the stack up failed with
failed to bind host port 0.0.0.0:5432, which is thepostgresport being published on every hostinterface. That was raised as a follow-up on #194 and again on #195 and does not appear to have been
filed. Same file, different problem, and worth fixing in the same pass if you take this one.