Skip to content

Do not start a container this supervisor does not own - #105

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
beardthelion:fix/supervisor-foreign-container
Aug 21, 2026
Merged

Do not start a container this supervisor does not own#105
davidmckayv merged 2 commits into
CopilotKit:mainfrom
beardthelion:fix/supervisor-foreign-container

Conversation

@beardthelion

Copy link
Copy Markdown
Contributor

Closes #102.

ensure treats a 409 from createContainer as the other request creating the same computer and goes on to start the container by name. That is right when the container already there is ours and wrong otherwise: a container left by a deployment under a different namespace, or made by hand, gets started and handed back as this Bot's computer, and its address is where COMPUTER_TOKEN goes next.

The file's own header says a name that lacks the label is treated as absent. This was the one path that adopted it instead.

What it does

  • A 409 is followed by the same inspectOwned check the rest of the module uses. Ours, and the request carries on as before, which is what keeps ensure idempotent under concurrent calls for one Bot. Not ours, and it is refused with NameHeldError and left alone, stopped.
  • NameHeldError is its own class, answered as 409 rather than 503. The daemon is fine, and an operator sent to look at Docker would find nothing; what has to happen is that somebody looks at the container holding the name.
  • waitUntilAnswering throws instead of returning at its deadline, as ComputerNotAnsweringError, answered as 503 because the caller's next move is still to wait and ask again. A wait that cannot fail is a sleep, and it was reporting every computer that never came up as ready.
  • The readiness timeout is now an EnsureOptions field. It decides when a slow start becomes an error rather than when a log line appears, so a deployment pulling a large image on a cold host should be able to say so.

Verification

Reproduced against a real daemon before the change: a container named openbot-computer-<bot> carrying someone.else=true was started by ensure and returned as the computer with status: unknown. After: refused, and docker inspect shows it still created.

supervisor/tests/docker.integration.test.ts covers three cases against a real socket:

  • the foreign container is refused and never started, asserted on the daemon's own view of it rather than on the thrown error alone
  • a container this supervisor owns is still started, which is the case a check written as "does the name exist" would break
  • an owned container that never reports healthy fails instead of being handed out

Both halves reverted with the tests live to confirm they fail.

The tests skip where the socket is not reachable, which is most CI. A stub cannot show any of this: the behaviour under test is the daemon's 409, not ours, and a fake would be asserting about itself. bun test supervisor/tests runs 11 with 3 skipped where there is no socket, 3 passing where there is.

bunx tsc --noEmit and bunx biome check clean. Whole-tree failure set identical to main.

One interaction

ensure now has a failure mode it did not have, so a deployment whose computer takes longer than 60s to answer gets a 503 where it previously got a "ready" it could not use. That is the intended direction, and readyTimeoutMs is there for the deployment that needs longer.

`ensure` treats a 409 from `createContainer` as the other request creating the same
computer, and goes on to start the container by name. That is right when the container
already there is ours and wrong the rest of the time: a container left by a deployment
under a different namespace, made by hand, or put there by somebody who guessed the
name gets started and handed back as this Bot's computer, and the server then sends the
deployment's computer token to whatever is listening inside it.

The file's own rule says otherwise. Ownership is checked in `inspectOwned` so that stop,
reset and inspect treat a name that is not ours as absent, and this was the one path
that adopted it instead. Now a 409 is followed by the same ownership check, and a name
held by something else is refused with its own error rather than a Docker outage, since
the daemon is answering and the thing an operator has to look at is the container.

Reproduced against a real daemon before and after: a container named
`openbot-computer-<bot>` carrying somebody else's label was started by `ensure` and
returned as the computer; it is now refused and left alone, and a container this
supervisor does own is still started.

The second half is the readiness wait, which could not fail. `waitUntilAnswering` fell
out of its loop at the deadline and returned, so a computer that never came up was
reported ready, which is the exact outcome the function exists to prevent. It throws
now, the timeout is configurable because it decides when a slow start becomes an error,
and 503 still tells the caller to wait and ask again.

The tests drive a real Docker socket and skip where there is not one. A stub cannot
show any of this: the behaviour under test is the daemon's 409, not ours.
@beardthelion
beardthelion force-pushed the fix/supervisor-foreign-container branch from 3cf8bc2 to cda2c5a Compare August 21, 2026 20:52
`supervisor` is not one of the root workspaces, so a root `bun install` never installs
`dockerode`, and `bun test` from the root walks this directory anyway. Importing the
client at module scope, or `../src/docker` which holds one, therefore failed to resolve
in CI and took the whole file down instead of skipping it: the suite reported an
unhandled error rather than a test result.

It passed locally because a `bun install` inside `supervisor/` had already put the
package there, which is the case `tests/clean-checkout.test.ts` was written about. That
guard did not catch this one: it filters the declared dependencies to the single name
that broke before, so a new one is invisible to it.

Both imports now happen inside the function that decides whether the tests can run at
all, alongside the socket check that was already there. Three reasons to skip rather
than fail, none of them a property of the code under test: no socket on this machine, no
client installed, or a socket that will not answer.

Verified both ways, because a test file that skips everywhere would have made CI green
by removing the coverage. With the root install alone the three tests skip and nothing
errors. With the package present and a reachable daemon they run and pass, and removing
the ownership check from `ensure` still turns the foreign-container one red.
@beardthelion
beardthelion force-pushed the fix/supervisor-foreign-container branch from 196d4da to 28f7d74 Compare August 21, 2026 21:11
@davidmckayv
davidmckayv merged commit 0e2dd69 into CopilotKit:main Aug 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The supervisor starts and hands out a container it does not own when the name is already taken

2 participants