mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
doctor: warn on macOS cloud-synced state directories (#31004)
* Doctor: detect macOS cloud-synced state directories * Doctor tests: cover cloud-synced macOS state detection * Docs: note cloud-synced state warning in doctor guide * Docs: recommend local macOS state dir placement * Changelog: add macOS cloud-synced state dir warning * Changelog: credit macOS cloud state warning PR * Doctor state: anchor cloud-sync roots to macOS home * Doctor tests: cover OPENCLAW_HOME cloud-sync override * Doctor state: prefer resolved target for cloud detection * Doctor tests: cover local-target cloud symlink case
This commit is contained in:
@@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- ACP/Harness thread spawn routing: force ACP harness thread creation through `sessions_spawn` (`runtime: "acp"`, `thread: true`) and explicitly forbid `message action=thread-create` for ACP harness requests, avoiding misrouted `Unknown channel` errors. (#30957) Thanks @dutifulbob.
|
||||
- CLI/Startup (Raspberry Pi + small hosts): speed up startup by avoiding unnecessary plugin preload on fast routes, adding root `--version` fast-path bootstrap bypass, parallelizing status JSON/non-JSON scans where safe, and enabling Node compile cache at startup with env override compatibility (`NODE_COMPILE_CACHE`, `NODE_DISABLE_COMPILE_CACHE`). (#5871) Thanks @BookCatKid and @vincentkoc for raising startup reports, and @lupuletic for related startup work in #27973.
|
||||
- Doctor/macOS state-dir safety: warn when OpenClaw state resolves inside iCloud Drive (`~/Library/Mobile Documents/com~apple~CloudDocs/...`) or `~/Library/CloudStorage/...`, because sync-backed paths can cause slower I/O and lock/sync races. (#31004) Thanks @vincentkoc.
|
||||
- CLI/Startup follow-up: add root `--help` fast-path bootstrap bypass with strict root-only matching, lazily resolve CLI channel options only when commands need them, merge build-time startup metadata (`dist/cli-startup-metadata.json`) with runtime catalog discovery so dynamic catalogs are preserved, and add low-power Linux doctor hints for compile-cache placement and respawn tuning. (#30975) Thanks @vincentkoc.
|
||||
- Telegram/Outbound API proxy env: keep the Node 22 `autoSelectFamily` global-dispatcher workaround while restoring env-proxy support by using `EnvHttpProxyAgent` so `HTTP_PROXY`/`HTTPS_PROXY` continue to apply to outbound requests. (#26207) Thanks @qsysbio-cjw for reporting and @rylena and @vincentkoc for work.
|
||||
- Browser/Security: fail closed on browser-control auth bootstrap errors; if auto-auth setup fails and no explicit token/password exists, browser control server startup now aborts instead of starting unauthenticated. This ships in the next npm release. Thanks @ijxpwastaken.
|
||||
|
||||
@@ -164,6 +164,10 @@ Doctor checks:
|
||||
the directory, and reminds you that it cannot recover missing data.
|
||||
- **State dir permissions**: verifies writability; offers to repair permissions
|
||||
(and emits a `chown` hint when owner/group mismatch is detected).
|
||||
- **macOS cloud-synced state dir**: warns when state resolves under iCloud Drive
|
||||
(`~/Library/Mobile Documents/com~apple~CloudDocs/...`) or
|
||||
`~/Library/CloudStorage/...` because sync-backed paths can cause slower I/O
|
||||
and lock/sync races.
|
||||
- **Session dirs missing**: `sessions/` and the session store directory are
|
||||
required to persist history and avoid `ENOENT` crashes.
|
||||
- **Transcript mismatch**: warns when recent session entries have missing
|
||||
|
||||
@@ -143,6 +143,25 @@ Safety:
|
||||
3. Ensure **Local** mode is active and the Gateway is running.
|
||||
4. Install the CLI if you want terminal access.
|
||||
|
||||
## State dir placement (macOS)
|
||||
|
||||
Avoid putting your OpenClaw state dir in iCloud or other cloud-synced folders.
|
||||
Sync-backed paths can add latency and occasionally cause file-lock/sync races for
|
||||
sessions and credentials.
|
||||
|
||||
Prefer a local non-synced state path such as:
|
||||
|
||||
```bash
|
||||
OPENCLAW_STATE_DIR=~/.openclaw
|
||||
```
|
||||
|
||||
If `openclaw doctor` detects state under:
|
||||
|
||||
- `~/Library/Mobile Documents/com~apple~CloudDocs/...`
|
||||
- `~/Library/CloudStorage/...`
|
||||
|
||||
it will warn and recommend moving back to a local path.
|
||||
|
||||
## Build & dev workflow (native)
|
||||
|
||||
- `cd apps/macos && swift build`
|
||||
|
||||
128
src/commands/doctor-state-integrity.cloud-storage.test.ts
Normal file
128
src/commands/doctor-state-integrity.cloud-storage.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { detectMacCloudSyncedStateDir } from "./doctor-state-integrity.js";
|
||||
|
||||
describe("detectMacCloudSyncedStateDir", () => {
|
||||
const home = "/Users/tester";
|
||||
|
||||
it("detects state dir under iCloud Drive", () => {
|
||||
const stateDir = path.join(
|
||||
home,
|
||||
"Library",
|
||||
"Mobile Documents",
|
||||
"com~apple~CloudDocs",
|
||||
"OpenClaw",
|
||||
".openclaw",
|
||||
);
|
||||
|
||||
const result = detectMacCloudSyncedStateDir(stateDir, {
|
||||
platform: "darwin",
|
||||
homedir: home,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: path.resolve(stateDir),
|
||||
storage: "iCloud Drive",
|
||||
});
|
||||
});
|
||||
|
||||
it("detects state dir under Library/CloudStorage", () => {
|
||||
const stateDir = path.join(home, "Library", "CloudStorage", "Dropbox", "OpenClaw", ".openclaw");
|
||||
|
||||
const result = detectMacCloudSyncedStateDir(stateDir, {
|
||||
platform: "darwin",
|
||||
homedir: home,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: path.resolve(stateDir),
|
||||
storage: "CloudStorage provider",
|
||||
});
|
||||
});
|
||||
|
||||
it("detects cloud-synced target when state dir resolves via symlink", () => {
|
||||
const symlinkPath = "/tmp/openclaw-state";
|
||||
const resolvedCloudPath = path.join(
|
||||
home,
|
||||
"Library",
|
||||
"CloudStorage",
|
||||
"OneDrive-Personal",
|
||||
"OpenClaw",
|
||||
".openclaw",
|
||||
);
|
||||
|
||||
const result = detectMacCloudSyncedStateDir(symlinkPath, {
|
||||
platform: "darwin",
|
||||
homedir: home,
|
||||
resolveRealPath: () => resolvedCloudPath,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: path.resolve(resolvedCloudPath),
|
||||
storage: "CloudStorage provider",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores cloud-synced symlink prefix when resolved target is local", () => {
|
||||
const symlinkPath = path.join(
|
||||
home,
|
||||
"Library",
|
||||
"CloudStorage",
|
||||
"OneDrive-Personal",
|
||||
"OpenClaw",
|
||||
".openclaw",
|
||||
);
|
||||
const resolvedLocalPath = path.join(home, ".openclaw");
|
||||
|
||||
const result = detectMacCloudSyncedStateDir(symlinkPath, {
|
||||
platform: "darwin",
|
||||
homedir: home,
|
||||
resolveRealPath: () => resolvedLocalPath,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("anchors cloud detection to OS homedir when OPENCLAW_HOME is overridden", () => {
|
||||
const stateDir = path.join(home, "Library", "CloudStorage", "iCloud Drive", ".openclaw");
|
||||
const originalOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
process.env.OPENCLAW_HOME = "/tmp/openclaw-home-override";
|
||||
const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue(home);
|
||||
try {
|
||||
const result = detectMacCloudSyncedStateDir(stateDir, {
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: path.resolve(stateDir),
|
||||
storage: "CloudStorage provider",
|
||||
});
|
||||
} finally {
|
||||
homedirSpy.mockRestore();
|
||||
if (originalOpenClawHome === undefined) {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
} else {
|
||||
process.env.OPENCLAW_HOME = originalOpenClawHome;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("returns null outside darwin", () => {
|
||||
const stateDir = path.join(
|
||||
home,
|
||||
"Library",
|
||||
"Mobile Documents",
|
||||
"com~apple~CloudDocs",
|
||||
"OpenClaw",
|
||||
".openclaw",
|
||||
);
|
||||
|
||||
const result = detectMacCloudSyncedStateDir(stateDir, {
|
||||
platform: "linux",
|
||||
homedir: home,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -137,6 +137,68 @@ function findOtherStateDirs(stateDir: string): string[] {
|
||||
return found;
|
||||
}
|
||||
|
||||
function isPathUnderRoot(targetPath: string, rootPath: string): boolean {
|
||||
const normalizedTarget = path.resolve(targetPath);
|
||||
const normalizedRoot = path.resolve(rootPath);
|
||||
return (
|
||||
normalizedTarget === normalizedRoot ||
|
||||
normalizedTarget.startsWith(`${normalizedRoot}${path.sep}`)
|
||||
);
|
||||
}
|
||||
|
||||
function tryResolveRealPath(targetPath: string): string | null {
|
||||
try {
|
||||
return fs.realpathSync(targetPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function detectMacCloudSyncedStateDir(
|
||||
stateDir: string,
|
||||
deps?: {
|
||||
platform?: NodeJS.Platform;
|
||||
homedir?: string;
|
||||
resolveRealPath?: (targetPath: string) => string | null;
|
||||
},
|
||||
): {
|
||||
path: string;
|
||||
storage: "iCloud Drive" | "CloudStorage provider";
|
||||
} | null {
|
||||
const platform = deps?.platform ?? process.platform;
|
||||
if (platform !== "darwin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cloud-sync roots should always be anchored to the OS account home on macOS.
|
||||
// OPENCLAW_HOME can relocate app data defaults, but iCloud/CloudStorage remain under the OS home.
|
||||
const homedir = deps?.homedir ?? os.homedir();
|
||||
const roots = [
|
||||
{
|
||||
storage: "iCloud Drive" as const,
|
||||
root: path.join(homedir, "Library", "Mobile Documents", "com~apple~CloudDocs"),
|
||||
},
|
||||
{
|
||||
storage: "CloudStorage provider" as const,
|
||||
root: path.join(homedir, "Library", "CloudStorage"),
|
||||
},
|
||||
];
|
||||
const realPath = (deps?.resolveRealPath ?? tryResolveRealPath)(stateDir);
|
||||
// Prefer the resolved target path when available so symlink prefixes do not
|
||||
// misclassify local state dirs as cloud-synced.
|
||||
const candidates = realPath ? [path.resolve(realPath)] : [path.resolve(stateDir)];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
for (const { storage, root } of roots) {
|
||||
if (isPathUnderRoot(candidate, root)) {
|
||||
return { path: candidate, storage };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
@@ -222,6 +284,18 @@ export async function noteStateIntegrity(
|
||||
const displayStoreDir = shortenHomePath(storeDir);
|
||||
const displayConfigPath = configPath ? shortenHomePath(configPath) : undefined;
|
||||
const requireOAuthDir = shouldRequireOAuthDir(cfg, env);
|
||||
const cloudSyncedStateDir = detectMacCloudSyncedStateDir(stateDir);
|
||||
|
||||
if (cloudSyncedStateDir) {
|
||||
warnings.push(
|
||||
[
|
||||
`- State directory is under macOS cloud-synced storage (${displayStateDir}; ${cloudSyncedStateDir.storage}).`,
|
||||
"- This can cause slow I/O and sync/lock races for sessions and credentials.",
|
||||
"- Prefer a local non-synced state dir (for example: ~/.openclaw).",
|
||||
` Set locally: OPENCLAW_STATE_DIR=~/.openclaw ${formatCliCommand("openclaw doctor")}`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
let stateDirExists = existsDir(stateDir);
|
||||
if (!stateDirExists) {
|
||||
|
||||
Reference in New Issue
Block a user