feat(agent): add controlled task execution
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import express from "express";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { request as httpRequest, type Server } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { buildChatRouter } from "../../src/routes/chat.js";
|
||||
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
|
||||
import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
describe("chat session HTTP isolation", () => {
|
||||
let metadataDir: string;
|
||||
let server: Server;
|
||||
|
||||
beforeEach(async () => {
|
||||
metadataDir = await mkdtemp(join(tmpdir(), "tjwater-route-sessions-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (server) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
await rm(metadataDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
it("hides task sessions from ordinary history and scopes every list to the verified user and project", async () => {
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
const authorization = headers.get("authorization");
|
||||
const projectId = headers.get("x-project-id");
|
||||
const userId = authorization === "Bearer token-a" ? "user-a" : "user-b";
|
||||
return Response.json({
|
||||
user_id: userId,
|
||||
project_id: projectId,
|
||||
network: "supply",
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const sessionMetadataStore = new SessionMetadataStore(metadataDir);
|
||||
await sessionMetadataStore.initialize();
|
||||
const ownerContext = {
|
||||
actorKey: toActorKey("product:user-a"),
|
||||
projectId: "10000000-0000-4000-8000-000000000001",
|
||||
projectKey: toProjectKey("product:10000000-0000-4000-8000-000000000001"),
|
||||
userId: "user-a",
|
||||
};
|
||||
await sessionMetadataStore.ensure({
|
||||
...ownerContext,
|
||||
sessionId: "ordinary-session",
|
||||
});
|
||||
await sessionMetadataStore.ensure({
|
||||
...ownerContext,
|
||||
sessionId: "execution-session",
|
||||
purpose: "task_execution",
|
||||
taskId: "50000000-0000-4000-8000-000000000001",
|
||||
requestId: "60000000-0000-4000-8000-000000000001",
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api/v1/agent/chat", buildChatRouter({
|
||||
frontendActionCoordinator: {} as never,
|
||||
learningOrchestrator: {} as never,
|
||||
memoryStore: {} as never,
|
||||
resultReferenceResolver: {} as never,
|
||||
runtime: {} as never,
|
||||
sessionBridge: {} as never,
|
||||
sessionMetadataStore,
|
||||
sessionTranscriptStore: {} as never,
|
||||
sessionUiStateStore: {} as never,
|
||||
}));
|
||||
server = app.listen(0, "127.0.0.1");
|
||||
await new Promise<void>((resolve) => server.once("listening", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("expected HTTP server address");
|
||||
|
||||
const ownedOrdinary = await getJson(address.port, "/api/v1/agent/chat/sessions", "token-a", ownerContext.projectId);
|
||||
const ownedExecution = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", ownerContext.projectId);
|
||||
const otherUser = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-b", ownerContext.projectId);
|
||||
const otherProject = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", "10000000-0000-4000-8000-000000000002");
|
||||
|
||||
expect(ownedOrdinary.body.sessions.map((item: { id: string }) => item.id)).toEqual(["ordinary-session"]);
|
||||
expect(ownedExecution.body.sessions.map((item: { id: string }) => item.id)).toEqual(["execution-session"]);
|
||||
expect(otherUser.body.sessions).toEqual([]);
|
||||
expect(otherProject.body.sessions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
const getJson = (
|
||||
port: number,
|
||||
path: string,
|
||||
token: string,
|
||||
projectId: string,
|
||||
) => new Promise<{ status: number; body: Record<string, any> }>((resolve, reject) => {
|
||||
const request = httpRequest({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
path,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Project-Id": projectId,
|
||||
},
|
||||
}, (response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
response.on("end", () => {
|
||||
resolve({
|
||||
status: response.statusCode ?? 0,
|
||||
body: JSON.parse(Buffer.concat(chunks).toString("utf8")),
|
||||
});
|
||||
});
|
||||
});
|
||||
request.on("error", reject);
|
||||
request.end();
|
||||
});
|
||||
Reference in New Issue
Block a user