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:
@@ -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
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user