openclaw/src/hooks/bundled/workspace-sync/handler.ts
Ash Brener 7e0cd65156 feat: add workspace cloud sync via rclone
Add bidirectional workspace synchronization with cloud storage providers
(Dropbox, Google Drive, OneDrive, S3/R2) using rclone.

Features:
- Config-driven setup via workspace.sync in moltbot.json
- Native CLI: moltbot workspace setup/sync/status/authorize/list
- Session hooks: onSessionStart/onSessionEnd triggers (no LLM cost)
- Background interval sync via gateway (no LLM cost)
- Auto-install rclone when missing (Homebrew/official script)
- Dropbox app folder support for ringfenced security
- Secure defaults: syncs shared/ subdirectory only

New files:
- src/infra/rclone.ts - rclone wrapper
- src/cli/workspace-cli.ts - CLI commands
- src/hooks/bundled/workspace-sync/ - session hooks
- src/gateway/workspace-sync-manager.ts - background sync
- src/config/types.workspace.ts - config types
- docs/gateway/workspace-sync.md - full documentation

Tests: 23 unit tests for rclone helpers and hook handler
2026-01-28 10:53:04 +02:00

119 lines
3.3 KiB
TypeScript

/**
* Workspace sync hook handler
*
* Syncs workspace with cloud storage on session start/end
* when workspace.sync is configured.
*/
import type { MoltbotConfig } from "../../../config/config.js";
import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
import type { HookHandler } from "../../hooks.js";
import {
isRcloneInstalled,
isRcloneConfigured,
resolveSyncConfig,
runBisync,
} from "../../../infra/rclone.js";
/**
* Sync workspace on session start or end
*/
const workspaceSyncHandler: HookHandler = async (event) => {
// Only handle session events
if (event.type !== "session") {
return;
}
const context = event.context || {};
const cfg = context.cfg as MoltbotConfig | undefined;
if (!cfg?.workspace?.sync) {
return;
}
const syncConfig = cfg.workspace.sync;
// Check if sync is enabled for this event
const isStart = event.action === "start";
const isEnd = event.action === "end" || event.action === "stop";
if (isStart && !syncConfig.onSessionStart) {
return;
}
if (isEnd && !syncConfig.onSessionEnd) {
return;
}
if (!isStart && !isEnd) {
return;
}
// Check if provider is configured
if (!syncConfig.provider || syncConfig.provider === "off") {
return;
}
console.log(`[workspace-sync] Triggered on session ${event.action}`);
try {
// Check if rclone is installed
const installed = await isRcloneInstalled();
if (!installed) {
console.warn("[workspace-sync] rclone not installed, skipping sync");
return;
}
// Resolve workspace and config
const agentId = resolveAgentIdFromSessionKey(event.sessionKey);
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const stateDir = context.stateDir as string | undefined;
const resolved = resolveSyncConfig(syncConfig, workspaceDir, stateDir);
// Check if rclone is configured
if (!isRcloneConfigured(resolved.configPath, resolved.remoteName)) {
console.warn(
`[workspace-sync] rclone not configured for remote "${resolved.remoteName}", skipping sync`,
);
return;
}
console.log(
`[workspace-sync] Syncing ${resolved.remoteName}:${resolved.remotePath} <-> ${resolved.localPath}`,
);
// Run sync
const result = await runBisync({
configPath: resolved.configPath,
remoteName: resolved.remoteName,
remotePath: resolved.remotePath,
localPath: resolved.localPath,
conflictResolve: resolved.conflictResolve,
exclude: resolved.exclude,
verbose: false,
});
if (result.ok) {
console.log("[workspace-sync] Sync completed successfully");
if (result.filesTransferred) {
console.log(`[workspace-sync] Files transferred: ${result.filesTransferred}`);
}
} else {
// Check if this is a first-run issue
if (result.error?.includes("--resync")) {
console.warn(
"[workspace-sync] First sync requires manual --resync. Run: moltbot workspace sync --resync",
);
} else {
console.error(`[workspace-sync] Sync failed: ${result.error}`);
}
}
} catch (err) {
console.error("[workspace-sync] Error:", err instanceof Error ? err.message : String(err));
}
};
export default workspaceSyncHandler;