Spaces:
Running
Running
Anurag commited on
Commit ·
be6d947
1
Parent(s): c68251f
Set default Jupyter root to filesystem root
Browse files- README.md +1 -1
- health-server.js +13 -1
- openclaw-sync.py +72 -14
- 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
#
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 531 |
|
| 532 |
-
|
| 533 |
-
|
|
|
|
| 534 |
"""
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:-/
|
| 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"
|