bopodev-api 0.1.28 → 0.1.29
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 +4 -4
- package/src/app.ts +17 -69
- package/src/lib/run-artifact-paths.ts +8 -0
- package/src/middleware/cors-config.ts +36 -0
- package/src/middleware/request-actor.ts +10 -16
- package/src/middleware/request-id.ts +9 -0
- package/src/middleware/request-logging.ts +24 -0
- package/src/routes/agents.ts +3 -9
- package/src/routes/companies.ts +18 -1
- package/src/routes/goals.ts +7 -13
- package/src/routes/governance.ts +2 -5
- package/src/routes/heartbeats.ts +7 -25
- package/src/routes/issues.ts +62 -120
- package/src/routes/observability.ts +6 -1
- package/src/routes/plugins.ts +5 -17
- package/src/routes/projects.ts +7 -25
- package/src/routes/templates.ts +6 -21
- package/src/scripts/onboard-seed.ts +5 -7
- package/src/server.ts +33 -292
- package/src/services/company-export-service.ts +63 -0
- package/src/services/governance-service.ts +4 -1
- package/src/services/heartbeat-service/active-runs.ts +15 -0
- package/src/services/heartbeat-service/budget-override.ts +46 -0
- package/src/services/heartbeat-service/claims.ts +61 -0
- package/src/services/heartbeat-service/cron.ts +58 -0
- package/src/services/heartbeat-service/heartbeat-realtime.ts +28 -0
- package/src/services/heartbeat-service/heartbeat-run-summary-text.ts +53 -0
- package/src/services/{heartbeat-service.ts → heartbeat-service/heartbeat-run.ts} +183 -633
- package/src/services/heartbeat-service/index.ts +5 -0
- package/src/services/heartbeat-service/stop.ts +90 -0
- package/src/services/heartbeat-service/sweep.ts +145 -0
- package/src/services/heartbeat-service/types.ts +65 -0
- package/src/services/memory-file-service.ts +10 -2
- package/src/shutdown/graceful-shutdown.ts +77 -0
- package/src/startup/database.ts +41 -0
- package/src/startup/deployment-validation.ts +37 -0
- package/src/startup/env.ts +17 -0
- package/src/startup/runtime-health.ts +128 -0
- package/src/startup/scheduler-config.ts +39 -0
- package/src/types/express.d.ts +13 -0
- package/src/types/request-actor.ts +6 -0
- package/src/validation/issue-routes.ts +79 -0
package/src/routes/issues.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { Router } from "express";
|
|
|
2
2
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, extname, join, resolve } from "node:path";
|
|
4
4
|
import multer from "multer";
|
|
5
|
-
import { z } from "zod";
|
|
6
5
|
import { IssueDetailSchema, IssueSchema } from "bopodev-contracts";
|
|
7
6
|
import {
|
|
8
7
|
addIssueAttachment,
|
|
@@ -25,6 +24,7 @@ import {
|
|
|
25
24
|
listIssueAttachments,
|
|
26
25
|
listIssueActivity,
|
|
27
26
|
listIssueComments,
|
|
27
|
+
listIssueGoalIdsBatch,
|
|
28
28
|
listIssues,
|
|
29
29
|
projects,
|
|
30
30
|
projectWorkspaces,
|
|
@@ -42,70 +42,16 @@ import {
|
|
|
42
42
|
} from "../lib/comment-recipients";
|
|
43
43
|
import { isInsidePath, normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
44
44
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
45
|
-
import {
|
|
45
|
+
import { enforcePermission } from "../middleware/request-actor";
|
|
46
46
|
import { triggerIssueCommentDispatchWorker } from "../services/comment-recipient-dispatch-service";
|
|
47
47
|
import { publishAttentionSnapshot } from "../realtime/attention";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.object({
|
|
56
|
-
delegatedHiringIntent: z
|
|
57
|
-
.object({
|
|
58
|
-
intentType: z.literal("agent_hiring_request"),
|
|
59
|
-
requestedRole: z.string().nullable().optional(),
|
|
60
|
-
requestedRoleKey: z.string().nullable().optional(),
|
|
61
|
-
requestedTitle: z.string().nullable().optional(),
|
|
62
|
-
requestedName: z.string().nullable().optional(),
|
|
63
|
-
requestedManagerAgentId: z.string().nullable().optional(),
|
|
64
|
-
requestedProviderType: z.string().nullable().optional(),
|
|
65
|
-
requestedRuntimeModel: z.string().nullable().optional()
|
|
66
|
-
})
|
|
67
|
-
.optional()
|
|
68
|
-
})
|
|
69
|
-
.optional(),
|
|
70
|
-
status: z.enum(["todo", "in_progress", "blocked", "in_review", "done", "canceled"]).default("todo"),
|
|
71
|
-
priority: z.enum(["none", "low", "medium", "high", "urgent"]).default("none"),
|
|
72
|
-
assigneeAgentId: z.string().nullable().optional(),
|
|
73
|
-
labels: z.array(z.string()).default([]),
|
|
74
|
-
tags: z.array(z.string()).default([])
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const createIssueCommentSchema = z.object({
|
|
78
|
-
body: z.string().min(1),
|
|
79
|
-
recipients: z
|
|
80
|
-
.array(
|
|
81
|
-
z.object({
|
|
82
|
-
recipientType: z.enum(["agent", "board", "member"]),
|
|
83
|
-
recipientId: z.string().nullable().optional()
|
|
84
|
-
})
|
|
85
|
-
)
|
|
86
|
-
.default([]),
|
|
87
|
-
authorType: z.enum(["human", "agent", "system"]).optional(),
|
|
88
|
-
authorId: z.string().optional()
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const createIssueCommentLegacySchema = z.object({
|
|
92
|
-
issueId: z.string().min(1),
|
|
93
|
-
body: z.string().min(1),
|
|
94
|
-
recipients: z
|
|
95
|
-
.array(
|
|
96
|
-
z.object({
|
|
97
|
-
recipientType: z.enum(["agent", "board", "member"]),
|
|
98
|
-
recipientId: z.string().nullable().optional()
|
|
99
|
-
})
|
|
100
|
-
)
|
|
101
|
-
.default([]),
|
|
102
|
-
authorType: z.enum(["human", "agent", "system"]).optional(),
|
|
103
|
-
authorId: z.string().optional()
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const updateIssueCommentSchema = z.object({
|
|
107
|
-
body: z.string().min(1)
|
|
108
|
-
});
|
|
48
|
+
import {
|
|
49
|
+
createIssueCommentLegacySchema,
|
|
50
|
+
createIssueCommentSchema,
|
|
51
|
+
createIssueSchema,
|
|
52
|
+
updateIssueCommentSchema,
|
|
53
|
+
updateIssueSchema
|
|
54
|
+
} from "../validation/issue-routes";
|
|
109
55
|
|
|
110
56
|
const MAX_ATTACHMENTS_PER_REQUEST = parsePositiveIntEnv("BOPO_ISSUE_ATTACHMENTS_MAX_FILES", 10);
|
|
111
57
|
const MAX_ATTACHMENT_SIZE_BYTES = parsePositiveIntEnv("BOPO_ISSUE_ATTACHMENTS_MAX_BYTES", 20 * 1024 * 1024);
|
|
@@ -148,30 +94,32 @@ function parseStringArray(value: unknown) {
|
|
|
148
94
|
}
|
|
149
95
|
}
|
|
150
96
|
|
|
151
|
-
function
|
|
97
|
+
function normalizeOptionalExternalLink(value: string | null | undefined) {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const trimmed = value?.trim() ?? "";
|
|
102
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toIssueResponse(issue: Record<string, unknown>, goalIds: string[] = []) {
|
|
152
106
|
const labels = parseStringArray(issue.labelsJson);
|
|
153
107
|
const tags = parseStringArray(issue.tagsJson);
|
|
154
|
-
const { labelsJson: _labelsJson, tagsJson: _tagsJson, ...rest } = issue
|
|
108
|
+
const { labelsJson: _labelsJson, tagsJson: _tagsJson, goalId: _legacyGoalId, ...rest } = issue as Record<string, unknown> & {
|
|
109
|
+
goalId?: unknown;
|
|
110
|
+
};
|
|
111
|
+
const externalRaw = rest.externalLink;
|
|
112
|
+
const externalLink =
|
|
113
|
+
typeof externalRaw === "string" && externalRaw.trim().length > 0 ? externalRaw.trim() : null;
|
|
155
114
|
return {
|
|
156
115
|
...rest,
|
|
116
|
+
externalLink,
|
|
157
117
|
labels,
|
|
158
|
-
tags
|
|
118
|
+
tags,
|
|
119
|
+
goalIds
|
|
159
120
|
};
|
|
160
121
|
}
|
|
161
122
|
|
|
162
|
-
const updateIssueSchema = z
|
|
163
|
-
.object({
|
|
164
|
-
projectId: z.string().min(1).optional(),
|
|
165
|
-
title: z.string().min(1).optional(),
|
|
166
|
-
body: z.string().nullable().optional(),
|
|
167
|
-
status: z.enum(["todo", "in_progress", "blocked", "in_review", "done", "canceled"]).optional(),
|
|
168
|
-
priority: z.enum(["none", "low", "medium", "high", "urgent"]).optional(),
|
|
169
|
-
assigneeAgentId: z.string().nullable().optional(),
|
|
170
|
-
labels: z.array(z.string()).optional(),
|
|
171
|
-
tags: z.array(z.string()).optional()
|
|
172
|
-
})
|
|
173
|
-
.refine((payload) => Object.keys(payload).length > 0, "At least one field must be provided.");
|
|
174
|
-
|
|
175
123
|
export function createIssuesRouter(ctx: AppContext) {
|
|
176
124
|
const router = Router();
|
|
177
125
|
const upload = multer({
|
|
@@ -186,10 +134,17 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
186
134
|
router.get("/", async (req, res) => {
|
|
187
135
|
const projectId = req.query.projectId?.toString();
|
|
188
136
|
const rows = await listIssues(ctx.db, req.companyId!, projectId);
|
|
137
|
+
const goalMap = await listIssueGoalIdsBatch(
|
|
138
|
+
ctx.db,
|
|
139
|
+
req.companyId!,
|
|
140
|
+
rows.map((row) => row.id)
|
|
141
|
+
);
|
|
189
142
|
return sendOkValidated(
|
|
190
143
|
res,
|
|
191
144
|
IssueSchema.array(),
|
|
192
|
-
rows.map((row) =>
|
|
145
|
+
rows.map((row) =>
|
|
146
|
+
toIssueResponse(row as unknown as Record<string, unknown>, goalMap.get(row.id) ?? [])
|
|
147
|
+
),
|
|
193
148
|
"issues.list"
|
|
194
149
|
);
|
|
195
150
|
});
|
|
@@ -200,7 +155,8 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
200
155
|
if (!issueRow) {
|
|
201
156
|
return sendError(res, "Issue not found.", 404);
|
|
202
157
|
}
|
|
203
|
-
const
|
|
158
|
+
const goalMap = await listIssueGoalIdsBatch(ctx.db, req.companyId!, [issueId]);
|
|
159
|
+
const base = toIssueResponse(issueRow as unknown as Record<string, unknown>, goalMap.get(issueId) ?? []);
|
|
204
160
|
const attachmentRows = await listIssueAttachments(ctx.db, req.companyId!, issueId);
|
|
205
161
|
const attachments = attachmentRows.map((row) =>
|
|
206
162
|
toIssueAttachmentResponse(row as unknown as Record<string, unknown>, issueId)
|
|
@@ -209,10 +165,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
209
165
|
});
|
|
210
166
|
|
|
211
167
|
router.post("/", async (req, res) => {
|
|
212
|
-
|
|
213
|
-
if (res.headersSent) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
168
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
216
169
|
const parsed = createIssueSchema.safeParse(req.body);
|
|
217
170
|
if (!parsed.success) {
|
|
218
171
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -232,8 +185,10 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
232
185
|
companyId: req.companyId!,
|
|
233
186
|
projectId: parsed.data.projectId,
|
|
234
187
|
parentIssueId: parsed.data.parentIssueId,
|
|
188
|
+
goalIds: parsed.data.goalIds,
|
|
235
189
|
title: parsed.data.title,
|
|
236
190
|
body: applyIssueMetadataToBody(parsed.data.body, parsed.data.metadata),
|
|
191
|
+
externalLink: normalizeOptionalExternalLink(parsed.data.externalLink ?? null),
|
|
237
192
|
status: parsed.data.status,
|
|
238
193
|
priority: parsed.data.priority,
|
|
239
194
|
assigneeAgentId: parsed.data.assigneeAgentId,
|
|
@@ -255,14 +210,14 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
255
210
|
entityId: issue.id,
|
|
256
211
|
payload: issue
|
|
257
212
|
});
|
|
258
|
-
return sendOk(
|
|
213
|
+
return sendOk(
|
|
214
|
+
res,
|
|
215
|
+
toIssueResponse(issue as unknown as Record<string, unknown>, parsed.data.goalIds)
|
|
216
|
+
);
|
|
259
217
|
});
|
|
260
218
|
|
|
261
219
|
router.post("/:issueId/attachments", async (req, res) => {
|
|
262
|
-
|
|
263
|
-
if (res.headersSent) {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
220
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
266
221
|
|
|
267
222
|
upload.array("files", MAX_ATTACHMENTS_PER_REQUEST)(req, res, async (uploadError) => {
|
|
268
223
|
if (uploadError) {
|
|
@@ -409,10 +364,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
409
364
|
});
|
|
410
365
|
|
|
411
366
|
router.delete("/:issueId/attachments/:attachmentId", async (req, res) => {
|
|
412
|
-
|
|
413
|
-
if (res.headersSent) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
367
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
416
368
|
const issueContext = await getIssueContextForAttachment(ctx, req.companyId!, req.params.issueId);
|
|
417
369
|
if (!issueContext) {
|
|
418
370
|
return sendError(res, "Issue not found.", 404);
|
|
@@ -468,10 +420,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
468
420
|
});
|
|
469
421
|
|
|
470
422
|
router.post("/:issueId/comments", async (req, res) => {
|
|
471
|
-
|
|
472
|
-
if (res.headersSent) {
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
423
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
475
424
|
const parsed = createIssueCommentSchema.safeParse(req.body);
|
|
476
425
|
if (!parsed.success) {
|
|
477
426
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -487,10 +436,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
487
436
|
|
|
488
437
|
// Backward-compatible endpoint used by older clients.
|
|
489
438
|
router.post("/comment", async (req, res) => {
|
|
490
|
-
|
|
491
|
-
if (res.headersSent) {
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
439
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
494
440
|
const parsed = createIssueCommentLegacySchema.safeParse(req.body);
|
|
495
441
|
if (!parsed.success) {
|
|
496
442
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -505,10 +451,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
505
451
|
});
|
|
506
452
|
|
|
507
453
|
router.put("/:issueId/comments/:commentId", async (req, res) => {
|
|
508
|
-
|
|
509
|
-
if (res.headersSent) {
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
454
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
512
455
|
const parsed = updateIssueCommentSchema.safeParse(req.body);
|
|
513
456
|
if (!parsed.success) {
|
|
514
457
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -550,10 +493,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
550
493
|
});
|
|
551
494
|
|
|
552
495
|
router.delete("/:issueId/comments/:commentId", async (req, res) => {
|
|
553
|
-
|
|
554
|
-
if (res.headersSent) {
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
496
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
557
497
|
const deleted = await deleteIssueComment(ctx.db, req.companyId!, req.params.issueId, req.params.commentId);
|
|
558
498
|
if (!deleted) {
|
|
559
499
|
return sendError(res, "Comment not found.", 404);
|
|
@@ -580,10 +520,7 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
580
520
|
});
|
|
581
521
|
|
|
582
522
|
router.put("/:issueId", async (req, res) => {
|
|
583
|
-
|
|
584
|
-
if (res.headersSent) {
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
523
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
587
524
|
const parsed = updateIssueSchema.safeParse(req.body);
|
|
588
525
|
if (!parsed.success) {
|
|
589
526
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -617,7 +554,11 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
617
554
|
}
|
|
618
555
|
}
|
|
619
556
|
|
|
620
|
-
const
|
|
557
|
+
const updateBody = { ...parsed.data };
|
|
558
|
+
if (updateBody.externalLink !== undefined) {
|
|
559
|
+
updateBody.externalLink = normalizeOptionalExternalLink(updateBody.externalLink) ?? null;
|
|
560
|
+
}
|
|
561
|
+
const issue = await updateIssue(ctx.db, { companyId: req.companyId!, id: req.params.issueId, ...updateBody });
|
|
621
562
|
if (!issue) {
|
|
622
563
|
return sendError(res, "Issue not found.", 404);
|
|
623
564
|
}
|
|
@@ -637,14 +578,15 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
637
578
|
entityId: issue.id,
|
|
638
579
|
payload: issue
|
|
639
580
|
});
|
|
640
|
-
|
|
581
|
+
const goalMap = await listIssueGoalIdsBatch(ctx.db, req.companyId!, [issue.id]);
|
|
582
|
+
return sendOk(
|
|
583
|
+
res,
|
|
584
|
+
toIssueResponse(issue as unknown as Record<string, unknown>, goalMap.get(issue.id) ?? [])
|
|
585
|
+
);
|
|
641
586
|
});
|
|
642
587
|
|
|
643
588
|
router.delete("/:issueId", async (req, res) => {
|
|
644
|
-
|
|
645
|
-
if (res.headersSent) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
589
|
+
if (!enforcePermission(req, res, "issues:write")) return;
|
|
648
590
|
const deleted = await deleteIssue(ctx.db, req.companyId!, req.params.issueId);
|
|
649
591
|
if (!deleted) {
|
|
650
592
|
return sendError(res, "Issue not found.", 404);
|
|
@@ -292,7 +292,12 @@ export function createObservabilityRouter(ctx: AppContext) {
|
|
|
292
292
|
.filter((goal) => goal.status === "active" && goal.level === "project" && goal.projectId && projectIds.includes(goal.projectId))
|
|
293
293
|
.map((goal) => goal.title);
|
|
294
294
|
const activeAgentGoals = goals
|
|
295
|
-
.filter(
|
|
295
|
+
.filter(
|
|
296
|
+
(goal) =>
|
|
297
|
+
goal.status === "active" &&
|
|
298
|
+
goal.level === "agent" &&
|
|
299
|
+
(!goal.ownerAgentId || goal.ownerAgentId === agentId)
|
|
300
|
+
)
|
|
296
301
|
.map((goal) => goal.title);
|
|
297
302
|
const compiledPreview = [
|
|
298
303
|
`Agent: ${agent.name} (${agent.role})`,
|
package/src/routes/plugins.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import type { AppContext } from "../context";
|
|
15
15
|
import { sendError, sendOk } from "../http";
|
|
16
16
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
17
|
-
import {
|
|
17
|
+
import { enforcePermission, requireBoardRole } from "../middleware/request-actor";
|
|
18
18
|
import { deletePluginManifestFromFilesystem, writePluginManifestToFilesystem } from "../services/plugin-manifest-loader";
|
|
19
19
|
import { registerPluginManifest } from "../services/plugin-runtime";
|
|
20
20
|
|
|
@@ -74,10 +74,7 @@ export function createPluginsRouter(ctx: AppContext) {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
router.put("/:pluginId", async (req, res) => {
|
|
77
|
-
|
|
78
|
-
if (res.headersSent) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
77
|
+
if (!enforcePermission(req, res, "plugins:write")) return;
|
|
81
78
|
const parsed = pluginConfigSchema.safeParse(req.body);
|
|
82
79
|
if (!parsed.success) {
|
|
83
80
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -123,10 +120,7 @@ export function createPluginsRouter(ctx: AppContext) {
|
|
|
123
120
|
});
|
|
124
121
|
|
|
125
122
|
router.post("/install-from-json", async (req, res) => {
|
|
126
|
-
|
|
127
|
-
if (res.headersSent) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
123
|
+
if (!enforcePermission(req, res, "plugins:write")) return;
|
|
130
124
|
const parsed = pluginManifestCreateSchema.safeParse(req.body);
|
|
131
125
|
if (!parsed.success) {
|
|
132
126
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -164,10 +158,7 @@ export function createPluginsRouter(ctx: AppContext) {
|
|
|
164
158
|
});
|
|
165
159
|
|
|
166
160
|
router.post("/:pluginId/install", async (req, res) => {
|
|
167
|
-
|
|
168
|
-
if (res.headersSent) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
161
|
+
if (!enforcePermission(req, res, "plugins:write")) return;
|
|
171
162
|
const pluginId = readPluginIdParam(req.params.pluginId);
|
|
172
163
|
if (!pluginId) {
|
|
173
164
|
return sendError(res, "Missing plugin id.", 422);
|
|
@@ -193,10 +184,7 @@ export function createPluginsRouter(ctx: AppContext) {
|
|
|
193
184
|
});
|
|
194
185
|
|
|
195
186
|
router.delete("/:pluginId/install", async (req, res) => {
|
|
196
|
-
|
|
197
|
-
if (res.headersSent) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
187
|
+
if (!enforcePermission(req, res, "plugins:write")) return;
|
|
200
188
|
const pluginId = readPluginIdParam(req.params.pluginId);
|
|
201
189
|
if (!pluginId) {
|
|
202
190
|
return sendError(res, "Missing plugin id.", 422);
|
package/src/routes/projects.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { AppContext } from "../context";
|
|
|
19
19
|
import { sendError, sendOk, sendOkValidated } from "../http";
|
|
20
20
|
import { normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
21
21
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
22
|
-
import {
|
|
22
|
+
import { enforcePermission } from "../middleware/request-actor";
|
|
23
23
|
|
|
24
24
|
const projectStatusSchema = z.enum(["planned", "active", "paused", "blocked", "completed", "archived"]);
|
|
25
25
|
const executionWorkspacePolicySchema = z
|
|
@@ -129,10 +129,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
router.post("/", async (req, res) => {
|
|
132
|
-
|
|
133
|
-
if (res.headersSent) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
132
|
+
if (!enforcePermission(req, res, "projects:write")) return;
|
|
136
133
|
const parsed = createProjectSchema.safeParse(req.body);
|
|
137
134
|
if (!parsed.success) {
|
|
138
135
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -203,10 +200,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
203
200
|
});
|
|
204
201
|
|
|
205
202
|
router.put("/:projectId", async (req, res) => {
|
|
206
|
-
|
|
207
|
-
if (res.headersSent) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
203
|
+
if (!enforcePermission(req, res, "projects:write")) return;
|
|
210
204
|
const parsed = updateProjectSchema.safeParse(req.body);
|
|
211
205
|
if (!parsed.success) {
|
|
212
206
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -257,10 +251,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
257
251
|
});
|
|
258
252
|
|
|
259
253
|
router.post("/:projectId/workspaces", async (req, res) => {
|
|
260
|
-
|
|
261
|
-
if (res.headersSent) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
254
|
+
if (!enforcePermission(req, res, "projects:write")) return;
|
|
264
255
|
const parsed = createProjectWorkspaceSchema.safeParse(req.body);
|
|
265
256
|
if (!parsed.success) {
|
|
266
257
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -306,10 +297,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
306
297
|
});
|
|
307
298
|
|
|
308
299
|
router.put("/:projectId/workspaces/:workspaceId", async (req, res) => {
|
|
309
|
-
|
|
310
|
-
if (res.headersSent) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
300
|
+
if (!enforcePermission(req, res, "projects:write")) return;
|
|
313
301
|
const parsed = updateProjectWorkspaceSchema.safeParse(req.body);
|
|
314
302
|
if (!parsed.success) {
|
|
315
303
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -356,10 +344,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
356
344
|
});
|
|
357
345
|
|
|
358
346
|
router.delete("/:projectId/workspaces/:workspaceId", async (req, res) => {
|
|
359
|
-
|
|
360
|
-
if (res.headersSent) {
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
347
|
+
if (!enforcePermission(req, res, "projects:write")) return;
|
|
363
348
|
const deleted = await deleteProjectWorkspace(ctx.db, {
|
|
364
349
|
companyId: req.companyId!,
|
|
365
350
|
projectId: req.params.projectId,
|
|
@@ -380,10 +365,7 @@ export function createProjectsRouter(ctx: AppContext) {
|
|
|
380
365
|
});
|
|
381
366
|
|
|
382
367
|
router.delete("/:projectId", async (req, res) => {
|
|
383
|
-
|
|
384
|
-
if (res.headersSent) {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
368
|
+
if (!enforcePermission(req, res, "projects:write")) return;
|
|
387
369
|
const deleted = await deleteProject(ctx.db, req.companyId!, req.params.projectId);
|
|
388
370
|
if (!deleted) {
|
|
389
371
|
return sendError(res, "Project not found.", 404);
|
package/src/routes/templates.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { AppContext } from "../context";
|
|
25
25
|
import { sendError, sendOk } from "../http";
|
|
26
26
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
27
|
-
import {
|
|
27
|
+
import { enforcePermission } from "../middleware/request-actor";
|
|
28
28
|
import { applyTemplateManifest } from "../services/template-apply-service";
|
|
29
29
|
import { buildTemplatePreview } from "../services/template-preview-service";
|
|
30
30
|
|
|
@@ -39,10 +39,7 @@ export function createTemplatesRouter(ctx: AppContext) {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
router.post("/", async (req, res) => {
|
|
42
|
-
|
|
43
|
-
if (res.headersSent) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
42
|
+
if (!enforcePermission(req, res, "templates:write")) return;
|
|
46
43
|
const parsed = TemplateCreateRequestSchema.safeParse(req.body);
|
|
47
44
|
if (!parsed.success) {
|
|
48
45
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -83,10 +80,7 @@ export function createTemplatesRouter(ctx: AppContext) {
|
|
|
83
80
|
});
|
|
84
81
|
|
|
85
82
|
router.put("/:templateId", async (req, res) => {
|
|
86
|
-
|
|
87
|
-
if (res.headersSent) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
83
|
+
if (!enforcePermission(req, res, "templates:write")) return;
|
|
90
84
|
const parsed = TemplateUpdateRequestSchema.safeParse(req.body);
|
|
91
85
|
if (!parsed.success) {
|
|
92
86
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -138,10 +132,7 @@ export function createTemplatesRouter(ctx: AppContext) {
|
|
|
138
132
|
});
|
|
139
133
|
|
|
140
134
|
router.delete("/:templateId", async (req, res) => {
|
|
141
|
-
|
|
142
|
-
if (res.headersSent) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
135
|
+
if (!enforcePermission(req, res, "templates:write")) return;
|
|
145
136
|
const deleted = await deleteTemplate(ctx.db, req.companyId!, req.params.templateId);
|
|
146
137
|
if (!deleted) {
|
|
147
138
|
return sendError(res, "Template not found.", 404);
|
|
@@ -188,10 +179,7 @@ export function createTemplatesRouter(ctx: AppContext) {
|
|
|
188
179
|
});
|
|
189
180
|
|
|
190
181
|
router.post("/:templateId/apply", async (req, res) => {
|
|
191
|
-
|
|
192
|
-
if (res.headersSent) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
182
|
+
if (!enforcePermission(req, res, "templates:write")) return;
|
|
195
183
|
const parsed = TemplateApplyRequestSchema.safeParse(req.body);
|
|
196
184
|
if (!parsed.success) {
|
|
197
185
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -261,10 +249,7 @@ export function createTemplatesRouter(ctx: AppContext) {
|
|
|
261
249
|
});
|
|
262
250
|
|
|
263
251
|
router.post("/import", async (req, res) => {
|
|
264
|
-
|
|
265
|
-
if (res.headersSent) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
252
|
+
if (!enforcePermission(req, res, "templates:write")) return;
|
|
268
253
|
const parsed = TemplateImportRequestSchema.safeParse(req.body);
|
|
269
254
|
if (!parsed.success) {
|
|
270
255
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -316,18 +316,16 @@ async function ensureCeoStartupTask(
|
|
|
316
316
|
` - \`${ceoOperatingFolder}/HEARTBEAT.md\``,
|
|
317
317
|
` - \`${ceoOperatingFolder}/SOUL.md\``,
|
|
318
318
|
` - \`${ceoOperatingFolder}/TOOLS.md\``,
|
|
319
|
-
"3.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
" - Prefer heredoc/stdin payloads (for example `curl --data-binary @- <<'JSON' ... JSON`) to avoid temp-file cleanup failures under runtime policy.",
|
|
323
|
-
` - If you must use payload files, store them in \`${ceoTmpFolder}/\` (or OS temp via \`mktemp\`) and avoid chaining cleanup commands into critical task flow.`,
|
|
319
|
+
"3. Each heartbeat already includes your operating directory via `$BOPODEV_AGENT_OPERATING_DIR` and directs you to AGENTS.md and related files there when they exist.",
|
|
320
|
+
" You do not need to save file paths into `bootstrapPrompt` for operating docs—reserve `bootstrapPrompt` only for optional extra standing instructions.",
|
|
321
|
+
` - Prefer heredoc/stdin payloads (for example \`curl --data-binary @- <<'JSON' ... JSON\`) when calling APIs; if you must use payload files, store them in \`${ceoTmpFolder}/\` (or OS temp via \`mktemp\`).`,
|
|
324
322
|
"4. To inspect your own agent record, use `GET /agents` and filter by your agent id. Do not call `GET /agents/:agentId`.",
|
|
325
323
|
" - `GET /agents` uses envelope shape `{ \"ok\": true, \"data\": [...] }`; treat any other shape as failure.",
|
|
326
|
-
" - Deterministic filter: `jq -er --arg id \"$BOPODEV_AGENT_ID\" '.data | if type==\"array\" then . else error(\"invalid_agents_payload\") end | map(select((.id? // \"\") == $id))[0] | {id,name,role
|
|
324
|
+
" - Deterministic filter: `jq -er --arg id \"$BOPODEV_AGENT_ID\" '.data | if type==\"array\" then . else error(\"invalid_agents_payload\") end | map(select((.id? // \"\") == $id))[0] | {id,name,role}'`",
|
|
327
325
|
"5. Heartbeat-assigned issues are already claimed for the current run. Do not call a checkout endpoint; update status with `PUT /issues/:issueId` only.",
|
|
328
326
|
"6. After your operating files are active, submit a hire request for a Founding Engineer via `POST /agents` using supported fields:",
|
|
329
327
|
" - `name`, `role`, `providerType`, `heartbeatCron`, `monthlyBudgetUsd`",
|
|
330
|
-
" - optional `managerAgentId`, `bootstrapPrompt
|
|
328
|
+
" - optional `managerAgentId`, `bootstrapPrompt` (extra standing instructions only), `runtimeConfig`, `canHireAgents`",
|
|
331
329
|
" - `requestApproval: true` and `sourceIssueId`",
|
|
332
330
|
"7. Do not use unsupported hire fields such as `adapterType`, `adapterConfig`, or `reportsTo`.",
|
|
333
331
|
"",
|