refactor(core): dedupe final pairing and sandbox media clones

This commit is contained in:
Peter Steinberger 2026-03-02 21:36:13 +00:00
parent 453a1c179d
commit 5897eed6e9
2 changed files with 69 additions and 69 deletions

View File

@ -26,22 +26,40 @@ afterEach(() => {
childProcessMocks.spawn.mockClear();
});
function setupSandboxWorkspace(home: string): {
cfg: ReturnType<typeof createSandboxMediaStageConfig>;
workspaceDir: string;
sandboxDir: string;
} {
const cfg = createSandboxMediaStageConfig(home);
const workspaceDir = join(home, "openclaw");
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
return { cfg, workspaceDir, sandboxDir };
}
async function writeInboundMedia(
home: string,
fileName: string,
payload: string | Buffer,
): Promise<string> {
const inboundDir = join(home, ".openclaw", "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
const mediaPath = join(inboundDir, fileName);
await fs.writeFile(mediaPath, payload);
return mediaPath;
}
describe("stageSandboxMedia", () => {
it("stages allowed media and blocks unsafe paths", async () => {
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
const cfg = createSandboxMediaStageConfig(home);
const workspaceDir = join(home, "openclaw");
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home);
{
const inboundDir = join(home, ".openclaw", "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
const mediaPath = join(inboundDir, "photo.jpg");
await fs.writeFile(mediaPath, "test");
const mediaPath = await writeInboundMedia(home, "photo.jpg", "test");
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
await stageSandboxMedia({
@ -105,18 +123,9 @@ describe("stageSandboxMedia", () => {
it("blocks destination symlink escapes when staging into sandbox workspace", async () => {
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
const cfg = createSandboxMediaStageConfig(home);
const workspaceDir = join(home, "openclaw");
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home);
const inboundDir = join(home, ".openclaw", "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
const mediaPath = join(inboundDir, "payload.txt");
await fs.writeFile(mediaPath, "PAYLOAD");
const mediaPath = await writeInboundMedia(home, "payload.txt", "PAYLOAD");
const outsideDir = join(home, "outside");
const outsideInboundDir = join(outsideDir, "inbound");
@ -145,18 +154,13 @@ describe("stageSandboxMedia", () => {
it("skips oversized media staging and keeps original media paths", async () => {
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
const cfg = createSandboxMediaStageConfig(home);
const workspaceDir = join(home, "openclaw");
const sandboxDir = join(home, "sandboxes", "session");
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
workspaceDir: sandboxDir,
containerWorkdir: "/work",
});
const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home);
const inboundDir = join(home, ".openclaw", "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
const mediaPath = join(inboundDir, "oversized.bin");
await fs.writeFile(mediaPath, Buffer.alloc(MEDIA_MAX_BYTES + 1, 0x41));
const mediaPath = await writeInboundMedia(
home,
"oversized.bin",
Buffer.alloc(MEDIA_MAX_BYTES + 1, 0x41),
);
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
await stageSandboxMedia({

View File

@ -30,6 +30,7 @@ type AllowFromReadCacheEntry = {
size: number | null;
entries: string[];
};
type AllowFromStatLike = { mtimeMs: number; size: number } | null;
const allowFromReadCache = new Map<string, AllowFromReadCacheEntry>();
@ -321,6 +322,31 @@ function resolveAllowFromReadCacheHit(params: {
return cloneAllowFromCacheEntry(cached);
}
function resolveAllowFromReadCacheOrMissing(
filePath: string,
stat: AllowFromStatLike,
): { entries: string[]; exists: boolean } | null {
const cached = resolveAllowFromReadCacheHit({
filePath,
exists: Boolean(stat),
mtimeMs: stat?.mtimeMs ?? null,
size: stat?.size ?? null,
});
if (cached) {
return { entries: cached.entries, exists: cached.exists };
}
if (!stat) {
setAllowFromReadCache(filePath, {
exists: false,
mtimeMs: null,
size: null,
entries: [],
});
return { entries: [], exists: false };
}
return null;
}
async function readAllowFromStateForPathWithExists(
channel: PairingChannel,
filePath: string,
@ -335,24 +361,9 @@ async function readAllowFromStateForPathWithExists(
}
}
const cached = resolveAllowFromReadCacheHit({
filePath,
exists: Boolean(stat),
mtimeMs: stat?.mtimeMs ?? null,
size: stat?.size ?? null,
});
if (cached) {
return { entries: cached.entries, exists: cached.exists };
}
if (!stat) {
setAllowFromReadCache(filePath, {
exists: false,
mtimeMs: null,
size: null,
entries: [],
});
return { entries: [], exists: false };
const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
if (cachedOrMissing) {
return cachedOrMissing;
}
const { value, exists } = await readJsonFile<AllowFromStore>(filePath, {
@ -387,24 +398,9 @@ function readAllowFromStateForPathSyncWithExists(
}
}
const cached = resolveAllowFromReadCacheHit({
filePath,
exists: Boolean(stat),
mtimeMs: stat?.mtimeMs ?? null,
size: stat?.size ?? null,
});
if (cached) {
return { entries: cached.entries, exists: cached.exists };
}
if (!stat) {
setAllowFromReadCache(filePath, {
exists: false,
mtimeMs: null,
size: null,
entries: [],
});
return { entries: [], exists: false };
const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
if (cachedOrMissing) {
return cachedOrMissing;
}
let raw = "";