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.
- package/LICENSE +21 -0
- package/package.json +36 -0
- package/src/app.ts +76 -0
- package/src/context.ts +8 -0
- package/src/http.ts +9 -0
- package/src/middleware/company-scope.ts +15 -0
- package/src/middleware/request-actor.ts +81 -0
- package/src/realtime/governance.ts +66 -0
- package/src/realtime/hub.ts +142 -0
- package/src/realtime/office-space.ts +448 -0
- package/src/routes/agents.ts +305 -0
- package/src/routes/companies.ts +58 -0
- package/src/routes/goals.ts +134 -0
- package/src/routes/governance.ts +208 -0
- package/src/routes/heartbeats.ts +61 -0
- package/src/routes/issues.ts +319 -0
- package/src/routes/observability.ts +47 -0
- package/src/routes/projects.ts +152 -0
- package/src/scripts/db-init.ts +13 -0
- package/src/server.ts +51 -0
- package/src/services/budget-service.ts +31 -0
- package/src/services/governance-service.ts +229 -0
- package/src/services/heartbeat-service.ts +706 -0
- package/src/worker/scheduler.ts +23 -0
- package/tsconfig.json +7 -0
|
@@ -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
|
+
}
|