fix: preserve sandbox write payload stdin (#43876)

Merged via squash.

Prepared head SHA: a10fd4b21c78ec57411e6a4f387f16b1441660c2
Co-authored-by: glitch418x <189487110+glitch418x@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
glitch
2026-03-12 12:42:57 +03:00
committed by GitHub
parent f640326e31
commit 8ea79b64d0
5 changed files with 66 additions and 3 deletions

View File

@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- Mattermost/block streaming: fix duplicate message delivery (one threaded, one top-level) when block streaming is active by excluding `replyToId` from the block reply dedup key and adding an explicit `threading` dock to the Mattermost plugin. (#41362) Thanks @mathiasnagler and @vincentkoc.
- BlueBubbles/self-chat echo dedupe: drop reflected duplicate webhook copies only when a matching `fromMe` event was just seen for the same chat, body, and timestamp, preventing self-chat loops without broad webhook suppression. Related to #32166. (#38442) Thanks @vincentkoc.
- Models/Kimi Coding: send `anthropic-messages` tools in native Anthropic format again so `kimi-coding` stops degrading tool calls into XML/plain-text pseudo invocations instead of real `tool_use` blocks. (#38669, #39907, #40552) Thanks @opriz.
- Sandbox/write: preserve pinned mutation-helper payload stdin so sandboxed `write` no longer reports success while creating empty files. (#43876) Thanks @glitch418x.
## 2026.3.11

View File

@@ -3,7 +3,10 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { SANDBOX_PINNED_MUTATION_PYTHON } from "./fs-bridge-mutation-helper.js";
import {
buildPinnedWritePlan,
SANDBOX_PINNED_MUTATION_PYTHON,
} from "./fs-bridge-mutation-helper.js";
async function withTempRoot<T>(prefix: string, run: (root: string) => Promise<T>): Promise<T> {
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
@@ -22,6 +25,35 @@ function runMutation(args: string[], input?: string) {
});
}
function runWritePlan(args: string[], input?: string) {
const plan = buildPinnedWritePlan({
check: {
target: {
hostPath: args[1] ?? "",
containerPath: args[1] ?? "",
relativePath: path.posix.join(args[2] ?? "", args[3] ?? ""),
writable: true,
},
options: {
action: "write files",
requireWritable: true,
},
},
pinned: {
mountRootPath: args[1] ?? "",
relativeParentPath: args[2] ?? "",
basename: args[3] ?? "",
},
mkdir: args[4] === "1",
});
return spawnSync("sh", ["-c", plan.script, "moltbot-sandbox-fs", ...(plan.args ?? [])], {
input,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
});
}
describe("sandbox pinned mutation helper", () => {
it("writes through a pinned directory fd", async () => {
await withTempRoot("openclaw-mutation-helper-", async (root) => {
@@ -37,6 +69,26 @@ describe("sandbox pinned mutation helper", () => {
});
});
it.runIf(process.platform !== "win32")(
"preserves stdin payload bytes when the pinned write plan runs through sh",
async () => {
await withTempRoot("openclaw-mutation-helper-", async (root) => {
const workspace = path.join(root, "workspace");
await fs.mkdir(workspace, { recursive: true });
const result = runWritePlan(
["write", workspace, "nested/deeper", "note.txt", "1"],
"hello",
);
expect(result.status).toBe(0);
await expect(
fs.readFile(path.join(workspace, "nested", "deeper", "note.txt"), "utf8"),
).resolves.toBe("hello");
});
},
);
it.runIf(process.platform !== "win32")(
"rejects symlink-parent writes instead of materializing a temp file outside the mount",
async () => {

View File

@@ -257,7 +257,13 @@ function buildPinnedMutationPlan(params: {
return {
checks: params.checks,
recheckBeforeCommand: true,
script: ["set -eu", "python3 - \"$@\" <<'PY'", SANDBOX_PINNED_MUTATION_PYTHON, "PY"].join("\n"),
// Feed the helper source over fd 3 so stdin stays available for write payload bytes.
script: [
"set -eu",
"python3 /dev/fd/3 \"$@\" 3<<'PY'",
SANDBOX_PINNED_MUTATION_PYTHON,
"PY",
].join("\n"),
args: params.args,
};
}

View File

@@ -120,7 +120,7 @@ describe("sandbox fs bridge anchored ops", () => {
const opCall = mockedExecDockerRaw.mock.calls.find(
([args]) =>
typeof args[5] === "string" &&
args[5].includes("python3 - \"$@\" <<'PY'") &&
args[5].includes("python3 /dev/fd/3 \"$@\" 3<<'PY'") &&
getDockerArg(args, 1) === testCase.expectedArgs[0],
);
expect(opCall).toBeDefined();

View File

@@ -129,6 +129,10 @@ describe("sandbox fs bridge shell compatibility", () => {
await bridge.writeFile({ filePath: "b.txt", data: "hello" });
const scripts = getScriptsFromCalls();
expect(scripts.some((script) => script.includes("python3 - \"$@\" <<'PY'"))).toBe(false);
expect(scripts.some((script) => script.includes("python3 /dev/fd/3 \"$@\" 3<<'PY'"))).toBe(
true,
);
expect(scripts.some((script) => script.includes('cat >"$1"'))).toBe(false);
expect(scripts.some((script) => script.includes('cat >"$tmp"'))).toBe(false);
expect(scripts.some((script) => script.includes("os.replace("))).toBe(true);