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

Fix JupyterLab writable extension paths

Browse files
Files changed (4) hide show
  1. Dockerfile +19 -8
  2. README.md +5 -1
  3. env-builder.js +18 -0
  4. start.sh +79 -15
Dockerfile CHANGED
@@ -15,6 +15,7 @@ FROM ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} AS openclaw
15
  FROM node:22-slim
16
  ARG OPENCLAW_VERSION=latest
17
  ARG DEV_MODE=false
 
18
  # DEV_MODE intentionally not baked into runtime ENV β€” defaults to unset so
19
  # start.sh can auto-enable terminal when GATEWAY_TOKEN is present. Users can
20
  # override by setting DEV_MODE=false as an HF Space Variable to opt out.
@@ -70,16 +71,25 @@ RUN pip3 install --no-cache-dir --break-system-packages \
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.
 
 
75
  RUN mkdir -p /home/node/app /home/node/.openclaw && \
76
  chown -R 1000:1000 /home/node && \
77
- printf '%s\n' \
78
- 'Cmnd_Alias HUGGINGCLAW_APT = /usr/bin/apt, /usr/bin/apt-get, /usr/bin/dpkg' \
79
- 'node ALL=(root) NOPASSWD: HUGGINGCLAW_APT' \
80
- > /etc/sudoers.d/huggingclaw-apt && \
81
- chmod 0440 /etc/sudoers.d/huggingclaw-apt && \
82
- visudo -cf /etc/sudoers.d/huggingclaw-apt
 
 
 
 
 
 
 
83
 
84
  # Copy pre-built OpenClaw (skips npm install entirely β€” much faster!)
85
  COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
@@ -121,6 +131,7 @@ USER node
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 \
 
15
  FROM node:22-slim
16
  ARG OPENCLAW_VERSION=latest
17
  ARG DEV_MODE=false
18
+ ARG HUGGINGCLAW_FULL_SUDO=false
19
  # DEV_MODE intentionally not baked into runtime ENV β€” defaults to unset so
20
  # start.sh can auto-enable terminal when GATEWAY_TOKEN is present. Users can
21
  # override by setting DEV_MODE=false as an HF Space Variable to opt out.
 
71
  tornado==6.5.5 \
72
  ipywidgets==8.1.8
73
 
74
+ # Reuse existing node user (UID 1000). By default, allow passwordless
75
+ # package-manager commands only so runtime apt installs can be replayed after
76
+ # HF Space restarts without granting unrestricted root. Private Spaces can opt
77
+ # into full passwordless sudo at build time with HUGGINGCLAW_FULL_SUDO=true.
78
  RUN mkdir -p /home/node/app /home/node/.openclaw && \
79
  chown -R 1000:1000 /home/node && \
80
+ case "$(printf '%s' "${HUGGINGCLAW_FULL_SUDO}" | tr '[:upper:]' '[:lower:]')" in \
81
+ 1|true|yes|on) \
82
+ printf '%s\n' \
83
+ 'node ALL=(root) NOPASSWD: ALL' \
84
+ > /etc/sudoers.d/huggingclaw ;; \
85
+ *) \
86
+ printf '%s\n' \
87
+ 'Cmnd_Alias HUGGINGCLAW_APT = /usr/bin/apt, /usr/bin/apt-get, /usr/bin/dpkg' \
88
+ 'node ALL=(root) NOPASSWD: HUGGINGCLAW_APT' \
89
+ > /etc/sudoers.d/huggingclaw ;; \
90
+ esac && \
91
+ chmod 0440 /etc/sudoers.d/huggingclaw && \
92
+ visudo -cf /etc/sudoers.d/huggingclaw
93
 
94
  # Copy pre-built OpenClaw (skips npm install entirely β€” much faster!)
95
  COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
 
131
 
132
  ENV HOME=/home/node \
133
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
134
+ HUGGINGCLAW_FULL_SUDO_BUILT=${HUGGINGCLAW_FULL_SUDO} \
135
  PIP_BREAK_SYSTEM_PACKAGES=1 \
136
  PYTHONUSERBASE=/home/node/.local \
137
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
README.md CHANGED
@@ -210,6 +210,8 @@ Example:
210
  ```bash
211
  HUGGINGCLAW_RUN="""
212
  set -e
 
 
213
  sudo apt-get update
214
  sudo apt-get install -y ffmpeg
215
  python3 -m pip install --user pandas requests
@@ -241,7 +243,9 @@ Advanced/backward-compatible variables still work if you prefer package-specific
241
 
242
 
243
  > [!IMPORTANT]
244
- > `sudo` is available for package-manager commands only (`apt`, `apt-get`, and `dpkg`) and is not unrestricted root access. In terminal shells, common user-space commands (for example `sudo unzip`, `sudo tar`, `sudo curl`, `sudo pip`) are passed through and run without escalation for convenience. Apt-installed packages still disappear on Space restart, so put them in `HUGGINGCLAW_RUN` or let the shell wrapper record the command in `startup.sh`.
 
 
245
 
246
  ## πŸ’“ Staying Alive *(Recommended on Free HF Spaces)*
247
 
 
210
  ```bash
211
  HUGGINGCLAW_RUN="""
212
  set -e
213
+ # In HUGGINGCLAW_RUN use normal startup commands. For apt packages,
214
+ # HUGGINGCLAW_APT_PACKAGES=ffmpeg is preferred; sudo also works here.
215
  sudo apt-get update
216
  sudo apt-get install -y ffmpeg
217
  python3 -m pip install --user pandas requests
 
243
 
244
 
245
  > [!IMPORTANT]
246
+ > Terminal shells are admin-convenient but not unrestricted root shells. You normally do **not** need to type `sudo` for package installs in the Jupyter/OpenClaw terminal: `apt install ...` and `apt-get install ...` are wrapped to use the image's passwordless package-manager sudo internally, while `pip`, `python -m pip`, `npm`, and related user-space tools install into HuggingClaw's writable runtime prefix. Direct `sudo` remains limited to `apt`, `apt-get`, and `dpkg`; common user-space commands such as `sudo unzip`, `sudo tar`, `sudo curl`, and `sudo pip` are passed through without escalation for convenience. Apt-installed packages still disappear on Space restart, so put them in `HUGGINGCLAW_APT_PACKAGES`/`HUGGINGCLAW_RUN` or let the shell wrapper record the command in `startup.sh`.
247
+
248
+ If you really need an unrestricted root-capable Jupyter terminal for a private Space, rebuild the image with `--build-arg HUGGINGCLAW_FULL_SUDO=true` (setting this only as a runtime/env-builder variable is not enough). This changes the Docker sudoers file to `node ALL=(root) NOPASSWD: ALL`, so anyone who can access the Jupyter token can become root inside the container. Keep this disabled on public/shared Spaces.
249
 
250
  ## πŸ’“ Staying Alive *(Recommended on Free HF Spaces)*
251
 
env-builder.js CHANGED
@@ -788,6 +788,24 @@ const FIELDS = [
788
  "common": 1,
789
  "tag": "build"
790
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
  {
792
  "g": "Startup",
793
  "icon": "🩺",
 
788
  "common": 1,
789
  "tag": "build"
790
  },
791
+ {
792
+ "g": "Startup",
793
+ "icon": "πŸ”",
794
+ "k": "HUGGINGCLAW_FULL_SUDO",
795
+ "lbl": "Full sudo for Jupyter terminal (Docker build arg only; private Spaces only)",
796
+ "type": "toggle",
797
+ "ph": "false",
798
+ "tag": "build"
799
+ },
800
+ {
801
+ "g": "Runtime",
802
+ "icon": "βš™οΈ",
803
+ "k": "HUGGINGCLAW_WRITABLE_BASE",
804
+ "lbl": "Writable runtime base (blank = /data, HOME fallback, then /tmp)",
805
+ "type": "text",
806
+ "ph": "/data",
807
+ "tag": "advanced"
808
+ },
809
  {
810
  "g": "Startup",
811
  "icon": "🩺",
start.sh CHANGED
@@ -228,6 +228,12 @@ echo ""
228
  echo " ╔══════════════════════════════════════╗"
229
  echo " β•‘ 🦞 HuggingClaw + πŸ’» JupyterLab β•‘"
230
  echo " β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"
 
 
 
 
 
 
231
  echo ""
232
 
233
  # ── Validate required secrets ──
@@ -248,6 +254,29 @@ if [ -n "$ERRORS" ]; then
248
  exit 1
249
  fi
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  # Resolve the actual bundled OpenClaw version so the banner reflects what is
252
  # inside the image, not just the requested tag.
253
  if [ -f "$OPENCLAW_APP_DIR/package.json" ]; then
@@ -288,14 +317,14 @@ if [ "$_do_runtime_upgrade" = "true" ]; then
288
  _upgrade_pkg="openclaw"
289
  [ "$_requested_ver" != "latest" ] && _upgrade_pkg="openclaw@${_requested_ver}"
290
 
291
- # npm install -g respects NPM_CONFIG_PREFIX which is set later in start.sh,
292
- # so use the user-writable prefix explicitly to avoid needing sudo.
293
- _npm_prefix="${NPM_CONFIG_PREFIX:-/home/node/.local}"
294
  if NPM_CONFIG_PREFIX="$_npm_prefix" npm install -g "$_upgrade_pkg" --prefer-online 2>/tmp/openclaw-upgrade.log; then
295
  # Re-read version from the installed package under the explicit prefix
296
  _new_ver=$(node -p "require('${_npm_prefix}/lib/node_modules/openclaw/package.json').version" 2>/dev/null || true)
297
- # PATH already has /home/node/.local/bin before /usr/local/bin (set in
298
- # Dockerfile ENV), so the newly installed binary is picked up automatically
299
  # by 'command openclaw' without needing to update /usr/local/bin/openclaw.
300
  echo "OpenClaw : upgraded to ${_new_ver:-${_requested_ver}} βœ“"
301
  OPENCLAW_RUNTIME_VERSION="${_new_ver:-$OPENCLAW_RUNTIME_VERSION}"
@@ -493,7 +522,8 @@ mkdir -p /home/node/.openclaw/credentials
493
  mkdir -p /home/node/.openclaw/memory
494
  mkdir -p /home/node/.openclaw/extensions
495
  mkdir -p /home/node/.openclaw/workspace
496
- mkdir -p /home/node/.local/bin /home/node/.local/lib /home/node/.npm-global
 
497
  chmod 700 /home/node/.openclaw
498
  chmod 700 /home/node/.openclaw/credentials
499
 
@@ -501,9 +531,17 @@ chmod 700 /home/node/.openclaw/credentials
501
  # npm/pip installs in user-writable locations, make apt noninteractive,
502
  # and persist only a tiny replay script in the synced workspace so packages
503
  # are re-installed after restart.
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
@@ -511,6 +549,13 @@ export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
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).
@@ -1461,7 +1506,7 @@ else
1461
  RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
1462
  fi
1463
  # Add user bin to PATH for jupyter-lab (installed in Dockerfile when DEV_MODE=true)
1464
- export PATH="$HOME/.local/bin:$PATH"
1465
 
1466
  # Runtime install fallback: only attempt if DEV_MODE is enabled but install failed during build
1467
  if [ "$DEV_MODE_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
@@ -1667,18 +1712,29 @@ start_jupyter_once() {
1667
  fi
1668
 
1669
  # Pre-create runtime directory
1670
- mkdir -p "$JUPYTER_ROOT_DIR/.jupyter"
1671
 
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" \
@@ -1748,10 +1804,11 @@ if [ ! -f "$STARTUP_FILE" ]; then
1748
  echo "Created workspace/startup.sh"
1749
  fi
1750
  cat > /home/node/.bashrc << 'BASHRC'
1751
- export PATH="/home/node/.local/bin:$PATH"
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
@@ -1759,6 +1816,13 @@ export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
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}"
 
228
  echo " ╔══════════════════════════════════════╗"
229
  echo " β•‘ 🦞 HuggingClaw + πŸ’» JupyterLab β•‘"
230
  echo " β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"
231
+ if hc_is_true "${HUGGINGCLAW_FULL_SUDO_BUILT:-false}"; then
232
+ echo "WARNING: full passwordless sudo was enabled at image build time. JupyterLab terminal users have root access."
233
+ elif hc_is_true "${HUGGINGCLAW_FULL_SUDO:-false}"; then
234
+ echo "WARNING: HUGGINGCLAW_FULL_SUDO=true was set at runtime, but full sudo requires rebuilding with --build-arg HUGGINGCLAW_FULL_SUDO=true."
235
+ fi
236
+
237
  echo ""
238
 
239
  # ── Validate required secrets ──
 
254
  exit 1
255
  fi
256
 
257
+ # ── Runtime-writable locations ──
258
+ # Hugging Face Docker Spaces only guarantee persistent writable storage under
259
+ # /data when persistent storage is attached. HOME can vary by base image and
260
+ # system/env Jupyter prefixes can be read-only, so pick one writable root once
261
+ # and point Python, npm, and JupyterLab user-writable state there.
262
+ hc_choose_writable_base() {
263
+ local candidate
264
+ for candidate in "${HUGGINGCLAW_WRITABLE_BASE:-}" /data "${HOME:-}/.huggingclaw-runtime" /tmp/huggingclaw-runtime; do
265
+ [ -n "$candidate" ] || continue
266
+ if mkdir -p "$candidate" 2>/dev/null && [ -w "$candidate" ]; then
267
+ printf '%s' "$candidate"
268
+ return 0
269
+ fi
270
+ done
271
+ return 1
272
+ }
273
+
274
+ HC_WRITABLE_BASE="$(hc_choose_writable_base)" || {
275
+ echo "ERROR: no writable runtime directory found for Jupyter/Python/npm state." >&2
276
+ exit 1
277
+ }
278
+ export HC_WRITABLE_BASE
279
+
280
  # Resolve the actual bundled OpenClaw version so the banner reflects what is
281
  # inside the image, not just the requested tag.
282
  if [ -f "$OPENCLAW_APP_DIR/package.json" ]; then
 
317
  _upgrade_pkg="openclaw"
318
  [ "$_requested_ver" != "latest" ] && _upgrade_pkg="openclaw@${_requested_ver}"
319
 
320
+ # npm install -g respects NPM_CONFIG_PREFIX; use the selected writable
321
+ # prefix explicitly to avoid needing sudo or writing into a read-only HOME.
322
+ _npm_prefix="${NPM_CONFIG_PREFIX:-$HC_WRITABLE_BASE/.local}"
323
  if NPM_CONFIG_PREFIX="$_npm_prefix" npm install -g "$_upgrade_pkg" --prefer-online 2>/tmp/openclaw-upgrade.log; then
324
  # Re-read version from the installed package under the explicit prefix
325
  _new_ver=$(node -p "require('${_npm_prefix}/lib/node_modules/openclaw/package.json').version" 2>/dev/null || true)
326
+ # PATH is updated below to prefer the selected writable prefix, so the
327
+ # newly installed binary is picked up automatically
328
  # by 'command openclaw' without needing to update /usr/local/bin/openclaw.
329
  echo "OpenClaw : upgraded to ${_new_ver:-${_requested_ver}} βœ“"
330
  OPENCLAW_RUNTIME_VERSION="${_new_ver:-$OPENCLAW_RUNTIME_VERSION}"
 
522
  mkdir -p /home/node/.openclaw/memory
523
  mkdir -p /home/node/.openclaw/extensions
524
  mkdir -p /home/node/.openclaw/workspace
525
+ mkdir -p "$HC_WRITABLE_BASE/.local/bin" "$HC_WRITABLE_BASE/.local/lib" "$HC_WRITABLE_BASE/.npm-global"
526
+ mkdir -p "$HC_WRITABLE_BASE/.jupyter" "$HC_WRITABLE_BASE/.local/share/jupyter" "$HC_WRITABLE_BASE/.local/share/jupyter/runtime" "$HC_WRITABLE_BASE/.local/share/jupyter/lab"
527
  chmod 700 /home/node/.openclaw
528
  chmod 700 /home/node/.openclaw/credentials
529
 
 
531
  # npm/pip installs in user-writable locations, make apt noninteractive,
532
  # and persist only a tiny replay script in the synced workspace so packages
533
  # are re-installed after restart.
534
+ if [ -z "${NPM_CONFIG_PREFIX:-}" ] || [ "${NPM_CONFIG_PREFIX:-}" = "/home/node/.local" ]; then
535
+ export NPM_CONFIG_PREFIX="$HC_WRITABLE_BASE/.local"
536
+ else
537
+ export NPM_CONFIG_PREFIX
538
+ fi
539
  export npm_config_prefix="$NPM_CONFIG_PREFIX"
540
+ if [ -z "${PYTHONUSERBASE:-}" ] || [ "${PYTHONUSERBASE:-}" = "/home/node/.local" ]; then
541
+ export PYTHONUSERBASE="$HC_WRITABLE_BASE/.local"
542
+ else
543
+ export PYTHONUSERBASE
544
+ fi
545
  # Debian/Ubuntu images mark system Python as externally managed (PEP 668).
546
  # JupyterLab's PyPI extension manager runs pip from the server process, so it
547
  # does not see the interactive shell wrappers below. Allow that non-interactive
 
549
  # "externally-managed-environment".
550
  export PIP_BREAK_SYSTEM_PACKAGES="${PIP_BREAK_SYSTEM_PACKAGES:-1}"
551
  export PIP_USER="${PIP_USER:-1}"
552
+ export JUPYTER_CONFIG_DIR="${JUPYTER_CONFIG_DIR:-$HC_WRITABLE_BASE/.jupyter}"
553
+ export JUPYTER_DATA_DIR="${JUPYTER_DATA_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter}"
554
+ export JUPYTER_RUNTIME_DIR="${JUPYTER_RUNTIME_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter/runtime}"
555
+ export JUPYTERLAB_DIR="${JUPYTERLAB_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter/lab}"
556
+ export JUPYTERLAB_SETTINGS_DIR="${JUPYTERLAB_SETTINGS_DIR:-$JUPYTER_CONFIG_DIR/lab/user-settings}"
557
+ export JUPYTERLAB_WORKSPACES_DIR="${JUPYTERLAB_WORKSPACES_DIR:-$JUPYTER_CONFIG_DIR/lab/workspaces}"
558
+ export JUPYTER_PREFER_ENV_PATH="${JUPYTER_PREFER_ENV_PATH:-0}"
559
  export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
560
  # Show current working directory in terminal prompt (JupyterLab terminals can
561
  # otherwise display only "$" when PS1 is unset/minimal).
 
1506
  RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
1507
  fi
1508
  # Add user bin to PATH for jupyter-lab (installed in Dockerfile when DEV_MODE=true)
1509
+ export PATH="$HC_WRITABLE_BASE/.local/bin:$HOME/.local/bin:$PATH"
1510
 
1511
  # Runtime install fallback: only attempt if DEV_MODE is enabled but install failed during build
1512
  if [ "$DEV_MODE_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
 
1712
  fi
1713
 
1714
  # Pre-create runtime directory
1715
+ mkdir -p "$JUPYTER_ROOT_DIR/.jupyter" "$JUPYTER_CONFIG_DIR" "$JUPYTER_DATA_DIR" "$JUPYTER_RUNTIME_DIR" "$JUPYTERLAB_DIR" "$JUPYTERLAB_SETTINGS_DIR" "$JUPYTERLAB_WORKSPACES_DIR"
1716
 
1717
  echo "Terminal : starting (root: $JUPYTER_ROOT_DIR)"
1718
  JUPYTER_LOG_FILE="/tmp/jupyterlab.log"
1719
 
1720
  # Use explicit Python to avoid PATH issues; set memory-friendly limits.
1721
  # Keep pip user-site defaults in this process so JupyterLab's PyPI Manager
1722
+ # can install extensions/packages under the selected writable base on PEP 668 images.
1723
  export PYTHONPATH=""
1724
+ if [ -z "${PYTHONUSERBASE:-}" ] || [ "${PYTHONUSERBASE:-}" = "/home/node/.local" ]; then
1725
+ export PYTHONUSERBASE="$HC_WRITABLE_BASE/.local"
1726
+ else
1727
+ export PYTHONUSERBASE
1728
+ fi
1729
  export PIP_BREAK_SYSTEM_PACKAGES="${PIP_BREAK_SYSTEM_PACKAGES:-1}"
1730
  export PIP_USER="${PIP_USER:-1}"
1731
+ export JUPYTER_CONFIG_DIR="${JUPYTER_CONFIG_DIR:-$HC_WRITABLE_BASE/.jupyter}"
1732
+ export JUPYTER_DATA_DIR="${JUPYTER_DATA_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter}"
1733
+ export JUPYTER_RUNTIME_DIR="${JUPYTER_RUNTIME_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter/runtime}"
1734
+ export JUPYTERLAB_DIR="${JUPYTERLAB_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter/lab}"
1735
+ export JUPYTERLAB_SETTINGS_DIR="${JUPYTERLAB_SETTINGS_DIR:-$JUPYTER_CONFIG_DIR/lab/user-settings}"
1736
+ export JUPYTERLAB_WORKSPACES_DIR="${JUPYTERLAB_WORKSPACES_DIR:-$JUPYTER_CONFIG_DIR/lab/workspaces}"
1737
+ export JUPYTER_PREFER_ENV_PATH="${JUPYTER_PREFER_ENV_PATH:-0}"
1738
  hc_env_without_gateway_preloads python3 -m jupyterlab \
1739
  --ip 127.0.0.1 \
1740
  --port "$JUPYTER_PORT" \
 
1804
  echo "Created workspace/startup.sh"
1805
  fi
1806
  cat > /home/node/.bashrc << 'BASHRC'
1807
+ export HC_WRITABLE_BASE="${HC_WRITABLE_BASE:-/tmp/huggingclaw-runtime}"
1808
+ export PATH="$HC_WRITABLE_BASE/.local/bin:/home/node/.local/bin:$PATH"
1809
+ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-$HC_WRITABLE_BASE/.local}"
1810
  export npm_config_prefix="$NPM_CONFIG_PREFIX"
1811
+ export PYTHONUSERBASE="${PYTHONUSERBASE:-$HC_WRITABLE_BASE/.local}"
1812
  # Debian/Ubuntu images mark system Python as externally managed (PEP 668).
1813
  # JupyterLab's PyPI extension manager runs pip from the server process, so it
1814
  # does not see the interactive shell wrappers below. Allow that non-interactive
 
1816
  # "externally-managed-environment".
1817
  export PIP_BREAK_SYSTEM_PACKAGES="${PIP_BREAK_SYSTEM_PACKAGES:-1}"
1818
  export PIP_USER="${PIP_USER:-1}"
1819
+ export JUPYTER_CONFIG_DIR="${JUPYTER_CONFIG_DIR:-$HC_WRITABLE_BASE/.jupyter}"
1820
+ export JUPYTER_DATA_DIR="${JUPYTER_DATA_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter}"
1821
+ export JUPYTER_RUNTIME_DIR="${JUPYTER_RUNTIME_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter/runtime}"
1822
+ export JUPYTERLAB_DIR="${JUPYTERLAB_DIR:-$HC_WRITABLE_BASE/.local/share/jupyter/lab}"
1823
+ export JUPYTERLAB_SETTINGS_DIR="${JUPYTERLAB_SETTINGS_DIR:-$JUPYTER_CONFIG_DIR/lab/user-settings}"
1824
+ export JUPYTERLAB_WORKSPACES_DIR="${JUPYTERLAB_WORKSPACES_DIR:-$JUPYTER_CONFIG_DIR/lab/workspaces}"
1825
+ export JUPYTER_PREFER_ENV_PATH="${JUPYTER_PREFER_ENV_PATH:-0}"
1826
  export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
1827
  export HISTFILE="${HISTFILE:-/home/node/.bash_history}"
1828
  export HISTSIZE="${HISTSIZE:-50000}"