Skip to content

Commit ab425ce

Browse files
lroolleclaude
andcommitted
refactor(slug): redesign container naming to deva--agent--auth--slug..hash
Old format leaked implementation details and was hard to read: deva-lroolle-deploydock..i47b207..va3797701..credentials-file-fb38c4e0-claude New format puts semantic fields first with -- separators: deva--claude--auth-file-mykey--lroolle-deploydock..5a8cfeba - Agent always in name (different agents no longer share containers) - Auth tag is human-readable: auth-file-<stem>, api-key-<last4>, auth-default - One unified shape hash replaces scattered ..i/..v/..c suffixes - Normalize short_hash to SHA-256; drop cksum fallback (non-hex output) - Sanitize unknown auth methods through sanitize_slug_component - Anchor project_container_rows regex to prevent substring false positives - Backward-compatible regex matches both old and new format for rm/clean - 59 unit + integration tests in scripts/test-container-slug.sh Close #313 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 09ca3b0 commit ab425ce

2 files changed

Lines changed: 479 additions & 106 deletions

File tree

deva.sh

Lines changed: 161 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,94 @@ generate_container_slug() {
643643
generate_container_slug_for_path "$(pwd)"
644644
}
645645

646+
extract_auth_file_stem() {
647+
local path="$1"
648+
local base
649+
base="$(basename "$path")"
650+
base="${base%.credentials.json}"
651+
base="${base%.json}"
652+
base="$(sanitize_slug_component "$base")"
653+
printf '%s' "${base:0:20}"
654+
}
655+
656+
generate_auth_tag() {
657+
local agent="$1"
658+
local auth_method="${2:-}"
659+
local creds_file="${3:-}"
660+
local env_override="${4:-false}"
661+
662+
if [ "$env_override" = true ]; then
663+
printf '%s' "env"
664+
return
665+
fi
666+
667+
if [ -z "$auth_method" ]; then
668+
printf '%s' "auth-default"
669+
return
670+
fi
671+
672+
case "$agent:$auth_method" in
673+
claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth)
674+
printf '%s' "auth-default"
675+
return
676+
;;
677+
esac
678+
679+
case "$auth_method" in
680+
credentials-file)
681+
if [ -n "$creds_file" ]; then
682+
local stem
683+
stem="$(extract_auth_file_stem "$creds_file")"
684+
printf '%s' "auth-file-${stem}"
685+
else
686+
printf '%s' "auth-file"
687+
fi
688+
;;
689+
api-key|gemini-api-key)
690+
local key_val=""
691+
case "$agent" in
692+
claude) key_val="${ANTHROPIC_API_KEY:-}" ;;
693+
codex) key_val="${OPENAI_API_KEY:-}" ;;
694+
gemini) key_val="${GEMINI_API_KEY:-${GOOGLE_API_KEY:-}}" ;;
695+
esac
696+
if [ -n "$key_val" ] && [ ${#key_val} -ge 4 ]; then
697+
printf '%s' "api-key-${key_val: -4}"
698+
else
699+
printf '%s' "api-key"
700+
fi
701+
;;
702+
*)
703+
printf '%s' "$(sanitize_slug_component "$auth_method")"
704+
;;
705+
esac
706+
}
707+
708+
compute_shape_hash() {
709+
local image_ref="$1"
710+
local volume_input="${2:-}"
711+
local config_input="${3:-}"
712+
local combined="$image_ref"
713+
[ -n "$volume_input" ] && combined="${combined}|${volume_input}"
714+
[ -n "$config_input" ] && combined="${combined}|${config_input}"
715+
short_hash "$combined" 8
716+
}
717+
718+
build_container_name() {
719+
local prefix="$1"
720+
local agent="$2"
721+
local auth_tag="$3"
722+
local slug="$4"
723+
local shape_hash="$5"
724+
local ephemeral="${6:-false}"
725+
local pid="${7:-}"
726+
727+
local name="${prefix}--${agent}--${auth_tag}--${slug}..${shape_hash}"
728+
if [ "$ephemeral" = true ] && [ -n "$pid" ]; then
729+
name="${name}--${pid}"
730+
fi
731+
printf '%s' "$name"
732+
}
733+
646734
slug_candidates_for_path() {
647735
local path="$1"
648736
local parent project
@@ -740,15 +828,19 @@ project_container_rows() {
740828
return
741829
fi
742830

831+
local esc_prefix
832+
esc_prefix=$(printf '%s' "$DEVA_CONTAINER_PREFIX" | sed -e 's/[.[\\^$*+?{}()|]/\\&/g')
743833
local pattern=""
744834
local slug escaped
745835
for slug in $slugs; do
746836
[ -n "$slug" ] || continue
747837
escaped=$(printf '%s' "$slug" | sed -e 's/[.[\\^$*+?{}()|]/\\&/g')
838+
local new_fmt="^${esc_prefix}--.*--${escaped}([.-]|$)"
839+
local old_fmt="^${esc_prefix}-${escaped}([.-]|$)"
748840
if [ -n "$pattern" ]; then
749-
pattern="${pattern}|-${escaped}([.-]|$)"
841+
pattern="${pattern}|(${new_fmt})|(${old_fmt})"
750842
else
751-
pattern="-${escaped}([.-]|$)"
843+
pattern="(${new_fmt})|(${old_fmt})"
752844
fi
753845
done
754846

@@ -769,15 +861,19 @@ project_container_rows() {
769861

770862
extract_agent_from_name() {
771863
local name="$1"
772-
local rest="${name#"${DEVA_CONTAINER_PREFIX}"-}"
864+
local rest="${name#"${DEVA_CONTAINER_PREFIX}"}"
865+
866+
if [[ "$rest" =~ ^--([a-z]+)-- ]]; then
867+
printf '%s' "${BASH_REMATCH[1]}"
868+
return
869+
fi
773870

774-
# Ephemeral pattern: ends with -<agent>-<pid> where pid is all digits
775871
if [[ "$rest" =~ -([a-z]+)-([0-9]+)$ ]]; then
776-
local agent="${BASH_REMATCH[1]}"
777-
printf '%s' "$agent"
778-
else
779-
printf 'share'
872+
printf '%s' "${BASH_REMATCH[1]}"
873+
return
780874
fi
875+
876+
printf 'share'
781877
}
782878

783879
pick_container() {
@@ -902,37 +998,33 @@ prepare_base_docker_args() {
902998
local slug
903999
slug="$(generate_container_slug)"
9041000
905-
local volume_hash=""
1001+
local volume_input=""
9061002
if [ ${#USER_VOLUMES[@]} -gt 0 ]; then
907-
volume_hash=$(compute_volume_hash)
1003+
volume_input=$(printf '%s\n' "${USER_VOLUMES[@]}" | sort | tr '\n' '|')
9081004
fi
9091005
910-
# Include config-home in container identity when explicit.
911-
# Use CONFIG_HOME if set, else CONFIG_ROOT (root mode clears CONFIG_HOME).
912-
local config_hash=""
913-
local config_hash_source=""
1006+
local config_input=""
9141007
if [ "$CONFIG_HOME_FROM_CLI" = true ]; then
9151008
if [ -n "$CONFIG_HOME" ]; then
916-
config_hash_source="$CONFIG_HOME"
1009+
config_input="$CONFIG_HOME"
9171010
elif [ -n "$CONFIG_ROOT" ]; then
918-
config_hash_source="$CONFIG_ROOT"
1011+
config_input="$CONFIG_ROOT"
9191012
fi
9201013
fi
921-
[ -n "$config_hash_source" ] && config_hash=$(short_hash "$config_hash_source" 6)
9221014
923-
local image_hash=""
924-
image_hash=$(short_hash "$(docker_image_ref)" 6)
1015+
local image_ref
1016+
image_ref="$(docker_image_ref)"
1017+
local shape_hash
1018+
shape_hash=$(compute_shape_hash "$image_ref" "$volume_input" "$config_input")
9251019
926-
local suffix=""
927-
[ -n "$image_hash" ] && suffix="..i${image_hash}"
928-
[ -n "$volume_hash" ] && suffix="${suffix}..v${volume_hash}"
929-
[ -n "$config_hash" ] && suffix="${suffix}..c${config_hash}"
1020+
local auth_tag="auth-default"
1021+
container_name=$(build_container_name \
1022+
"$DEVA_CONTAINER_PREFIX" "$ACTIVE_AGENT" "$auth_tag" \
1023+
"$slug" "$shape_hash" "$EPHEMERAL_MODE" "$$")
9301024
9311025
if [ "$EPHEMERAL_MODE" = true ]; then
932-
container_name="${DEVA_CONTAINER_PREFIX}-${slug}${suffix}-${ACTIVE_AGENT}-$$"
9331026
DOCKER_ARGS=(run --rm "${DOCKER_TERMINAL_ARGS[@]}")
9341027
else
935-
container_name="${DEVA_CONTAINER_PREFIX}-${slug}${suffix}"
9361028
DOCKER_ARGS=(run -d)
9371029
fi
9381030
@@ -947,7 +1039,6 @@ prepare_base_docker_args() {
9471039
--add-host host.docker.internal:host-gateway
9481040
)
9491041
950-
# Attach labels to identify workspace and container grouping
9511042
local ws_hash
9521043
ws_hash=$(workspace_hash)
9531044
DOCKER_ARGS+=(
@@ -957,14 +1048,9 @@ prepare_base_docker_args() {
9571048
--label "deva.workspace_hash=${ws_hash}"
9581049
--label "deva.agent=${ACTIVE_AGENT}"
9591050
--label "deva.ephemeral=${EPHEMERAL_MODE}"
960-
--label "deva.image=$(docker_image_ref)"
1051+
--label "deva.image=$image_ref"
1052+
--label "deva.shape_hash=${shape_hash}"
9611053
)
962-
if [ -n "$volume_hash" ]; then
963-
DOCKER_ARGS+=(--label "deva.volhash=${volume_hash}")
964-
fi
965-
if [ -n "$image_hash" ]; then
966-
DOCKER_ARGS+=(--label "deva.image_hash=${image_hash}")
967-
fi
9681054
9691055
if [ -n "${LANG:-}" ]; then DOCKER_ARGS+=(-e "LANG=$LANG"); fi
9701056
if [ -n "${LC_ALL:-}" ]; then DOCKER_ARGS+=(-e "LC_ALL=$LC_ALL"); fi
@@ -1427,12 +1513,12 @@ short_hash() {
14271513
local input="$1"
14281514
local length="${2:-8}"
14291515
1430-
if command -v md5sum >/dev/null 2>&1; then
1431-
printf '%s' "$input" | md5sum | cut -c1-"$length"
1516+
if command -v sha256sum >/dev/null 2>&1; then
1517+
printf '%s' "$input" | sha256sum | cut -c1-"$length"
14321518
elif command -v shasum >/dev/null 2>&1; then
1433-
printf '%s' "$input" | shasum | cut -c1-"$length"
1519+
printf '%s' "$input" | shasum -a 256 | cut -c1-"$length"
14341520
else
1435-
printf '%s' "$input" | cksum | cut -d' ' -f1 | cut -c1-"$length"
1521+
printf '%s' "$input" | md5sum | cut -c1-"$length"
14361522
fi
14371523
}
14381524
@@ -2097,7 +2183,7 @@ if [ "$MANAGEMENT_MODE" = "shell" ] || [ "$MANAGEMENT_MODE" = "ps" ] || [ "$MANA
20972183
else
20982184
slug="$(generate_container_slug)"
20992185
escaped_slug=$(printf '%s' "$slug" | sed 's/[.[\\^$*+?{}()|]/\\&/g')
2100-
rgx="^${DEVA_CONTAINER_PREFIX}-${escaped_slug}(\\.\\.|-|$)"
2186+
rgx="^${DEVA_CONTAINER_PREFIX}(--.*--${escaped_slug}|-${escaped_slug})(\\.\\.|-|$)"
21012187
all_names=$(docker ps -a --format '{{.Names}}')
21022188
matching_names=$(echo "$all_names" | grep -E -- "$rgx" || true)
21032189
fi
@@ -2164,7 +2250,7 @@ if [ "$MANAGEMENT_MODE" = "shell" ] || [ "$MANAGEMENT_MODE" = "ps" ] || [ "$MANA
21642250
if [ -z "$matching_stopped" ]; then
21652251
slug="$(generate_container_slug)"
21662252
escaped_slug=$(printf '%s' "$slug" | sed 's/[.[\\^$*+?{}()|]/\\&/g')
2167-
rgx="^${DEVA_CONTAINER_PREFIX}-${escaped_slug}(\\.\\.|-|$)"
2253+
rgx="^${DEVA_CONTAINER_PREFIX}(--.*--${escaped_slug}|-${escaped_slug})(\\.\\.|-|$)"
21682254
all_stopped=$(docker ps -a --filter "status=exited" --format '{{.Names}}')
21692255
matching_stopped=$(echo "$all_stopped" | grep -E -- "$rgx" || true)
21702256
fi
@@ -2494,91 +2580,60 @@ else
24942580
agent_prepare
24952581
fi
24962582
2497-
# Update container name based on auth method
2498-
if [ -n "${AUTH_METHOD:-}" ]; then
2499-
# Determine if we need auth suffix
2500-
needs_auth_suffix=false
2583+
# Rewrite container name now that AUTH_METHOD is known from agent_prepare().
2584+
# prepare_base_docker_args used auth-default; update if auth is non-default.
2585+
{
2586+
_needs_rewrite=false
25012587
_env_auth_override=false
2502-
if [ "$ACTIVE_AGENT" = "claude" ] && [ "$AUTH_METHOD" != "claude" ]; then
2503-
needs_auth_suffix=true
2504-
elif [ "$ACTIVE_AGENT" = "codex" ] && [ "$AUTH_METHOD" != "chatgpt" ]; then
2505-
needs_auth_suffix=true
2506-
elif [ "$ACTIVE_AGENT" = "gemini" ] && [ "$AUTH_METHOD" != "oauth" ] && [ "$AUTH_METHOD" != "gemini-app-oauth" ]; then
2507-
needs_auth_suffix=true
2508-
fi
2509-
2510-
# Env-var auth override: default method but auth env vars reaching container.
2511-
# Container name must change when effective auth source changes.
2512-
if [ "$needs_auth_suffix" = false ] && has_auth_override; then
2513-
needs_auth_suffix=true
2514-
_env_auth_override=true
2515-
fi
25162588
2517-
if [ "$needs_auth_suffix" = true ]; then
2518-
slug="$(generate_container_slug)"
2519-
volume_hash=""
2520-
if [ ${#USER_VOLUMES[@]} -gt 0 ]; then
2521-
volume_hash=$(compute_volume_hash)
2522-
fi
2589+
if [ -n "${AUTH_METHOD:-}" ]; then
2590+
case "${ACTIVE_AGENT}:${AUTH_METHOD}" in
2591+
claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth) ;;
2592+
*) _needs_rewrite=true ;;
2593+
esac
25232594
2524-
# Recompute config hash to preserve in auth-rewritten name
2525-
auth_config_hash=""
2526-
auth_config_src=""
2527-
if [ "$CONFIG_HOME_FROM_CLI" = true ]; then
2528-
if [ -n "$CONFIG_HOME" ]; then
2529-
auth_config_src="$CONFIG_HOME"
2530-
elif [ -n "$CONFIG_ROOT" ]; then
2531-
auth_config_src="$CONFIG_ROOT"
2532-
fi
2595+
if [ "$_needs_rewrite" = false ] && has_auth_override; then
2596+
_needs_rewrite=true
2597+
_env_auth_override=true
25332598
fi
2534-
[ -n "$auth_config_src" ] && auth_config_hash=$(short_hash "$auth_config_src" 6)
2599+
fi
25352600
2536-
image_hash=$(short_hash "$(docker_image_ref)" 6)
2601+
if [ "$_needs_rewrite" = true ]; then
2602+
_rw_slug="$(generate_container_slug)"
25372603
2538-
# Hash credential file path for credentials-file auth
2539-
creds_hash=""
2540-
if [ "$AUTH_METHOD" = "credentials-file" ] && [ -n "${CUSTOM_CREDENTIALS_FILE:-}" ]; then
2541-
creds_hash=$(short_hash "$CUSTOM_CREDENTIALS_FILE" 8)
2604+
_rw_vol_input=""
2605+
if [ ${#USER_VOLUMES[@]} -gt 0 ]; then
2606+
_rw_vol_input=$(printf '%s\n' "${USER_VOLUMES[@]}" | sort | tr '\n' '|')
25422607
fi
2543-
2544-
new_container_name=""
2545-
if [ "$_env_auth_override" = true ]; then
2546-
auth_suffix="env"
2547-
else
2548-
auth_suffix="${AUTH_METHOD}"
2608+
_rw_cfg_input=""
2609+
if [ "$CONFIG_HOME_FROM_CLI" = true ]; then
2610+
[ -n "$CONFIG_HOME" ] && _rw_cfg_input="$CONFIG_HOME"
2611+
[ -z "$_rw_cfg_input" ] && [ -n "$CONFIG_ROOT" ] && _rw_cfg_input="$CONFIG_ROOT"
25492612
fi
2550-
[ -n "$creds_hash" ] && auth_suffix="${AUTH_METHOD}-${creds_hash}"
2551-
auth_suffix="${auth_suffix}-${ACTIVE_AGENT}"
25522613
2553-
# Build suffix chain: volume + config + auth
2554-
name_suffix=""
2555-
[ -n "$image_hash" ] && name_suffix="..i${image_hash}"
2556-
[ -n "$volume_hash" ] && name_suffix="${name_suffix}..v${volume_hash}"
2557-
[ -n "$auth_config_hash" ] && name_suffix="${name_suffix}..c${auth_config_hash}"
2558-
name_suffix="${name_suffix}..${auth_suffix}"
2614+
_rw_shape=$(compute_shape_hash "$(docker_image_ref)" "$_rw_vol_input" "$_rw_cfg_input")
2615+
_rw_auth_tag=$(generate_auth_tag "$ACTIVE_AGENT" "$AUTH_METHOD" "${CUSTOM_CREDENTIALS_FILE:-}" "$_env_auth_override")
25592616
2560-
if [ "$EPHEMERAL_MODE" = true ]; then
2561-
new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}${name_suffix}-${ACTIVE_AGENT}-$$"
2562-
else
2563-
new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}${name_suffix}"
2564-
fi
2617+
_rw_name=$(build_container_name \
2618+
"$DEVA_CONTAINER_PREFIX" "$ACTIVE_AGENT" "$_rw_auth_tag" \
2619+
"$_rw_slug" "$_rw_shape" "$EPHEMERAL_MODE" "$$")
25652620
2566-
# Update container name in DOCKER_ARGS
25672621
for ((i = 0; i < ${#DOCKER_ARGS[@]}; i++)); do
25682622
if [ "${DOCKER_ARGS[$i]}" = "--name" ] && [ $((i + 1)) -lt ${#DOCKER_ARGS[@]} ]; then
2569-
DOCKER_ARGS[i + 1]="$new_container_name"
2623+
DOCKER_ARGS[i + 1]="$_rw_name"
25702624
break
25712625
fi
25722626
done
2573-
fi
25742627
2575-
# Label auth method for easier filtering
2576-
DOCKER_ARGS+=(--label "deva.auth=${AUTH_METHOD}")
2628+
DOCKER_ARGS+=(--label "deva.auth_tag=${_rw_auth_tag}")
2629+
fi
25772630
2578-
# Export container introspection variables
2579-
DOCKER_ARGS+=(-e "DEVA_AUTH_METHOD=${AUTH_METHOD}")
2580-
[ -n "${AUTH_DETAILS:-}" ] && DOCKER_ARGS+=(-e "DEVA_AUTH_DETAILS=${AUTH_DETAILS}")
2581-
fi
2631+
if [ -n "${AUTH_METHOD:-}" ]; then
2632+
DOCKER_ARGS+=(--label "deva.auth=${AUTH_METHOD}")
2633+
DOCKER_ARGS+=(-e "DEVA_AUTH_METHOD=${AUTH_METHOD}")
2634+
[ -n "${AUTH_DETAILS:-}" ] && DOCKER_ARGS+=(-e "DEVA_AUTH_DETAILS=${AUTH_DETAILS}")
2635+
fi
2636+
}
25822637
25832638
# Determine container name early for env injection
25842639
CONTAINER_NAME=""

0 commit comments

Comments
 (0)