Skip to content

Commit 43c5317

Browse files
committed
fix(agents): harden spawn cleanup and patch paths
1 parent cb313d5 commit 43c5317

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

src/agents/acp-spawn.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2735,6 +2735,15 @@ describe("spawnAcpDirect", () => {
27352735
expect(expectFailedSpawn(result, "error").error).toContain("agent dispatch failed");
27362736
expect(relayHandle.dispose).toHaveBeenCalledTimes(1);
27372737
expect(relayHandle.notifyStarted).not.toHaveBeenCalled();
2738+
expect(hoisted.cleanupFailedAcpSpawnMock).toHaveBeenCalledWith(
2739+
expect.objectContaining({
2740+
runtimeCloseHandle: expect.objectContaining({
2741+
handle: expect.objectContaining({
2742+
backend: "acpx",
2743+
}),
2744+
}),
2745+
}),
2746+
);
27382747
});
27392748

27402749
it('rejects streamTo="parent" without requester session context', async () => {

src/agents/acp-spawn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,7 @@ export async function spawnAcpDirect(
14401440
sessionKey,
14411441
shouldDeleteSession: true,
14421442
deleteTranscript: true,
1443+
runtimeCloseHandle: initializedRuntime,
14431444
});
14441445
return createAcpSpawnFailure({
14451446
status: "error",

src/agents/apply-patch.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,38 @@ describe("applyPatch", () => {
409409
});
410410
});
411411

412+
it("rejects move targets whose parent path is a symlink outside cwd", async () => {
413+
if (process.platform === "win32") {
414+
return;
415+
}
416+
await withTempDir(async (dir) => {
417+
const outsideDir = await fs.mkdtemp(path.join(path.dirname(dir), "openclaw-patch-outside-"));
418+
try {
419+
const sourcePath = path.join(dir, "source.txt");
420+
const outsideTarget = path.join(outsideDir, "moved.txt");
421+
const linkDir = path.join(dir, "link");
422+
await fs.writeFile(sourcePath, "before\n", "utf8");
423+
await fs.symlink(outsideDir, linkDir);
424+
425+
const patch = `*** Begin Patch
426+
*** Update File: source.txt
427+
*** Move to: link/moved.txt
428+
@@
429+
-before
430+
+after
431+
*** End Patch`;
432+
433+
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(
434+
/path alias under sandbox root|symlink escapes sandbox root/i,
435+
);
436+
await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe("before\n");
437+
await expectMissingPath(fs.readFile(outsideTarget, "utf8"));
438+
} finally {
439+
await fs.rm(outsideDir, { recursive: true, force: true });
440+
}
441+
});
442+
});
443+
412444
it.runIf(process.platform !== "win32")(
413445
"does not delete out-of-root files when a checked directory is rebound before remove",
414446
async () => {

0 commit comments

Comments
 (0)