mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Operators need an audit record of agent actions across tasks, comments, documents, approvals, and runs > - The permission-gated audit read API provides that record, but operators cannot inspect it in the product > - A readable UI must preserve company boundaries, server-side permission decisions, and redaction > - Audit exports must also be safe to open in spreadsheet software and must record the export itself > - This pull request adds company and per-agent audit views plus a guarded CSV export > - The benefit is a searchable, filterable, and reviewable agent action history with direct links back to work ## Linked Issues or Issue Description **Feature.** This change adds the frontend and CSV export for the agent action audit log. Refs #9731 and #9735. - Problem: agent actions are recorded, but operators have no readable product surface to inspect or export them. - Solution: add a company audit page and a per-agent Audit tab that use the permission-gated audit API. - Alternative: build a separate plugin-only surface. This was rejected because the existing permission model already supports a unified, server-authoritative view. This pull request targets the audit epic branch, which contains the merged #9735 audit API. ## What Changed - Added a company Audit page and sidebar entry. - Added a per-agent Audit tab with a fixed agent filter. - Added filters for agent, responsible user, action domain, entity type, and date range. - Added task and run links, responsible-user context, cursor pagination, and readable action text. - Added a permission-denied Enterprise card for callers without `audit:view_agent_actions`. - Added a CSV export that is permission-gated, capped, self-audited, CSV-escaped, and protected against spreadsheet formula injection. - Preserved the merged audit API cursor validation, redaction, and sub-millisecond pagination behavior. ## Verification - `pnpm exec vitest run ui/src/pages/audit/AuditFeed.test.tsx` — 6 passed. - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/agent-action-audit-routes.test.ts` — 8 passed with embedded PostgreSQL. - `pnpm -r typecheck` — passed across all workspaces. - `pnpm build` — passed across all workspaces. - `pnpm test:run` — all completed shards passed except one environment-sensitive CLI assertion caused by injected static AWS credential variables; the exact test passes 8/8 with those variables unset. - Manual Chromium QA exercised the populated feed, active filters, permission-denied card, per-agent tab, and CSV export. ## Screenshots and Manual QA - [All audit states exercised in Chromium](https://github.com/paperclipai/paperclip/pull/9744#issuecomment-4998997001) - [Detailed browser report and per-agent tab root cause](https://github.com/paperclipai/paperclip/pull/9744#issuecomment-4998771061) The per-agent redirect defect found during QA is fixed in this branch. ## Risks Low to moderate risk. The UI and export route are additive and use the existing company-scoped permission gate. The main risks are large exports and spreadsheet interpretation. The export is capped at 10,000 rows, records truncation accurately, and prefixes formula-like cells as text. There are no schema changes or migrations. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 4.8, 1M context, extended thinking, tool use, and code execution produced the original implementation. - OpenAI Codex, GPT-5 (deployment ID and context window not exposed), reasoning, tool use, code execution, browser-test orchestration, and GitHub review tooling repaired and verified the pull request. ## 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 `#NNN` / `github.com/paperclipai/paperclip` URLs) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [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> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import { api } from "./client";
|
|
|
|
/**
|
|
* Agent audit API client.
|
|
*
|
|
* Consumes the unified read API shipped by Phase 2c
|
|
* (`server/src/routes/activity.ts` → `services/agent-action-audit.ts`):
|
|
* GET /companies/:companyId/audit/agent-actions
|
|
* Gated server-side by the `audit:view_agent_actions` board permission — the
|
|
* client renders an upsell/permission-denied state when the request 403s
|
|
* (see `ui/src/pages/CompanyAudit.tsx`). CSV export streams from the sibling
|
|
* `.csv` endpoint, which logs the export action itself.
|
|
*/
|
|
|
|
/** Render-ready entity snippets attached to each row at read time (no N+1). */
|
|
export interface AuditEntitySnippet {
|
|
issue: { id: string; identifier: string | null; title: string | null } | null;
|
|
comment: { id: string; excerpt: string } | null;
|
|
document: { id: string; key: string } | null;
|
|
}
|
|
|
|
/** One enriched `activity_log` row from the agent-action audit feed. */
|
|
export interface AuditActionRecord {
|
|
id: string;
|
|
companyId: string;
|
|
actorType: "agent" | "user" | "system" | "plugin" | null;
|
|
actorId: string | null;
|
|
action: string;
|
|
entityType: string;
|
|
entityId: string;
|
|
agentId: string | null;
|
|
runId: string | null;
|
|
responsibleUserId: string | null;
|
|
details: Record<string, unknown> | null;
|
|
createdAt: string;
|
|
entity: AuditEntitySnippet;
|
|
}
|
|
|
|
export interface AuditActionsResponse {
|
|
items: AuditActionRecord[];
|
|
nextCursor: string | null;
|
|
}
|
|
|
|
/** Server-side filters for the audit feed. All optional. */
|
|
export interface AuditActionFilters {
|
|
agentId?: string | null;
|
|
responsibleUserId?: string | null;
|
|
runId?: string | null;
|
|
entityType?: string | null;
|
|
entityId?: string | null;
|
|
/** Action-domain prefix, e.g. `issue.` or `issue.comment_added`. */
|
|
action?: string | null;
|
|
/** ISO-8601 with offset. */
|
|
from?: string | null;
|
|
to?: string | null;
|
|
actorType?: "agent" | "user" | "system" | "plugin" | null;
|
|
cursor?: string | null;
|
|
limit?: number;
|
|
}
|
|
|
|
function buildAuditQuery(filters: AuditActionFilters): URLSearchParams {
|
|
const search = new URLSearchParams();
|
|
if (filters.agentId) search.set("agentId", filters.agentId);
|
|
if (filters.responsibleUserId) search.set("responsibleUserId", filters.responsibleUserId);
|
|
if (filters.runId) search.set("runId", filters.runId);
|
|
if (filters.entityType) search.set("entityType", filters.entityType);
|
|
if (filters.entityId) search.set("entityId", filters.entityId);
|
|
if (filters.action) search.set("action", filters.action);
|
|
if (filters.from) search.set("from", filters.from);
|
|
if (filters.to) search.set("to", filters.to);
|
|
if (filters.actorType) search.set("actorType", filters.actorType);
|
|
if (filters.cursor) search.set("cursor", filters.cursor);
|
|
if (filters.limit != null) search.set("limit", String(filters.limit));
|
|
return search;
|
|
}
|
|
|
|
export const auditApi = {
|
|
/**
|
|
* Cursor-paginated agent-action feed. Returns `{ items, nextCursor }`.
|
|
* Throws `ApiError` with status 403 when the caller lacks
|
|
* `audit:view_agent_actions`.
|
|
*/
|
|
listAgentActions: (companyId: string, filters: AuditActionFilters = {}) => {
|
|
const search = buildAuditQuery(filters);
|
|
const qs = search.toString();
|
|
return api.get<AuditActionsResponse>(
|
|
`/companies/${companyId}/audit/agent-actions${qs ? `?${qs}` : ""}`,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Fetch the filtered feed as a CSV blob. The server logs an `audit.exported`
|
|
* activity row for the export itself (training-data export precedent).
|
|
*/
|
|
exportAgentActionsCsv: async (
|
|
companyId: string,
|
|
filters: Omit<AuditActionFilters, "cursor" | "limit"> = {},
|
|
): Promise<Blob> => {
|
|
const search = buildAuditQuery(filters);
|
|
const qs = search.toString();
|
|
const res = await fetch(
|
|
`/api/companies/${companyId}/audit/agent-actions.csv${qs ? `?${qs}` : ""}`,
|
|
{ credentials: "include", headers: { Accept: "text/csv" } },
|
|
);
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => null);
|
|
const message = (body as { error?: string } | null)?.error ?? `Export failed: ${res.status}`;
|
|
throw new Error(message);
|
|
}
|
|
return res.blob();
|
|
},
|
|
};
|