Anurag commited on
Commit
1f050ab
·
1 Parent(s): 7106068

Preserve venv pip behavior with Jupyter defaults

Browse files
Files changed (4) hide show
  1. Dockerfile +12 -11
  2. health-server.js +17 -1
  3. jupyter-devdata-sync.py +3 -2
  4. start.sh +28 -1
Dockerfile CHANGED
@@ -34,6 +34,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
34
  procps \
35
  python3 \
36
  python3-pip \
 
37
  p7zip-full \
38
  chromium \
39
  libnss3 \
@@ -60,16 +61,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
60
  pip3 install --no-cache-dir --break-system-packages huggingface_hub hf_transfer && \
61
  rm -rf /var/lib/apt/lists/*
62
 
63
- # Install JupyterLab only when DEV_MODE is enabled (build-time)
64
- # This avoids installing large packages when terminal is not needed
65
- RUN if [ "${DEV_MODE}" = "true" ] || [ "${DEV_MODE}" = "1" ] || [ "${DEV_MODE}" = "yes" ] || [ "${DEV_MODE}" = "on" ]; then \
66
- pip3 install --no-cache-dir --break-system-packages \
67
- jupyterlab==4.5.7 \
68
- tornado==6.5.5 \
69
- ipywidgets==8.1.8 && \
70
- # Copy login template into jupyter_server templates dir
71
- python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')" || true; \
72
- fi
73
 
74
  # Reuse existing node user (UID 1000). Allow passwordless package-manager
75
  # commands only so runtime apt installs can be replayed after HF Space restarts.
@@ -110,7 +109,7 @@ COPY --chown=1000:1000 env-builder.html /home/node/app/env-builder.html
110
  COPY --chown=1000:1000 env-builder.js /home/node/app/env-builder.js
111
  COPY --chown=1000:1000 key-rotator-manager.html /home/node/app/key-rotator-manager.html
112
  COPY --chown=1000:1000 jupyter-devdata-sync.py /home/node/app/jupyter-devdata-sync.py
113
- # login.html template is now copied inside the DEV_MODE install block above
114
  RUN chmod +x /home/node/app/start.sh \
115
  /home/node/app/cloudflare-proxy-setup.py \
116
  /home/node/app/cloudflare-keepalive-setup.py \
@@ -122,6 +121,8 @@ USER node
122
 
123
  ENV HOME=/home/node \
124
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
 
 
125
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
126
  NODE_PATH=/home/node/browser-deps/node_modules \
127
  NODE_OPTIONS="--require /opt/cloudflare-proxy.js"
 
34
  procps \
35
  python3 \
36
  python3-pip \
37
+ python3-venv \
38
  p7zip-full \
39
  chromium \
40
  libnss3 \
 
61
  pip3 install --no-cache-dir --break-system-packages huggingface_hub hf_transfer && \
62
  rm -rf /var/lib/apt/lists/*
63
 
64
+ # Install JupyterLab at build time because start.sh can auto-enable the
65
+ # terminal at runtime when GATEWAY_TOKEN is present. If the dependency is only
66
+ # installed when the build arg DEV_MODE=true, the documented default path
67
+ # starts a terminal process that cannot import jupyterlab.
68
+ RUN pip3 install --no-cache-dir --break-system-packages \
69
+ jupyterlab==4.5.7 \
70
+ tornado==6.5.5 \
71
+ ipywidgets==8.1.8
 
 
72
 
73
  # Reuse existing node user (UID 1000). Allow passwordless package-manager
74
  # commands only so runtime apt installs can be replayed after HF Space restarts.
 
109
  COPY --chown=1000:1000 env-builder.js /home/node/app/env-builder.js
110
  COPY --chown=1000:1000 key-rotator-manager.html /home/node/app/key-rotator-manager.html
111
  COPY --chown=1000:1000 jupyter-devdata-sync.py /home/node/app/jupyter-devdata-sync.py
112
+ RUN python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')"
113
  RUN chmod +x /home/node/app/start.sh \
114
  /home/node/app/cloudflare-proxy-setup.py \
115
  /home/node/app/cloudflare-keepalive-setup.py \
 
121
 
122
  ENV HOME=/home/node \
123
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
124
+ PIP_BREAK_SYSTEM_PACKAGES=1 \
125
+ PYTHONUSERBASE=/home/node/.local \
126
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
127
  NODE_PATH=/home/node/browser-deps/node_modules \
128
  NODE_OPTIONS="--require /opt/cloudflare-proxy.js"
health-server.js CHANGED
@@ -263,7 +263,22 @@ function escapeHtml(v) {
263
 
264
  function parseCookies(req) {
265
  const h = req.headers.cookie || "";
266
- return Object.fromEntries(h.split(";").map(c => c.trim().split("=")).filter(p => p.length >= 2).map(([k, ...v]) => [k.trim(), decodeURIComponent(v.join("=").trim())]));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  }
268
 
269
  // Constant-time comparison using crypto — prevent timing attacks
@@ -1119,6 +1134,7 @@ server.on("upgrade", (req, socket, head) => {
1119
  const header = req.rawHeaders[i];
1120
  const lower = header.toLowerCase();
1121
  if (["host", "x-forwarded-for", "x-forwarded-host", "x-forwarded-proto", "x-forwarded-prefix"].includes(lower)) continue;
 
1122
  ps.write(`${header}: ${req.rawHeaders[i + 1]}\r\n`);
1123
  }
1124
  ps.write("\r\n");
 
263
 
264
  function parseCookies(req) {
265
  const h = req.headers.cookie || "";
266
+ const cookies = {};
267
+ for (const rawCookie of h.split(";")) {
268
+ const parts = rawCookie.trim().split("=");
269
+ if (parts.length < 2) continue;
270
+ const key = parts.shift().trim();
271
+ if (!key) continue;
272
+ const rawValue = parts.join("=").trim();
273
+ try {
274
+ cookies[key] = decodeURIComponent(rawValue);
275
+ } catch {
276
+ // Browsers and crawlers can send malformed percent-encoded cookie values.
277
+ // Treat that single cookie as unusable instead of letting URIError crash
278
+ // the dashboard/auth reverse proxy.
279
+ }
280
+ }
281
+ return cookies;
282
  }
283
 
284
  // Constant-time comparison using crypto — prevent timing attacks
 
1134
  const header = req.rawHeaders[i];
1135
  const lower = header.toLowerCase();
1136
  if (["host", "x-forwarded-for", "x-forwarded-host", "x-forwarded-proto", "x-forwarded-prefix"].includes(lower)) continue;
1137
+ if (lower === "authorization" && isApp && bridgeGatewayAuth && GATEWAY_TOKEN) continue;
1138
  ps.write(`${header}: ${req.rawHeaders[i + 1]}\r\n`);
1139
  }
1140
  ps.write("\r\n");
jupyter-devdata-sync.py CHANGED
@@ -9,6 +9,7 @@ HF_USERNAME = os.environ.get("HF_USERNAME", "").strip() or os.environ.get("SPACE
9
  DATASET_NAME = os.environ.get("DEVDATA_DATASET_NAME", "").strip() or "huggingclaw-devdata"
10
  BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "").strip() or os.environ.get("BACKUP_DATASET", "").strip() or "huggingclaw-backup"
11
  JUPYTER_ROOT = Path(os.environ.get("JUPYTER_ROOT_DIR", "/home/node")).resolve()
 
12
  INTERVAL = int((os.environ.get("DEVDATA_SYNC_INTERVAL", "").strip() or "180"))
13
  # BUG FIX #5: Respect max file size so giant files don't stall uploads.
14
  # Matches the 50 MB ceiling in openclaw-sync.py; override with DEVDATA_MAX_FILE_BYTES.
@@ -438,7 +439,7 @@ if __name__ == "__main__":
438
 
439
  # Normal background sync mode — no restore; go straight to upload loop.
440
  validate_jupyter_paths()
441
- if is_jupyter_running():
442
  print("DevData: background sync started (JupyterLab is live, restore already done by --restore).")
443
  else:
444
  # Fallback: JupyterLab not detected. Should not normally happen
@@ -446,6 +447,6 @@ if __name__ == "__main__":
446
  # waits for the gateway before launching this background process.
447
  # Log a warning and proceed to sync; do NOT restore to avoid racing
448
  # with a JupyterLab that may be in the middle of starting up.
449
- print("DevData: WARNING — JupyterLab not detected on port 8888. Skipping restore to be safe; starting sync loop.")
450
 
451
  sync_loop(api, rid)
 
9
  DATASET_NAME = os.environ.get("DEVDATA_DATASET_NAME", "").strip() or "huggingclaw-devdata"
10
  BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "").strip() or os.environ.get("BACKUP_DATASET", "").strip() or "huggingclaw-backup"
11
  JUPYTER_ROOT = Path(os.environ.get("JUPYTER_ROOT_DIR", "/home/node")).resolve()
12
+ JUPYTER_PORT = int((os.environ.get("JUPYTER_PORT", "").strip() or "8888"))
13
  INTERVAL = int((os.environ.get("DEVDATA_SYNC_INTERVAL", "").strip() or "180"))
14
  # BUG FIX #5: Respect max file size so giant files don't stall uploads.
15
  # Matches the 50 MB ceiling in openclaw-sync.py; override with DEVDATA_MAX_FILE_BYTES.
 
439
 
440
  # Normal background sync mode — no restore; go straight to upload loop.
441
  validate_jupyter_paths()
442
+ if is_jupyter_running(JUPYTER_PORT):
443
  print("DevData: background sync started (JupyterLab is live, restore already done by --restore).")
444
  else:
445
  # Fallback: JupyterLab not detected. Should not normally happen
 
447
  # waits for the gateway before launching this background process.
448
  # Log a warning and proceed to sync; do NOT restore to avoid racing
449
  # with a JupyterLab that may be in the middle of starting up.
450
+ print(f"DevData: WARNING — JupyterLab not detected on port {JUPYTER_PORT}. Skipping restore to be safe; starting sync loop.")
451
 
452
  sync_loop(api, rid)
start.sh CHANGED
@@ -504,6 +504,13 @@ chmod 700 /home/node/.openclaw/credentials
504
  export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
505
  export npm_config_prefix="$NPM_CONFIG_PREFIX"
506
  export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
 
 
 
 
 
 
 
507
  export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
508
  # Show current working directory in terminal prompt (JupyterLab terminals can
509
  # otherwise display only "$" when PS1 is unset/minimal).
@@ -1665,8 +1672,13 @@ start_jupyter_once() {
1665
  echo "Terminal : starting (root: $JUPYTER_ROOT_DIR)"
1666
  JUPYTER_LOG_FILE="/tmp/jupyterlab.log"
1667
 
1668
- # Use explicit Python to avoid PATH issues; set memory-friendly limits
 
 
1669
  export PYTHONPATH=""
 
 
 
1670
  hc_env_without_gateway_preloads python3 -m jupyterlab \
1671
  --ip 127.0.0.1 \
1672
  --port "$JUPYTER_PORT" \
@@ -1740,6 +1752,13 @@ export PATH="/home/node/.local/bin:$PATH"
1740
  export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
1741
  export npm_config_prefix="$NPM_CONFIG_PREFIX"
1742
  export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
 
 
 
 
 
 
 
1743
  export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
1744
  export HISTFILE="${HISTFILE:-/home/node/.bash_history}"
1745
  export HISTSIZE="${HISTSIZE:-50000}"
@@ -1958,6 +1977,8 @@ sudo() {
1958
  pip() {
1959
  if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
1960
  command pip install --user --break-system-packages "${@:2}"
 
 
1961
  else
1962
  command pip "$@"
1963
  fi
@@ -1973,6 +1994,8 @@ pip() {
1973
  pip3() {
1974
  if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
1975
  command pip3 install --user --break-system-packages "${@:2}"
 
 
1976
  else
1977
  command pip3 "$@"
1978
  fi
@@ -1987,6 +2010,8 @@ pip3() {
1987
  python() {
1988
  if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
1989
  command python -m pip install --user --break-system-packages "${@:4}"
 
 
1990
  else
1991
  command python "$@"
1992
  fi
@@ -2001,6 +2026,8 @@ python() {
2001
  python3() {
2002
  if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
2003
  command python3 -m pip install --user --break-system-packages "${@:4}"
 
 
2004
  else
2005
  command python3 "$@"
2006
  fi
 
504
  export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
505
  export npm_config_prefix="$NPM_CONFIG_PREFIX"
506
  export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
507
+ # Debian/Ubuntu images mark system Python as externally managed (PEP 668).
508
+ # JupyterLab's PyPI extension manager runs pip from the server process, so it
509
+ # does not see the interactive shell wrappers below. Allow that non-interactive
510
+ # pip to install into node's user site instead of failing with
511
+ # "externally-managed-environment".
512
+ export PIP_BREAK_SYSTEM_PACKAGES="${PIP_BREAK_SYSTEM_PACKAGES:-1}"
513
+ export PIP_USER="${PIP_USER:-1}"
514
  export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
515
  # Show current working directory in terminal prompt (JupyterLab terminals can
516
  # otherwise display only "$" when PS1 is unset/minimal).
 
1672
  echo "Terminal : starting (root: $JUPYTER_ROOT_DIR)"
1673
  JUPYTER_LOG_FILE="/tmp/jupyterlab.log"
1674
 
1675
+ # Use explicit Python to avoid PATH issues; set memory-friendly limits.
1676
+ # Keep pip user-site defaults in this process so JupyterLab's PyPI Manager
1677
+ # can install extensions/packages under /home/node/.local on PEP 668 images.
1678
  export PYTHONPATH=""
1679
+ export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
1680
+ export PIP_BREAK_SYSTEM_PACKAGES="${PIP_BREAK_SYSTEM_PACKAGES:-1}"
1681
+ export PIP_USER="${PIP_USER:-1}"
1682
  hc_env_without_gateway_preloads python3 -m jupyterlab \
1683
  --ip 127.0.0.1 \
1684
  --port "$JUPYTER_PORT" \
 
1752
  export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
1753
  export npm_config_prefix="$NPM_CONFIG_PREFIX"
1754
  export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
1755
+ # Debian/Ubuntu images mark system Python as externally managed (PEP 668).
1756
+ # JupyterLab's PyPI extension manager runs pip from the server process, so it
1757
+ # does not see the interactive shell wrappers below. Allow that non-interactive
1758
+ # pip to install into node's user site instead of failing with
1759
+ # "externally-managed-environment".
1760
+ export PIP_BREAK_SYSTEM_PACKAGES="${PIP_BREAK_SYSTEM_PACKAGES:-1}"
1761
+ export PIP_USER="${PIP_USER:-1}"
1762
  export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
1763
  export HISTFILE="${HISTFILE:-/home/node/.bash_history}"
1764
  export HISTSIZE="${HISTSIZE:-50000}"
 
1977
  pip() {
1978
  if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
1979
  command pip install --user --break-system-packages "${@:2}"
1980
+ elif [ -n "${VIRTUAL_ENV:-}" ]; then
1981
+ env -u PIP_USER pip "$@"
1982
  else
1983
  command pip "$@"
1984
  fi
 
1994
  pip3() {
1995
  if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
1996
  command pip3 install --user --break-system-packages "${@:2}"
1997
+ elif [ -n "${VIRTUAL_ENV:-}" ]; then
1998
+ env -u PIP_USER pip3 "$@"
1999
  else
2000
  command pip3 "$@"
2001
  fi
 
2010
  python() {
2011
  if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
2012
  command python -m pip install --user --break-system-packages "${@:4}"
2013
+ elif [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ -n "${VIRTUAL_ENV:-}" ]; then
2014
+ env -u PIP_USER python "$@"
2015
  else
2016
  command python "$@"
2017
  fi
 
2026
  python3() {
2027
  if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
2028
  command python3 -m pip install --user --break-system-packages "${@:4}"
2029
+ elif [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ -n "${VIRTUAL_ENV:-}" ]; then
2030
+ env -u PIP_USER python3 "$@"
2031
  else
2032
  command python3 "$@"
2033
  fi