Summary
GET /api/version returns "version": "unknown" (and components.backend, components.agent_server, components.base_image with it) on any image built without the VERSION build arg — i.e. any docker compose -f docker-compose.prod.yml build that doesn't go through scripts/deploy/start.sh.
#993 anticipated exactly this operator and added two independent mitigations. Both are present in the tree, and neither works, because the "unknown" sentinel is a truthy string that short-circuits the fallback chain before the file is ever consulted.
Observed upgrading a live instance v0.8.0 → v0.8.5:
{ "version": "unknown", "components": { "backend": "unknown",
"base_image": "trinity-agent-base:unknown" }, "git_commit": "unknown" }
The instance could not report which build it was running — during an upgrade, which is precisely when you need it.
Root cause
The intended 3-tier resolution (src/backend/main.py:1282-1299):
# Version resolution order (#993):
# 1. VERSION env var — build-stamped from git (e.g. "0.9.0+g4c640b6e"), …
# 2. VERSION file — curated semver, mounted in dev / copied in image.
# 3. "unknown" — neither present.
version = os.getenv("VERSION") or None
if not version:
version_paths = [Path("/app/VERSION"), …]
…
version = version or "unknown"
Tier 2 is unreachable. docker/backend/Dockerfile:19-25:
ARG VERSION=unknown
…
ENV VERSION=${VERSION} \
ENV is unconditional, so the env var is always set in the image — to the literal string "unknown" when no build arg was passed. os.getenv("VERSION") therefore returns "unknown", which is truthy, so or None does not fire, if not version: is False, and the /app/VERSION file is never read.
Both mitigations #993 added are consequently dead code in the only case they were written for:
docker/backend/Dockerfile:114 — COPY ../../VERSION /app/VERSION, whose own comment reads "prod compose doesn't [mount it], so without this COPY the version field falls back to unknown". The COPY lands; the reader never gets there.
docker/backend/Dockerfile:17-18 — "Defaults to unknown so local builds and operators who skip start.sh still get a well-typed response." Well-typed, but the response is unknown rather than the curated semver sitting in the image one path away.
This is a sentinel-vs-absence bug: ARG+ENV cannot express "unset", so the sentinel must be filtered on read.
Suggested fix
Treat the sentinel as absence at the single read site:
_version_env = os.getenv("VERSION")
version = _version_env if _version_env and _version_env != "unknown" else None
Tier 2 then resolves 0.8.5 from the baked file, and an operator who skips start.sh gets the curated semver without the git suffix — a correct, honest, partial answer instead of nothing.
Scope note: the same ARG=unknown → ENV shape applies to GIT_COMMIT, GIT_BRANCH, BUILD_DATE, GIT_COMMIT_SUBJECT, GIT_COMMIT_TIMESTAMP. Those have no file fallback (git metadata genuinely isn't in the image), so "unknown" is the right answer for them and they need no change — but it's worth a comment saying so, since the asymmetry is what makes this bug easy to reintroduce.
Acceptance criteria
Environment
Trinity 0.8.5 (e9eb40da), prod compose, built via docker compose build without start.sh.
Summary
GET /api/versionreturns"version": "unknown"(andcomponents.backend,components.agent_server,components.base_imagewith it) on any image built without theVERSIONbuild arg — i.e. anydocker compose -f docker-compose.prod.yml buildthat doesn't go throughscripts/deploy/start.sh.#993 anticipated exactly this operator and added two independent mitigations. Both are present in the tree, and neither works, because the
"unknown"sentinel is a truthy string that short-circuits the fallback chain before the file is ever consulted.Observed upgrading a live instance v0.8.0 → v0.8.5:
{ "version": "unknown", "components": { "backend": "unknown", "base_image": "trinity-agent-base:unknown" }, "git_commit": "unknown" }The instance could not report which build it was running — during an upgrade, which is precisely when you need it.
Root cause
The intended 3-tier resolution (
src/backend/main.py:1282-1299):Tier 2 is unreachable.
docker/backend/Dockerfile:19-25:ENVis unconditional, so the env var is always set in the image — to the literal string"unknown"when no build arg was passed.os.getenv("VERSION")therefore returns"unknown", which is truthy, soor Nonedoes not fire,if not version:is False, and the/app/VERSIONfile is never read.Both mitigations #993 added are consequently dead code in the only case they were written for:
docker/backend/Dockerfile:114—COPY ../../VERSION /app/VERSION, whose own comment reads "prod compose doesn't [mount it], so without this COPY the version field falls back tounknown". The COPY lands; the reader never gets there.docker/backend/Dockerfile:17-18— "Defaults tounknownso local builds and operators who skip start.sh still get a well-typed response." Well-typed, but the response isunknownrather than the curated semver sitting in the image one path away.This is a sentinel-vs-absence bug:
ARG+ENVcannot express "unset", so the sentinel must be filtered on read.Suggested fix
Treat the sentinel as absence at the single read site:
Tier 2 then resolves
0.8.5from the baked file, and an operator who skipsstart.shgets the curated semver without the git suffix — a correct, honest, partial answer instead of nothing.Scope note: the same
ARG=unknown → ENVshape applies toGIT_COMMIT,GIT_BRANCH,BUILD_DATE,GIT_COMMIT_SUBJECT,GIT_COMMIT_TIMESTAMP. Those have no file fallback (git metadata genuinely isn't in the image), so"unknown"is the right answer for them and they need no change — but it's worth a comment saying so, since the asymmetry is what makes this bug easy to reintroduce.Acceptance criteria
docker compose -f docker-compose.prod.yml build(no build args) →/api/versionreports theVERSIONfile contents, not"unknown"0.8.5+g<sha>[.dirty])components.backend/agent_server/base_imagetrack the resolved valueVERSION=unknownin env plus a present/app/VERSION— the exact combination that is broken todayEnvironment
Trinity
0.8.5(e9eb40da), prod compose, built viadocker compose buildwithoutstart.sh.