mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:34:33 +00:00
refactor(core): dedupe final pairing and sandbox media clones
This commit is contained in:
@@ -26,22 +26,40 @@ afterEach(() => {
|
|||||||
childProcessMocks.spawn.mockClear();
|
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", () => {
|
describe("stageSandboxMedia", () => {
|
||||||
it("stages allowed media and blocks unsafe paths", async () => {
|
it("stages allowed media and blocks unsafe paths", async () => {
|
||||||
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
|
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
|
||||||
const cfg = createSandboxMediaStageConfig(home);
|
const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home);
|
||||||
const workspaceDir = join(home, "openclaw");
|
|
||||||
const sandboxDir = join(home, "sandboxes", "session");
|
|
||||||
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
|
|
||||||
workspaceDir: sandboxDir,
|
|
||||||
containerWorkdir: "/work",
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const inboundDir = join(home, ".openclaw", "media", "inbound");
|
const mediaPath = await writeInboundMedia(home, "photo.jpg", "test");
|
||||||
await fs.mkdir(inboundDir, { recursive: true });
|
|
||||||
const mediaPath = join(inboundDir, "photo.jpg");
|
|
||||||
await fs.writeFile(mediaPath, "test");
|
|
||||||
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
|
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
|
||||||
|
|
||||||
await stageSandboxMedia({
|
await stageSandboxMedia({
|
||||||
@@ -105,18 +123,9 @@ describe("stageSandboxMedia", () => {
|
|||||||
|
|
||||||
it("blocks destination symlink escapes when staging into sandbox workspace", async () => {
|
it("blocks destination symlink escapes when staging into sandbox workspace", async () => {
|
||||||
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
|
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
|
||||||
const cfg = createSandboxMediaStageConfig(home);
|
const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home);
|
||||||
const workspaceDir = join(home, "openclaw");
|
|
||||||
const sandboxDir = join(home, "sandboxes", "session");
|
|
||||||
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
|
|
||||||
workspaceDir: sandboxDir,
|
|
||||||
containerWorkdir: "/work",
|
|
||||||
});
|
|
||||||
|
|
||||||
const inboundDir = join(home, ".openclaw", "media", "inbound");
|
const mediaPath = await writeInboundMedia(home, "payload.txt", "PAYLOAD");
|
||||||
await fs.mkdir(inboundDir, { recursive: true });
|
|
||||||
const mediaPath = join(inboundDir, "payload.txt");
|
|
||||||
await fs.writeFile(mediaPath, "PAYLOAD");
|
|
||||||
|
|
||||||
const outsideDir = join(home, "outside");
|
const outsideDir = join(home, "outside");
|
||||||
const outsideInboundDir = join(outsideDir, "inbound");
|
const outsideInboundDir = join(outsideDir, "inbound");
|
||||||
@@ -145,18 +154,13 @@ describe("stageSandboxMedia", () => {
|
|||||||
|
|
||||||
it("skips oversized media staging and keeps original media paths", async () => {
|
it("skips oversized media staging and keeps original media paths", async () => {
|
||||||
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
|
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
|
||||||
const cfg = createSandboxMediaStageConfig(home);
|
const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home);
|
||||||
const workspaceDir = join(home, "openclaw");
|
|
||||||
const sandboxDir = join(home, "sandboxes", "session");
|
|
||||||
vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({
|
|
||||||
workspaceDir: sandboxDir,
|
|
||||||
containerWorkdir: "/work",
|
|
||||||
});
|
|
||||||
|
|
||||||
const inboundDir = join(home, ".openclaw", "media", "inbound");
|
const mediaPath = await writeInboundMedia(
|
||||||
await fs.mkdir(inboundDir, { recursive: true });
|
home,
|
||||||
const mediaPath = join(inboundDir, "oversized.bin");
|
"oversized.bin",
|
||||||
await fs.writeFile(mediaPath, Buffer.alloc(MEDIA_MAX_BYTES + 1, 0x41));
|
Buffer.alloc(MEDIA_MAX_BYTES + 1, 0x41),
|
||||||
|
);
|
||||||
|
|
||||||
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
|
const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath);
|
||||||
await stageSandboxMedia({
|
await stageSandboxMedia({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type AllowFromReadCacheEntry = {
|
|||||||
size: number | null;
|
size: number | null;
|
||||||
entries: string[];
|
entries: string[];
|
||||||
};
|
};
|
||||||
|
type AllowFromStatLike = { mtimeMs: number; size: number } | null;
|
||||||
|
|
||||||
const allowFromReadCache = new Map<string, AllowFromReadCacheEntry>();
|
const allowFromReadCache = new Map<string, AllowFromReadCacheEntry>();
|
||||||
|
|
||||||
@@ -321,6 +322,31 @@ function resolveAllowFromReadCacheHit(params: {
|
|||||||
return cloneAllowFromCacheEntry(cached);
|
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(
|
async function readAllowFromStateForPathWithExists(
|
||||||
channel: PairingChannel,
|
channel: PairingChannel,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
@@ -335,24 +361,9 @@ async function readAllowFromStateForPathWithExists(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = resolveAllowFromReadCacheHit({
|
const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
|
||||||
filePath,
|
if (cachedOrMissing) {
|
||||||
exists: Boolean(stat),
|
return cachedOrMissing;
|
||||||
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 { value, exists } = await readJsonFile<AllowFromStore>(filePath, {
|
const { value, exists } = await readJsonFile<AllowFromStore>(filePath, {
|
||||||
@@ -387,24 +398,9 @@ function readAllowFromStateForPathSyncWithExists(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = resolveAllowFromReadCacheHit({
|
const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
|
||||||
filePath,
|
if (cachedOrMissing) {
|
||||||
exists: Boolean(stat),
|
return cachedOrMissing;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let raw = "";
|
let raw = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user