Ask for the model provider on every start
run.sh shows a window (zenity form, terminal prompts without zenity) for a base URL, token, auth header and optional model list. Left empty, Claude Desktop starts normally with the claude.ai sign-in. Otherwise it runs in its third-party gateway mode: run.sh writes the managed config (inferenceProvider "gateway" and friends) and copies it into the created container as root, because Claude Desktop ignores /etc/claude-desktop/managed-settings.json unless root owns it. The host copy is deleted right away; nothing is kept between starts. Claude Desktop only allows plain http to loopback, so for an http:// gateway launch-apps.sh runs socat on 127.0.0.1:18080 and forwards to it. localhost is rewritten to host.docker.internal for servers on the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,115 @@ for c in "$CONTAINER" "$REA_CONTAINER"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Model provider ----------------------------------------------------------
|
||||
# Asked on every start. Left empty, Claude Desktop starts normally (claude.ai
|
||||
# sign-in, Anthropic's models). Given a base URL, Claude Desktop runs in its
|
||||
# third-party "gateway" mode: Chat, Code and Cowork send their requests to
|
||||
# that URL, which must speak the Anthropic Messages API (/v1/messages), e.g.
|
||||
# a self-hosted model behind LiteLLM. The settings go into Claude Desktop's
|
||||
# managed configuration file (/etc/claude-desktop/managed-settings.json on
|
||||
# Linux). Claude Desktop ignores that file unless root owns it and only root
|
||||
# can write it, so it is copied into the created container (as root, 0644)
|
||||
# just before it starts, rather than mounted; the copy on this side, in a
|
||||
# private runtime folder, is deleted right after. Nothing is remembered
|
||||
# between starts, the token included.
|
||||
managed_settings_dir="$xauth_dir/managed-settings"
|
||||
rm -rf "$managed_settings_dir"
|
||||
gateway_url=""
|
||||
gateway_token=""
|
||||
gateway_scheme="bearer"
|
||||
gateway_models=""
|
||||
|
||||
# JSON string literal for $1 (refuses control characters).
|
||||
json_str() {
|
||||
[[ "$1" != *[[:cntrl:]]* ]] || die "control characters are not allowed in: $1"
|
||||
local s="${1//\\/\\\\}"
|
||||
printf '"%s"' "${s//\"/\\\"}"
|
||||
}
|
||||
|
||||
provider_text="Leave the base URL empty to use Anthropic's models (sign in to claude.ai as usual).
|
||||
|
||||
To use another model, enter the base URL of a server that speaks the Anthropic Messages API, without /v1 (e.g. http://host.docker.internal:4000 for a LiteLLM proxy on this machine). With http:// the token is sent unencrypted. Models: comma-separated IDs, the first is the default; leave empty to ask the server's /v1/models."
|
||||
if command -v zenity >/dev/null; then
|
||||
info "Choose the model provider in the dialog"
|
||||
form="$(zenity --forms --title="Claude RE workstation: model provider" \
|
||||
--text="$provider_text" --separator=$'\x1f' \
|
||||
--add-entry="Base URL (ANTHROPIC_BASE_URL)" \
|
||||
--add-password="Token" \
|
||||
--add-combo="Send token as" --combo-values="Authorization: Bearer|x-api-key" \
|
||||
--add-entry="Models (optional)" 2>/dev/null)" \
|
||||
|| die "model provider dialog cancelled; not starting Claude"
|
||||
IFS=$'\x1f' read -r gateway_url gateway_token scheme_choice gateway_models <<< "$form"
|
||||
[[ "${scheme_choice:-}" == "x-api-key" ]] && gateway_scheme="x-api-key"
|
||||
elif [[ -t 0 ]]; then
|
||||
echo "$provider_text"
|
||||
read -r -p "Base URL (empty = Anthropic): " gateway_url
|
||||
if [[ -n "$gateway_url" ]]; then
|
||||
read -r -s -p "Token: " gateway_token; echo
|
||||
read -r -p "Models (optional): " gateway_models
|
||||
fi
|
||||
fi
|
||||
|
||||
# Trim spaces and a trailing slash or /v1 (Claude Desktop appends /v1/messages).
|
||||
gateway_url="$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's#/*$##' -e 's#/v1$##' <<< "$gateway_url")"
|
||||
gateway_token="$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' <<< "$gateway_token")"
|
||||
if [[ -n "$gateway_url" ]]; then
|
||||
[[ "$gateway_url" =~ ^https?://[^/[:space:]]+ ]] \
|
||||
|| die "the base URL must start with http:// or https://: $gateway_url"
|
||||
# localhost in the container is the container itself; a server on this
|
||||
# machine is reached through host.docker.internal (added to the container
|
||||
# below). It has to listen on an address Docker's bridge can reach, not
|
||||
# only 127.0.0.1.
|
||||
if [[ "$gateway_url" =~ ^(https?://)(localhost|127\.0\.0\.1|\[::1\])(:|/|$)(.*)$ ]]; then
|
||||
gateway_url="${BASH_REMATCH[1]}host.docker.internal${BASH_REMATCH[3]}${BASH_REMATCH[4]}"
|
||||
info "Using $gateway_url (localhost inside the container is the container itself)"
|
||||
fi
|
||||
gateway_shown="$gateway_url"
|
||||
# Claude Desktop accepts plain http only for loopback addresses. For an
|
||||
# http:// server elsewhere, socat in the container listens on
|
||||
# 127.0.0.1:18080 and forwards to it (started by launch-apps.sh), and
|
||||
# Claude Desktop is pointed there. The token still crosses the network
|
||||
# unencrypted, as it would with any plain-HTTP server.
|
||||
if [[ "$gateway_url" =~ ^http://([^/:]+)(:([0-9]+))?(/.*)?$ ]]; then
|
||||
fwd_target="${BASH_REMATCH[1]}:${BASH_REMATCH[3]:-80}"
|
||||
run_args+=(-e "CLAUDE_RE_GATEWAY_FORWARD=$fwd_target")
|
||||
gateway_url="http://127.0.0.1:18080${BASH_REMATCH[4]}"
|
||||
info "Plain HTTP: forwarding 127.0.0.1:18080 in the container to $fwd_target (token not encrypted)"
|
||||
elif [[ "$gateway_url" == http://* ]]; then
|
||||
die "unsupported http:// address (use a hostname or IPv4 address, or https://): $gateway_url"
|
||||
fi
|
||||
# The key is required in gateway mode; local servers usually ignore it.
|
||||
[[ -n "$gateway_token" ]] || gateway_token="none"
|
||||
|
||||
models_json=""
|
||||
IFS=',' read -r -a model_list <<< "$gateway_models"
|
||||
for m in "${model_list[@]}"; do
|
||||
m="$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' <<< "$m")"
|
||||
[[ -n "$m" ]] && models_json+="${models_json:+, }$(json_str "$m")"
|
||||
done
|
||||
|
||||
# Removed again if the start fails before it is copied in.
|
||||
trap 'rm -rf "$managed_settings_dir"' EXIT
|
||||
mkdir -m 700 "$managed_settings_dir"
|
||||
( umask 077
|
||||
{
|
||||
echo "{"
|
||||
echo " \"inferenceProvider\": \"gateway\","
|
||||
echo " \"inferenceGatewayBaseUrl\": $(json_str "$gateway_url"),"
|
||||
echo " \"inferenceGatewayApiKey\": $(json_str "$gateway_token"),"
|
||||
echo " \"inferenceGatewayAuthScheme\": \"$gateway_scheme\","
|
||||
[[ -n "$models_json" ]] && echo " \"inferenceModels\": [$models_json],"
|
||||
echo " \"disableDeploymentModeChooser\": true"
|
||||
echo "}"
|
||||
} > "$managed_settings_dir/managed-settings.json" )
|
||||
info "Model provider: gateway at $gateway_shown"
|
||||
else
|
||||
info "Model provider: Anthropic (claude.ai sign-in)"
|
||||
fi
|
||||
unset gateway_token
|
||||
# For self-hosted servers on this machine (see above).
|
||||
run_args+=(--add-host host.docker.internal:host-gateway)
|
||||
|
||||
# --- Workspace ---------------------------------------------------------------
|
||||
# The folder Claude can read and write. Chosen in the desktop's own folder
|
||||
# picker (zenity on GNOME, kdialog on KDE), else asked in the terminal.
|
||||
@@ -247,13 +356,24 @@ stop_rea() { "$RUNTIME" stop "$REA_CONTAINER" >/dev/null 2>&1 || true; }
|
||||
# Created first, so it is on the internal network (as well as the default one,
|
||||
# for sign-in) before it starts.
|
||||
info "Starting Claude Desktop and Ghidra in container '$CONTAINER' (workspace: $WORKSPACE -> /workspace)"
|
||||
# Copies the provider settings (if any) into the created container, owned
|
||||
# by root and readable by Claude Desktop, then deletes them on this side.
|
||||
copy_managed_settings() {
|
||||
[[ -d "$managed_settings_dir" ]] || return 0
|
||||
tar -C "$managed_settings_dir" --owner=0 --group=0 --numeric-owner --mode=0644 \
|
||||
-cf - managed-settings.json \
|
||||
| "$RUNTIME" cp - "$CONTAINER:/etc/claude-desktop/" \
|
||||
&& rm -rf "$managed_settings_dir"
|
||||
}
|
||||
if ! { "$RUNTIME" create "${run_args[@]}" --name "$CONTAINER" "${ws_mount[@]}" "$IMAGE" >/dev/null \
|
||||
&& "$RUNTIME" network connect "$NETWORK" "$CONTAINER" \
|
||||
&& copy_managed_settings \
|
||||
&& "$RUNTIME" start "$CONTAINER" >/dev/null; }; then
|
||||
"$RUNTIME" rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
stop_rea
|
||||
die "could not start Claude Desktop"
|
||||
fi
|
||||
trap - EXIT
|
||||
|
||||
# Stop REA once Claude Desktop's container is gone, in the background and
|
||||
# detached from this terminal.
|
||||
|
||||
Reference in New Issue
Block a user