From 39e3daa1682df7ac28a70e15eac279ee2c636901 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 29 May 2026 17:40:35 +0530 Subject: [PATCH] fix(trajectory): preserve safe path checks for window writes --- src/trajectory/runtime.test.ts | 24 ++++++++++++++++++++++++ src/trajectory/runtime.ts | 12 ++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/trajectory/runtime.test.ts b/src/trajectory/runtime.test.ts index 5e5c38422e26..ec50d0bfe5db 100644 --- a/src/trajectory/runtime.test.ts +++ b/src/trajectory/runtime.test.ts @@ -228,6 +228,30 @@ describe("trajectory runtime", () => { expect(raw).toContain("new-recorder"); }); + it.runIf(process.platform !== "win32")( + "refuses runtime capture through symlinked parent directories", + async () => { + const tmpDir = makeTempDir(); + const targetDir = path.join(tmpDir, "target"); + const linkDir = path.join(tmpDir, "link"); + fs.mkdirSync(targetDir); + fs.symlinkSync(targetDir, linkDir); + const recorder = createTrajectoryRuntimeRecorder({ + sessionId: "session-1", + sessionFile: path.join(linkDir, "session.jsonl"), + maxRuntimeFileBytes: 2_400, + }); + + const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder); + runtimeRecorder.recordEvent("prompt.submitted", { + prompt: "hello", + }); + await runtimeRecorder.flush(); + + expect(fs.existsSync(path.join(targetDir, "session.trajectory.jsonl"))).toBe(false); + }, + ); + it("describes queued writer state for cleanup timeout logs", () => { const recorder = createTrajectoryRuntimeRecorder({ sessionId: "session-1", diff --git a/src/trajectory/runtime.ts b/src/trajectory/runtime.ts index 99d8d636a3cc..9e3a36c53530 100644 --- a/src/trajectory/runtime.ts +++ b/src/trajectory/runtime.ts @@ -6,7 +6,7 @@ import type { QueuedFileWriterDiagnostics, } from "../agents/queued-file-writer.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { writeSiblingTempFile } from "../infra/fs-safe-advanced.js"; +import { assertNoSymlinkParents, writeSiblingTempFile } from "../infra/fs-safe-advanced.js"; import { readRegularFileSync } from "../infra/fs-safe.js"; import { redactSecrets } from "../logging/redact.js"; import { parseBooleanValue } from "../utils/boolean.js"; @@ -283,10 +283,18 @@ async function replaceTrajectoryWindow(params: { appendedLines: string[]; }): Promise { const dir = path.dirname(params.filePath); + await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 }); + await assertNoSymlinkParents({ + rootDir: path.parse(path.resolve(dir)).root, + targetPath: path.resolve(dir), + allowMissing: false, + allowRootChildSymlink: true, + requireDirectories: true, + messagePrefix: "Refusing to write trajectory under", + }); const lines = readTrajectoryWindowLines(params.filePath, params.maxFileBytes); lines.push(...params.appendedLines); trimJsonlWindow(lines, params.maxFileBytes); - await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 }); await writeSiblingTempFile({ dir, chmodDir: false,