bopodev-api 0.1.8 → 0.1.10
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 +7 -4
- package/src/lib/agent-config.ts +255 -0
- package/src/lib/instance-paths.ts +88 -0
- package/src/lib/workspace-policy.ts +75 -0
- package/src/middleware/request-actor.ts +26 -5
- package/src/realtime/office-space.ts +7 -0
- package/src/routes/agents.ts +335 -66
- package/src/routes/heartbeats.ts +21 -2
- package/src/routes/issues.ts +122 -4
- package/src/routes/projects.ts +60 -3
- package/src/scripts/backfill-project-workspaces.ts +118 -0
- package/src/scripts/onboard-seed.ts +314 -0
- package/src/server.ts +43 -13
- package/src/services/governance-service.ts +144 -18
- package/src/services/heartbeat-service.ts +616 -3
- package/src/worker/scheduler.ts +6 -63
package/src/routes/agents.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
+
import { executeAgentRuntime } from "bopodev-agent-sdk";
|
|
5
|
+
import { AgentCreateRequestSchema, AgentUpdateRequestSchema } from "bopodev-contracts";
|
|
6
|
+
import {
|
|
7
|
+
appendAuditEvent,
|
|
8
|
+
createAgent,
|
|
9
|
+
createApprovalRequest,
|
|
10
|
+
deleteAgent,
|
|
11
|
+
getApprovalRequest,
|
|
12
|
+
listAgents,
|
|
13
|
+
updateAgent
|
|
14
|
+
} from "bopodev-db";
|
|
4
15
|
import type { AppContext } from "../context";
|
|
5
16
|
import { sendError, sendOk } from "../http";
|
|
17
|
+
import {
|
|
18
|
+
normalizeRuntimeConfig,
|
|
19
|
+
parseRuntimeConfigFromAgentRow,
|
|
20
|
+
requiresRuntimeCwd,
|
|
21
|
+
runtimeConfigToDb,
|
|
22
|
+
runtimeConfigToStateBlobPatch
|
|
23
|
+
} from "../lib/agent-config";
|
|
24
|
+
import { hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
6
25
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
7
26
|
import { requireBoardRole, requirePermission } from "../middleware/request-actor";
|
|
8
27
|
import { createGovernanceRealtimeEvent, serializeStoredApproval } from "../realtime/governance";
|
|
@@ -12,37 +31,74 @@ import {
|
|
|
12
31
|
} from "../realtime/office-space";
|
|
13
32
|
import { isApprovalRequired } from "../services/governance-service";
|
|
14
33
|
|
|
15
|
-
const
|
|
16
|
-
managerAgentId: z.string().optional(),
|
|
17
|
-
role: z.string().min(1),
|
|
18
|
-
name: z.string().min(1),
|
|
19
|
-
providerType: z.enum(["claude_code", "codex", "http", "shell"]),
|
|
20
|
-
heartbeatCron: z.string().default("*/5 * * * *"),
|
|
21
|
-
monthlyBudgetUsd: z.number().nonnegative().default(30),
|
|
22
|
-
canHireAgents: z.boolean().default(false),
|
|
23
|
-
requestApproval: z.boolean().default(true),
|
|
34
|
+
const legacyRuntimeConfigSchema = z.object({
|
|
24
35
|
runtimeCommand: z.string().optional(),
|
|
25
36
|
runtimeArgs: z.array(z.string()).optional(),
|
|
26
37
|
runtimeCwd: z.string().optional(),
|
|
27
|
-
runtimeTimeoutMs: z.number().int().positive().max(600000).optional()
|
|
38
|
+
runtimeTimeoutMs: z.number().int().positive().max(600000).optional(),
|
|
39
|
+
runtimeModel: z.string().optional(),
|
|
40
|
+
runtimeThinkingEffort: z.enum(["auto", "low", "medium", "high"]).optional(),
|
|
41
|
+
bootstrapPrompt: z.string().optional(),
|
|
42
|
+
runtimeTimeoutSec: z.number().int().nonnegative().optional(),
|
|
43
|
+
interruptGraceSec: z.number().int().nonnegative().optional(),
|
|
44
|
+
runtimeEnv: z.record(z.string(), z.string()).optional(),
|
|
45
|
+
runPolicy: z
|
|
46
|
+
.object({
|
|
47
|
+
sandboxMode: z.enum(["workspace_write", "full_access"]).optional(),
|
|
48
|
+
allowWebSearch: z.boolean().optional()
|
|
49
|
+
})
|
|
50
|
+
.optional()
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const createAgentSchema = AgentCreateRequestSchema.extend({
|
|
54
|
+
...legacyRuntimeConfigSchema.shape
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const updateAgentSchema = AgentUpdateRequestSchema.extend({
|
|
58
|
+
...legacyRuntimeConfigSchema.shape
|
|
28
59
|
});
|
|
29
60
|
|
|
30
|
-
const
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
const runtimePreflightSchema = z.object({
|
|
62
|
+
providerType: z.enum(["claude_code", "codex", "http", "shell"]),
|
|
63
|
+
runtimeConfig: z.record(z.string(), z.unknown()).optional(),
|
|
64
|
+
...legacyRuntimeConfigSchema.shape
|
|
65
|
+
});
|
|
66
|
+
const CODEX_AUTH_REQUIRED_RE =
|
|
67
|
+
/(not\s+logged\s+in|login\s+required|authentication\s+required|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|api[_\s-]?key.*required|missing bearer|missing bearer or basic authentication)/i;
|
|
68
|
+
const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
69
|
+
"managerAgentId",
|
|
70
|
+
"role",
|
|
71
|
+
"name",
|
|
72
|
+
"providerType",
|
|
73
|
+
"status",
|
|
74
|
+
"heartbeatCron",
|
|
75
|
+
"monthlyBudgetUsd",
|
|
76
|
+
"canHireAgents",
|
|
77
|
+
"runtimeConfig",
|
|
78
|
+
"runtimeCommand",
|
|
79
|
+
"runtimeArgs",
|
|
80
|
+
"runtimeCwd",
|
|
81
|
+
"runtimeTimeoutMs",
|
|
82
|
+
"runtimeModel",
|
|
83
|
+
"runtimeThinkingEffort",
|
|
84
|
+
"bootstrapPrompt",
|
|
85
|
+
"runtimeTimeoutSec",
|
|
86
|
+
"interruptGraceSec",
|
|
87
|
+
"runtimeEnv",
|
|
88
|
+
"runPolicy"
|
|
89
|
+
]);
|
|
90
|
+
const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
|
|
91
|
+
"runtimeCommand",
|
|
92
|
+
"runtimeArgs",
|
|
93
|
+
"runtimeCwd",
|
|
94
|
+
"runtimeEnv",
|
|
95
|
+
"runtimeModel",
|
|
96
|
+
"runtimeThinkingEffort",
|
|
97
|
+
"bootstrapPrompt",
|
|
98
|
+
"runtimeTimeoutSec",
|
|
99
|
+
"interruptGraceSec",
|
|
100
|
+
"runPolicy"
|
|
101
|
+
]);
|
|
46
102
|
|
|
47
103
|
function toAgentResponse(agent: Record<string, unknown>) {
|
|
48
104
|
return {
|
|
@@ -65,22 +121,195 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
65
121
|
);
|
|
66
122
|
});
|
|
67
123
|
|
|
124
|
+
router.get("/runtime-default-cwd", async (req, res) => {
|
|
125
|
+
const runtimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
|
|
126
|
+
await mkdir(runtimeCwd, { recursive: true });
|
|
127
|
+
return sendOk(res, { runtimeCwd });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
router.post("/runtime-preflight", async (req, res) => {
|
|
131
|
+
const parsed = runtimePreflightSchema.safeParse(req.body);
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
return sendError(res, parsed.error.message, 422);
|
|
134
|
+
}
|
|
135
|
+
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
|
|
136
|
+
const runtimeConfig = normalizeRuntimeConfig({
|
|
137
|
+
runtimeConfig: parsed.data.runtimeConfig,
|
|
138
|
+
legacy: {
|
|
139
|
+
runtimeCommand: parsed.data.runtimeCommand,
|
|
140
|
+
runtimeArgs: parsed.data.runtimeArgs,
|
|
141
|
+
runtimeCwd: parsed.data.runtimeCwd,
|
|
142
|
+
runtimeTimeoutMs: parsed.data.runtimeTimeoutMs,
|
|
143
|
+
runtimeModel: parsed.data.runtimeModel,
|
|
144
|
+
runtimeThinkingEffort: parsed.data.runtimeThinkingEffort,
|
|
145
|
+
bootstrapPrompt: parsed.data.bootstrapPrompt,
|
|
146
|
+
runtimeTimeoutSec: parsed.data.runtimeTimeoutSec,
|
|
147
|
+
interruptGraceSec: parsed.data.interruptGraceSec,
|
|
148
|
+
runtimeEnv: parsed.data.runtimeEnv,
|
|
149
|
+
runPolicy: parsed.data.runPolicy
|
|
150
|
+
},
|
|
151
|
+
defaultRuntimeCwd
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const checks: Array<{
|
|
155
|
+
code: string;
|
|
156
|
+
level: "info" | "warn" | "error";
|
|
157
|
+
message: string;
|
|
158
|
+
detail?: string;
|
|
159
|
+
hint?: string;
|
|
160
|
+
}> = [];
|
|
161
|
+
|
|
162
|
+
if (parsed.data.providerType !== "codex") {
|
|
163
|
+
return sendOk(res, {
|
|
164
|
+
status: "pass",
|
|
165
|
+
testedAt: new Date().toISOString(),
|
|
166
|
+
checks: [
|
|
167
|
+
{
|
|
168
|
+
code: "preflight_not_required",
|
|
169
|
+
level: "info",
|
|
170
|
+
message: "Preflight probe is currently required only for Codex runtime."
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!runtimeConfig.runtimeCwd) {
|
|
177
|
+
checks.push({
|
|
178
|
+
code: "codex_cwd_missing",
|
|
179
|
+
level: "error",
|
|
180
|
+
message: "Runtime working directory is required for Codex preflight."
|
|
181
|
+
});
|
|
182
|
+
return sendOk(res, { status: "fail", testedAt: new Date().toISOString(), checks });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await mkdir(runtimeConfig.runtimeCwd, { recursive: true });
|
|
186
|
+
|
|
187
|
+
const timeoutMs =
|
|
188
|
+
runtimeConfig.runtimeTimeoutSec > 0 ? Math.min(runtimeConfig.runtimeTimeoutSec * 1000, 45_000) : 45_000;
|
|
189
|
+
const probe = await executeAgentRuntime("codex", "Respond with hello.", {
|
|
190
|
+
command: runtimeConfig.runtimeCommand,
|
|
191
|
+
args: runtimeConfig.runtimeArgs,
|
|
192
|
+
cwd: runtimeConfig.runtimeCwd,
|
|
193
|
+
env: runtimeConfig.runtimeEnv,
|
|
194
|
+
model: runtimeConfig.runtimeModel,
|
|
195
|
+
thinkingEffort: runtimeConfig.runtimeThinkingEffort,
|
|
196
|
+
runPolicy: runtimeConfig.runPolicy,
|
|
197
|
+
timeoutMs,
|
|
198
|
+
retryCount: 0
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (probe.ok) {
|
|
202
|
+
const summary = (probe.parsedUsage?.summary ?? "").trim();
|
|
203
|
+
const hasHello = /\bhello\b/i.test(summary || probe.stdout);
|
|
204
|
+
checks.push({
|
|
205
|
+
code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
|
|
206
|
+
level: hasHello ? "info" : "warn",
|
|
207
|
+
message: hasHello ? "Codex preflight probe succeeded." : "Codex probe succeeded but response was unexpected.",
|
|
208
|
+
...(summary ? { detail: summary.slice(0, 240) } : {})
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
const detail = `${probe.stderr || ""}\n${probe.stdout || ""}`.trim().slice(0, 500);
|
|
212
|
+
if (probe.failureType === "spawn_error") {
|
|
213
|
+
checks.push({
|
|
214
|
+
code: "codex_command_unresolvable",
|
|
215
|
+
level: "error",
|
|
216
|
+
message: "Codex command is not executable from this runtime configuration.",
|
|
217
|
+
detail,
|
|
218
|
+
hint: "Install Codex CLI or set runtime command to an executable path."
|
|
219
|
+
});
|
|
220
|
+
} else if (probe.timedOut) {
|
|
221
|
+
checks.push({
|
|
222
|
+
code: "codex_hello_probe_timed_out",
|
|
223
|
+
level: "warn",
|
|
224
|
+
message: "Codex preflight timed out.",
|
|
225
|
+
detail,
|
|
226
|
+
hint: "Retry preflight. If this repeats, check runtime command/cwd and local Codex health."
|
|
227
|
+
});
|
|
228
|
+
} else if (CODEX_AUTH_REQUIRED_RE.test(detail)) {
|
|
229
|
+
checks.push({
|
|
230
|
+
code: "codex_auth_required",
|
|
231
|
+
level: "warn",
|
|
232
|
+
message: "Codex authentication is not ready for this runtime.",
|
|
233
|
+
detail,
|
|
234
|
+
hint: "Run `codex login` locally or set a global `BOPO_OPENAI_API_KEY`/`OPENAI_API_KEY`."
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
checks.push({
|
|
238
|
+
code: "codex_hello_probe_failed",
|
|
239
|
+
level: "error",
|
|
240
|
+
message: "Codex preflight failed.",
|
|
241
|
+
detail,
|
|
242
|
+
hint: "Run `codex exec --json -` manually in the runtime directory with prompt `Respond with hello.`."
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const status =
|
|
248
|
+
checks.some((check) => check.level === "error")
|
|
249
|
+
? "fail"
|
|
250
|
+
: checks.some((check) => check.level === "warn")
|
|
251
|
+
? "warn"
|
|
252
|
+
: "pass";
|
|
253
|
+
return sendOk(res, {
|
|
254
|
+
status,
|
|
255
|
+
testedAt: new Date().toISOString(),
|
|
256
|
+
checks
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
68
260
|
router.post("/", async (req, res) => {
|
|
69
261
|
const requireCreate = requirePermission("agents:write");
|
|
70
262
|
requireCreate(req, res, () => {});
|
|
71
263
|
if (res.headersSent) {
|
|
72
264
|
return;
|
|
73
265
|
}
|
|
266
|
+
if (req.actor?.type === "agent") {
|
|
267
|
+
const companyAgents = await listAgents(ctx.db, req.companyId!);
|
|
268
|
+
const requestingAgent = companyAgents.find((row) => row.id === req.actor?.id);
|
|
269
|
+
if (!requestingAgent) {
|
|
270
|
+
return sendError(res, "Requesting agent not found.", 403);
|
|
271
|
+
}
|
|
272
|
+
if (!requestingAgent.canHireAgents) {
|
|
273
|
+
return sendError(res, "This agent is not allowed to create new agents.", 403);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
74
276
|
const parsed = createAgentSchema.safeParse(req.body);
|
|
75
277
|
if (!parsed.success) {
|
|
76
278
|
return sendError(res, parsed.error.message, 422);
|
|
77
279
|
}
|
|
280
|
+
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
|
|
281
|
+
const runtimeConfig = normalizeRuntimeConfig({
|
|
282
|
+
runtimeConfig: parsed.data.runtimeConfig,
|
|
283
|
+
legacy: {
|
|
284
|
+
runtimeCommand: parsed.data.runtimeCommand,
|
|
285
|
+
runtimeArgs: parsed.data.runtimeArgs,
|
|
286
|
+
runtimeCwd: parsed.data.runtimeCwd,
|
|
287
|
+
runtimeTimeoutMs: parsed.data.runtimeTimeoutMs,
|
|
288
|
+
runtimeModel: parsed.data.runtimeModel,
|
|
289
|
+
runtimeThinkingEffort: parsed.data.runtimeThinkingEffort,
|
|
290
|
+
bootstrapPrompt: parsed.data.bootstrapPrompt,
|
|
291
|
+
runtimeTimeoutSec: parsed.data.runtimeTimeoutSec,
|
|
292
|
+
interruptGraceSec: parsed.data.interruptGraceSec,
|
|
293
|
+
runtimeEnv: parsed.data.runtimeEnv,
|
|
294
|
+
runPolicy: parsed.data.runPolicy
|
|
295
|
+
},
|
|
296
|
+
defaultRuntimeCwd
|
|
297
|
+
});
|
|
298
|
+
if (requiresRuntimeCwd(parsed.data.providerType) && !hasText(runtimeConfig.runtimeCwd)) {
|
|
299
|
+
return sendError(res, "Runtime working directory is required for this runtime provider.", 422);
|
|
300
|
+
}
|
|
301
|
+
if (requiresRuntimeCwd(parsed.data.providerType) && hasText(runtimeConfig.runtimeCwd)) {
|
|
302
|
+
await mkdir(runtimeConfig.runtimeCwd!, { recursive: true });
|
|
303
|
+
}
|
|
78
304
|
|
|
79
305
|
if (parsed.data.requestApproval && isApprovalRequired("hire_agent")) {
|
|
80
306
|
const approvalId = await createApprovalRequest(ctx.db, {
|
|
81
307
|
companyId: req.companyId!,
|
|
82
308
|
action: "hire_agent",
|
|
83
|
-
payload:
|
|
309
|
+
payload: {
|
|
310
|
+
...parsed.data,
|
|
311
|
+
runtimeConfig
|
|
312
|
+
}
|
|
84
313
|
});
|
|
85
314
|
const approval = await getApprovalRequest(ctx.db, req.companyId!, approvalId);
|
|
86
315
|
if (approval) {
|
|
@@ -104,14 +333,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
104
333
|
heartbeatCron: parsed.data.heartbeatCron,
|
|
105
334
|
monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
|
|
106
335
|
canHireAgents: parsed.data.canHireAgents,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
command: parsed.data.runtimeCommand,
|
|
110
|
-
args: parsed.data.runtimeArgs,
|
|
111
|
-
cwd: parsed.data.runtimeCwd,
|
|
112
|
-
timeoutMs: parsed.data.runtimeTimeoutMs
|
|
113
|
-
}
|
|
114
|
-
}
|
|
336
|
+
...runtimeConfigToDb(runtimeConfig),
|
|
337
|
+
initialState: runtimeConfigToStateBlobPatch(runtimeConfig)
|
|
115
338
|
});
|
|
116
339
|
|
|
117
340
|
await appendAuditEvent(ctx.db, {
|
|
@@ -132,6 +355,15 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
132
355
|
if (res.headersSent) {
|
|
133
356
|
return;
|
|
134
357
|
}
|
|
358
|
+
const unsupportedKeys = listUnsupportedAgentUpdateKeys(req.body);
|
|
359
|
+
if (unsupportedKeys.length > 0) {
|
|
360
|
+
return sendError(
|
|
361
|
+
res,
|
|
362
|
+
`Unsupported agent update fields: ${unsupportedKeys.join(", ")}. Supported fields: ${Array.from(UPDATE_AGENT_ALLOWED_KEYS).join(", ")}.`,
|
|
363
|
+
422
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
135
367
|
const parsed = updateAgentSchema.safeParse(req.body);
|
|
136
368
|
if (!parsed.success) {
|
|
137
369
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -141,39 +373,56 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
141
373
|
if (!existingAgent) {
|
|
142
374
|
return sendError(res, "Agent not found.", 404);
|
|
143
375
|
}
|
|
144
|
-
|
|
145
|
-
const
|
|
376
|
+
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
|
|
377
|
+
const existingRuntime = parseRuntimeConfigFromAgentRow(existingAgent as unknown as Record<string, unknown>);
|
|
378
|
+
const effectiveProviderType = parsed.data.providerType ?? existingAgent.providerType;
|
|
379
|
+
const hasRuntimeInput =
|
|
380
|
+
parsed.data.runtimeConfig !== undefined ||
|
|
146
381
|
parsed.data.runtimeCommand !== undefined ||
|
|
147
382
|
parsed.data.runtimeArgs !== undefined ||
|
|
148
383
|
parsed.data.runtimeCwd !== undefined ||
|
|
149
|
-
parsed.data.runtimeTimeoutMs !== undefined
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
384
|
+
parsed.data.runtimeTimeoutMs !== undefined ||
|
|
385
|
+
parsed.data.runtimeModel !== undefined ||
|
|
386
|
+
parsed.data.runtimeThinkingEffort !== undefined ||
|
|
387
|
+
parsed.data.bootstrapPrompt !== undefined ||
|
|
388
|
+
parsed.data.runtimeTimeoutSec !== undefined ||
|
|
389
|
+
parsed.data.interruptGraceSec !== undefined ||
|
|
390
|
+
parsed.data.runtimeEnv !== undefined ||
|
|
391
|
+
parsed.data.runPolicy !== undefined;
|
|
392
|
+
const nextRuntime = {
|
|
393
|
+
...existingRuntime,
|
|
394
|
+
...(hasRuntimeInput
|
|
395
|
+
? normalizeRuntimeConfig({
|
|
396
|
+
runtimeConfig: {
|
|
397
|
+
...existingRuntime,
|
|
398
|
+
...(parsed.data.runtimeConfig ?? {})
|
|
399
|
+
},
|
|
400
|
+
legacy: {
|
|
401
|
+
runtimeCommand: parsed.data.runtimeCommand,
|
|
402
|
+
runtimeArgs: parsed.data.runtimeArgs,
|
|
403
|
+
runtimeCwd: parsed.data.runtimeCwd,
|
|
404
|
+
runtimeTimeoutMs: parsed.data.runtimeTimeoutMs,
|
|
405
|
+
runtimeModel: parsed.data.runtimeModel,
|
|
406
|
+
runtimeThinkingEffort: parsed.data.runtimeThinkingEffort,
|
|
407
|
+
bootstrapPrompt: parsed.data.bootstrapPrompt,
|
|
408
|
+
runtimeTimeoutSec: parsed.data.runtimeTimeoutSec ?? existingRuntime.runtimeTimeoutSec,
|
|
409
|
+
interruptGraceSec: parsed.data.interruptGraceSec,
|
|
410
|
+
runtimeEnv: parsed.data.runtimeEnv,
|
|
411
|
+
runPolicy: parsed.data.runPolicy
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
: {})
|
|
415
|
+
};
|
|
416
|
+
if (!nextRuntime.runtimeCwd && defaultRuntimeCwd) {
|
|
417
|
+
nextRuntime.runtimeCwd = defaultRuntimeCwd;
|
|
418
|
+
}
|
|
419
|
+
const effectiveRuntimeCwd = nextRuntime.runtimeCwd ?? "";
|
|
420
|
+
if (requiresRuntimeCwd(effectiveProviderType) && !hasText(effectiveRuntimeCwd)) {
|
|
421
|
+
return sendError(res, "Runtime working directory is required for this runtime provider.", 422);
|
|
422
|
+
}
|
|
423
|
+
if (requiresRuntimeCwd(effectiveProviderType) && hasText(effectiveRuntimeCwd)) {
|
|
424
|
+
await mkdir(effectiveRuntimeCwd, { recursive: true });
|
|
175
425
|
}
|
|
176
|
-
|
|
177
426
|
const agent = await updateAgent(ctx.db, {
|
|
178
427
|
companyId: req.companyId!,
|
|
179
428
|
id: req.params.agentId,
|
|
@@ -186,7 +435,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
186
435
|
monthlyBudgetUsd:
|
|
187
436
|
typeof parsed.data.monthlyBudgetUsd === "number" ? parsed.data.monthlyBudgetUsd.toFixed(4) : undefined,
|
|
188
437
|
canHireAgents: parsed.data.canHireAgents,
|
|
189
|
-
|
|
438
|
+
...runtimeConfigToDb(nextRuntime),
|
|
439
|
+
stateBlob: runtimeConfigToStateBlobPatch(nextRuntime)
|
|
190
440
|
});
|
|
191
441
|
if (!agent) {
|
|
192
442
|
return sendError(res, "Agent not found.", 404);
|
|
@@ -303,3 +553,22 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
303
553
|
|
|
304
554
|
return router;
|
|
305
555
|
}
|
|
556
|
+
|
|
557
|
+
function listUnsupportedAgentUpdateKeys(payload: unknown) {
|
|
558
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
559
|
+
return [] as string[];
|
|
560
|
+
}
|
|
561
|
+
const body = payload as Record<string, unknown>;
|
|
562
|
+
const unsupported = Object.keys(body)
|
|
563
|
+
.filter((key) => !UPDATE_AGENT_ALLOWED_KEYS.has(key))
|
|
564
|
+
.sort();
|
|
565
|
+
const runtimeConfig = body.runtimeConfig;
|
|
566
|
+
if (runtimeConfig && typeof runtimeConfig === "object" && !Array.isArray(runtimeConfig)) {
|
|
567
|
+
for (const key of Object.keys(runtimeConfig as Record<string, unknown>).sort()) {
|
|
568
|
+
if (!UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS.has(key)) {
|
|
569
|
+
unsupported.push(`runtimeConfig.${key}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return unsupported;
|
|
574
|
+
}
|
package/src/routes/heartbeats.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { and, eq } from "drizzle-orm";
|
|
4
|
-
import { agents } from "bopodev-db";
|
|
4
|
+
import { agents, heartbeatRuns } from "bopodev-db";
|
|
5
5
|
import type { AppContext } from "../context";
|
|
6
6
|
import { sendError, sendOk } from "../http";
|
|
7
7
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
@@ -42,7 +42,26 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
42
42
|
trigger: "manual",
|
|
43
43
|
realtimeHub: ctx.realtimeHub
|
|
44
44
|
});
|
|
45
|
-
|
|
45
|
+
if (!runId) {
|
|
46
|
+
return sendError(res, "Heartbeat could not be started for this agent.", 409);
|
|
47
|
+
}
|
|
48
|
+
const [runRow] = await ctx.db
|
|
49
|
+
.select({ id: heartbeatRuns.id, status: heartbeatRuns.status, message: heartbeatRuns.message })
|
|
50
|
+
.from(heartbeatRuns)
|
|
51
|
+
.where(and(eq(heartbeatRuns.companyId, req.companyId!), eq(heartbeatRuns.id, runId)))
|
|
52
|
+
.limit(1);
|
|
53
|
+
const invokeStatus =
|
|
54
|
+
runRow?.status === "skipped" && String(runRow.message ?? "").includes("already in progress")
|
|
55
|
+
? "skipped_overlap"
|
|
56
|
+
: runRow?.status === "skipped"
|
|
57
|
+
? "skipped"
|
|
58
|
+
: "started";
|
|
59
|
+
return sendOk(res, {
|
|
60
|
+
runId,
|
|
61
|
+
requestId: req.requestId,
|
|
62
|
+
status: invokeStatus,
|
|
63
|
+
message: runRow?.message ?? null
|
|
64
|
+
});
|
|
46
65
|
});
|
|
47
66
|
|
|
48
67
|
router.post("/sweep", async (req, res) => {
|