feat: add individual agent interaction permissions (#20411)

Add eleven sparse incoming and outgoing restrictions, human CLI/API administration, shared mutation enforcement, and pending-delivery checks. Preserve existing defaults, agent settings, and existing authorization boundaries.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-09 20:34:04 -06:00
1 parent 4158799d1b
commit 5bad074baa
97 files changed
+3590 -433

No files matched your search

@@ -12,6 +12,7 @@ import type { LifecycleEffects } from "./effects.js";
export async function updateAgentConfiguration(tx: Db, effects: LifecycleEffects, id: string, data: AgentConfigurationPatch, options?: UpdateAgentOptions, publications: ActivityPublication[] = []) {
if (!("nestedIndex" in tx)) throw new AgentLifecycleConflict("Configuration integration requires a database transaction");
assertConfigurationFields(data);
options = await effects.lockConfigurationInteractions(tx, id, data, options);
const [current] = await tx.select().from(agents).where(eq(agents.id, id)).for("no key update");
if (!current) return null;
if (options?.expectedConfigurationRevision && options.expectedConfigurationRevision !== agentConfigurationRevision(current)) {
@@ -16,6 +16,7 @@ export interface LifecycleEffects {
beforeConnectionEligibilityRestore?(db: Db, companyId: string, agentId: string): Promise<void>;
reportFailure(context: LifecycleFailure, error: unknown): void;
deleteDependencies(db: Db, companyId: string, agentId: string): Promise<void>;
lockConfigurationInteractions(db: Db, id: string, data: AgentConfigurationPatch, options?: UpdateAgentOptions): Promise<UpdateAgentOptions | undefined>;
prepareConfiguration(db: Db, current: AgentConfigurationRecord, data: AgentConfigurationPatch, options?: UpdateAgentOptions): Promise<AgentConfigurationPatch>;
completeConfiguration(db: Db, before: AgentConfigurationRecord, after: AgentConfigurationRecord, patch: AgentConfigurationPatch, options: UpdateAgentOptions | undefined, publications: ActivityPublication[]): Promise<void>;
transaction<T>(db: Db, companyId: string, work: (tx: Db, publications: ActivityPublication[]) => Promise<T>): Promise<T>;
@@ -1,3 +1,4 @@
import { preserveAgentInteractionPermissions } from "../../../lib/agent-interaction-permissions.js";
import type { LifecycleEffects } from "./effects.js";
import { randomUUID } from "node:crypto";
import { and, eq, sql } from "drizzle-orm";
@@ -43,6 +44,7 @@ export function agentRecords(db: Db, effects: LifecycleEffects) {
const createdStatus=status === "pending_approval" ? "pending_approval" : status === "terminated" ? "terminated" : "paused";
const values: typeof agents.$inferInsert = {
...prepared,
...(setup ? { permissions: preserveAgentInteractionPermissions(setup.permissions, prepared.permissions) } : {}),
createdByUserId: setup?.createdByUserId ?? options?.createdByUserId ?? null,
lifecycleState,
lifecycleVersion: setup ? setup.lifecycleVersion + 1 : 1,
@@ -1,3 +1,5 @@
import type { ActivityPublication } from "../../types/activity-publication.js";
import { AgentLifecycleConflict } from "./domain/policy.js";
import { hireApprovalService as approvalRecords, type HireDecisionTarget } from "./adapters/approvals.js";
import { agents, companies } from "@paperclipai/db";
import { and, asc, desc, eq, gt, inArray, lte, sql } from "drizzle-orm";
@@ -43,6 +45,24 @@ export function processAgentLifecycle(db: Db, id: string): Promise<void> {
return workers.get(db)?.process(id) ?? Promise.resolve();
}
export function initializeImportedAgentConfigurationInTransaction(tx: Db, effects: LifecycleEffects, publications: ActivityPublication[],
id: string, data: Pick<AgentConfigurationPatch, "adapterConfig" | "reportsTo">) {
if (!("nestedIndex" in tx)) throw new AgentLifecycleConflict("Imported configuration requires a database transaction");
if (Object.keys(data).some(key => key !== "adapterConfig" && key !== "reportsTo")) throw new AgentLifecycleConflict("Only initial imported instructions and reporting links can be initialized");
return updateAgentConfiguration(tx, effects, id, data, undefined, publications);
}
export async function requestAgentHireInTransaction(tx: Db, effects: LifecycleEffects, publications: ActivityPublication[],
...args: Parameters<ReturnType<typeof agentRecords>["create"]>) {
if (!("nestedIndex" in tx)) throw new AgentLifecycleConflict("Imported hires require a database transaction");
const records = agentRecords(tx, { ...effects, transaction: async (database, companyId, work) => {
const [company] = await database.select({ id: companies.id }).from(companies).where(eq(companies.id, companyId)).for("no key update");
if (!company) throw new AgentLifecycleConflict("Import company is unavailable");
return work(database, publications);
} });
return records.create(...args);
}
export function createAgentLifecycle(db: Db, effects: LifecycleEffects) {
assertRootDatabase(db);
const records = agentRecords(db, effects);