mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(commentary): tell an agent denied a commentary read where a person can see it
Agents can submit commentary but not read it. A denied agent got only
"Board access required", so it could name the feed but not link it. The
403 now carries a remediation naming /<prefix>/activity/commentary for
the agent's own company. Cross-company requests stay a plain 403.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4279d57aa9)
This commit is contained in:
1 parent
4c9bb36abb
commit
288257a052
2 files changed
+26
-3
No files matched your search
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { activityLog, agentApiKeys, agentCommentary, agents, authUsers, companyMemberships, heartbeatRuns, issues, nativeRunResults } from "@paperclipai/db";
|
||||
import { activityLog, agentApiKeys, agentCommentary, agents, authUsers, companies, companyMemberships, heartbeatRuns, issues, nativeRunResults } from "@paperclipai/db";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
|
||||
import { submitAgentCommentary } from "../services/agent-commentary.js";
|
||||
@@ -125,6 +125,20 @@ describe("internal agent commentary through both transports", () => {
|
||||
expect(await rows(f)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("tells an agent denied a commentary read where a person can see it, for its own company only", async () => {
|
||||
const f = await legacy();
|
||||
const [company] = await server.db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, f.companyId));
|
||||
const read = (companyId: string) => fetch(`${server.apiUrl}/api/companies/${companyId}/agent-commentary`, { headers: { Authorization: `Bearer ${f.token}` } });
|
||||
const own = await read(f.companyId);
|
||||
expect(own.status).toBe(403);
|
||||
const body = await own.json() as { error: string; remediation?: string };
|
||||
expect(body.error).toBe("Board access required");
|
||||
expect(body.remediation).toContain(`/${company!.issuePrefix}/activity/commentary`);
|
||||
const foreign = await read(f.foreignCompanyId);
|
||||
expect(foreign.status).toBe(403);
|
||||
expect(((await foreign.json()) as { remediation?: string }).remediation).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(["cancelled", "succeeded", "failed"])("rejects submissions and replay after the run is %s", async (status) => {
|
||||
const f = await legacy();
|
||||
expect((await post(f, { ...input, kind: "complaint" })).status).toBe(201);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router, type Request } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { companies, type Db } from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { agentCommentaryQuerySchema, isUuidLike } from "@paperclipai/shared";
|
||||
import { badRequest, forbidden, notFound } from "../errors.js";
|
||||
import { submitAgentCommentary } from "../services/agent-commentary.js";
|
||||
@@ -14,8 +15,16 @@ export function agentCommentaryRoutes(db: Db) {
|
||||
const access = accessService(db);
|
||||
|
||||
async function authorizeRead(req: Request, companyId: string) {
|
||||
assertBoard(req);
|
||||
assertCompanyAccess(req, companyId);
|
||||
if (req.actor.type !== "board") {
|
||||
// Agents can submit commentary but never read it. Name the human page so
|
||||
// an agent can send the person somewhere useful instead of a dead end.
|
||||
const [company] = await db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, companyId)).limit(1);
|
||||
throw forbidden("Board access required", company ? {
|
||||
remediation: `Agent commentary is readable only by people with commentary access. Send the person to /${company.issuePrefix}/activity/commentary.`,
|
||||
} : undefined);
|
||||
}
|
||||
assertBoard(req);
|
||||
if (req.actor.source !== "local_implicit" && !req.actor.isInstanceAdmin
|
||||
&& (!req.actor.userId || !await access.canUser(companyId, req.actor.userId, "commentary:view"))) {
|
||||
throw forbidden("Missing permission: commentary:view");
|
||||
|
||||
Reference in new issue
Block a user