supervisor/src/docker.ts:21 states the rule the module is built on:
Every write is scoped by ownership. Containers are created carrying openbot.supervisor, and stop, reset and inspect refuse anything without it. A container whose name happens to match but lacks the label is treated as absent.
ensure is the one path that does not. On a 409 from createContainer it assumes the conflict is the other request creating the same computer, and goes on to docker.getContainer(names.container).start(), which names the container rather than the container this supervisor made.
So a container already holding openbot-computer-<botId> and carrying none of our labels gets started and returned as that Bot's computer. inspectOwned correctly reported it as absent, which is what routed the code into the create branch in the first place.
Reproduced
Against a real daemon on main 2b2bc39:
docker create --name openbot-computer-reprobot --label someone.else=true debian:bookworm-slim sleep 600
ensure(namesFor("reprobot"), { image, environment: [] })
-> {"botId":"reprobot","container":"openbot-computer-reprobot","status":"unknown"}
docker inspect -> status=running labels=map[someone.else:true]
The container was started, and the supervisor answered as though it were the Bot's computer.
Why it matters
The address ensure returns is where the server sends COMPUTER_TOKEN. In the compose arrangement (network set) the URL is derived from the container name, so it is returned even when settled is null and the status is unknown, and index.ts:445 puts that token in the query string of the live-screen websocket URL.
It does not need an attacker with the socket to happen: a container left by a deployment that ran under a different COMPUTER_NAMESPACE, or one created by hand while debugging, fails the label check and is adopted the same way.
A second one on the same path
waitUntilAnswering (docker.ts:164) cannot fail. It polls until its deadline and then falls out of the loop and returns, so a computer that never came up is reported ready. The function's own docstring says why that is the wrong answer:
A caller told a computer is ready and then refused by it cannot tell that from a broken one, so ensure waits.
The wait is exactly what the timeout path skips.
Test plan
A PR follows.
supervisor/src/docker.ts:21states the rule the module is built on:ensureis the one path that does not. On a 409 fromcreateContainerit assumes the conflict is the other request creating the same computer, and goes on todocker.getContainer(names.container).start(), which names the container rather than the container this supervisor made.So a container already holding
openbot-computer-<botId>and carrying none of our labels gets started and returned as that Bot's computer.inspectOwnedcorrectly reported it as absent, which is what routed the code into the create branch in the first place.Reproduced
Against a real daemon on
main2b2bc39:The container was started, and the supervisor answered as though it were the Bot's computer.
Why it matters
The address
ensurereturns is where the server sendsCOMPUTER_TOKEN. In the compose arrangement (networkset) the URL is derived from the container name, so it is returned even whensettledis null and the status isunknown, andindex.ts:445puts that token in the query string of the live-screen websocket URL.It does not need an attacker with the socket to happen: a container left by a deployment that ran under a different
COMPUTER_NAMESPACE, or one created by hand while debugging, fails the label check and is adopted the same way.A second one on the same path
waitUntilAnswering(docker.ts:164) cannot fail. It polls until its deadline and then falls out of the loop and returns, so a computer that never came up is reported ready. The function's own docstring says why that is the wrong answer:The wait is exactly what the timeout path skips.
Test plan
openbot-computer-<botId>with no OpenBot labels, then ask the supervisor for that Bot's computer. Expect a refusal, and the container still stopped.ensureis idempotent and that must not change.A PR follows.