feat(openclaw): add adapter hire-approved hooks
This commit is contained in:
@@ -3,6 +3,7 @@ import type { Db } from "@paperclipai/db";
|
||||
import { approvalComments, approvals } from "@paperclipai/db";
|
||||
import { notFound, unprocessable } from "../errors.js";
|
||||
import { agentService } from "./agents.js";
|
||||
import { notifyHireApproved } from "./hire-hook.js";
|
||||
|
||||
export function approvalService(db: Db) {
|
||||
const agentsSvc = agentService(db);
|
||||
@@ -59,13 +60,15 @@ export function approvalService(db: Db) {
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
|
||||
let hireApprovedAgentId: string | null = null;
|
||||
if (updated.type === "hire_agent") {
|
||||
const payload = updated.payload as Record<string, unknown>;
|
||||
const payloadAgentId = typeof payload.agentId === "string" ? payload.agentId : null;
|
||||
if (payloadAgentId) {
|
||||
await agentsSvc.activatePendingApproval(payloadAgentId);
|
||||
hireApprovedAgentId = payloadAgentId;
|
||||
} else {
|
||||
await agentsSvc.create(updated.companyId, {
|
||||
const created = await agentsSvc.create(updated.companyId, {
|
||||
name: String(payload.name ?? "New Agent"),
|
||||
role: String(payload.role ?? "general"),
|
||||
title: typeof payload.title === "string" ? payload.title : null,
|
||||
@@ -87,6 +90,16 @@ export function approvalService(db: Db) {
|
||||
permissions: undefined,
|
||||
lastHeartbeatAt: null,
|
||||
});
|
||||
hireApprovedAgentId = created?.id ?? null;
|
||||
}
|
||||
if (hireApprovedAgentId) {
|
||||
void notifyHireApproved(db, {
|
||||
companyId: updated.companyId,
|
||||
agentId: hireApprovedAgentId,
|
||||
source: "approval",
|
||||
sourceId: id,
|
||||
approvedAt: now,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
113
server/src/services/hire-hook.ts
Normal file
113
server/src/services/hire-hook.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { agents } from "@paperclipai/db";
|
||||
import type { HireApprovedPayload } from "@paperclipai/adapter-utils";
|
||||
import { findServerAdapter } from "../adapters/registry.js";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
import { logActivity } from "./activity-log.js";
|
||||
|
||||
const HIRE_APPROVED_MESSAGE =
|
||||
"Tell your user that your hire was approved, now they should assign you a task in Paperclip or ask you to create issues.";
|
||||
|
||||
export interface NotifyHireApprovedInput {
|
||||
companyId: string;
|
||||
agentId: string;
|
||||
source: "join_request" | "approval";
|
||||
sourceId: string;
|
||||
approvedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the adapter's onHireApproved hook when an agent is approved (join-request or hire_agent approval).
|
||||
* Failures are non-fatal: we log and write to activity, never throw.
|
||||
*/
|
||||
export async function notifyHireApproved(
|
||||
db: Db,
|
||||
input: NotifyHireApprovedInput,
|
||||
): Promise<void> {
|
||||
const { companyId, agentId, source, sourceId } = input;
|
||||
const approvedAt = input.approvedAt ?? new Date();
|
||||
|
||||
const row = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(and(eq(agents.id, agentId), eq(agents.companyId, companyId)))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
|
||||
if (!row) {
|
||||
logger.warn({ companyId, agentId, source, sourceId }, "hire hook: agent not found in company, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const adapterType = row.adapterType ?? "process";
|
||||
const adapter = findServerAdapter(adapterType);
|
||||
const onHireApproved = adapter?.onHireApproved;
|
||||
if (!onHireApproved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: HireApprovedPayload = {
|
||||
companyId,
|
||||
agentId,
|
||||
agentName: row.name,
|
||||
adapterType,
|
||||
source,
|
||||
sourceId,
|
||||
approvedAt: approvedAt.toISOString(),
|
||||
message: HIRE_APPROVED_MESSAGE,
|
||||
};
|
||||
|
||||
const adapterConfig =
|
||||
typeof row.adapterConfig === "object" && row.adapterConfig !== null && !Array.isArray(row.adapterConfig)
|
||||
? (row.adapterConfig as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
try {
|
||||
const result = await onHireApproved(payload, adapterConfig);
|
||||
if (result.ok) {
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: "system",
|
||||
actorId: "hire_hook",
|
||||
action: "hire_hook.succeeded",
|
||||
entityType: "agent",
|
||||
entityId: agentId,
|
||||
details: { source, sourceId, adapterType },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
{ companyId, agentId, adapterType, source, sourceId, error: result.error, detail: result.detail },
|
||||
"hire hook: adapter returned failure",
|
||||
);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: "system",
|
||||
actorId: "hire_hook",
|
||||
action: "hire_hook.failed",
|
||||
entityType: "agent",
|
||||
entityId: agentId,
|
||||
details: { source, sourceId, adapterType, error: result.error, detail: result.detail },
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, companyId, agentId, adapterType, source, sourceId },
|
||||
"hire hook: adapter threw",
|
||||
);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: "system",
|
||||
actorId: "hire_hook",
|
||||
action: "hire_hook.error",
|
||||
entityType: "agent",
|
||||
entityId: agentId,
|
||||
details: {
|
||||
source,
|
||||
sourceId,
|
||||
adapterType,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,6 @@ export { sidebarBadgeService } from "./sidebar-badges.js";
|
||||
export { accessService } from "./access.js";
|
||||
export { companyPortabilityService } from "./company-portability.js";
|
||||
export { logActivity, type LogActivityInput } from "./activity-log.js";
|
||||
export { notifyHireApproved, type NotifyHireApprovedInput } from "./hire-hook.js";
|
||||
export { publishLiveEvent, subscribeCompanyLiveEvents } from "./live-events.js";
|
||||
export { createStorageServiceFromConfig, getStorageService } from "../storage/index.js";
|
||||
|
||||
Reference in New Issue
Block a user