Skip to content

fix: record the environment in the host owner record, and refuse a mismatch - #71

Merged
vishr merged 2 commits into
mainfrom
fix/host-owner-carries-environment
Aug 18, 2026
Merged

fix: record the environment in the host owner record, and refuse a mismatch#71
vishr merged 2 commits into
mainfrom
fix/host-owner-carries-environment

Conversation

@vishr

@vishr vishr commented Aug 18, 2026

Copy link
Copy Markdown
Member

Closes #58 — the only open audit item that could lose data.

The bug

Two environments of one application could share a host, and the second would adopt the first's containers and volumes rather than collide with them.

Every runtime name carries the application and not the environment, because one box runs one application:

Derived name Value Environment in it?
Compose project <app> no
Container <app>-<workload>-<n> no
Volume ob_<app>_<workload>_<volume> no

Environment.BasePath is a per-environment override, so release directories differ. Nothing else does.

Point staging at production's server and every check passes. The owner record names the application, and it matches. Preflight hunts for foreign resources and finds none — the names it meets are its own. Then the rollout takes over production's containers and mounts production's volumes.

Nothing in the system could see the difference, because the environment was never written down anywhere the host could compare it.

The fix

The owner record becomes <application> <environment>. RequireHostOwner compares both halves and refuses a second environment with a new code, host_environment_mismatch, before anything changes.

Preflight performs the same comparison, so a plan is never green for a deploy the engine will then reject.

Backward compatibility

A record claimed before this change carries the application alone. It still identifies the owner, so it is honoured rather than refused — refusing would strand every host claimed by an older ob. bootstrap upgrades it in place under the host lock.

That upgrade cannot use set -C, unlike a first claim: the file it replaces already exists. It runs under the lock, having just re-read the record it is rewriting.

Tests

Five new tests: the refusal, the same-environment acceptance, the legacy-record acceptance, the still-refused foreign application, and record round-tripping.

Non-vacuous — verified by removing the guard and watching them go red, not assumed:

--- FAIL: TestRequireHostOwnerRefusesAnotherEnvironmentOfTheSameApplication
    staging against a production-claimed host = <nil>, want HostEnvironmentMismatchError

Docs

errors.mdx regenerates with the new code. Two explanation pages updated by hand — the ownership boundary and the refusals list — because the interesting part is why two environments do not collide, which is the opposite of what a reader would assume.

Verified

just check, golangci-lint run ./..., go test -race ./... all pass.

vishr and others added 2 commits August 18, 2026 08:54
…smatch

Two environments of one application could share a host, and the second would
adopt the first's containers and volumes rather than collide with them.

Every runtime name Onebox derives carries the application and not the
environment — the Compose project is the application name, containers are
<app>-<workload>-<n>, volumes are ob_<app>_<workload>_<volume> — because one box
runs one application. Environment.BasePath is a per-environment override, so the
release directories differ, but nothing else does.

Point staging at production's server and every check passes. The host owner
record names the application, and it matches. Preflight looks for foreign
resources and finds none, because the names it meets are its own. Then the
rollout takes over production's containers and mounts production's volumes.
Nothing in the system could see the difference, because the environment was
never written down anywhere the host could compare.

So it is written down now. The owner record becomes "<application>
<environment>", RequireHostOwner compares both halves, and a second environment
is refused with host_environment_mismatch before anything changes. Preflight
performs the same comparison, so a plan is not green for a deploy the engine
will reject.

A record claimed before this change carries the application alone. That still
identifies the owner, so it is honoured rather than refused — refusing would
strand every host claimed by an older ob. bootstrap upgrades it in place under
the host lock, which is also why the upgrade cannot use `set -C`: the file it is
replacing already exists. Until that runs, the application check applies exactly
as before.

The tests fail without the guard, which was checked by removing it and watching
them go red rather than assumed:

    --- FAIL: TestRequireHostOwnerRefusesAnotherEnvironmentOfTheSameApplication
        staging against a production-claimed host = <nil>, want HostEnvironmentMismatchError

`just check`, `golangci-lint run ./...` and `go test -race ./...` pass.

Closes #58.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`connect()` built engine.Options without Environment, so it was the empty
string. The engine derives every host path from it —
`names() = Spec.NamesFor(Opts.Environment)` — and an environment that is not in
the map falls back to the project default.

Reproduced against a project whose staging overrides base_path:

    Environment="staging" -> AppDir /srv/staging/sample
    Environment=""        -> AppDir /var/lib/ob/sample

So `ob status --env staging`, `ob audit --env staging` and `ob logs --env
staging` have been reporting on /var/lib/ob/<app> while staging lives somewhere
else, and saying nothing about it. That predates this branch; it is the same
field, so it is fixed here rather than filed and left.

It was also a landmine under the ownership change. Those three commands are
read-only and never reach RequireHostOwner, so nothing breaks today — but the
next mutation added to that path would have compared an empty environment
against a recorded one and refused everything, naming an environment with no
name.

Found by reviewing this branch rather than by a test, because no test built an
engine the way cmd/ob does. There is one now.

`just check`, `golangci-lint run ./...` and `go test -race ./...` pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vishr
vishr merged commit 6a7ad68 into main Aug 18, 2026
5 checks passed
vishr added a commit that referenced this pull request Aug 18, 2026
fix: record the environment in the host owner record, and refuse a mismatch
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.

Runtime names carry no environment, and nothing guards two environments on one host

1 participant