Preserve creator retry access while draft computers stop

Treat stopping setup drafts as creator-configurable so users with agent creation permission can retry cleanup and update their selection. Keep hire completion blocked until stopping finishes, and preserve other users configuration denials.

Verified all 66 lifecycle and policy tests, server TypeScript, and map checks. The new route case covers owner success, other-creator denial, and premature completion refusal.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-10-26 10:23:27 -07:00
1 parent 50b5a08af3
commit 43be3c93dd
4 files changed
+27 -3

No files matched your search

+1 -1
View File
@@ -59,7 +59,7 @@ Normal agent-authored hiring keeps its existing approval policy.
An agent-owned plugin may prepare the draft and leave its lifecycle in
`verifying` while that setup hold remains. Only bounded Core-issued setup probes
can run there. The creator's company creation permission also covers configuring
and retrying their draft during preparation; other actors still require the
and retrying their draft during preparation or stopping; other actors still require the
ordinary configuration permission. This is a board wizard path, not new agent
execution or probe authority. Lifecycle and component regressions cover these
transitions; no permitted-agent or live-provider journey is claimed here.
@@ -233,6 +233,30 @@ const support = await getEmbeddedPostgresTestSupport();
expect((await current(setup.id)).lifecycleHolds).toEqual(["setup"]);
});
it("lets only the setup creator configure and retry a stopping draft with creation permission", async () => {
await db.insert(companyMemberships).values(["setup-owner", "setup-other"].map(principalId => ({ companyId,
principalType: "user", principalId, membershipRole: "member" })));
await db.insert(principalPermissionGrants).values(["setup-owner", "setup-other"].map(principalId => ({ companyId,
principalType: "user", principalId, permissionKey: "agents:create" })));
const lifecycle = createAgentLifecycle(db);
const draft = await lifecycle.requestHire(companyId, { name: "Stopping setup", adapterType: "process" },
{ setupOnly: true, createdByUserId: "setup-owner" });
await db.update(agents).set({ lifecycleState: "pausing" }).where(eq(agents.id, draft.id));
function app(userId: string) {
const value = express(); value.use(express.json());
value.use((req, _res, next) => { req.actor = { type: "board", source: "session", userId,
companyIds: [companyId], memberships: [{ companyId, membershipRole: "member", status: "active" }] }; next(); });
value.use("/api", agentRoutes(db)); value.use(errorHandler); return value;
}
const updated = await request(app("setup-owner")).patch(`/api/agents/${draft.id}`).send({ runtimeConfig: { heartbeat: { enabled: false } } });
expect(updated.status, JSON.stringify(updated.body)).toBe(200);
await request(app("setup-owner")).post(`/api/agents/${draft.id}/lifecycle/retry`).send({}).expect(200);
await request(app("setup-other")).patch(`/api/agents/${draft.id}`).send({ runtimeConfig: { heartbeat: { enabled: true } } }).expect(403);
await request(app("setup-other")).post(`/api/agents/${draft.id}/lifecycle/retry`).send({}).expect(403);
await expect(lifecycle.requestHire(companyId, { name: draft.name }, { setupAgentId: draft.id })).rejects.toThrow("no longer available");
expect((await current(draft.id)).lifecycleState).toBe("pausing");
});
it("creates a board-owned setup before requesting approval for its final configuration", async () => {
await db.update(companies).set({ requireBoardApprovalForNewAgents: true }).where(eq(companies.id, companyId));
await db.insert(companyMemberships).values(["board", "other-creator"].map(principalId => ({ companyId, principalType: "user", principalId, membershipRole: "member" })));
@@ -26,7 +26,7 @@ export function agentRecords(db: Db, effects: LifecycleEffects) {
if (!existing) throw notFound("Agent setup not found");
return existing;
}
if (setup && (!isAgentSetupDraft(setup) || !setup.lifecycleHolds.includes("setup"))) {
if (setup && (!isAgentSetupDraft(setup) || setup.lifecycleState === "pausing" || !setup.lifecycleHolds.includes("setup"))) {
throw conflict("Agent setup is no longer available");
}
if (options?.setupAgentId && !setup) throw notFound("Agent setup not found");
@@ -6,7 +6,7 @@ export class AgentLifecycleConflict extends Error {
}
export function isAgentSetupDraft(agent: { lifecycleState?: string; pauseReason?: string | null }) {
return agent.pauseReason === "setup" && ["paused", "preparing", "verifying"].includes(agent.lifecycleState ?? "");
return agent.pauseReason === "setup" && ["paused", "preparing", "verifying", "pausing"].includes(agent.lifecycleState ?? "");
}
export function canConfigureAgentConnection(agent: { lifecycleState: string; status: string }): boolean {