siclaw 0.1.0 → 0.1.2
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/README.md +75 -114
- package/dist/agentbox/gateway-client.d.ts +2 -1
- package/dist/agentbox/gateway-client.js +6 -2
- package/dist/agentbox/gateway-client.js.map +1 -1
- package/dist/agentbox/http-server.js +184 -19
- package/dist/agentbox/http-server.js.map +1 -1
- package/dist/agentbox/resource-handlers.d.ts +1 -0
- package/dist/agentbox/resource-handlers.js +23 -23
- package/dist/agentbox/resource-handlers.js.map +1 -1
- package/dist/agentbox/session.js +85 -5
- package/dist/agentbox/session.js.map +1 -1
- package/dist/agentbox-main.d.ts +2 -1
- package/dist/agentbox-main.js +65 -18
- package/dist/agentbox-main.js.map +1 -1
- package/dist/cli-credentials.d.ts +1 -0
- package/dist/cli-credentials.js +109 -0
- package/dist/cli-credentials.js.map +1 -0
- package/dist/cli-first-run.d.ts +11 -0
- package/dist/cli-first-run.js +99 -0
- package/dist/cli-first-run.js.map +1 -0
- package/dist/cli-main.js +33 -11
- package/dist/cli-main.js.map +1 -1
- package/dist/cli-setup.d.ts +5 -11
- package/dist/cli-setup.js +12 -225
- package/dist/cli-setup.js.map +1 -1
- package/dist/core/agent-factory.d.ts +4 -0
- package/dist/core/agent-factory.js +102 -151
- package/dist/core/agent-factory.js.map +1 -1
- package/dist/core/config.d.ts +10 -3
- package/dist/core/config.js +11 -95
- package/dist/core/config.js.map +1 -1
- package/dist/core/extensions/deep-investigation.d.ts +2 -1
- package/dist/core/extensions/deep-investigation.js +144 -24
- package/dist/core/extensions/deep-investigation.js.map +1 -1
- package/dist/core/extensions/setup.d.ts +8 -0
- package/dist/core/extensions/setup.js +669 -0
- package/dist/core/extensions/setup.js.map +1 -0
- package/dist/core/llm-proxy.js +7 -3
- package/dist/core/llm-proxy.js.map +1 -1
- package/dist/core/mcp-client.d.ts +0 -10
- package/dist/core/mcp-client.js +0 -65
- package/dist/core/mcp-client.js.map +1 -1
- package/dist/core/prompt.d.ts +1 -1
- package/dist/core/prompt.js +42 -5
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/provider-presets.d.ts +14 -0
- package/dist/core/provider-presets.js +81 -0
- package/dist/core/provider-presets.js.map +1 -0
- package/dist/cron/cron-coordinator.d.ts +2 -0
- package/dist/cron/cron-coordinator.js +46 -14
- package/dist/cron/cron-coordinator.js.map +1 -1
- package/dist/cron/cron-executor.js +33 -8
- package/dist/cron/cron-executor.js.map +1 -1
- package/dist/cron/cron-scheduler.d.ts +1 -1
- package/dist/cron/gateway-client.d.ts +5 -0
- package/dist/cron/gateway-client.js +43 -8
- package/dist/cron/gateway-client.js.map +1 -1
- package/dist/cron-main.js +39 -9
- package/dist/cron-main.js.map +1 -1
- package/dist/gateway/agentbox/client.d.ts +11 -0
- package/dist/gateway/agentbox/client.js +18 -0
- package/dist/gateway/agentbox/client.js.map +1 -1
- package/dist/gateway/agentbox/k8s-spawner.d.ts +11 -2
- package/dist/gateway/agentbox/k8s-spawner.js +95 -52
- package/dist/gateway/agentbox/k8s-spawner.js.map +1 -1
- package/dist/gateway/agentbox/local-spawner.d.ts +1 -1
- package/dist/gateway/agentbox/local-spawner.js +4 -2
- package/dist/gateway/agentbox/local-spawner.js.map +1 -1
- package/dist/gateway/agentbox/manager.d.ts +0 -10
- package/dist/gateway/agentbox/manager.js +11 -30
- package/dist/gateway/agentbox/manager.js.map +1 -1
- package/dist/gateway/agentbox/types.d.ts +6 -4
- package/dist/gateway/cron/cron-service.d.ts +49 -0
- package/dist/gateway/cron/cron-service.js +259 -0
- package/dist/gateway/cron/cron-service.js.map +1 -0
- package/dist/gateway/db/init-schema.js +44 -0
- package/dist/gateway/db/init-schema.js.map +1 -1
- package/dist/gateway/db/migrate-sqlite.js +73 -4
- package/dist/gateway/db/migrate-sqlite.js.map +1 -1
- package/dist/gateway/db/repositories/chat-repo.d.ts +56 -2
- package/dist/gateway/db/repositories/chat-repo.js +132 -2
- package/dist/gateway/db/repositories/chat-repo.js.map +1 -1
- package/dist/gateway/db/repositories/config-repo.d.ts +31 -2
- package/dist/gateway/db/repositories/config-repo.js +57 -7
- package/dist/gateway/db/repositories/config-repo.js.map +1 -1
- package/dist/gateway/db/repositories/env-repo.d.ts +14 -0
- package/dist/gateway/db/repositories/env-repo.js +15 -2
- package/dist/gateway/db/repositories/env-repo.js.map +1 -1
- package/dist/gateway/db/repositories/model-config-repo.d.ts +1 -1
- package/dist/gateway/db/repositories/model-config-repo.js +26 -12
- package/dist/gateway/db/repositories/model-config-repo.js.map +1 -1
- package/dist/gateway/db/repositories/skill-repo.d.ts +0 -5
- package/dist/gateway/db/repositories/skill-review-repo.d.ts +1 -0
- package/dist/gateway/db/repositories/skill-review-repo.js +4 -1
- package/dist/gateway/db/repositories/skill-review-repo.js.map +1 -1
- package/dist/gateway/db/repositories/skill-version-repo.js +0 -1
- package/dist/gateway/db/repositories/skill-version-repo.js.map +1 -1
- package/dist/gateway/db/repositories/system-config-repo.d.ts +1 -1
- package/dist/gateway/db/repositories/system-config-repo.js +2 -1
- package/dist/gateway/db/repositories/system-config-repo.js.map +1 -1
- package/dist/gateway/db/repositories/user-env-config-repo.d.ts +13 -0
- package/dist/gateway/db/repositories/user-env-config-repo.js +11 -0
- package/dist/gateway/db/repositories/user-env-config-repo.js.map +1 -1
- package/dist/gateway/db/repositories/workspace-repo.d.ts +3 -2
- package/dist/gateway/db/repositories/workspace-repo.js +6 -2
- package/dist/gateway/db/repositories/workspace-repo.js.map +1 -1
- package/dist/gateway/db/schema-mysql.d.ts +473 -51
- package/dist/gateway/db/schema-mysql.js +35 -4
- package/dist/gateway/db/schema-mysql.js.map +1 -1
- package/dist/gateway/db/schema-sqlite.d.ts +522 -57
- package/dist/gateway/db/schema-sqlite.js +38 -6
- package/dist/gateway/db/schema-sqlite.js.map +1 -1
- package/dist/gateway/db/schema.d.ts +471 -51
- package/dist/gateway/db/schema.js +1 -1
- package/dist/gateway/db/schema.js.map +1 -1
- package/dist/gateway/metrics-aggregator.d.ts +65 -0
- package/dist/gateway/metrics-aggregator.js +244 -0
- package/dist/gateway/metrics-aggregator.js.map +1 -0
- package/dist/gateway/plugins/channel-bridge.d.ts +4 -1
- package/dist/gateway/plugins/channel-bridge.js +78 -86
- package/dist/gateway/plugins/channel-bridge.js.map +1 -1
- package/dist/gateway/rpc-methods.d.ts +4 -2
- package/dist/gateway/rpc-methods.js +962 -163
- package/dist/gateway/rpc-methods.js.map +1 -1
- package/dist/gateway/security/cert-manager.d.ts +2 -2
- package/dist/gateway/security/cert-manager.js +4 -2
- package/dist/gateway/security/cert-manager.js.map +1 -1
- package/dist/gateway/server.d.ts +4 -8
- package/dist/gateway/server.js +297 -261
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/skills/file-writer.js +17 -11
- package/dist/gateway/skills/file-writer.js.map +1 -1
- package/dist/gateway/skills/script-evaluator.js +12 -9
- package/dist/gateway/skills/script-evaluator.js.map +1 -1
- package/dist/gateway/web/dist/assets/index-0p17ZeTP.js +740 -0
- package/dist/gateway/web/dist/assets/index-9eP6nPUq.js +741 -0
- package/dist/gateway/web/dist/assets/index-9eP6nPUq.js.map +1 -0
- package/dist/gateway/web/dist/assets/index-CAmSY91d.js +675 -0
- package/dist/gateway/web/dist/assets/index-DMFEh8Pp.css +1 -0
- package/dist/gateway/web/dist/assets/index-DyowBCEj.css +1 -0
- package/dist/gateway/web/dist/assets/index-PDK5JJDO.css +1 -0
- package/dist/gateway/web/dist/index.html +2 -2
- package/dist/gateway-main.js +27 -10
- package/dist/gateway-main.js.map +1 -1
- package/dist/memory/embeddings.js +5 -4
- package/dist/memory/embeddings.js.map +1 -1
- package/dist/memory/indexer.d.ts +23 -3
- package/dist/memory/indexer.js +235 -23
- package/dist/memory/indexer.js.map +1 -1
- package/dist/memory/schema.js +15 -1
- package/dist/memory/schema.js.map +1 -1
- package/dist/memory/types.d.ts +18 -0
- package/dist/memory/types.js +6 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/shared/detect-language.d.ts +12 -0
- package/dist/shared/detect-language.js +78 -0
- package/dist/shared/detect-language.js.map +1 -0
- package/dist/shared/diagnostic-events.d.ts +70 -0
- package/dist/shared/diagnostic-events.js +38 -0
- package/dist/shared/diagnostic-events.js.map +1 -0
- package/dist/shared/local-collector.d.ts +56 -0
- package/dist/shared/local-collector.js +284 -0
- package/dist/shared/local-collector.js.map +1 -0
- package/dist/shared/metrics-types.d.ts +64 -0
- package/dist/shared/metrics-types.js +25 -0
- package/dist/shared/metrics-types.js.map +1 -0
- package/dist/shared/metrics.d.ts +19 -0
- package/dist/shared/metrics.js +185 -0
- package/dist/shared/metrics.js.map +1 -0
- package/dist/shared/path-utils.d.ts +15 -0
- package/dist/shared/path-utils.js +23 -0
- package/dist/shared/path-utils.js.map +1 -0
- package/dist/shared/retry.d.ts +35 -0
- package/dist/shared/retry.js +61 -0
- package/dist/shared/retry.js.map +1 -0
- package/dist/tools/command-sets.d.ts +18 -2
- package/dist/tools/command-sets.js +207 -32
- package/dist/tools/command-sets.js.map +1 -1
- package/dist/tools/command-validator.d.ts +56 -0
- package/dist/tools/command-validator.js +357 -0
- package/dist/tools/command-validator.js.map +1 -0
- package/dist/tools/create-skill.js +26 -1
- package/dist/tools/create-skill.js.map +1 -1
- package/dist/tools/credential-list.js +1 -23
- package/dist/tools/credential-list.js.map +1 -1
- package/dist/tools/credential-manager.d.ts +98 -0
- package/dist/tools/credential-manager.js +313 -0
- package/dist/tools/credential-manager.js.map +1 -0
- package/dist/tools/deep-search/engine.js +184 -127
- package/dist/tools/deep-search/engine.js.map +1 -1
- package/dist/tools/deep-search/prompts.d.ts +10 -2
- package/dist/tools/deep-search/prompts.js +37 -36
- package/dist/tools/deep-search/prompts.js.map +1 -1
- package/dist/tools/deep-search/schemas.d.ts +87 -0
- package/dist/tools/deep-search/schemas.js +85 -0
- package/dist/tools/deep-search/schemas.js.map +1 -0
- package/dist/tools/deep-search/sub-agent.d.ts +21 -0
- package/dist/tools/deep-search/sub-agent.js +153 -4
- package/dist/tools/deep-search/sub-agent.js.map +1 -1
- package/dist/tools/deep-search/tool.js +1 -0
- package/dist/tools/deep-search/tool.js.map +1 -1
- package/dist/tools/deep-search/types.d.ts +2 -0
- package/dist/tools/deep-search/types.js.map +1 -1
- package/dist/tools/dp-tools.js +29 -5
- package/dist/tools/dp-tools.js.map +1 -1
- package/dist/tools/exec-utils.d.ts +85 -0
- package/dist/tools/exec-utils.js +294 -0
- package/dist/tools/exec-utils.js.map +1 -0
- package/dist/tools/fork-skill.js +14 -2
- package/dist/tools/fork-skill.js.map +1 -1
- package/dist/tools/investigation-feedback.d.ts +3 -0
- package/dist/tools/investigation-feedback.js +71 -0
- package/dist/tools/investigation-feedback.js.map +1 -0
- package/dist/tools/manage-schedule.js +16 -6
- package/dist/tools/manage-schedule.js.map +1 -1
- package/dist/tools/netns-script.js +27 -281
- package/dist/tools/netns-script.js.map +1 -1
- package/dist/tools/node-exec.d.ts +2 -14
- package/dist/tools/node-exec.js +18 -225
- package/dist/tools/node-exec.js.map +1 -1
- package/dist/tools/node-script.js +14 -168
- package/dist/tools/node-script.js.map +1 -1
- package/dist/tools/pod-exec.d.ts +1 -1
- package/dist/tools/pod-exec.js +10 -26
- package/dist/tools/pod-exec.js.map +1 -1
- package/dist/tools/pod-nsenter-exec.js +21 -225
- package/dist/tools/pod-nsenter-exec.js.map +1 -1
- package/dist/tools/pod-script.js +10 -19
- package/dist/tools/pod-script.js.map +1 -1
- package/dist/tools/restricted-bash.d.ts +1 -17
- package/dist/tools/restricted-bash.js +38 -252
- package/dist/tools/restricted-bash.js.map +1 -1
- package/dist/tools/run-skill.d.ts +3 -1
- package/dist/tools/run-skill.js +21 -1
- package/dist/tools/run-skill.js.map +1 -1
- package/dist/tools/script-resolver.d.ts +3 -1
- package/dist/tools/script-resolver.js +74 -30
- package/dist/tools/script-resolver.js.map +1 -1
- package/dist/tools/update-skill.js +17 -6
- package/dist/tools/update-skill.js.map +1 -1
- package/package.json +8 -6
- package/siclaw.mjs +10 -1
- package/skills/core/cluster-events/SKILL.md +1 -1
- package/skills/core/deep-investigation/SKILL.md +11 -0
- package/skills/core/deployment-rollout-debug/SKILL.md +1 -1
- package/skills/core/dns-debug/SKILL.md +1 -0
- package/skills/core/meta.json +12 -1
- package/skills/core/networkpolicy-debug/SKILL.md +332 -0
- package/skills/core/node-logs/scripts/get-node-logs.sh +19 -9
- package/skills/core/pod-pending-debug/SKILL.md +1 -0
- package/skills/core/quota-debug/SKILL.md +203 -0
- package/skills/core/service-debug/SKILL.md +1 -0
- package/skills/core/statefulset-debug/SKILL.md +280 -0
- package/skills/core/volcano-diagnose-pod/SKILL.md +196 -0
- package/skills/core/volcano-diagnose-pod/scripts/diagnose-pod.sh +175 -0
- package/skills/core/volcano-gang-scheduling/SKILL.md +299 -0
- package/skills/core/volcano-job-diagnose/SKILL.md +319 -0
- package/skills/core/volcano-job-diagnose/scripts/diagnose-job.sh +253 -0
- package/skills/core/volcano-node-resources/SKILL.md +334 -0
- package/skills/core/volcano-node-resources/scripts/get-node-resources.sh +281 -0
- package/skills/core/volcano-queue-diagnose/SKILL.md +294 -0
- package/skills/core/volcano-queue-diagnose/scripts/diagnose-queue.sh +283 -0
- package/skills/core/volcano-resource-insufficient/SKILL.md +315 -0
- package/skills/core/volcano-scheduler-config/SKILL.md +371 -0
- package/skills/core/volcano-scheduler-config/scripts/get-scheduler-config.sh +297 -0
- package/skills/core/volcano-scheduler-logs/SKILL.md +241 -0
- package/skills/core/volcano-scheduler-logs/scripts/get-scheduler-logs.sh +159 -0
- package/skills/platform/create-skill/SKILL.md +35 -3
- package/skills/platform/manage-skill/SKILL.md +9 -2
- package/skills/platform/update-skill/SKILL.md +17 -6
package/dist/gateway/server.js
CHANGED
|
@@ -13,18 +13,23 @@ import { createDb, closeDb } from "./db/index.js";
|
|
|
13
13
|
import { initSchema } from "./db/init-schema.js";
|
|
14
14
|
import { ConfigRepository } from "./db/repositories/config-repo.js";
|
|
15
15
|
import { NotificationRepository } from "./db/repositories/notification-repo.js";
|
|
16
|
+
import { CronService } from "./cron/cron-service.js";
|
|
17
|
+
import { ChatRepository } from "./db/repositories/chat-repo.js";
|
|
16
18
|
import { PermissionRepository } from "./db/repositories/permission-repo.js";
|
|
17
19
|
import { UserRepository } from "./db/repositories/user-repo.js";
|
|
18
20
|
import { ModelConfigRepository } from "./db/repositories/model-config-repo.js";
|
|
19
21
|
import { SystemConfigRepository } from "./db/repositories/system-config-repo.js";
|
|
20
22
|
import { WorkspaceRepository } from "./db/repositories/workspace-repo.js";
|
|
21
23
|
import { McpServerRepository } from "./db/repositories/mcp-server-repo.js";
|
|
22
|
-
import {
|
|
24
|
+
import { loadConfig } from "../core/config.js";
|
|
23
25
|
import { buildMergedMcpConfig } from "./mcp-config-builder.js";
|
|
24
26
|
import { CertificateManager } from "./security/cert-manager.js";
|
|
25
27
|
import { createMtlsMiddleware } from "./security/mtls-middleware.js";
|
|
26
28
|
import { createResourceNotifier } from "./resource-notifier.js";
|
|
27
29
|
import { LocalSpawner } from "./agentbox/local-spawner.js";
|
|
30
|
+
import { emitDiagnostic } from "../shared/diagnostic-events.js";
|
|
31
|
+
import { checkMetricsAuth } from "../shared/metrics.js"; // also registers metrics subscriber (side-effect)
|
|
32
|
+
import { MetricsAggregator } from "./metrics-aggregator.js";
|
|
28
33
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
34
|
// Static files: web React build
|
|
30
35
|
// Production: dist/gateway/web/dist/ Dev: src/gateway/web/dist/
|
|
@@ -40,7 +45,7 @@ const MIME_TYPES = {
|
|
|
40
45
|
".svg": "image/svg+xml",
|
|
41
46
|
".ico": "image/x-icon",
|
|
42
47
|
};
|
|
43
|
-
function serveStatic(res, urlPath) {
|
|
48
|
+
function serveStatic(res, urlPath, frameSrc) {
|
|
44
49
|
const withoutQuery = urlPath.split("?")[0];
|
|
45
50
|
const safePath = path.normalize(withoutQuery).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
46
51
|
let filePath = path.join(WEB_DIR, safePath === "/" ? "index.html" : safePath);
|
|
@@ -66,7 +71,11 @@ function serveStatic(res, urlPath) {
|
|
|
66
71
|
const ext = path.extname(filePath);
|
|
67
72
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
68
73
|
const content = fs.readFileSync(filePath);
|
|
69
|
-
|
|
74
|
+
const headers = { "Content-Type": contentType };
|
|
75
|
+
if (frameSrc && contentType.startsWith("text/html")) {
|
|
76
|
+
headers["Content-Security-Policy"] = `frame-src 'self' ${frameSrc}`;
|
|
77
|
+
}
|
|
78
|
+
res.writeHead(200, headers);
|
|
70
79
|
res.end(content);
|
|
71
80
|
}
|
|
72
81
|
export async function startGateway(opts) {
|
|
@@ -97,41 +106,22 @@ export async function startGateway(opts) {
|
|
|
97
106
|
const db = await createDb();
|
|
98
107
|
await initSchema(db);
|
|
99
108
|
console.log("[gateway] Database initialized");
|
|
100
|
-
// Config repo for webhook route
|
|
109
|
+
// Config repo for webhook route + cron service
|
|
101
110
|
const configRepo = db ? new ConfigRepository(db) : null;
|
|
102
|
-
|
|
111
|
+
const notifRepo = db ? new NotificationRepository(db) : null;
|
|
112
|
+
// In-process cron service (replaces standalone cron process)
|
|
113
|
+
const cronService = (configRepo && notifRepo)
|
|
114
|
+
? new CronService({ configRepo, notifRepo, sendToUser, gatewayPort: config.port })
|
|
115
|
+
: null;
|
|
116
|
+
// System config repo (used by JWT, SSO, cert-manager, metrics cache, etc.)
|
|
117
|
+
const sysConfigRepo = db ? new SystemConfigRepository(db) : null;
|
|
118
|
+
// Clean orphan model entries on startup
|
|
103
119
|
if (db) {
|
|
104
120
|
const modelConfigRepo = new ModelConfigRepository(db);
|
|
105
121
|
await modelConfigRepo.cleanOrphanModels();
|
|
106
|
-
agentBoxManager.setEnvResolver(async () => {
|
|
107
|
-
const env = {};
|
|
108
|
-
const llm = await modelConfigRepo.getResolvedDefaultConfig();
|
|
109
|
-
if (llm) {
|
|
110
|
-
if (llm.baseUrl)
|
|
111
|
-
env.SICLAW_LLM_BASE_URL = llm.baseUrl;
|
|
112
|
-
if (llm.apiKey)
|
|
113
|
-
env.SICLAW_LLM_API_KEY = resolveApiKey(llm.apiKey);
|
|
114
|
-
if (llm.model)
|
|
115
|
-
env.SICLAW_LLM_MODEL = llm.model;
|
|
116
|
-
}
|
|
117
|
-
const emb = await modelConfigRepo.getResolvedEmbeddingConfig();
|
|
118
|
-
if (emb) {
|
|
119
|
-
if (emb.baseUrl)
|
|
120
|
-
env.SICLAW_EMBEDDING_BASE_URL = emb.baseUrl;
|
|
121
|
-
if (emb.apiKey)
|
|
122
|
-
env.SICLAW_EMBEDDING_API_KEY = resolveApiKey(emb.apiKey);
|
|
123
|
-
if (emb.model)
|
|
124
|
-
env.SICLAW_EMBEDDING_MODEL = emb.model;
|
|
125
|
-
if (emb.dimensions)
|
|
126
|
-
env.SICLAW_EMBEDDING_DIMENSIONS = String(emb.dimensions);
|
|
127
|
-
}
|
|
128
|
-
return env;
|
|
129
|
-
});
|
|
130
122
|
}
|
|
131
123
|
// Workspace repo (used by internal API to resolve default workspace)
|
|
132
124
|
const internalWorkspaceRepo = db ? new WorkspaceRepository(db) : null;
|
|
133
|
-
// System config repo (used by JWT, SSO, cert-manager, etc.)
|
|
134
|
-
const sysConfigRepo = db ? new SystemConfigRepository(db) : null;
|
|
135
125
|
// Initialize Certificate Manager for mTLS (CA persisted in DB)
|
|
136
126
|
const certManager = await CertificateManager.create(sysConfigRepo);
|
|
137
127
|
agentBoxManager.setCertManager(certManager);
|
|
@@ -154,18 +144,80 @@ export async function startGateway(opts) {
|
|
|
154
144
|
? (type, userId) => localSpawner.reloadResource(type, userId)
|
|
155
145
|
: undefined;
|
|
156
146
|
const resourceNotifier = createResourceNotifier(agentBoxManager, agentBoxTlsOptions, localReloader);
|
|
147
|
+
// Create MetricsAggregator (Local mode: proxy LocalCollector; K8s mode: pull loop)
|
|
148
|
+
const isK8sMode = !(spawner instanceof LocalSpawner);
|
|
149
|
+
let metricsAggregator;
|
|
150
|
+
if (isK8sMode) {
|
|
151
|
+
metricsAggregator = new MetricsAggregator("k8s", undefined, agentBoxManager, {
|
|
152
|
+
async fetch(endpoint) {
|
|
153
|
+
try {
|
|
154
|
+
const client = new AgentBoxClient(endpoint, 3000, agentBoxTlsOptions);
|
|
155
|
+
return await client.getJson("/api/internal/metrics-snapshot");
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const { localCollector } = await import("../shared/local-collector.js");
|
|
165
|
+
metricsAggregator = new MetricsAggregator("local", localCollector);
|
|
166
|
+
}
|
|
167
|
+
if (db)
|
|
168
|
+
metricsAggregator.setDb(db);
|
|
169
|
+
// CSP frame-src cache for Grafana iframe embedding
|
|
170
|
+
let cachedFrameSrc = null;
|
|
171
|
+
const refreshCspCache = async () => {
|
|
172
|
+
if (!sysConfigRepo)
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
const url = await sysConfigRepo.get("system.grafanaUrl");
|
|
176
|
+
cachedFrameSrc = url ? new URL(url).origin : null;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
cachedFrameSrc = null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
await refreshCspCache();
|
|
183
|
+
// Metrics config cache — Gateway reads from DB, falls back to env var
|
|
184
|
+
let cachedMetricsToken;
|
|
185
|
+
const refreshMetricsConfig = async () => {
|
|
186
|
+
if (!sysConfigRepo)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
cachedMetricsToken = (await sysConfigRepo.get("metrics.token")) ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
catch { /* keep previous cachedMetricsToken */ }
|
|
192
|
+
try {
|
|
193
|
+
const userIdVal = await sysConfigRepo.get("metrics.includeUserId");
|
|
194
|
+
if (userIdVal !== null) {
|
|
195
|
+
const { setIncludeUserId } = await import("../shared/metrics.js");
|
|
196
|
+
setIncludeUserId(userIdVal !== "false");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* keep previous includeUserId */ }
|
|
200
|
+
};
|
|
201
|
+
await refreshMetricsConfig();
|
|
157
202
|
// Create RPC methods using AgentBoxManager
|
|
158
|
-
const { methods: rpcMethods, buildCredentialPayload, getSkillBundle, cleanupForWs } = createRpcMethods(agentBoxManager, broadcast, db, sendToUser, activePromptUsers, agentBoxTlsOptions, resourceNotifier);
|
|
203
|
+
const { methods: rpcMethods, buildCredentialPayload, getSkillBundle, cleanupForWs } = createRpcMethods(agentBoxManager, broadcast, db, sendToUser, activePromptUsers, agentBoxTlsOptions, resourceNotifier, metricsAggregator, cronService);
|
|
204
|
+
// Wrap system.saveSection to refresh caches when settings change
|
|
205
|
+
const origSaveSection = rpcMethods.get("system.saveSection");
|
|
206
|
+
if (origSaveSection) {
|
|
207
|
+
rpcMethods.set("system.saveSection", async (params, context) => {
|
|
208
|
+
const result = await origSaveSection(params, context);
|
|
209
|
+
const section = params.section;
|
|
210
|
+
if (section === "system")
|
|
211
|
+
await refreshCspCache();
|
|
212
|
+
if (section === "metrics")
|
|
213
|
+
await refreshMetricsConfig();
|
|
214
|
+
return result;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
159
217
|
// Wire skill bundle provider into LocalSpawner (getSkillBundle comes from createRpcMethods)
|
|
160
218
|
if (localSpawner) {
|
|
161
219
|
localSpawner.setSkillBundleProvider(getSkillBundle);
|
|
162
220
|
}
|
|
163
|
-
// Apply DB-stored agentbox image override (takes effect on next pod spawn)
|
|
164
|
-
if (sysConfigRepo) {
|
|
165
|
-
const img = await sysConfigRepo.get("system.agentboxImage");
|
|
166
|
-
if (img)
|
|
167
|
-
agentBoxManager.setSpawnerImage(img);
|
|
168
|
-
}
|
|
169
221
|
// Auth setup — auto-generate JWT secret on first run if not provided
|
|
170
222
|
const jwtSecret = await resolveJwtSecret(sysConfigRepo);
|
|
171
223
|
const userStore = new UserStore(db);
|
|
@@ -327,6 +379,25 @@ export async function startGateway(opts) {
|
|
|
327
379
|
res.end(JSON.stringify({ status: "ok" }));
|
|
328
380
|
return;
|
|
329
381
|
}
|
|
382
|
+
// Prometheus metrics endpoint
|
|
383
|
+
if (url === "/metrics" && method === "GET") {
|
|
384
|
+
if (!checkMetricsAuth(req, res, cachedMetricsToken))
|
|
385
|
+
return;
|
|
386
|
+
(async () => {
|
|
387
|
+
try {
|
|
388
|
+
const { metricsRegistry } = await import("../shared/metrics.js");
|
|
389
|
+
const metricsBody = await metricsRegistry.metrics();
|
|
390
|
+
res.writeHead(200, { "Content-Type": metricsRegistry.contentType });
|
|
391
|
+
res.end(metricsBody);
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
console.error("[gateway] /metrics error:", err);
|
|
395
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
396
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
397
|
+
}
|
|
398
|
+
})();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
330
401
|
// Login API
|
|
331
402
|
if (url === "/api/login") {
|
|
332
403
|
handleLogin(req, res);
|
|
@@ -455,171 +526,6 @@ export async function startGateway(opts) {
|
|
|
455
526
|
}
|
|
456
527
|
// NOTE: /api/internal/cron-list has been moved to HTTPS server (port 3002)
|
|
457
528
|
// with mTLS authentication for AgentBox access only.
|
|
458
|
-
// ─── Internal cron coordination API (used by cron service) ────────
|
|
459
|
-
// These thin wrappers let cron-main talk to the DB through gateway,
|
|
460
|
-
// so cron never needs its own database connection.
|
|
461
|
-
if (url.startsWith("/api/internal/cron/") && configRepo) {
|
|
462
|
-
const cronPath = url.replace("/api/internal/cron/", "").split("?")[0];
|
|
463
|
-
const fullUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
464
|
-
// POST endpoints
|
|
465
|
-
if (method === "POST") {
|
|
466
|
-
let body = "";
|
|
467
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
468
|
-
req.on("end", async () => {
|
|
469
|
-
try {
|
|
470
|
-
const data = body ? JSON.parse(body) : {};
|
|
471
|
-
if (cronPath === "register") {
|
|
472
|
-
await configRepo.registerCronInstance(data.instanceId, data.endpoint);
|
|
473
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
474
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (cronPath === "heartbeat") {
|
|
478
|
-
await configRepo.updateHeartbeat(data.instanceId, data.jobCount);
|
|
479
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
480
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
if (cronPath === "delete-instance") {
|
|
484
|
-
await configRepo.deleteInstance(data.instanceId);
|
|
485
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
486
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (cronPath === "release-jobs") {
|
|
490
|
-
await configRepo.releaseInstanceJobs(data.instanceId);
|
|
491
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
492
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (cronPath === "claim-job") {
|
|
496
|
-
const claimed = await configRepo.claimUnassignedJob(data.jobId, data.instanceId);
|
|
497
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
498
|
-
res.end(JSON.stringify({ claimed }));
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
if (cronPath === "job-run") {
|
|
502
|
-
await configRepo.updateCronJobRun(data.jobId, data.result);
|
|
503
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
504
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (cronPath === "reassign-jobs") {
|
|
508
|
-
await configRepo.reassignOrphanedJobs(data.fromInstanceId, data.toInstanceId);
|
|
509
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
510
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
514
|
-
res.end(JSON.stringify({ error: "Unknown cron POST endpoint" }));
|
|
515
|
-
}
|
|
516
|
-
catch (err) {
|
|
517
|
-
console.error(`[gateway] cron/${cronPath} error:`, err);
|
|
518
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
519
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
// GET endpoints
|
|
525
|
-
if (method === "GET") {
|
|
526
|
-
(async () => {
|
|
527
|
-
try {
|
|
528
|
-
if (cronPath === "jobs") {
|
|
529
|
-
const instanceId = fullUrl.searchParams.get("instanceId");
|
|
530
|
-
const unassigned = fullUrl.searchParams.get("unassigned");
|
|
531
|
-
if (instanceId) {
|
|
532
|
-
const jobs = await configRepo.listCronJobsByInstance(instanceId);
|
|
533
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
534
|
-
res.end(JSON.stringify({ jobs }));
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (unassigned === "1") {
|
|
538
|
-
const jobs = await configRepo.getUnassignedActiveJobs();
|
|
539
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
540
|
-
res.end(JSON.stringify({ jobs }));
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
544
|
-
res.end(JSON.stringify({ error: "instanceId or unassigned=1 required" }));
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
// GET /api/internal/cron/jobs/:id
|
|
548
|
-
if (cronPath.startsWith("jobs/")) {
|
|
549
|
-
const jobId = cronPath.slice("jobs/".length);
|
|
550
|
-
const job = await configRepo.getCronJobById(jobId);
|
|
551
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
552
|
-
res.end(JSON.stringify({ job }));
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (cronPath === "dead-instances") {
|
|
556
|
-
const thresholdMs = parseInt(fullUrl.searchParams.get("thresholdMs") || "90000", 10);
|
|
557
|
-
const instances = await configRepo.getDeadInstances(thresholdMs);
|
|
558
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
559
|
-
res.end(JSON.stringify({ instances }));
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (cronPath === "least-loaded") {
|
|
563
|
-
const thresholdMs = parseInt(fullUrl.searchParams.get("thresholdMs") || "90000", 10);
|
|
564
|
-
const instance = await configRepo.getLeastLoadedInstance(thresholdMs);
|
|
565
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
566
|
-
res.end(JSON.stringify({ instance }));
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
570
|
-
res.end(JSON.stringify({ error: "Unknown cron GET endpoint" }));
|
|
571
|
-
}
|
|
572
|
-
catch (err) {
|
|
573
|
-
console.error(`[gateway] cron/${cronPath} error:`, err);
|
|
574
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
575
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
576
|
-
}
|
|
577
|
-
})();
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Internal cron notification endpoint: POST /api/internal/cron-notify
|
|
582
|
-
if (url === "/api/internal/cron-notify" && method === "POST") {
|
|
583
|
-
let body = "";
|
|
584
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
585
|
-
req.on("end", async () => {
|
|
586
|
-
try {
|
|
587
|
-
const data = JSON.parse(body);
|
|
588
|
-
// 1. Write notification to DB
|
|
589
|
-
if (db) {
|
|
590
|
-
const notifRepo = new NotificationRepository(db);
|
|
591
|
-
const notifId = await notifRepo.create({
|
|
592
|
-
userId: data.userId,
|
|
593
|
-
type: "cron_result",
|
|
594
|
-
title: data.jobName,
|
|
595
|
-
message: data.result === "success" ? data.resultText : (data.error || "Unknown error"),
|
|
596
|
-
relatedId: data.jobId,
|
|
597
|
-
});
|
|
598
|
-
// 2. Push via WebSocket
|
|
599
|
-
sendToUser(data.userId, "notification", {
|
|
600
|
-
id: notifId,
|
|
601
|
-
type: "cron_result",
|
|
602
|
-
title: data.jobName,
|
|
603
|
-
message: data.result === "success" ? data.resultText : (data.error || "Unknown error"),
|
|
604
|
-
result: data.result,
|
|
605
|
-
relatedId: data.jobId,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
// 3. Delegate to channel push via callback
|
|
609
|
-
if (gatewayServer.onCronNotify) {
|
|
610
|
-
gatewayServer.onCronNotify(data);
|
|
611
|
-
}
|
|
612
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
613
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
614
|
-
}
|
|
615
|
-
catch (err) {
|
|
616
|
-
console.error("[gateway] cron-notify error:", err);
|
|
617
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
618
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
529
|
// Internal agent-prompt endpoint: POST /api/internal/agent-prompt
|
|
624
530
|
// Synchronous execution — waits for agent to finish and returns result text.
|
|
625
531
|
// Used by cron, triggers, and other internal callers.
|
|
@@ -629,6 +535,8 @@ export async function startGateway(opts) {
|
|
|
629
535
|
req.on("end", async () => {
|
|
630
536
|
const startTime = Date.now();
|
|
631
537
|
let userId = "";
|
|
538
|
+
let client = null;
|
|
539
|
+
let sessionId;
|
|
632
540
|
try {
|
|
633
541
|
const data = JSON.parse(body);
|
|
634
542
|
userId = data.userId;
|
|
@@ -641,22 +549,76 @@ export async function startGateway(opts) {
|
|
|
641
549
|
}
|
|
642
550
|
console.log(`[gateway] agent-prompt from=${caller} user=${userId} session=${data.sessionId}`);
|
|
643
551
|
// 1. Get or create user's AgentBox (resolve real workspace ID from DB)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
552
|
+
if (!data.workspaceId && !internalWorkspaceRepo)
|
|
553
|
+
throw new Error("Database not available");
|
|
554
|
+
const workspace = data.workspaceId
|
|
555
|
+
? await internalWorkspaceRepo?.getById(data.workspaceId) ?? null
|
|
556
|
+
: await internalWorkspaceRepo.getOrCreateDefault(userId);
|
|
557
|
+
const wsId = workspace?.id || data.workspaceId;
|
|
558
|
+
const isDefaultWs = workspace?.isDefault ?? true;
|
|
647
559
|
const handle = await agentBoxManager.getOrCreate(userId, wsId);
|
|
648
|
-
|
|
649
|
-
// 2.
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
560
|
+
client = new AgentBoxClient(handle.endpoint, 30000, agentBoxTlsOptions);
|
|
561
|
+
// 2. Build credential payload so AgentBox has kubeconfig etc.
|
|
562
|
+
const credentials = await buildCredentialPayload(userId, wsId, isDefaultWs).catch((err) => {
|
|
563
|
+
console.warn(`[gateway] agent-prompt credential build failed:`, err instanceof Error ? err.message : err);
|
|
564
|
+
return undefined;
|
|
565
|
+
});
|
|
566
|
+
// 3. Resolve model config (workspace default → global default)
|
|
567
|
+
let modelProvider;
|
|
568
|
+
let modelId;
|
|
569
|
+
let modelConfig;
|
|
570
|
+
if (db) {
|
|
571
|
+
const mcRepo = new ModelConfigRepository(db);
|
|
572
|
+
// Try workspace default model first, then global default
|
|
573
|
+
const wsDefault = workspace?.configJson?.defaultModel;
|
|
574
|
+
const defaultModel = wsDefault?.provider && wsDefault?.modelId
|
|
575
|
+
? { provider: wsDefault.provider, modelId: wsDefault.modelId }
|
|
576
|
+
: await mcRepo.getDefault();
|
|
577
|
+
if (defaultModel) {
|
|
578
|
+
modelProvider = defaultModel.provider;
|
|
579
|
+
modelId = defaultModel.modelId;
|
|
580
|
+
try {
|
|
581
|
+
const providerConfig = await mcRepo.getProviderWithModels(modelProvider);
|
|
582
|
+
if (providerConfig)
|
|
583
|
+
modelConfig = providerConfig;
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
console.warn(`[gateway] agent-prompt provider config resolve failed:`, err instanceof Error ? err.message : err);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// 4. Send prompt with credentials and model config
|
|
591
|
+
const promptResult = await client.prompt({ sessionId: data.sessionId, text: data.text, credentials, modelProvider, modelId, modelConfig });
|
|
592
|
+
sessionId = promptResult.sessionId;
|
|
593
|
+
// 4. Wait for completion with cancellable timeout
|
|
594
|
+
const timeout = rejectAfterTimeout(timeoutMs, data.sessionId);
|
|
595
|
+
try {
|
|
596
|
+
const resultText = await Promise.race([
|
|
597
|
+
waitForAgentCompletion(client, promptResult.sessionId),
|
|
598
|
+
timeout.promise,
|
|
599
|
+
]);
|
|
600
|
+
timeout.cancel();
|
|
601
|
+
const durationMs = Date.now() - startTime;
|
|
602
|
+
console.log(`[gateway] agent-prompt completed user=${userId} duration=${durationMs}ms resultLen=${resultText.length}`);
|
|
603
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
604
|
+
res.end(JSON.stringify({ status: "success", resultText, durationMs }));
|
|
605
|
+
}
|
|
606
|
+
catch (innerErr) {
|
|
607
|
+
timeout.cancel();
|
|
608
|
+
// On timeout, attempt to abort + close the orphaned agent session
|
|
609
|
+
if (client && sessionId && innerErr instanceof ExecutionTimeoutError) {
|
|
610
|
+
try {
|
|
611
|
+
await client.abortSession(sessionId);
|
|
612
|
+
}
|
|
613
|
+
catch { /* best-effort */ }
|
|
614
|
+
try {
|
|
615
|
+
await client.closeSession(sessionId);
|
|
616
|
+
}
|
|
617
|
+
catch { /* best-effort */ }
|
|
618
|
+
console.log(`[gateway] agent-prompt session=${sessionId} aborted after timeout`);
|
|
619
|
+
}
|
|
620
|
+
throw innerErr;
|
|
621
|
+
}
|
|
660
622
|
}
|
|
661
623
|
catch (err) {
|
|
662
624
|
const durationMs = Date.now() - startTime;
|
|
@@ -720,6 +682,42 @@ export async function startGateway(opts) {
|
|
|
720
682
|
});
|
|
721
683
|
return;
|
|
722
684
|
}
|
|
685
|
+
// Internal session/stats purge endpoint: POST /api/internal/sessions/purge
|
|
686
|
+
if (url === "/api/internal/sessions/purge" && method === "POST") {
|
|
687
|
+
if (!db) {
|
|
688
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
689
|
+
res.end(JSON.stringify({ error: "Database not available" }));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
let body = "";
|
|
693
|
+
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
694
|
+
req.on("end", async () => {
|
|
695
|
+
try {
|
|
696
|
+
const { softDeleteInactiveDays = 180, statsRetentionDays = 90, hardDeleteAfterDays = 30, } = body ? JSON.parse(body) : {};
|
|
697
|
+
if (softDeleteInactiveDays < 1 || statsRetentionDays < 1 || hardDeleteAfterDays < 1) {
|
|
698
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
699
|
+
res.end(JSON.stringify({ error: "Retention days must be >= 1" }));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const chatRepo = new ChatRepository(db);
|
|
703
|
+
// Step 1: soft-delete inactive sessions
|
|
704
|
+
const softDeleted = await chatRepo.softDeleteInactiveSessions(softDeleteInactiveDays);
|
|
705
|
+
// Step 2: hard-delete old session_stats
|
|
706
|
+
const statsPurged = await chatRepo.purgeOldSessionStats(statsRetentionDays);
|
|
707
|
+
// Step 3: hard-delete soft-deleted sessions (messages cascade)
|
|
708
|
+
const sessionsPurged = await chatRepo.purgeDeletedSessions(hardDeleteAfterDays);
|
|
709
|
+
console.log(`[gateway] Session purge: softDeleted=${softDeleted}, statsPurged=${statsPurged}, sessionsPurged=${sessionsPurged}`);
|
|
710
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
711
|
+
res.end(JSON.stringify({ status: "ok", softDeleted, statsPurged, sessionsPurged }));
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
console.error("[gateway] sessions/purge error:", err);
|
|
715
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
716
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
723
721
|
// Webhook endpoint: POST /hooks/v1/:triggerId
|
|
724
722
|
if (url.startsWith("/hooks/v1/") && method === "POST") {
|
|
725
723
|
const triggerId = url.split("/hooks/v1/")[1]?.split("?")[0];
|
|
@@ -788,7 +786,7 @@ export async function startGateway(opts) {
|
|
|
788
786
|
}
|
|
789
787
|
}
|
|
790
788
|
// Serve static web UI
|
|
791
|
-
serveStatic(res, url);
|
|
789
|
+
serveStatic(res, url, cachedFrameSrc);
|
|
792
790
|
});
|
|
793
791
|
// WebSocket server
|
|
794
792
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -827,6 +825,7 @@ export async function startGateway(opts) {
|
|
|
827
825
|
wss.on("connection", (ws, auth) => {
|
|
828
826
|
clients.add(ws);
|
|
829
827
|
aliveClients.add(ws);
|
|
828
|
+
emitDiagnostic({ type: "ws_connected" });
|
|
830
829
|
ws.on("pong", () => aliveClients.add(ws));
|
|
831
830
|
const authWs = ws;
|
|
832
831
|
const authInfo = auth ? `user=${auth.username}` : "anonymous";
|
|
@@ -882,10 +881,13 @@ export async function startGateway(opts) {
|
|
|
882
881
|
}
|
|
883
882
|
}
|
|
884
883
|
}
|
|
884
|
+
emitDiagnostic({ type: "ws_disconnected" });
|
|
885
885
|
console.log(`[gateway] WS client disconnected (total: ${clients.size})`);
|
|
886
886
|
});
|
|
887
887
|
ws.on("error", (err) => {
|
|
888
888
|
console.error("[gateway] WS error:", err.message);
|
|
889
|
+
// Note: do NOT emit ws_disconnected here — the "close" event always
|
|
890
|
+
// fires after "error" and handles the decrement + cleanup.
|
|
889
891
|
clients.delete(ws);
|
|
890
892
|
});
|
|
891
893
|
});
|
|
@@ -897,6 +899,9 @@ export async function startGateway(opts) {
|
|
|
897
899
|
console.log(`[gateway] Listening on http://${config.host}:${config.port}`);
|
|
898
900
|
console.log(`[gateway] Web UI: http://${config.host}:${config.port}/`);
|
|
899
901
|
console.log(`[gateway] WebSocket: ws://${config.host}:${config.port}/ws`);
|
|
902
|
+
if (!cachedMetricsToken && !process.env.SICLAW_METRICS_TOKEN) {
|
|
903
|
+
console.warn(`[gateway] WARNING: metrics token is not configured — /metrics endpoint is unauthenticated`);
|
|
904
|
+
}
|
|
900
905
|
});
|
|
901
906
|
// HTTPS server for internal mTLS API (AgentBox connections)
|
|
902
907
|
const internalPort = config.internalPort || 3002;
|
|
@@ -927,6 +932,23 @@ export async function startGateway(opts) {
|
|
|
927
932
|
try {
|
|
928
933
|
const modelConfigRepo = new ModelConfigRepository(db);
|
|
929
934
|
const settings = await modelConfigRepo.exportSettingsConfig();
|
|
935
|
+
// Append debugImage if configured via env
|
|
936
|
+
if (process.env.SICLAW_DEBUG_IMAGE) {
|
|
937
|
+
settings.debugImage = process.env.SICLAW_DEBUG_IMAGE;
|
|
938
|
+
}
|
|
939
|
+
// Append metrics config from system_config table
|
|
940
|
+
if (sysConfigRepo) {
|
|
941
|
+
const metricsPort = await sysConfigRepo.get("metrics.port");
|
|
942
|
+
const metricsToken = await sysConfigRepo.get("metrics.token");
|
|
943
|
+
const includeUserId = await sysConfigRepo.get("metrics.includeUserId");
|
|
944
|
+
if (metricsPort || metricsToken || includeUserId) {
|
|
945
|
+
settings.metrics = {
|
|
946
|
+
...(metricsPort ? { port: parseInt(metricsPort, 10) } : {}),
|
|
947
|
+
...(metricsToken ? { token: metricsToken } : {}),
|
|
948
|
+
...(includeUserId ? { includeUserId: includeUserId === "true" } : {}),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
930
952
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
931
953
|
res.end(JSON.stringify(settings));
|
|
932
954
|
}
|
|
@@ -954,27 +976,16 @@ export async function startGateway(opts) {
|
|
|
954
976
|
res.end(JSON.stringify({ error: "Client certificate required" }));
|
|
955
977
|
return;
|
|
956
978
|
}
|
|
957
|
-
//
|
|
979
|
+
// Use userId from mTLS certificate identity (authoritative)
|
|
958
980
|
const urlObj = new URL(url, `https://${req.headers.host}`);
|
|
959
|
-
const
|
|
960
|
-
if (!requestedUserId) {
|
|
961
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
962
|
-
res.end(JSON.stringify({ error: "Missing userId parameter" }));
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
// Authorization: certificate userId must match requested userId
|
|
966
|
-
if (identity.userId !== requestedUserId) {
|
|
967
|
-
console.warn(`[gateway] cron-list authorization failed: cert userId=${identity.userId} requested=${requestedUserId}`);
|
|
968
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
969
|
-
res.end(JSON.stringify({ error: "Forbidden: userId mismatch" }));
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
981
|
+
const userId = identity.userId;
|
|
972
982
|
// Query cron jobs using ConfigRepository
|
|
973
983
|
const configRepo = new ConfigRepository(db);
|
|
974
|
-
const
|
|
984
|
+
const workspaceId = urlObj.searchParams.get("workspaceId") || identity.workspaceId;
|
|
985
|
+
const jobs = await configRepo.listCronJobs(userId, workspaceId ? { workspaceId } : undefined);
|
|
975
986
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
976
987
|
res.end(JSON.stringify({ jobs }));
|
|
977
|
-
console.log(`[gateway] Listed ${jobs.length} cron jobs for userId=${
|
|
988
|
+
console.log(`[gateway] Listed ${jobs.length} cron jobs for userId=${userId}${workspaceId ? ` workspaceId=${workspaceId}` : ""}`);
|
|
978
989
|
}
|
|
979
990
|
catch (err) {
|
|
980
991
|
console.error("[gateway] cron-list error:", err);
|
|
@@ -1013,7 +1024,10 @@ export async function startGateway(opts) {
|
|
|
1013
1024
|
if (url === "/api/internal/mcp-servers" && method === "GET") {
|
|
1014
1025
|
(async () => {
|
|
1015
1026
|
try {
|
|
1016
|
-
const
|
|
1027
|
+
const config = loadConfig();
|
|
1028
|
+
const localConfig = Object.keys(config.mcpServers).length > 0
|
|
1029
|
+
? { mcpServers: config.mcpServers }
|
|
1030
|
+
: null;
|
|
1017
1031
|
const mcpRepo = db ? new McpServerRepository(db) : null;
|
|
1018
1032
|
const merged = await buildMergedMcpConfig(localConfig, mcpRepo);
|
|
1019
1033
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1051,8 +1065,11 @@ export async function startGateway(opts) {
|
|
|
1051
1065
|
bindCodeStore,
|
|
1052
1066
|
db,
|
|
1053
1067
|
rpcMethods,
|
|
1068
|
+
cronService,
|
|
1054
1069
|
buildCredentialPayload,
|
|
1070
|
+
agentBoxTlsOptions,
|
|
1055
1071
|
async close() {
|
|
1072
|
+
cronService?.stop();
|
|
1056
1073
|
bindCodeStore.dispose();
|
|
1057
1074
|
await agentBoxManager.cleanup();
|
|
1058
1075
|
for (const ws of clients) {
|
|
@@ -1072,33 +1089,70 @@ export async function startGateway(opts) {
|
|
|
1072
1089
|
/** Consume SSE stream from AgentBox and extract final assistant text */
|
|
1073
1090
|
async function waitForAgentCompletion(client, sessionId) {
|
|
1074
1091
|
let resultText = "";
|
|
1092
|
+
// Accumulate text deltas per message (claude-sdk brain emits text via
|
|
1093
|
+
// message_update/text_delta and sends empty content in message_end)
|
|
1094
|
+
let currentMsgText = "";
|
|
1075
1095
|
for await (const event of client.streamEvents(sessionId)) {
|
|
1076
1096
|
const evt = event;
|
|
1097
|
+
// Accumulate streaming text deltas
|
|
1098
|
+
if (evt.type === "message_update") {
|
|
1099
|
+
const ame = evt.assistantMessageEvent;
|
|
1100
|
+
if (ame?.type === "text_delta" && typeof ame.delta === "string") {
|
|
1101
|
+
currentMsgText += ame.delta;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (evt.type === "message_start") {
|
|
1105
|
+
// New message — reset accumulated text
|
|
1106
|
+
currentMsgText = "";
|
|
1107
|
+
}
|
|
1077
1108
|
if (evt.type === "message_end" || evt.type === "turn_end") {
|
|
1078
1109
|
const message = evt.message;
|
|
1079
1110
|
if (message?.role === "assistant") {
|
|
1111
|
+
// Try to extract from message.content first (pi-agent brain)
|
|
1112
|
+
let extracted = "";
|
|
1080
1113
|
const content = message.content;
|
|
1081
|
-
if (typeof content === "string") {
|
|
1082
|
-
|
|
1114
|
+
if (typeof content === "string" && content) {
|
|
1115
|
+
extracted = content;
|
|
1083
1116
|
}
|
|
1084
1117
|
else if (Array.isArray(content)) {
|
|
1085
|
-
|
|
1118
|
+
extracted = content
|
|
1086
1119
|
.filter((c) => c.type === "text")
|
|
1087
1120
|
.map((c) => c.text ?? "")
|
|
1088
1121
|
.join("");
|
|
1089
|
-
if (text)
|
|
1090
|
-
resultText = text;
|
|
1091
1122
|
}
|
|
1123
|
+
// Use extracted content, or fall back to accumulated text deltas
|
|
1124
|
+
// (claude-sdk brain sends empty content in message_end)
|
|
1125
|
+
resultText = extracted || currentMsgText || resultText;
|
|
1092
1126
|
}
|
|
1127
|
+
currentMsgText = "";
|
|
1093
1128
|
}
|
|
1094
1129
|
if (evt.type === "agent_end")
|
|
1095
1130
|
break;
|
|
1096
1131
|
}
|
|
1132
|
+
// Final fallback: if no message_end was captured but we have accumulated text
|
|
1133
|
+
if (!resultText && currentMsgText) {
|
|
1134
|
+
resultText = currentMsgText;
|
|
1135
|
+
}
|
|
1097
1136
|
return resultText;
|
|
1098
1137
|
}
|
|
1099
|
-
|
|
1138
|
+
class ExecutionTimeoutError extends Error {
|
|
1139
|
+
constructor(sessionId, ms) {
|
|
1140
|
+
super(`agent-prompt session=${sessionId} timed out after ${ms / 1000}s`);
|
|
1141
|
+
this.name = "ExecutionTimeoutError";
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/** Returns a cancellable promise that rejects after the given timeout */
|
|
1100
1145
|
function rejectAfterTimeout(ms, sessionId) {
|
|
1101
|
-
|
|
1146
|
+
let timer;
|
|
1147
|
+
const promise = new Promise((_, reject) => {
|
|
1148
|
+
timer = setTimeout(() => reject(new ExecutionTimeoutError(sessionId, ms)), ms);
|
|
1149
|
+
timer.unref();
|
|
1150
|
+
});
|
|
1151
|
+
return {
|
|
1152
|
+
promise,
|
|
1153
|
+
cancel: () => { if (timer)
|
|
1154
|
+
clearTimeout(timer); },
|
|
1155
|
+
};
|
|
1102
1156
|
}
|
|
1103
1157
|
/**
|
|
1104
1158
|
* Resolve JWT secret: env var > DB > generate new and persist to DB.
|
|
@@ -1125,22 +1179,4 @@ async function resolveJwtSecret(sysConfigRepo) {
|
|
|
1125
1179
|
}
|
|
1126
1180
|
return generated;
|
|
1127
1181
|
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Resolve an API key value — if it looks like an env var name (no slashes,
|
|
1130
|
-
* no dots, all uppercase/underscores), resolve it from process.env.
|
|
1131
|
-
*/
|
|
1132
|
-
function resolveApiKey(value) {
|
|
1133
|
-
// Legacy: bare ALL_CAPS_NAME treated as env var name
|
|
1134
|
-
if (/^[A-Z_][A-Z0-9_]*$/.test(value)) {
|
|
1135
|
-
return process.env[value] ?? value;
|
|
1136
|
-
}
|
|
1137
|
-
// $VAR / ${VAR} syntax — inline env-var references
|
|
1138
|
-
if (/\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*/.test(value)) {
|
|
1139
|
-
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) => {
|
|
1140
|
-
const name = braced || bare;
|
|
1141
|
-
return process.env[name] ?? "";
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
return value;
|
|
1145
|
-
}
|
|
1146
1182
|
//# sourceMappingURL=server.js.map
|