fix(agent): isolate conversation workspaces
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, rm, stat, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -12,13 +12,18 @@ import {
|
||||
|
||||
describe("ResultReferenceResolver", () => {
|
||||
let tempDir: string;
|
||||
let importRoot: string;
|
||||
let conversationWorkspace: string;
|
||||
let store: ResultReferenceStore;
|
||||
let resolver: ResultReferenceResolver;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "tjwater-result-ref-"));
|
||||
store = new ResultReferenceStore(tempDir, 60_000);
|
||||
resolver = new ResultReferenceResolver(store, tempDir, 1024 * 1024);
|
||||
importRoot = join(tempDir, "conversation-workspaces");
|
||||
conversationWorkspace = join(importRoot, "conversation-1");
|
||||
await mkdir(conversationWorkspace, { recursive: true });
|
||||
store = new ResultReferenceStore(join(tempDir, "refs"), 60_000);
|
||||
resolver = new ResultReferenceResolver(store, importRoot, 1024 * 1024);
|
||||
await store.initialize();
|
||||
});
|
||||
|
||||
@@ -127,7 +132,7 @@ describe("ResultReferenceResolver", () => {
|
||||
});
|
||||
|
||||
it("registers render refs from local wrapper files and normalizes payloads", async () => {
|
||||
const filePath = join(tempDir, "render-wrapper.json");
|
||||
const filePath = join(conversationWorkspace, "render-wrapper.json");
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify(
|
||||
@@ -166,6 +171,7 @@ describe("ResultReferenceResolver", () => {
|
||||
sessionId: "session-3",
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: "trace-3",
|
||||
workspaceDirectory: conversationWorkspace,
|
||||
});
|
||||
|
||||
expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
|
||||
@@ -218,6 +224,7 @@ describe("ResultReferenceResolver", () => {
|
||||
sessionId: "session-4",
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: "trace-4",
|
||||
workspaceDirectory: outsideDir,
|
||||
}),
|
||||
).rejects.toThrow("RESULT_REF_IMPORT_DIR");
|
||||
} finally {
|
||||
@@ -226,9 +233,9 @@ describe("ResultReferenceResolver", () => {
|
||||
});
|
||||
|
||||
it("rejects oversized render payload files before parsing", async () => {
|
||||
const filePath = join(tempDir, "oversized.json");
|
||||
const filePath = join(conversationWorkspace, "oversized.json");
|
||||
await writeFile(filePath, "x".repeat(128), "utf8");
|
||||
const sizeLimitedResolver = new ResultReferenceResolver(store, tempDir, 64);
|
||||
const sizeLimitedResolver = new ResultReferenceResolver(store, importRoot, 64);
|
||||
|
||||
await expect(
|
||||
sizeLimitedResolver.registerRenderPayloadFile(filePath, {
|
||||
@@ -238,8 +245,65 @@ describe("ResultReferenceResolver", () => {
|
||||
sessionId: "session-5",
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: "trace-5",
|
||||
workspaceDirectory: conversationWorkspace,
|
||||
}),
|
||||
).rejects.toThrow("RESULT_REF_IMPORT_MAX_BYTES");
|
||||
});
|
||||
|
||||
it("rejects render payload files owned by another conversation workspace", async () => {
|
||||
const otherWorkspace = join(importRoot, "conversation-2");
|
||||
await mkdir(otherWorkspace);
|
||||
const filePath = join(otherWorkspace, "render-wrapper.json");
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
metadata: {},
|
||||
location: { file_path: filePath },
|
||||
data: { node_area_map: { J1: "DMA-1" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolver.registerRenderPayloadFile(filePath, {
|
||||
actorKey: "actor-6",
|
||||
clientSessionId: "client-6",
|
||||
projectKey: "project-key-6",
|
||||
sessionId: "session-6",
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: "trace-6",
|
||||
workspaceDirectory: conversationWorkspace,
|
||||
}),
|
||||
).rejects.toThrow("current conversation workspace");
|
||||
});
|
||||
|
||||
it("rejects a conversation workspace that is itself a symbolic link", async () => {
|
||||
const targetWorkspace = join(importRoot, "conversation-target");
|
||||
const linkedWorkspace = join(importRoot, "conversation-linked");
|
||||
await mkdir(targetWorkspace);
|
||||
await symlink(targetWorkspace, linkedWorkspace, "dir");
|
||||
const filePath = join(targetWorkspace, "render-wrapper.json");
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
metadata: {},
|
||||
location: { file_path: filePath },
|
||||
data: { node_area_map: { J1: "DMA-1" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolver.registerRenderPayloadFile(filePath, {
|
||||
actorKey: "actor-7",
|
||||
clientSessionId: "client-7",
|
||||
projectKey: "project-key-7",
|
||||
sessionId: "session-7",
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: "trace-7",
|
||||
workspaceDirectory: linkedWorkspace,
|
||||
}),
|
||||
).rejects.toThrow("symbolic link");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user