mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
fix(server): address agent audit review findings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d370b8e987
commit
a1237f9560
4 files changed
+37
-12
No files matched your search
@@ -128,6 +128,15 @@ describePostgres("agent action audit routes", () => {
|
||||
expect(boardResponse.body.error).toContain("audit:view_agent_actions");
|
||||
});
|
||||
|
||||
it("returns a client error for invalid audit query parameters", async () => {
|
||||
const { company } = await seed();
|
||||
const response = await request(await createApp(db, {
|
||||
type: "board", userId: "local-board", companyIds: [company.id], source: "local_implicit", isInstanceAdmin: false,
|
||||
})).get(`/api/companies/${company.id}/audit/agent-actions?limit=invalid`);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe("Invalid agent action audit query");
|
||||
});
|
||||
|
||||
it("paginates, filters, enriches entities, and falls back to the run responsible user", async () => {
|
||||
const { company, agent, otherAgent, issue, comment, issueDocument, run } = await seed();
|
||||
const app = await createApp(db, { type: "board", userId: "local-board", companyIds: [company.id], source: "local_implicit", isInstanceAdmin: false });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { activityService, normalizeActivityLimit } from "../services/activity.js
|
||||
import { assertAuthenticated, assertBoard, assertCompanyAccess, getAccessibleResource, hasCompanyAccess } from "./authz.js";
|
||||
import { accessService, heartbeatService, issueService } from "../services/index.js";
|
||||
import { sanitizeRecord } from "../redaction.js";
|
||||
import { forbidden } from "../errors.js";
|
||||
import { badRequest, forbidden } from "../errors.js";
|
||||
import { agentActionAuditService } from "../services/agent-action-audit.js";
|
||||
|
||||
const createActivitySchema = z.object({
|
||||
@@ -116,9 +116,12 @@ export function activityRoutes(db: Db) {
|
||||
router.get("/companies/:companyId/audit/agent-actions", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
await assertAgentAuditPermission(req, companyId);
|
||||
const query = agentActionAuditQuerySchema.parse(req.query);
|
||||
const parsedQuery = agentActionAuditQuerySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
throw badRequest("Invalid agent action audit query", parsedQuery.error.issues);
|
||||
}
|
||||
try {
|
||||
res.json(await agentAudit.list({ companyId, ...query }));
|
||||
res.json(await agentAudit.list({ companyId, ...parsedQuery.data }));
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Invalid audit cursor") {
|
||||
res.status(400).json({ error: error.message });
|
||||
|
||||
@@ -65,12 +65,18 @@ export interface LogActivityInput {
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export async function redactActivityDetails(db: Db, details: Record<string, unknown> | null) {
|
||||
if (!details) return null;
|
||||
export async function createActivityDetailsRedactor(db: Db) {
|
||||
const currentUserRedactionOptions = {
|
||||
enabled: (await instanceSettingsService(db).getGeneral()).censorUsernameInLogs,
|
||||
};
|
||||
return redactCurrentUserValue(sanitizeRecord(details), currentUserRedactionOptions);
|
||||
return (details: Record<string, unknown> | null) => (
|
||||
details ? redactCurrentUserValue(sanitizeRecord(details), currentUserRedactionOptions) : null
|
||||
);
|
||||
}
|
||||
|
||||
export async function redactActivityDetails(db: Db, details: Record<string, unknown> | null) {
|
||||
if (!details) return null;
|
||||
return (await createActivityDetailsRedactor(db))(details);
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { and, desc, eq, gte, inArray, isNotNull, lt, lte, or, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { activityLog, heartbeatRuns, issueComments, issueDocuments, issues } from "@paperclipai/db";
|
||||
import { redactActivityDetails } from "./activity-log.js";
|
||||
import { createActivityDetailsRedactor } from "./activity-log.js";
|
||||
|
||||
export interface AgentActionAuditFilters {
|
||||
companyId: string;
|
||||
@@ -49,7 +49,13 @@ export function agentActionAuditService(db: Db) {
|
||||
const effectiveResponsibleUserId = sql<string | null>`coalesce(${activityLog.responsibleUserId}, ${heartbeatRuns.responsibleUserId})`;
|
||||
const conditions = [eq(activityLog.companyId, filters.companyId), isNotNull(activityLog.agentId)];
|
||||
if (filters.agentId) conditions.push(eq(activityLog.agentId, filters.agentId));
|
||||
if (filters.responsibleUserId) conditions.push(eq(effectiveResponsibleUserId, filters.responsibleUserId));
|
||||
if (filters.responsibleUserId) conditions.push(or(
|
||||
eq(activityLog.responsibleUserId, filters.responsibleUserId),
|
||||
and(
|
||||
isNull(activityLog.responsibleUserId),
|
||||
eq(heartbeatRuns.responsibleUserId, filters.responsibleUserId),
|
||||
),
|
||||
)!);
|
||||
if (filters.runId) conditions.push(eq(activityLog.runId, filters.runId));
|
||||
if (filters.entityType) conditions.push(eq(activityLog.entityType, filters.entityType));
|
||||
if (filters.entityId) conditions.push(eq(activityLog.entityId, filters.entityId));
|
||||
@@ -108,7 +114,8 @@ export function agentActionAuditService(db: Db) {
|
||||
documents.set(row.documentId, row);
|
||||
}
|
||||
|
||||
const items = await Promise.all(page.map(async (row) => {
|
||||
const redactDetails = await createActivityDetailsRedactor(db);
|
||||
const items = page.map((row) => {
|
||||
const comment = comments.get(row.entityId);
|
||||
const issue = issueMap.get(row.entityId);
|
||||
const document = documents.get(row.entityId);
|
||||
@@ -119,14 +126,14 @@ export function agentActionAuditService(db: Db) {
|
||||
: issue ? { id: issue.id, identifier: issue.identifier, title: issue.title } : null;
|
||||
return {
|
||||
...row,
|
||||
details: await redactActivityDetails(db, row.details),
|
||||
details: redactDetails(row.details),
|
||||
entity: {
|
||||
issue: issueSnippet,
|
||||
comment: comment ? { id: comment.id, excerpt: excerpt(comment.body) } : null,
|
||||
document: document ? { id: document.documentId, key: document.key } : null,
|
||||
},
|
||||
};
|
||||
}));
|
||||
});
|
||||
const last = page.at(-1);
|
||||
return {
|
||||
items,
|
||||
|
||||
Reference in new issue
Block a user