mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(authz): say every plugin management step is board-only in plugin denials (#20207)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - People ask a chat agent to do work, and some of that work is board-only, such as plugin management > - When an agent gets a board-only denial, the denial tells it to stop and reply with a link to the board page > - The denial only spoke about the one route that was denied. An agent that read the plugin list first was not told that plugin configuration and secret binding are also board-only > - The agent then searched the API catalog for an agent route to configure the plugin. It searched for 110 seconds, and its reply came after the chat window closed > - This pull request adds one fact to every board-only denial on a plugin route: all plugin management steps have no agent route > - The benefit is that the agent replies at once with the plugin settings link, and does not search for a route that does not exist ## Linked Issues or Issue Description Refs #20182 (the plugin denial link and "reply now" text that this builds on). **What happened** In the chat eval `plugins.configure-plugin`, the person asked the agent to finish configuring the preview plugin. The agent called `GET /api/plugins` and got the board-only denial. Its reasoning said: "Denial final. But ... perhaps the preview plugin is configured through a different route accessible to agents". It then made 14 more catalog searches for plugin config, credential and secret-binding routes. The run did not reply before the 150 second chat window closed. **Expected behavior** After a board-only denial on any plugin route, the agent replies now. It says that a board user must configure the plugin, and it links the plugin settings page. **Steps to reproduce** 1. Install a plugin that has a required secret setting, and do not configure it. 2. In chat, ask an agent: "Finish configuring the preview plugin." 3. The agent calls `GET /api/plugins` and gets 403 "Board access required". 4. Before this change, the denial does not say that configuration and secret binding are also board-only, and the agent searches for another route. ## What Changed - `server/src/routes/authz.ts`: a board-only denial for an agent on any `/api/plugins` route adds one sentence. The sentence says that listing, installing, configuring (including choosing a secret for a plugin setting), enabling, disabling, upgrading and removing plugins have no agent route. - Other board-only denials do not change. Plugin tools that are assigned to an agent are not mentioned, so tool use is not discouraged. - `server/src/__tests__/board-access-denial.test.ts`: a regression test for the plugin list and plugin config denials, and a negative check for a non-plugin denial. ## Verification - `cd server && npx vitest run src/__tests__/board-access-denial.test.ts` passes (26 tests). - Without the `authz.ts` change, the new test fails. - The chat eval `plugins.configure-plugin` is rerun on a case branch that includes this commit. The result is in the PR comments. ## Risks - Low risk. The change only adds text to the 403 body that agents get. People and board actors get the same body as before. - The sentence is a fact about the current routes. Every plugin management route uses `assertBoard`, `assertBoardOrgAccess` or `assertInstanceAdmin`. If a plugin management route becomes agent-accessible later, this sentence must change too. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution in Claude Code. ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6fd46c0025
commit
09e24dd12c
2 files changed
+25
-2
No files matched your search
@@ -58,6 +58,19 @@ describe("board-only denial", () => {
|
||||
expect(res.body.remediation).toContain(link);
|
||||
});
|
||||
|
||||
it("tells an agent denied any plugin route that every plugin management step is board-only", async () => {
|
||||
// An agent asked to finish configuring a plugin usually reads the plugin list first.
|
||||
// That denial must also settle configuration and secret binding, or the agent keeps
|
||||
// searching the catalog for an agent route to them.
|
||||
for (const path of ["/api/plugins", `/api/plugins/${AGENT_ID}/config`]) {
|
||||
const res = await request(createApp(agentActor)).get(path).expect(403);
|
||||
expect(res.body.remediation).toContain("Plugin management is board-only throughout");
|
||||
expect(res.body.remediation).toContain("configuring (including choosing a secret for a plugin setting)");
|
||||
}
|
||||
const other = await request(createApp(agentActor)).post(`/api/companies/${COMPANY_ID}/secrets`).expect(403);
|
||||
expect(other.body.remediation).not.toContain("Plugin management");
|
||||
});
|
||||
|
||||
it("links the resolved project when an agent names the project by its short reference", async () => {
|
||||
const app = express();
|
||||
app.use((req, _res, next) => {
|
||||
|
||||
@@ -81,8 +81,17 @@ function pluginControlPage(path: string): [string, string] | null {
|
||||
: ["plugin settings", "/<prefix>/company/settings/instance/plugins"];
|
||||
}
|
||||
|
||||
// Every plugin management route is board-only, so a denial on any one of them
|
||||
// (often the list an agent reads first) also answers the plugin's other management steps.
|
||||
const PLUGIN_ROUTE = /^\/plugins(?:\/|$)/;
|
||||
const PLUGIN_ADMINISTRATION_SCOPE = "Plugin management is board-only throughout: listing, installing, configuring (including choosing a secret for a plugin setting), enabling, disabling, upgrading and removing plugins have no agent route either.";
|
||||
|
||||
function deniedApiPath(req: Request) {
|
||||
return (req.originalUrl ?? "").split("?")[0]!.replace(/^\/api(?=\/)/, "");
|
||||
}
|
||||
|
||||
function boardControlPage(req: Request) {
|
||||
const path = (req.originalUrl ?? "").split("?")[0]!.replace(/^\/api(?=\/)/, "");
|
||||
const path = deniedApiPath(req);
|
||||
const pluginPage = pluginControlPage(path);
|
||||
if (pluginPage) return pluginPage;
|
||||
for (const [pattern, page] of BOARD_CONTROL_PAGES) {
|
||||
@@ -99,12 +108,13 @@ function boardControlPage(req: Request) {
|
||||
export function boardAccessRequired(req: Request) {
|
||||
if (req.actor.type !== "agent") return forbidden("Board access required");
|
||||
const page = boardControlPage(req);
|
||||
const scope = PLUGIN_ROUTE.test(deniedApiPath(req)) ? `${PLUGIN_ADMINISTRATION_SCOPE} ` : "";
|
||||
const where = page
|
||||
? `They can do it in the Paperclip web app: link [Open ${page[0]}](${page[1]}), with this conversation's company prefix as <prefix>.`
|
||||
: "They can do it in the Paperclip web app or with their own board credentials. Link a page only if you already know its route; do not search for one.";
|
||||
return forbidden("Board access required", {
|
||||
code: BOARD_ACCESS_REQUIRED_CODE,
|
||||
remediation: `This action is board-only by design and this denial is final: no agent credential, tool or other API route can do it, so do not search for one. Stop working on this step and reply now: tell the person that a board user must do it. ${where}`,
|
||||
remediation: `This action is board-only by design and this denial is final: no agent credential, tool or other API route can do it, so do not search for one. Stop working on this step and reply now: tell the person that a board user must do it. ${scope}${where}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user