mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-20 04:23:29 +00:00
fix(docker): auto-sync host Claude CLI credentials and show Docker-aware login instructions (#398)
When running in Docker, the Claude CLI provider setup showed `claude auth login` which doesn't work inside a container. This change: - Mounts host ~/.claude as read-only into the container - Entrypoint syncs credentials to a writable volume (respects cap_drop: ALL) - Backend detects Docker via /.dockerenv and returns `in_docker` in auth-status API - UI shows `docker compose exec goclaw claude auth login` for Docker deployments Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
1 parent
19eef35653
commit
d63a7d4ced
5 files changed
+32
-4
No files matched your search
+4
-2
@@ -103,15 +103,17 @@ RUN chmod +x /app/docker-entrypoint.sh && \
|
||||
# Create data directories.
|
||||
# .runtime has split ownership: root owns the dir (so pkg-helper can write apk-packages),
|
||||
# while pip/npm subdirs are goclaw-owned (runtime installs by the app process).
|
||||
# Symlink .claude → data volume so Claude CLI credentials persist across container recreates.
|
||||
RUN mkdir -p /app/workspace /app/data/.runtime/pip /app/data/.runtime/npm-global/lib \
|
||||
/app/data/.runtime/pip-cache /app/skills /app/tsnet-state /app/.goclaw \
|
||||
/app/data/.runtime/pip-cache /app/data/.claude /app/skills /app/tsnet-state /app/.goclaw \
|
||||
&& ln -s /app/data/.claude /app/.claude \
|
||||
&& touch /app/data/.runtime/apk-packages \
|
||||
&& chown -R goclaw:goclaw /app/workspace /app/skills /app/tsnet-state /app/.goclaw \
|
||||
&& chown goclaw:goclaw /app/bundled-skills /app/data \
|
||||
&& chown root:goclaw /app/data/.runtime /app/data/.runtime/apk-packages \
|
||||
&& chmod 0750 /app/data/.runtime \
|
||||
&& chmod 0640 /app/data/.runtime/apk-packages \
|
||||
&& chown -R goclaw:goclaw /app/data/.runtime/pip /app/data/.runtime/npm-global /app/data/.runtime/pip-cache
|
||||
&& chown -R goclaw:goclaw /app/data/.runtime/pip /app/data/.runtime/npm-global /app/data/.runtime/pip-cache /app/data/.claude
|
||||
|
||||
# Default environment
|
||||
ENV GOCLAW_CONFIG=/app/config.json \
|
||||
|
||||
@@ -40,6 +40,7 @@ services:
|
||||
volumes:
|
||||
- goclaw-data:/app/data
|
||||
- goclaw-workspace:/app/workspace
|
||||
- ${HOME}/.claude:/app/.claude-host:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
init: true
|
||||
|
||||
@@ -72,6 +72,20 @@ if [ -x /app/pkg-helper ] && [ "$(id -u)" = "0" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy Claude CLI credentials from root-owned read-only mount to goclaw-accessible location.
|
||||
# /app/.claude is a symlink → /app/data/.claude (writable volume, see Dockerfile).
|
||||
# Root lacks CAP_DAC_OVERRIDE (cap_drop: ALL), so use /tmp as staging area:
|
||||
# root reads the source (root-owned 600) → /tmp, then goclaw copies to data volume.
|
||||
if [ -f /app/.claude-host/.credentials.json ]; then
|
||||
(cp /app/.claude-host/.credentials.json /tmp/.claude-credentials \
|
||||
&& chmod 644 /tmp/.claude-credentials \
|
||||
&& su-exec goclaw mkdir -p /app/data/.claude \
|
||||
&& su-exec goclaw cp /tmp/.claude-credentials /app/data/.claude/.credentials.json \
|
||||
&& su-exec goclaw chmod 600 /app/data/.claude/.credentials.json \
|
||||
&& rm -f /tmp/.claude-credentials \
|
||||
&& echo "Claude CLI credentials synced from host.") || echo "WARNING: Claude credentials copy failed (non-fatal)"
|
||||
fi
|
||||
|
||||
# Run command with privilege drop (su-exec in Docker, direct otherwise).
|
||||
run_as_goclaw() {
|
||||
if command -v su-exec >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -138,11 +139,15 @@ func (h *ProvidersHandler) handleClaudeCLIAuthStatus(w http.ResponseWriter, r *h
|
||||
}
|
||||
}
|
||||
|
||||
_, dockerErr := os.Stat("/.dockerenv")
|
||||
inDocker := dockerErr == nil
|
||||
|
||||
status, err := providers.CheckClaudeAuthStatus(ctx, cliPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"logged_in": false,
|
||||
"error": err.Error(),
|
||||
"in_docker": inDocker,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -151,6 +156,7 @@ func (h *ProvidersHandler) handleClaudeCLIAuthStatus(w http.ResponseWriter, r *h
|
||||
"logged_in": status.LoggedIn,
|
||||
"email": status.Email,
|
||||
"subscription_type": status.SubscriptionType,
|
||||
"in_docker": inDocker,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ interface CLIAuthStatus {
|
||||
email?: string;
|
||||
subscription_type?: string;
|
||||
error?: string;
|
||||
in_docker?: boolean;
|
||||
}
|
||||
|
||||
export function CLISection({ open }: { open: boolean }) {
|
||||
@@ -70,7 +71,11 @@ export function CLISection({ open }: { open: boolean }) {
|
||||
<summary className="cursor-pointer hover:text-foreground">{t("cli.switchAccount")}</summary>
|
||||
<div className="mt-1.5 space-y-1 rounded-md border bg-muted/50 px-3 py-2">
|
||||
<p>{t("cli.switchAccountInstructions")}</p>
|
||||
<code className="block rounded bg-muted px-2 py-1 font-mono">claude auth logout && claude auth login</code>
|
||||
<code className="block rounded bg-muted px-2 py-1 font-mono">
|
||||
{cliAuth?.in_docker
|
||||
? "docker compose exec goclaw claude auth logout && docker compose exec goclaw claude auth login"
|
||||
: "claude auth logout && claude auth login"}
|
||||
</code>
|
||||
<p>{t("cli.switchAccountRecheck")} <RefreshCw className="inline h-3 w-3" /> {t("cli.switchAccountRecheckSuffix")}</p>
|
||||
</div>
|
||||
</details>
|
||||
@@ -97,7 +102,7 @@ export function CLISection({ open }: { open: boolean }) {
|
||||
{t("cli.runOnServer")}
|
||||
</p>
|
||||
<code className="mt-1 block rounded bg-amber-100 px-2 py-1 text-xs font-mono dark:bg-amber-900 dark:text-amber-300">
|
||||
claude auth login
|
||||
{cliAuth.in_docker ? "docker compose exec goclaw claude auth login" : "claude auth login"}
|
||||
</code>
|
||||
{cliAuth.error && (
|
||||
<p className="mt-1 text-xs text-amber-500">{cliAuth.error}</p>
|
||||
|
||||
Reference in new issue
Block a user