Skip to content

Stop treating a service that answers as a service that is current - #137

Merged
davidmckayv merged 1 commit into
mainfrom
fix/a-healthy-container-is-not-a-current-one
Aug 22, 2026
Merged

Stop treating a service that answers as a service that is current#137
davidmckayv merged 1 commit into
mainfrom
fix/a-healthy-container-is-not-a-current-one

Conversation

@davidmckayv

Copy link
Copy Markdown
Contributor

The drift #136 made visible. That change put the failure on the audit trail; this one stops producing it.

The mistake, twice

start.sh skipped work for anything already up:

if curl -fsS "http://localhost:$port/health"; then
  info "  $svc: already answering on $port"    # ← excluded from `docker compose up`
if ! curl -fsS "http://localhost:$SERVER_PORT/api/capabilities"; then
  … start the server …                          # ← so a running one is never restarted

Both read as optimisations. Both are the same error: answering proves a process is alive and says nothing about whether it still agrees with the deployment.

A container left out of compose up is never compared against its configuration, so compose cannot notice its environment changed. A server that is answering was started earlier and read .env once, then.

What it cost

AGENT_TOOL_TOKEN is minted by this script and written to .env. On a run where the stack was already up, neither side got it: the container kept the previous value because it was healthy, the server kept its own because it had already read the file.

Every callback a Bot made was then refused. Which does not surface as an error — it surfaces as:

No files were returned from Google Drive for the search query "PRD."

about a Drive containing three matching files. That is how this was found, and it took an hour, because before #136 the refusal left no row anywhere.

The two fixes

Bot services always go to compose. docker compose up -d is declarative and is the only thing that can actually compare a container's configuration against the file. Naming them all costs a comparison and buys the guarantee.

The server is restarted when this run minted a secret. Nothing can diff a process that read its environment an hour ago, so the trigger is the one fact the script knows for certain: it generated a secret just now. A SECRETS_ROTATED flag is set wherever MANAGED_AGENT_TOKEN or AGENT_TOOL_TOKEN is minted, and the server block consults it before deciding a running server is fine.

Deliberately not "restart the server every run". It is the API; bouncing it when nothing changed is a cost with no reason. The flag is the honest signal.

Proof

Rotation, with the whole stack up. Cleared AGENT_TOOL_TOKEN, re-ran start.sh:

Generated AGENT_TOOL_TOKEN and wrote it to .env.
  a secret was generated this run, so the server is restarted to pick it up
  server ready
  app ready
container in sync

Then, in the browser, the same question that had been answered "no files":

Exact filenames returned:
OpenBot PRD: Knowledge, Cloud, and Per-User Access
OpenBot PRD: Knowledge, Cloud, and Per-User Access
OpenBot PRD: Knowledge, Cloud, and Per-User Access

audited as mcp.call_succeeded, reachedAs equal to the asking user.

Drift, before the fix, reproduced deliberately: rotate the token in .env and re-run. The container stayed stale, and the trail showed mcp.callback_refused | Not authorised. — which is #136 working, and is what made this diagnosable in seconds rather than an hour.

result
bun run test 1137 pass, 8 skip, 0 fail, 1145 across 102 files
bun run typecheck clean, all four packages
bun run format:check clean

The cost, stated

Bot containers now go through compose on every run, so a run that rebuilds an image recreates them: about five seconds, and it bounces an idle Bot. supervisor already behaved this way, so this makes the three consistent with it rather than introducing something new.

The alternative is what we had: fast re-runs that can leave a Bot holding a secret the deployment has stopped accepting, reporting empty results to whoever asks.

Tests

None. start.sh is a shell entry point with no harness in this repo, and building one is a larger change than this. It is driven above instead, in both directions.

`start.sh` skipped work for anything already up: a Bot container answering
its health route was left out of `docker compose up` entirely, and a
server answering `/api/capabilities` was never restarted. Both read as
optimisations. Both are the same mistake, which is that answering proves
a process is alive and says nothing about whether it still agrees with
the deployment.

So a secret minted by this script never reached the things that use it.
`AGENT_TOOL_TOKEN` is generated here and written to .env; the container
kept the previous one because it was healthy, and the server kept its own
because it had read .env once at startup. Every callback a Bot made was
refused, which reaches a person as "no files were returned" rather than
as an error, about a Drive that has the files.

Two changes, matching the two ways it went wrong. Bot services are always
handed to compose, which is declarative and is the thing that can actually
compare a container's configuration. The server is restarted when this run
minted a secret, because nothing can diff a process that read its
environment an hour ago.

Driven: rotate the token with the whole stack up, re-run, and a Bot
returns real Drive filenames again. Before, it reported none.
@davidmckayv
davidmckayv merged commit 4f072bf into main Aug 22, 2026
8 checks passed
@davidmckayv
davidmckayv deleted the fix/a-healthy-container-is-not-a-current-one branch August 22, 2026 02:52
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.

1 participant