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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"nanoid": "^5.1.5",
|
|
18
18
|
"ws": "^8.19.0",
|
|
19
19
|
"zod": "^4.1.5",
|
|
20
|
-
"bopodev-
|
|
21
|
-
"bopodev-
|
|
22
|
-
"bopodev-
|
|
20
|
+
"bopodev-contracts": "0.1.29",
|
|
21
|
+
"bopodev-agent-sdk": "0.1.29",
|
|
22
|
+
"bopodev-db": "0.1.29"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/cors": "^2.8.19",
|
package/src/app.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import cors from "cors";
|
|
2
1
|
import express from "express";
|
|
3
2
|
import type { NextFunction, Request, Response } from "express";
|
|
4
|
-
import {
|
|
5
|
-
import { nanoid } from "nanoid";
|
|
3
|
+
import { pingDatabase, RepositoryValidationError } from "bopodev-db";
|
|
6
4
|
import type { AppContext } from "./context";
|
|
7
5
|
import { createAgentsRouter } from "./routes/agents";
|
|
8
6
|
import { createAuthRouter } from "./routes/auth";
|
|
@@ -17,6 +15,9 @@ import { createProjectsRouter } from "./routes/projects";
|
|
|
17
15
|
import { createPluginsRouter } from "./routes/plugins";
|
|
18
16
|
import { createTemplatesRouter } from "./routes/templates";
|
|
19
17
|
import { sendError } from "./http";
|
|
18
|
+
import { createCorsMiddleware } from "./middleware/cors-config";
|
|
19
|
+
import { attachCrudRequestLogging } from "./middleware/request-logging";
|
|
20
|
+
import { attachRequestId } from "./middleware/request-id";
|
|
20
21
|
import { attachRequestActor } from "./middleware/request-actor";
|
|
21
22
|
import { resolveAllowedOrigins, resolveDeploymentMode } from "./security/deployment-mode";
|
|
22
23
|
|
|
@@ -24,75 +25,20 @@ export function createApp(ctx: AppContext) {
|
|
|
24
25
|
const app = express();
|
|
25
26
|
const deploymentMode = ctx.deploymentMode ?? resolveDeploymentMode();
|
|
26
27
|
const allowedOrigins = ctx.allowedOrigins ?? resolveAllowedOrigins(deploymentMode);
|
|
27
|
-
app.use(
|
|
28
|
-
cors({
|
|
29
|
-
origin(origin, callback) {
|
|
30
|
-
if (!origin) {
|
|
31
|
-
callback(null, true);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
deploymentMode === "local" &&
|
|
36
|
-
(origin.startsWith("http://localhost:") || origin.startsWith("http://127.0.0.1:"))
|
|
37
|
-
) {
|
|
38
|
-
callback(null, true);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (allowedOrigins.includes(origin)) {
|
|
42
|
-
callback(null, true);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
callback(new Error(`CORS origin denied: ${origin}`));
|
|
46
|
-
},
|
|
47
|
-
credentials: true,
|
|
48
|
-
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
49
|
-
allowedHeaders: [
|
|
50
|
-
"content-type",
|
|
51
|
-
"x-company-id",
|
|
52
|
-
"authorization",
|
|
53
|
-
"x-client-trace-id",
|
|
54
|
-
"x-bopo-actor-token",
|
|
55
|
-
"x-request-id",
|
|
56
|
-
"x-bopodev-run-id"
|
|
57
|
-
]
|
|
58
|
-
})
|
|
59
|
-
);
|
|
28
|
+
app.use(createCorsMiddleware(deploymentMode, allowedOrigins));
|
|
60
29
|
app.use(express.json());
|
|
61
30
|
app.use(attachRequestActor);
|
|
62
|
-
|
|
63
|
-
app.use((req, res, next) => {
|
|
64
|
-
const requestId = req.header("x-request-id")?.trim() || nanoid(14);
|
|
65
|
-
req.requestId = requestId;
|
|
66
|
-
res.setHeader("x-request-id", requestId);
|
|
67
|
-
next();
|
|
68
|
-
});
|
|
31
|
+
app.use(attachRequestId);
|
|
69
32
|
const logApiRequests = process.env.BOPO_LOG_API_REQUESTS !== "0";
|
|
70
33
|
if (logApiRequests) {
|
|
71
|
-
app.use(
|
|
72
|
-
if (req.path === "/health") {
|
|
73
|
-
next();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const method = req.method.toUpperCase();
|
|
77
|
-
if (!isCrudMethod(method)) {
|
|
78
|
-
next();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const startedAt = Date.now();
|
|
82
|
-
res.on("finish", () => {
|
|
83
|
-
const elapsedMs = Date.now() - startedAt;
|
|
84
|
-
const timestamp = new Date().toTimeString().slice(0, 8);
|
|
85
|
-
process.stderr.write(`[${timestamp}] INFO: ${method} ${req.originalUrl} ${res.statusCode} ${elapsedMs}ms\n`);
|
|
86
|
-
});
|
|
87
|
-
next();
|
|
88
|
-
});
|
|
34
|
+
app.use(attachCrudRequestLogging);
|
|
89
35
|
}
|
|
90
36
|
|
|
91
37
|
app.get("/health", async (_req, res) => {
|
|
92
38
|
let dbReady = false;
|
|
93
39
|
let dbError: string | undefined;
|
|
94
40
|
try {
|
|
95
|
-
await ctx.db
|
|
41
|
+
await pingDatabase(ctx.db);
|
|
96
42
|
dbReady = true;
|
|
97
43
|
} catch (error) {
|
|
98
44
|
dbError = String(error);
|
|
@@ -126,18 +72,20 @@ export function createApp(ctx: AppContext) {
|
|
|
126
72
|
app.use("/plugins", createPluginsRouter(ctx));
|
|
127
73
|
app.use("/templates", createTemplatesRouter(ctx));
|
|
128
74
|
|
|
129
|
-
app.use((error: unknown,
|
|
75
|
+
app.use((error: unknown, req: Request, res: Response, _next: NextFunction) => {
|
|
130
76
|
if (error instanceof RepositoryValidationError) {
|
|
131
77
|
return sendError(res, error.message, 422);
|
|
132
78
|
}
|
|
133
|
-
|
|
134
|
-
|
|
79
|
+
const requestId = req.requestId;
|
|
80
|
+
if (requestId) {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.error(`[request ${requestId}]`, error);
|
|
83
|
+
} else {
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.error(error);
|
|
86
|
+
}
|
|
135
87
|
return sendError(res, "Internal server error", 500);
|
|
136
88
|
});
|
|
137
89
|
|
|
138
90
|
return app;
|
|
139
91
|
}
|
|
140
|
-
|
|
141
|
-
function isCrudMethod(method: string) {
|
|
142
|
-
return method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
|
|
143
|
-
}
|
|
@@ -70,6 +70,14 @@ function normalizeWorkspaceRelativeArtifactPath(value: string) {
|
|
|
70
70
|
}
|
|
71
71
|
const workspaceScopedMatch = normalized.match(/(?:^|\/)workspace\/([^/]+)\/(.+)$/);
|
|
72
72
|
if (!workspaceScopedMatch) {
|
|
73
|
+
const projectAgentsMatch = normalized.match(/(?:^|\/)projects\/agents\/([^/]+)\/operating\/(.+)$/);
|
|
74
|
+
if (projectAgentsMatch) {
|
|
75
|
+
const [, agentId, suffix] = projectAgentsMatch;
|
|
76
|
+
if (!agentId || !suffix) {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
return `agents/${agentId}/operating/${suffix}`;
|
|
80
|
+
}
|
|
73
81
|
return normalized;
|
|
74
82
|
}
|
|
75
83
|
const scopedRelativePath = workspaceScopedMatch[2];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import cors from "cors";
|
|
2
|
+
import type { DeploymentMode } from "../security/deployment-mode";
|
|
3
|
+
|
|
4
|
+
export function createCorsMiddleware(deploymentMode: DeploymentMode, allowedOrigins: string[]) {
|
|
5
|
+
return cors({
|
|
6
|
+
origin(origin, callback) {
|
|
7
|
+
if (!origin) {
|
|
8
|
+
callback(null, true);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (
|
|
12
|
+
deploymentMode === "local" &&
|
|
13
|
+
(origin.startsWith("http://localhost:") || origin.startsWith("http://127.0.0.1:"))
|
|
14
|
+
) {
|
|
15
|
+
callback(null, true);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (allowedOrigins.includes(origin)) {
|
|
19
|
+
callback(null, true);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
callback(new Error(`CORS origin denied: ${origin}`));
|
|
23
|
+
},
|
|
24
|
+
credentials: true,
|
|
25
|
+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
26
|
+
allowedHeaders: [
|
|
27
|
+
"content-type",
|
|
28
|
+
"x-company-id",
|
|
29
|
+
"authorization",
|
|
30
|
+
"x-client-trace-id",
|
|
31
|
+
"x-bopo-actor-token",
|
|
32
|
+
"x-request-id",
|
|
33
|
+
"x-bopodev-run-id"
|
|
34
|
+
]
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -3,23 +3,9 @@ import { RequestActorHeadersSchema } from "bopodev-contracts";
|
|
|
3
3
|
import { sendError } from "../http";
|
|
4
4
|
import { verifyActorToken } from "../security/actor-token";
|
|
5
5
|
import { isAuthenticatedMode, resolveDeploymentMode } from "../security/deployment-mode";
|
|
6
|
+
import type { RequestActor } from "../types/request-actor";
|
|
6
7
|
|
|
7
|
-
export type RequestActor
|
|
8
|
-
type: "board" | "member" | "agent";
|
|
9
|
-
id: string;
|
|
10
|
-
companyIds: string[] | null;
|
|
11
|
-
permissions: string[];
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
declare global {
|
|
15
|
-
namespace Express {
|
|
16
|
-
interface Request {
|
|
17
|
-
actor?: RequestActor;
|
|
18
|
-
companyId?: string;
|
|
19
|
-
requestId?: string;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
8
|
+
export type { RequestActor };
|
|
23
9
|
|
|
24
10
|
export function attachRequestActor(req: Request, res: Response, next: NextFunction) {
|
|
25
11
|
const deploymentMode = resolveDeploymentMode();
|
|
@@ -136,6 +122,14 @@ export function requirePermission(permission: string) {
|
|
|
136
122
|
};
|
|
137
123
|
}
|
|
138
124
|
|
|
125
|
+
export function enforcePermission(req: Request, res: Response, permission: string): boolean {
|
|
126
|
+
let allowed = false;
|
|
127
|
+
requirePermission(permission)(req, res, () => {
|
|
128
|
+
allowed = true;
|
|
129
|
+
});
|
|
130
|
+
return allowed;
|
|
131
|
+
}
|
|
132
|
+
|
|
139
133
|
export function requireBoardRole(req: Request, res: Response, next: NextFunction) {
|
|
140
134
|
if (req.actor?.type !== "board") {
|
|
141
135
|
return sendError(res, "Board role required.", 403);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
|
|
4
|
+
export function attachRequestId(req: Request, res: Response, next: NextFunction) {
|
|
5
|
+
const requestId = req.header("x-request-id")?.trim() || nanoid(14);
|
|
6
|
+
req.requestId = requestId;
|
|
7
|
+
res.setHeader("x-request-id", requestId);
|
|
8
|
+
next();
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
function isCrudMethod(method: string) {
|
|
4
|
+
return method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function attachCrudRequestLogging(req: Request, res: Response, next: NextFunction) {
|
|
8
|
+
if (req.path === "/health") {
|
|
9
|
+
next();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const method = req.method.toUpperCase();
|
|
13
|
+
if (!isCrudMethod(method)) {
|
|
14
|
+
next();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
res.on("finish", () => {
|
|
19
|
+
const elapsedMs = Date.now() - startedAt;
|
|
20
|
+
const timestamp = new Date().toTimeString().slice(0, 8);
|
|
21
|
+
process.stderr.write(`[${timestamp}] INFO: ${method} ${req.originalUrl} ${res.statusCode} ${elapsedMs}ms\n`);
|
|
22
|
+
});
|
|
23
|
+
next();
|
|
24
|
+
}
|
package/src/routes/agents.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { resolveHiringDelegate } from "../lib/hiring-delegate";
|
|
|
37
37
|
import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
|
|
38
38
|
import { assertRuntimeCwdForCompany, hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
39
39
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
40
|
-
import { requireBoardRole, requirePermission } from "../middleware/request-actor";
|
|
40
|
+
import { enforcePermission, requireBoardRole, requirePermission } from "../middleware/request-actor";
|
|
41
41
|
import { createGovernanceRealtimeEvent, serializeStoredApproval } from "../realtime/governance";
|
|
42
42
|
import { publishAttentionSnapshot } from "../realtime/attention";
|
|
43
43
|
import {
|
|
@@ -629,10 +629,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
629
629
|
});
|
|
630
630
|
|
|
631
631
|
router.post("/:agentId/pause", async (req, res) => {
|
|
632
|
-
|
|
633
|
-
if (res.headersSent) {
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
632
|
+
if (!enforcePermission(req, res, "agents:lifecycle")) return;
|
|
636
633
|
const agent = await updateAgent(ctx.db, {
|
|
637
634
|
companyId: req.companyId!,
|
|
638
635
|
id: req.params.agentId,
|
|
@@ -656,10 +653,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
656
653
|
});
|
|
657
654
|
|
|
658
655
|
router.post("/:agentId/resume", async (req, res) => {
|
|
659
|
-
|
|
660
|
-
if (res.headersSent) {
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
656
|
+
if (!enforcePermission(req, res, "agents:lifecycle")) return;
|
|
663
657
|
const agent = await updateAgent(ctx.db, {
|
|
664
658
|
companyId: req.companyId!,
|
|
665
659
|
id: req.params.agentId,
|
package/src/routes/companies.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { normalizeRuntimeConfig, resolveRuntimeModelForProvider, runtimeConfigTo
|
|
|
10
10
|
import { buildDefaultCeoBootstrapPrompt } from "../lib/ceo-bootstrap-prompt";
|
|
11
11
|
import { resolveOpencodeRuntimeModel } from "../lib/opencode-model";
|
|
12
12
|
import { resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
13
|
+
import { buildCompanyPortabilityExport } from "../services/company-export-service";
|
|
13
14
|
import { canAccessCompany, requireBoardRole, requirePermission } from "../middleware/request-actor";
|
|
14
15
|
import { ensureCompanyBuiltinPluginDefaults } from "../services/plugin-runtime";
|
|
15
16
|
import { ensureCompanyBuiltinTemplateDefaults } from "../services/template-catalog";
|
|
@@ -21,7 +22,7 @@ const createCompanySchema = z.object({
|
|
|
21
22
|
name: z.string().min(1),
|
|
22
23
|
mission: z.string().optional(),
|
|
23
24
|
providerType: z
|
|
24
|
-
.enum(["codex", "claude_code", "cursor", "gemini_cli", "opencode", "openai_api", "anthropic_api", "shell"])
|
|
25
|
+
.enum(["codex", "claude_code", "cursor", "gemini_cli", "opencode", "openai_api", "anthropic_api", "http", "shell"])
|
|
25
26
|
.optional(),
|
|
26
27
|
runtimeModel: z.string().optional()
|
|
27
28
|
});
|
|
@@ -53,6 +54,21 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
53
54
|
);
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
router.get("/:companyId/export", async (req, res) => {
|
|
58
|
+
const companyId = readCompanyIdParam(req);
|
|
59
|
+
if (!companyId) {
|
|
60
|
+
return sendError(res, "Missing company id.", 422);
|
|
61
|
+
}
|
|
62
|
+
if (!canAccessCompany(req, companyId)) {
|
|
63
|
+
return sendError(res, "Actor does not have access to this company.", 403);
|
|
64
|
+
}
|
|
65
|
+
const payload = await buildCompanyPortabilityExport(ctx.db, companyId);
|
|
66
|
+
if (!payload) {
|
|
67
|
+
return sendError(res, "Company not found.", 404);
|
|
68
|
+
}
|
|
69
|
+
return sendOk(res, payload);
|
|
70
|
+
});
|
|
71
|
+
|
|
56
72
|
router.post("/", requireBoardRole, async (req, res) => {
|
|
57
73
|
const parsed = createCompanySchema.safeParse(req.body);
|
|
58
74
|
if (!parsed.success) {
|
|
@@ -171,6 +187,7 @@ function parseAgentProvider(value: unknown) {
|
|
|
171
187
|
value === "opencode" ||
|
|
172
188
|
value === "openai_api" ||
|
|
173
189
|
value === "anthropic_api" ||
|
|
190
|
+
value === "http" ||
|
|
174
191
|
value === "shell"
|
|
175
192
|
) {
|
|
176
193
|
return value;
|
package/src/routes/goals.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { appendAuditEvent, createApprovalRequest, createGoal, deleteGoal, getApp
|
|
|
5
5
|
import type { AppContext } from "../context";
|
|
6
6
|
import { sendError, sendOk, sendOkValidated } from "../http";
|
|
7
7
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
8
|
-
import {
|
|
8
|
+
import { enforcePermission } from "../middleware/request-actor";
|
|
9
9
|
import { createGovernanceRealtimeEvent, serializeStoredApproval } from "../realtime/governance";
|
|
10
10
|
import { publishAttentionSnapshot } from "../realtime/attention";
|
|
11
11
|
import { isApprovalRequired } from "../services/governance-service";
|
|
@@ -13,6 +13,7 @@ import { isApprovalRequired } from "../services/governance-service";
|
|
|
13
13
|
const createGoalSchema = z.object({
|
|
14
14
|
projectId: z.string().optional(),
|
|
15
15
|
parentGoalId: z.string().optional(),
|
|
16
|
+
ownerAgentId: z.string().optional(),
|
|
16
17
|
level: z.enum(["company", "project", "agent"]),
|
|
17
18
|
title: z.string().min(1),
|
|
18
19
|
description: z.string().optional(),
|
|
@@ -23,6 +24,7 @@ const updateGoalSchema = z
|
|
|
23
24
|
.object({
|
|
24
25
|
projectId: z.string().nullable().optional(),
|
|
25
26
|
parentGoalId: z.string().nullable().optional(),
|
|
27
|
+
ownerAgentId: z.string().nullable().optional(),
|
|
26
28
|
level: z.enum(["company", "project", "agent"]).optional(),
|
|
27
29
|
title: z.string().min(1).optional(),
|
|
28
30
|
description: z.string().nullable().optional(),
|
|
@@ -44,10 +46,7 @@ export function createGoalsRouter(ctx: AppContext) {
|
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
router.post("/", async (req, res) => {
|
|
47
|
-
|
|
48
|
-
if (res.headersSent) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
49
|
+
if (!enforcePermission(req, res, "goals:write")) return;
|
|
51
50
|
const parsed = createGoalSchema.safeParse(req.body);
|
|
52
51
|
if (!parsed.success) {
|
|
53
52
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -76,6 +75,7 @@ export function createGoalsRouter(ctx: AppContext) {
|
|
|
76
75
|
companyId: req.companyId!,
|
|
77
76
|
projectId: parsed.data.projectId,
|
|
78
77
|
parentGoalId: parsed.data.parentGoalId,
|
|
78
|
+
ownerAgentId: parsed.data.ownerAgentId,
|
|
79
79
|
level: parsed.data.level,
|
|
80
80
|
title: parsed.data.title,
|
|
81
81
|
description: parsed.data.description
|
|
@@ -92,10 +92,7 @@ export function createGoalsRouter(ctx: AppContext) {
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
router.put("/:goalId", async (req, res) => {
|
|
95
|
-
|
|
96
|
-
if (res.headersSent) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
95
|
+
if (!enforcePermission(req, res, "goals:write")) return;
|
|
99
96
|
const parsed = updateGoalSchema.safeParse(req.body);
|
|
100
97
|
if (!parsed.success) {
|
|
101
98
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -118,10 +115,7 @@ export function createGoalsRouter(ctx: AppContext) {
|
|
|
118
115
|
});
|
|
119
116
|
|
|
120
117
|
router.delete("/:goalId", async (req, res) => {
|
|
121
|
-
|
|
122
|
-
if (res.headersSent) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
118
|
+
if (!enforcePermission(req, res, "goals:write")) return;
|
|
125
119
|
const deleted = await deleteGoal(ctx.db, req.companyId!, req.params.goalId);
|
|
126
120
|
if (!deleted) {
|
|
127
121
|
return sendError(res, "Goal not found.", 404);
|
package/src/routes/governance.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 } from "../middleware/request-actor";
|
|
18
18
|
import { createGovernanceRealtimeEvent, serializeStoredApproval } from "../realtime/governance";
|
|
19
19
|
import { publishAttentionSnapshot } from "../realtime/attention";
|
|
20
20
|
import {
|
|
@@ -146,10 +146,7 @@ export function createGovernanceRouter(ctx: AppContext) {
|
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
router.post("/resolve", async (req, res) => {
|
|
149
|
-
|
|
150
|
-
if (res.headersSent) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
149
|
+
if (!enforcePermission(req, res, "governance:resolve")) return;
|
|
153
150
|
const parsed = resolveSchema.safeParse(req.body);
|
|
154
151
|
if (!parsed.success) {
|
|
155
152
|
return sendError(res, parsed.error.message, 422);
|
package/src/routes/heartbeats.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { agents, and, eq, heartbeatRuns, listHeartbeatQueueJobs } from "bopodev-
|
|
|
4
4
|
import type { AppContext } from "../context";
|
|
5
5
|
import { sendError, sendOk } from "../http";
|
|
6
6
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
7
|
-
import {
|
|
7
|
+
import { enforcePermission } from "../middleware/request-actor";
|
|
8
8
|
import {
|
|
9
9
|
findPendingProjectBudgetOverrideBlocksForAgent,
|
|
10
10
|
runHeartbeatSweep,
|
|
@@ -30,10 +30,7 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
30
30
|
router.use(requireCompanyScope);
|
|
31
31
|
|
|
32
32
|
router.post("/run-agent", async (req, res) => {
|
|
33
|
-
|
|
34
|
-
if (res.headersSent) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
33
|
+
if (!enforcePermission(req, res, "heartbeats:run")) return;
|
|
37
34
|
const parsed = runAgentSchema.safeParse(req.body);
|
|
38
35
|
if (!parsed.success) {
|
|
39
36
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -84,10 +81,7 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
84
81
|
});
|
|
85
82
|
|
|
86
83
|
router.post("/:runId/stop", async (req, res) => {
|
|
87
|
-
|
|
88
|
-
if (res.headersSent) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
84
|
+
if (!enforcePermission(req, res, "heartbeats:run")) return;
|
|
91
85
|
const parsed = runIdParamsSchema.safeParse(req.params);
|
|
92
86
|
if (!parsed.success) {
|
|
93
87
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -173,10 +167,7 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
173
167
|
}
|
|
174
168
|
|
|
175
169
|
router.post("/:runId/resume", async (req, res) => {
|
|
176
|
-
|
|
177
|
-
if (res.headersSent) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
170
|
+
if (!enforcePermission(req, res, "heartbeats:run")) return;
|
|
180
171
|
const parsed = runIdParamsSchema.safeParse(req.params);
|
|
181
172
|
if (!parsed.success) {
|
|
182
173
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -200,10 +191,7 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
200
191
|
});
|
|
201
192
|
|
|
202
193
|
router.post("/:runId/redo", async (req, res) => {
|
|
203
|
-
|
|
204
|
-
if (res.headersSent) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
194
|
+
if (!enforcePermission(req, res, "heartbeats:run")) return;
|
|
207
195
|
const parsed = runIdParamsSchema.safeParse(req.params);
|
|
208
196
|
if (!parsed.success) {
|
|
209
197
|
return sendError(res, parsed.error.message, 422);
|
|
@@ -227,10 +215,7 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
227
215
|
});
|
|
228
216
|
|
|
229
217
|
router.post("/sweep", async (req, res) => {
|
|
230
|
-
|
|
231
|
-
if (res.headersSent) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
218
|
+
if (!enforcePermission(req, res, "heartbeats:sweep")) return;
|
|
234
219
|
const runIds = await runHeartbeatSweep(ctx.db, req.companyId!, {
|
|
235
220
|
requestId: req.requestId,
|
|
236
221
|
realtimeHub: ctx.realtimeHub
|
|
@@ -239,10 +224,7 @@ export function createHeartbeatRouter(ctx: AppContext) {
|
|
|
239
224
|
});
|
|
240
225
|
|
|
241
226
|
router.get("/queue", async (req, res) => {
|
|
242
|
-
|
|
243
|
-
if (res.headersSent) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
227
|
+
if (!enforcePermission(req, res, "heartbeats:run")) return;
|
|
246
228
|
const parsed = queueQuerySchema.safeParse(req.query);
|
|
247
229
|
if (!parsed.success) {
|
|
248
230
|
return sendError(res, parsed.error.message, 422);
|