mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(agents): give the terminate denial a ready link to the agent's profile
An agent refused termination relayed the profile path as plain text. The denial now carries a Markdown link to /<prefix>/agents/<id>, so the agent can send the person straight to the Terminate control. POST /terminate now resolves the agent before the board check, so the link names the target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff9ee74c46
commit
952636e1d2
2 files changed
+13
-6
No files matched your search
@@ -244,6 +244,7 @@ function createDbStub(options: { requireBoardApprovalForNewAgents?: boolean } =
|
||||
Promise.resolve(resolve([{
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: options.requireBoardApprovalForNewAgents ?? false,
|
||||
}])),
|
||||
),
|
||||
@@ -1725,7 +1726,7 @@ describe("agent permission routes", () => {
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("only a board user can terminate an agent");
|
||||
expect(res.body.error).toContain("agent's profile");
|
||||
expect(res.body.error).toContain(`[the agent's profile](/PAP/agents/${agentId})`);
|
||||
expect(mockAgentService.terminate).not.toHaveBeenCalled();
|
||||
expect(mockAgentService.update).not.toHaveBeenCalled();
|
||||
expect(mockHeartbeatService.cancelInvocationsForAgents).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1777,10 +1777,16 @@ export function agentRoutes(
|
||||
};
|
||||
}
|
||||
|
||||
/** Terminating an agent is a board-only governed action; tell other actors where a person can do it. */
|
||||
function assertBoardCanTerminateAgent(req: Request) {
|
||||
/**
|
||||
* Terminating an agent is a board-only governed action. The denial gives
|
||||
* other actors a ready profile link, so they can send a person to the control.
|
||||
*/
|
||||
async function assertBoardCanTerminateAgent(req: Request, target: { id: string; companyId: string }) {
|
||||
if (req.actor.type === "board") return;
|
||||
throw forbidden("Board access required: only a board user can terminate an agent, using Terminate in the actions menu on the agent's profile.");
|
||||
const [company] = await db.select({ issuePrefix: companies.issuePrefix }).from(companies)
|
||||
.where(eq(companies.id, target.companyId));
|
||||
const profile = company?.issuePrefix ? `[the agent's profile](/${company.issuePrefix}/agents/${target.id})` : "the agent's profile";
|
||||
throw forbidden(`Board access required: only a board user can terminate an agent. Ask a person with board access to open ${profile} and choose Terminate from its actions menu.`);
|
||||
}
|
||||
|
||||
async function assertCanUpdateAgent(req: Request, targetAgent: { id: string; companyId: string }) {
|
||||
@@ -5513,7 +5519,7 @@ export function agentRoutes(
|
||||
delete patchData.status;
|
||||
if (requestedStatus !== undefined && !["paused", "idle", "terminated"].includes(String(requestedStatus))) throw badRequest("Use an agent lifecycle command to change status");
|
||||
// Termination is board-only and irreversible on every route, not just POST /terminate.
|
||||
if (requestedStatus === "terminated") assertBoardCanTerminateAgent(req);
|
||||
if (requestedStatus === "terminated") await assertBoardCanTerminateAgent(req, existing);
|
||||
const replaceAdapterConfig = patchData.replaceAdapterConfig === true;
|
||||
delete patchData.replaceAdapterConfig;
|
||||
// The apply-existing flag is not an agent column. The server binds the fixed
|
||||
@@ -5951,12 +5957,12 @@ export function agentRoutes(
|
||||
});
|
||||
|
||||
router.post("/agents/:id/terminate", async (req, res) => {
|
||||
assertBoardCanTerminateAgent(req);
|
||||
const id = req.params.id as string;
|
||||
const existing = await getAccessibleAgent(req, res, id);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
await assertBoardCanTerminateAgent(req, existing);
|
||||
|
||||
const agent = existing.status === "pending_approval"
|
||||
? (await approvalsSvc.rejectHire(id, req.actor.userId ?? "board"))?.agent
|
||||
|
||||
Reference in new issue
Block a user