mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Operators need one activity feed for human, agent, plugin, and system changes > - The existing audit endpoint returns only rows that have agent attribution > - The full audit view also requires a dedicated permission > - This pull request adds an explicit all-actors scope with basic and privileged access tiers > - The benefit is that company members can inspect the shared activity history while sensitive attribution and export controls stay protected ## Linked Issues or Issue Description **What existing behavior does this improve?** The company audit activity endpoint and the board audit route. **Subsystem affected** Server REST API and board UI routing/API contracts. **Current behavior** The agent-action audit endpoint excludes activity without an agent ID. It also rejects company members who do not have the full audit permission. **Proposed behavior** Callers can opt into `actorScope=all`. A company member receives all actor kinds with sensitive attribution fields removed. A permitted board user receives complete rows and can use attribution filters. The default scope and CSV permission remain unchanged. **Reason and benefit** The board needs one chronological activity source for user, agent, plugin, and system actions. A two-tier response keeps the feed useful without widening access to detailed attribution or export capabilities. **Breaking changes** None. The endpoint keeps the existing agent-only scope and permission behavior by default. ## What Changed - Added `actorScope=all` to the unified audit query and included activity from every actor type. - Added a company-readable basic tier that removes run, responsible-user, agent, and details attribution. - Kept attribution filters and CSV export behind `audit:view_agent_actions`. - Added route and integration coverage for basic readers, permitted readers, pagination, filter denial, and all actor kinds. - Added the missing unprefixed `/audit` redirect and company route classification. ## Verification - `pnpm exec vitest run server/src/__tests__/activity-routes.test.ts server/src/__tests__/agent-action-audit-routes.test.ts ui/src/lib/company-routes.test.ts --reporter=verbose` (35 tests passed) - `pnpm -r typecheck` - `pnpm test:run` - `pnpm build` ## Risks - The all-actors query can return more rows than the legacy agent-only query. Cursor pagination and existing limits bound each request. - The basic tier intentionally exposes action and actor-kind context. It removes detailed run, agent, responsible-user, and details attribution. - The legacy endpoint behavior remains the default, which reduces compatibility risk. > The roadmap marks activity log and action attribution as shipped. This change improves that existing capability and does not introduce a separate workflow system. ## Model Used - OpenAI Codex, `gpt-5.6-sol`, 114K context, agentic reasoning with tool use and code execution. ## 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) - [x] 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>
119 lines
4.4 KiB
TypeScript
119 lines
4.4 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
|
|
* The default agent scope is gated server-side by the
|
|
* `audit:view_agent_actions` board permission. The explicit all-actors scope
|
|
* also supports a company-read basic tier with attribution stripped. CSV
|
|
* export streams from the permission-gated 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;
|
|
/** Controls whether attribution filters and CSV export are available. */
|
|
accessTier: "basic" | "full";
|
|
}
|
|
|
|
/** Server-side filters for the audit feed. All optional. */
|
|
export interface AuditActionFilters {
|
|
/** Defaults to `agents`; `all` opts into the unified all-actors feed. */
|
|
actorScope?: "agents" | "all";
|
|
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.actorScope) search.set("actorScope", filters.actorScope);
|
|
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 activity/audit feed. The default agent scope throws a
|
|
* 403 without `audit:view_agent_actions`; `actorScope: "all"` returns a
|
|
* redacted basic tier to ordinary company readers.
|
|
*/
|
|
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();
|
|
},
|
|
};
|