mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
merge: integrate agent lifecycle ownership from master
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
ad5d8826ad
159 files changed
+7173
-2805
No files matched your search
@@ -1340,6 +1340,7 @@ export const PLUGIN_CAPABILITIES = [
|
||||
"approvals.read",
|
||||
"issue.documents.read",
|
||||
"agents.read",
|
||||
"agents.lifecycle.manage",
|
||||
"goals.read",
|
||||
"goals.create",
|
||||
"goals.update",
|
||||
|
||||
@@ -2859,3 +2859,7 @@ export * from "./agent-avatar-upload.js";
|
||||
|
||||
export type { DotBinding, DotInvitation, DotConnection, DotPairing } from "./dot-invitations.js";
|
||||
export { agentHarnessType, agentRunner, paperclipRunnerProfileForHarness, paperclipRunnerSupportsPlatform, type AgentRunnerChoice, type AgentRunner, type AgentRunnerAvailability } from "./agent-runner.js";
|
||||
|
||||
export { AGENT_LIFECYCLE_STATES, type AgentLifecycleState, type AgentLifecycleOperation } from "./types/agent-lifecycle.js";
|
||||
|
||||
export type { AgentLifecycleRequest, AgentLifecycleResult } from "./types/agent-lifecycle.js";
|
||||
@@ -0,0 +1,31 @@
|
||||
/** The lifecycle controls admission. Execution status describes current work. */
|
||||
export const AGENT_LIFECYCLE_STATES = [
|
||||
"pending_approval", "preparing", "verifying", "ready", "pausing", "paused",
|
||||
"resuming", "terminating", "cleaning_up", "terminated", "rejected",
|
||||
] as const;
|
||||
export type AgentLifecycleState = typeof AGENT_LIFECYCLE_STATES[number];
|
||||
|
||||
export interface AgentLifecycleOperation {
|
||||
id: string;
|
||||
resumeState?: "preparing" | "verifying" | "ready";
|
||||
hostComplete: boolean;
|
||||
completedPluginIds: string[];
|
||||
leaseOwner?: string;
|
||||
leaseUntil?: string;
|
||||
retryAt?: string;
|
||||
attempts: number;
|
||||
responsibleUserId?: string | null;
|
||||
}
|
||||
|
||||
export interface AgentLifecycleRequest {
|
||||
companyId: string;
|
||||
agentId: string;
|
||||
operationId: string;
|
||||
version: number;
|
||||
phase: AgentLifecycleState;
|
||||
}
|
||||
export interface AgentLifecycleResult {
|
||||
operationId: string;
|
||||
version: number;
|
||||
status: "complete" | "pending";
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AgentLifecycleState } from "./agent-lifecycle.js";
|
||||
import type { AgentAppearance } from "../agent-appearance.js";
|
||||
import type { AiConnectionLoginIntent } from "../ai-connections.js";
|
||||
import type {
|
||||
@@ -90,6 +91,9 @@ export interface Agent {
|
||||
appearance?: AgentAppearance | null;
|
||||
avatarUrl?: string;
|
||||
status: AgentStatus;
|
||||
lifecycleState?: AgentLifecycleState;
|
||||
lifecycleVersion?: number;
|
||||
lifecycleError?: string | null;
|
||||
reportsTo: string | null;
|
||||
capabilities: string | null;
|
||||
adapterType: AgentAdapterType;
|
||||
|
||||
@@ -1098,3 +1098,5 @@ export type { AgentInstructionErrorCode, AgentInstructionErrorDetails, AgentInst
|
||||
export type { AgentInstructionCandidate } from "./agent.js";
|
||||
|
||||
export * from "./skill-source.js";
|
||||
|
||||
export * from "./agent-lifecycle.js";
|
||||
@@ -688,6 +688,8 @@ export interface PaperclipPluginManifestV1 {
|
||||
/** Capabilities this plugin requires from the host. Enforced at runtime. */
|
||||
capabilities: PluginCapability[];
|
||||
/** Opt into the native pooled-connection catalog, setup, and management UI. */
|
||||
/** Required participant in agent preparation, stop, resume, and cleanup. */
|
||||
agentLifecycle?: true;
|
||||
aiConnectionRouter?: { name: string; description: string };
|
||||
/** Entrypoint paths relative to the package root. */
|
||||
entrypoints: {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { updateAgentSchema } from "./agent.js";
|
||||
|
||||
describe("agent update schema", () => {
|
||||
it.each(["paused", "idle", "terminated"])("accepts lifecycle status request %s", status => {
|
||||
expect(updateAgentSchema.parse({ status })).toEqual({ status });
|
||||
});
|
||||
it.each(["active", "running", "error", "pending_approval"])("rejects direct status write %s", status => {
|
||||
const result = updateAgentSchema.safeParse({ name: "Renamed", status });
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) expect(result.error.issues).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ path: ["status"] }),
|
||||
]));
|
||||
});
|
||||
it.each([{ spentMonthlyCents: 0 }, { name: "Renamed", spentMonthlyCents: 10 }])("rejects spend totals: %j", payload => {
|
||||
const result = updateAgentSchema.safeParse(payload);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) expect(result.error.issues).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ path: ["spentMonthlyCents"] }),
|
||||
]));
|
||||
});
|
||||
it("accepts profile and budget limit changes", () => {
|
||||
expect(updateAgentSchema.parse({ name: "Renamed", budgetMonthlyCents: 100 })).toEqual({ name: "Renamed", budgetMonthlyCents: 100 });
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { z } from "zod";
|
||||
import {
|
||||
AGENT_ICON_NAMES,
|
||||
AGENT_ROLES,
|
||||
AGENT_STATUSES,
|
||||
INBOX_MINE_ISSUE_STATUS_FILTER,
|
||||
} from "../constants.js";
|
||||
import { agentAdapterTypeSchema } from "../adapter-type.js";
|
||||
@@ -157,8 +156,8 @@ export const updateAgentSchema = objectWithoutDefaults(
|
||||
.extend({
|
||||
permissions: z.never().optional(),
|
||||
replaceAdapterConfig: z.boolean().optional(),
|
||||
status: z.enum(AGENT_STATUSES).optional(),
|
||||
spentMonthlyCents: z.number().int().nonnegative().optional(),
|
||||
status: z.enum(["paused", "idle", "terminated"]).optional(),
|
||||
spentMonthlyCents: z.never().optional(),
|
||||
});
|
||||
|
||||
export type UpdateAgent = z.infer<typeof updateAgentSchema>;
|
||||
|
||||
@@ -33,6 +33,12 @@ describe("plugin capability constants", () => {
|
||||
});
|
||||
|
||||
describe("plugin manifest validators", () => {
|
||||
it("requires authority for required agent lifecycle participants", () => {
|
||||
const manifest = { id: "example.lifecycle", apiVersion: 1, version: "0.1.0", displayName: "Lifecycle", description: "Lifecycle tests", author: "Tests", categories: ["automation"], entrypoints: { worker: "worker.js" }, agentLifecycle: true };
|
||||
expect(pluginManifestV1Schema.safeParse({ ...manifest, capabilities: ["agents.read"] }).success).toBe(false);
|
||||
expect(pluginManifestV1Schema.parse({ ...manifest, capabilities: ["agents.lifecycle.manage"] }).agentLifecycle).toBe(true);
|
||||
});
|
||||
|
||||
it("requires routing authority for native pooled connector declarations without a custom UI bundle", () => {
|
||||
const manifest = { id: "example.pool", apiVersion: 1, version: "0.1.0", displayName: "Pool", description: "Pool", author: "Tests", categories: ["connector"], entrypoints: { worker: "worker.js" }, aiConnectionRouter: { name: "AI connection pool", description: "Use saved connections" } };
|
||||
expect(pluginManifestV1Schema.safeParse({ ...manifest, capabilities: ["ui.page.register"] }).success).toBe(false);
|
||||
|
||||
@@ -782,6 +782,7 @@ export const pluginManifestV1Schema = z.object({
|
||||
"minimumPaperclipVersion must follow semver (e.g. 1.0.0)",
|
||||
).optional(),
|
||||
capabilities: z.array(z.enum(PLUGIN_CAPABILITIES)).min(1),
|
||||
agentLifecycle: z.literal(true).optional(),
|
||||
aiConnectionRouter: z.object({ name: z.string().trim().min(1).max(100), description: z.string().trim().min(1).max(500) }).strict().optional(),
|
||||
entrypoints: z.object({
|
||||
worker: z.string().min(1),
|
||||
@@ -806,6 +807,10 @@ export const pluginManifestV1Schema = z.object({
|
||||
launchers: z.array(pluginLauncherDeclarationSchema).optional(),
|
||||
}).optional(),
|
||||
}).superRefine((manifest, ctx) => {
|
||||
if (manifest.agentLifecycle && !manifest.capabilities.includes("agents.lifecycle.manage")) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent lifecycle participation requires agents.lifecycle.manage", path: ["capabilities"] });
|
||||
}
|
||||
|
||||
// ── Entrypoint ↔ UI slot consistency ──────────────────────────────────
|
||||
// Plugins that declare UI slots must also declare a UI entrypoint so the
|
||||
// host knows where to load the bundle from (PLUGIN_SPEC.md §10.1).
|
||||
|
||||
Reference in new issue
Block a user