@opentrust/dashboard 7.1.0
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/dist/index.js +84 -0
- package/dist/middleware/error-handler.js +7 -0
- package/dist/middleware/session-auth.js +58 -0
- package/dist/routes/agents.js +90 -0
- package/dist/routes/auth.js +122 -0
- package/dist/routes/detection.js +101 -0
- package/dist/routes/discovery.js +171 -0
- package/dist/routes/observations.js +111 -0
- package/dist/routes/policies.js +64 -0
- package/dist/routes/results.js +20 -0
- package/dist/routes/scanners.js +40 -0
- package/dist/routes/settings.js +71 -0
- package/dist/routes/usage.js +40 -0
- package/dist/schemas/validation.js +77 -0
- package/dist/services/core-client.js +56 -0
- package/dist/services/discovery.js +1 -0
- package/dist/services/tier-service.js +18 -0
- package/package.json +49 -0
- package/public/assets/index-Bg4pNJBb.js +184 -0
- package/public/assets/index-Bg4pNJBb.js.map +1 -0
- package/public/assets/index-CctIwU_L.css +1 -0
- package/public/index.html +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import helmet from "helmet";
|
|
4
|
+
import morgan from "morgan";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { sessionAuth } from "./middleware/session-auth.js";
|
|
9
|
+
import { authRouter } from "./routes/auth.js";
|
|
10
|
+
import { settingsRouter } from "./routes/settings.js";
|
|
11
|
+
import { agentsRouter } from "./routes/agents.js";
|
|
12
|
+
import { scannersRouter } from "./routes/scanners.js";
|
|
13
|
+
import { policiesRouter } from "./routes/policies.js";
|
|
14
|
+
import { detectionRouter } from "./routes/detection.js";
|
|
15
|
+
import { usageRouter } from "./routes/usage.js";
|
|
16
|
+
import { resultsRouter } from "./routes/results.js";
|
|
17
|
+
import { discoveryRouter } from "./routes/discovery.js";
|
|
18
|
+
import { observationsRouter } from "./routes/observations.js";
|
|
19
|
+
import { errorHandler } from "./middleware/error-handler.js";
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const app = express();
|
|
22
|
+
const PORT = parseInt(process.env.PORT || process.env.API_PORT || "53667", 10);
|
|
23
|
+
const DASHBOARD_MODE = (process.env.DASHBOARD_MODE || "selfhosted");
|
|
24
|
+
app.use(helmet({ contentSecurityPolicy: false }));
|
|
25
|
+
app.use(cors({
|
|
26
|
+
origin: DASHBOARD_MODE === "embedded" ? true : (process.env.WEB_ORIGIN || "http://localhost:53668"),
|
|
27
|
+
credentials: true,
|
|
28
|
+
}));
|
|
29
|
+
app.use(morgan("short"));
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
// Public routes
|
|
32
|
+
app.get("/health", (_req, res) => {
|
|
33
|
+
res.json({ status: "ok", service: "opentrust-api", timestamp: new Date().toISOString() });
|
|
34
|
+
});
|
|
35
|
+
app.use("/api/auth", authRouter); // /request, /verify/:token, /me, /logout
|
|
36
|
+
// Serve static web app in embedded mode (before auth middleware)
|
|
37
|
+
if (DASHBOARD_MODE === "embedded") {
|
|
38
|
+
const webOutPaths = [
|
|
39
|
+
join(__dirname, "..", "..", "web", "out"), // monorepo: apps/api/dist → apps/web/out
|
|
40
|
+
join(__dirname, "..", "public"), // npm package: dist → public
|
|
41
|
+
];
|
|
42
|
+
const webOutDir = webOutPaths.find((p) => existsSync(p));
|
|
43
|
+
if (webOutDir) {
|
|
44
|
+
// 挂载到 /dashboard/ 以匹配 Vite base 配置
|
|
45
|
+
app.use("/dashboard", express.static(webOutDir, { extensions: ["html"] }));
|
|
46
|
+
// 根路径重定向到 /dashboard/
|
|
47
|
+
app.get("/", (_req, res) => res.redirect("/dashboard/"));
|
|
48
|
+
// SPA fallback: /dashboard/ 下非静态文件路由返回 index.html
|
|
49
|
+
app.use("/dashboard", (req, res, next) => {
|
|
50
|
+
// 跳过已匹配的静态文件(有扩展名的路径)
|
|
51
|
+
if (req.path.match(/\.\w+$/))
|
|
52
|
+
return next();
|
|
53
|
+
const indexPath = join(webOutDir, "index.html");
|
|
54
|
+
if (existsSync(indexPath)) {
|
|
55
|
+
res.sendFile(indexPath);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
next();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Session-protected routes
|
|
64
|
+
app.use(sessionAuth);
|
|
65
|
+
app.use("/api/settings", settingsRouter);
|
|
66
|
+
app.use("/api/agents", agentsRouter);
|
|
67
|
+
app.use("/api/scanners", scannersRouter);
|
|
68
|
+
app.use("/api/policies", policiesRouter);
|
|
69
|
+
app.use("/api/detect", detectionRouter);
|
|
70
|
+
app.use("/api/usage", usageRouter);
|
|
71
|
+
app.use("/api/results", resultsRouter);
|
|
72
|
+
app.use("/api/discovery", discoveryRouter);
|
|
73
|
+
app.use("/api/observations", observationsRouter);
|
|
74
|
+
app.use(errorHandler);
|
|
75
|
+
app.listen(PORT, () => {
|
|
76
|
+
console.log(`OpenTrust API running on port ${PORT}`);
|
|
77
|
+
console.log(`DashboardMode: ${DASHBOARD_MODE}`);
|
|
78
|
+
if (process.env.DEV_MODE === "true") {
|
|
79
|
+
console.log(`DevMode: ON — authentication disabled`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`Auth: POST /api/auth/request — send magic link`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const IS_DEV_MODE = process.env.DEV_MODE === "true";
|
|
2
|
+
const CORE_URL = process.env.OG_CORE_URL || "http://localhost:53666";
|
|
3
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
4
|
+
const DEV_TENANT_ID = "default";
|
|
5
|
+
const DEV_EMAIL = "dev@localhost";
|
|
6
|
+
const DEV_API_KEY = "sk-og-dev";
|
|
7
|
+
const sessionCache = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Session authentication middleware for OpenTrust.
|
|
10
|
+
*
|
|
11
|
+
* When DEV_MODE=true, skips all validation and uses default dev identity.
|
|
12
|
+
* Otherwise validates the API key against the core (cached for 5 minutes).
|
|
13
|
+
* Sets res.locals.tenantId, res.locals.userEmail, and res.locals.coreApiKey.
|
|
14
|
+
*/
|
|
15
|
+
export async function sessionAuth(req, res, next) {
|
|
16
|
+
if (IS_DEV_MODE) {
|
|
17
|
+
res.locals.tenantId = DEV_TENANT_ID;
|
|
18
|
+
res.locals.userEmail = DEV_EMAIL;
|
|
19
|
+
res.locals.coreApiKey = DEV_API_KEY;
|
|
20
|
+
next();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const apiKey = req.headers.authorization?.replace("Bearer ", "");
|
|
24
|
+
if (!apiKey?.startsWith("sk-og-")) {
|
|
25
|
+
res.status(401).json({ success: false, error: "Not authenticated" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const cached = sessionCache.get(apiKey);
|
|
29
|
+
if (cached && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
|
|
30
|
+
res.locals.tenantId = cached.email;
|
|
31
|
+
res.locals.userEmail = cached.email;
|
|
32
|
+
res.locals.coreApiKey = apiKey;
|
|
33
|
+
next();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const coreRes = await fetch(`${CORE_URL}/api/v1/account`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
39
|
+
});
|
|
40
|
+
if (!coreRes.ok) {
|
|
41
|
+
res.status(401).json({ success: false, error: "Invalid or inactive API key" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const data = await coreRes.json();
|
|
45
|
+
if (!data.success || !data.email) {
|
|
46
|
+
res.status(401).json({ success: false, error: "Agent not activated" });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
sessionCache.set(apiKey, { email: data.email, agentId: data.agentId, cachedAt: Date.now() });
|
|
50
|
+
res.locals.tenantId = data.email;
|
|
51
|
+
res.locals.userEmail = data.email;
|
|
52
|
+
res.locals.coreApiKey = apiKey;
|
|
53
|
+
next();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
res.status(503).json({ success: false, error: "Core service unavailable" });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { db, agentQueries } from "@opentrust/db";
|
|
3
|
+
import { MAX_AGENTS } from "@opentrust/shared";
|
|
4
|
+
const agents = agentQueries(db);
|
|
5
|
+
export const agentsRouter = Router();
|
|
6
|
+
// GET /api/agents
|
|
7
|
+
agentsRouter.get("/", async (_req, res, next) => {
|
|
8
|
+
try {
|
|
9
|
+
const tenantId = res.locals.tenantId;
|
|
10
|
+
const data = await agents.findAll(tenantId);
|
|
11
|
+
res.json({ success: true, data });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
next(err);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
// POST /api/agents
|
|
18
|
+
agentsRouter.post("/", async (req, res, next) => {
|
|
19
|
+
try {
|
|
20
|
+
const tenantId = res.locals.tenantId;
|
|
21
|
+
const { name, description, provider, metadata } = req.body;
|
|
22
|
+
if (!name) {
|
|
23
|
+
res.status(400).json({ success: false, error: "name is required" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const currentCount = await agents.countAll(tenantId);
|
|
27
|
+
if (currentCount >= MAX_AGENTS) {
|
|
28
|
+
res.status(403).json({
|
|
29
|
+
success: false,
|
|
30
|
+
error: `Agent limit reached (${MAX_AGENTS}).`,
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const agent = await agents.create({
|
|
35
|
+
name,
|
|
36
|
+
description: description || null,
|
|
37
|
+
provider: provider || "custom",
|
|
38
|
+
metadata: metadata || {},
|
|
39
|
+
tenantId,
|
|
40
|
+
});
|
|
41
|
+
res.status(201).json({ success: true, data: agent });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
next(err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// PUT /api/agents/:id
|
|
48
|
+
agentsRouter.put("/:id", async (req, res, next) => {
|
|
49
|
+
try {
|
|
50
|
+
const tenantId = res.locals.tenantId;
|
|
51
|
+
const { name, description, provider, status, metadata } = req.body;
|
|
52
|
+
const agent = await agents.update(req.params.id, {
|
|
53
|
+
...(name && { name }),
|
|
54
|
+
...(description !== undefined && { description }),
|
|
55
|
+
...(provider && { provider }),
|
|
56
|
+
...(status && { status }),
|
|
57
|
+
...(metadata && { metadata }),
|
|
58
|
+
}, tenantId);
|
|
59
|
+
if (!agent) {
|
|
60
|
+
res.status(404).json({ success: false, error: "Agent not found" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.json({ success: true, data: agent });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
next(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// DELETE /api/agents/:id
|
|
70
|
+
agentsRouter.delete("/:id", async (req, res, next) => {
|
|
71
|
+
try {
|
|
72
|
+
const tenantId = res.locals.tenantId;
|
|
73
|
+
await agents.delete(req.params.id, tenantId);
|
|
74
|
+
res.json({ success: true });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
next(err);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// POST /api/agents/:id/heartbeat
|
|
81
|
+
agentsRouter.post("/:id/heartbeat", async (req, res, next) => {
|
|
82
|
+
try {
|
|
83
|
+
const tenantId = res.locals.tenantId;
|
|
84
|
+
await agents.heartbeat(req.params.id, tenantId);
|
|
85
|
+
res.json({ success: true });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
next(err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
export const authRouter = Router();
|
|
3
|
+
const IS_DEV_MODE = process.env.DEV_MODE === "true";
|
|
4
|
+
const CORE_URL = process.env.OG_CORE_URL || "http://localhost:53666";
|
|
5
|
+
const DEV_EMAIL = "dev@localhost";
|
|
6
|
+
const DEV_RESPONSE = {
|
|
7
|
+
success: true,
|
|
8
|
+
email: DEV_EMAIL,
|
|
9
|
+
agentId: "dev-agent",
|
|
10
|
+
name: "Dev Agent",
|
|
11
|
+
quotaTotal: 999999,
|
|
12
|
+
quotaUsed: 0,
|
|
13
|
+
quotaRemaining: 999999,
|
|
14
|
+
agents: [],
|
|
15
|
+
};
|
|
16
|
+
async function fetchCoreAccount(apiKey) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`${CORE_URL}/api/v1/account`, {
|
|
19
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
return null;
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function fetchCoreAccounts(apiKey) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`${CORE_URL}/api/v1/accounts`, {
|
|
32
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
return null;
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
authRouter.post("/login", async (req, res, next) => {
|
|
43
|
+
if (IS_DEV_MODE) {
|
|
44
|
+
res.json(DEV_RESPONSE);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const { apiKey, email } = req.body;
|
|
49
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
50
|
+
res.status(400).json({ success: false, error: "Valid email required" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!apiKey || !apiKey.startsWith("sk-og-")) {
|
|
54
|
+
res.status(400).json({ success: false, error: "Valid API key required (sk-og-...)" });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const account = await fetchCoreAccount(apiKey);
|
|
58
|
+
if (!account?.success) {
|
|
59
|
+
res.status(401).json({ success: false, error: "Invalid API key" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!account.email) {
|
|
63
|
+
res.status(403).json({ success: false, error: "Agent not yet activated. Complete email verification first." });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (account.email.toLowerCase() !== email.toLowerCase()) {
|
|
67
|
+
res.status(401).json({ success: false, error: "Email does not match this API key" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const accounts = await fetchCoreAccounts(apiKey);
|
|
71
|
+
const agents = accounts?.agents ?? [];
|
|
72
|
+
res.json({
|
|
73
|
+
success: true,
|
|
74
|
+
email: account.email,
|
|
75
|
+
agentId: account.agentId,
|
|
76
|
+
name: account.name,
|
|
77
|
+
quotaTotal: account.quotaTotal,
|
|
78
|
+
quotaUsed: account.quotaUsed,
|
|
79
|
+
quotaRemaining: account.quotaRemaining,
|
|
80
|
+
agents,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
next(err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
authRouter.get("/me", async (req, res, next) => {
|
|
88
|
+
if (IS_DEV_MODE) {
|
|
89
|
+
res.json(DEV_RESPONSE);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const apiKey = req.headers.authorization?.replace("Bearer ", "");
|
|
94
|
+
if (!apiKey?.startsWith("sk-og-")) {
|
|
95
|
+
res.status(401).json({ success: false, error: "Not authenticated" });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const account = await fetchCoreAccount(apiKey);
|
|
99
|
+
if (!account?.success || !account.email) {
|
|
100
|
+
res.status(401).json({ success: false, error: "Invalid or inactive API key" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const accounts = await fetchCoreAccounts(apiKey);
|
|
104
|
+
const agents = accounts?.agents ?? [];
|
|
105
|
+
res.json({
|
|
106
|
+
success: true,
|
|
107
|
+
email: account.email,
|
|
108
|
+
agentId: account.agentId,
|
|
109
|
+
name: account.name,
|
|
110
|
+
quotaTotal: account.quotaTotal,
|
|
111
|
+
quotaUsed: account.quotaUsed,
|
|
112
|
+
quotaRemaining: account.quotaRemaining,
|
|
113
|
+
agents,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
next(err);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
authRouter.post("/logout", (_req, res) => {
|
|
121
|
+
res.json({ success: true });
|
|
122
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { db, scannerQueries, policyQueries, usageQueries, detectionResultQueries, settingsQueries } from "@opentrust/db";
|
|
3
|
+
import { callCoreDetect } from "../services/core-client.js";
|
|
4
|
+
const scanners = scannerQueries(db);
|
|
5
|
+
const policies = policyQueries(db);
|
|
6
|
+
const usage = usageQueries(db);
|
|
7
|
+
const detectionResults = detectionResultQueries(db);
|
|
8
|
+
const settings = settingsQueries(db);
|
|
9
|
+
export const detectionRouter = Router();
|
|
10
|
+
/**
|
|
11
|
+
* POST /api/detect
|
|
12
|
+
* Detection proxy endpoint.
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Check core key is configured
|
|
15
|
+
* 2. Get scanner config
|
|
16
|
+
* 3. Call core /v1/detect
|
|
17
|
+
* 4. Evaluate policies
|
|
18
|
+
* 5. Record usage + detection result
|
|
19
|
+
* 6. Return response
|
|
20
|
+
*/
|
|
21
|
+
detectionRouter.post("/", async (req, res, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const tenantId = res.locals.tenantId;
|
|
24
|
+
// 1. Check core key (skip in dev mode)
|
|
25
|
+
const coreKey = await settings.get("og_core_key");
|
|
26
|
+
if (!coreKey && process.env.DEV_MODE !== "true") {
|
|
27
|
+
res.status(503).json({
|
|
28
|
+
success: false,
|
|
29
|
+
error: "core key not configured. Go to Settings to add your key.",
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// 2. Get scanner config
|
|
34
|
+
const allScanners = await scanners.getAll(tenantId);
|
|
35
|
+
const coreScanners = allScanners.map((s) => ({
|
|
36
|
+
scannerId: s.scannerId,
|
|
37
|
+
name: s.name,
|
|
38
|
+
description: s.description,
|
|
39
|
+
isEnabled: s.isEnabled,
|
|
40
|
+
}));
|
|
41
|
+
// Validate request body
|
|
42
|
+
const { messages, format, role, agentId } = req.body;
|
|
43
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
44
|
+
res.status(400).json({ success: false, error: "messages array is required and must not be empty" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// 3. Call core
|
|
48
|
+
const coreResult = await callCoreDetect(messages, coreScanners, { format, role });
|
|
49
|
+
// 4. Evaluate policies
|
|
50
|
+
let policyAction = null;
|
|
51
|
+
if (!coreResult.safe) {
|
|
52
|
+
const enabledPolicies = await policies.getEnabled(tenantId);
|
|
53
|
+
for (const policy of enabledPolicies) {
|
|
54
|
+
const policyScannerIds = policy.scannerIds;
|
|
55
|
+
const matchesCategory = coreResult.categories.some((c) => policyScannerIds.includes(c));
|
|
56
|
+
if (matchesCategory && coreResult.sensitivity_score >= policy.sensitivityThreshold) {
|
|
57
|
+
policyAction = policy.action;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 5. Record usage + detection result
|
|
63
|
+
await usage.log({
|
|
64
|
+
agentId: agentId || null,
|
|
65
|
+
endpoint: "/api/detect",
|
|
66
|
+
statusCode: 200,
|
|
67
|
+
responseSafe: coreResult.safe,
|
|
68
|
+
categories: coreResult.categories,
|
|
69
|
+
latencyMs: coreResult.latency_ms,
|
|
70
|
+
requestId: coreResult.request_id,
|
|
71
|
+
tenantId,
|
|
72
|
+
});
|
|
73
|
+
await detectionResults.create({
|
|
74
|
+
agentId: agentId || null,
|
|
75
|
+
safe: coreResult.safe,
|
|
76
|
+
categories: coreResult.categories,
|
|
77
|
+
sensitivityScore: coreResult.sensitivity_score,
|
|
78
|
+
findings: coreResult.findings,
|
|
79
|
+
latencyMs: coreResult.latency_ms,
|
|
80
|
+
requestId: coreResult.request_id,
|
|
81
|
+
tenantId,
|
|
82
|
+
});
|
|
83
|
+
// 6. Return response with policy action
|
|
84
|
+
const response = {
|
|
85
|
+
...coreResult,
|
|
86
|
+
...(policyAction && { policy_action: policyAction }),
|
|
87
|
+
};
|
|
88
|
+
if (policyAction === "block") {
|
|
89
|
+
res.status(403).json({ success: true, data: response, blocked: true });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.json({ success: true, data: response });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (err instanceof Error && (err.message.includes("ECONNREFUSED") || err.message.includes("fetch failed"))) {
|
|
96
|
+
res.status(503).json({ success: false, error: "Detection service is temporarily unavailable. Please try again later." });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
next(err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { db, agentQueries } from "@opentrust/db";
|
|
3
|
+
const agentsDb = agentQueries(db);
|
|
4
|
+
export const discoveryRouter = Router();
|
|
5
|
+
function registeredToDiscovered(a) {
|
|
6
|
+
const m = (a.metadata ?? {});
|
|
7
|
+
return {
|
|
8
|
+
id: m.openclawId ?? a.id,
|
|
9
|
+
name: a.name,
|
|
10
|
+
status: a.status,
|
|
11
|
+
emoji: m.emoji ?? "🤖",
|
|
12
|
+
creature: m.creature ?? "",
|
|
13
|
+
vibe: m.vibe ?? "",
|
|
14
|
+
model: m.model ?? "",
|
|
15
|
+
provider: m.provider ?? a.provider,
|
|
16
|
+
workspacePath: "",
|
|
17
|
+
ownerName: m.ownerName ?? "",
|
|
18
|
+
avatarUrl: null,
|
|
19
|
+
skills: m.skills ?? [],
|
|
20
|
+
connectedSystems: m.connectedSystems ?? [],
|
|
21
|
+
channels: m.channels ?? [],
|
|
22
|
+
plugins: m.plugins ?? [],
|
|
23
|
+
hooks: m.hooks ?? [],
|
|
24
|
+
sessionCount: m.sessionCount ?? 0,
|
|
25
|
+
lastActive: m.lastActive ?? a.lastSeenAt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function registeredToProfile(a) {
|
|
29
|
+
const m = (a.metadata ?? {});
|
|
30
|
+
const wf = m.workspaceFiles ?? {};
|
|
31
|
+
return {
|
|
32
|
+
...registeredToDiscovered(a),
|
|
33
|
+
workspaceFiles: {
|
|
34
|
+
soul: wf.soul ?? "",
|
|
35
|
+
identity: wf.identity ?? "",
|
|
36
|
+
user: wf.user ?? "",
|
|
37
|
+
agents: wf.agents ?? "",
|
|
38
|
+
tools: wf.tools ?? "",
|
|
39
|
+
heartbeat: wf.heartbeat ?? "",
|
|
40
|
+
},
|
|
41
|
+
bootstrapExists: m.bootstrapExists ?? false,
|
|
42
|
+
cronJobs: m.cronJobs ?? [],
|
|
43
|
+
allSkills: (m.skills ?? []).map((s) => ({ ...s, source: "workspace" })),
|
|
44
|
+
bundledExtensions: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ── Routes ───────────────────────────────────────────────────────────────────
|
|
48
|
+
// GET /api/discovery/agents — list all agents (DB only)
|
|
49
|
+
discoveryRouter.get("/agents", async (_req, res, next) => {
|
|
50
|
+
try {
|
|
51
|
+
const tenantId = res.locals.tenantId;
|
|
52
|
+
const registered = await agentsDb.findAll(tenantId);
|
|
53
|
+
res.json({ success: true, data: registered.map(registeredToDiscovered) });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
next(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// GET /api/discovery/agents/:id — single agent (DB only)
|
|
60
|
+
discoveryRouter.get("/agents/:id", async (req, res, next) => {
|
|
61
|
+
try {
|
|
62
|
+
const tenantId = res.locals.tenantId;
|
|
63
|
+
const id = req.params.id;
|
|
64
|
+
const registered = await agentsDb.findAll(tenantId);
|
|
65
|
+
const match = registered.find((a) => {
|
|
66
|
+
const meta = (a.metadata ?? {});
|
|
67
|
+
return meta.openclawId === id || a.id === id;
|
|
68
|
+
});
|
|
69
|
+
if (!match) {
|
|
70
|
+
res.status(404).json({ success: false, error: "Agent not found" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
res.json({ success: true, data: registeredToDiscovered(match) });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
next(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// GET /api/discovery/agents/:id/profile — enriched profile (DB only)
|
|
80
|
+
discoveryRouter.get("/agents/:id/profile", async (req, res, next) => {
|
|
81
|
+
try {
|
|
82
|
+
const tenantId = res.locals.tenantId;
|
|
83
|
+
const id = req.params.id;
|
|
84
|
+
const registered = await agentsDb.findAll(tenantId);
|
|
85
|
+
const match = registered.find((a) => {
|
|
86
|
+
const meta = (a.metadata ?? {});
|
|
87
|
+
return meta.openclawId === id || a.id === id;
|
|
88
|
+
});
|
|
89
|
+
if (!match) {
|
|
90
|
+
res.status(404).json({ success: false, error: "Agent not found" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const profile = registeredToProfile(match);
|
|
94
|
+
res.json({ success: true, data: { ...profile, registeredAgentId: match.id } });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
next(err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// GET /api/discovery/agents/:id/summary — LLM-generated summary
|
|
101
|
+
discoveryRouter.get("/agents/:id/summary", async (req, res, next) => {
|
|
102
|
+
try {
|
|
103
|
+
const tenantId = res.locals.tenantId;
|
|
104
|
+
const id = req.params.id;
|
|
105
|
+
const registered = await agentsDb.findAll(tenantId);
|
|
106
|
+
const match = registered.find((a) => {
|
|
107
|
+
const meta = (a.metadata ?? {});
|
|
108
|
+
return meta.openclawId === id || a.id === id;
|
|
109
|
+
});
|
|
110
|
+
if (!match) {
|
|
111
|
+
res.status(404).json({ success: false, error: "Agent not found" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const agent = registeredToDiscovered(match);
|
|
115
|
+
const prompt = `Summarize this AI agent in 2-3 concise paragraphs for a security dashboard:
|
|
116
|
+
|
|
117
|
+
Name: ${agent.name} ${agent.emoji}
|
|
118
|
+
Creature: ${agent.creature}
|
|
119
|
+
Vibe: ${agent.vibe}
|
|
120
|
+
Model: ${agent.provider}/${agent.model}
|
|
121
|
+
Skills: ${agent.skills.map((s) => s.name).join(", ") || "none"}
|
|
122
|
+
Connected Systems: ${agent.connectedSystems.join(", ") || "none"}
|
|
123
|
+
Channels: ${agent.channels.join(", ") || "none"}
|
|
124
|
+
Plugins: ${agent.plugins.map((p) => `${p.name}${p.enabled ? "" : " (disabled)"}`).join(", ") || "none"}
|
|
125
|
+
Hooks: ${agent.hooks.map((h) => `${h.name}${h.enabled ? "" : " (disabled)"}`).join(", ") || "none"}
|
|
126
|
+
Sessions: ${agent.sessionCount}
|
|
127
|
+
Last Active: ${agent.lastActive || "unknown"}
|
|
128
|
+
|
|
129
|
+
Focus on: what this agent does, its capabilities, connected systems and potential security surface area. Keep it factual and useful for a security team.`;
|
|
130
|
+
const gatewayPort = process.env.GATEWAY_PORT || "8900";
|
|
131
|
+
try {
|
|
132
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || "not-set";
|
|
133
|
+
const response = await fetch(`http://localhost:${gatewayPort}/v1/messages`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
"x-api-key": apiKey,
|
|
138
|
+
"anthropic-version": "2023-06-01",
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
model: "claude-sonnet-4-5-20250929",
|
|
142
|
+
max_tokens: 500,
|
|
143
|
+
messages: [{ role: "user", content: prompt }],
|
|
144
|
+
}),
|
|
145
|
+
signal: AbortSignal.timeout(30000),
|
|
146
|
+
});
|
|
147
|
+
if (response.ok) {
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
const text = data.content?.find((c) => c.type === "text")?.text;
|
|
150
|
+
if (text) {
|
|
151
|
+
res.json({ success: true, data: { summary: text } });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Gateway not available, fall through to static summary
|
|
158
|
+
}
|
|
159
|
+
const skillList = agent.skills.map((s) => s.name).join(", ");
|
|
160
|
+
const systemList = agent.connectedSystems.join(", ");
|
|
161
|
+
const summary = `${agent.name} is an AI agent running on ${agent.provider}/${agent.model}. ` +
|
|
162
|
+
(skillList ? `It has ${agent.skills.length} skill(s): ${skillList}. ` : "It has no registered skills. ") +
|
|
163
|
+
(systemList ? `Connected systems: ${systemList}. ` : "") +
|
|
164
|
+
`It has ${agent.sessionCount} recorded session(s)` +
|
|
165
|
+
(agent.lastActive ? `, last active ${new Date(agent.lastActive).toLocaleDateString()}.` : ".");
|
|
166
|
+
res.json({ success: true, data: { summary } });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
next(err);
|
|
170
|
+
}
|
|
171
|
+
});
|