bopodev-api 0.1.12 → 0.1.13
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/package.json +6 -4
- package/src/app.ts +2 -0
- package/src/lib/instance-paths.ts +12 -0
- package/src/lib/opencode-model.ts +35 -0
- package/src/lib/workspace-policy.ts +5 -0
- package/src/realtime/heartbeat-runs.ts +78 -0
- package/src/realtime/hub.ts +37 -1
- package/src/realtime/office-space.ts +10 -1
- package/src/routes/agents.ts +89 -2
- package/src/routes/companies.ts +2 -0
- package/src/routes/governance.ts +9 -2
- package/src/routes/heartbeats.ts +2 -1
- package/src/routes/issues.ts +321 -0
- package/src/routes/observability.ts +546 -18
- package/src/routes/plugins.ts +257 -0
- package/src/scripts/onboard-seed.ts +57 -12
- package/src/server.ts +62 -3
- package/src/services/governance-service.ts +97 -23
- package/src/services/heartbeat-service.ts +633 -31
- package/src/services/memory-file-service.ts +249 -0
- package/src/services/plugin-manifest-loader.ts +65 -0
- package/src/services/plugin-runtime.ts +580 -0
- package/src/services/plugin-webhook-executor.ts +94 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -13,16 +13,18 @@
|
|
|
13
13
|
"dotenv": "^17.0.1",
|
|
14
14
|
"drizzle-orm": "^0.44.5",
|
|
15
15
|
"express": "^5.1.0",
|
|
16
|
+
"multer": "^2.1.1",
|
|
16
17
|
"nanoid": "^5.1.5",
|
|
17
18
|
"ws": "^8.19.0",
|
|
18
19
|
"zod": "^4.1.5",
|
|
19
|
-
"bopodev-contracts": "0.1.
|
|
20
|
-
"bopodev-
|
|
21
|
-
"bopodev-
|
|
20
|
+
"bopodev-contracts": "0.1.13",
|
|
21
|
+
"bopodev-agent-sdk": "0.1.13",
|
|
22
|
+
"bopodev-db": "0.1.13"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/cors": "^2.8.19",
|
|
25
26
|
"@types/express": "^5.0.3",
|
|
27
|
+
"@types/multer": "^2.1.0",
|
|
26
28
|
"@types/ws": "^8.18.1",
|
|
27
29
|
"tsx": "^4.20.5"
|
|
28
30
|
},
|
package/src/app.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { createHeartbeatRouter } from "./routes/heartbeats";
|
|
|
13
13
|
import { createIssuesRouter } from "./routes/issues";
|
|
14
14
|
import { createObservabilityRouter } from "./routes/observability";
|
|
15
15
|
import { createProjectsRouter } from "./routes/projects";
|
|
16
|
+
import { createPluginsRouter } from "./routes/plugins";
|
|
16
17
|
import { sendError } from "./http";
|
|
17
18
|
import { attachRequestActor } from "./middleware/request-actor";
|
|
18
19
|
|
|
@@ -62,6 +63,7 @@ export function createApp(ctx: AppContext) {
|
|
|
62
63
|
app.use("/governance", createGovernanceRouter(ctx));
|
|
63
64
|
app.use("/heartbeats", createHeartbeatRouter(ctx));
|
|
64
65
|
app.use("/observability", createObservabilityRouter(ctx));
|
|
66
|
+
app.use("/plugins", createPluginsRouter(ctx));
|
|
65
67
|
|
|
66
68
|
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
|
67
69
|
if (error instanceof RepositoryValidationError) {
|
|
@@ -62,6 +62,18 @@ export function resolveAgentFallbackWorkspacePath(companyId: string, agentId: st
|
|
|
62
62
|
return join(resolveBopoInstanceRoot(), "workspaces", companyId, "agents", agentId);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export function resolveAgentMemoryRootPath(companyId: string, agentId: string) {
|
|
66
|
+
return join(resolveAgentFallbackWorkspacePath(companyId, agentId), "memory");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveAgentDurableMemoryPath(companyId: string, agentId: string) {
|
|
70
|
+
return join(resolveAgentMemoryRootPath(companyId, agentId), "life");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveAgentDailyMemoryPath(companyId: string, agentId: string) {
|
|
74
|
+
return join(resolveAgentMemoryRootPath(companyId, agentId), "memory");
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
export function resolveStorageRoot() {
|
|
66
78
|
return join(resolveBopoInstanceRoot(), "data", "storage");
|
|
67
79
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getAdapterModels } from "bopodev-agent-sdk";
|
|
2
|
+
import type { NormalizedRuntimeConfig } from "./agent-config";
|
|
3
|
+
|
|
4
|
+
export async function resolveOpencodeRuntimeModel(
|
|
5
|
+
providerType: string,
|
|
6
|
+
runtimeConfig: NormalizedRuntimeConfig
|
|
7
|
+
): Promise<string | undefined> {
|
|
8
|
+
if (providerType !== "opencode") {
|
|
9
|
+
return runtimeConfig.runtimeModel;
|
|
10
|
+
}
|
|
11
|
+
if (runtimeConfig.runtimeModel?.trim()) {
|
|
12
|
+
return runtimeConfig.runtimeModel.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const configured =
|
|
16
|
+
process.env.BOPO_OPENCODE_MODEL?.trim() ||
|
|
17
|
+
process.env.OPENCODE_MODEL?.trim() ||
|
|
18
|
+
undefined;
|
|
19
|
+
try {
|
|
20
|
+
const discovered = await getAdapterModels("opencode", {
|
|
21
|
+
command: runtimeConfig.runtimeCommand,
|
|
22
|
+
cwd: runtimeConfig.runtimeCwd,
|
|
23
|
+
env: runtimeConfig.runtimeEnv
|
|
24
|
+
});
|
|
25
|
+
if (configured && discovered.some((entry) => entry.id === configured)) {
|
|
26
|
+
return configured;
|
|
27
|
+
}
|
|
28
|
+
if (discovered.length > 0) {
|
|
29
|
+
return discovered[0]!.id;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Fall back to configured env default when discovery is unavailable.
|
|
33
|
+
}
|
|
34
|
+
return configured;
|
|
35
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isInsidePath,
|
|
6
6
|
normalizeAbsolutePath,
|
|
7
7
|
resolveAgentFallbackWorkspacePath,
|
|
8
|
+
resolveAgentMemoryRootPath,
|
|
8
9
|
resolveCompanyProjectsWorkspacePath
|
|
9
10
|
} from "./instance-paths";
|
|
10
11
|
|
|
@@ -73,3 +74,7 @@ export function ensureRuntimeWorkspaceCompatible(projectWorkspacePath: string, r
|
|
|
73
74
|
export function resolveAgentFallbackWorkspace(companyId: string, agentId: string) {
|
|
74
75
|
return resolveAgentFallbackWorkspacePath(companyId, agentId);
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
export function resolveAgentMemoryRoot(companyId: string, agentId: string) {
|
|
79
|
+
return resolveAgentMemoryRootPath(companyId, agentId);
|
|
80
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeartbeatRunRealtimeEvent,
|
|
3
|
+
RealtimeEventEnvelope,
|
|
4
|
+
RealtimeMessage
|
|
5
|
+
} from "bopodev-contracts";
|
|
6
|
+
import { listHeartbeatRunMessagesForRuns, listHeartbeatRuns, type BopoDb } from "bopodev-db";
|
|
7
|
+
|
|
8
|
+
export function createHeartbeatRunsRealtimeEvent(
|
|
9
|
+
companyId: string,
|
|
10
|
+
event: HeartbeatRunRealtimeEvent
|
|
11
|
+
): Extract<RealtimeMessage, { kind: "event" }> {
|
|
12
|
+
return createRealtimeEvent(companyId, {
|
|
13
|
+
channel: "heartbeat-runs",
|
|
14
|
+
event
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadHeartbeatRunsRealtimeSnapshot(
|
|
19
|
+
db: BopoDb,
|
|
20
|
+
companyId: string
|
|
21
|
+
): Promise<Extract<RealtimeMessage, { kind: "event" }>> {
|
|
22
|
+
const runs = await listHeartbeatRuns(db, companyId, 8);
|
|
23
|
+
const transcriptsByRunId = await listHeartbeatRunMessagesForRuns(db, {
|
|
24
|
+
companyId,
|
|
25
|
+
runIds: runs.map((run) => run.id),
|
|
26
|
+
perRunLimit: 60
|
|
27
|
+
});
|
|
28
|
+
const transcripts = runs.map((run) => {
|
|
29
|
+
const result = transcriptsByRunId.get(run.id) ?? { items: [], nextCursor: null };
|
|
30
|
+
return {
|
|
31
|
+
runId: run.id,
|
|
32
|
+
messages: result.items.map((message) => ({
|
|
33
|
+
id: message.id,
|
|
34
|
+
runId: message.runId,
|
|
35
|
+
sequence: message.sequence,
|
|
36
|
+
kind: message.kind as
|
|
37
|
+
| "system"
|
|
38
|
+
| "assistant"
|
|
39
|
+
| "thinking"
|
|
40
|
+
| "tool_call"
|
|
41
|
+
| "tool_result"
|
|
42
|
+
| "result"
|
|
43
|
+
| "stderr",
|
|
44
|
+
label: message.label,
|
|
45
|
+
text: message.text,
|
|
46
|
+
payload: message.payloadJson,
|
|
47
|
+
signalLevel: (message.signalLevel as "high" | "medium" | "low" | "noise" | null) ?? undefined,
|
|
48
|
+
groupKey: message.groupKey,
|
|
49
|
+
source: (message.source as "stdout" | "stderr" | "trace_fallback" | null) ?? undefined,
|
|
50
|
+
createdAt: message.createdAt.toISOString()
|
|
51
|
+
})),
|
|
52
|
+
nextCursor: result.nextCursor
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return createHeartbeatRunsRealtimeEvent(companyId, {
|
|
57
|
+
type: "runs.snapshot",
|
|
58
|
+
runs: runs.map((run) => ({
|
|
59
|
+
runId: run.id,
|
|
60
|
+
status: run.status as "started" | "completed" | "failed" | "skipped",
|
|
61
|
+
message: run.message ?? null,
|
|
62
|
+
startedAt: run.startedAt.toISOString(),
|
|
63
|
+
finishedAt: run.finishedAt?.toISOString() ?? null
|
|
64
|
+
})),
|
|
65
|
+
transcripts
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createRealtimeEvent(
|
|
70
|
+
companyId: string,
|
|
71
|
+
envelope: Extract<RealtimeEventEnvelope, { channel: "heartbeat-runs" }>
|
|
72
|
+
): Extract<RealtimeMessage, { kind: "event" }> {
|
|
73
|
+
return {
|
|
74
|
+
kind: "event",
|
|
75
|
+
companyId,
|
|
76
|
+
...envelope
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/realtime/hub.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function attachRealtimeHub(
|
|
|
25
25
|
|
|
26
26
|
wss.on("connection", async (socket, request) => {
|
|
27
27
|
const subscription = getSubscription(request.url);
|
|
28
|
-
if (!subscription) {
|
|
28
|
+
if (!subscription || !canSubscribeToCompany(request.headers, subscription.companyId)) {
|
|
29
29
|
socket.close(1008, "Invalid realtime subscription");
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
@@ -131,6 +131,42 @@ function getSubscription(requestUrl: string | undefined) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function canSubscribeToCompany(
|
|
135
|
+
headers: Record<string, string | string[] | undefined>,
|
|
136
|
+
companyId: string
|
|
137
|
+
) {
|
|
138
|
+
const actorType = readHeader(headers, "x-actor-type")?.toLowerCase();
|
|
139
|
+
const actorCompanies = parseCommaList(readHeader(headers, "x-actor-companies"));
|
|
140
|
+
const hasActorHeaders = Boolean(actorType || actorCompanies.length > 0);
|
|
141
|
+
const allowLocalBoardFallback = process.env.NODE_ENV !== "production" && process.env.BOPO_ALLOW_LOCAL_BOARD_FALLBACK !== "0";
|
|
142
|
+
|
|
143
|
+
if (!hasActorHeaders) {
|
|
144
|
+
return allowLocalBoardFallback;
|
|
145
|
+
}
|
|
146
|
+
if (actorType === "board") {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return actorCompanies.includes(companyId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readHeader(headers: Record<string, string | string[] | undefined>, name: string) {
|
|
153
|
+
const value = headers[name];
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
return value[0];
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseCommaList(value: string | undefined) {
|
|
161
|
+
if (!value) {
|
|
162
|
+
return [] as string[];
|
|
163
|
+
}
|
|
164
|
+
return value
|
|
165
|
+
.split(",")
|
|
166
|
+
.map((entry) => entry.trim())
|
|
167
|
+
.filter((entry) => entry.length > 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
134
170
|
function buildSubscriptionKey(companyId: string, channel: RealtimeChannel) {
|
|
135
171
|
return `${companyId}:${channel}`;
|
|
136
172
|
}
|
|
@@ -427,7 +427,16 @@ function sortOccupants(occupants: OfficeOccupant[]) {
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
function normalizeProviderType(value: string): OfficeOccupant["providerType"] {
|
|
430
|
-
return value === "claude_code" ||
|
|
430
|
+
return value === "claude_code" ||
|
|
431
|
+
value === "codex" ||
|
|
432
|
+
value === "cursor" ||
|
|
433
|
+
value === "opencode" ||
|
|
434
|
+
value === "openai_api" ||
|
|
435
|
+
value === "anthropic_api" ||
|
|
436
|
+
value === "http" ||
|
|
437
|
+
value === "shell"
|
|
438
|
+
? value
|
|
439
|
+
: null;
|
|
431
440
|
}
|
|
432
441
|
|
|
433
442
|
function formatActionLabel(action: string) {
|
package/src/routes/agents.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
deleteAgent,
|
|
15
15
|
getApprovalRequest,
|
|
16
16
|
listAgents,
|
|
17
|
+
listApprovalRequests,
|
|
17
18
|
updateAgent
|
|
18
19
|
} from "bopodev-db";
|
|
19
20
|
import type { AppContext } from "../context";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
runtimeConfigToDb,
|
|
26
27
|
runtimeConfigToStateBlobPatch
|
|
27
28
|
} from "../lib/agent-config";
|
|
29
|
+
import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
|
|
28
30
|
import { hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
29
31
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
30
32
|
import { requireBoardRole, requirePermission } from "../middleware/request-actor";
|
|
@@ -63,7 +65,16 @@ const updateAgentSchema = AgentUpdateRequestSchema.extend({
|
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
const runtimePreflightSchema = z.object({
|
|
66
|
-
providerType: z.enum([
|
|
68
|
+
providerType: z.enum([
|
|
69
|
+
"claude_code",
|
|
70
|
+
"codex",
|
|
71
|
+
"cursor",
|
|
72
|
+
"opencode",
|
|
73
|
+
"openai_api",
|
|
74
|
+
"anthropic_api",
|
|
75
|
+
"http",
|
|
76
|
+
"shell"
|
|
77
|
+
]),
|
|
67
78
|
runtimeConfig: z.record(z.string(), z.unknown()).optional(),
|
|
68
79
|
...legacyRuntimeConfigSchema.shape
|
|
69
80
|
});
|
|
@@ -143,7 +154,15 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
143
154
|
runtimeConfig: req.body?.runtimeConfig,
|
|
144
155
|
defaultRuntimeCwd
|
|
145
156
|
});
|
|
146
|
-
const typedProviderType = providerType as
|
|
157
|
+
const typedProviderType = providerType as
|
|
158
|
+
| "claude_code"
|
|
159
|
+
| "codex"
|
|
160
|
+
| "cursor"
|
|
161
|
+
| "opencode"
|
|
162
|
+
| "openai_api"
|
|
163
|
+
| "anthropic_api"
|
|
164
|
+
| "http"
|
|
165
|
+
| "shell";
|
|
147
166
|
const models = await getAdapterModels(typedProviderType, {
|
|
148
167
|
command: runtimeConfig.runtimeCommand,
|
|
149
168
|
args: runtimeConfig.runtimeArgs,
|
|
@@ -245,6 +264,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
245
264
|
},
|
|
246
265
|
defaultRuntimeCwd
|
|
247
266
|
});
|
|
267
|
+
runtimeConfig.runtimeModel = await resolveOpencodeRuntimeModel(parsed.data.providerType, runtimeConfig);
|
|
248
268
|
if (requiresRuntimeCwd(parsed.data.providerType) && !hasText(runtimeConfig.runtimeCwd)) {
|
|
249
269
|
return sendError(res, "Runtime working directory is required for this runtime provider.", 422);
|
|
250
270
|
}
|
|
@@ -253,6 +273,19 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
if (parsed.data.requestApproval && isApprovalRequired("hire_agent")) {
|
|
276
|
+
const duplicate = await findDuplicateHireRequest(ctx.db, req.companyId!, {
|
|
277
|
+
role: parsed.data.role,
|
|
278
|
+
managerAgentId: parsed.data.managerAgentId ?? null
|
|
279
|
+
});
|
|
280
|
+
if (duplicate) {
|
|
281
|
+
return sendOk(res, {
|
|
282
|
+
queuedForApproval: false,
|
|
283
|
+
duplicate: true,
|
|
284
|
+
existingAgentId: duplicate.existingAgentId ?? null,
|
|
285
|
+
pendingApprovalId: duplicate.pendingApprovalId ?? null,
|
|
286
|
+
message: duplicateMessage(duplicate)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
256
289
|
const approvalId = await createApprovalRequest(ctx.db, {
|
|
257
290
|
companyId: req.companyId!,
|
|
258
291
|
action: "hire_agent",
|
|
@@ -363,6 +396,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
363
396
|
})
|
|
364
397
|
: {})
|
|
365
398
|
};
|
|
399
|
+
nextRuntime.runtimeModel = await resolveOpencodeRuntimeModel(effectiveProviderType, nextRuntime);
|
|
366
400
|
if (!nextRuntime.runtimeCwd && defaultRuntimeCwd) {
|
|
367
401
|
nextRuntime.runtimeCwd = defaultRuntimeCwd;
|
|
368
402
|
}
|
|
@@ -522,3 +556,56 @@ function listUnsupportedAgentUpdateKeys(payload: unknown) {
|
|
|
522
556
|
}
|
|
523
557
|
return unsupported;
|
|
524
558
|
}
|
|
559
|
+
|
|
560
|
+
async function findDuplicateHireRequest(
|
|
561
|
+
db: AppContext["db"],
|
|
562
|
+
companyId: string,
|
|
563
|
+
input: { role: string; managerAgentId: string | null }
|
|
564
|
+
) {
|
|
565
|
+
const role = input.role.trim();
|
|
566
|
+
const managerAgentId = input.managerAgentId ?? null;
|
|
567
|
+
const agents = await listAgents(db, companyId);
|
|
568
|
+
const existingAgent = agents.find(
|
|
569
|
+
(agent) =>
|
|
570
|
+
agent.role === role &&
|
|
571
|
+
(agent.managerAgentId ?? null) === managerAgentId &&
|
|
572
|
+
agent.status !== "terminated"
|
|
573
|
+
);
|
|
574
|
+
const approvals = await listApprovalRequests(db, companyId);
|
|
575
|
+
const pendingApproval = approvals.find((approval) => {
|
|
576
|
+
if (approval.status !== "pending" || approval.action !== "hire_agent") {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
const payload = parseApprovalPayload(approval.payloadJson);
|
|
580
|
+
return payload.role === role && (payload.managerAgentId ?? null) === managerAgentId;
|
|
581
|
+
});
|
|
582
|
+
if (!existingAgent && !pendingApproval) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
existingAgentId: existingAgent?.id ?? null,
|
|
587
|
+
pendingApprovalId: pendingApproval?.id ?? null
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function parseApprovalPayload(payloadJson: string): { role?: string; managerAgentId?: string | null } {
|
|
592
|
+
try {
|
|
593
|
+
const parsed = JSON.parse(payloadJson) as Record<string, unknown>;
|
|
594
|
+
return {
|
|
595
|
+
role: typeof parsed.role === "string" ? parsed.role : undefined,
|
|
596
|
+
managerAgentId: typeof parsed.managerAgentId === "string" ? parsed.managerAgentId : null
|
|
597
|
+
};
|
|
598
|
+
} catch {
|
|
599
|
+
return {};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function duplicateMessage(input: { existingAgentId: string | null; pendingApprovalId: string | null }) {
|
|
604
|
+
if (input.existingAgentId && input.pendingApprovalId) {
|
|
605
|
+
return `Duplicate hire request blocked: existing agent ${input.existingAgentId} and pending approval ${input.pendingApprovalId}.`;
|
|
606
|
+
}
|
|
607
|
+
if (input.existingAgentId) {
|
|
608
|
+
return `Duplicate hire request blocked: existing agent ${input.existingAgentId}.`;
|
|
609
|
+
}
|
|
610
|
+
return `Duplicate hire request blocked: pending approval ${input.pendingApprovalId}.`;
|
|
611
|
+
}
|
package/src/routes/companies.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { createCompany, deleteCompany, listCompanies, updateCompany } from "bopodev-db";
|
|
4
4
|
import type { AppContext } from "../context";
|
|
5
5
|
import { sendError, sendOk } from "../http";
|
|
6
|
+
import { ensureCompanyBuiltinPluginDefaults } from "../services/plugin-runtime";
|
|
6
7
|
|
|
7
8
|
const createCompanySchema = z.object({
|
|
8
9
|
name: z.string().min(1),
|
|
@@ -30,6 +31,7 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
30
31
|
return sendError(res, parsed.error.message, 422);
|
|
31
32
|
}
|
|
32
33
|
const company = await createCompany(ctx.db, parsed.data);
|
|
34
|
+
await ensureCompanyBuiltinPluginDefaults(ctx.db, company.id);
|
|
33
35
|
return sendOk(res, company);
|
|
34
36
|
});
|
|
35
37
|
|
package/src/routes/governance.ts
CHANGED
|
@@ -163,11 +163,18 @@ export function createGovernanceRouter(ctx: AppContext) {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
if (resolution.execution.applied && resolution.execution.entityType && resolution.execution.entityId) {
|
|
166
|
+
const eventType =
|
|
167
|
+
resolution.action === "grant_plugin_capabilities"
|
|
168
|
+
? "plugin.capabilities_granted_from_approval"
|
|
169
|
+
: resolution.execution.entityType === "agent"
|
|
170
|
+
? "agent.hired_from_approval"
|
|
171
|
+
: resolution.execution.entityType === "goal"
|
|
172
|
+
? "goal.activated_from_approval"
|
|
173
|
+
: "memory.promoted_from_approval";
|
|
166
174
|
await appendAuditEvent(ctx.db, {
|
|
167
175
|
companyId: req.companyId!,
|
|
168
176
|
actorType: "human",
|
|
169
|
-
eventType
|
|
170
|
-
resolution.execution.entityType === "agent" ? "agent.hired_from_approval" : "goal.activated_from_approval",
|
|
177
|
+
eventType,
|
|
171
178
|
entityType: resolution.execution.entityType,
|
|
172
179
|
entityId: resolution.execution.entityId,
|
|
173
180
|
payload: resolution.execution.entity ?? { id: resolution.execution.entityId }
|
package/src/routes/heartbeats.ts
CHANGED
|
@@ -79,7 +79,8 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
79
79
|
const stopResult = await stopHeartbeatRun(ctx.db, req.companyId!, parsed.data.runId, {
|
|
80
80
|
requestId: req.requestId,
|
|
81
81
|
trigger: "manual",
|
|
82
|
-
actorId: req.actor?.id ?? undefined
|
|
82
|
+
actorId: req.actor?.id ?? undefined,
|
|
83
|
+
realtimeHub: ctx.realtimeHub
|
|
83
84
|
});
|
|
84
85
|
if (!stopResult.ok) {
|
|
85
86
|
if (stopResult.reason === "not_found") {
|