@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 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,7 @@
1
+ export function errorHandler(err, _req, res, _next) {
2
+ console.error("Unhandled error:", err);
3
+ res.status(500).json({
4
+ success: false,
5
+ error: "Internal server error",
6
+ });
7
+ }
@@ -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
+ });