mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent Chat uses the shared task surface and browser panel. > - An unsaved chat has a temporary ID instead of a stored task UUID. > - The browser poll sent that ID to a PostgreSQL UUID query and returned a server error. > - This pull request waits for a saved task and validates task IDs before the query. > - Saved chats keep their browser tools and access checks. ## Linked Issues or Issue Description Refs #14331. That report describes the same draft-ID problem in email requests. This change covers the browser routes; it does not change email behavior. Opening an unsaved Agent Chat sends `chat:<agent-id>` to `/issues/:issueId/browsers` every three seconds. PostgreSQL rejects this as a task UUID. The draft is only a UI view model. The first send or upload creates its stored task. ## What Changed - Disable browser polling for unsaved chat IDs. Resume polling when the chat has a task UUID. - Return the existing task-not-found response for malformed task IDs before database access. Keep board authentication first and company and credential checks unchanged. - Test all six task browser routes, draft-to-saved polling, normal tasks, saved chats, and denied access. Keep canonical UUID v4, v7, and nil values queryable. - Document the draft and saved-chat browser contract. ## Verification - `pnpm install --frozen-lockfile` passed with pnpm 9.15.4 and Node 24.21.0. - `pnpm exec vitest run server/src/__tests__/browser-use-route-scope.test.ts server/src/__tests__/browser-use-connection.test.ts` passed all 35 tests. - `pnpm exec vitest run ui/src/hooks/useTaskBrowsers.test.ts` passed all 5 tests. - Independent review passed 14 hook and route tests plus both real task and saved-chat authorization cases. - `pnpm check:token-gates` and `git diff --check` passed. - Full workspace `pnpm -r typecheck` and `pnpm build` passed. - Full Linux CI passed on `dafff44a32`: 53 successful checks and two intentional Storybook skips. This includes all general and serialized test groups, UI and browser tests, typecheck, build, and the canary dry run. No CI retries were needed. - The full local `TMPDIR=/private/tmp pnpm test:run` started but did not complete. It was stopped after full Linux CI passed. Before the stop, three company-skills cache cases failed on macOS. A focused rerun reproduced all three as `EACCES` while renaming immutable cache staging directories. The test, company-skills service, and runtime cache files are byte-identical to the baseline previously reproduced on `e99854249c` for #15291. Other local groups were not reached; the full Linux CI run provides aggregate coverage. - Greptile scored the exact head `dafff44a32` 5/5 with no findings or unresolved review threads. ## Risks - Malformed task IDs now return 404 instead of reaching PostgreSQL and returning 500. The route does not map draft IDs to agents or tasks. - No schema, browser provider, task creation, or authorization policy changes. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository editing, code execution, and independent agent review. The exact deployment model ID and context window are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub references) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass (focused suites; full local run limitations are reported above) - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
74 lines
3.3 KiB
TypeScript
74 lines
3.3 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import express from "express";
|
|
import http from "supertest";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { Db } from "@paperclipai/db";
|
|
import { errorHandler } from "../middleware/error-handler.js";
|
|
import { browserUseRoutes } from "../routes/browser-use.js";
|
|
|
|
const routes = [
|
|
{ method: "get", suffix: "" },
|
|
{ method: "get", suffix: "/viewer" },
|
|
{ method: "post", suffix: "/presence" },
|
|
{ method: "post", suffix: "/viewport" },
|
|
{ method: "post", suffix: "/viewport/release" },
|
|
{ method: "post", suffix: "/control" },
|
|
] as const;
|
|
|
|
describe("browser task route scope", () => {
|
|
it.each([
|
|
"12345678-1234-4234-8234-123456789abc",
|
|
"12345678-1234-7234-8234-123456789ABC",
|
|
"00000000-0000-0000-0000-000000000000",
|
|
])("keeps canonical PostgreSQL task UUIDs queryable: %s", async (id) => {
|
|
const companyId = randomUUID();
|
|
const limit = vi.fn().mockResolvedValue([{ companyId }]);
|
|
const db = { select: () => ({ from: () => ({ where: () => ({ limit }) }) }) } as unknown as Db;
|
|
const list = vi.fn().mockResolvedValue([]);
|
|
const service = { list } as unknown as NonNullable<Parameters<typeof browserUseRoutes>[1]>;
|
|
const app = express();
|
|
app.use((req, _res, next) => {
|
|
req.actor = { type: "board", userId: "browser-reviewer", source: "session", companyIds: [companyId] };
|
|
next();
|
|
});
|
|
app.use(browserUseRoutes(db, service));
|
|
app.use(errorHandler);
|
|
expect((await http(app).get(`/issues/${id}/browsers`)).status).toBe(200);
|
|
expect(limit).toHaveBeenCalledExactlyOnceWith(1);
|
|
expect(list).toHaveBeenCalledExactlyOnceWith(companyId, id, "browser-reviewer");
|
|
});
|
|
|
|
it.each(routes)("rejects invalid task IDs before any query: $method $suffix", async ({ method, suffix }) => {
|
|
const select = vi.fn(() => { throw new Error("Invalid scope reached the database"); });
|
|
const db = { select } as unknown as Db;
|
|
const service = Object.fromEntries(
|
|
["list", "viewer", "presence", "resize", "releaseViewport", "humanSession", "control"]
|
|
.map((name) => [name, vi.fn()]),
|
|
) as unknown as NonNullable<Parameters<typeof browserUseRoutes>[1]>;
|
|
let actorType: "board" | "agent" | "none" = "board";
|
|
const app = express();
|
|
app.use((req, _res, next) => {
|
|
req.actor = actorType === "board"
|
|
? { type: "board", userId: "browser-reviewer", source: "session", companyIds: [] }
|
|
: actorType === "agent"
|
|
? { type: "agent", companyId: randomUUID(), agentId: randomUUID(), source: "agent_key" }
|
|
: { type: "none" };
|
|
next();
|
|
});
|
|
app.use(browserUseRoutes(db, service));
|
|
app.use(errorHandler);
|
|
const id = randomUUID();
|
|
for (const invalidId of [`chat:${id}`, "not-a-uuid", `${id}'`, ` ${id}`, `${id}\n`]) {
|
|
const path = `/issues/${encodeURIComponent(invalidId)}/browsers${suffix ? `/${id}${suffix}` : ""}`;
|
|
for (const type of ["board", "agent", "none"] as const) {
|
|
actorType = type;
|
|
const response = await http(app)[method](path);
|
|
expect(response.status).toBe(type === "board" ? 404 : 403);
|
|
expect(response.body).toEqual({ error: type === "board" ? "Task not found" : "Board access required" });
|
|
}
|
|
}
|
|
expect(select).not.toHaveBeenCalled();
|
|
for (const handler of Object.values(service)) expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
});
|