mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
Keep draft computer preparation behind hire approval
Companies that require hire approval must not allocate agent-owned resources from an unapproved setup draft. Save the selected account with a pending verification warning and let the approved hire prepare and verify its computer. Other companies retain background wizard preparation. Verified 123 lifecycle, policy, lease, and API tests; 112 wizard/helper tests; server/UI typechecks; map and token checks. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
11 files changed
+68
-6
No files matched your search
@@ -78,6 +78,9 @@ databaseSuite("Core plugin Runner task lifecycle", () => {
|
||||
expect((await setup.resolveAdapterTestExecutionContext(input)).fallbackChecks).toMatchObject([{ code: "environment_agent_required", level: "warn" }]);
|
||||
await db.insert(agents).values({ id: agentId, companyId, name: "Draft", lifecycleState: "preparing", lifecycleHolds: ["setup"], status: "paused" });
|
||||
expect((await setup.resolveAdapterTestExecutionContext({ ...input, agentId })).fallbackChecks).toMatchObject([{ code: "environment_agent_preparing", level: "warn" }]);
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: true }).where(eq(companies.id, companyId));
|
||||
await db.update(agents).set({ lifecycleState: "paused", pauseReason: "setup" }).where(eq(agents.id, agentId));
|
||||
expect((await setup.resolveAdapterTestExecutionContext({ ...input, agentId })).fallbackChecks).toMatchObject([{ code: "environment_agent_approval_required", level: "warn" }]);
|
||||
const foreign = randomUUID();
|
||||
await db.insert(companies).values({ id: foreign, name: "Other company", issuePrefix: "OTHER" });
|
||||
expect((await setup.resolveAdapterTestExecutionContext({ ...input, companyId: foreign, agentId })).fallbackChecks).toMatchObject([{ code: "environment_agent_preparing" }]);
|
||||
|
||||
@@ -193,6 +193,26 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
expect(call.mock.calls.map(([, , request]) => request.phase).at(-1)).toBe("verifying");
|
||||
});
|
||||
|
||||
it("does not prepare agent-owned resources before required hire approval", async () => {
|
||||
const pluginId = randomUUID(), environmentId = randomUUID();
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: true }).where(eq(companies.id, companyId));
|
||||
await db.insert(plugins).values({ id: pluginId, pluginKey: pluginId, packageName: "approval-computer-test", version: "1.0.0", status: "ready",
|
||||
manifestJson: { agentLifecycle: true, capabilities: ["agents.lifecycle.manage", "environment.drivers.register"],
|
||||
environmentDrivers: [{ driverKey: "tasks", requiresAgent: true, supportsTasks: true }] } as never });
|
||||
await db.insert(environments).values({ id: environmentId, name: "Approval computer", driver: "plugin", status: "active",
|
||||
config: { pluginKey: pluginId, driverKey: "tasks", driverConfig: {} } });
|
||||
const runHost = vi.fn(async () => "complete" as const);
|
||||
const work = worker(runHost);
|
||||
const lifecycle = createAgentLifecycle(db);
|
||||
const draft = await lifecycle.requestHire(companyId, { name: "Unapproved computer", adapterType: "paperclip_runner",
|
||||
adapterConfig: { provider: "codex" }, defaultEnvironmentId: environmentId }, { setupOnly: true, runnerResolved: true });
|
||||
await work.process(draft.id);
|
||||
await lifecycle.updateConfiguration(draft.id, { adapterConfig: { provider: "codex", model: "changed" } });
|
||||
await work.process(draft.id);
|
||||
expect(await current(draft.id)).toMatchObject({ lifecycleState: "paused", lifecycleHolds: ["setup"] });
|
||||
expect(runHost).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("freezes the final setup configuration for hire approval", async () => {
|
||||
const lifecycle = createAgentLifecycle(db);
|
||||
const id = randomUUID();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
||||
import { probePluginRunnerEnvironment } from "./native-runtime/plugin-runner-probe.js";
|
||||
import { registerPluginTaskCredentialHandoff } from "./plugin-task-credential-writeback.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { agents, type Db } from "@paperclipai/db";
|
||||
import { agents, companies, type Db } from "@paperclipai/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
|
||||
import type { AdapterEnvironmentCheck } from "@paperclipai/adapter-utils";
|
||||
@@ -215,8 +215,16 @@ export function agentEnvironmentTestService(db: Db, pluginWorkerManager?: Plugin
|
||||
// operator can inspect the exact sandbox from the provider dashboard while
|
||||
// provider-side expiry reaps it later.
|
||||
if (environment.driver === "plugin" && await pluginEnvironmentRequiresAgent(db, environment)) {
|
||||
const [agent] = input.agentId ? await db.select({ state: agents.lifecycleState }).from(agents)
|
||||
const [agent] = input.agentId ? await db.select({ state: agents.lifecycleState, pauseReason: agents.pauseReason,
|
||||
requiresApproval: companies.requireBoardApprovalForNewAgents }).from(agents)
|
||||
.innerJoin(companies, eq(companies.id, agents.companyId))
|
||||
.where(and(eq(agents.id, input.agentId), eq(agents.companyId, input.companyId))) : [];
|
||||
if (agent?.state === "paused" && agent.pauseReason === "setup" && agent.requiresApproval) return {
|
||||
executionTarget: null, environmentName: environment.name, fallbackChecks: [{
|
||||
code: "environment_agent_approval_required", level: "warn",
|
||||
message: "The selected account will be verified on the agent computer after hire approval.",
|
||||
}], release: noopRelease,
|
||||
};
|
||||
if (!agent || ["paused", "preparing"].includes(agent.state)) return {
|
||||
executionTarget: null, environmentName: environment.name, fallbackChecks: [{
|
||||
code: !input.agentId ? "environment_agent_required" : "environment_agent_preparing", level: "warn",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { logger } from "../middleware/logger.js";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { companies, type Db } from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as lifecycle from "../modules/agent-lifecycle/index.js";
|
||||
import { withAccountingTransaction } from "./accounting-transaction.js";
|
||||
import { budgetServiceInTransaction, policyBlocks, type BudgetServiceHooks } from "./budgets.js";
|
||||
@@ -25,6 +26,8 @@ export function createAgentLifecycleEffects(_hooks: BudgetServiceHooks = {}): li
|
||||
const id = environmentId ?? (await instanceSettingsService(db).get()).defaultEnvironmentId;
|
||||
const environment = id ? await environmentService(db).getById(id) : null;
|
||||
if (!environment || !await pluginEnvironmentRequiresAgent(db, environment)) return false;
|
||||
const [company] = await db.select({ requiresApproval: companies.requireBoardApprovalForNewAgents }).from(companies).where(eq(companies.id, companyId));
|
||||
if (!company || company.requiresApproval) return false;
|
||||
const bound = await environmentService(db).listBoundCompanyIds(environment.id);
|
||||
if (bound.length && !bound.includes(companyId)) throw new lifecycle.AgentLifecycleConflict("The environment belongs to another company");
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user