Skip to content

Commit 2d4fed8

Browse files
committed
feat: support custom credential files via --auth-with
Allow passing credential file paths directly to --auth-with instead of only accepting predefined auth methods. When a file path is detected (absolute, relative, tilde-expanded, or *.json pattern), automatically mount it as the agent's credential file with proper backup of existing credentials. - Add is_file_path() and expand_and_validate_file() helpers for path detection - Implement backup_credentials() to preserve existing creds before switching - Support credentials-file auth method in both claude and codex agents - Add workspace hashing and session tracking (~/config/deva/sessions/*.json) - Attach Docker labels for workspace/auth/ephemeral tracking - Export DEVA_AUTH_* environment variables into container for introspection - Add 'status' management command to inspect running sessions - Improve 'rm' command with --all flag and better container selection Backward compatible: predefined auth methods still work as before.
1 parent d053fc3 commit 2d4fed8

11 files changed

Lines changed: 700 additions & 927 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Created by https://www.toptal.com/developers/gitignore/api/node
22
# Edit at https://www.toptal.com/developers/gitignore?templates=node
33

4+
*.bak
5+
46
### Node ###
57
# Logs
68
logs

Dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
7575

7676
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
7777

78+
# Pre-install Python 3.14t (free-threaded) for uv
79+
RUN /root/.local/bin/uv python install 3.14t
80+
7881
RUN --mount=type=cache,target=/tmp/go-cache,sharing=locked \
7982
ARCH=$(dpkg --print-architecture) && \
8083
GO_ARCH=$([ "$ARCH" = "amd64" ] && echo "amd64" || echo "arm64") && \
@@ -173,7 +176,7 @@ RUN --mount=type=cache,target=/home/deva/.npm,uid=${DEVA_UID},gid=${DEVA_GID},sh
173176
npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex || true
174177

175178
# Install Go tools for Atlassian integration (Confluence/Jira/Bitbucket)
176-
RUN go install github.com/lroolle/atlas-cli/cmd/atl@main && \
179+
RUN go install github.com/lroolle/atlas-cli/cmd/atl@5f6a20c4d164bf6fe6f5c60f9ac12dfccf210758 && \
177180
sudo mv $HOME/go/bin/atl /usr/local/bin/
178181

179182
RUN git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh "$DEVA_HOME/.oh-my-zsh" && \
@@ -187,6 +190,10 @@ RUN echo 'export ZSH="$HOME/.oh-my-zsh"' > "$DEVA_HOME/.zshrc" && \
187190
echo 'source $ZSH/oh-my-zsh.sh' >> "$DEVA_HOME/.zshrc" && \
188191
echo 'export PATH=$HOME/.local/bin:$HOME/.npm-global/bin:$HOME/go/bin:/usr/local/go/bin:$PATH' >> "$DEVA_HOME/.zshrc"
189192

193+
# Pre-install uv for deva user and warm Python 3.14t
194+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
195+
$DEVA_HOME/.local/bin/uv python install 3.14t
196+
190197
USER root
191198

192199
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh

agents/claude.sh

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,32 @@ setup_claude_auth() {
4141

4242
case "$method" in
4343
claude)
44+
AUTH_DETAILS="claude-app-oauth (~/.claude)"
4445
;;
4546
api-key)
46-
validate_anthropic_key || auth_error "ANTHROPIC_API_KEY not set for --auth-with api-key" \
47-
"Set: export ANTHROPIC_API_KEY=your_api_key"
48-
DOCKER_ARGS+=("-e" "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
47+
# Auto-detect OAuth token vs regular API key
48+
if [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
49+
DOCKER_ARGS+=("-e" "CLAUDE_CODE_OAUTH_TOKEN=$CLAUDE_CODE_OAUTH_TOKEN")
50+
AUTH_DETAILS="oauth-token (CLAUDE_CODE_OAUTH_TOKEN)"
51+
echo "Using OAuth token from CLAUDE_CODE_OAUTH_TOKEN" >&2
52+
elif [ -n "${ANTHROPIC_API_KEY:-}" ] && is_oauth_token_pattern "$ANTHROPIC_API_KEY"; then
53+
DOCKER_ARGS+=("-e" "CLAUDE_CODE_OAUTH_TOKEN=$ANTHROPIC_API_KEY")
54+
AUTH_DETAILS="oauth-token (auto-detected from ANTHROPIC_API_KEY)"
55+
echo "Detected OAuth token in ANTHROPIC_API_KEY, using as CLAUDE_CODE_OAUTH_TOKEN" >&2
56+
elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then
57+
DOCKER_ARGS+=("-e" "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
58+
AUTH_DETAILS="api-key (ANTHROPIC_API_KEY)"
59+
else
60+
auth_error "No API key found for --auth-with api-key" \
61+
"Set: export ANTHROPIC_API_KEY=sk-ant-... or export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-..."
62+
fi
4963
;;
5064
copilot)
5165
validate_github_token || auth_error "No GitHub token found for copilot auth" \
5266
"Run: copilot-api auth, or set GH_TOKEN=\$(gh auth token)"
5367
start_copilot_proxy
5468

69+
AUTH_DETAILS="github-copilot (proxy port $COPILOT_PROXY_PORT)"
5570
DOCKER_ARGS+=("-e" "ANTHROPIC_BASE_URL=http://$COPILOT_HOST_MAPPING:$COPILOT_PROXY_PORT")
5671
DOCKER_ARGS+=("-e" "ANTHROPIC_API_KEY=dummy")
5772

@@ -74,9 +89,11 @@ setup_claude_auth() {
7489
auth_error "CLAUDE_CODE_OAUTH_TOKEN not set for --auth-with oat" \
7590
"Set: export CLAUDE_CODE_OAUTH_TOKEN=your_token"
7691
fi
92+
AUTH_DETAILS="oauth-token (CLAUDE_CODE_OAUTH_TOKEN)"
7793
DOCKER_ARGS+=("-e" "CLAUDE_CODE_OAUTH_TOKEN=$CLAUDE_CODE_OAUTH_TOKEN")
7894
;;
7995
bedrock)
96+
AUTH_DETAILS="aws-bedrock (region: ${AWS_REGION:-default})"
8097
DOCKER_ARGS+=("-e" "CLAUDE_CODE_USE_BEDROCK=1")
8198
if [ -d "$HOME/.aws" ]; then
8299
DOCKER_ARGS+=("-v" "$HOME/.aws:/home/deva/.aws:ro")
@@ -95,6 +112,7 @@ setup_claude_auth() {
95112
fi
96113
;;
97114
vertex)
115+
AUTH_DETAILS="google-vertex (gcloud)"
98116
DOCKER_ARGS+=("-e" "CLAUDE_CODE_USE_VERTEX=1")
99117
if [ -d "$HOME/.config/gcloud" ]; then
100118
DOCKER_ARGS+=("-v" "$HOME/.config/gcloud:/home/deva/.config/gcloud:ro")
@@ -104,6 +122,15 @@ setup_claude_auth() {
104122
DOCKER_ARGS+=("-e" "GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS")
105123
fi
106124
;;
125+
credentials-file)
126+
if [ -z "${CUSTOM_CREDENTIALS_FILE:-}" ]; then
127+
auth_error "CUSTOM_CREDENTIALS_FILE not set for credentials-file auth"
128+
fi
129+
AUTH_DETAILS="credentials-file ($CUSTOM_CREDENTIALS_FILE)"
130+
backup_credentials "claude" "${CONFIG_ROOT:-}" "$CUSTOM_CREDENTIALS_FILE"
131+
DOCKER_ARGS+=("-v" "$CUSTOM_CREDENTIALS_FILE:/home/deva/.claude/.credentials.json")
132+
echo "Using custom credentials: $CUSTOM_CREDENTIALS_FILE -> /home/deva/.claude/.credentials.json" >&2
133+
;;
107134
*)
108135
auth_error "auth method '$method' not implemented"
109136
;;

agents/codex.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,20 @@ setup_codex_auth() {
6161

6262
case "$method" in
6363
chatgpt)
64+
AUTH_DETAILS="chatgpt-oauth (~/.codex)"
6465
;;
6566
api-key)
6667
validate_openai_key || auth_error "OPENAI_API_KEY not set for --auth-with api-key" \
6768
"Set: export OPENAI_API_KEY=your_api_key"
69+
AUTH_DETAILS="api-key (OPENAI_API_KEY)"
6870
DOCKER_ARGS+=("-e" "OPENAI_API_KEY=$OPENAI_API_KEY")
6971
;;
7072
copilot)
7173
validate_github_token || auth_error "No GitHub token found for copilot auth" \
7274
"Run: copilot-api auth, or set GH_TOKEN=\$(gh auth token)"
7375
start_copilot_proxy
7476

77+
AUTH_DETAILS="github-copilot (proxy port $COPILOT_PROXY_PORT)"
7578
DOCKER_ARGS+=("-e" "OPENAI_BASE_URL=http://$COPILOT_HOST_MAPPING:$COPILOT_PROXY_PORT")
7679
DOCKER_ARGS+=("-e" "OPENAI_API_KEY=dummy")
7780

@@ -87,6 +90,15 @@ setup_codex_auth() {
8790
DOCKER_ARGS+=("-e" "NO_PROXY=${NO_PROXY:+$NO_PROXY,}$no_proxy")
8891
DOCKER_ARGS+=("-e" "no_grpc_proxy=${NO_GRPC_PROXY:+$NO_GRPC_PROXY,}$no_proxy")
8992
;;
93+
credentials-file)
94+
if [ -z "${CUSTOM_CREDENTIALS_FILE:-}" ]; then
95+
auth_error "CUSTOM_CREDENTIALS_FILE not set for credentials-file auth"
96+
fi
97+
AUTH_DETAILS="credentials-file ($CUSTOM_CREDENTIALS_FILE)"
98+
backup_credentials "codex" "${CONFIG_ROOT:-}" "$CUSTOM_CREDENTIALS_FILE"
99+
DOCKER_ARGS+=("-v" "$CUSTOM_CREDENTIALS_FILE:/home/deva/.codex/auth.json")
100+
echo "Using custom credentials: $CUSTOM_CREDENTIALS_FILE -> /home/deva/.codex/auth.json" >&2
101+
;;
90102
*)
91103
auth_error "auth method '$method' not implemented"
92104
;;

agents/shared_auth.sh

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,22 @@ validate_github_token() {
4343
}
4444

4545
validate_anthropic_key() {
46-
[ -n "${ANTHROPIC_API_KEY:-}" ]
46+
[ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]
4747
}
4848

4949
validate_openai_key() {
5050
[ -n "${OPENAI_API_KEY:-}" ]
5151
}
5252

53+
# Detects OAuth token pattern in environment variables only
54+
# (not for inspecting credential file contents)
55+
# OAuth tokens: sk-ant-oat01-*
56+
# API keys: sk-ant-api03-*
57+
is_oauth_token_pattern() {
58+
local key="$1"
59+
[[ "$key" == sk-ant-oat01-* ]]
60+
}
61+
5362
COPILOT_PROXY_PID=""
5463
COPILOT_PROXY_PORT="$COPILOT_DEFAULT_PORT"
5564

@@ -212,6 +221,66 @@ convert_openai_model_alias() {
212221
esac
213222
}
214223

224+
is_file_path() {
225+
local arg="$1"
226+
[[ "$arg" == /* ]] || [[ "$arg" == ~* ]] || [[ "$arg" == ./* ]] || [[ "$arg" == ../* ]] || [[ "$arg" == *.json ]]
227+
}
228+
229+
expand_and_validate_file() {
230+
local path="$1"
231+
if [[ "$path" == ~* ]]; then
232+
path="${path/#\~/$HOME}"
233+
fi
234+
if [ ! -f "$path" ]; then
235+
auth_error "Credentials file not found: $path"
236+
fi
237+
if [[ "$path" == /* ]]; then
238+
echo "$path"
239+
else
240+
echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
241+
fi
242+
}
243+
244+
backup_credentials() {
245+
local agent_name="$1"
246+
local config_root="$2"
247+
local source_file="${3:-}" # Optional: file to compare against
248+
local backup_path=""
249+
local creds_file=""
250+
251+
case "$agent_name" in
252+
claude)
253+
if [ -n "$config_root" ] && [ -d "$config_root/claude/.claude" ]; then
254+
creds_file="$config_root/claude/.claude/.credentials.json"
255+
elif [ -d "$HOME/.claude" ]; then
256+
creds_file="$HOME/.claude/.credentials.json"
257+
fi
258+
;;
259+
codex)
260+
if [ -n "$config_root" ] && [ -d "$config_root/codex/.codex" ]; then
261+
creds_file="$config_root/codex/.codex/auth.json"
262+
elif [ -d "$HOME/.codex" ]; then
263+
creds_file="$HOME/.codex/auth.json"
264+
fi
265+
;;
266+
esac
267+
268+
if [ -n "$creds_file" ] && [ -f "$creds_file" ]; then
269+
# Compare with source file if provided (avoid duplicate backups)
270+
if [ -n "$source_file" ] && [ -f "$source_file" ]; then
271+
if cmp -s "$creds_file" "$source_file"; then
272+
echo "Credentials file identical to source, skipping backup" >&2
273+
return 0
274+
fi
275+
fi
276+
277+
backup_path="${creds_file}.backup-$(date +%Y%m%d-%H%M%S)"
278+
echo "Backing up existing credentials: $creds_file -> $backup_path" >&2
279+
cp "$creds_file" "$backup_path"
280+
echo "To restore: mv $backup_path $creds_file" >&2
281+
fi
282+
}
283+
215284
parse_auth_args() {
216285
local agent_name="$1"
217286
shift
@@ -238,24 +307,30 @@ parse_auth_args() {
238307
case "${args[$i]}" in
239308
--auth-with)
240309
if [ $((i + 1)) -ge ${#args[@]} ]; then
241-
auth_error "--auth-with requires a method"
310+
auth_error "--auth-with requires a method or file path"
242311
fi
243312
auth_method="${args[$((i + 1))]}"
244313

245-
local method_supported=false
246-
local method
247-
for method in "${supported_methods[@]}"; do
248-
if [ "$method" = "$auth_method" ]; then
249-
method_supported=true
250-
break
314+
if is_file_path "$auth_method"; then
315+
# shellcheck disable=SC2034
316+
CUSTOM_CREDENTIALS_FILE=$(expand_and_validate_file "$auth_method")
317+
auth_method="credentials-file"
318+
else
319+
local method_supported=false
320+
local method
321+
for method in "${supported_methods[@]}"; do
322+
if [ "$method" = "$auth_method" ]; then
323+
method_supported=true
324+
break
325+
fi
326+
done
327+
328+
if [ "$method_supported" = false ]; then
329+
local supported_list
330+
supported_list=$(IFS=', '; echo "${supported_methods[*]}")
331+
auth_error "$agent_name agent doesn't support auth method '$auth_method'" \
332+
"Supported: $supported_list, or provide a credentials file path"
251333
fi
252-
done
253-
254-
if [ "$method_supported" = false ]; then
255-
local supported_list
256-
supported_list=$(IFS=', '; echo "${supported_methods[*]}")
257-
auth_error "$agent_name agent doesn't support auth method '$auth_method'" \
258-
"Supported: $supported_list"
259334
fi
260335

261336
i=$((i + 2))
@@ -278,4 +353,4 @@ parse_auth_args() {
278353
PARSED_AUTH_METHOD="$auth_method"
279354
# shellcheck disable=SC2034
280355
PARSED_REMAINING_ARGS=("${remaining_args[@]+"${remaining_args[@]}"}")
281-
}
356+
}

0 commit comments

Comments
 (0)