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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import crypto from "node:crypto";
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { AgentBoxClient } from "./agentbox/client.js";
|
|
11
12
|
import { ChatRepository } from "./db/repositories/chat-repo.js";
|
|
12
13
|
import { SkillRepository } from "./db/repositories/skill-repo.js";
|
|
@@ -20,6 +21,8 @@ import { ModelConfigRepository } from "./db/repositories/model-config-repo.js";
|
|
|
20
21
|
import { CredentialRepository } from "./db/repositories/credential-repo.js";
|
|
21
22
|
import { WorkspaceRepository } from "./db/repositories/workspace-repo.js";
|
|
22
23
|
import { SystemConfigRepository } from "./db/repositories/system-config-repo.js";
|
|
24
|
+
import { EnvironmentRepository } from "./db/repositories/env-repo.js";
|
|
25
|
+
import { UserEnvConfigRepository } from "./db/repositories/user-env-config-repo.js";
|
|
23
26
|
import { getLabelsForSkill, batchGetLabels, listAllLabels } from "./skill-labels.js";
|
|
24
27
|
import { McpServerRepository } from "./db/repositories/mcp-server-repo.js";
|
|
25
28
|
import { SkillFileWriter } from "./skills/file-writer.js";
|
|
@@ -28,10 +31,11 @@ import { ScriptEvaluator } from "./skills/script-evaluator.js";
|
|
|
28
31
|
import { SkillVersionRepository } from "./db/repositories/skill-version-repo.js";
|
|
29
32
|
import { createTwoFilesPatch } from "diff";
|
|
30
33
|
import yaml from "js-yaml";
|
|
31
|
-
import { notifyCronService as notifyCronServiceImpl } from "./cron/notify.js";
|
|
32
34
|
import { buildSkillBundle } from "./skills/skill-bundle.js";
|
|
33
35
|
import { buildRedactionConfig, redactText } from "./output-redactor.js";
|
|
34
36
|
import { RESOURCE_DESCRIPTORS } from "../shared/resource-sync.js";
|
|
37
|
+
import { sql, gte, sum, count } from "drizzle-orm";
|
|
38
|
+
import { sessionStats } from "./db/schema.js";
|
|
35
39
|
function requireAuth(context) {
|
|
36
40
|
const userId = context.auth?.userId;
|
|
37
41
|
if (!userId)
|
|
@@ -40,11 +44,32 @@ function requireAuth(context) {
|
|
|
40
44
|
}
|
|
41
45
|
function requireAdmin(context) {
|
|
42
46
|
const userId = requireAuth(context);
|
|
43
|
-
if (context
|
|
47
|
+
if (!isAdminUser(context))
|
|
44
48
|
throw new Error("Forbidden: admin access required");
|
|
45
49
|
return userId;
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
function isAdminUser(context) {
|
|
52
|
+
return context.auth?.username === "admin";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compare two Kubernetes API server URLs by hostname (and port if present).
|
|
56
|
+
* Prevents substring bypass (e.g. "evil-https://real-server:6443" matching "real-server:6443").
|
|
57
|
+
*/
|
|
58
|
+
function apiServerHostMatch(kubeconfigServer, envApiServer) {
|
|
59
|
+
try {
|
|
60
|
+
const a = new URL(kubeconfigServer);
|
|
61
|
+
const b = new URL(envApiServer.includes("://") ? envApiServer : `https://${envApiServer}`);
|
|
62
|
+
// Require explicit port on the env side — environments without port are legacy and must be updated
|
|
63
|
+
if (!b.port)
|
|
64
|
+
return false;
|
|
65
|
+
return a.hostname === b.hostname && (a.port || "443") === b.port;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// If URL parsing fails, reject the match — don't fall back to loose comparison
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, activePromptUsers, agentBoxTlsOptions, resourceNotifier, metricsAggregator, cronService) {
|
|
48
73
|
const methods = new Map();
|
|
49
74
|
// Initialize repositories (null-safe — methods check before use)
|
|
50
75
|
const chatRepo = db ? new ChatRepository(db) : null;
|
|
@@ -62,13 +87,15 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
62
87
|
const sysConfigRepo = db ? new SystemConfigRepository(db) : null;
|
|
63
88
|
const mcpRepo = db ? new McpServerRepository(db) : null;
|
|
64
89
|
const skillContentRepo = db ? new SkillContentRepository(db) : null;
|
|
90
|
+
const envRepo = db ? new EnvironmentRepository(db) : null;
|
|
91
|
+
const userEnvConfigRepo = db ? new UserEnvConfigRepository(db) : null;
|
|
65
92
|
const scriptEvaluator = new ScriptEvaluator(modelConfigRepo);
|
|
66
|
-
/** Resolve workspaceId for a session from DB
|
|
93
|
+
/** Resolve workspaceId for a session from DB */
|
|
67
94
|
async function resolveSessionWorkspace(sessionId) {
|
|
68
95
|
if (!chatRepo)
|
|
69
|
-
return
|
|
96
|
+
return undefined;
|
|
70
97
|
const session = await chatRepo.getSession(sessionId);
|
|
71
|
-
return session?.workspaceId ??
|
|
98
|
+
return session?.workspaceId ?? undefined;
|
|
72
99
|
}
|
|
73
100
|
/** Find an AgentBox handle for a user, trying session workspace first, then any active box */
|
|
74
101
|
async function findAgentBoxForSession(userId, sessionId) {
|
|
@@ -119,6 +146,56 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
119
146
|
console.warn(`[resource-notify] All skill reload failed:`, err.message);
|
|
120
147
|
});
|
|
121
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Push updated credentials to all active AgentBoxes for a user (fire-and-forget).
|
|
151
|
+
* Builds the credential payload per workspace and POSTs to each box's /api/reload-credentials.
|
|
152
|
+
*/
|
|
153
|
+
function pushCredentialsToUser(userId) {
|
|
154
|
+
if (!workspaceRepo)
|
|
155
|
+
return;
|
|
156
|
+
// Async fire-and-forget
|
|
157
|
+
(async () => {
|
|
158
|
+
// Local mode: handles carry workspaceId from cache key
|
|
159
|
+
let handles = agentBoxManager.getForUser(userId);
|
|
160
|
+
if (handles.length === 0) {
|
|
161
|
+
// K8s mode: find running pods via list(), workspace label provides workspaceId
|
|
162
|
+
const allBoxes = await agentBoxManager.list();
|
|
163
|
+
const userBoxes = allBoxes.filter((b) => b.userId === userId && b.status === "running" && b.endpoint);
|
|
164
|
+
if (userBoxes.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
for (const box of userBoxes) {
|
|
167
|
+
handles.push({
|
|
168
|
+
boxId: box.boxId,
|
|
169
|
+
userId: box.userId,
|
|
170
|
+
endpoint: box.endpoint,
|
|
171
|
+
workspaceId: box.workspaceId,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (handles.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
for (const handle of handles) {
|
|
178
|
+
try {
|
|
179
|
+
// Resolve workspace for this box
|
|
180
|
+
const wsId = handle.workspaceId;
|
|
181
|
+
const ws = wsId
|
|
182
|
+
? (await workspaceRepo.getById(wsId)) ?? (await workspaceRepo.getOrCreateDefault(userId))
|
|
183
|
+
: await workspaceRepo.getOrCreateDefault(userId);
|
|
184
|
+
if (!ws)
|
|
185
|
+
continue;
|
|
186
|
+
const payload = await buildCredentialPayload(userId, ws.id, ws.isDefault);
|
|
187
|
+
const client = new AgentBoxClient(handle.endpoint, 15000, agentBoxTlsOptions);
|
|
188
|
+
await client.reloadCredentials(payload);
|
|
189
|
+
console.log(`[credential-push] Pushed credentials to box=${handle.boxId} ws=${ws.name} (${payload.files.length} files)`);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.warn(`[credential-push] Failed for box=${handle.boxId}:`, err instanceof Error ? err.message : err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
})().catch((err) => {
|
|
196
|
+
console.warn(`[credential-push] Unexpected error for userId=${userId}:`, err instanceof Error ? err.message : err);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
122
199
|
// Initialize skills dir
|
|
123
200
|
skillWriter.init()
|
|
124
201
|
.then(async () => {
|
|
@@ -127,10 +204,13 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
127
204
|
.catch((err) => {
|
|
128
205
|
console.error("[rpc] Failed to initialize skills:", err);
|
|
129
206
|
});
|
|
130
|
-
// Resolve core/extension skills directory: prefer baked-in
|
|
131
|
-
|
|
207
|
+
// Resolve core/extension skills directory: prefer baked-in package path over NFS
|
|
208
|
+
// Use import.meta.url to locate the npm package root (dist/gateway/rpc-methods.js → package root)
|
|
209
|
+
const __rpcDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
210
|
+
const packageRoot = path.resolve(__rpcDirname, "..", "..");
|
|
211
|
+
const builtinCoreDir = path.join(packageRoot, "skills", "core");
|
|
132
212
|
const coreSkillsDir = fs.existsSync(builtinCoreDir) ? builtinCoreDir : path.join(skillsDir, "core");
|
|
133
|
-
const builtinExtDir = path.join(
|
|
213
|
+
const builtinExtDir = path.join(packageRoot, "skills", "extension");
|
|
134
214
|
const extSkillsDir = fs.existsSync(builtinExtDir) ? builtinExtDir : path.join(skillsDir, "extension");
|
|
135
215
|
/** Resolve the filesystem dir for a builtin skill dirName (core first, then extension) */
|
|
136
216
|
function resolveBuiltinSkillDir(dirName) {
|
|
@@ -265,7 +345,9 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
265
345
|
workspace = null;
|
|
266
346
|
}
|
|
267
347
|
}
|
|
268
|
-
|
|
348
|
+
if (!workspace)
|
|
349
|
+
throw new Error("Failed to resolve workspace");
|
|
350
|
+
const effectiveWorkspaceId = workspace.id;
|
|
269
351
|
// Ensure session exists in DB
|
|
270
352
|
if (chatRepo) {
|
|
271
353
|
if (sessionId) {
|
|
@@ -292,7 +374,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
292
374
|
});
|
|
293
375
|
}
|
|
294
376
|
if (!sessionId)
|
|
295
|
-
|
|
377
|
+
throw new Error("Failed to create session");
|
|
296
378
|
// Forward model selection and brain type from frontend
|
|
297
379
|
// Use workspace default model if user didn't specify one
|
|
298
380
|
const modelProvider = params.modelProvider ?? workspace?.configJson?.defaultModel?.provider;
|
|
@@ -327,9 +409,12 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
327
409
|
}
|
|
328
410
|
}
|
|
329
411
|
// Get or create AgentBox (per workspace)
|
|
412
|
+
// Encode workspace envType into cert so Gateway trusts the cert, not AgentBox's self-declaration
|
|
413
|
+
const podEnv = (workspace?.envType === "test" ? "test" : "prod");
|
|
330
414
|
const handle = await agentBoxManager.getOrCreate(userId, effectiveWorkspaceId, {
|
|
331
415
|
workspaceId: effectiveWorkspaceId,
|
|
332
416
|
allowedTools,
|
|
417
|
+
podEnv,
|
|
333
418
|
});
|
|
334
419
|
const client = new AgentBoxClient(handle.endpoint, 30000, agentBoxTlsOptions);
|
|
335
420
|
// Send prompt
|
|
@@ -361,7 +446,9 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
361
446
|
// Async SSE processing
|
|
362
447
|
(async () => {
|
|
363
448
|
let assistantContent = "";
|
|
364
|
-
|
|
449
|
+
// Map keyed by toolName to handle parallel tool calls correctly
|
|
450
|
+
const pendingToolInputs = new Map();
|
|
451
|
+
const pendingToolStartTimes = new Map();
|
|
365
452
|
let sseEventCount = 0;
|
|
366
453
|
const sseStartTime = Date.now();
|
|
367
454
|
// Mark user as having an active prompt so WS teardown won't kill the pod
|
|
@@ -387,15 +474,32 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
387
474
|
.map((c) => c.text ?? "")
|
|
388
475
|
.join("") ?? "";
|
|
389
476
|
const toolName = eventData.toolName || "tool";
|
|
477
|
+
// Audit: determine outcome
|
|
478
|
+
let outcome = "success";
|
|
479
|
+
if (toolResult?.details?.blocked) {
|
|
480
|
+
outcome = "blocked";
|
|
481
|
+
}
|
|
482
|
+
else if (toolResult?.details?.error) {
|
|
483
|
+
outcome = "error";
|
|
484
|
+
}
|
|
485
|
+
const startTime = pendingToolStartTimes.get(toolName);
|
|
486
|
+
const durationMs = startTime != null
|
|
487
|
+
? Date.now() - startTime
|
|
488
|
+
: undefined;
|
|
489
|
+
const toolInput = pendingToolInputs.get(toolName) || "";
|
|
390
490
|
dbMessageId = await chatRepo.appendMessage({
|
|
391
491
|
sessionId: result.sessionId,
|
|
392
492
|
role: "tool",
|
|
393
493
|
content: redactText(text, redactionConfig),
|
|
394
494
|
toolName,
|
|
395
|
-
toolInput:
|
|
495
|
+
toolInput: toolInput ? redactText(toolInput, redactionConfig) : undefined,
|
|
496
|
+
userId,
|
|
497
|
+
outcome,
|
|
498
|
+
durationMs,
|
|
396
499
|
});
|
|
397
500
|
await chatRepo.incrementMessageCount(result.sessionId);
|
|
398
|
-
|
|
501
|
+
pendingToolInputs.delete(toolName);
|
|
502
|
+
pendingToolStartTimes.delete(toolName);
|
|
399
503
|
}
|
|
400
504
|
// Forward event to frontend via sendToUser (targets all WS connections
|
|
401
505
|
// for this user, so reconnected sessions also receive live events)
|
|
@@ -465,9 +569,11 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
465
569
|
}
|
|
466
570
|
}
|
|
467
571
|
else if (eventType === "tool_execution_start") {
|
|
468
|
-
// Capture tool input for DB persistence
|
|
572
|
+
// Capture tool input and start time for DB persistence (keyed by toolName for parallel calls)
|
|
573
|
+
const startToolName = eventData.toolName || "tool";
|
|
469
574
|
const args = eventData.args;
|
|
470
|
-
|
|
575
|
+
pendingToolInputs.set(startToolName, args ? JSON.stringify(args) : "");
|
|
576
|
+
pendingToolStartTimes.set(startToolName, Date.now());
|
|
471
577
|
}
|
|
472
578
|
// tool_execution_end already handled above
|
|
473
579
|
}
|
|
@@ -595,9 +701,11 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
595
701
|
const providerName = params.provider;
|
|
596
702
|
const baseUrl = params.baseUrl;
|
|
597
703
|
const apiKey = params.apiKey;
|
|
704
|
+
const api = params.api;
|
|
705
|
+
const authHeader = params.authHeader;
|
|
598
706
|
if (!providerName)
|
|
599
707
|
throw new Error("Missing provider name");
|
|
600
|
-
await modelConfigRepo.saveProvider(providerName, baseUrl, apiKey);
|
|
708
|
+
await modelConfigRepo.saveProvider(providerName, baseUrl, apiKey, api, authHeader);
|
|
601
709
|
return { ok: true };
|
|
602
710
|
});
|
|
603
711
|
methods.set("provider.delete", async (params, context) => {
|
|
@@ -643,6 +751,113 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
643
751
|
await modelConfigRepo.removeModel(providerName, modelId);
|
|
644
752
|
return { ok: true };
|
|
645
753
|
});
|
|
754
|
+
methods.set("provider.testConnection", async (params, context) => {
|
|
755
|
+
requireAdmin(context);
|
|
756
|
+
const baseUrl = params.baseUrl;
|
|
757
|
+
const apiKey = params.apiKey;
|
|
758
|
+
const api = params.api ?? "openai-completions";
|
|
759
|
+
if (!baseUrl || !apiKey)
|
|
760
|
+
throw new Error("Missing required params: baseUrl, apiKey");
|
|
761
|
+
const controller = new AbortController();
|
|
762
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
763
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
764
|
+
try {
|
|
765
|
+
if (api === "anthropic") {
|
|
766
|
+
// Anthropic: auth-only check via counting message tokens (free, no completion)
|
|
767
|
+
const res = await fetch(`${base}/v1/messages/count_tokens`, {
|
|
768
|
+
method: "POST",
|
|
769
|
+
headers: {
|
|
770
|
+
"Content-Type": "application/json",
|
|
771
|
+
"x-api-key": apiKey,
|
|
772
|
+
"anthropic-version": "2023-06-01",
|
|
773
|
+
},
|
|
774
|
+
body: JSON.stringify({
|
|
775
|
+
model: "claude-sonnet-4-20250514",
|
|
776
|
+
messages: [{ role: "user", content: "hi" }],
|
|
777
|
+
}),
|
|
778
|
+
signal: controller.signal,
|
|
779
|
+
});
|
|
780
|
+
if (res.ok)
|
|
781
|
+
return { ok: true, message: "Connection successful" };
|
|
782
|
+
// 404 means endpoint not available — try a simple auth header check
|
|
783
|
+
if (res.status === 404) {
|
|
784
|
+
// Any authenticated GET that returns non-401 means key is valid
|
|
785
|
+
const fallback = await fetch(`${base}/v1/models`, {
|
|
786
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
787
|
+
signal: controller.signal,
|
|
788
|
+
});
|
|
789
|
+
if (fallback.status !== 401 && fallback.status !== 403) {
|
|
790
|
+
return { ok: true, message: "Connection successful" };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const body = await res.text().catch(() => "");
|
|
794
|
+
return { ok: false, message: `HTTP ${res.status}: ${body.slice(0, 200)}` };
|
|
795
|
+
}
|
|
796
|
+
// OpenAI-compatible: try GET /models (auth-only, no completion)
|
|
797
|
+
const modelsPaths = [`${base}/models`, `${base}/v1/models`];
|
|
798
|
+
for (const url of modelsPaths) {
|
|
799
|
+
const res = await fetch(url, {
|
|
800
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
801
|
+
signal: controller.signal,
|
|
802
|
+
});
|
|
803
|
+
if (res.ok) {
|
|
804
|
+
return { ok: true, message: "Connection successful" };
|
|
805
|
+
}
|
|
806
|
+
// 401/403 = bad key — definitive failure
|
|
807
|
+
if (res.status === 401 || res.status === 403) {
|
|
808
|
+
const body = await res.text().catch(() => "");
|
|
809
|
+
return { ok: false, message: `Authentication failed (HTTP ${res.status})` };
|
|
810
|
+
}
|
|
811
|
+
// 404 = endpoint not found but server responded — try next path
|
|
812
|
+
}
|
|
813
|
+
// All paths returned 404: server is reachable and didn't reject the key
|
|
814
|
+
return { ok: true, message: "Connection successful" };
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
818
|
+
return { ok: false, message: msg.includes("abort") ? "Connection timed out (10s)" : msg };
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
clearTimeout(timeout);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
methods.set("provider.quickSetup", async (params, context) => {
|
|
825
|
+
requireAdmin(context);
|
|
826
|
+
if (!modelConfigRepo)
|
|
827
|
+
throw new Error("Database not available");
|
|
828
|
+
const providerName = params.provider;
|
|
829
|
+
const baseUrl = params.baseUrl;
|
|
830
|
+
const apiKey = params.apiKey;
|
|
831
|
+
const api = params.api ?? "openai-completions";
|
|
832
|
+
const authHeader = params.authHeader ?? false;
|
|
833
|
+
const model = params.model;
|
|
834
|
+
const setAsDefault = params.setAsDefault ?? true;
|
|
835
|
+
if (!providerName)
|
|
836
|
+
throw new Error("Missing provider name");
|
|
837
|
+
// 1. Save provider
|
|
838
|
+
await modelConfigRepo.saveProvider(providerName, baseUrl, apiKey, api, authHeader);
|
|
839
|
+
// 2. Add model if provided
|
|
840
|
+
if (model?.id && model?.name) {
|
|
841
|
+
try {
|
|
842
|
+
await modelConfigRepo.addModel(providerName, {
|
|
843
|
+
id: model.id,
|
|
844
|
+
name: model.name,
|
|
845
|
+
reasoning: model.reasoning ?? false,
|
|
846
|
+
contextWindow: model.contextWindow ?? 128000,
|
|
847
|
+
maxTokens: model.maxTokens ?? 65536,
|
|
848
|
+
category: model.category ?? "llm",
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
// Model may already exist (e.g. re-running quick setup) — that's fine
|
|
853
|
+
}
|
|
854
|
+
// 3. Set as default
|
|
855
|
+
if (setAsDefault) {
|
|
856
|
+
await modelConfigRepo.setDefault(providerName, model.id);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return { ok: true };
|
|
860
|
+
});
|
|
646
861
|
methods.set("config.getDefaultModel", async (_params, context) => {
|
|
647
862
|
requireAuth(context);
|
|
648
863
|
if (!modelConfigRepo)
|
|
@@ -888,12 +1103,14 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
888
1103
|
const userId = requireAuth(context);
|
|
889
1104
|
const sessionId = params.sessionId;
|
|
890
1105
|
const snapKey = sessionId ? `${userId}:${sessionId}` : userId;
|
|
1106
|
+
const streamKey = sessionId ? `${userId}:${sessionId}` : undefined;
|
|
1107
|
+
const promptActive = streamKey ? activeStreams.has(streamKey) : false;
|
|
891
1108
|
const snap = dpProgressSnapshots.get(snapKey);
|
|
892
1109
|
if (!snap || Date.now() - snap.updatedAt > 600_000) {
|
|
893
1110
|
dpProgressSnapshots.delete(snapKey);
|
|
894
|
-
return { events: null };
|
|
1111
|
+
return { events: null, promptActive };
|
|
895
1112
|
}
|
|
896
|
-
return { sessionId: snap.sessionId, events: snap.events };
|
|
1113
|
+
return { sessionId: snap.sessionId, events: snap.events, promptActive };
|
|
897
1114
|
});
|
|
898
1115
|
methods.set("chat.history", async (params, context) => {
|
|
899
1116
|
const userId = requireAuth(context);
|
|
@@ -943,34 +1160,27 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
943
1160
|
// ─────────────────────────────────────────────────
|
|
944
1161
|
methods.set("session.list", async (params, context) => {
|
|
945
1162
|
const userId = requireAuth(context);
|
|
1163
|
+
if (!chatRepo)
|
|
1164
|
+
throw new Error("Database not available");
|
|
946
1165
|
const workspaceId = params?.workspaceId;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
// Fallback: get from AgentBox
|
|
961
|
-
const handle = await agentBoxManager.getAsync(userId, workspaceId ?? "default");
|
|
962
|
-
if (!handle)
|
|
963
|
-
return { sessions: [] };
|
|
964
|
-
const client = new AgentBoxClient(handle.endpoint, 30000, agentBoxTlsOptions);
|
|
965
|
-
return client.listSessions();
|
|
1166
|
+
const rows = await chatRepo.listSessions(userId, 20, workspaceId);
|
|
1167
|
+
return {
|
|
1168
|
+
sessions: rows.map((s) => ({
|
|
1169
|
+
key: s.id,
|
|
1170
|
+
title: s.title,
|
|
1171
|
+
preview: s.preview,
|
|
1172
|
+
createdAt: s.createdAt?.toISOString(),
|
|
1173
|
+
lastActiveAt: s.lastActiveAt?.toISOString(),
|
|
1174
|
+
messageCount: s.messageCount,
|
|
1175
|
+
})),
|
|
1176
|
+
};
|
|
966
1177
|
});
|
|
967
1178
|
methods.set("session.create", async (_params, context) => {
|
|
968
1179
|
const userId = requireAuth(context);
|
|
969
|
-
if (chatRepo)
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
return { sessionId: "default", sessionKey: "default" };
|
|
1180
|
+
if (!chatRepo)
|
|
1181
|
+
throw new Error("Database not available");
|
|
1182
|
+
const session = await chatRepo.createSession(userId);
|
|
1183
|
+
return { sessionId: session.id, sessionKey: session.id };
|
|
974
1184
|
});
|
|
975
1185
|
methods.set("session.delete", async (params, context) => {
|
|
976
1186
|
const userId = requireAuth(context);
|
|
@@ -987,7 +1197,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
987
1197
|
// ─────────────────────────────────────────────────
|
|
988
1198
|
methods.set("box.status", async (params, context) => {
|
|
989
1199
|
const userId = requireAuth(context);
|
|
990
|
-
const workspaceId = params?.workspaceId
|
|
1200
|
+
const workspaceId = params?.workspaceId;
|
|
991
1201
|
const handle = await agentBoxManager.getAsync(userId, workspaceId);
|
|
992
1202
|
if (!handle)
|
|
993
1203
|
return { boxStatus: "not_created" };
|
|
@@ -1824,12 +2034,17 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1824
2034
|
await skillRepo.update(skillId, updates);
|
|
1825
2035
|
}
|
|
1826
2036
|
// 5. AI script review using staged files
|
|
2037
|
+
// Clear old reviews first so the UI shows "in progress" while the new review runs
|
|
2038
|
+
if (skillReviewRepo) {
|
|
2039
|
+
await skillReviewRepo.deleteAiReviewsForSkill(skillId);
|
|
2040
|
+
}
|
|
1827
2041
|
let stagedFiles = null;
|
|
1828
2042
|
if (skillContentRepo) {
|
|
1829
2043
|
stagedFiles = await skillContentRepo.read(skillId, "staging");
|
|
1830
2044
|
}
|
|
1831
|
-
if (stagedFiles?.scripts?.length) {
|
|
1832
|
-
|
|
2045
|
+
if (stagedFiles?.scripts?.length || stagedFiles?.specs) {
|
|
2046
|
+
// Review both scripts and specs — specs contain command templates the agent will follow
|
|
2047
|
+
triggerScriptReview(skillId, meta.name, stagedFiles?.scripts ?? [], stagedFiles?.specs).catch(console.error);
|
|
1833
2048
|
}
|
|
1834
2049
|
// 6. Notify reviewers (only on first submit to avoid flooding)
|
|
1835
2050
|
if (!isPending) {
|
|
@@ -1865,14 +2080,17 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1865
2080
|
if (currentReviewStatus !== "pending") {
|
|
1866
2081
|
throw new Error("This skill is not pending review");
|
|
1867
2082
|
}
|
|
1868
|
-
// Record reviewer decision
|
|
2083
|
+
// Record reviewer decision — inherit riskLevel from the latest AI review if available
|
|
1869
2084
|
if (skillReviewRepo) {
|
|
2085
|
+
const reviews = await skillReviewRepo.listForSkill(skillId);
|
|
2086
|
+
const aiReview = reviews.find((r) => r.reviewerType === "ai");
|
|
2087
|
+
const riskLevel = aiReview?.riskLevel ?? "low";
|
|
1870
2088
|
await skillReviewRepo.create({
|
|
1871
2089
|
skillId,
|
|
1872
2090
|
version: meta.version,
|
|
1873
2091
|
reviewerType: "admin",
|
|
1874
2092
|
reviewerId,
|
|
1875
|
-
riskLevel
|
|
2093
|
+
riskLevel,
|
|
1876
2094
|
summary: reason || (decision === "approve" ? "Approved by reviewer" : "Rejected by reviewer"),
|
|
1877
2095
|
findings: [],
|
|
1878
2096
|
decision,
|
|
@@ -2092,19 +2310,27 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2092
2310
|
// ─────────────────────────────────────────────────
|
|
2093
2311
|
// Cron Job Methods
|
|
2094
2312
|
// ─────────────────────────────────────────────────
|
|
2095
|
-
|
|
2096
|
-
function notifyCronService(payload) {
|
|
2097
|
-
notifyCronServiceImpl(payload, configRepo);
|
|
2098
|
-
}
|
|
2099
|
-
methods.set("cron.list", async (_params, context) => {
|
|
2313
|
+
methods.set("cron.list", async (params, context) => {
|
|
2100
2314
|
const userId = requireAuth(context);
|
|
2101
2315
|
if (!configRepo)
|
|
2102
2316
|
return { jobs: [] };
|
|
2103
|
-
const
|
|
2317
|
+
const workspaceId = params.workspaceId;
|
|
2318
|
+
const opts = workspaceId ? { workspaceId } : undefined;
|
|
2319
|
+
const rows = await configRepo.listCronJobs(userId, opts);
|
|
2320
|
+
// Enrich with workspace names
|
|
2321
|
+
const wsIds = [...new Set(rows.map((r) => r.workspaceId).filter(Boolean))];
|
|
2322
|
+
const wsNameMap = new Map();
|
|
2323
|
+
if (wsIds.length > 0 && workspaceRepo) {
|
|
2324
|
+
for (const wsId of wsIds) {
|
|
2325
|
+
const ws = await workspaceRepo.getById(wsId);
|
|
2326
|
+
if (ws)
|
|
2327
|
+
wsNameMap.set(wsId, ws.name);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2104
2330
|
return {
|
|
2105
2331
|
jobs: rows.map((r) => ({
|
|
2106
2332
|
...r,
|
|
2107
|
-
|
|
2333
|
+
workspaceName: r.workspaceId ? (wsNameMap.get(r.workspaceId) ?? null) : null,
|
|
2108
2334
|
})),
|
|
2109
2335
|
};
|
|
2110
2336
|
});
|
|
@@ -2116,10 +2342,8 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2116
2342
|
const description = params.description;
|
|
2117
2343
|
const schedule = params.schedule;
|
|
2118
2344
|
const status = params.status ?? "active";
|
|
2119
|
-
// For updates: fetch existing job to get current assignedTo
|
|
2120
2345
|
const existingId = params.id;
|
|
2121
|
-
const
|
|
2122
|
-
const envId = params.envId;
|
|
2346
|
+
const workspaceId = params.workspaceId;
|
|
2123
2347
|
const id = await configRepo.saveCronJob(userId, {
|
|
2124
2348
|
id: existingId,
|
|
2125
2349
|
name,
|
|
@@ -2127,38 +2351,20 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2127
2351
|
schedule,
|
|
2128
2352
|
skillId: params.skillId,
|
|
2129
2353
|
status,
|
|
2130
|
-
|
|
2354
|
+
workspaceId: workspaceId ?? null,
|
|
2131
2355
|
});
|
|
2132
|
-
if (
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
}
|
|
2136
|
-
else {
|
|
2137
|
-
// Active — keep existing assignment if possible, otherwise assign to least-loaded
|
|
2138
|
-
let assignedTo = existingJob?.assignedTo ?? null;
|
|
2139
|
-
if (!assignedTo) {
|
|
2140
|
-
try {
|
|
2141
|
-
const leastLoaded = await configRepo.getLeastLoadedInstance();
|
|
2142
|
-
if (leastLoaded) {
|
|
2143
|
-
assignedTo = leastLoaded.instanceId;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
catch {
|
|
2147
|
-
// No instances available yet — coordinator will pick up unassigned jobs
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
if (assignedTo) {
|
|
2151
|
-
await configRepo.assignCronJob(id, assignedTo);
|
|
2356
|
+
if (cronService) {
|
|
2357
|
+
if (status === "paused") {
|
|
2358
|
+
cronService.cancel(id);
|
|
2152
2359
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
job: {
|
|
2360
|
+
else {
|
|
2361
|
+
cronService.addOrUpdate({
|
|
2156
2362
|
id, userId, name, description: description ?? null, schedule, status,
|
|
2157
|
-
skillId: params.skillId ?? null, assignedTo,
|
|
2363
|
+
skillId: params.skillId ?? null, assignedTo: null,
|
|
2158
2364
|
lastRunAt: null, lastResult: null, lockedBy: null, lockedAt: null,
|
|
2159
|
-
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2365
|
+
workspaceId: workspaceId ?? null,
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2162
2368
|
}
|
|
2163
2369
|
return { id, name, schedule, status };
|
|
2164
2370
|
});
|
|
@@ -2176,7 +2382,13 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2176
2382
|
if (job.userId !== userId)
|
|
2177
2383
|
throw new Error("Forbidden");
|
|
2178
2384
|
await configRepo.deleteCronJob(id);
|
|
2179
|
-
|
|
2385
|
+
cronService?.cancel(id);
|
|
2386
|
+
// Auto-dismiss notifications for the deleted job
|
|
2387
|
+
if (notifRepo) {
|
|
2388
|
+
await notifRepo.dismissByTypeAndRelatedId("cron_success", id);
|
|
2389
|
+
await notifRepo.dismissByTypeAndRelatedId("cron_failure", id);
|
|
2390
|
+
await notifRepo.dismissByTypeAndRelatedId("cron_result", id); // legacy type
|
|
2391
|
+
}
|
|
2180
2392
|
return { status: "deleted" };
|
|
2181
2393
|
});
|
|
2182
2394
|
methods.set("cron.setStatus", async (params, context) => {
|
|
@@ -2204,21 +2416,23 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2204
2416
|
schedule: job.schedule,
|
|
2205
2417
|
skillId: job.skillId ?? undefined,
|
|
2206
2418
|
status,
|
|
2207
|
-
|
|
2419
|
+
workspaceId: job.workspaceId ?? null,
|
|
2208
2420
|
});
|
|
2209
|
-
//
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2421
|
+
// Update scheduler
|
|
2422
|
+
if (cronService) {
|
|
2423
|
+
if (status === "paused") {
|
|
2424
|
+
cronService.cancel(id);
|
|
2425
|
+
}
|
|
2426
|
+
else {
|
|
2427
|
+
cronService.addOrUpdate({
|
|
2214
2428
|
id, userId, name: job.name, description: job.description ?? null,
|
|
2215
2429
|
schedule: job.schedule, status, skillId: job.skillId ?? null,
|
|
2216
|
-
assignedTo:
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2430
|
+
assignedTo: null, lastRunAt: null, lastResult: null,
|
|
2431
|
+
lockedBy: null, lockedAt: null,
|
|
2432
|
+
workspaceId: job.workspaceId ?? null,
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2222
2436
|
return { id, status };
|
|
2223
2437
|
});
|
|
2224
2438
|
methods.set("cron.rename", async (params, context) => {
|
|
@@ -2245,21 +2459,46 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2245
2459
|
schedule: job.schedule,
|
|
2246
2460
|
skillId: job.skillId ?? undefined,
|
|
2247
2461
|
status: job.status,
|
|
2248
|
-
|
|
2462
|
+
workspaceId: job.workspaceId ?? null,
|
|
2249
2463
|
});
|
|
2250
|
-
//
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
job: {
|
|
2464
|
+
// Update scheduler (name change — reschedule with updated job data)
|
|
2465
|
+
if (cronService && job.status === "active") {
|
|
2466
|
+
cronService.addOrUpdate({
|
|
2254
2467
|
id, userId, name: newName.trim(), description: job.description ?? null,
|
|
2255
|
-
schedule: job.schedule, status: job.status,
|
|
2256
|
-
|
|
2468
|
+
schedule: job.schedule, status: job.status,
|
|
2469
|
+
skillId: job.skillId ?? null, assignedTo: null,
|
|
2257
2470
|
lastRunAt: null, lastResult: null, lockedBy: null, lockedAt: null,
|
|
2258
|
-
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2471
|
+
workspaceId: job.workspaceId ?? null,
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2261
2474
|
return { id, name: newName.trim() };
|
|
2262
2475
|
});
|
|
2476
|
+
methods.set("cron.runs", async (params, context) => {
|
|
2477
|
+
const userId = requireAuth(context);
|
|
2478
|
+
if (!configRepo)
|
|
2479
|
+
throw new Error("Database not available");
|
|
2480
|
+
const jobId = params.jobId;
|
|
2481
|
+
if (!jobId)
|
|
2482
|
+
throw new Error("Missing required param: jobId");
|
|
2483
|
+
// Verify ownership
|
|
2484
|
+
const job = await configRepo.getCronJobById(jobId);
|
|
2485
|
+
if (!job)
|
|
2486
|
+
throw new Error("Job not found");
|
|
2487
|
+
if (job.userId !== userId)
|
|
2488
|
+
throw new Error("Forbidden");
|
|
2489
|
+
const limit = Math.min(Number(params.limit) || 20, 100);
|
|
2490
|
+
const runs = await configRepo.listCronJobRuns(jobId, limit);
|
|
2491
|
+
return {
|
|
2492
|
+
runs: runs.map((r) => ({
|
|
2493
|
+
id: r.id,
|
|
2494
|
+
status: r.status,
|
|
2495
|
+
resultText: r.resultText,
|
|
2496
|
+
error: r.error,
|
|
2497
|
+
durationMs: r.durationMs,
|
|
2498
|
+
createdAt: r.createdAt instanceof Date ? r.createdAt.toISOString() : r.createdAt ? new Date(Number(r.createdAt) * 1000).toISOString() : null,
|
|
2499
|
+
})),
|
|
2500
|
+
};
|
|
2501
|
+
});
|
|
2263
2502
|
// ─────────────────────────────────────────────────
|
|
2264
2503
|
// Trigger Methods
|
|
2265
2504
|
// ─────────────────────────────────────────────────
|
|
@@ -2707,6 +2946,9 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2707
2946
|
const configJson = params.configJson;
|
|
2708
2947
|
if (!name)
|
|
2709
2948
|
throw new Error("Missing required param: name");
|
|
2949
|
+
if (type === "kubeconfig") {
|
|
2950
|
+
throw new Error("Kubeconfig credentials are now managed via Environments. Use userEnvConfig.set instead.");
|
|
2951
|
+
}
|
|
2710
2952
|
if (!type || !CREDENTIAL_TYPES.includes(type)) {
|
|
2711
2953
|
throw new Error(`Invalid credential type. Must be one of: ${CREDENTIAL_TYPES.join(", ")}`);
|
|
2712
2954
|
}
|
|
@@ -2714,6 +2956,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2714
2956
|
throw new Error("Missing required param: configJson");
|
|
2715
2957
|
validateCredentialConfig(type, configJson);
|
|
2716
2958
|
const id = await credRepo.create({ userId, name, type, description, configJson });
|
|
2959
|
+
pushCredentialsToUser(userId);
|
|
2717
2960
|
return { id, name, type };
|
|
2718
2961
|
});
|
|
2719
2962
|
methods.set("credential.update", async (params, context) => {
|
|
@@ -2737,6 +2980,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2737
2980
|
updates.configJson = configJson;
|
|
2738
2981
|
}
|
|
2739
2982
|
await credRepo.update(userId, id, updates);
|
|
2983
|
+
pushCredentialsToUser(userId);
|
|
2740
2984
|
return { status: "updated" };
|
|
2741
2985
|
});
|
|
2742
2986
|
methods.set("credential.delete", async (params, context) => {
|
|
@@ -2750,9 +2994,253 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2750
2994
|
if (!existing)
|
|
2751
2995
|
throw new Error("Credential not found");
|
|
2752
2996
|
await credRepo.delete(userId, id);
|
|
2997
|
+
pushCredentialsToUser(userId);
|
|
2998
|
+
return { status: "deleted" };
|
|
2999
|
+
});
|
|
3000
|
+
// ─────────────────────────────────────────────────
|
|
3001
|
+
// Environment Methods (admin-only)
|
|
3002
|
+
// ─────────────────────────────────────────────────
|
|
3003
|
+
methods.set("environment.list", async (_params, context) => {
|
|
3004
|
+
const userId = requireAuth(context);
|
|
3005
|
+
if (!envRepo)
|
|
3006
|
+
return { environments: [], isAdmin: false };
|
|
3007
|
+
const isAdmin = isAdminUser(context);
|
|
3008
|
+
// Check testOnly
|
|
3009
|
+
let isTestOnly = false;
|
|
3010
|
+
if (userRepo) {
|
|
3011
|
+
const dbUser = await userRepo.getById(userId);
|
|
3012
|
+
isTestOnly = dbUser?.testOnly ?? false;
|
|
3013
|
+
}
|
|
3014
|
+
const allEnvs = await envRepo.list();
|
|
3015
|
+
const visibleEnvs = isTestOnly ? allEnvs.filter((e) => e.isTest) : allEnvs;
|
|
3016
|
+
// Fetch user's kubeconfig status
|
|
3017
|
+
const userConfigs = userEnvConfigRepo ? await userEnvConfigRepo.listForUser(userId) : [];
|
|
3018
|
+
const configMap = new Map(userConfigs.map((c) => [c.envId, c]));
|
|
3019
|
+
return {
|
|
3020
|
+
isAdmin,
|
|
3021
|
+
environments: visibleEnvs.map((e) => ({
|
|
3022
|
+
id: e.id,
|
|
3023
|
+
name: e.name,
|
|
3024
|
+
isTest: e.isTest,
|
|
3025
|
+
apiServer: e.apiServer,
|
|
3026
|
+
allowedServers: e.allowedServers ? e.allowedServers.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
3027
|
+
hasDefaultKubeconfig: !!e.defaultKubeconfig,
|
|
3028
|
+
hasUserKubeconfig: configMap.has(e.id),
|
|
3029
|
+
userConfigUpdatedAt: configMap.get(e.id)?.updatedAt?.toISOString?.() ?? configMap.get(e.id)?.updatedAt ?? null,
|
|
3030
|
+
createdBy: e.createdBy,
|
|
3031
|
+
createdAt: e.createdAt,
|
|
3032
|
+
updatedAt: e.updatedAt,
|
|
3033
|
+
})),
|
|
3034
|
+
};
|
|
3035
|
+
});
|
|
3036
|
+
methods.set("environment.create", async (params, context) => {
|
|
3037
|
+
const userId = requireAdmin(context);
|
|
3038
|
+
if (!envRepo)
|
|
3039
|
+
throw new Error("Database not available");
|
|
3040
|
+
const name = params.name;
|
|
3041
|
+
const isTest = params.isTest;
|
|
3042
|
+
const apiServer = params.apiServer;
|
|
3043
|
+
const rawAllowedServers = params.allowedServers;
|
|
3044
|
+
const allowedServers = Array.isArray(rawAllowedServers) ? rawAllowedServers.join(", ") : rawAllowedServers;
|
|
3045
|
+
const defaultKubeconfig = params.defaultKubeconfig;
|
|
3046
|
+
if (!name)
|
|
3047
|
+
throw new Error("Missing required param: name");
|
|
3048
|
+
if (!apiServer)
|
|
3049
|
+
throw new Error("Missing required param: apiServer");
|
|
3050
|
+
// Require explicit port in apiServer (e.g. https://host:6443)
|
|
3051
|
+
try {
|
|
3052
|
+
const u = new URL(apiServer.includes("://") ? apiServer : `https://${apiServer}`);
|
|
3053
|
+
if (!u.port)
|
|
3054
|
+
throw new Error("API Server must include an explicit port (e.g. https://host:6443)");
|
|
3055
|
+
}
|
|
3056
|
+
catch (e) {
|
|
3057
|
+
if (e instanceof Error && e.message.startsWith("API Server"))
|
|
3058
|
+
throw e;
|
|
3059
|
+
throw new Error("API Server must be a valid URL with an explicit port (e.g. https://host:6443)");
|
|
3060
|
+
}
|
|
3061
|
+
// defaultKubeconfig only accepted for test environments
|
|
3062
|
+
if (defaultKubeconfig && !isTest) {
|
|
3063
|
+
throw new Error("defaultKubeconfig can only be set for test environments");
|
|
3064
|
+
}
|
|
3065
|
+
const id = await envRepo.save({ name, isTest, apiServer, allowedServers: allowedServers || null, defaultKubeconfig: defaultKubeconfig ?? null }, userId);
|
|
3066
|
+
return { id, name };
|
|
3067
|
+
});
|
|
3068
|
+
methods.set("environment.update", async (params, context) => {
|
|
3069
|
+
requireAdmin(context);
|
|
3070
|
+
if (!envRepo)
|
|
3071
|
+
throw new Error("Database not available");
|
|
3072
|
+
const id = params.id;
|
|
3073
|
+
if (!id)
|
|
3074
|
+
throw new Error("Missing required param: id");
|
|
3075
|
+
const existing = await envRepo.getById(id);
|
|
3076
|
+
if (!existing)
|
|
3077
|
+
throw new Error("Environment not found");
|
|
3078
|
+
const name = params.name ?? existing.name;
|
|
3079
|
+
const apiServer = params.apiServer ?? existing.apiServer;
|
|
3080
|
+
const isTest = params.isTest !== undefined ? params.isTest : existing.isTest;
|
|
3081
|
+
const rawAllowed = params.allowedServers;
|
|
3082
|
+
const allowedServers = rawAllowed !== undefined
|
|
3083
|
+
? (Array.isArray(rawAllowed) ? rawAllowed.join(", ") : rawAllowed)
|
|
3084
|
+
: existing.allowedServers;
|
|
3085
|
+
let defaultKubeconfig = params.defaultKubeconfig !== undefined ? params.defaultKubeconfig : existing.defaultKubeconfig;
|
|
3086
|
+
if (!apiServer?.trim()) {
|
|
3087
|
+
throw new Error("apiServer must be a non-empty string");
|
|
3088
|
+
}
|
|
3089
|
+
// Require explicit port in apiServer (e.g. https://host:6443)
|
|
3090
|
+
try {
|
|
3091
|
+
const u = new URL(apiServer.includes("://") ? apiServer : `https://${apiServer}`);
|
|
3092
|
+
if (!u.port)
|
|
3093
|
+
throw new Error("API Server must include an explicit port (e.g. https://host:6443)");
|
|
3094
|
+
}
|
|
3095
|
+
catch (e) {
|
|
3096
|
+
if (e instanceof Error && e.message.startsWith("API Server"))
|
|
3097
|
+
throw e;
|
|
3098
|
+
throw new Error("API Server must be a valid URL with an explicit port (e.g. https://host:6443)");
|
|
3099
|
+
}
|
|
3100
|
+
// If promoting from test to prod, auto-clear defaultKubeconfig
|
|
3101
|
+
if (existing.isTest && !isTest) {
|
|
3102
|
+
defaultKubeconfig = null;
|
|
3103
|
+
}
|
|
3104
|
+
// defaultKubeconfig only valid for test environments
|
|
3105
|
+
if (defaultKubeconfig && !isTest) {
|
|
3106
|
+
throw new Error("defaultKubeconfig can only be set for test environments");
|
|
3107
|
+
}
|
|
3108
|
+
const oldApiServer = existing.apiServer;
|
|
3109
|
+
await envRepo.save({ id, name, isTest, apiServer, allowedServers, defaultKubeconfig });
|
|
3110
|
+
// If apiServer changed, invalidate mismatched user kubeconfigs
|
|
3111
|
+
if (userEnvConfigRepo && apiServer !== oldApiServer) {
|
|
3112
|
+
const fullConfigs = await userEnvConfigRepo.listFullForEnv(id);
|
|
3113
|
+
const affectedUserIds = new Set();
|
|
3114
|
+
for (const cfg of fullConfigs) {
|
|
3115
|
+
try {
|
|
3116
|
+
const parsed = yaml.load(cfg.kubeconfig);
|
|
3117
|
+
const clusters = parsed?.clusters ?? [];
|
|
3118
|
+
const servers = clusters.map((c) => c.cluster?.server).filter(Boolean);
|
|
3119
|
+
const matches = servers.some((s) => apiServerHostMatch(s, apiServer));
|
|
3120
|
+
if (!matches) {
|
|
3121
|
+
await userEnvConfigRepo.remove(cfg.userId, id);
|
|
3122
|
+
affectedUserIds.add(cfg.userId);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
catch {
|
|
3126
|
+
// If kubeconfig can't be parsed, remove it as invalid
|
|
3127
|
+
await userEnvConfigRepo.remove(cfg.userId, id);
|
|
3128
|
+
affectedUserIds.add(cfg.userId);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
// Push updated credentials to affected users
|
|
3132
|
+
for (const uid of affectedUserIds) {
|
|
3133
|
+
pushCredentialsToUser(uid);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
return { status: "updated" };
|
|
3137
|
+
});
|
|
3138
|
+
methods.set("environment.delete", async (params, context) => {
|
|
3139
|
+
requireAdmin(context);
|
|
3140
|
+
if (!envRepo)
|
|
3141
|
+
throw new Error("Database not available");
|
|
3142
|
+
const id = params.id;
|
|
3143
|
+
if (!id)
|
|
3144
|
+
throw new Error("Missing required param: id");
|
|
3145
|
+
const existing = await envRepo.getById(id);
|
|
3146
|
+
if (!existing)
|
|
3147
|
+
throw new Error("Environment not found");
|
|
3148
|
+
// Collect affected users before cleanup
|
|
3149
|
+
const affectedUserIds = new Set();
|
|
3150
|
+
if (userEnvConfigRepo) {
|
|
3151
|
+
const envConfigs = await userEnvConfigRepo.listForEnv(id);
|
|
3152
|
+
for (const c of envConfigs)
|
|
3153
|
+
affectedUserIds.add(c.userId);
|
|
3154
|
+
await userEnvConfigRepo.removeAllForEnv(id);
|
|
3155
|
+
}
|
|
3156
|
+
await envRepo.delete(id);
|
|
3157
|
+
// Push updated credentials to all affected users
|
|
3158
|
+
for (const uid of affectedUserIds) {
|
|
3159
|
+
pushCredentialsToUser(uid);
|
|
3160
|
+
}
|
|
2753
3161
|
return { status: "deleted" };
|
|
2754
3162
|
});
|
|
2755
3163
|
// ─────────────────────────────────────────────────
|
|
3164
|
+
// User Environment Config Methods (kubeconfig upload)
|
|
3165
|
+
// ─────────────────────────────────────────────────
|
|
3166
|
+
methods.set("userEnvConfig.list", async (_params, context) => {
|
|
3167
|
+
const userId = requireAuth(context);
|
|
3168
|
+
if (!envRepo || !userEnvConfigRepo)
|
|
3169
|
+
return { configs: [] };
|
|
3170
|
+
// Fetch all environments and user's configs
|
|
3171
|
+
const allEnvs = await envRepo.list();
|
|
3172
|
+
const userConfigs = await userEnvConfigRepo.listForUser(userId);
|
|
3173
|
+
// Check if user is testOnly
|
|
3174
|
+
let isTestOnly = false;
|
|
3175
|
+
if (userRepo) {
|
|
3176
|
+
const dbUser = await userRepo.getById(userId);
|
|
3177
|
+
isTestOnly = dbUser?.testOnly ?? false;
|
|
3178
|
+
}
|
|
3179
|
+
// Filter out production environments for testOnly users
|
|
3180
|
+
const visibleEnvs = isTestOnly ? allEnvs.filter((e) => e.isTest) : allEnvs;
|
|
3181
|
+
const configMap = new Map(userConfigs.map((c) => [c.envId, c]));
|
|
3182
|
+
return {
|
|
3183
|
+
configs: visibleEnvs.map((env) => ({
|
|
3184
|
+
envId: env.id,
|
|
3185
|
+
envName: env.name,
|
|
3186
|
+
isTest: env.isTest,
|
|
3187
|
+
apiServer: env.apiServer,
|
|
3188
|
+
hasKubeconfig: configMap.has(env.id),
|
|
3189
|
+
updatedAt: configMap.get(env.id)?.updatedAt ?? null,
|
|
3190
|
+
})),
|
|
3191
|
+
};
|
|
3192
|
+
});
|
|
3193
|
+
methods.set("userEnvConfig.set", async (params, context) => {
|
|
3194
|
+
const userId = requireAuth(context);
|
|
3195
|
+
if (!envRepo || !userEnvConfigRepo)
|
|
3196
|
+
throw new Error("Database not available");
|
|
3197
|
+
const envId = params.envId;
|
|
3198
|
+
const kubeconfig = params.kubeconfig;
|
|
3199
|
+
if (!envId)
|
|
3200
|
+
throw new Error("Missing required param: envId");
|
|
3201
|
+
if (!kubeconfig)
|
|
3202
|
+
throw new Error("Missing required param: kubeconfig");
|
|
3203
|
+
// Fetch environment
|
|
3204
|
+
const env = await envRepo.getById(envId);
|
|
3205
|
+
if (!env)
|
|
3206
|
+
throw new Error("Environment not found");
|
|
3207
|
+
// testOnly user check
|
|
3208
|
+
if (userRepo) {
|
|
3209
|
+
const dbUser = await userRepo.getById(userId);
|
|
3210
|
+
if (dbUser?.testOnly && !env.isTest) {
|
|
3211
|
+
throw new Error("Test-only users cannot configure production environments");
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
// Parse and validate kubeconfig YAML
|
|
3215
|
+
let parsed;
|
|
3216
|
+
try {
|
|
3217
|
+
parsed = yaml.load(kubeconfig);
|
|
3218
|
+
}
|
|
3219
|
+
catch {
|
|
3220
|
+
throw new Error("Invalid kubeconfig: YAML parse error");
|
|
3221
|
+
}
|
|
3222
|
+
// Validate apiServer appears in kubeconfig clusters
|
|
3223
|
+
const clusters = parsed?.clusters ?? [];
|
|
3224
|
+
const servers = clusters.map((c) => c.cluster?.server).filter(Boolean);
|
|
3225
|
+
if (!servers.some((s) => apiServerHostMatch(s, env.apiServer))) {
|
|
3226
|
+
throw new Error(`Kubeconfig does not contain a cluster matching apiServer "${env.apiServer}"`);
|
|
3227
|
+
}
|
|
3228
|
+
await userEnvConfigRepo.set(userId, envId, kubeconfig);
|
|
3229
|
+
pushCredentialsToUser(userId);
|
|
3230
|
+
return { status: "saved" };
|
|
3231
|
+
});
|
|
3232
|
+
methods.set("userEnvConfig.remove", async (params, context) => {
|
|
3233
|
+
const userId = requireAuth(context);
|
|
3234
|
+
if (!userEnvConfigRepo)
|
|
3235
|
+
throw new Error("Database not available");
|
|
3236
|
+
const envId = params.envId;
|
|
3237
|
+
if (!envId)
|
|
3238
|
+
throw new Error("Missing required param: envId");
|
|
3239
|
+
await userEnvConfigRepo.remove(userId, envId);
|
|
3240
|
+
pushCredentialsToUser(userId);
|
|
3241
|
+
return { status: "removed" };
|
|
3242
|
+
});
|
|
3243
|
+
// ─────────────────────────────────────────────────
|
|
2756
3244
|
// Workspace Methods
|
|
2757
3245
|
// ─────────────────────────────────────────────────
|
|
2758
3246
|
methods.set("workspace.list", async (_params, context) => {
|
|
@@ -2762,7 +3250,14 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2762
3250
|
let list = await workspaceRepo.list(userId);
|
|
2763
3251
|
// Auto-create default workspace if none exists
|
|
2764
3252
|
if (list.length === 0) {
|
|
2765
|
-
|
|
3253
|
+
// testOnly users get a test-type default workspace
|
|
3254
|
+
let defaultEnvType;
|
|
3255
|
+
if (userRepo) {
|
|
3256
|
+
const dbUser = await userRepo.getById(userId);
|
|
3257
|
+
if (dbUser?.testOnly)
|
|
3258
|
+
defaultEnvType = "test";
|
|
3259
|
+
}
|
|
3260
|
+
await workspaceRepo.getOrCreateDefault(userId, defaultEnvType);
|
|
2766
3261
|
list = await workspaceRepo.list(userId);
|
|
2767
3262
|
}
|
|
2768
3263
|
return { workspaces: list };
|
|
@@ -2774,8 +3269,18 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2774
3269
|
const name = params.name;
|
|
2775
3270
|
if (!name)
|
|
2776
3271
|
throw new Error("Missing required param: name");
|
|
3272
|
+
// Determine envType — testOnly users forced to "test"
|
|
3273
|
+
let envType = params.envType ?? "prod";
|
|
3274
|
+
if (envType !== "prod" && envType !== "test") {
|
|
3275
|
+
throw new Error("envType must be 'prod' or 'test'");
|
|
3276
|
+
}
|
|
3277
|
+
if (userRepo) {
|
|
3278
|
+
const dbUser = await userRepo.getById(userId);
|
|
3279
|
+
if (dbUser?.testOnly)
|
|
3280
|
+
envType = "test";
|
|
3281
|
+
}
|
|
2777
3282
|
const config = params.config;
|
|
2778
|
-
const ws = await workspaceRepo.create(userId, name, config);
|
|
3283
|
+
const ws = await workspaceRepo.create(userId, name, config, envType);
|
|
2779
3284
|
// Build workspace skills directory
|
|
2780
3285
|
await syncWorkspaceSkills(userId, ws.id, ws.isDefault, [], []);
|
|
2781
3286
|
return { workspace: ws };
|
|
@@ -2796,6 +3301,31 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2796
3301
|
updates.name = params.name;
|
|
2797
3302
|
if (params.config !== undefined)
|
|
2798
3303
|
updates.configJson = params.config;
|
|
3304
|
+
if (params.envType !== undefined) {
|
|
3305
|
+
const envType = params.envType;
|
|
3306
|
+
if (envType !== "prod" && envType !== "test") {
|
|
3307
|
+
throw new Error("envType must be 'prod' or 'test'");
|
|
3308
|
+
}
|
|
3309
|
+
// testOnly users cannot set envType to "prod"
|
|
3310
|
+
if (userRepo) {
|
|
3311
|
+
const dbUser = await userRepo.getById(userId);
|
|
3312
|
+
if (dbUser?.testOnly && envType === "prod") {
|
|
3313
|
+
throw new Error("Test-only users cannot create production workspaces");
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
// If changing to "test", verify all bound environments are test
|
|
3317
|
+
if (params.envType === "test" && envRepo && workspaceRepo) {
|
|
3318
|
+
const boundEnvIds = await workspaceRepo.getEnvironments(id);
|
|
3319
|
+
if (boundEnvIds.length > 0) {
|
|
3320
|
+
const boundEnvs = await envRepo.listByIds(boundEnvIds);
|
|
3321
|
+
const nonTest = boundEnvs.filter((e) => !e.isTest);
|
|
3322
|
+
if (nonTest.length > 0) {
|
|
3323
|
+
throw new Error(`Cannot change to test type: workspace has ${nonTest.length} non-test environment(s) bound. Unbind them first.`);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
updates.envType = params.envType;
|
|
3328
|
+
}
|
|
2799
3329
|
await workspaceRepo.update(id, updates);
|
|
2800
3330
|
return { status: "updated" };
|
|
2801
3331
|
});
|
|
@@ -2827,16 +3357,25 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2827
3357
|
const ws = await workspaceRepo.getById(id);
|
|
2828
3358
|
if (!ws || ws.userId !== userId)
|
|
2829
3359
|
throw new Error("Workspace not found");
|
|
2830
|
-
const [wsSkills, wsTools, wsCreds] = await Promise.all([
|
|
3360
|
+
const [wsSkills, wsTools, wsCreds, wsEnvIds] = await Promise.all([
|
|
2831
3361
|
workspaceRepo.getSkills(id),
|
|
2832
3362
|
workspaceRepo.getTools(id),
|
|
2833
3363
|
workspaceRepo.getCredentials(id),
|
|
3364
|
+
workspaceRepo.getEnvironments(id),
|
|
2834
3365
|
]);
|
|
3366
|
+
// Fetch full environment details for bound environments
|
|
3367
|
+
let envDetails = [];
|
|
3368
|
+
if (envRepo && wsEnvIds.length > 0) {
|
|
3369
|
+
const envs = await envRepo.listByIds(wsEnvIds);
|
|
3370
|
+
envDetails = envs.map((e) => ({ id: e.id, name: e.name, isTest: e.isTest, apiServer: e.apiServer }));
|
|
3371
|
+
}
|
|
2835
3372
|
return {
|
|
2836
3373
|
workspace: ws,
|
|
2837
3374
|
skills: wsSkills,
|
|
2838
3375
|
tools: wsTools,
|
|
2839
3376
|
credentials: wsCreds,
|
|
3377
|
+
environments: wsEnvIds,
|
|
3378
|
+
environmentDetails: envDetails,
|
|
2840
3379
|
};
|
|
2841
3380
|
});
|
|
2842
3381
|
methods.set("workspace.setSkills", async (params, context) => {
|
|
@@ -2903,6 +3442,50 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2903
3442
|
}
|
|
2904
3443
|
}
|
|
2905
3444
|
await workspaceRepo.setCredentials(workspaceId, credentialIds);
|
|
3445
|
+
// Push updated credentials to running AgentBox for this workspace
|
|
3446
|
+
pushCredentialsToUser(userId);
|
|
3447
|
+
return { status: "updated" };
|
|
3448
|
+
});
|
|
3449
|
+
methods.set("workspace.setEnvironments", async (params, context) => {
|
|
3450
|
+
const userId = requireAuth(context);
|
|
3451
|
+
if (!workspaceRepo)
|
|
3452
|
+
throw new Error("Database not available");
|
|
3453
|
+
const workspaceId = params.workspaceId;
|
|
3454
|
+
const envIds = params.envIds;
|
|
3455
|
+
if (!workspaceId || !Array.isArray(envIds))
|
|
3456
|
+
throw new Error("Missing required params");
|
|
3457
|
+
const ws = await workspaceRepo.getById(workspaceId);
|
|
3458
|
+
if (!ws || ws.userId !== userId)
|
|
3459
|
+
throw new Error("Workspace not found");
|
|
3460
|
+
// Validate: if workspace is test, all bound environments must be test
|
|
3461
|
+
if (envIds.length > 0 && !envRepo) {
|
|
3462
|
+
throw new Error("Environment database not available");
|
|
3463
|
+
}
|
|
3464
|
+
if (envIds.length > 0 && envRepo) {
|
|
3465
|
+
const envs = await envRepo.listByIds(envIds);
|
|
3466
|
+
if (envs.length !== envIds.length) {
|
|
3467
|
+
throw new Error("One or more environments not found");
|
|
3468
|
+
}
|
|
3469
|
+
if (ws.envType === "test") {
|
|
3470
|
+
const nonTest = envs.filter((e) => !e.isTest);
|
|
3471
|
+
if (nonTest.length > 0) {
|
|
3472
|
+
throw new Error("Test workspaces can only bind test environments");
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
// testOnly user check
|
|
3476
|
+
if (userRepo) {
|
|
3477
|
+
const dbUser = await userRepo.getById(userId);
|
|
3478
|
+
if (dbUser?.testOnly) {
|
|
3479
|
+
const nonTest = envs.filter((e) => !e.isTest);
|
|
3480
|
+
if (nonTest.length > 0) {
|
|
3481
|
+
throw new Error("Test-only users cannot bind production environments");
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
await workspaceRepo.setEnvironments(workspaceId, envIds);
|
|
3487
|
+
// Push updated credentials to running AgentBox for this workspace
|
|
3488
|
+
pushCredentialsToUser(userId);
|
|
2906
3489
|
return { status: "updated" };
|
|
2907
3490
|
});
|
|
2908
3491
|
methods.set("workspace.availableTools", async (_params, context) => {
|
|
@@ -2928,8 +3511,12 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2928
3511
|
});
|
|
2929
3512
|
/**
|
|
2930
3513
|
* Build credential payload for a workspace.
|
|
2931
|
-
*
|
|
2932
|
-
*
|
|
3514
|
+
*
|
|
3515
|
+
* Kubeconfigs: sourced from environment-bound userEnvConfigs (NOT credentials table).
|
|
3516
|
+
* Other credentials: from credentials table, filtered by workspace envType.
|
|
3517
|
+
* - prod workspace: all workspace-linked credentials
|
|
3518
|
+
* - test workspace: NO non-kubeconfig credentials (SSH, API tokens hidden)
|
|
3519
|
+
*
|
|
2933
3520
|
* Returns data only — does NOT write to disk. Agentbox materializes files locally.
|
|
2934
3521
|
*/
|
|
2935
3522
|
async function buildCredentialPayload(userId, workspaceId, isDefault) {
|
|
@@ -2938,16 +3525,83 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2938
3525
|
if (!fs.existsSync(agentDataDir)) {
|
|
2939
3526
|
fs.mkdirSync(agentDataDir, { recursive: true });
|
|
2940
3527
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
// Determine
|
|
3528
|
+
const manifest = [];
|
|
3529
|
+
const files = [];
|
|
3530
|
+
// ── Step 1: Determine workspace envType ──
|
|
3531
|
+
let envType = "prod";
|
|
3532
|
+
if (workspaceRepo) {
|
|
3533
|
+
const ws = await workspaceRepo.getById(workspaceId);
|
|
3534
|
+
if (ws)
|
|
3535
|
+
envType = ws.envType ?? "prod";
|
|
3536
|
+
}
|
|
3537
|
+
// ── Step 2: Kubeconfigs from environments ──
|
|
3538
|
+
if (envRepo && userEnvConfigRepo) {
|
|
3539
|
+
// Default workspace: all environments; non-default: only workspace-bound
|
|
3540
|
+
let envs;
|
|
3541
|
+
if (isDefault) {
|
|
3542
|
+
envs = await envRepo.list();
|
|
3543
|
+
}
|
|
3544
|
+
else if (workspaceRepo) {
|
|
3545
|
+
const boundEnvIds = await workspaceRepo.getEnvironments(workspaceId);
|
|
3546
|
+
envs = boundEnvIds.length > 0 ? await envRepo.listByIds(boundEnvIds) : [];
|
|
3547
|
+
}
|
|
3548
|
+
else {
|
|
3549
|
+
envs = [];
|
|
3550
|
+
}
|
|
3551
|
+
if (envs.length > 0) {
|
|
3552
|
+
for (const env of envs) {
|
|
3553
|
+
// Runtime filter: test workspace skips prod environments
|
|
3554
|
+
if (envType === "test" && !env.isTest)
|
|
3555
|
+
continue;
|
|
3556
|
+
// Get user's kubeconfig for this environment
|
|
3557
|
+
const userConfig = await userEnvConfigRepo.get(userId, env.id);
|
|
3558
|
+
let kubeconfigContent = userConfig?.kubeconfig ?? null;
|
|
3559
|
+
// Fallback to defaultKubeconfig for test environments
|
|
3560
|
+
if (!kubeconfigContent && env.isTest && env.defaultKubeconfig) {
|
|
3561
|
+
kubeconfigContent = env.defaultKubeconfig;
|
|
3562
|
+
}
|
|
3563
|
+
if (kubeconfigContent) {
|
|
3564
|
+
const safeName = env.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3565
|
+
const filename = `${safeName}.kubeconfig`;
|
|
3566
|
+
files.push({ name: filename, content: kubeconfigContent });
|
|
3567
|
+
const fileNames = [filename];
|
|
3568
|
+
let metadata;
|
|
3569
|
+
try {
|
|
3570
|
+
const kc = yaml.load(kubeconfigContent);
|
|
3571
|
+
const clusters = kc?.clusters ?? [];
|
|
3572
|
+
const contexts = kc?.contexts ?? [];
|
|
3573
|
+
metadata = {
|
|
3574
|
+
clusters: clusters.map((c) => ({ name: c.name, server: c.cluster?.server })),
|
|
3575
|
+
contexts: contexts.map((c) => ({ name: c.name, cluster: c.context?.cluster, namespace: c.context?.namespace })),
|
|
3576
|
+
currentContext: kc?.["current-context"],
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
catch {
|
|
3580
|
+
// ignore parse errors
|
|
3581
|
+
}
|
|
3582
|
+
manifest.push({
|
|
3583
|
+
name: env.name,
|
|
3584
|
+
type: "kubeconfig",
|
|
3585
|
+
description: `Kubeconfig for environment: ${env.name}`,
|
|
3586
|
+
files: fileNames,
|
|
3587
|
+
...(metadata ? { metadata } : {}),
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
// ── Step 3: Non-kubeconfig credentials from credentials table ──
|
|
3594
|
+
// Test workspaces get NO non-kubeconfig credentials
|
|
3595
|
+
if (envType === "test" || !credRepo) {
|
|
3596
|
+
return { manifest, files };
|
|
3597
|
+
}
|
|
2944
3598
|
let creds;
|
|
2945
3599
|
if (isDefault) {
|
|
2946
3600
|
creds = await credRepo.listForUser(userId);
|
|
2947
3601
|
}
|
|
2948
3602
|
else {
|
|
2949
3603
|
if (!workspaceRepo)
|
|
2950
|
-
return { manifest
|
|
3604
|
+
return { manifest, files };
|
|
2951
3605
|
const linkedIds = await workspaceRepo.getCredentials(workspaceId);
|
|
2952
3606
|
if (linkedIds.length === 0) {
|
|
2953
3607
|
creds = [];
|
|
@@ -2958,43 +3612,15 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2958
3612
|
}
|
|
2959
3613
|
// IdentityFile path used inside agentbox (resolves to .siclaw/credentials/)
|
|
2960
3614
|
const credsDirInBox = path.resolve(process.cwd(), ".siclaw/credentials");
|
|
2961
|
-
const manifest = [];
|
|
2962
|
-
const files = [];
|
|
2963
3615
|
for (const cred of creds) {
|
|
3616
|
+
// Skip kubeconfig type credentials (now handled via environments)
|
|
3617
|
+
if (cred.type === "kubeconfig")
|
|
3618
|
+
continue;
|
|
2964
3619
|
const config = (cred.configJson ?? {});
|
|
2965
3620
|
const safeName = cred.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2966
3621
|
const fileNames = [];
|
|
2967
3622
|
let metadata;
|
|
2968
3623
|
switch (cred.type) {
|
|
2969
|
-
case "kubeconfig": {
|
|
2970
|
-
const content = config.content;
|
|
2971
|
-
if (content) {
|
|
2972
|
-
const filename = `${safeName}.kubeconfig`;
|
|
2973
|
-
files.push({ name: filename, content });
|
|
2974
|
-
fileNames.push(filename);
|
|
2975
|
-
try {
|
|
2976
|
-
const kc = yaml.load(content);
|
|
2977
|
-
const clusters = kc?.clusters ?? [];
|
|
2978
|
-
const contexts = kc?.contexts ?? [];
|
|
2979
|
-
metadata = {
|
|
2980
|
-
clusters: clusters.map((c) => ({
|
|
2981
|
-
name: c.name,
|
|
2982
|
-
server: c.cluster?.server,
|
|
2983
|
-
})),
|
|
2984
|
-
contexts: contexts.map((c) => ({
|
|
2985
|
-
name: c.name,
|
|
2986
|
-
cluster: c.context?.cluster,
|
|
2987
|
-
namespace: c.context?.namespace,
|
|
2988
|
-
})),
|
|
2989
|
-
currentContext: kc?.["current-context"],
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
catch {
|
|
2993
|
-
// ignore parse errors
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
break;
|
|
2997
|
-
}
|
|
2998
3624
|
case "ssh_key": {
|
|
2999
3625
|
const privateKey = config.privateKey;
|
|
3000
3626
|
if (privateKey) {
|
|
@@ -3124,11 +3750,17 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3124
3750
|
const sessions = await chatRepo.listSessions(userId, 1);
|
|
3125
3751
|
sessionCount = sessions.length;
|
|
3126
3752
|
}
|
|
3127
|
-
// PROFILE.md exists?
|
|
3753
|
+
// PROFILE.md exists with meaningful (non-skeleton) content?
|
|
3754
|
+
// A skeleton PROFILE.md (all TBD) doesn't count — the user still needs onboarding.
|
|
3128
3755
|
const userDataDir = process.env.SICLAW_USER_DATA_DIR || ".siclaw/user-data";
|
|
3129
3756
|
const profilePath = path.resolve(userDataDir, "memory", "PROFILE.md");
|
|
3130
|
-
|
|
3131
|
-
|
|
3757
|
+
let hasProfile = false;
|
|
3758
|
+
if (fs.existsSync(profilePath)) {
|
|
3759
|
+
const content = fs.readFileSync(profilePath, "utf-8");
|
|
3760
|
+
// Profile is "real" if Name field has been filled (not TBD)
|
|
3761
|
+
hasProfile = /\*\*Name\*\*:\s*(?!TBD).+/i.test(content);
|
|
3762
|
+
}
|
|
3763
|
+
// Credentials by type count (SSH/API + kubeconfigs)
|
|
3132
3764
|
const credentials = {};
|
|
3133
3765
|
if (credRepo) {
|
|
3134
3766
|
const creds = await credRepo.listForUser(userId);
|
|
@@ -3136,6 +3768,20 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3136
3768
|
credentials[c.type] = (credentials[c.type] || 0) + 1;
|
|
3137
3769
|
}
|
|
3138
3770
|
}
|
|
3771
|
+
if (envRepo && userEnvConfigRepo) {
|
|
3772
|
+
const allEnvs = await envRepo.list();
|
|
3773
|
+
let kubeconfigCount = 0;
|
|
3774
|
+
for (const env of allEnvs) {
|
|
3775
|
+
// Count if user has a personal kubeconfig, OR if it's a test env with a default kubeconfig
|
|
3776
|
+
const userConfig = await userEnvConfigRepo.get(userId, env.id);
|
|
3777
|
+
if (userConfig?.kubeconfig || (env.isTest && env.defaultKubeconfig)) {
|
|
3778
|
+
kubeconfigCount++;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
if (kubeconfigCount > 0) {
|
|
3782
|
+
credentials["kubeconfig"] = kubeconfigCount;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3139
3785
|
return { hasModels, hasProfile, sessionCount, credentials };
|
|
3140
3786
|
});
|
|
3141
3787
|
// ─────────────────────────────────────────────────
|
|
@@ -3156,7 +3802,8 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3156
3802
|
const values = params.values;
|
|
3157
3803
|
const ALLOWED_SECTIONS = {
|
|
3158
3804
|
sso: ["sso.enabled", "sso.issuer", "sso.clientId", "sso.clientSecret", "sso.redirectUri"],
|
|
3159
|
-
system: ["system.
|
|
3805
|
+
system: ["system.grafanaUrl"],
|
|
3806
|
+
metrics: ["metrics.port", "metrics.token", "metrics.includeUserId"],
|
|
3160
3807
|
};
|
|
3161
3808
|
const allowedKeys = ALLOWED_SECTIONS[section];
|
|
3162
3809
|
if (!allowedKeys)
|
|
@@ -3170,29 +3817,181 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3170
3817
|
}
|
|
3171
3818
|
}
|
|
3172
3819
|
await sysConfigRepo.setMany(entries);
|
|
3173
|
-
// Apply agentbox image change at runtime
|
|
3174
|
-
if (section === "system" && entries["system.agentboxImage"]) {
|
|
3175
|
-
agentBoxManager.setSpawnerImage(entries["system.agentboxImage"]);
|
|
3176
|
-
}
|
|
3177
3820
|
return { ok: true };
|
|
3178
3821
|
});
|
|
3179
|
-
/** Build a skill bundle for a given user and environment (used by mTLS bundle API)
|
|
3822
|
+
/** Build a skill bundle for a given user and environment (used by mTLS bundle API).
|
|
3823
|
+
* "test" maps to "dev" behavior (working copies of personal skills). */
|
|
3180
3824
|
async function getSkillBundle(userId, env) {
|
|
3181
3825
|
if (!skillRepo || !skillContentRepo)
|
|
3182
3826
|
throw new Error("Database not available");
|
|
3183
3827
|
const disabled = new Set(await skillRepo.listDisabledSkills(userId));
|
|
3184
|
-
|
|
3828
|
+
// Map "test" → "dev" for skill bundle purposes (test = dev-like skill access)
|
|
3829
|
+
const bundleEnv = env === "test" ? "dev" : env;
|
|
3830
|
+
return buildSkillBundle(userId, bundleEnv, skillWriter, skillRepo, skillContentRepo, disabled);
|
|
3185
3831
|
}
|
|
3186
|
-
/**
|
|
3832
|
+
/** Detach WebSocket from SSE streams — SSE continues so DB persistence and
|
|
3833
|
+
* dpProgressSnapshots keep updating. User reconnect resumes live events. */
|
|
3187
3834
|
function cleanupForWs(ws) {
|
|
3188
3835
|
for (const [key, stream] of activeStreams.entries()) {
|
|
3189
3836
|
if (stream.ws === ws) {
|
|
3190
|
-
console.log(`[rpc]
|
|
3191
|
-
stream.
|
|
3192
|
-
activeStreams.delete(key);
|
|
3837
|
+
console.log(`[rpc] WS detached from SSE stream ${key} — SSE continues`);
|
|
3838
|
+
stream.ws = undefined;
|
|
3193
3839
|
}
|
|
3194
3840
|
}
|
|
3195
3841
|
}
|
|
3842
|
+
// ── Monitoring Dashboard ──
|
|
3843
|
+
methods.set("metrics.timeseries", async (params, context) => {
|
|
3844
|
+
requireAuth(context);
|
|
3845
|
+
if (!metricsAggregator)
|
|
3846
|
+
return { buckets: [], snapshot: { activeSessions: 0, wsConnections: 0 }, topTools: [], topSkills: [] };
|
|
3847
|
+
const range = params.range || "1h";
|
|
3848
|
+
if (range !== "1h" && range !== "6h" && range !== "24h") {
|
|
3849
|
+
throw new Error("Invalid range: must be 1h, 6h, or 24h");
|
|
3850
|
+
}
|
|
3851
|
+
const buckets = metricsAggregator.query(range).map((b) => ({
|
|
3852
|
+
timestamp: b.timestamp,
|
|
3853
|
+
tokensInput: b.tokensInput,
|
|
3854
|
+
tokensOutput: b.tokensOutput,
|
|
3855
|
+
tokensCacheRead: b.tokensCacheRead,
|
|
3856
|
+
tokensCacheWrite: b.tokensCacheWrite,
|
|
3857
|
+
promptCount: b.promptCount,
|
|
3858
|
+
promptErrors: b.promptErrors,
|
|
3859
|
+
promptDurationAvg: b.promptCount + b.promptErrors > 0
|
|
3860
|
+
? b.promptDurationSum / (b.promptCount + b.promptErrors)
|
|
3861
|
+
: 0,
|
|
3862
|
+
promptDurationMax: b.promptDurationMax,
|
|
3863
|
+
activeSessions: b.activeSessions,
|
|
3864
|
+
wsConnections: b.wsConnections,
|
|
3865
|
+
toolCalls: b.toolCalls,
|
|
3866
|
+
toolErrors: b.toolErrors,
|
|
3867
|
+
skillSuccesses: b.skillSuccesses,
|
|
3868
|
+
skillErrors: b.skillErrors,
|
|
3869
|
+
}));
|
|
3870
|
+
return {
|
|
3871
|
+
buckets,
|
|
3872
|
+
snapshot: metricsAggregator.snapshot(),
|
|
3873
|
+
topTools: metricsAggregator.topTools(10),
|
|
3874
|
+
topSkills: metricsAggregator.topSkills(10),
|
|
3875
|
+
};
|
|
3876
|
+
});
|
|
3877
|
+
methods.set("metrics.summary", async (params, context) => {
|
|
3878
|
+
requireAuth(context);
|
|
3879
|
+
if (!db)
|
|
3880
|
+
return { totalTokens: 0, totalPrompts: 0, totalSessions: 0, tokenBreakdown: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, byModel: [] };
|
|
3881
|
+
const period = params.period || "today";
|
|
3882
|
+
const now = new Date();
|
|
3883
|
+
let cutoffMs;
|
|
3884
|
+
if (period === "7d") {
|
|
3885
|
+
cutoffMs = now.getTime() - 7 * 86_400_000;
|
|
3886
|
+
}
|
|
3887
|
+
else if (period === "30d") {
|
|
3888
|
+
cutoffMs = now.getTime() - 30 * 86_400_000;
|
|
3889
|
+
}
|
|
3890
|
+
else {
|
|
3891
|
+
// "today" — UTC start of day
|
|
3892
|
+
cutoffMs = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())).getTime();
|
|
3893
|
+
}
|
|
3894
|
+
const rows = await db.select({
|
|
3895
|
+
provider: sessionStats.provider,
|
|
3896
|
+
model: sessionStats.model,
|
|
3897
|
+
session_count: count(),
|
|
3898
|
+
total_input: sum(sessionStats.inputTokens),
|
|
3899
|
+
total_output: sum(sessionStats.outputTokens),
|
|
3900
|
+
total_cache_read: sum(sessionStats.cacheReadTokens),
|
|
3901
|
+
total_cache_write: sum(sessionStats.cacheWriteTokens),
|
|
3902
|
+
total_prompts: sum(sessionStats.promptCount),
|
|
3903
|
+
})
|
|
3904
|
+
.from(sessionStats)
|
|
3905
|
+
.where(gte(sessionStats.createdAt, cutoffMs))
|
|
3906
|
+
.groupBy(sessionStats.provider, sessionStats.model)
|
|
3907
|
+
.orderBy(sql `(SUM(${sessionStats.inputTokens}) + SUM(${sessionStats.outputTokens})) DESC`);
|
|
3908
|
+
let totalTokens = 0;
|
|
3909
|
+
let totalPrompts = 0;
|
|
3910
|
+
let totalSessions = 0;
|
|
3911
|
+
const tokenBreakdown = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
3912
|
+
const byModel = [];
|
|
3913
|
+
for (const row of rows) {
|
|
3914
|
+
const input = Number(row.total_input) || 0;
|
|
3915
|
+
const output = Number(row.total_output) || 0;
|
|
3916
|
+
const cacheRead = Number(row.total_cache_read) || 0;
|
|
3917
|
+
const cacheWrite = Number(row.total_cache_write) || 0;
|
|
3918
|
+
const tokens = input + output;
|
|
3919
|
+
const prompts = Number(row.total_prompts) || 0;
|
|
3920
|
+
const sessions = Number(row.session_count) || 0;
|
|
3921
|
+
totalTokens += tokens;
|
|
3922
|
+
totalPrompts += prompts;
|
|
3923
|
+
totalSessions += sessions;
|
|
3924
|
+
tokenBreakdown.input += input;
|
|
3925
|
+
tokenBreakdown.output += output;
|
|
3926
|
+
tokenBreakdown.cacheRead += cacheRead;
|
|
3927
|
+
tokenBreakdown.cacheWrite += cacheWrite;
|
|
3928
|
+
byModel.push({
|
|
3929
|
+
provider: row.provider || "unknown",
|
|
3930
|
+
model: row.model || "unknown",
|
|
3931
|
+
tokens,
|
|
3932
|
+
sessions,
|
|
3933
|
+
percentage: 0, // filled below
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
// Calculate percentages
|
|
3937
|
+
for (const entry of byModel) {
|
|
3938
|
+
entry.percentage = totalTokens > 0 ? Math.round((entry.tokens / totalTokens) * 100) : 0;
|
|
3939
|
+
}
|
|
3940
|
+
return { totalTokens, totalPrompts, totalSessions, tokenBreakdown, byModel };
|
|
3941
|
+
});
|
|
3942
|
+
// ── Audit ──────────────────────────────────────────────────────────
|
|
3943
|
+
methods.set("audit.list", async (params, context) => {
|
|
3944
|
+
const userId = requireAuth(context);
|
|
3945
|
+
if (!chatRepo)
|
|
3946
|
+
throw new Error("Database not available");
|
|
3947
|
+
const p = params;
|
|
3948
|
+
const queryUserId = isAdminUser(context) ? (p.userId || undefined) : userId;
|
|
3949
|
+
const limit = Math.min(p.limit ?? 50, 200);
|
|
3950
|
+
const validOutcomes = ["success", "error", "blocked"];
|
|
3951
|
+
const outcome = p.outcome && validOutcomes.includes(p.outcome) ? p.outcome : undefined;
|
|
3952
|
+
const rows = await chatRepo.queryAuditLogs({
|
|
3953
|
+
userId: queryUserId,
|
|
3954
|
+
userName: isAdminUser(context) ? p.userName : undefined,
|
|
3955
|
+
toolName: p.toolName,
|
|
3956
|
+
outcome,
|
|
3957
|
+
startDate: p.startDate ? Math.floor(new Date(p.startDate).getTime() / 1000) : undefined,
|
|
3958
|
+
endDate: p.endDate ? Math.floor(new Date(p.endDate).getTime() / 1000) : undefined,
|
|
3959
|
+
cursorTs: p.cursorTs,
|
|
3960
|
+
cursorId: p.cursorId,
|
|
3961
|
+
limit,
|
|
3962
|
+
});
|
|
3963
|
+
const hasMore = rows.length > limit;
|
|
3964
|
+
const logs = hasMore ? rows.slice(0, limit) : rows;
|
|
3965
|
+
return { logs, hasMore };
|
|
3966
|
+
});
|
|
3967
|
+
methods.set("audit.detail", async (params, context) => {
|
|
3968
|
+
const userId = requireAuth(context);
|
|
3969
|
+
if (!chatRepo)
|
|
3970
|
+
throw new Error("Database not available");
|
|
3971
|
+
const { messageId } = params;
|
|
3972
|
+
if (!messageId)
|
|
3973
|
+
throw new Error("messageId is required");
|
|
3974
|
+
const msg = await chatRepo.getMessageById(messageId);
|
|
3975
|
+
if (!msg || msg.role !== "tool")
|
|
3976
|
+
throw new Error("Message not found");
|
|
3977
|
+
// Ownership check via session
|
|
3978
|
+
const session = await chatRepo.getSession(msg.sessionId);
|
|
3979
|
+
if (!session)
|
|
3980
|
+
throw new Error("Session not found");
|
|
3981
|
+
if (!isAdminUser(context) && session.userId !== userId) {
|
|
3982
|
+
throw new Error("Forbidden: not your message");
|
|
3983
|
+
}
|
|
3984
|
+
return {
|
|
3985
|
+
id: msg.id,
|
|
3986
|
+
userId: msg.userId,
|
|
3987
|
+
content: msg.content,
|
|
3988
|
+
toolName: msg.toolName,
|
|
3989
|
+
toolInput: msg.toolInput,
|
|
3990
|
+
outcome: msg.outcome,
|
|
3991
|
+
durationMs: msg.durationMs,
|
|
3992
|
+
timestamp: msg.timestamp,
|
|
3993
|
+
};
|
|
3994
|
+
});
|
|
3196
3995
|
return { methods, buildCredentialPayload, getSkillBundle, cleanupForWs };
|
|
3197
3996
|
}
|
|
3198
3997
|
//# sourceMappingURL=rpc-methods.js.map
|