bopodev-api 0.1.28 → 0.1.30
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/ceo-bootstrap-prompt.ts +1 -0
- 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/realtime/office-space.ts +1 -0
- package/src/routes/agents.ts +90 -46
- package/src/routes/companies.ts +20 -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 +65 -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 +18 -8
- package/src/server.ts +33 -292
- package/src/services/company-export-service.ts +63 -0
- package/src/services/governance-service.ts +10 -14
- 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} +201 -634
- 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 +66 -0
- package/src/services/memory-file-service.ts +10 -2
- package/src/services/template-apply-service.ts +6 -0
- package/src/services/template-catalog.ts +37 -3
- 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 +80 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
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-db": "0.1.
|
|
22
|
-
"bopodev-
|
|
20
|
+
"bopodev-contracts": "0.1.30",
|
|
21
|
+
"bopodev-db": "0.1.30",
|
|
22
|
+
"bopodev-agent-sdk": "0.1.30"
|
|
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
|
-
}
|
|
@@ -5,6 +5,7 @@ export function buildDefaultCeoBootstrapPrompt() {
|
|
|
5
5
|
"- Clarify missing constraints before hiring when requirements are ambiguous.",
|
|
6
6
|
"- Choose reporting lines, provider, model, and permissions that fit company goals and budget.",
|
|
7
7
|
"- Use governance-safe hiring via `POST /agents` with `requestApproval: true` unless explicitly told otherwise.",
|
|
8
|
+
"- Always set `capabilities` on every new hire: one or two sentences describing what they do, for the org chart and team roster (delegation). If the issue metadata or body specifies requested capabilities, use or refine that text; if missing, write an appropriate line from the role and request details.",
|
|
8
9
|
"- Avoid duplicate hires by checking existing agents and pending approvals first.",
|
|
9
10
|
"- Use the control-plane coordination skill as the source of truth for endpoint paths, required headers, and approval workflow steps.",
|
|
10
11
|
"- Record hiring rationale and key decisions in issue comments for auditability."
|
|
@@ -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
|
+
}
|
|
@@ -437,6 +437,7 @@ function normalizeProviderType(value: string): OfficeOccupant["providerType"] {
|
|
|
437
437
|
value === "gemini_cli" ||
|
|
438
438
|
value === "openai_api" ||
|
|
439
439
|
value === "anthropic_api" ||
|
|
440
|
+
value === "openclaw_gateway" ||
|
|
440
441
|
value === "http" ||
|
|
441
442
|
value === "shell"
|
|
442
443
|
? value
|
package/src/routes/agents.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
1
|
+
import { Router, type Response } from "express";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import {
|
|
@@ -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 {
|
|
@@ -82,17 +82,25 @@ const runtimePreflightSchema = z.object({
|
|
|
82
82
|
"gemini_cli",
|
|
83
83
|
"openai_api",
|
|
84
84
|
"anthropic_api",
|
|
85
|
+
"openclaw_gateway",
|
|
85
86
|
"http",
|
|
86
87
|
"shell"
|
|
87
88
|
]),
|
|
88
89
|
runtimeConfig: z.record(z.string(), z.unknown()).optional(),
|
|
89
90
|
...legacyRuntimeConfigSchema.shape
|
|
90
91
|
});
|
|
92
|
+
|
|
93
|
+
/** Body for POST /agents/adapter-models/:providerType (runtime for CLI discovery). */
|
|
94
|
+
const adapterModelsBodySchema = z.object({
|
|
95
|
+
runtimeConfig: z.record(z.string(), z.unknown()).optional(),
|
|
96
|
+
...legacyRuntimeConfigSchema.shape
|
|
97
|
+
});
|
|
91
98
|
const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
92
99
|
"managerAgentId",
|
|
93
100
|
"role",
|
|
94
101
|
"roleKey",
|
|
95
102
|
"title",
|
|
103
|
+
"capabilities",
|
|
96
104
|
"name",
|
|
97
105
|
"providerType",
|
|
98
106
|
"status",
|
|
@@ -135,7 +143,7 @@ function toAgentResponse(agent: Record<string, unknown>) {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
function providerRequiresNamedModel(providerType: string) {
|
|
138
|
-
return providerType !== "http" && providerType !== "shell";
|
|
146
|
+
return providerType !== "http" && providerType !== "shell" && providerType !== "openclaw_gateway";
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
const agentResponseSchema = AgentSchema.extend({
|
|
@@ -149,6 +157,66 @@ function ensureNamedRuntimeModel(providerType: string, runtimeModel: string | un
|
|
|
149
157
|
return hasText(runtimeModel);
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
type AdapterModelsProviderType = NonNullable<z.infer<typeof runtimePreflightSchema>["providerType"]>;
|
|
161
|
+
|
|
162
|
+
async function handleAdapterModelsRequest(
|
|
163
|
+
ctx: AppContext,
|
|
164
|
+
res: Response,
|
|
165
|
+
companyId: string,
|
|
166
|
+
providerType: string,
|
|
167
|
+
parsedBody: z.infer<typeof adapterModelsBodySchema> | null
|
|
168
|
+
) {
|
|
169
|
+
if (!runtimePreflightSchema.shape.providerType.safeParse(providerType).success) {
|
|
170
|
+
return sendError(res, `Unsupported provider type: ${providerType}`, 422);
|
|
171
|
+
}
|
|
172
|
+
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, companyId);
|
|
173
|
+
let runtimeConfig: ReturnType<typeof normalizeRuntimeConfig>;
|
|
174
|
+
try {
|
|
175
|
+
if (parsedBody) {
|
|
176
|
+
runtimeConfig = normalizeRuntimeConfig({
|
|
177
|
+
runtimeConfig: parsedBody.runtimeConfig,
|
|
178
|
+
legacy: {
|
|
179
|
+
runtimeCommand: parsedBody.runtimeCommand,
|
|
180
|
+
runtimeArgs: parsedBody.runtimeArgs,
|
|
181
|
+
runtimeCwd: parsedBody.runtimeCwd,
|
|
182
|
+
runtimeTimeoutMs: parsedBody.runtimeTimeoutMs,
|
|
183
|
+
runtimeModel: parsedBody.runtimeModel,
|
|
184
|
+
runtimeThinkingEffort: parsedBody.runtimeThinkingEffort,
|
|
185
|
+
bootstrapPrompt: parsedBody.bootstrapPrompt,
|
|
186
|
+
runtimeTimeoutSec: parsedBody.runtimeTimeoutSec,
|
|
187
|
+
interruptGraceSec: parsedBody.interruptGraceSec,
|
|
188
|
+
runtimeEnv: parsedBody.runtimeEnv,
|
|
189
|
+
runPolicy: parsedBody.runPolicy
|
|
190
|
+
},
|
|
191
|
+
defaultRuntimeCwd
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
runtimeConfig = normalizeRuntimeConfig({ defaultRuntimeCwd });
|
|
195
|
+
}
|
|
196
|
+
runtimeConfig = enforceRuntimeCwdPolicy(companyId, runtimeConfig);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return sendError(res, String(error), 422);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (parsedBody && runtimeConfig.runtimeCwd) {
|
|
202
|
+
await mkdir(runtimeConfig.runtimeCwd, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const typedProviderType = providerType as AdapterModelsProviderType;
|
|
206
|
+
const models = await getAdapterModels(typedProviderType, {
|
|
207
|
+
command: runtimeConfig.runtimeCommand,
|
|
208
|
+
args: runtimeConfig.runtimeArgs,
|
|
209
|
+
cwd: runtimeConfig.runtimeCwd,
|
|
210
|
+
env: runtimeConfig.runtimeEnv,
|
|
211
|
+
model: runtimeConfig.runtimeModel,
|
|
212
|
+
thinkingEffort: runtimeConfig.runtimeThinkingEffort,
|
|
213
|
+
timeoutMs: runtimeConfig.runtimeTimeoutSec > 0 ? runtimeConfig.runtimeTimeoutSec * 1000 : undefined,
|
|
214
|
+
interruptGraceSec: runtimeConfig.interruptGraceSec,
|
|
215
|
+
runPolicy: runtimeConfig.runPolicy
|
|
216
|
+
});
|
|
217
|
+
return sendOk(res, { providerType: typedProviderType, models });
|
|
218
|
+
}
|
|
219
|
+
|
|
152
220
|
export function createAgentsRouter(ctx: AppContext) {
|
|
153
221
|
const router = Router();
|
|
154
222
|
router.use(requireCompanyScope);
|
|
@@ -229,42 +297,16 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
229
297
|
|
|
230
298
|
router.get("/adapter-models/:providerType", async (req, res) => {
|
|
231
299
|
const providerType = req.params.providerType;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
defaultRuntimeCwd
|
|
241
|
-
});
|
|
242
|
-
runtimeConfig = enforceRuntimeCwdPolicy(req.companyId!, runtimeConfig);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
return sendError(res, String(error), 422);
|
|
300
|
+
return handleAdapterModelsRequest(ctx, res, req.companyId!, providerType, null);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
router.post("/adapter-models/:providerType", async (req, res) => {
|
|
304
|
+
const providerType = req.params.providerType;
|
|
305
|
+
const parsed = adapterModelsBodySchema.safeParse(req.body ?? {});
|
|
306
|
+
if (!parsed.success) {
|
|
307
|
+
return sendError(res, parsed.error.message, 422);
|
|
245
308
|
}
|
|
246
|
-
|
|
247
|
-
| "claude_code"
|
|
248
|
-
| "codex"
|
|
249
|
-
| "cursor"
|
|
250
|
-
| "opencode"
|
|
251
|
-
| "gemini_cli"
|
|
252
|
-
| "openai_api"
|
|
253
|
-
| "anthropic_api"
|
|
254
|
-
| "http"
|
|
255
|
-
| "shell";
|
|
256
|
-
const models = await getAdapterModels(typedProviderType, {
|
|
257
|
-
command: runtimeConfig.runtimeCommand,
|
|
258
|
-
args: runtimeConfig.runtimeArgs,
|
|
259
|
-
cwd: runtimeConfig.runtimeCwd,
|
|
260
|
-
env: runtimeConfig.runtimeEnv,
|
|
261
|
-
model: runtimeConfig.runtimeModel,
|
|
262
|
-
thinkingEffort: runtimeConfig.runtimeThinkingEffort,
|
|
263
|
-
timeoutMs: runtimeConfig.runtimeTimeoutSec > 0 ? runtimeConfig.runtimeTimeoutSec * 1000 : undefined,
|
|
264
|
-
interruptGraceSec: runtimeConfig.interruptGraceSec,
|
|
265
|
-
runPolicy: runtimeConfig.runPolicy
|
|
266
|
-
});
|
|
267
|
-
return sendOk(res, { providerType: typedProviderType, models });
|
|
309
|
+
return handleAdapterModelsRequest(ctx, res, req.companyId!, providerType, parsed.data);
|
|
268
310
|
});
|
|
269
311
|
|
|
270
312
|
router.post("/runtime-preflight", async (req, res) => {
|
|
@@ -425,6 +467,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
425
467
|
role: resolveAgentRoleText(parsed.data.role, parsed.data.roleKey, parsed.data.title),
|
|
426
468
|
roleKey: normalizeRoleKey(parsed.data.roleKey),
|
|
427
469
|
title: normalizeTitle(parsed.data.title),
|
|
470
|
+
capabilities: normalizeCapabilities(parsed.data.capabilities),
|
|
428
471
|
name: parsed.data.name,
|
|
429
472
|
providerType: parsed.data.providerType,
|
|
430
473
|
heartbeatCron: parsed.data.heartbeatCron,
|
|
@@ -573,6 +616,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
573
616
|
: undefined,
|
|
574
617
|
roleKey: parsed.data.roleKey !== undefined ? normalizeRoleKey(parsed.data.roleKey) : undefined,
|
|
575
618
|
title: parsed.data.title !== undefined ? normalizeTitle(parsed.data.title) : undefined,
|
|
619
|
+
capabilities:
|
|
620
|
+
parsed.data.capabilities !== undefined ? normalizeCapabilities(parsed.data.capabilities) : undefined,
|
|
576
621
|
name: parsed.data.name,
|
|
577
622
|
providerType: parsed.data.providerType,
|
|
578
623
|
status: parsed.data.status,
|
|
@@ -629,10 +674,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
629
674
|
});
|
|
630
675
|
|
|
631
676
|
router.post("/:agentId/pause", async (req, res) => {
|
|
632
|
-
|
|
633
|
-
if (res.headersSent) {
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
677
|
+
if (!enforcePermission(req, res, "agents:lifecycle")) return;
|
|
636
678
|
const agent = await updateAgent(ctx.db, {
|
|
637
679
|
companyId: req.companyId!,
|
|
638
680
|
id: req.params.agentId,
|
|
@@ -656,10 +698,7 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
656
698
|
});
|
|
657
699
|
|
|
658
700
|
router.post("/:agentId/resume", async (req, res) => {
|
|
659
|
-
|
|
660
|
-
if (res.headersSent) {
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
701
|
+
if (!enforcePermission(req, res, "agents:lifecycle")) return;
|
|
663
702
|
const agent = await updateAgent(ctx.db, {
|
|
664
703
|
companyId: req.companyId!,
|
|
665
704
|
id: req.params.agentId,
|
|
@@ -829,6 +868,11 @@ function normalizeTitle(input: string | null | undefined) {
|
|
|
829
868
|
return normalized ? normalized : null;
|
|
830
869
|
}
|
|
831
870
|
|
|
871
|
+
function normalizeCapabilities(input: string | null | undefined) {
|
|
872
|
+
const normalized = input?.trim();
|
|
873
|
+
return normalized ? normalized : null;
|
|
874
|
+
}
|
|
875
|
+
|
|
832
876
|
function resolveAgentRoleText(
|
|
833
877
|
legacyRole: string | undefined,
|
|
834
878
|
roleKeyInput: string | null | undefined,
|
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) {
|
|
@@ -98,6 +114,8 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
98
114
|
role: "CEO",
|
|
99
115
|
roleKey: "ceo",
|
|
100
116
|
title: "CEO",
|
|
117
|
+
capabilities:
|
|
118
|
+
"Company leadership: priorities, hiring, governance, and aligning agents to mission and budget.",
|
|
101
119
|
name: "CEO",
|
|
102
120
|
providerType,
|
|
103
121
|
heartbeatCron: "*/5 * * * *",
|
|
@@ -171,6 +189,7 @@ function parseAgentProvider(value: unknown) {
|
|
|
171
189
|
value === "opencode" ||
|
|
172
190
|
value === "openai_api" ||
|
|
173
191
|
value === "anthropic_api" ||
|
|
192
|
+
value === "http" ||
|
|
174
193
|
value === "shell"
|
|
175
194
|
) {
|
|
176
195
|
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);
|