bopodev-api 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import { z } from "zod";
3
+ import type { BopoDb } from "bopodev-db";
4
+ import { approvalRequests, createAgent, createGoal, goals, projects } from "bopodev-db";
5
+
6
+ const approvalGatedActions = new Set([
7
+ "hire_agent",
8
+ "activate_goal",
9
+ "override_budget",
10
+ "pause_agent",
11
+ "terminate_agent"
12
+ ]);
13
+
14
+ const hireAgentPayloadSchema = z.object({
15
+ managerAgentId: z.string().optional(),
16
+ role: z.string().min(1),
17
+ name: z.string().min(1),
18
+ providerType: z.enum(["claude_code", "codex", "http", "shell"]),
19
+ heartbeatCron: z.string().min(1),
20
+ monthlyBudgetUsd: z.number().nonnegative(),
21
+ canHireAgents: z.boolean().default(false),
22
+ runtimeCommand: z.string().optional(),
23
+ runtimeArgs: z.array(z.string()).optional(),
24
+ runtimeCwd: z.string().optional(),
25
+ runtimeTimeoutMs: z.number().int().positive().max(600000).optional()
26
+ });
27
+
28
+ const activateGoalPayloadSchema = z.object({
29
+ projectId: z.string().optional(),
30
+ parentGoalId: z.string().optional(),
31
+ level: z.enum(["company", "project", "agent"]),
32
+ title: z.string().min(1),
33
+ description: z.string().optional()
34
+ });
35
+
36
+ export class GovernanceError extends Error {
37
+ constructor(message: string) {
38
+ super(message);
39
+ this.name = "GovernanceError";
40
+ }
41
+ }
42
+
43
+ export function isApprovalRequired(action: string) {
44
+ return approvalGatedActions.has(action);
45
+ }
46
+
47
+ export async function resolveApproval(
48
+ db: BopoDb,
49
+ companyId: string,
50
+ approvalId: string,
51
+ status: "approved" | "rejected" | "overridden"
52
+ ) {
53
+ return db.transaction(async (tx) => {
54
+ const [approval] = await tx
55
+ .select()
56
+ .from(approvalRequests)
57
+ .where(and(eq(approvalRequests.companyId, companyId), eq(approvalRequests.id, approvalId)))
58
+ .limit(1);
59
+
60
+ if (!approval) {
61
+ throw new GovernanceError("Approval request not found.");
62
+ }
63
+ if (approval.status !== "pending") {
64
+ if (approval.status === status) {
65
+ // Idempotent retry: requested state already applied.
66
+ return {
67
+ approvalId,
68
+ action: approval.action,
69
+ status: approval.status,
70
+ execution: { applied: false }
71
+ };
72
+ }
73
+ throw new GovernanceError("Approval request has already been resolved.");
74
+ }
75
+
76
+ let execution:
77
+ | {
78
+ applied: boolean;
79
+ entityType?: "agent" | "goal";
80
+ entityId?: string;
81
+ entity?: Record<string, unknown>;
82
+ }
83
+ | undefined;
84
+
85
+ if (status === "approved") {
86
+ execution = await applyApprovalAction(tx as unknown as BopoDb, companyId, approval.action, approval.payloadJson);
87
+ }
88
+
89
+ const [updated] = await tx
90
+ .update(approvalRequests)
91
+ .set({ status, resolvedAt: new Date() })
92
+ .where(
93
+ and(
94
+ eq(approvalRequests.companyId, companyId),
95
+ eq(approvalRequests.id, approvalId),
96
+ eq(approvalRequests.status, "pending")
97
+ )
98
+ )
99
+ .returning({ id: approvalRequests.id });
100
+
101
+ if (!updated) {
102
+ const [latest] = await tx
103
+ .select()
104
+ .from(approvalRequests)
105
+ .where(and(eq(approvalRequests.companyId, companyId), eq(approvalRequests.id, approvalId)))
106
+ .limit(1);
107
+ if (latest && latest.status === status) {
108
+ return {
109
+ approvalId,
110
+ action: approval.action,
111
+ status: latest.status,
112
+ execution: { applied: false }
113
+ };
114
+ }
115
+ throw new GovernanceError("Approval request could not be resolved due to a concurrent update.");
116
+ }
117
+
118
+ return {
119
+ approvalId,
120
+ action: approval.action,
121
+ status,
122
+ execution: execution ?? { applied: false }
123
+ };
124
+ });
125
+ }
126
+
127
+ async function applyApprovalAction(db: BopoDb, companyId: string, action: string, payloadJson: string) {
128
+ const payload = parsePayload(payloadJson);
129
+
130
+ if (action === "hire_agent") {
131
+ const parsed = hireAgentPayloadSchema.safeParse(payload);
132
+ if (!parsed.success) {
133
+ throw new GovernanceError("Approval payload for agent hiring is invalid.");
134
+ }
135
+
136
+ const agent = await createAgent(db, {
137
+ companyId,
138
+ managerAgentId: parsed.data.managerAgentId,
139
+ role: parsed.data.role,
140
+ name: parsed.data.name,
141
+ providerType: parsed.data.providerType,
142
+ heartbeatCron: parsed.data.heartbeatCron,
143
+ monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
144
+ canHireAgents: parsed.data.canHireAgents,
145
+ initialState: {
146
+ runtime: {
147
+ command: parsed.data.runtimeCommand,
148
+ args: parsed.data.runtimeArgs,
149
+ cwd: parsed.data.runtimeCwd,
150
+ timeoutMs: parsed.data.runtimeTimeoutMs
151
+ }
152
+ }
153
+ });
154
+
155
+ return {
156
+ applied: true,
157
+ entityType: "agent" as const,
158
+ entityId: agent.id,
159
+ entity: agent as Record<string, unknown>
160
+ };
161
+ }
162
+
163
+ if (action === "activate_goal") {
164
+ const parsed = activateGoalPayloadSchema.safeParse(payload);
165
+ if (!parsed.success) {
166
+ throw new GovernanceError("Approval payload for goal activation is invalid.");
167
+ }
168
+
169
+ if (parsed.data.parentGoalId) {
170
+ const [parentGoal] = await db
171
+ .select({ id: goals.id })
172
+ .from(goals)
173
+ .where(and(eq(goals.companyId, companyId), eq(goals.id, parsed.data.parentGoalId)))
174
+ .limit(1);
175
+
176
+ if (!parentGoal) {
177
+ throw new GovernanceError("Parent goal not found for activation request.");
178
+ }
179
+ }
180
+
181
+ if (parsed.data.projectId) {
182
+ const [project] = await db
183
+ .select({ id: projects.id })
184
+ .from(projects)
185
+ .where(and(eq(projects.companyId, companyId), eq(projects.id, parsed.data.projectId)))
186
+ .limit(1);
187
+
188
+ if (!project) {
189
+ throw new GovernanceError("Project not found for activation request.");
190
+ }
191
+ }
192
+
193
+ const goal = await createGoal(db, {
194
+ companyId,
195
+ projectId: parsed.data.projectId,
196
+ parentGoalId: parsed.data.parentGoalId,
197
+ level: parsed.data.level,
198
+ title: parsed.data.title,
199
+ description: parsed.data.description
200
+ });
201
+
202
+ await db
203
+ .update(goals)
204
+ .set({ status: "active", updatedAt: new Date() })
205
+ .where(and(eq(goals.companyId, companyId), eq(goals.id, goal.id)));
206
+
207
+ return {
208
+ applied: true,
209
+ entityType: "goal" as const,
210
+ entityId: goal.id,
211
+ entity: { ...goal, status: "active" }
212
+ };
213
+ }
214
+
215
+ if (action === "pause_agent" || action === "terminate_agent" || action === "override_budget") {
216
+ return { applied: false };
217
+ }
218
+
219
+ throw new GovernanceError(`Unsupported approval action: ${action}`);
220
+ }
221
+
222
+ function parsePayload(payloadJson: string) {
223
+ try {
224
+ const parsed = JSON.parse(payloadJson) as unknown;
225
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
226
+ } catch {
227
+ return {};
228
+ }
229
+ }