Skip to content

Commit d8bcb00

Browse files
authored
Merge pull request #424 from thevibeworks/feat/name-version-420
feat(name): embed agent CLI version in container names
2 parents 4cc3aa8 + e863eff commit d8bcb00

3 files changed

Lines changed: 85 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
`DEVA_TRACE=1` and removes it on the next non-traced start. Covers
2121
subprocesses and tools that ignore the CA env vars; never touches the
2222
host trust store
23+
- Container names embed the agent CLI version from the image label:
24+
`deva--claude-v2.1.204--auth-file-max--proj..hash`. Persistent
25+
containers pin old images; a CLI bump now creates a distinct container
26+
instead of silently attaching a stale CLI to the same config home.
27+
Label-less images keep the bare agent name (#420)
2328

2429
### Changed
2530
- cctrace pin bumped 0.4.0 -> 0.11.0 (client profiles, shape-first

deva.sh

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,43 @@ compute_shape_hash() {
929929
short_hash "$combined" 8
930930
}
931931

932+
# Agent CLI version from the image label. Persistent containers pin the
933+
# image they were created from; embedding the CLI version in the name keeps
934+
# version drift visible and stops silent attach to a stale-version container
935+
# sharing the same config home (#420). check_image runs before any name is
936+
# built, so the inspect is reliable; label-less images fall back to the
937+
# bare agent name.
938+
AGENT_VERSION_TAG_CACHE_AGENT=""
939+
AGENT_VERSION_TAG_CACHE=""
940+
agent_version_tag() {
941+
local agent="$1"
942+
943+
if [ "$AGENT_VERSION_TAG_CACHE_AGENT" = "$agent" ]; then
944+
printf '%s' "$AGENT_VERSION_TAG_CACHE"
945+
return
946+
fi
947+
948+
local label=""
949+
case "$agent" in
950+
claude) label="org.opencontainers.image.claude_code_version" ;;
951+
codex) label="org.opencontainers.image.codex_version" ;;
952+
gemini) label="org.opencontainers.image.gemini_cli_version" ;;
953+
grok) label="org.opencontainers.image.grok_cli_version" ;;
954+
esac
955+
956+
local ver=""
957+
if [ -n "$label" ]; then
958+
ver=$(docker image inspect --format "{{ index .Config.Labels \"$label\" }}" "$(docker_image_ref)" 2>/dev/null | head -1)
959+
[ "$ver" = "<no value>" ] && ver=""
960+
# Container names allow [a-zA-Z0-9_.-]; drop anything else (keep dots)
961+
ver=$(printf '%s' "$ver" | tr -cd 'a-zA-Z0-9._-')
962+
fi
963+
964+
AGENT_VERSION_TAG_CACHE_AGENT="$agent"
965+
AGENT_VERSION_TAG_CACHE="$ver"
966+
printf '%s' "$ver"
967+
}
968+
932969
build_container_name() {
933970
local prefix="$1"
934971
local agent="$2"
@@ -938,7 +975,12 @@ build_container_name() {
938975
local ephemeral="${6:-false}"
939976
local pid="${7:-}"
940977

941-
local name="${prefix}--${agent}--${auth_tag}--${slug}..${shape_hash}"
978+
local agent_seg="$agent"
979+
local agent_ver
980+
agent_ver="$(agent_version_tag "$agent")"
981+
[ -n "$agent_ver" ] && agent_seg="${agent}-v${agent_ver}"
982+
983+
local name="${prefix}--${agent_seg}--${auth_tag}--${slug}..${shape_hash}"
942984
if [ "$ephemeral" = true ] && [ -n "$pid" ]; then
943985
name="${name}--${pid}"
944986
fi
@@ -1077,8 +1119,8 @@ extract_agent_from_name() {
10771119
local name="$1"
10781120
local rest="${name#"${DEVA_CONTAINER_PREFIX}"}"
10791121

1080-
# New format: deva--<agent>--<auth>--<slug>..<hash>
1081-
if [[ "$rest" =~ ^--([a-z]+)-- ]]; then
1122+
# New format: deva--<agent>[-v<version>]--<auth>--<slug>..<hash>
1123+
if [[ "$rest" =~ ^--([a-z]+)(-v[A-Za-z0-9._-]+)?-- ]]; then
10821124
printf '%s' "${BASH_REMATCH[1]}"
10831125
return
10841126
fi

scripts/test-container-slug.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ source_helpers() {
5252
eval "$(sed -n '/^generate_container_slug_for_path()/,/^}/p' "$REPO_ROOT/deva.sh")"
5353
eval "$(sed -n '/^compute_slug_components_for_path()/,/^}/p' "$REPO_ROOT/deva.sh")"
5454
eval "$(sed -n '/^extract_agent_from_name()/,/^}/p' "$REPO_ROOT/deva.sh")"
55+
eval "$(sed -n '/^agent_version_tag()/,/^}/p' "$REPO_ROOT/deva.sh")"
56+
AGENT_VERSION_TAG_CACHE_AGENT=""
57+
AGENT_VERSION_TAG_CACHE=""
58+
# No image label in unit tests unless a test stubs docker itself
59+
docker_image_ref() { printf 'deva-test:none'; }
5560
DEVA_CONTAINER_PREFIX="deva"
5661
}
5762

@@ -178,6 +183,26 @@ name=$(build_container_name "deva" "gemini" "vertex" "big-project" "11223344" "f
178183
assert_eq "persistent vertex auth" \
179184
"deva--gemini--vertex--big-project..11223344" "$name"
180185

186+
# Versioned agent segment (#420): stub docker to return an image label
187+
docker() {
188+
if [ "$1" = "image" ] && [ "$2" = "inspect" ]; then
189+
printf '2.1.204\n'
190+
return 0
191+
fi
192+
return 1
193+
}
194+
AGENT_VERSION_TAG_CACHE_AGENT="" AGENT_VERSION_TAG_CACHE=""
195+
name=$(build_container_name "deva" "claude" "auth-file-max" "myrepo" "ab12cd34" "false" "")
196+
assert_eq "persistent with agent version" \
197+
"deva--claude-v2.1.204--auth-file-max--myrepo..ab12cd34" "$name"
198+
199+
AGENT_VERSION_TAG_CACHE_AGENT="" AGENT_VERSION_TAG_CACHE=""
200+
name=$(build_container_name "deva" "mystery" "auth-default" "myrepo" "ab12cd34" "false" "")
201+
assert_eq "unmapped agent stays bare" \
202+
"deva--mystery--auth-default--myrepo..ab12cd34" "$name"
203+
unset -f docker
204+
AGENT_VERSION_TAG_CACHE_AGENT="" AGENT_VERSION_TAG_CACHE=""
205+
181206
# ──────────────────────────────────────
182207
echo "=== extract_agent_from_name ==="
183208
# ──────────────────────────────────────
@@ -194,6 +219,12 @@ assert_eq "new format gemini" "gemini" \
194219
assert_eq "new format ephemeral" "claude" \
195220
"$(extract_agent_from_name "deva--claude--api-key-abcd--myrepo..ab12cd34--99999")"
196221

222+
assert_eq "versioned agent segment" "claude" \
223+
"$(extract_agent_from_name "deva--claude-v2.1.204--auth-file-max--myrepo..ab12cd34")"
224+
225+
assert_eq "versioned ephemeral" "codex" \
226+
"$(extract_agent_from_name "deva--codex-v0.131.0--auth-default--myrepo..ab12cd34--4242")"
227+
197228
assert_eq "legacy ephemeral" "claude" \
198229
"$(extract_agent_from_name "deva-myrepo..i47b207-claude-12345")"
199230

@@ -254,7 +285,7 @@ out="$(run_dry claude --debug --dry-run || true)"
254285
cname="$(extract_container_name "$out")"
255286
if [ -n "$cname" ]; then
256287
assert_match "dry-run: new format structure" \
257-
"^deva--claude--auth-default--" "$cname"
288+
"^deva--claude(-v[A-Za-z0-9._-]+)?--auth-default--" "$cname"
258289
assert_match "dry-run: ends with ..hash" \
259290
'\.\.[a-f0-9]{8}$' "$cname"
260291
assert_no_match "dry-run: no old-style ..i prefix" \
@@ -267,7 +298,7 @@ out="$(run_dry codex --debug --dry-run || true)"
267298
cname="$(extract_container_name "$out")"
268299
if [ -n "$cname" ]; then
269300
assert_match "dry-run codex: agent in name" \
270-
"^deva--codex--" "$cname"
301+
"^deva--codex(-v[A-Za-z0-9._-]+)?--" "$cname"
271302
else
272303
fail "dry-run codex: could not extract container name"
273304
fi
@@ -276,7 +307,7 @@ out="$(run_dry gemini --debug --dry-run || true)"
276307
cname="$(extract_container_name "$out")"
277308
if [ -n "$cname" ]; then
278309
assert_match "dry-run gemini: agent in name" \
279-
"^deva--gemini--" "$cname"
310+
"^deva--gemini(-v[A-Za-z0-9._-]+)?--" "$cname"
280311
else
281312
fail "dry-run gemini: could not extract container name"
282313
fi
@@ -288,7 +319,7 @@ if [ -n "$cname" ]; then
288319
assert_match "dry-run ephemeral: has PID suffix" \
289320
'--[0-9]+$' "$cname"
290321
assert_match "dry-run ephemeral: agent in name" \
291-
"^deva--claude--" "$cname"
322+
"^deva--claude(-v[A-Za-z0-9._-]+)?--" "$cname"
292323
else
293324
fail "dry-run ephemeral: could not extract container name"
294325
fi

0 commit comments

Comments
 (0)