@@ -39,6 +39,37 @@ LOADED_CONFIGS=()
3939AGENT_ARGS=()
4040AGENT_EXPLICIT=false
4141
42+ normalize_docker_image_parts () {
43+ local tail=" ${DEVA_DOCKER_IMAGE##*/ } "
44+
45+ if [[ " $DEVA_DOCKER_IMAGE " == * @* ]]; then
46+ DEVA_DOCKER_TAG=" "
47+ DEVA_DOCKER_TAG_ENV_SET=true
48+ return
49+ fi
50+
51+ if [[ " $tail " == * :* ]]; then
52+ local embedded_tag=" ${tail##*: } "
53+ DEVA_DOCKER_IMAGE=" ${DEVA_DOCKER_IMAGE%:* } "
54+ if [ " $DEVA_DOCKER_TAG_ENV_SET " = false ]; then
55+ DEVA_DOCKER_TAG=" $embedded_tag "
56+ DEVA_DOCKER_TAG_ENV_SET=true
57+ fi
58+ fi
59+ }
60+
61+ docker_image_ref () {
62+ if [[ " $DEVA_DOCKER_IMAGE " == * @* ]]; then
63+ printf ' %s' " $DEVA_DOCKER_IMAGE "
64+ elif [ -n " ${DEVA_DOCKER_TAG:- } " ]; then
65+ printf ' %s:%s' " $DEVA_DOCKER_IMAGE " " $DEVA_DOCKER_TAG "
66+ else
67+ printf ' %s' " $DEVA_DOCKER_IMAGE "
68+ fi
69+ }
70+
71+ normalize_docker_image_parts
72+
4273EPHEMERAL_MODE=false
4374QUICK_MODE=false
4475GLOBAL_MODE=false
@@ -90,15 +121,15 @@ Deva flags:
90121
91122Container Behavior (NEW in v0.8.0):
92123 Default (persistent): Shared per project by default, but split when container shape changes
93- (extra volumes, explicit config-home, auth mode).
124+ (image/profile, extra volumes, explicit config-home, auth mode).
94125 Preserves state (npm packages, builds, etc).
95126 Faster startup, and default-auth runs can share one warm container.
96127
97128 With --rm (ephemeral): Create new container, auto-remove after exit.
98129 Agent-specific naming for parallel runs.
99130
100131Container Naming (NEW):
101- Persistent: deva-<parent>-<project>[..shape] # shape may encode volumes/config/auth
132+ Persistent: deva-<parent>-<project>[..shape] # shape may encode image/ volumes/config/auth
102133 Ephemeral: deva-<parent>-<project>-<agent>-<pid> # Agent-specific
103134
104135 Example:
@@ -185,33 +216,37 @@ check_agent() {
185216}
186217
187218check_image () {
188- if docker image inspect " ${DEVA_DOCKER_IMAGE} :${DEVA_DOCKER_TAG} " > /dev/null 2>&1 ; then
219+ local image_ref
220+ image_ref=" $( docker_image_ref) "
221+
222+ if docker image inspect " $image_ref " > /dev/null 2>&1 ; then
189223 return
190224 fi
191225
192226 # Try pulling first
193- if docker pull " ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} " > /dev/null 2>&1 ; then
227+ if docker pull " $image_ref " > /dev/null 2>&1 ; then
194228 return
195229 fi
196230
197- # Smart fallback: check for available profile images locally
231+ # Smart fallback: check for available profile images locally.
232+ # Digest-pinned refs are exact; tag fallback does not make sense there.
198233 local available_tags=" "
199- local original_tag= " $DEVA_DOCKER_TAG "
200-
201- # Check common profile tags (prefer rust as it's a superset of base)
202- for tag in rust latest ; do
203- if [ " $tag " = " $DEVA_DOCKER_TAG " ] ; then
204- continue # Skip the one we already tried
205- fi
206- if docker image inspect " ${DEVA_DOCKER_IMAGE} : ${tag} " > /dev/null 2>&1 ; then
207- available_tags= " ${available_tags}${tag} "
208- fi
209- done
234+ if [[ " $DEVA_DOCKER_IMAGE " != * @ * ]] ; then
235+ # Check common profile tags (prefer rust as it's a superset of base)
236+ for tag in rust latest ; do
237+ if [ " $ tag" = " $DEVA_DOCKER_TAG " ] ; then
238+ continue # Skip the one we already tried
239+ fi
240+ if docker image inspect " ${DEVA_DOCKER_IMAGE} : ${tag} " > /dev/null 2>&1 ; then
241+ available_tags= " ${available_tags} ${tag} "
242+ fi
243+ done
244+ fi
210245
211246 if [ -n " $available_tags " ]; then
212247 # Found alternative images - use the first one
213248 local fallback_tag=" ${available_tags%% * } " # Get first tag
214- echo " Image ${DEVA_DOCKER_IMAGE} : ${original_tag} not found" >&2
249+ echo " Image $image_ref not found" >&2
215250 echo " Using available image: ${DEVA_DOCKER_IMAGE} :${fallback_tag} " >&2
216251 DEVA_DOCKER_TAG=" $fallback_tag "
217252 return
@@ -225,7 +260,7 @@ check_image() {
225260 ;;
226261 esac
227262
228- echo " Docker image ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} not found locally" >&2
263+ echo " Docker image $image_ref not found locally" >&2
229264 if [ -n " $df " ]; then
230265 echo " A matching Dockerfile exists at: $df " >&2
231266 case " ${PROFILE:- } " in
@@ -242,7 +277,7 @@ check_image() {
242277 ;;
243278 esac
244279 else
245- echo " Pull with: docker pull ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} " >&2
280+ echo " Pull with: docker pull $image_ref " >&2
246281 fi
247282 exit 1
248283}
@@ -609,7 +644,7 @@ show_config() {
609644 fi
610645 echo " "
611646
612- echo " Docker image: ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} "
647+ echo " Docker image: $( docker_image_ref ) "
613648 echo " Container prefix: $DEVA_CONTAINER_PREFIX "
614649}
615650
@@ -634,18 +669,14 @@ prepare_base_docker_args() {
634669 config_hash_source=" $CONFIG_ROOT "
635670 fi
636671 fi
637- if [ -n " $config_hash_source " ]; then
638- if command -v md5sum > /dev/null 2>&1 ; then
639- config_hash=$( printf ' %s' " $config_hash_source " | md5sum | cut -c1-6)
640- elif command -v shasum > /dev/null 2>&1 ; then
641- config_hash=$( printf ' %s' " $config_hash_source " | shasum | cut -c1-6)
642- else
643- config_hash=$( printf ' %s' " $config_hash_source " | cksum | cut -d' ' -f1 | cut -c1-6)
644- fi
645- fi
672+ [ -n " $config_hash_source " ] && config_hash=$( short_hash " $config_hash_source " 6)
673+
674+ local image_hash=" "
675+ image_hash=$( short_hash " $( docker_image_ref) " 6)
646676
647677 local suffix=" "
648- [ -n " $volume_hash " ] && suffix=" ..v${volume_hash} "
678+ [ -n " $image_hash " ] && suffix=" ..i${image_hash} "
679+ [ -n " $volume_hash " ] && suffix=" ${suffix} ..v${volume_hash} "
649680 [ -n " $config_hash " ] && suffix=" ${suffix} ..c${config_hash} "
650681
651682 if [ " $EPHEMERAL_MODE " = true ]; then
@@ -677,10 +708,14 @@ prepare_base_docker_args() {
677708 --label " deva.workspace_hash=${ws_hash} "
678709 --label " deva.agent=${ACTIVE_AGENT} "
679710 --label " deva.ephemeral=${EPHEMERAL_MODE} "
711+ --label " deva.image=$( docker_image_ref) "
680712 )
681713 if [ -n " $volume_hash " ]; then
682714 DOCKER_ARGS+=(--label " deva.volhash=${volume_hash} " )
683715 fi
716+ if [ -n " $image_hash " ]; then
717+ DOCKER_ARGS+=(--label " deva.image_hash=${image_hash} " )
718+ fi
684719
685720 if [ -n " ${LANG:- } " ]; then DOCKER_ARGS+=(-e " LANG=$LANG " ); fi
686721 if [ -n " ${LC_ALL:- } " ]; then DOCKER_ARGS+=(-e " LC_ALL=$LC_ALL " ); fi
@@ -1139,6 +1174,19 @@ normalize_volume_spec() {
11391174 echo " $src :$remainder "
11401175}
11411176
1177+ short_hash () {
1178+ local input=" $1 "
1179+ local length=" ${2:- 8} "
1180+
1181+ if command -v md5sum > /dev/null 2>&1 ; then
1182+ printf ' %s' " $input " | md5sum | cut -c1-" $length "
1183+ elif command -v shasum > /dev/null 2>&1 ; then
1184+ printf ' %s' " $input " | shasum | cut -c1-" $length "
1185+ else
1186+ printf ' %s' " $input " | cksum | cut -d' ' -f1 | cut -c1-" $length "
1187+ fi
1188+ }
1189+
11421190compute_volume_hash () {
11431191 if [ ${# USER_VOLUMES[@]} -eq 0 ]; then
11441192 return
@@ -1161,15 +1209,7 @@ compute_volume_hash() {
11611209 hash_input=" ${hash_input}${src} :${vol#*: } |"
11621210 done <<< " $sorted_vols"
11631211
1164- if [ -n " $hash_input " ]; then
1165- if command -v md5sum > /dev/null 2>&1 ; then
1166- echo " $hash_input " | md5sum | cut -c1-8
1167- elif command -v shasum > /dev/null 2>&1 ; then
1168- echo " $hash_input " | shasum | cut -c1-8
1169- else
1170- echo " $hash_input " | cksum | cut -d' ' -f1 | cut -c1-8
1171- fi
1172- fi
1212+ [ -n " $hash_input " ] && short_hash " $hash_input " 8
11731213}
11741214
11751215workspace_hash () {
@@ -1180,13 +1220,7 @@ workspace_hash() {
11801220
11811221 local p
11821222 p=" $( pwd) "
1183- if command -v md5sum > /dev/null 2>&1 ; then
1184- _WS_HASH_CACHE=$( printf ' %s' " $p " | md5sum | cut -c1-8)
1185- elif command -v shasum > /dev/null 2>&1 ; then
1186- _WS_HASH_CACHE=$( printf ' %s' " $p " | shasum | cut -c1-8)
1187- else
1188- _WS_HASH_CACHE=$( printf ' %s' " $p " | cksum | cut -d' ' -f1 | cut -c1-8)
1189- fi
1223+ _WS_HASH_CACHE=$( short_hash " $p " 8)
11901224 printf ' %s' " $_WS_HASH_CACHE "
11911225}
11921226
@@ -1399,6 +1433,20 @@ process_var_config() {
13991433 set_config_home_value " $value "
14001434 fi
14011435 ;;
1436+ DEVA_DOCKER_IMAGE)
1437+ DEVA_DOCKER_IMAGE=" $value "
1438+ DEVA_DOCKER_IMAGE_ENV_SET=true
1439+ normalize_docker_image_parts
1440+ export DEVA_DOCKER_IMAGE
1441+ USER_ENVS+=(" $name =$value " )
1442+ ;;
1443+ DEVA_DOCKER_TAG)
1444+ DEVA_DOCKER_TAG=" $value "
1445+ DEVA_DOCKER_TAG_ENV_SET=true
1446+ normalize_docker_image_parts
1447+ export DEVA_DOCKER_TAG
1448+ USER_ENVS+=(" $name =$value " )
1449+ ;;
14021450 DEFAULT_AGENT)
14031451 DEFAULT_AGENT=" $value "
14041452 ;;
@@ -1716,7 +1764,7 @@ if [ ${#PRE_ARGS[@]} -gt 0 ]; then
17161764 ;;
17171765 --version)
17181766 echo " deva.sh v${VERSION} "
1719- echo " Docker Image: ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} "
1767+ echo " Docker Image: $( docker_image_ref ) "
17201768 exit 0
17211769 ;;
17221770 --show-config)
@@ -1766,6 +1814,7 @@ if [ "$MANAGEMENT_MODE" = "shell" ] || [ "$MANAGEMENT_MODE" = "ps" ] || [ "$MANA
17661814 if [ -z " $ACTIVE_AGENT " ]; then
17671815 ACTIVE_AGENT=" $DEFAULT_AGENT "
17681816 fi
1817+ resolve_profile
17691818 show_config
17701819 exit 0
17711820 fi
@@ -2232,26 +2281,14 @@ if [ -n "${AUTH_METHOD:-}" ]; then
22322281 auth_config_src=" $CONFIG_ROOT "
22332282 fi
22342283 fi
2235- if [ -n " $auth_config_src " ]; then
2236- if command -v md5sum > /dev/null 2>&1 ; then
2237- auth_config_hash=$( printf ' %s' " $auth_config_src " | md5sum | cut -c1-6)
2238- elif command -v shasum > /dev/null 2>&1 ; then
2239- auth_config_hash=$( printf ' %s' " $auth_config_src " | shasum | cut -c1-6)
2240- else
2241- auth_config_hash=$( printf ' %s' " $auth_config_src " | cksum | cut -d' ' -f1 | cut -c1-6)
2242- fi
2243- fi
2284+ [ -n " $auth_config_src " ] && auth_config_hash=$( short_hash " $auth_config_src " 6)
2285+
2286+ image_hash=$( short_hash " $( docker_image_ref) " 6)
22442287
22452288 # Hash credential file path for credentials-file auth
22462289 creds_hash=" "
22472290 if [ " $AUTH_METHOD " = " credentials-file" ] && [ -n " ${CUSTOM_CREDENTIALS_FILE:- } " ]; then
2248- if command -v md5sum > /dev/null 2>&1 ; then
2249- creds_hash=$( printf ' %s' " $CUSTOM_CREDENTIALS_FILE " | md5sum | cut -c1-8)
2250- elif command -v shasum > /dev/null 2>&1 ; then
2251- creds_hash=$( printf ' %s' " $CUSTOM_CREDENTIALS_FILE " | shasum | cut -c1-8)
2252- else
2253- creds_hash=$( printf ' %s' " $CUSTOM_CREDENTIALS_FILE " | cksum | cut -d' ' -f1 | cut -c1-8)
2254- fi
2291+ creds_hash=$( short_hash " $CUSTOM_CREDENTIALS_FILE " 8)
22552292 fi
22562293
22572294 new_container_name=" "
@@ -2265,7 +2302,8 @@ if [ -n "${AUTH_METHOD:-}" ]; then
22652302
22662303 # Build suffix chain: volume + config + auth
22672304 name_suffix=" "
2268- [ -n " $volume_hash " ] && name_suffix=" ..v${volume_hash} "
2305+ [ -n " $image_hash " ] && name_suffix=" ..i${image_hash} "
2306+ [ -n " $volume_hash " ] && name_suffix=" ${name_suffix} ..v${volume_hash} "
22692307 [ -n " $auth_config_hash " ] && name_suffix=" ${name_suffix} ..c${auth_config_hash} "
22702308 name_suffix=" ${name_suffix} ..${auth_suffix} "
22712309
@@ -2360,7 +2398,7 @@ if [ "$QUICK_MODE" = false ] && [ -d "$(pwd)/.claude" ]; then
23602398 DOCKER_ARGS+=(" -v" " $( pwd) /.claude:$( pwd) /.claude" )
23612399fi
23622400
2363- DOCKER_ARGS+= (" ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} " )
2401+ DOCKER_ARGS+= (" $( docker_image_ref ) " )
23642402
23652403if [ " $ACTION " = " shell" ]; then
23662404 AGENT_COMMAND=(" /bin/zsh" )
@@ -2479,7 +2517,7 @@ if [ "$EPHEMERAL_MODE" = false ]; then
24792517
24802518 exec docker exec " ${DOCKER_TERMINAL_ARGS[@]} " " $CONTAINER_NAME " /usr/local/bin/docker-entrypoint.sh " ${AGENT_COMMAND[@]} "
24812519else
2482- echo " Launching ${ACTIVE_AGENT} (ephemeral mode) via ${DEVA_DOCKER_IMAGE} : ${DEVA_DOCKER_TAG} "
2520+ echo " Launching ${ACTIVE_AGENT} (ephemeral mode) via $( docker_image_ref ) "
24832521 write_session_file
24842522 if ! docker " ${DOCKER_ARGS[@]} " " ${AGENT_COMMAND[@]} " ; then
24852523 echo " error: failed to launch ephemeral container" >&2
0 commit comments