diff --git a/nemoclaw/src/blueprint/snapshot.ts b/nemoclaw/src/blueprint/snapshot.ts index d711f68808..c1675cd990 100644 --- a/nemoclaw/src/blueprint/snapshot.ts +++ b/nemoclaw/src/blueprint/snapshot.ts @@ -92,7 +92,36 @@ export async function restoreIntoSandbox( ["sandbox", "cp", source, `${sandboxName}:/sandbox/.openclaw`], { reject: false }, ); - return result.exitCode === 0; + if (result.exitCode !== 0) { + return false; + } + + // Files copied via `openshell sandbox cp` land as root:root because + // the helper runs as root inside the pod. /sandbox/.openclaw is the + // immutable gateway-config layer, but the symlinks under it point at + // /sandbox/.openclaw-data (the writable side), so the copied agent + // workspace and per-agent runtime dirs end up unwritable by the + // sandbox user. That broke writes to models.json, agent state, and + // workspace markdown files. Fix it with a best-effort recursive + // chown after the cp succeeds. We deliberately keep this best-effort + // (don't fail the restore if the chown fails) so a future runtime + // that already gets ownership right doesn't trip on a missing + // chown binary or a tightened exec policy. See #1229. + await execa( + "openshell", + [ + "sandbox", + "exec", + sandboxName, + "--", + "chown", + "-R", + "sandbox:sandbox", + "/sandbox/.openclaw-data", + ], + { reject: false }, + ); + return true; } export function cutoverHost(): boolean {