mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
## Thinking Path > - Paperclip lets people manage AI agents and inspect their work. > - Commentary collects agent suggestions and complaints for company members. > - Commentary used the permission for detailed agent-action audits. > - Owners, admins and viewers could therefore receive a permission error when they opened Commentary. > - This pull request gives commentary its own permission and enables it for all human company roles. > - Members can read feedback while task privacy and detailed audit access keep their own checks. ## Linked Issues or Issue Description Refs #16884, which added the Commentary browser. A search found no open PR for this permission fix. **What happened?** Commentary returned `Missing permission: audit:view_agent_actions` for company members without the detailed audit grant. Default owners, admins and viewers did not receive that grant. **Expected behavior** All human company roles can read commentary by default. A separate `commentary:view` permission controls this access. Private source tasks remain private. Commentary access does not grant detailed agent-action audits or CSV exports. **Steps to reproduce** 1. Sign in as an active company owner, admin or viewer without `audit:view_agent_actions`. 2. Open Audit, then Commentary. 3. Observe the permission error before this change. **Paperclip version or commit** Base commit: `7c58ea07b2`. **Deployment mode** Authenticated server or Cloud tenant. ## What Changed - Add `commentary:view` to the shared permission contract and all human role defaults. - Require that permission for commentary list and detail reads. - Add an idempotent upgrade migration for existing active human members. Preserve existing grants and scopes. - Add the commentary default when approving saved human invitation grants that omit it, including pre-upgrade invitations. Preserve explicit commentary scopes. - Add regression coverage for role defaults, permission removal, audit separation, private tasks and migration replay. - Update the API description, specifications, database notes and access maps. ## Verification - Passed 68 tests across commentary reads, role grants, old-invitation HTTP acceptance, migration replay, agent audit routes, access services and migration snapshot drift under the repository's locked dependencies. - Confirmed the new role and read tests failed before the implementation. - `node scripts/check-maps.mjs` passed. - Migration numbering and safety checks passed during the build. - Full `pnpm -r typecheck` and `pnpm build` passed locally after the invitation fix. - The final-commit CI matrix passed, including all test shards, typecheck, build, public-install checks and canary smoke. The redundant local `pnpm test:run` was stopped after the full CI matrix completed; the 68 focused local regressions passed separately. - The first install bypassed the stale lockfile and selected a newer diff-viewer API. The initial typecheck and build failed in that unrelated plugin. Installing the locked versions cleared both failures; the tracked lockfile is unchanged. - The first locked-dependency regression run hit the 15-second limit while importing the audit route graph in its first test. Moving that import into suite setup fixed the timeout; all 58 regressions then passed. - Greptile found that pre-upgrade invitations retain old grant lists. New helper and HTTP regressions reproduced this gap before the fix, then passed after approval added the missing commentary default. - Greptile reviewed the final commit at 5/5 with no remaining actionable findings. Its earlier thread is resolved. - No UI code changed. The permission tests use real disposable PostgreSQL databases and HTTP routes. ## Risks - The upgrade deliberately enables commentary for every existing active human company member, including viewers. Existing commentary grants retain their saved scopes. - Source-task privacy still filters previews, counts, searches and full messages. Agent-authenticated browsing remains denied. - Agent-action audit grants, detailed filters and CSV export permissions keep their current behavior. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository editing and command execution. The exact runtime model ID and context window are not exposed to 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 `#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>
56 lines
2.6 KiB
TypeScript
56 lines
2.6 KiB
TypeScript
import { Router, type Request } from "express";
|
|
import type { Db } from "@paperclipai/db";
|
|
import { agentCommentaryQuerySchema, isUuidLike } from "@paperclipai/shared";
|
|
import { badRequest, forbidden, notFound } from "../errors.js";
|
|
import { submitAgentCommentary } from "../services/agent-commentary.js";
|
|
import { agentCommentaryReader } from "../services/agent-commentary-reader.js";
|
|
import { issueReadSqlCondition } from "../services/authorization.js";
|
|
import { accessService } from "../services/access.js";
|
|
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
|
|
|
export function agentCommentaryRoutes(db: Db) {
|
|
const router = Router();
|
|
const reader = agentCommentaryReader(db);
|
|
const access = accessService(db);
|
|
|
|
async function authorizeRead(req: Request, companyId: string) {
|
|
assertBoard(req);
|
|
assertCompanyAccess(req, companyId);
|
|
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");
|
|
}
|
|
return issueReadSqlCondition(db, req.actor);
|
|
}
|
|
|
|
router.get("/companies/:companyId/agent-commentary", async (req, res) => {
|
|
const companyId = req.params.companyId as string;
|
|
const readCondition = await authorizeRead(req, companyId);
|
|
const query = agentCommentaryQuerySchema.safeParse(req.query);
|
|
if (!query.success) throw badRequest("Invalid agent commentary filters", query.error.issues);
|
|
res.json(await reader.list(companyId, query.data, readCondition));
|
|
});
|
|
|
|
router.get("/companies/:companyId/agent-commentary/:id", async (req, res) => {
|
|
const companyId = req.params.companyId as string;
|
|
const readCondition = await authorizeRead(req, companyId);
|
|
const id = req.params.id as string;
|
|
if (!isUuidLike(id)) throw notFound("Commentary not found");
|
|
const entry = await reader.get(companyId, id, readCondition);
|
|
if (!entry) throw notFound("Commentary not found");
|
|
res.json(entry);
|
|
});
|
|
router.post("/companies/:companyId/agent-commentary", async (req, res) => {
|
|
const companyId = req.params.companyId as string;
|
|
assertCompanyAccess(req, companyId);
|
|
if (req.actor.type !== "agent" || !req.actor.agentId || !req.actor.runId) {
|
|
throw forbidden("Feedback requires an authenticated agent run");
|
|
}
|
|
const result = await submitAgentCommentary(db, {
|
|
companyId, agentId: req.actor.agentId, runId: req.actor.runId, agentApiKeyId: req.actor.keyId,
|
|
}, req.body);
|
|
res.status(result.replayed ? 200 : 201).json(result);
|
|
});
|
|
return router;
|
|
}
|