Anurag commited on
Commit
be6d947
·
1 Parent(s): c68251f

Set default Jupyter root to filesystem root

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. health-server.js +13 -1
  3. openclaw-sync.py +72 -14
  4. start.sh +6 -1
README.md CHANGED
@@ -370,7 +370,7 @@ The merged Space includes the Hugging Face JupyterLab template behavior inside t
370
  | `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
371
  | `/terminal/` | JupyterLab terminal | `8888` | Auto-enabled when `GATEWAY_TOKEN` is set; uses `GATEWAY_TOKEN` as auth token unless `JUPYTER_TOKEN` is set separately. Set `DEV_MODE=false` to disable. |
372
 
373
- When enabled, the terminal notebook root is `/home/node`, so you can inspect HuggingClaw files, logs, workspace state, and runtime scripts from the browser.
374
 
375
  > [!IMPORTANT]
376
  > No extra secret needed — `GATEWAY_TOKEN` is automatically reused as `JUPYTER_TOKEN`. Set a separate `JUPYTER_TOKEN` secret only if you want a different terminal credential.
 
370
  | `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
371
  | `/terminal/` | JupyterLab terminal | `8888` | Auto-enabled when `GATEWAY_TOKEN` is set; uses `GATEWAY_TOKEN` as auth token unless `JUPYTER_TOKEN` is set separately. Set `DEV_MODE=false` to disable. |
372
 
373
+ When enabled, the terminal notebook root defaults to `/`, so you can browse the full container filesystem from the browser (including `/home/node`). Handy shortcuts are also created: `HuggingClaw`, `HuggingClaw-Workspace`, and `OpenClaw-Home`.
374
 
375
  > [!IMPORTANT]
376
  > No extra secret needed — `GATEWAY_TOKEN` is automatically reused as `JUPYTER_TOKEN`. Set a separate `JUPYTER_TOKEN` secret only if you want a different terminal credential.
health-server.js CHANGED
@@ -62,6 +62,10 @@ function deriveHfSpaceUrl() {
62
  return "";
63
  }
64
  const HF_SPACE_URL = deriveHfSpaceUrl();
 
 
 
 
65
 
66
  // ── Privacy Detection ──
67
  // Priority order:
@@ -643,7 +647,14 @@ const server = http.createServer(async (req, res) => {
643
  // the fail-secure default (SPACE_IS_PRIVATE=true), causing private redirects
644
  // even when the space is actually public or the owner is accessing via HF App.
645
  // After the very first HTML request, _privacyDetectionDone=true so no delay.
646
- if (isHtmlRequest && !_privacyDetectionDone) await privacyDetectionReady;
 
 
 
 
 
 
 
647
 
648
  // In-app navigation (clicking links within the HF iframe) sends a Referer
649
  // from the same .hf.space origin — don't redirect those, only redirect
@@ -662,6 +673,7 @@ const server = http.createServer(async (req, res) => {
662
  ));
663
  // NOTE: computed AFTER detection is awaited above — always uses real value.
664
  const isDirectHfSpaceRequest = SPACE_IS_PRIVATE &&
 
665
  HF_SPACE_URL &&
666
  isHtmlRequest &&
667
  typeof req.headers.host === "string" &&
 
62
  return "";
63
  }
64
  const HF_SPACE_URL = deriveHfSpaceUrl();
65
+ const _privacyWaitRaw = Number(process.env.PRIVACY_DETECTION_WAIT_MS || "1500");
66
+ const PRIVACY_DETECTION_WAIT_MS = Number.isFinite(_privacyWaitRaw)
67
+ ? Math.max(0, Math.floor(_privacyWaitRaw))
68
+ : 1500;
69
 
70
  // ── Privacy Detection ──
71
  // Priority order:
 
647
  // the fail-secure default (SPACE_IS_PRIVATE=true), causing private redirects
648
  // even when the space is actually public or the owner is accessing via HF App.
649
  // After the very first HTML request, _privacyDetectionDone=true so no delay.
650
+ let privacyWaitTimedOut = false;
651
+ if (isHtmlRequest && !_privacyDetectionDone) {
652
+ const waitResult = await Promise.race([
653
+ privacyDetectionReady.then(() => "detected"),
654
+ new Promise((resolve) => setTimeout(() => resolve("timeout"), PRIVACY_DETECTION_WAIT_MS)),
655
+ ]);
656
+ privacyWaitTimedOut = waitResult == "timeout";
657
+ }
658
 
659
  // In-app navigation (clicking links within the HF iframe) sends a Referer
660
  // from the same .hf.space origin — don't redirect those, only redirect
 
673
  ));
674
  // NOTE: computed AFTER detection is awaited above — always uses real value.
675
  const isDirectHfSpaceRequest = SPACE_IS_PRIVATE &&
676
+ !privacyWaitTimedOut &&
677
  HF_SPACE_URL &&
678
  isHtmlRequest &&
679
  typeof req.headers.host === "string" &&
openclaw-sync.py CHANGED
@@ -75,7 +75,7 @@ EXCLUDED_STATE_NAMES = {
75
  "browser",
76
  "npm",
77
  }
78
- SESSIONS_DIR = OPENCLAW_HOME / "agents" / "main" / "sessions"
79
  WHATSAPP_CREDS_DIR = OPENCLAW_HOME / "credentials" / "whatsapp" / "default"
80
  WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
81
  RESET_MARKER = WORKSPACE / ".reset_credentials"
@@ -109,6 +109,30 @@ def count_files(path: Path) -> int:
109
  return sum(1 for child in path.rglob("*") if child.is_file())
110
 
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  def snapshot_state_into_workspace() -> None:
113
  try:
114
  STATE_DIR.mkdir(parents=True, exist_ok=True)
@@ -120,20 +144,32 @@ def snapshot_state_into_workspace() -> None:
120
  shutil.rmtree(staging_dir, ignore_errors=True)
121
  staging_dir.mkdir(parents=True, exist_ok=True)
122
 
 
123
  for source_path in OPENCLAW_HOME.iterdir():
124
  if source_path.name in EXCLUDED_STATE_NAMES:
125
  continue
126
 
127
  backup_path = staging_dir / source_path.name
128
- if source_path.is_dir():
129
- shutil.copytree(source_path, backup_path)
130
- elif source_path.is_file():
131
- shutil.copy2(source_path, backup_path)
132
-
133
- # Atomically swap staging real backup dir
134
- if OPENCLAW_STATE_BACKUP_DIR.exists():
135
- shutil.rmtree(OPENCLAW_STATE_BACKUP_DIR, ignore_errors=True)
136
- staging_dir.rename(OPENCLAW_STATE_BACKUP_DIR)
 
 
 
 
 
 
 
 
 
 
 
137
  except Exception as exc:
138
  # Clean up staging on failure so it doesn't interfere next time
139
  staging_dir = STATE_DIR / ".openclaw-staging"
@@ -527,12 +563,34 @@ def is_valid_json_file(path: Path) -> bool:
527
 
528
 
529
  def sessions_marker() -> tuple[int, int, int, str]:
530
- """Return a lightweight marker for the sessions directory.
531
 
532
- Uses the same metadata_marker() logic so any new, deleted, or modified
533
- session file is detected without hashing file contents.
 
534
  """
535
- return metadata_marker(SESSIONS_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
 
538
  def wait_for_config_settle(config_marker: tuple[int, int, int]) -> tuple[str, tuple[int, int, int]]:
 
75
  "browser",
76
  "npm",
77
  }
78
+ SESSIONS_ROOT = OPENCLAW_HOME / "agents"
79
  WHATSAPP_CREDS_DIR = OPENCLAW_HOME / "credentials" / "whatsapp" / "default"
80
  WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
81
  RESET_MARKER = WORKSPACE / ".reset_credentials"
 
109
  return sum(1 for child in path.rglob("*") if child.is_file())
110
 
111
 
112
+ def copy_state_entry_with_retry(source_path: Path, backup_path: Path, attempts: int = 3) -> None:
113
+ """Copy one top-level .openclaw entry with short retries for hot files/dirs."""
114
+ last_exc: Exception | None = None
115
+ for attempt in range(1, attempts + 1):
116
+ try:
117
+ if source_path.is_dir():
118
+ shutil.copytree(source_path, backup_path)
119
+ return
120
+ if source_path.is_file():
121
+ shutil.copy2(source_path, backup_path)
122
+ return
123
+ return
124
+ except Exception as exc:
125
+ last_exc = exc
126
+ if attempt < attempts:
127
+ time.sleep(0.2 * attempt)
128
+ if backup_path.exists():
129
+ if backup_path.is_dir():
130
+ shutil.rmtree(backup_path, ignore_errors=True)
131
+ else:
132
+ backup_path.unlink(missing_ok=True)
133
+ continue
134
+ raise last_exc
135
+
136
  def snapshot_state_into_workspace() -> None:
137
  try:
138
  STATE_DIR.mkdir(parents=True, exist_ok=True)
 
144
  shutil.rmtree(staging_dir, ignore_errors=True)
145
  staging_dir.mkdir(parents=True, exist_ok=True)
146
 
147
+ skipped_entries: list[tuple[str, Exception]] = []
148
  for source_path in OPENCLAW_HOME.iterdir():
149
  if source_path.name in EXCLUDED_STATE_NAMES:
150
  continue
151
 
152
  backup_path = staging_dir / source_path.name
153
+ try:
154
+ copy_state_entry_with_retry(source_path, backup_path)
155
+ except Exception as entry_exc:
156
+ skipped_entries.append((source_path.name, entry_exc))
157
+
158
+ # If any top-level state entries could not be copied, keep the
159
+ # previous known-good snapshot instead of replacing it with a partial
160
+ # backup. We'll retry next pass.
161
+ if skipped_entries:
162
+ for name, entry_exc in skipped_entries:
163
+ print(f"Warning: skipping state entry {name}: {entry_exc}")
164
+ print(
165
+ "Warning: OpenClaw state snapshot incomplete; keeping previous backup and retrying next sync."
166
+ )
167
+ shutil.rmtree(staging_dir, ignore_errors=True)
168
+ else:
169
+ # Atomically swap staging → real backup dir
170
+ if OPENCLAW_STATE_BACKUP_DIR.exists():
171
+ shutil.rmtree(OPENCLAW_STATE_BACKUP_DIR, ignore_errors=True)
172
+ staging_dir.rename(OPENCLAW_STATE_BACKUP_DIR)
173
  except Exception as exc:
174
  # Clean up staging on failure so it doesn't interfere next time
175
  staging_dir = STATE_DIR / ".openclaw-staging"
 
563
 
564
 
565
  def sessions_marker() -> tuple[int, int, int, str]:
566
+ """Return a lightweight marker for all agent session directories.
567
 
568
+ OpenClaw can use agent profiles beyond "main". Watch every
569
+ */sessions path under .openclaw/agents so session changes always trigger
570
+ syncs regardless of profile name.
571
  """
572
+ if not SESSIONS_ROOT.exists():
573
+ return (0, 0, 0, "")
574
+
575
+ file_count = 0
576
+ total_size = 0
577
+ newest_mtime = 0
578
+ metadata_hasher = hashlib.sha256()
579
+
580
+ for profile_dir in sorted(SESSIONS_ROOT.iterdir()):
581
+ if not profile_dir.is_dir():
582
+ continue
583
+ sessions_dir = profile_dir / "sessions"
584
+ marker = metadata_marker(sessions_dir)
585
+ file_count += marker[0]
586
+ total_size += marker[1]
587
+ newest_mtime = max(newest_mtime, marker[2])
588
+ metadata_hasher.update(profile_dir.name.encode("utf-8"))
589
+ metadata_hasher.update(b"\0")
590
+ metadata_hasher.update(marker[3].encode("ascii"))
591
+ metadata_hasher.update(b"\0")
592
+
593
+ return (file_count, total_size, newest_mtime, metadata_hasher.hexdigest())
594
 
595
 
596
  def wait_for_config_settle(config_marker: tuple[int, int, int]) -> tuple[str, tuple[int, int, int]]:
start.sh CHANGED
@@ -1016,7 +1016,7 @@ start_jupyter_once() {
1016
  echo " DEV_MODE active but JupyterLab will NOT start until JUPYTER_TOKEN is changed." >&2
1017
  return 1
1018
  fi
1019
- JUPYTER_ROOT_DIR="${JUPYTER_ROOT_DIR:-/home/node}"
1020
  if [ "$JUPYTER_ROOT_DIR" = "/home/node/.openclaw/workspace" ] && [ "$DEVDATA_ENABLED" = "true" ]; then
1021
  echo "Jupyter root was set to OpenClaw workspace; moving Jupyter root to /home/node/devdata to keep BACKUP and DEVDATA datasets separate."
1022
  JUPYTER_ROOT_DIR="/home/node/devdata"
@@ -1033,6 +1033,11 @@ start_jupyter_once() {
1033
  ln -sfn /home/node/.openclaw/workspace "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace"
1034
  fi
1035
  fi
 
 
 
 
 
1036
 
1037
  # Pre-create runtime directory
1038
  mkdir -p "$JUPYTER_ROOT_DIR/.jupyter"
 
1016
  echo " DEV_MODE active but JupyterLab will NOT start until JUPYTER_TOKEN is changed." >&2
1017
  return 1
1018
  fi
1019
+ JUPYTER_ROOT_DIR="${JUPYTER_ROOT_DIR:-/}"
1020
  if [ "$JUPYTER_ROOT_DIR" = "/home/node/.openclaw/workspace" ] && [ "$DEVDATA_ENABLED" = "true" ]; then
1021
  echo "Jupyter root was set to OpenClaw workspace; moving Jupyter root to /home/node/devdata to keep BACKUP and DEVDATA datasets separate."
1022
  JUPYTER_ROOT_DIR="/home/node/devdata"
 
1033
  ln -sfn /home/node/.openclaw/workspace "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace"
1034
  fi
1035
  fi
1036
+ if [ "$JUPYTER_ROOT_DIR" != "/home/node/.openclaw" ]; then
1037
+ if [ -L "$JUPYTER_ROOT_DIR/OpenClaw-Home" ] || [ ! -e "$JUPYTER_ROOT_DIR/OpenClaw-Home" ]; then
1038
+ ln -sfn /home/node/.openclaw "$JUPYTER_ROOT_DIR/OpenClaw-Home"
1039
+ fi
1040
+ fi
1041
 
1042
  # Pre-create runtime directory
1043
  mkdir -p "$JUPYTER_ROOT_DIR/.jupyter"