From 80f59f675b469972d101e77eb06f89551443cd6d Mon Sep 17 00:00:00 2001 From: ColinM-sys Date: Thu, 9 Apr 2026 00:28:38 -0400 Subject: [PATCH] fix(blueprint): chown copied files to sandbox user after restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `openshell sandbox cp` runs as root inside the pod, so files copied into /sandbox/.openclaw via restoreIntoSandbox land as root:root. The symlinks under /sandbox/.openclaw point at the writable /sandbox/.openclaw-data tree, so without an explicit chown the 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 with `EACCES: permission denied`. After a successful cp, run a best-effort recursive `chown -R sandbox:sandbox /sandbox/.openclaw-data` via `openshell sandbox exec`. The chown is intentionally best-effort (its failure does NOT flip the restore result) so a future runtime that already gets ownership right won't break the migration. Tests: snapshot.test.ts has pre-existing vitest 4.x mocking breakage on main (8/16 tests fail before this change, same 8/16 fail after — no new regressions). The mock-fs harness needs a separate fix; once that lands, regression tests can be added that assert the chown call shape and the best-effort semantics. Refs: #1229 Signed-off-by: ColinM-sys --- nemoclaw/src/blueprint/snapshot.ts | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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 {