lvwerra HF Staff commited on
Commit
3c13663
·
verified ·
1 Parent(s): 413e015

OpenClaw gets its own HOME on local disk (no symlinks — boundary check rejects them; no FUSE — fence rejects that): boot restore + 60s backup

Browse files
entrypoint.sh CHANGED
@@ -40,25 +40,30 @@ export PATH="$AM_LOCAL/py/bin:$AM_LOCAL/npm/bin:$AM_LOCAL/bin:$HOME/.local/bin:$
40
  # embedded prompt lock was released"). Its state therefore lives on LOCAL disk,
41
  # with a durable copy on the bucket: restored on boot, synced back every 60s.
42
  # Worst case on an unclean stop: the last minute of chat history.
43
- export OPENCLAW_STATE_DIR="$AM_LOCAL/openclaw"
44
- OPENCLAW_BACKUP="$DATA_DIR/state/openclaw-backup"
45
- mkdir -p "$OPENCLAW_STATE_DIR" "$OPENCLAW_BACKUP"
46
- # seed local state from the durable backup
47
- if [ -n "$(ls -A "$OPENCLAW_BACKUP" 2>/dev/null)" ]; then
48
- cp -a "$OPENCLAW_BACKUP/." "$OPENCLAW_STATE_DIR/" 2>/dev/null || true
49
- fi
50
- # The embedded runtime ignores OPENCLAW_STATE_DIR and resolves ~/.openclaw
51
- # directly, so ~/.openclaw becomes a SYMLINK to the local dir. If a real dir
52
- # is there (first boot, or written by an older build), merge its contents
53
- # (newer than the backup) and park it as .openclaw.pre-symlink.
54
- if [ -d "$HOME/.openclaw" ] && [ ! -L "$HOME/.openclaw" ]; then
55
- cp -a "$HOME/.openclaw/." "$OPENCLAW_STATE_DIR/" 2>/dev/null || true
56
- mv "$HOME/.openclaw" "$HOME/.openclaw.pre-symlink" 2>/dev/null || true
57
- fi
58
- [ -L "$HOME/.openclaw" ] || ln -s "$OPENCLAW_STATE_DIR" "$HOME/.openclaw" 2>/dev/null || true
 
 
 
 
 
59
  ( while :; do
60
  sleep 60
61
- rsync -a --delete "$OPENCLAW_STATE_DIR/" "$OPENCLAW_BACKUP/" 2>/dev/null || true
62
  done ) &
63
 
64
  # Durable, user-editable setup script. Runs on EVERY start (keep it idempotent);
 
40
  # embedded prompt lock was released"). Its state therefore lives on LOCAL disk,
41
  # with a durable copy on the bucket: restored on boot, synced back every 60s.
42
  # Worst case on an unclean stop: the last minute of chat history.
43
+ # OpenClaw can't run its state on the FUSE bucket (its session fence
44
+ # false-positives on unstable metadata) and it REJECTS symlinked paths (the
45
+ # workspace boundary check). No symlinks, no env overrides — OpenClaw simply
46
+ # gets its OWN HOME on local disk: a real, ordinary install from its point of
47
+ # view. Durable copy on the bucket: restored on boot, synced back every 60s.
48
+ # Worst case on an unclean stop: the last minute of claw state.
49
+ export OPENCLAW_HOME="$AM_LOCAL/oc-home" # runner launches openclaw with HOME=$OPENCLAW_HOME
50
+ export OPENCLAW_STATE_DIR="$OPENCLAW_HOME/.openclaw" # where the server finds its config/traces
51
+ OC_BACKUP="$DATA_DIR/state/openclaw-backup"
52
+ mkdir -p "$OPENCLAW_STATE_DIR" "$OC_BACKUP"
53
+ # heal from the earlier symlink experiment
54
+ [ -L "$HOME/.openclaw" ] && rm "$HOME/.openclaw"
55
+ # seed local state: backup (freshest) first, then legacy dirs fill gaps (--update: never clobber newer)
56
+ [ -n "$(ls -A "$OC_BACKUP" 2>/dev/null)" ] && rsync -a "$OC_BACKUP/" "$OPENCLAW_STATE_DIR/" 2>/dev/null
57
+ for legacy in "$HOME/.openclaw.pre-symlink" "$HOME/.openclaw"; do
58
+ if [ -d "$legacy" ] && [ ! -L "$legacy" ]; then
59
+ rsync -a --update "$legacy/" "$OPENCLAW_STATE_DIR/" 2>/dev/null || true
60
+ fi
61
+ done
62
+ # small comforts in the private HOME (harmless if missing)
63
+ cp "$HOME/.gitconfig" "$OPENCLAW_HOME/.gitconfig" 2>/dev/null || true
64
  ( while :; do
65
  sleep 60
66
+ rsync -a --delete "$OPENCLAW_STATE_DIR/" "$OC_BACKUP/" 2>/dev/null || true
67
  done ) &
68
 
69
  # Durable, user-editable setup script. Runs on EVERY start (keep it idempotent);
server/src/index.js CHANGED
@@ -180,7 +180,7 @@ const BUILD_ENV_KEYS = (() => {
180
  const NON_SECRET = new Set([
181
  'HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'NPM_CONFIG_PREFIX', 'PWD', 'OLDPWD', 'SHLVL', '_', 'HOSTNAME',
182
  'ACCELERATOR', 'COMMIT_SHA', 'CPU_CORES', 'HF_DATASETS_TRUST_REMOTE_CODE', 'IMAGE_SHA', 'MEMORY', 'OMP_NUM_THREADS',
183
- 'UV_CACHE_DIR', 'PIP_CACHE_DIR', 'PYTHONPYCACHEPREFIX', 'PYTHONUSERBASE', 'OPENCLAW_STATE_DIR',
184
  ]);
185
  const NON_SECRET_PREFIX = ['SPACE_', 'KUBERNETES_', 'NVIDIA_', 'CUDA_', 'NV_', 'AM_'];
186
  function injectedEnvKeys() {
@@ -255,7 +255,7 @@ Hermes — alongside plain shells and a file browser.
255
  - \`/data\` is **durable storage** (a mounted bucket). Files under \`/data/workspaces/…\` and \`/data/home/…\` survive restarts and sleep.
256
  - **Empty directories are not persisted** — only files. If a folder must exist, keep a file in it.
257
  - Sessions are **tmux-backed**: they keep running when the browser disconnects and can be resumed after the Space sleeps.
258
- - Exception: OpenClaw's state (\`$OPENCLAW_STATE_DIR\`) lives on local disk for filesystem compatibility and is backed up to the bucket every minute.
259
 
260
  ## You may not be alone
261
  - Other agents run in **sibling folders** under \`/data/workspaces/\`, and you may be **grouped** to share a single folder with other agents.
 
180
  const NON_SECRET = new Set([
181
  'HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'NPM_CONFIG_PREFIX', 'PWD', 'OLDPWD', 'SHLVL', '_', 'HOSTNAME',
182
  'ACCELERATOR', 'COMMIT_SHA', 'CPU_CORES', 'HF_DATASETS_TRUST_REMOTE_CODE', 'IMAGE_SHA', 'MEMORY', 'OMP_NUM_THREADS',
183
+ 'UV_CACHE_DIR', 'PIP_CACHE_DIR', 'PYTHONPYCACHEPREFIX', 'PYTHONUSERBASE', 'OPENCLAW_STATE_DIR', 'OPENCLAW_HOME',
184
  ]);
185
  const NON_SECRET_PREFIX = ['SPACE_', 'KUBERNETES_', 'NVIDIA_', 'CUDA_', 'NV_', 'AM_'];
186
  function injectedEnvKeys() {
 
255
  - \`/data\` is **durable storage** (a mounted bucket). Files under \`/data/workspaces/…\` and \`/data/home/…\` survive restarts and sleep.
256
  - **Empty directories are not persisted** — only files. If a folder must exist, keep a file in it.
257
  - Sessions are **tmux-backed**: they keep running when the browser disconnects and can be resumed after the Space sleeps.
258
+ - Exception: OpenClaw runs with its own \`$HOME\` on local disk for filesystem compatibility; that state is backed up to the bucket every minute.
259
 
260
  ## You may not be alone
261
  - Other agents run in **sibling folders** under \`/data/workspaces/\`, and you may be **grouped** to share a single folder with other agents.
server/src/runner.js CHANGED
@@ -212,8 +212,12 @@ function commandFor(session) {
212
  // config exists, go straight to the TUI. Decided by config-file existence —
213
  // same honest pattern as the Claude transcript check.
214
  if (cli.id === 'openclaw') {
215
- const cfg = '"${OPENCLAW_CONFIG_PATH:-${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/openclaw.json}"';
216
- return `if [ -s ${cfg} ]; then exec openclaw chat; else openclaw onboard && exec openclaw chat; fi`;
 
 
 
 
217
  }
218
 
219
  // Codex: resume this agent's pinned conversation (captured from its rollout
 
212
  // config exists, go straight to the TUI. Decided by config-file existence —
213
  // same honest pattern as the Claude transcript check.
214
  if (cli.id === 'openclaw') {
215
+ // OpenClaw runs with its own HOME on local disk (see entrypoint.sh): its
216
+ // state can't live on the FUSE bucket and it rejects symlinked paths.
217
+ // Locally (no OPENCLAW_HOME) the real HOME is used unchanged.
218
+ return 'HOME="${OPENCLAW_HOME:-$HOME}"; export HOME; '
219
+ + 'if [ -s "$HOME/.openclaw/openclaw.json" ]; then exec openclaw chat; '
220
+ + 'else openclaw onboard && exec openclaw chat; fi';
221
  }
222
 
223
  // Codex: resume this agent's pinned conversation (captured from its rollout
web/src/components/Sidebar.tsx CHANGED
@@ -229,14 +229,18 @@ export default function Sidebar({
229
  )}
230
  </div>
231
 
232
- {/* Overview: pinned above the session tree, selectable like a session */}
 
 
233
  <div className="ov-fixed">
234
  <div
235
- className={`row ov-row${activeRef === 'overview' ? ' active' : ''}`}
236
  onClick={() => onActivate('overview')}
 
237
  >
 
 
238
  <PulseGlyph className="ov-row-ico" />
239
- <span className="name">Overview</span>
240
  </div>
241
  </div>
242
 
 
229
  )}
230
  </div>
231
 
232
+ {/* Overview: pinned above the tree, styled EXACTLY like a session row
233
+ (dot · mono name · glyph) so it reads as clickable — its dot is the
234
+ aggregate of every agent's state. */}
235
  <div className="ov-fixed">
236
  <div
237
+ className={`row session ov-row${activeRef === 'overview' ? ' active' : ''}`}
238
  onClick={() => onActivate('overview')}
239
+ title="All agents: digests, states, replies"
240
  >
241
+ <span className={`status ${agg}`} />
242
+ <span className="name">overview</span>
243
  <PulseGlyph className="ov-row-ico" />
 
244
  </div>
245
  </div>
246
 
web/src/styles.css CHANGED
@@ -181,10 +181,10 @@ body {
181
  .stepper button:disabled { opacity: 0.35; cursor: not-allowed; }
182
  .stepper-n { min-width: 20px; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
183
 
184
- /* overview: pinned sidebar row + cards view */
185
  .ov-fixed { padding: 6px 8px 0; }
186
- .ov-row .name { font-family: var(--font-sans); font-size: 13px; font-weight: 600; }
187
- .ov-row-ico { width: 14px; height: 14px; color: var(--accent); flex: none; }
188
  /* ---- Overview: one reading column of capsules (design mock v4.12) ---- */
189
  .ov-wrap { height: 100%; overflow-y: auto; }
190
  .ov-feed { max-width: 680px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px; padding: 2px 2px 40px; }
 
181
  .stepper button:disabled { opacity: 0.35; cursor: not-allowed; }
182
  .stepper-n { min-width: 20px; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
183
 
184
+ /* overview: pinned sidebar row (session-row anatomy) + cards view */
185
  .ov-fixed { padding: 6px 8px 0; }
186
+ .ov-row { cursor: pointer; }
187
+ .ov-row-ico { width: 13px; height: 13px; color: var(--accent); flex: none; }
188
  /* ---- Overview: one reading column of capsules (design mock v4.12) ---- */
189
  .ov-wrap { height: 100%; overflow-y: auto; }
190
  .ov-feed { max-width: 680px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px; padding: 2px 2px 40px; }