siclaw 0.1.1 → 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 +74 -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.js +6 -5
- 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 +852 -166
- 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-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 +4 -2
- 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
|
@@ -21,6 +21,8 @@ import { ModelConfigRepository } from "./db/repositories/model-config-repo.js";
|
|
|
21
21
|
import { CredentialRepository } from "./db/repositories/credential-repo.js";
|
|
22
22
|
import { WorkspaceRepository } from "./db/repositories/workspace-repo.js";
|
|
23
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";
|
|
24
26
|
import { getLabelsForSkill, batchGetLabels, listAllLabels } from "./skill-labels.js";
|
|
25
27
|
import { McpServerRepository } from "./db/repositories/mcp-server-repo.js";
|
|
26
28
|
import { SkillFileWriter } from "./skills/file-writer.js";
|
|
@@ -29,10 +31,11 @@ import { ScriptEvaluator } from "./skills/script-evaluator.js";
|
|
|
29
31
|
import { SkillVersionRepository } from "./db/repositories/skill-version-repo.js";
|
|
30
32
|
import { createTwoFilesPatch } from "diff";
|
|
31
33
|
import yaml from "js-yaml";
|
|
32
|
-
import { notifyCronService as notifyCronServiceImpl } from "./cron/notify.js";
|
|
33
34
|
import { buildSkillBundle } from "./skills/skill-bundle.js";
|
|
34
35
|
import { buildRedactionConfig, redactText } from "./output-redactor.js";
|
|
35
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";
|
|
36
39
|
function requireAuth(context) {
|
|
37
40
|
const userId = context.auth?.userId;
|
|
38
41
|
if (!userId)
|
|
@@ -41,11 +44,32 @@ function requireAuth(context) {
|
|
|
41
44
|
}
|
|
42
45
|
function requireAdmin(context) {
|
|
43
46
|
const userId = requireAuth(context);
|
|
44
|
-
if (context
|
|
47
|
+
if (!isAdminUser(context))
|
|
45
48
|
throw new Error("Forbidden: admin access required");
|
|
46
49
|
return userId;
|
|
47
50
|
}
|
|
48
|
-
|
|
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) {
|
|
49
73
|
const methods = new Map();
|
|
50
74
|
// Initialize repositories (null-safe — methods check before use)
|
|
51
75
|
const chatRepo = db ? new ChatRepository(db) : null;
|
|
@@ -63,13 +87,15 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
63
87
|
const sysConfigRepo = db ? new SystemConfigRepository(db) : null;
|
|
64
88
|
const mcpRepo = db ? new McpServerRepository(db) : null;
|
|
65
89
|
const skillContentRepo = db ? new SkillContentRepository(db) : null;
|
|
90
|
+
const envRepo = db ? new EnvironmentRepository(db) : null;
|
|
91
|
+
const userEnvConfigRepo = db ? new UserEnvConfigRepository(db) : null;
|
|
66
92
|
const scriptEvaluator = new ScriptEvaluator(modelConfigRepo);
|
|
67
|
-
/** Resolve workspaceId for a session from DB
|
|
93
|
+
/** Resolve workspaceId for a session from DB */
|
|
68
94
|
async function resolveSessionWorkspace(sessionId) {
|
|
69
95
|
if (!chatRepo)
|
|
70
|
-
return
|
|
96
|
+
return undefined;
|
|
71
97
|
const session = await chatRepo.getSession(sessionId);
|
|
72
|
-
return session?.workspaceId ??
|
|
98
|
+
return session?.workspaceId ?? undefined;
|
|
73
99
|
}
|
|
74
100
|
/** Find an AgentBox handle for a user, trying session workspace first, then any active box */
|
|
75
101
|
async function findAgentBoxForSession(userId, sessionId) {
|
|
@@ -120,6 +146,56 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
120
146
|
console.warn(`[resource-notify] All skill reload failed:`, err.message);
|
|
121
147
|
});
|
|
122
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
|
+
}
|
|
123
199
|
// Initialize skills dir
|
|
124
200
|
skillWriter.init()
|
|
125
201
|
.then(async () => {
|
|
@@ -269,7 +345,9 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
269
345
|
workspace = null;
|
|
270
346
|
}
|
|
271
347
|
}
|
|
272
|
-
|
|
348
|
+
if (!workspace)
|
|
349
|
+
throw new Error("Failed to resolve workspace");
|
|
350
|
+
const effectiveWorkspaceId = workspace.id;
|
|
273
351
|
// Ensure session exists in DB
|
|
274
352
|
if (chatRepo) {
|
|
275
353
|
if (sessionId) {
|
|
@@ -296,7 +374,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
296
374
|
});
|
|
297
375
|
}
|
|
298
376
|
if (!sessionId)
|
|
299
|
-
|
|
377
|
+
throw new Error("Failed to create session");
|
|
300
378
|
// Forward model selection and brain type from frontend
|
|
301
379
|
// Use workspace default model if user didn't specify one
|
|
302
380
|
const modelProvider = params.modelProvider ?? workspace?.configJson?.defaultModel?.provider;
|
|
@@ -331,9 +409,12 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
331
409
|
}
|
|
332
410
|
}
|
|
333
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");
|
|
334
414
|
const handle = await agentBoxManager.getOrCreate(userId, effectiveWorkspaceId, {
|
|
335
415
|
workspaceId: effectiveWorkspaceId,
|
|
336
416
|
allowedTools,
|
|
417
|
+
podEnv,
|
|
337
418
|
});
|
|
338
419
|
const client = new AgentBoxClient(handle.endpoint, 30000, agentBoxTlsOptions);
|
|
339
420
|
// Send prompt
|
|
@@ -365,7 +446,9 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
365
446
|
// Async SSE processing
|
|
366
447
|
(async () => {
|
|
367
448
|
let assistantContent = "";
|
|
368
|
-
|
|
449
|
+
// Map keyed by toolName to handle parallel tool calls correctly
|
|
450
|
+
const pendingToolInputs = new Map();
|
|
451
|
+
const pendingToolStartTimes = new Map();
|
|
369
452
|
let sseEventCount = 0;
|
|
370
453
|
const sseStartTime = Date.now();
|
|
371
454
|
// Mark user as having an active prompt so WS teardown won't kill the pod
|
|
@@ -391,15 +474,32 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
391
474
|
.map((c) => c.text ?? "")
|
|
392
475
|
.join("") ?? "";
|
|
393
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) || "";
|
|
394
490
|
dbMessageId = await chatRepo.appendMessage({
|
|
395
491
|
sessionId: result.sessionId,
|
|
396
492
|
role: "tool",
|
|
397
493
|
content: redactText(text, redactionConfig),
|
|
398
494
|
toolName,
|
|
399
|
-
toolInput:
|
|
495
|
+
toolInput: toolInput ? redactText(toolInput, redactionConfig) : undefined,
|
|
496
|
+
userId,
|
|
497
|
+
outcome,
|
|
498
|
+
durationMs,
|
|
400
499
|
});
|
|
401
500
|
await chatRepo.incrementMessageCount(result.sessionId);
|
|
402
|
-
|
|
501
|
+
pendingToolInputs.delete(toolName);
|
|
502
|
+
pendingToolStartTimes.delete(toolName);
|
|
403
503
|
}
|
|
404
504
|
// Forward event to frontend via sendToUser (targets all WS connections
|
|
405
505
|
// for this user, so reconnected sessions also receive live events)
|
|
@@ -469,9 +569,11 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
469
569
|
}
|
|
470
570
|
}
|
|
471
571
|
else if (eventType === "tool_execution_start") {
|
|
472
|
-
// 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";
|
|
473
574
|
const args = eventData.args;
|
|
474
|
-
|
|
575
|
+
pendingToolInputs.set(startToolName, args ? JSON.stringify(args) : "");
|
|
576
|
+
pendingToolStartTimes.set(startToolName, Date.now());
|
|
475
577
|
}
|
|
476
578
|
// tool_execution_end already handled above
|
|
477
579
|
}
|
|
@@ -839,20 +941,20 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
839
941
|
})),
|
|
840
942
|
};
|
|
841
943
|
}
|
|
842
|
-
// Fallback: read
|
|
843
|
-
const mcpConfigPath = path.resolve(process.cwd(), "config", "mcp-servers.json");
|
|
944
|
+
// Fallback: read from settings.json mcpServers (CLI / no-DB mode)
|
|
844
945
|
try {
|
|
845
|
-
const
|
|
846
|
-
const config =
|
|
946
|
+
const { loadConfig } = await import("../core/config.js");
|
|
947
|
+
const config = loadConfig();
|
|
847
948
|
const servers = [];
|
|
848
949
|
for (const [name, serverConfig] of Object.entries(config.mcpServers ?? {})) {
|
|
950
|
+
const cfg = serverConfig;
|
|
849
951
|
servers.push({
|
|
850
952
|
id: name,
|
|
851
953
|
name,
|
|
852
|
-
url:
|
|
853
|
-
transport:
|
|
954
|
+
url: cfg.url,
|
|
955
|
+
transport: cfg.transport ?? (cfg.url ? "streamable-http" : "stdio"),
|
|
854
956
|
enabled: true,
|
|
855
|
-
source: "
|
|
957
|
+
source: "settings",
|
|
856
958
|
});
|
|
857
959
|
}
|
|
858
960
|
return { servers };
|
|
@@ -1001,12 +1103,14 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1001
1103
|
const userId = requireAuth(context);
|
|
1002
1104
|
const sessionId = params.sessionId;
|
|
1003
1105
|
const snapKey = sessionId ? `${userId}:${sessionId}` : userId;
|
|
1106
|
+
const streamKey = sessionId ? `${userId}:${sessionId}` : undefined;
|
|
1107
|
+
const promptActive = streamKey ? activeStreams.has(streamKey) : false;
|
|
1004
1108
|
const snap = dpProgressSnapshots.get(snapKey);
|
|
1005
1109
|
if (!snap || Date.now() - snap.updatedAt > 600_000) {
|
|
1006
1110
|
dpProgressSnapshots.delete(snapKey);
|
|
1007
|
-
return { events: null };
|
|
1111
|
+
return { events: null, promptActive };
|
|
1008
1112
|
}
|
|
1009
|
-
return { sessionId: snap.sessionId, events: snap.events };
|
|
1113
|
+
return { sessionId: snap.sessionId, events: snap.events, promptActive };
|
|
1010
1114
|
});
|
|
1011
1115
|
methods.set("chat.history", async (params, context) => {
|
|
1012
1116
|
const userId = requireAuth(context);
|
|
@@ -1056,34 +1160,27 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1056
1160
|
// ─────────────────────────────────────────────────
|
|
1057
1161
|
methods.set("session.list", async (params, context) => {
|
|
1058
1162
|
const userId = requireAuth(context);
|
|
1163
|
+
if (!chatRepo)
|
|
1164
|
+
throw new Error("Database not available");
|
|
1059
1165
|
const workspaceId = params?.workspaceId;
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
// Fallback: get from AgentBox
|
|
1074
|
-
const handle = await agentBoxManager.getAsync(userId, workspaceId ?? "default");
|
|
1075
|
-
if (!handle)
|
|
1076
|
-
return { sessions: [] };
|
|
1077
|
-
const client = new AgentBoxClient(handle.endpoint, 30000, agentBoxTlsOptions);
|
|
1078
|
-
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
|
+
};
|
|
1079
1177
|
});
|
|
1080
1178
|
methods.set("session.create", async (_params, context) => {
|
|
1081
1179
|
const userId = requireAuth(context);
|
|
1082
|
-
if (chatRepo)
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
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 };
|
|
1087
1184
|
});
|
|
1088
1185
|
methods.set("session.delete", async (params, context) => {
|
|
1089
1186
|
const userId = requireAuth(context);
|
|
@@ -1100,7 +1197,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1100
1197
|
// ─────────────────────────────────────────────────
|
|
1101
1198
|
methods.set("box.status", async (params, context) => {
|
|
1102
1199
|
const userId = requireAuth(context);
|
|
1103
|
-
const workspaceId = params?.workspaceId
|
|
1200
|
+
const workspaceId = params?.workspaceId;
|
|
1104
1201
|
const handle = await agentBoxManager.getAsync(userId, workspaceId);
|
|
1105
1202
|
if (!handle)
|
|
1106
1203
|
return { boxStatus: "not_created" };
|
|
@@ -1937,12 +2034,17 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1937
2034
|
await skillRepo.update(skillId, updates);
|
|
1938
2035
|
}
|
|
1939
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
|
+
}
|
|
1940
2041
|
let stagedFiles = null;
|
|
1941
2042
|
if (skillContentRepo) {
|
|
1942
2043
|
stagedFiles = await skillContentRepo.read(skillId, "staging");
|
|
1943
2044
|
}
|
|
1944
|
-
if (stagedFiles?.scripts?.length) {
|
|
1945
|
-
|
|
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);
|
|
1946
2048
|
}
|
|
1947
2049
|
// 6. Notify reviewers (only on first submit to avoid flooding)
|
|
1948
2050
|
if (!isPending) {
|
|
@@ -1978,14 +2080,17 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
1978
2080
|
if (currentReviewStatus !== "pending") {
|
|
1979
2081
|
throw new Error("This skill is not pending review");
|
|
1980
2082
|
}
|
|
1981
|
-
// Record reviewer decision
|
|
2083
|
+
// Record reviewer decision — inherit riskLevel from the latest AI review if available
|
|
1982
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";
|
|
1983
2088
|
await skillReviewRepo.create({
|
|
1984
2089
|
skillId,
|
|
1985
2090
|
version: meta.version,
|
|
1986
2091
|
reviewerType: "admin",
|
|
1987
2092
|
reviewerId,
|
|
1988
|
-
riskLevel
|
|
2093
|
+
riskLevel,
|
|
1989
2094
|
summary: reason || (decision === "approve" ? "Approved by reviewer" : "Rejected by reviewer"),
|
|
1990
2095
|
findings: [],
|
|
1991
2096
|
decision,
|
|
@@ -2205,19 +2310,27 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2205
2310
|
// ─────────────────────────────────────────────────
|
|
2206
2311
|
// Cron Job Methods
|
|
2207
2312
|
// ─────────────────────────────────────────────────
|
|
2208
|
-
|
|
2209
|
-
function notifyCronService(payload) {
|
|
2210
|
-
notifyCronServiceImpl(payload, configRepo);
|
|
2211
|
-
}
|
|
2212
|
-
methods.set("cron.list", async (_params, context) => {
|
|
2313
|
+
methods.set("cron.list", async (params, context) => {
|
|
2213
2314
|
const userId = requireAuth(context);
|
|
2214
2315
|
if (!configRepo)
|
|
2215
2316
|
return { jobs: [] };
|
|
2216
|
-
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
|
+
}
|
|
2217
2330
|
return {
|
|
2218
2331
|
jobs: rows.map((r) => ({
|
|
2219
2332
|
...r,
|
|
2220
|
-
|
|
2333
|
+
workspaceName: r.workspaceId ? (wsNameMap.get(r.workspaceId) ?? null) : null,
|
|
2221
2334
|
})),
|
|
2222
2335
|
};
|
|
2223
2336
|
});
|
|
@@ -2229,10 +2342,8 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2229
2342
|
const description = params.description;
|
|
2230
2343
|
const schedule = params.schedule;
|
|
2231
2344
|
const status = params.status ?? "active";
|
|
2232
|
-
// For updates: fetch existing job to get current assignedTo
|
|
2233
2345
|
const existingId = params.id;
|
|
2234
|
-
const
|
|
2235
|
-
const envId = params.envId;
|
|
2346
|
+
const workspaceId = params.workspaceId;
|
|
2236
2347
|
const id = await configRepo.saveCronJob(userId, {
|
|
2237
2348
|
id: existingId,
|
|
2238
2349
|
name,
|
|
@@ -2240,38 +2351,20 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2240
2351
|
schedule,
|
|
2241
2352
|
skillId: params.skillId,
|
|
2242
2353
|
status,
|
|
2243
|
-
|
|
2354
|
+
workspaceId: workspaceId ?? null,
|
|
2244
2355
|
});
|
|
2245
|
-
if (
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
}
|
|
2249
|
-
else {
|
|
2250
|
-
// Active — keep existing assignment if possible, otherwise assign to least-loaded
|
|
2251
|
-
let assignedTo = existingJob?.assignedTo ?? null;
|
|
2252
|
-
if (!assignedTo) {
|
|
2253
|
-
try {
|
|
2254
|
-
const leastLoaded = await configRepo.getLeastLoadedInstance();
|
|
2255
|
-
if (leastLoaded) {
|
|
2256
|
-
assignedTo = leastLoaded.instanceId;
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
catch {
|
|
2260
|
-
// No instances available yet — coordinator will pick up unassigned jobs
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
if (assignedTo) {
|
|
2264
|
-
await configRepo.assignCronJob(id, assignedTo);
|
|
2356
|
+
if (cronService) {
|
|
2357
|
+
if (status === "paused") {
|
|
2358
|
+
cronService.cancel(id);
|
|
2265
2359
|
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
job: {
|
|
2360
|
+
else {
|
|
2361
|
+
cronService.addOrUpdate({
|
|
2269
2362
|
id, userId, name, description: description ?? null, schedule, status,
|
|
2270
|
-
skillId: params.skillId ?? null, assignedTo,
|
|
2363
|
+
skillId: params.skillId ?? null, assignedTo: null,
|
|
2271
2364
|
lastRunAt: null, lastResult: null, lockedBy: null, lockedAt: null,
|
|
2272
|
-
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2365
|
+
workspaceId: workspaceId ?? null,
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2275
2368
|
}
|
|
2276
2369
|
return { id, name, schedule, status };
|
|
2277
2370
|
});
|
|
@@ -2289,7 +2382,13 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2289
2382
|
if (job.userId !== userId)
|
|
2290
2383
|
throw new Error("Forbidden");
|
|
2291
2384
|
await configRepo.deleteCronJob(id);
|
|
2292
|
-
|
|
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
|
+
}
|
|
2293
2392
|
return { status: "deleted" };
|
|
2294
2393
|
});
|
|
2295
2394
|
methods.set("cron.setStatus", async (params, context) => {
|
|
@@ -2317,21 +2416,23 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2317
2416
|
schedule: job.schedule,
|
|
2318
2417
|
skillId: job.skillId ?? undefined,
|
|
2319
2418
|
status,
|
|
2320
|
-
|
|
2419
|
+
workspaceId: job.workspaceId ?? null,
|
|
2321
2420
|
});
|
|
2322
|
-
//
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2421
|
+
// Update scheduler
|
|
2422
|
+
if (cronService) {
|
|
2423
|
+
if (status === "paused") {
|
|
2424
|
+
cronService.cancel(id);
|
|
2425
|
+
}
|
|
2426
|
+
else {
|
|
2427
|
+
cronService.addOrUpdate({
|
|
2327
2428
|
id, userId, name: job.name, description: job.description ?? null,
|
|
2328
2429
|
schedule: job.schedule, status, skillId: job.skillId ?? null,
|
|
2329
|
-
assignedTo:
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2430
|
+
assignedTo: null, lastRunAt: null, lastResult: null,
|
|
2431
|
+
lockedBy: null, lockedAt: null,
|
|
2432
|
+
workspaceId: job.workspaceId ?? null,
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2335
2436
|
return { id, status };
|
|
2336
2437
|
});
|
|
2337
2438
|
methods.set("cron.rename", async (params, context) => {
|
|
@@ -2358,21 +2459,46 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2358
2459
|
schedule: job.schedule,
|
|
2359
2460
|
skillId: job.skillId ?? undefined,
|
|
2360
2461
|
status: job.status,
|
|
2361
|
-
|
|
2462
|
+
workspaceId: job.workspaceId ?? null,
|
|
2362
2463
|
});
|
|
2363
|
-
//
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
job: {
|
|
2464
|
+
// Update scheduler (name change — reschedule with updated job data)
|
|
2465
|
+
if (cronService && job.status === "active") {
|
|
2466
|
+
cronService.addOrUpdate({
|
|
2367
2467
|
id, userId, name: newName.trim(), description: job.description ?? null,
|
|
2368
|
-
schedule: job.schedule, status: job.status,
|
|
2369
|
-
|
|
2468
|
+
schedule: job.schedule, status: job.status,
|
|
2469
|
+
skillId: job.skillId ?? null, assignedTo: null,
|
|
2370
2470
|
lastRunAt: null, lastResult: null, lockedBy: null, lockedAt: null,
|
|
2371
|
-
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2471
|
+
workspaceId: job.workspaceId ?? null,
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2374
2474
|
return { id, name: newName.trim() };
|
|
2375
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
|
+
});
|
|
2376
2502
|
// ─────────────────────────────────────────────────
|
|
2377
2503
|
// Trigger Methods
|
|
2378
2504
|
// ─────────────────────────────────────────────────
|
|
@@ -2820,6 +2946,9 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2820
2946
|
const configJson = params.configJson;
|
|
2821
2947
|
if (!name)
|
|
2822
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
|
+
}
|
|
2823
2952
|
if (!type || !CREDENTIAL_TYPES.includes(type)) {
|
|
2824
2953
|
throw new Error(`Invalid credential type. Must be one of: ${CREDENTIAL_TYPES.join(", ")}`);
|
|
2825
2954
|
}
|
|
@@ -2827,6 +2956,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2827
2956
|
throw new Error("Missing required param: configJson");
|
|
2828
2957
|
validateCredentialConfig(type, configJson);
|
|
2829
2958
|
const id = await credRepo.create({ userId, name, type, description, configJson });
|
|
2959
|
+
pushCredentialsToUser(userId);
|
|
2830
2960
|
return { id, name, type };
|
|
2831
2961
|
});
|
|
2832
2962
|
methods.set("credential.update", async (params, context) => {
|
|
@@ -2850,6 +2980,7 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2850
2980
|
updates.configJson = configJson;
|
|
2851
2981
|
}
|
|
2852
2982
|
await credRepo.update(userId, id, updates);
|
|
2983
|
+
pushCredentialsToUser(userId);
|
|
2853
2984
|
return { status: "updated" };
|
|
2854
2985
|
});
|
|
2855
2986
|
methods.set("credential.delete", async (params, context) => {
|
|
@@ -2863,9 +2994,253 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2863
2994
|
if (!existing)
|
|
2864
2995
|
throw new Error("Credential not found");
|
|
2865
2996
|
await credRepo.delete(userId, id);
|
|
2997
|
+
pushCredentialsToUser(userId);
|
|
2866
2998
|
return { status: "deleted" };
|
|
2867
2999
|
});
|
|
2868
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
|
+
}
|
|
3161
|
+
return { status: "deleted" };
|
|
3162
|
+
});
|
|
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
|
+
// ─────────────────────────────────────────────────
|
|
2869
3244
|
// Workspace Methods
|
|
2870
3245
|
// ─────────────────────────────────────────────────
|
|
2871
3246
|
methods.set("workspace.list", async (_params, context) => {
|
|
@@ -2875,7 +3250,14 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2875
3250
|
let list = await workspaceRepo.list(userId);
|
|
2876
3251
|
// Auto-create default workspace if none exists
|
|
2877
3252
|
if (list.length === 0) {
|
|
2878
|
-
|
|
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);
|
|
2879
3261
|
list = await workspaceRepo.list(userId);
|
|
2880
3262
|
}
|
|
2881
3263
|
return { workspaces: list };
|
|
@@ -2887,8 +3269,18 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2887
3269
|
const name = params.name;
|
|
2888
3270
|
if (!name)
|
|
2889
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
|
+
}
|
|
2890
3282
|
const config = params.config;
|
|
2891
|
-
const ws = await workspaceRepo.create(userId, name, config);
|
|
3283
|
+
const ws = await workspaceRepo.create(userId, name, config, envType);
|
|
2892
3284
|
// Build workspace skills directory
|
|
2893
3285
|
await syncWorkspaceSkills(userId, ws.id, ws.isDefault, [], []);
|
|
2894
3286
|
return { workspace: ws };
|
|
@@ -2909,6 +3301,31 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2909
3301
|
updates.name = params.name;
|
|
2910
3302
|
if (params.config !== undefined)
|
|
2911
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
|
+
}
|
|
2912
3329
|
await workspaceRepo.update(id, updates);
|
|
2913
3330
|
return { status: "updated" };
|
|
2914
3331
|
});
|
|
@@ -2940,16 +3357,25 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
2940
3357
|
const ws = await workspaceRepo.getById(id);
|
|
2941
3358
|
if (!ws || ws.userId !== userId)
|
|
2942
3359
|
throw new Error("Workspace not found");
|
|
2943
|
-
const [wsSkills, wsTools, wsCreds] = await Promise.all([
|
|
3360
|
+
const [wsSkills, wsTools, wsCreds, wsEnvIds] = await Promise.all([
|
|
2944
3361
|
workspaceRepo.getSkills(id),
|
|
2945
3362
|
workspaceRepo.getTools(id),
|
|
2946
3363
|
workspaceRepo.getCredentials(id),
|
|
3364
|
+
workspaceRepo.getEnvironments(id),
|
|
2947
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
|
+
}
|
|
2948
3372
|
return {
|
|
2949
3373
|
workspace: ws,
|
|
2950
3374
|
skills: wsSkills,
|
|
2951
3375
|
tools: wsTools,
|
|
2952
3376
|
credentials: wsCreds,
|
|
3377
|
+
environments: wsEnvIds,
|
|
3378
|
+
environmentDetails: envDetails,
|
|
2953
3379
|
};
|
|
2954
3380
|
});
|
|
2955
3381
|
methods.set("workspace.setSkills", async (params, context) => {
|
|
@@ -3016,6 +3442,50 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3016
3442
|
}
|
|
3017
3443
|
}
|
|
3018
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);
|
|
3019
3489
|
return { status: "updated" };
|
|
3020
3490
|
});
|
|
3021
3491
|
methods.set("workspace.availableTools", async (_params, context) => {
|
|
@@ -3041,8 +3511,12 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3041
3511
|
});
|
|
3042
3512
|
/**
|
|
3043
3513
|
* Build credential payload for a workspace.
|
|
3044
|
-
*
|
|
3045
|
-
*
|
|
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
|
+
*
|
|
3046
3520
|
* Returns data only — does NOT write to disk. Agentbox materializes files locally.
|
|
3047
3521
|
*/
|
|
3048
3522
|
async function buildCredentialPayload(userId, workspaceId, isDefault) {
|
|
@@ -3051,16 +3525,83 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3051
3525
|
if (!fs.existsSync(agentDataDir)) {
|
|
3052
3526
|
fs.mkdirSync(agentDataDir, { recursive: true });
|
|
3053
3527
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
// 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
|
+
}
|
|
3057
3598
|
let creds;
|
|
3058
3599
|
if (isDefault) {
|
|
3059
3600
|
creds = await credRepo.listForUser(userId);
|
|
3060
3601
|
}
|
|
3061
3602
|
else {
|
|
3062
3603
|
if (!workspaceRepo)
|
|
3063
|
-
return { manifest
|
|
3604
|
+
return { manifest, files };
|
|
3064
3605
|
const linkedIds = await workspaceRepo.getCredentials(workspaceId);
|
|
3065
3606
|
if (linkedIds.length === 0) {
|
|
3066
3607
|
creds = [];
|
|
@@ -3071,43 +3612,15 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3071
3612
|
}
|
|
3072
3613
|
// IdentityFile path used inside agentbox (resolves to .siclaw/credentials/)
|
|
3073
3614
|
const credsDirInBox = path.resolve(process.cwd(), ".siclaw/credentials");
|
|
3074
|
-
const manifest = [];
|
|
3075
|
-
const files = [];
|
|
3076
3615
|
for (const cred of creds) {
|
|
3616
|
+
// Skip kubeconfig type credentials (now handled via environments)
|
|
3617
|
+
if (cred.type === "kubeconfig")
|
|
3618
|
+
continue;
|
|
3077
3619
|
const config = (cred.configJson ?? {});
|
|
3078
3620
|
const safeName = cred.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3079
3621
|
const fileNames = [];
|
|
3080
3622
|
let metadata;
|
|
3081
3623
|
switch (cred.type) {
|
|
3082
|
-
case "kubeconfig": {
|
|
3083
|
-
const content = config.content;
|
|
3084
|
-
if (content) {
|
|
3085
|
-
const filename = `${safeName}.kubeconfig`;
|
|
3086
|
-
files.push({ name: filename, content });
|
|
3087
|
-
fileNames.push(filename);
|
|
3088
|
-
try {
|
|
3089
|
-
const kc = yaml.load(content);
|
|
3090
|
-
const clusters = kc?.clusters ?? [];
|
|
3091
|
-
const contexts = kc?.contexts ?? [];
|
|
3092
|
-
metadata = {
|
|
3093
|
-
clusters: clusters.map((c) => ({
|
|
3094
|
-
name: c.name,
|
|
3095
|
-
server: c.cluster?.server,
|
|
3096
|
-
})),
|
|
3097
|
-
contexts: contexts.map((c) => ({
|
|
3098
|
-
name: c.name,
|
|
3099
|
-
cluster: c.context?.cluster,
|
|
3100
|
-
namespace: c.context?.namespace,
|
|
3101
|
-
})),
|
|
3102
|
-
currentContext: kc?.["current-context"],
|
|
3103
|
-
};
|
|
3104
|
-
}
|
|
3105
|
-
catch {
|
|
3106
|
-
// ignore parse errors
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
break;
|
|
3110
|
-
}
|
|
3111
3624
|
case "ssh_key": {
|
|
3112
3625
|
const privateKey = config.privateKey;
|
|
3113
3626
|
if (privateKey) {
|
|
@@ -3237,11 +3750,17 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3237
3750
|
const sessions = await chatRepo.listSessions(userId, 1);
|
|
3238
3751
|
sessionCount = sessions.length;
|
|
3239
3752
|
}
|
|
3240
|
-
// 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.
|
|
3241
3755
|
const userDataDir = process.env.SICLAW_USER_DATA_DIR || ".siclaw/user-data";
|
|
3242
3756
|
const profilePath = path.resolve(userDataDir, "memory", "PROFILE.md");
|
|
3243
|
-
|
|
3244
|
-
|
|
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)
|
|
3245
3764
|
const credentials = {};
|
|
3246
3765
|
if (credRepo) {
|
|
3247
3766
|
const creds = await credRepo.listForUser(userId);
|
|
@@ -3249,6 +3768,20 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3249
3768
|
credentials[c.type] = (credentials[c.type] || 0) + 1;
|
|
3250
3769
|
}
|
|
3251
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
|
+
}
|
|
3252
3785
|
return { hasModels, hasProfile, sessionCount, credentials };
|
|
3253
3786
|
});
|
|
3254
3787
|
// ─────────────────────────────────────────────────
|
|
@@ -3269,7 +3802,8 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3269
3802
|
const values = params.values;
|
|
3270
3803
|
const ALLOWED_SECTIONS = {
|
|
3271
3804
|
sso: ["sso.enabled", "sso.issuer", "sso.clientId", "sso.clientSecret", "sso.redirectUri"],
|
|
3272
|
-
system: ["system.
|
|
3805
|
+
system: ["system.grafanaUrl"],
|
|
3806
|
+
metrics: ["metrics.port", "metrics.token", "metrics.includeUserId"],
|
|
3273
3807
|
};
|
|
3274
3808
|
const allowedKeys = ALLOWED_SECTIONS[section];
|
|
3275
3809
|
if (!allowedKeys)
|
|
@@ -3283,29 +3817,181 @@ export function createRpcMethods(agentBoxManager, broadcast, db, sendToUser, act
|
|
|
3283
3817
|
}
|
|
3284
3818
|
}
|
|
3285
3819
|
await sysConfigRepo.setMany(entries);
|
|
3286
|
-
// Apply agentbox image change at runtime
|
|
3287
|
-
if (section === "system" && entries["system.agentboxImage"]) {
|
|
3288
|
-
agentBoxManager.setSpawnerImage(entries["system.agentboxImage"]);
|
|
3289
|
-
}
|
|
3290
3820
|
return { ok: true };
|
|
3291
3821
|
});
|
|
3292
|
-
/** 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). */
|
|
3293
3824
|
async function getSkillBundle(userId, env) {
|
|
3294
3825
|
if (!skillRepo || !skillContentRepo)
|
|
3295
3826
|
throw new Error("Database not available");
|
|
3296
3827
|
const disabled = new Set(await skillRepo.listDisabledSkills(userId));
|
|
3297
|
-
|
|
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);
|
|
3298
3831
|
}
|
|
3299
|
-
/**
|
|
3832
|
+
/** Detach WebSocket from SSE streams — SSE continues so DB persistence and
|
|
3833
|
+
* dpProgressSnapshots keep updating. User reconnect resumes live events. */
|
|
3300
3834
|
function cleanupForWs(ws) {
|
|
3301
3835
|
for (const [key, stream] of activeStreams.entries()) {
|
|
3302
3836
|
if (stream.ws === ws) {
|
|
3303
|
-
console.log(`[rpc]
|
|
3304
|
-
stream.
|
|
3305
|
-
activeStreams.delete(key);
|
|
3837
|
+
console.log(`[rpc] WS detached from SSE stream ${key} — SSE continues`);
|
|
3838
|
+
stream.ws = undefined;
|
|
3306
3839
|
}
|
|
3307
3840
|
}
|
|
3308
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
|
+
});
|
|
3309
3995
|
return { methods, buildCredentialPayload, getSkillBundle, cleanupForWs };
|
|
3310
3996
|
}
|
|
3311
3997
|
//# sourceMappingURL=rpc-methods.js.map
|