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.
Files changed (270) hide show
  1. package/README.md +75 -114
  2. package/dist/agentbox/gateway-client.d.ts +2 -1
  3. package/dist/agentbox/gateway-client.js +6 -2
  4. package/dist/agentbox/gateway-client.js.map +1 -1
  5. package/dist/agentbox/http-server.js +184 -19
  6. package/dist/agentbox/http-server.js.map +1 -1
  7. package/dist/agentbox/resource-handlers.d.ts +1 -0
  8. package/dist/agentbox/resource-handlers.js +23 -23
  9. package/dist/agentbox/resource-handlers.js.map +1 -1
  10. package/dist/agentbox/session.js +85 -5
  11. package/dist/agentbox/session.js.map +1 -1
  12. package/dist/agentbox-main.d.ts +2 -1
  13. package/dist/agentbox-main.js +65 -18
  14. package/dist/agentbox-main.js.map +1 -1
  15. package/dist/cli-credentials.d.ts +1 -0
  16. package/dist/cli-credentials.js +109 -0
  17. package/dist/cli-credentials.js.map +1 -0
  18. package/dist/cli-first-run.d.ts +11 -0
  19. package/dist/cli-first-run.js +99 -0
  20. package/dist/cli-first-run.js.map +1 -0
  21. package/dist/cli-main.js +33 -11
  22. package/dist/cli-main.js.map +1 -1
  23. package/dist/cli-setup.d.ts +5 -11
  24. package/dist/cli-setup.js +12 -225
  25. package/dist/cli-setup.js.map +1 -1
  26. package/dist/core/agent-factory.d.ts +4 -0
  27. package/dist/core/agent-factory.js +102 -151
  28. package/dist/core/agent-factory.js.map +1 -1
  29. package/dist/core/config.d.ts +10 -3
  30. package/dist/core/config.js +11 -95
  31. package/dist/core/config.js.map +1 -1
  32. package/dist/core/extensions/deep-investigation.d.ts +2 -1
  33. package/dist/core/extensions/deep-investigation.js +144 -24
  34. package/dist/core/extensions/deep-investigation.js.map +1 -1
  35. package/dist/core/extensions/setup.d.ts +8 -0
  36. package/dist/core/extensions/setup.js +669 -0
  37. package/dist/core/extensions/setup.js.map +1 -0
  38. package/dist/core/llm-proxy.js +7 -3
  39. package/dist/core/llm-proxy.js.map +1 -1
  40. package/dist/core/mcp-client.d.ts +0 -10
  41. package/dist/core/mcp-client.js +0 -65
  42. package/dist/core/mcp-client.js.map +1 -1
  43. package/dist/core/prompt.d.ts +1 -1
  44. package/dist/core/prompt.js +42 -5
  45. package/dist/core/prompt.js.map +1 -1
  46. package/dist/core/provider-presets.d.ts +14 -0
  47. package/dist/core/provider-presets.js +81 -0
  48. package/dist/core/provider-presets.js.map +1 -0
  49. package/dist/cron/cron-coordinator.d.ts +2 -0
  50. package/dist/cron/cron-coordinator.js +46 -14
  51. package/dist/cron/cron-coordinator.js.map +1 -1
  52. package/dist/cron/cron-executor.js +33 -8
  53. package/dist/cron/cron-executor.js.map +1 -1
  54. package/dist/cron/cron-scheduler.d.ts +1 -1
  55. package/dist/cron/gateway-client.d.ts +5 -0
  56. package/dist/cron/gateway-client.js +43 -8
  57. package/dist/cron/gateway-client.js.map +1 -1
  58. package/dist/cron-main.js +39 -9
  59. package/dist/cron-main.js.map +1 -1
  60. package/dist/gateway/agentbox/client.d.ts +11 -0
  61. package/dist/gateway/agentbox/client.js +18 -0
  62. package/dist/gateway/agentbox/client.js.map +1 -1
  63. package/dist/gateway/agentbox/k8s-spawner.d.ts +11 -2
  64. package/dist/gateway/agentbox/k8s-spawner.js +95 -52
  65. package/dist/gateway/agentbox/k8s-spawner.js.map +1 -1
  66. package/dist/gateway/agentbox/local-spawner.d.ts +1 -1
  67. package/dist/gateway/agentbox/local-spawner.js +4 -2
  68. package/dist/gateway/agentbox/local-spawner.js.map +1 -1
  69. package/dist/gateway/agentbox/manager.d.ts +0 -10
  70. package/dist/gateway/agentbox/manager.js +11 -30
  71. package/dist/gateway/agentbox/manager.js.map +1 -1
  72. package/dist/gateway/agentbox/types.d.ts +6 -4
  73. package/dist/gateway/cron/cron-service.d.ts +49 -0
  74. package/dist/gateway/cron/cron-service.js +259 -0
  75. package/dist/gateway/cron/cron-service.js.map +1 -0
  76. package/dist/gateway/db/init-schema.js +44 -0
  77. package/dist/gateway/db/init-schema.js.map +1 -1
  78. package/dist/gateway/db/migrate-sqlite.js +73 -4
  79. package/dist/gateway/db/migrate-sqlite.js.map +1 -1
  80. package/dist/gateway/db/repositories/chat-repo.d.ts +56 -2
  81. package/dist/gateway/db/repositories/chat-repo.js +132 -2
  82. package/dist/gateway/db/repositories/chat-repo.js.map +1 -1
  83. package/dist/gateway/db/repositories/config-repo.d.ts +31 -2
  84. package/dist/gateway/db/repositories/config-repo.js +57 -7
  85. package/dist/gateway/db/repositories/config-repo.js.map +1 -1
  86. package/dist/gateway/db/repositories/env-repo.d.ts +14 -0
  87. package/dist/gateway/db/repositories/env-repo.js +15 -2
  88. package/dist/gateway/db/repositories/env-repo.js.map +1 -1
  89. package/dist/gateway/db/repositories/model-config-repo.d.ts +1 -1
  90. package/dist/gateway/db/repositories/model-config-repo.js +26 -12
  91. package/dist/gateway/db/repositories/model-config-repo.js.map +1 -1
  92. package/dist/gateway/db/repositories/skill-repo.d.ts +0 -5
  93. package/dist/gateway/db/repositories/skill-review-repo.d.ts +1 -0
  94. package/dist/gateway/db/repositories/skill-review-repo.js +4 -1
  95. package/dist/gateway/db/repositories/skill-review-repo.js.map +1 -1
  96. package/dist/gateway/db/repositories/skill-version-repo.js +0 -1
  97. package/dist/gateway/db/repositories/skill-version-repo.js.map +1 -1
  98. package/dist/gateway/db/repositories/system-config-repo.d.ts +1 -1
  99. package/dist/gateway/db/repositories/system-config-repo.js +2 -1
  100. package/dist/gateway/db/repositories/system-config-repo.js.map +1 -1
  101. package/dist/gateway/db/repositories/user-env-config-repo.d.ts +13 -0
  102. package/dist/gateway/db/repositories/user-env-config-repo.js +11 -0
  103. package/dist/gateway/db/repositories/user-env-config-repo.js.map +1 -1
  104. package/dist/gateway/db/repositories/workspace-repo.d.ts +3 -2
  105. package/dist/gateway/db/repositories/workspace-repo.js +6 -2
  106. package/dist/gateway/db/repositories/workspace-repo.js.map +1 -1
  107. package/dist/gateway/db/schema-mysql.d.ts +473 -51
  108. package/dist/gateway/db/schema-mysql.js +35 -4
  109. package/dist/gateway/db/schema-mysql.js.map +1 -1
  110. package/dist/gateway/db/schema-sqlite.d.ts +522 -57
  111. package/dist/gateway/db/schema-sqlite.js +38 -6
  112. package/dist/gateway/db/schema-sqlite.js.map +1 -1
  113. package/dist/gateway/db/schema.d.ts +471 -51
  114. package/dist/gateway/db/schema.js +1 -1
  115. package/dist/gateway/db/schema.js.map +1 -1
  116. package/dist/gateway/metrics-aggregator.d.ts +65 -0
  117. package/dist/gateway/metrics-aggregator.js +244 -0
  118. package/dist/gateway/metrics-aggregator.js.map +1 -0
  119. package/dist/gateway/plugins/channel-bridge.d.ts +4 -1
  120. package/dist/gateway/plugins/channel-bridge.js +78 -86
  121. package/dist/gateway/plugins/channel-bridge.js.map +1 -1
  122. package/dist/gateway/rpc-methods.d.ts +4 -2
  123. package/dist/gateway/rpc-methods.js +962 -163
  124. package/dist/gateway/rpc-methods.js.map +1 -1
  125. package/dist/gateway/security/cert-manager.d.ts +2 -2
  126. package/dist/gateway/security/cert-manager.js +4 -2
  127. package/dist/gateway/security/cert-manager.js.map +1 -1
  128. package/dist/gateway/server.d.ts +4 -8
  129. package/dist/gateway/server.js +297 -261
  130. package/dist/gateway/server.js.map +1 -1
  131. package/dist/gateway/skills/file-writer.js +17 -11
  132. package/dist/gateway/skills/file-writer.js.map +1 -1
  133. package/dist/gateway/skills/script-evaluator.js +12 -9
  134. package/dist/gateway/skills/script-evaluator.js.map +1 -1
  135. package/dist/gateway/web/dist/assets/index-0p17ZeTP.js +740 -0
  136. package/dist/gateway/web/dist/assets/index-9eP6nPUq.js +741 -0
  137. package/dist/gateway/web/dist/assets/index-9eP6nPUq.js.map +1 -0
  138. package/dist/gateway/web/dist/assets/index-CAmSY91d.js +675 -0
  139. package/dist/gateway/web/dist/assets/index-DMFEh8Pp.css +1 -0
  140. package/dist/gateway/web/dist/assets/index-DyowBCEj.css +1 -0
  141. package/dist/gateway/web/dist/assets/index-PDK5JJDO.css +1 -0
  142. package/dist/gateway/web/dist/index.html +2 -2
  143. package/dist/gateway-main.js +27 -10
  144. package/dist/gateway-main.js.map +1 -1
  145. package/dist/memory/embeddings.js +5 -4
  146. package/dist/memory/embeddings.js.map +1 -1
  147. package/dist/memory/indexer.d.ts +23 -3
  148. package/dist/memory/indexer.js +235 -23
  149. package/dist/memory/indexer.js.map +1 -1
  150. package/dist/memory/schema.js +15 -1
  151. package/dist/memory/schema.js.map +1 -1
  152. package/dist/memory/types.d.ts +18 -0
  153. package/dist/memory/types.js +6 -1
  154. package/dist/memory/types.js.map +1 -1
  155. package/dist/shared/detect-language.d.ts +12 -0
  156. package/dist/shared/detect-language.js +78 -0
  157. package/dist/shared/detect-language.js.map +1 -0
  158. package/dist/shared/diagnostic-events.d.ts +70 -0
  159. package/dist/shared/diagnostic-events.js +38 -0
  160. package/dist/shared/diagnostic-events.js.map +1 -0
  161. package/dist/shared/local-collector.d.ts +56 -0
  162. package/dist/shared/local-collector.js +284 -0
  163. package/dist/shared/local-collector.js.map +1 -0
  164. package/dist/shared/metrics-types.d.ts +64 -0
  165. package/dist/shared/metrics-types.js +25 -0
  166. package/dist/shared/metrics-types.js.map +1 -0
  167. package/dist/shared/metrics.d.ts +19 -0
  168. package/dist/shared/metrics.js +185 -0
  169. package/dist/shared/metrics.js.map +1 -0
  170. package/dist/shared/path-utils.d.ts +15 -0
  171. package/dist/shared/path-utils.js +23 -0
  172. package/dist/shared/path-utils.js.map +1 -0
  173. package/dist/shared/retry.d.ts +35 -0
  174. package/dist/shared/retry.js +61 -0
  175. package/dist/shared/retry.js.map +1 -0
  176. package/dist/tools/command-sets.d.ts +18 -2
  177. package/dist/tools/command-sets.js +207 -32
  178. package/dist/tools/command-sets.js.map +1 -1
  179. package/dist/tools/command-validator.d.ts +56 -0
  180. package/dist/tools/command-validator.js +357 -0
  181. package/dist/tools/command-validator.js.map +1 -0
  182. package/dist/tools/create-skill.js +26 -1
  183. package/dist/tools/create-skill.js.map +1 -1
  184. package/dist/tools/credential-list.js +1 -23
  185. package/dist/tools/credential-list.js.map +1 -1
  186. package/dist/tools/credential-manager.d.ts +98 -0
  187. package/dist/tools/credential-manager.js +313 -0
  188. package/dist/tools/credential-manager.js.map +1 -0
  189. package/dist/tools/deep-search/engine.js +184 -127
  190. package/dist/tools/deep-search/engine.js.map +1 -1
  191. package/dist/tools/deep-search/prompts.d.ts +10 -2
  192. package/dist/tools/deep-search/prompts.js +37 -36
  193. package/dist/tools/deep-search/prompts.js.map +1 -1
  194. package/dist/tools/deep-search/schemas.d.ts +87 -0
  195. package/dist/tools/deep-search/schemas.js +85 -0
  196. package/dist/tools/deep-search/schemas.js.map +1 -0
  197. package/dist/tools/deep-search/sub-agent.d.ts +21 -0
  198. package/dist/tools/deep-search/sub-agent.js +153 -4
  199. package/dist/tools/deep-search/sub-agent.js.map +1 -1
  200. package/dist/tools/deep-search/tool.js +1 -0
  201. package/dist/tools/deep-search/tool.js.map +1 -1
  202. package/dist/tools/deep-search/types.d.ts +2 -0
  203. package/dist/tools/deep-search/types.js.map +1 -1
  204. package/dist/tools/dp-tools.js +29 -5
  205. package/dist/tools/dp-tools.js.map +1 -1
  206. package/dist/tools/exec-utils.d.ts +85 -0
  207. package/dist/tools/exec-utils.js +294 -0
  208. package/dist/tools/exec-utils.js.map +1 -0
  209. package/dist/tools/fork-skill.js +14 -2
  210. package/dist/tools/fork-skill.js.map +1 -1
  211. package/dist/tools/investigation-feedback.d.ts +3 -0
  212. package/dist/tools/investigation-feedback.js +71 -0
  213. package/dist/tools/investigation-feedback.js.map +1 -0
  214. package/dist/tools/manage-schedule.js +16 -6
  215. package/dist/tools/manage-schedule.js.map +1 -1
  216. package/dist/tools/netns-script.js +27 -281
  217. package/dist/tools/netns-script.js.map +1 -1
  218. package/dist/tools/node-exec.d.ts +2 -14
  219. package/dist/tools/node-exec.js +18 -225
  220. package/dist/tools/node-exec.js.map +1 -1
  221. package/dist/tools/node-script.js +14 -168
  222. package/dist/tools/node-script.js.map +1 -1
  223. package/dist/tools/pod-exec.d.ts +1 -1
  224. package/dist/tools/pod-exec.js +10 -26
  225. package/dist/tools/pod-exec.js.map +1 -1
  226. package/dist/tools/pod-nsenter-exec.js +21 -225
  227. package/dist/tools/pod-nsenter-exec.js.map +1 -1
  228. package/dist/tools/pod-script.js +10 -19
  229. package/dist/tools/pod-script.js.map +1 -1
  230. package/dist/tools/restricted-bash.d.ts +1 -17
  231. package/dist/tools/restricted-bash.js +38 -252
  232. package/dist/tools/restricted-bash.js.map +1 -1
  233. package/dist/tools/run-skill.d.ts +3 -1
  234. package/dist/tools/run-skill.js +21 -1
  235. package/dist/tools/run-skill.js.map +1 -1
  236. package/dist/tools/script-resolver.d.ts +3 -1
  237. package/dist/tools/script-resolver.js +74 -30
  238. package/dist/tools/script-resolver.js.map +1 -1
  239. package/dist/tools/update-skill.js +17 -6
  240. package/dist/tools/update-skill.js.map +1 -1
  241. package/package.json +8 -6
  242. package/siclaw.mjs +10 -1
  243. package/skills/core/cluster-events/SKILL.md +1 -1
  244. package/skills/core/deep-investigation/SKILL.md +11 -0
  245. package/skills/core/deployment-rollout-debug/SKILL.md +1 -1
  246. package/skills/core/dns-debug/SKILL.md +1 -0
  247. package/skills/core/meta.json +12 -1
  248. package/skills/core/networkpolicy-debug/SKILL.md +332 -0
  249. package/skills/core/node-logs/scripts/get-node-logs.sh +19 -9
  250. package/skills/core/pod-pending-debug/SKILL.md +1 -0
  251. package/skills/core/quota-debug/SKILL.md +203 -0
  252. package/skills/core/service-debug/SKILL.md +1 -0
  253. package/skills/core/statefulset-debug/SKILL.md +280 -0
  254. package/skills/core/volcano-diagnose-pod/SKILL.md +196 -0
  255. package/skills/core/volcano-diagnose-pod/scripts/diagnose-pod.sh +175 -0
  256. package/skills/core/volcano-gang-scheduling/SKILL.md +299 -0
  257. package/skills/core/volcano-job-diagnose/SKILL.md +319 -0
  258. package/skills/core/volcano-job-diagnose/scripts/diagnose-job.sh +253 -0
  259. package/skills/core/volcano-node-resources/SKILL.md +334 -0
  260. package/skills/core/volcano-node-resources/scripts/get-node-resources.sh +281 -0
  261. package/skills/core/volcano-queue-diagnose/SKILL.md +294 -0
  262. package/skills/core/volcano-queue-diagnose/scripts/diagnose-queue.sh +283 -0
  263. package/skills/core/volcano-resource-insufficient/SKILL.md +315 -0
  264. package/skills/core/volcano-scheduler-config/SKILL.md +371 -0
  265. package/skills/core/volcano-scheduler-config/scripts/get-scheduler-config.sh +297 -0
  266. package/skills/core/volcano-scheduler-logs/SKILL.md +241 -0
  267. package/skills/core/volcano-scheduler-logs/scripts/get-scheduler-logs.sh +159 -0
  268. package/skills/platform/create-skill/SKILL.md +35 -3
  269. package/skills/platform/manage-skill/SKILL.md +9 -2
  270. package/skills/platform/update-skill/SKILL.md +17 -6
@@ -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 { loadMcpServersConfig } from "../core/mcp-client.js";
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
- res.writeHead(200, { "Content-Type": contentType });
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
- // Clean orphan model entries on startup, then set up env resolver
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
- const wsId = internalWorkspaceRepo
645
- ? (await internalWorkspaceRepo.getOrCreateDefault(userId)).id
646
- : "default";
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
- const client = new AgentBoxClient(handle.endpoint);
649
- // 2. Send prompt
650
- const promptResult = await client.prompt({ sessionId: data.sessionId, text: data.text });
651
- // 3. Wait for completion with timeout
652
- const resultText = await Promise.race([
653
- waitForAgentCompletion(client, promptResult.sessionId),
654
- rejectAfterTimeout(timeoutMs, data.sessionId),
655
- ]);
656
- const durationMs = Date.now() - startTime;
657
- console.log(`[gateway] agent-prompt completed user=${userId} duration=${durationMs}ms resultLen=${resultText.length}`);
658
- res.writeHead(200, { "Content-Type": "application/json" });
659
- res.end(JSON.stringify({ status: "success", resultText, durationMs }));
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
- // Parse userId from query params
979
+ // Use userId from mTLS certificate identity (authoritative)
958
980
  const urlObj = new URL(url, `https://${req.headers.host}`);
959
- const requestedUserId = urlObj.searchParams.get("userId");
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 jobs = await configRepo.listCronJobs(requestedUserId);
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=${requestedUserId}`);
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 localConfig = loadMcpServersConfig(undefined, { localOnly: true });
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
- resultText = content;
1114
+ if (typeof content === "string" && content) {
1115
+ extracted = content;
1083
1116
  }
1084
1117
  else if (Array.isArray(content)) {
1085
- const text = content
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
- /** Returns a promise that rejects after the given timeout */
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
- return new Promise((_, reject) => setTimeout(() => reject(new Error(`agent-prompt session=${sessionId} timed out after ${ms / 1000}s`)), ms));
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