mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
fix(server): tell agents who owns a plugin-managed routine and where it is changed (#20409)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A plugin can create and own routines ("plugin-managed routines").
The routine row shows the plugin in `managedByPlugin`.
> - An agent reads routines through `GET` and `PATCH
/api/routines/{id}`. These API catalog entries had no description.
> - So an agent that read `managedByPlugin` did not know what it meant
or where a person changes the routine.
> - In chat evals, the agent searched for managed-routine routes and
probed plugin files for over four minutes. It did not reply.
> - This pull request adds one shared description to both catalog
entries. It says the plugin owns the definition, links the board's
plugin settings page, and tells the agent not to rewrite, detach or
duplicate the routine.
> - The benefit is that the agent can answer at once and send the person
to the correct page.
## Linked Issues or Issue Description
No public issue exists. This is a bug report.
**What happened**
A person asked the CEO agent in chat to rewrite a routine that a plugin
manages. The agent read the routine and saw `managedByPlugin`. No tool
result or catalog entry told it what that field means. The agent
searched the API catalog ("plugin managed routine", "managedByPlugin",
"detach plugin managed routine"), tried to read plugin UI files, and
listed agents. After more than four minutes it had not replied, and the
chat timed out. This happened in two separate runs.
**Expected behavior**
The agent replies promptly. It says that the plugin owns the routine,
does not rewrite or copy it, and links the plugin settings page where a
board user changes what the plugin exposes.
**Steps to reproduce**
1. Install a plugin that declares a managed routine (`routines.managed`
capability, top-level `routines[]`).
2. Open a chat with the CEO and select the routine.
3. Ask: "Rewrite this plugin's daily maintenance routine."
4. Look at the agent's `search_api` results for `GET /api/routines/{id}`
and `PATCH /api/routines/{id}`. They have no description.
## What Changed
- `server/src/routes/openapi.ts`: add a shared
`PLUGIN_MANAGED_ROUTINE_GUIDANCE` description to `GET
/api/routines/{id}` and `PATCH /api/routines/{id}`. It names
`managedByPlugin`, links `[plugin
settings](/<prefix>/company/settings/instance/plugins/<pluginId>)`, and
tells agents to reply with that link instead of rewriting, detaching or
duplicating the routine. The `PATCH` entry also states the existing
agent rule (own routines only, no reassignment).
- `server/src/__tests__/plugin-managed-routine-guidance.test.ts`: new
regression test. The queries the agent actually used now find these
entries with the guidance, and the linked settings route is mounted in
`ui/src/App.tsx`.
## Verification
- Red then green: `cd server && npx vitest run
src/__tests__/plugin-managed-routine-guidance.test.ts` failed 6/6 before
the change and passes 6/6 after.
- `cd server && npx vitest run
src/services/native-runtime/runner-api.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/person-only-page-guidance.test.ts
src/__tests__/case-history-api-discovery.test.ts
src/__tests__/skill-import-discovery.test.ts` passes.
- `cd server && npx tsc --noEmit -p .` passes.
- Live chat eval `routines.plugin-managed-routine` on a branch with this
commit: see the comment on this PR for the run.
## Risks
- Low risk. The change adds documentation text to two API catalog
entries. Routes and permissions do not change.
- The catalog search scores description words, so these two entries can
rank a little higher for queries about plugins or managed resources.
## Model Used
- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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
e1010e7678
commit
1c868b86be
2 files changed
+86
-2
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, it } from "vitest";
|
||||
import { buildOpenApiDocument, applyPluginSettingsVisibility } from "../routes/openapi.js";
|
||||
import { runnerApiCatalog, searchRunnerApi } from "../services/native-runtime/runner-api-catalog.js";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const settingsLink = "[plugin settings](/<prefix>/company/settings/instance/plugins/<pluginId>)";
|
||||
|
||||
// Queries an agent searched after reading a routine whose `managedByPlugin` was set.
|
||||
it.each([
|
||||
"plugin managed routine",
|
||||
"plugin routine managed definition",
|
||||
"managedByPlugin",
|
||||
"detach plugin managed routine",
|
||||
"update routine",
|
||||
])("search %j explains who owns a plugin-managed routine and links its settings", (query) => {
|
||||
const found = searchRunnerApi({ query, limit: 10 }).results
|
||||
.filter(row => row.path === "/api/routines/{id}" && (row.method === "GET" || row.method === "PATCH"));
|
||||
expect(found.length, query).toBeGreaterThan(0);
|
||||
for (const operation of found) {
|
||||
expect(operation.description).toContain("managedByPlugin");
|
||||
expect(operation.description).toContain(settingsLink);
|
||||
expect(operation.description).toContain("Do not rewrite, detach or duplicate");
|
||||
}
|
||||
});
|
||||
|
||||
it("links a plugin settings page the board UI mounts", () => {
|
||||
const appRoutes = readFileSync(path.join(repoRoot, "ui/src/App.tsx"), "utf8");
|
||||
expect(appRoutes).toContain('path="company/settings/instance/plugins/:pluginId"');
|
||||
});
|
||||
|
||||
it.each(["GET /api/routines/{id}", "PATCH /api/routines/{id}"])("%s carries the full plugin-managed routine guidance", (operationId) => {
|
||||
const description = runnerApiCatalog().find(row => row.operationId === operationId)?.description ?? "";
|
||||
expect(description).toContain("managedByPlugin");
|
||||
expect(description).toContain(settingsLink);
|
||||
expect(description).toContain("Do not rewrite, detach or duplicate");
|
||||
expect(description).toContain("reply now with that link");
|
||||
});
|
||||
|
||||
it("names the hosting operator instead of linking plugin settings when the operator hides them", () => {
|
||||
const document = applyPluginSettingsVisibility(buildOpenApiDocument(), new Set(["instance.plugins"]));
|
||||
for (const operation of [document.paths["/api/routines/{id}"].get, document.paths["/api/routines/{id}"].patch]) {
|
||||
expect(operation.description).not.toContain("/company/settings/instance/plugins");
|
||||
expect(operation.description).toContain("the hosting operator controls this plugin's settings");
|
||||
expect(operation.description).toContain("Do not rewrite, detach or duplicate");
|
||||
}
|
||||
const visible = applyPluginSettingsVisibility(buildOpenApiDocument(), new Set());
|
||||
expect(visible.paths["/api/routines/{id}"].get.description).toContain(settingsLink);
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { Router } from "express";
|
||||
import { subscriptionPriceSchema, mergeSubscriptionsSchema } from "@paperclipai/shared";
|
||||
import { z } from "zod";
|
||||
import { getHiddenSettings } from "../services/settings-visibility.js";
|
||||
import {
|
||||
gitHubRepositoryPageQuerySchema,
|
||||
toggleAllGitHubRepositoriesSchema,
|
||||
@@ -5341,11 +5342,42 @@ registry.registerPath({
|
||||
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
||||
});
|
||||
|
||||
// A plugin-managed routine is still an ordinary routine row, so agents read it
|
||||
// through the routine routes; say there who owns it and where it is changed.
|
||||
const PLUGIN_MANAGED_ROUTINE_GUIDANCE =
|
||||
"A routine with a non-null `managedByPlugin` is a plugin-managed routine: that plugin created it and owns its definition "
|
||||
+ "(title, instructions, schedule, assignee), and the plugin can reset it to its manifest defaults. "
|
||||
+ "It is changed through the settings that plugin exposes, which a board user opens at "
|
||||
+ "[plugin settings](/<prefix>/company/settings/instance/plugins/<pluginId>), with `managedByPlugin.pluginId` as <pluginId> "
|
||||
+ "and this conversation's company prefix as <prefix>. "
|
||||
+ "Do not rewrite, detach or duplicate a plugin-managed routine to imitate a change, and do not search for another route: "
|
||||
+ "reply now with that link and the routine's page link.";
|
||||
// When the hosting operator hides plugin management, that settings page redirects
|
||||
// away, so the guidance names the operator instead of linking it.
|
||||
const PLUGIN_MANAGED_ROUTINE_OPERATOR_GUIDANCE =
|
||||
"A routine with a non-null `managedByPlugin` is a plugin-managed routine: that plugin created it and owns its definition "
|
||||
+ "(title, instructions, schedule, assignee), and the plugin can reset it to its manifest defaults. "
|
||||
+ "On this instance the hosting operator manages plugins, so no Paperclip page offers that change. "
|
||||
+ "Do not rewrite, detach or duplicate a plugin-managed routine to imitate a change, and do not search for another route: "
|
||||
+ "reply now that the hosting operator controls this plugin's settings, with the routine's page link.";
|
||||
|
||||
/** Picks the plugin-managed routine guidance that matches plugin settings visibility. */
|
||||
export function applyPluginSettingsVisibility(document: any, hidden: ReadonlySet<string> = getHiddenSettings()): any {
|
||||
if (!hidden.has("instance.plugins")) return document;
|
||||
for (const operation of [document.paths?.["/api/routines/{id}"]?.get, document.paths?.["/api/routines/{id}"]?.patch]) {
|
||||
if (typeof operation?.description === "string") {
|
||||
operation.description = operation.description.replace(PLUGIN_MANAGED_ROUTINE_GUIDANCE, PLUGIN_MANAGED_ROUTINE_OPERATOR_GUIDANCE);
|
||||
}
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/api/routines/{id}",
|
||||
tags: ["routines"],
|
||||
summary: "Get a routine",
|
||||
description: `Routine details including triggers. ${PLUGIN_MANAGED_ROUTINE_GUIDANCE}`,
|
||||
request: { params: z.object({ id: z.string() }) },
|
||||
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
||||
});
|
||||
@@ -5355,6 +5387,7 @@ registry.registerPath({
|
||||
path: "/api/routines/{id}",
|
||||
tags: ["routines"],
|
||||
summary: "Update a routine",
|
||||
description: `Agents can update only routines assigned to them and cannot reassign them. ${PLUGIN_MANAGED_ROUTINE_GUIDANCE}`,
|
||||
request: {
|
||||
params: z.object({ id: z.string() }),
|
||||
body: jsonBody(updateRoutineSchema),
|
||||
@@ -13357,7 +13390,7 @@ for (const [method, path, body] of experimentalApiPaths) {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function buildOpenApiDocument(): any {
|
||||
return applyDocumentFixups({
|
||||
return applyPluginSettingsVisibility(applyDocumentFixups({
|
||||
openapi: "3.0.0",
|
||||
info: {
|
||||
title: "Paperclip API",
|
||||
@@ -13367,7 +13400,7 @@ export function buildOpenApiDocument(): any {
|
||||
servers: [{ url: "/" }],
|
||||
components: registry.buildComponents(),
|
||||
paths: registry.buildPaths(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export const buildOpenApiSpec = buildOpenApiDocument;
|
||||
|
||||
Reference in new issue
Block a user