Files
appstore/apps/codex-claude-workstation/latest/scripts/init.sh
T
okxlin 66ec3e696c Update codex-claude-workstation with Paseo mobile access
Add mobile Codex sessions through the published Paseo-enabled image, default the host listener to loopback, preserve existing permissions and persistent state, and migrate missing or empty bind settings without overwriting custom ports or addresses.
2026-08-11 13:55:09 +08:00

179 lines
6.1 KiB
Bash
Executable File

#!/bin/bash
# init.sh — codex-claude-workstation 1Panel 初始化脚本
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
ENV_FILE="${ROOT_DIR}/.env"
if [[ -L "$ENV_FILE" ]]; then
echo "Refusing to read a symlinked .env file" >&2
exit 1
elif [[ -f "$ENV_FILE" ]]; then
chmod 0600 -- "$ENV_FILE"
fi
strip_matching_quotes() {
local value="$1"
if [[ ${#value} -ge 2 ]]; then
if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then
value="${value:1:${#value}-2}"
elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then
value="${value:1:${#value}-2}"
fi
fi
printf '%s\n' "$value"
}
get_config_value() {
local key="$1"
local default="$2"
local value=""
if [[ -n "${!key+x}" ]]; then
value="${!key}"
elif [[ -f "$ENV_FILE" && ! -L "$ENV_FILE" ]]; then
value="$(sed -n -E "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
value="$(strip_matching_quotes "$value")"
fi
if [[ -n "$value" ]]; then
printf '%s\n' "$value"
else
printf '%s\n' "$default"
fi
}
paseo_password_is_websocket_token() {
local value="${1-}"
local token_pattern="^[A-Za-z0-9!#\$%&'*+.^_\`|~-]+$"
[[ ${#value} -ge 1 && ${#value} -le 128 && "$value" =~ $token_pattern ]]
}
paseo_bind_ip_is_valid() {
local value="$1"
local octet decimal
local -a octets=()
[[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 1
IFS='.' read -r -a octets <<< "$value"
[[ ${#octets[@]} -eq 4 ]] || return 1
for octet in "${octets[@]}"; do
decimal=$((10#${octet}))
(( decimal <= 255 )) || return 1
done
}
resolve_app_path() {
local key="$1"
local raw="$2"
local clean candidate resolved current part
local -a parts=()
case "$raw" in
""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;;
esac
if [[ "$raw" =~ [[:cntrl:]] ]]; then
echo "unsafe ${key} path" >&2
return 1
fi
clean="${raw#./}"
[[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; }
command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; }
candidate="$ROOT_DIR/$clean"
resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; }
case "$resolved" in
"$ROOT_DIR"/*) ;;
*) echo "unsafe ${key} path" >&2; return 1 ;;
esac
current="$ROOT_DIR"
IFS='/' read -r -a parts <<< "$clean"
for part in "${parts[@]}"; do
[[ -z "$part" || "$part" == "." ]] && continue
current="$current/$part"
if [[ -L "$current" ]]; then
echo "unsafe ${key} path" >&2
return 1
fi
done
printf '%s\n' "$resolved"
}
APP_DATA_DIR_RAW="$(get_config_value APP_DATA_DIR ./data)"
CUSTOM_ENV_FILE_RAW="$(get_config_value CUSTOM_ENV_FILE ./data/custom.env)"
PANEL_APP_PORT_HTTP_VALUE="$(get_config_value PANEL_APP_PORT_HTTP 8080)"
PANEL_APP_PORT_PASEO_VALUE="$(get_config_value PANEL_APP_PORT_PASEO 6767)"
PASEO_BIND_IP_VALUE="$(get_config_value PASEO_BIND_IP 127.0.0.1)"
PASEO_PASSWORD_VALUE="$(get_config_value PASEO_PASSWORD "")"
CODE_SERVER_PASSWORD_VALUE="$(get_config_value CODE_SERVER_PASSWORD change-me)"
if ! paseo_bind_ip_is_valid "$PASEO_BIND_IP_VALUE"; then
echo "PASEO_BIND_IP must be a valid IPv4 address" >&2
exit 1
fi
if [[ -n "$PASEO_PASSWORD_VALUE" ]]; then
if ! paseo_password_is_websocket_token "$PASEO_PASSWORD_VALUE"; then
echo "PASEO_PASSWORD must be 1-128 HTTP-token-safe characters; use: openssl rand -hex 24" >&2
exit 1
elif [[ ${#PASEO_PASSWORD_VALUE} -lt 20 ]]; then
echo "PASEO_PASSWORD must contain at least 20 characters for public access" >&2
exit 1
fi
elif ! paseo_password_is_websocket_token "$CODE_SERVER_PASSWORD_VALUE"; then
echo "WARN: blank PASEO_PASSWORD falls back to a code-server password that is not browser WebSocket-token-safe." >&2
echo "WARN: code-server will remain usable, but set a separate PASEO_PASSWORD before enabling mobile access." >&2
elif [[ ${#CODE_SERVER_PASSWORD_VALUE} -lt 20 ]]; then
echo "WARN: blank PASEO_PASSWORD uses a code-server password shorter than the recommended 20 characters." >&2
fi
APP_DATA_DIR_ABS="$(resolve_app_path APP_DATA_DIR "$APP_DATA_DIR_RAW")"
WORKSPACE_DIR="$(resolve_app_path APP_DATA_DIR "${APP_DATA_DIR_RAW%/}/workspace")"
CUSTOM_ENV_FILE_ABS="$(resolve_app_path CUSTOM_ENV_FILE "$CUSTOM_ENV_FILE_RAW")"
mkdir -p -- "$WORKSPACE_DIR" "$(dirname -- "$CUSTOM_ENV_FILE_ABS")"
# Recheck after creation so a pre-existing or concurrently introduced symlink
# cannot silently redirect the file preparation outside the app directory.
[[ "$(resolve_app_path APP_DATA_DIR "$APP_DATA_DIR_RAW")" == "$APP_DATA_DIR_ABS" ]]
[[ "$(resolve_app_path APP_DATA_DIR "${APP_DATA_DIR_RAW%/}/workspace")" == "$WORKSPACE_DIR" ]]
[[ "$(resolve_app_path CUSTOM_ENV_FILE "$CUSTOM_ENV_FILE_RAW")" == "$CUSTOM_ENV_FILE_ABS" ]]
if [[ -L "$CUSTOM_ENV_FILE_ABS" || -d "$CUSTOM_ENV_FILE_ABS" ]]; then
echo "CUSTOM_ENV_FILE must be a regular non-symlink file" >&2
exit 1
fi
if [[ ! -e "$CUSTOM_ENV_FILE_ABS" ]]; then
install -m 0600 /dev/null "$CUSTOM_ENV_FILE_ABS"
fi
chmod 0600 -- "$CUSTOM_ENV_FILE_ABS"
echo "Codex Claude Workstation installed successfully."
echo ""
echo "Persistent directories:"
echo " ${WORKSPACE_DIR} -> /workspace"
echo " ${CUSTOM_ENV_FILE_ABS} -> custom env_file"
echo ""
echo "Access:"
echo " http://<server-ip>:${PANEL_APP_PORT_HTTP_VALUE}"
echo " Paseo: reverse proxy HTTPS to ${PASEO_BIND_IP_VALUE}:${PANEL_APP_PORT_PASEO_VALUE}"
echo " Login: code-server password"
echo " Switch to root: su - root (password configured from ROOT_PASSWORD)"
echo ""
echo "AI Tools (run in code-server terminal):"
echo " codex --help OpenAI Codex CLI"
echo " claude --help Anthropic Claude Code"
echo " npm install -g <pkg> Installs to persistent ~/.local"
echo ""
echo "Docker CLI (DOCKER_SOCK_SRC=/var/run/docker.sock by default; leave empty to disable):"
echo " docker ps"
echo " sudo docker ps # fallback if host socket permissions block direct access"
echo ""
echo "Codex sandbox:"
echo " doctor.sh"
echo " Host must allow kernel.unprivileged_userns_clone=1"
echo ""
echo "Proxy (manual enable):"
echo " supervisorctl start clash-meta # or sing-box, xray"
echo " Configs: ~/proxy/"