zoe-agent 0.3.1
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/CHANGELOG.md +154 -0
- package/LICENSE +96 -0
- package/README.md +568 -0
- package/dist/adapters/cli/agent.d.ts +59 -0
- package/dist/adapters/cli/agent.js +232 -0
- package/dist/adapters/cli/bootstrap.d.ts +25 -0
- package/dist/adapters/cli/bootstrap.js +204 -0
- package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
- package/dist/adapters/cli/commands/build-registry.js +88 -0
- package/dist/adapters/cli/commands/clear.d.ts +7 -0
- package/dist/adapters/cli/commands/clear.js +10 -0
- package/dist/adapters/cli/commands/compact.d.ts +13 -0
- package/dist/adapters/cli/commands/compact.js +96 -0
- package/dist/adapters/cli/commands/exit.d.ts +7 -0
- package/dist/adapters/cli/commands/exit.js +9 -0
- package/dist/adapters/cli/commands/gateway.d.ts +7 -0
- package/dist/adapters/cli/commands/gateway.js +152 -0
- package/dist/adapters/cli/commands/help.d.ts +9 -0
- package/dist/adapters/cli/commands/help.js +12 -0
- package/dist/adapters/cli/commands/models.d.ts +10 -0
- package/dist/adapters/cli/commands/models.js +32 -0
- package/dist/adapters/cli/commands/registry.d.ts +70 -0
- package/dist/adapters/cli/commands/registry.js +111 -0
- package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
- package/dist/adapters/cli/commands/settings-utils.js +182 -0
- package/dist/adapters/cli/commands/settings.d.ts +9 -0
- package/dist/adapters/cli/commands/settings.js +395 -0
- package/dist/adapters/cli/commands/skills.d.ts +7 -0
- package/dist/adapters/cli/commands/skills.js +21 -0
- package/dist/adapters/cli/config-loader.d.ts +27 -0
- package/dist/adapters/cli/config-loader.js +48 -0
- package/dist/adapters/cli/docker-utils.d.ts +37 -0
- package/dist/adapters/cli/docker-utils.js +90 -0
- package/dist/adapters/cli/index.d.ts +2 -0
- package/dist/adapters/cli/index.js +88 -0
- package/dist/adapters/cli/repl.d.ts +22 -0
- package/dist/adapters/cli/repl.js +256 -0
- package/dist/adapters/cli/setup.d.ts +19 -0
- package/dist/adapters/cli/setup.js +613 -0
- package/dist/adapters/cli/system-prompts.d.ts +56 -0
- package/dist/adapters/cli/system-prompts.js +131 -0
- package/dist/adapters/cli/tui/app.d.ts +58 -0
- package/dist/adapters/cli/tui/app.js +314 -0
- package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
- package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
- package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
- package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
- package/dist/adapters/cli/tui/components/command-palette.js +50 -0
- package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
- package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
- package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/error-message.js +8 -0
- package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
- package/dist/adapters/cli/tui/components/footer.js +19 -0
- package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
- package/dist/adapters/cli/tui/components/goal-status.js +22 -0
- package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/info-message.js +8 -0
- package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
- package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
- package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
- package/dist/adapters/cli/tui/components/markdown.js +92 -0
- package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
- package/dist/adapters/cli/tui/components/message-area.js +55 -0
- package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
- package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
- package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
- package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
- package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
- package/dist/adapters/cli/tui/components/text-input.js +142 -0
- package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
- package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
- package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/user-message.js +8 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
- package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
- package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
- package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
- package/dist/adapters/cli/tui/feed-serializer.js +70 -0
- package/dist/adapters/cli/tui/file-index.d.ts +8 -0
- package/dist/adapters/cli/tui/file-index.js +41 -0
- package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
- package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
- package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
- package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
- package/dist/adapters/cli/tui/index.d.ts +19 -0
- package/dist/adapters/cli/tui/index.js +206 -0
- package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
- package/dist/adapters/cli/tui/ink-reset.js +57 -0
- package/dist/adapters/cli/tui/layout.d.ts +15 -0
- package/dist/adapters/cli/tui/layout.js +15 -0
- package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
- package/dist/adapters/cli/tui/logo/gradient.js +31 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
- package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
- package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
- package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
- package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
- package/dist/adapters/cli/tui/session-export.d.ts +21 -0
- package/dist/adapters/cli/tui/session-export.js +63 -0
- package/dist/adapters/cli/tui/theme.d.ts +23 -0
- package/dist/adapters/cli/tui/theme.js +22 -0
- package/dist/adapters/cli/tui/types.d.ts +52 -0
- package/dist/adapters/cli/tui/types.js +12 -0
- package/dist/adapters/sdk/agent.d.ts +20 -0
- package/dist/adapters/sdk/agent.js +356 -0
- package/dist/adapters/sdk/http.d.ts +43 -0
- package/dist/adapters/sdk/http.js +61 -0
- package/dist/adapters/sdk/index.d.ts +58 -0
- package/dist/adapters/sdk/index.js +209 -0
- package/dist/adapters/sdk/settings.d.ts +18 -0
- package/dist/adapters/sdk/settings.js +57 -0
- package/dist/adapters/sdk/tools.d.ts +7 -0
- package/dist/adapters/sdk/tools.js +13 -0
- package/dist/adapters/server/auth.d.ts +53 -0
- package/dist/adapters/server/auth.js +168 -0
- package/dist/adapters/server/index.d.ts +40 -0
- package/dist/adapters/server/index.js +255 -0
- package/dist/adapters/server/rest-gateway.d.ts +13 -0
- package/dist/adapters/server/rest-gateway.js +218 -0
- package/dist/adapters/server/rest.d.ts +37 -0
- package/dist/adapters/server/rest.js +341 -0
- package/dist/adapters/server/server-core.d.ts +55 -0
- package/dist/adapters/server/server-core.js +121 -0
- package/dist/adapters/server/session-store.d.ts +81 -0
- package/dist/adapters/server/session-store.js +272 -0
- package/dist/adapters/server/settings-handlers.d.ts +24 -0
- package/dist/adapters/server/settings-handlers.js +360 -0
- package/dist/adapters/server/standalone.d.ts +19 -0
- package/dist/adapters/server/standalone.js +113 -0
- package/dist/adapters/server/websocket.d.ts +26 -0
- package/dist/adapters/server/websocket.js +68 -0
- package/dist/adapters/server/ws-handlers.d.ts +32 -0
- package/dist/adapters/server/ws-handlers.js +523 -0
- package/dist/adapters/server/ws-types.d.ts +304 -0
- package/dist/adapters/server/ws-types.js +7 -0
- package/dist/core/agent-loop.d.ts +68 -0
- package/dist/core/agent-loop.js +423 -0
- package/dist/core/config.d.ts +115 -0
- package/dist/core/config.js +189 -0
- package/dist/core/errors.d.ts +58 -0
- package/dist/core/errors.js +88 -0
- package/dist/core/hooks.d.ts +35 -0
- package/dist/core/hooks.js +49 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +29 -0
- package/dist/core/message-convert.d.ts +41 -0
- package/dist/core/message-convert.js +94 -0
- package/dist/core/middleware/auth.d.ts +24 -0
- package/dist/core/middleware/auth.js +28 -0
- package/dist/core/middleware/logging.d.ts +23 -0
- package/dist/core/middleware/logging.js +28 -0
- package/dist/core/middleware/rate-limit.d.ts +27 -0
- package/dist/core/middleware/rate-limit.js +38 -0
- package/dist/core/middleware/semantic-tools.d.ts +10 -0
- package/dist/core/middleware/semantic-tools.js +43 -0
- package/dist/core/middleware.d.ts +48 -0
- package/dist/core/middleware.js +38 -0
- package/dist/core/permission.d.ts +25 -0
- package/dist/core/permission.js +50 -0
- package/dist/core/provider-config.d.ts +129 -0
- package/dist/core/provider-config.js +273 -0
- package/dist/core/provider-env.d.ts +39 -0
- package/dist/core/provider-env.js +142 -0
- package/dist/core/provider-resolver.d.ts +12 -0
- package/dist/core/provider-resolver.js +12 -0
- package/dist/core/session-store.d.ts +75 -0
- package/dist/core/session-store.js +245 -0
- package/dist/core/settings-manager.d.ts +57 -0
- package/dist/core/settings-manager.js +359 -0
- package/dist/core/settings-schema.d.ts +38 -0
- package/dist/core/settings-schema.js +171 -0
- package/dist/core/skill-catalog.d.ts +6 -0
- package/dist/core/skill-catalog.js +17 -0
- package/dist/core/skill-invoker.d.ts +127 -0
- package/dist/core/skill-invoker.js +182 -0
- package/dist/core/stream-accumulator.d.ts +21 -0
- package/dist/core/stream-accumulator.js +51 -0
- package/dist/core/stream-manager.d.ts +58 -0
- package/dist/core/stream-manager.js +212 -0
- package/dist/core/tool-executor.d.ts +84 -0
- package/dist/core/tool-executor.js +256 -0
- package/dist/core/types.d.ts +259 -0
- package/dist/core/types.js +11 -0
- package/dist/gateway/gateway.d.ts +52 -0
- package/dist/gateway/gateway.js +537 -0
- package/dist/gateway/index.d.ts +21 -0
- package/dist/gateway/index.js +31 -0
- package/dist/gateway/openapi-importer.d.ts +15 -0
- package/dist/gateway/openapi-importer.js +66 -0
- package/dist/gateway/semantic-scorer.d.ts +7 -0
- package/dist/gateway/semantic-scorer.js +24 -0
- package/dist/gateway/settings-adapter.d.ts +49 -0
- package/dist/gateway/settings-adapter.js +137 -0
- package/dist/gateway/tool-factory.d.ts +9 -0
- package/dist/gateway/tool-factory.js +414 -0
- package/dist/gateway/types.d.ts +68 -0
- package/dist/gateway/types.js +7 -0
- package/dist/models-catalog.js +46 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +148 -0
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +71 -0
- package/dist/providers/types.d.ts +48 -0
- package/dist/providers/types.js +1 -0
- package/dist/skills/args.d.ts +37 -0
- package/dist/skills/args.js +99 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +23 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/parser.d.ts +7 -0
- package/dist/skills/parser.js +152 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +74 -0
- package/dist/skills/resolver.d.ts +19 -0
- package/dist/skills/resolver.js +116 -0
- package/dist/skills/types.d.ts +74 -0
- package/dist/skills/types.js +50 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +68 -0
- package/dist/tools/core.d.ts +20 -0
- package/dist/tools/core.js +244 -0
- package/dist/tools/email.d.ts +2 -0
- package/dist/tools/email.js +61 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.js +257 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +88 -0
- package/dist/tools/interface.d.ts +22 -0
- package/dist/tools/interface.js +1 -0
- package/dist/tools/notify.d.ts +2 -0
- package/dist/tools/notify.js +100 -0
- package/dist/tools/prompt-optimizer.d.ts +2 -0
- package/dist/tools/prompt-optimizer.js +65 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +184 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/todos.d.ts +10 -0
- package/dist/tools/todos.js +50 -0
- package/package.json +119 -0
- package/skills/docker-ops/SKILL.md +329 -0
- package/skills/k8s-deploy/SKILL.md +397 -0
- package/skills/log-analyzer/SKILL.md +331 -0
- package/skills/speckit-analyze/SKILL.md +260 -0
- package/skills/speckit-checklist/SKILL.md +374 -0
- package/skills/speckit-clarify/SKILL.md +286 -0
- package/skills/speckit-constitution/SKILL.md +157 -0
- package/skills/speckit-implement/SKILL.md +224 -0
- package/skills/speckit-plan/SKILL.md +171 -0
- package/skills/speckit-specify/SKILL.md +346 -0
- package/skills/speckit-tasks/SKILL.md +215 -0
- package/skills/speckit-taskstoissues/SKILL.md +107 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoe Server — WebSocket Protocol Handlers
|
|
3
|
+
*
|
|
4
|
+
* All handler functions, safeSend helper, and active connections registry.
|
|
5
|
+
* Extracted from websocket.ts for single-responsibility.
|
|
6
|
+
*/
|
|
7
|
+
import * as crypto from "crypto";
|
|
8
|
+
import { authMiddleware, hasScope } from "./auth.js";
|
|
9
|
+
import { hashKey } from "./session-store.js";
|
|
10
|
+
import { handleWsGetSettings, handleWsUpdateSettings } from "./settings-handlers.js";
|
|
11
|
+
// ── Active connections registry ──────────────────────────────────────
|
|
12
|
+
const activeConnections = new Map();
|
|
13
|
+
// ── Pending tool approvals ───────────────────────────────────────────
|
|
14
|
+
const pendingApprovals = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Get the number of currently active WebSocket connections.
|
|
17
|
+
*/
|
|
18
|
+
export function getActiveConnectionCount() {
|
|
19
|
+
return activeConnections.size;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get all connected WS clients (excluding the given one).
|
|
23
|
+
* Used by settings broadcast to notify other connections of changes.
|
|
24
|
+
*/
|
|
25
|
+
export function getOtherClients(excludeWs) {
|
|
26
|
+
const clients = [];
|
|
27
|
+
for (const [ws, state] of activeConnections) {
|
|
28
|
+
if (ws !== excludeWs) {
|
|
29
|
+
clients.push({ ws, state });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return clients;
|
|
33
|
+
}
|
|
34
|
+
// ── Send helper ──────────────────────────────────────────────────────
|
|
35
|
+
export function safeSend(ws, message) {
|
|
36
|
+
try {
|
|
37
|
+
if (ws.readyState === 1 /* OPEN */) {
|
|
38
|
+
ws.send(JSON.stringify(message));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Connection may have closed
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ── Protocol handler ─────────────────────────────────────────────────
|
|
46
|
+
export function handleConnection(ws, req, ctx) {
|
|
47
|
+
// Auth check — the token should have been validated during upgrade,
|
|
48
|
+
// but verify again for safety
|
|
49
|
+
const key = authMiddleware(req);
|
|
50
|
+
if (!key) {
|
|
51
|
+
safeSend(ws, {
|
|
52
|
+
type: "error",
|
|
53
|
+
code: "UNAUTHORIZED",
|
|
54
|
+
retryable: false,
|
|
55
|
+
message: "Authentication required",
|
|
56
|
+
});
|
|
57
|
+
ws.close(4001, "Unauthorized");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const state = {
|
|
61
|
+
sessionId: null,
|
|
62
|
+
currentAbortController: null,
|
|
63
|
+
activeProvider: null,
|
|
64
|
+
activeModel: null,
|
|
65
|
+
maxPermissionLevel: ctx.maxPermissionLevel,
|
|
66
|
+
apiKeyHash: hashKey(key.key),
|
|
67
|
+
apiKey: key,
|
|
68
|
+
};
|
|
69
|
+
activeConnections.set(ws, state);
|
|
70
|
+
// ── Message dispatch ───────────────────────────────────────────────
|
|
71
|
+
ws.on("message", (data) => {
|
|
72
|
+
let msg;
|
|
73
|
+
try {
|
|
74
|
+
msg = JSON.parse(data.toString("utf-8"));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
safeSend(ws, {
|
|
78
|
+
type: "error",
|
|
79
|
+
code: "INVALID_MESSAGE",
|
|
80
|
+
retryable: false,
|
|
81
|
+
message: "Invalid JSON message",
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
switch (msg.type) {
|
|
86
|
+
case "chat":
|
|
87
|
+
handleChat(ws, msg, state, ctx);
|
|
88
|
+
break;
|
|
89
|
+
case "abort":
|
|
90
|
+
handleAbort(ws, msg, state);
|
|
91
|
+
break;
|
|
92
|
+
case "tool_approval_response":
|
|
93
|
+
handleToolApprovalResponse(ws, msg);
|
|
94
|
+
break;
|
|
95
|
+
case "resume":
|
|
96
|
+
void handleResume(ws, msg, state, ctx);
|
|
97
|
+
break;
|
|
98
|
+
case "reconnect":
|
|
99
|
+
void handleReconnect(ws, msg, state, ctx);
|
|
100
|
+
break;
|
|
101
|
+
case "switch_provider":
|
|
102
|
+
handleSwitchProvider(ws, msg, state);
|
|
103
|
+
break;
|
|
104
|
+
case "list_models":
|
|
105
|
+
handleListModels(ws, ctx);
|
|
106
|
+
break;
|
|
107
|
+
case "list_skills":
|
|
108
|
+
handleListSkills(ws, ctx);
|
|
109
|
+
break;
|
|
110
|
+
case "ping":
|
|
111
|
+
safeSend(ws, {
|
|
112
|
+
type: "pong",
|
|
113
|
+
serverTime: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
case "get_settings":
|
|
117
|
+
handleWsSettingsMessage(ws, msg, state, ctx, (sCtx) => handleWsGetSettings(msg, ws, state, sCtx));
|
|
118
|
+
break;
|
|
119
|
+
case "update_settings":
|
|
120
|
+
handleWsSettingsMessage(ws, msg, state, ctx, (sCtx) => void handleWsUpdateSettings(msg, ws, state, sCtx));
|
|
121
|
+
break;
|
|
122
|
+
case "list_providers":
|
|
123
|
+
handleWsSettingsMessage(ws, msg, state, ctx, (sCtx) => handleWsListProviders(msg, ws, state, sCtx));
|
|
124
|
+
break;
|
|
125
|
+
case "set_provider":
|
|
126
|
+
handleWsSettingsMessage(ws, msg, state, ctx, (sCtx) => void handleWsSetProvider(msg, ws, state, sCtx));
|
|
127
|
+
break;
|
|
128
|
+
case "remove_provider":
|
|
129
|
+
handleWsSettingsMessage(ws, msg, state, ctx, (sCtx) => void handleWsRemoveProvider(msg, ws, state, sCtx));
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
safeSend(ws, {
|
|
133
|
+
type: "error",
|
|
134
|
+
code: "UNKNOWN_MESSAGE_TYPE",
|
|
135
|
+
retryable: false,
|
|
136
|
+
message: `Unknown message type: ${msg.type}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// ── Close ──────────────────────────────────────────────────────────
|
|
141
|
+
ws.on("close", () => {
|
|
142
|
+
// Abort any in-flight stream
|
|
143
|
+
if (state.currentAbortController) {
|
|
144
|
+
state.currentAbortController.abort();
|
|
145
|
+
state.currentAbortController = null;
|
|
146
|
+
}
|
|
147
|
+
activeConnections.delete(ws);
|
|
148
|
+
});
|
|
149
|
+
// ── Error ──────────────────────────────────────────────────────────
|
|
150
|
+
ws.on("error", (err) => {
|
|
151
|
+
console.error("[ws] Connection error:", err.message);
|
|
152
|
+
if (state.currentAbortController) {
|
|
153
|
+
state.currentAbortController.abort();
|
|
154
|
+
state.currentAbortController = null;
|
|
155
|
+
}
|
|
156
|
+
activeConnections.delete(ws);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ── Chat handler ─────────────────────────────────────────────────────
|
|
160
|
+
function handleChat(ws, msg, state, ctx) {
|
|
161
|
+
const serverMsgId = crypto.randomUUID();
|
|
162
|
+
// Acknowledge
|
|
163
|
+
safeSend(ws, {
|
|
164
|
+
type: "ack",
|
|
165
|
+
clientMsgId: msg.id,
|
|
166
|
+
serverMsgId,
|
|
167
|
+
timestamp: new Date().toISOString(),
|
|
168
|
+
});
|
|
169
|
+
// Create session if needed
|
|
170
|
+
if (!state.sessionId && msg.sessionId) {
|
|
171
|
+
state.sessionId = msg.sessionId;
|
|
172
|
+
}
|
|
173
|
+
// Set up abort controller
|
|
174
|
+
const abortController = new AbortController();
|
|
175
|
+
state.currentAbortController = abortController;
|
|
176
|
+
// Resolve options with connection-level overrides
|
|
177
|
+
const provider = msg.options?.provider ?? state.activeProvider ?? undefined;
|
|
178
|
+
const model = msg.options?.model ?? state.activeModel ?? undefined;
|
|
179
|
+
// Resolve permission level with server ceiling
|
|
180
|
+
let effectivePermissionLevel = msg.options?.permissionLevel ?? state.permissionLevel;
|
|
181
|
+
if (effectivePermissionLevel && state.maxPermissionLevel) {
|
|
182
|
+
const levels = ["strict", "moderate", "permissive"];
|
|
183
|
+
const maxIdx = levels.indexOf(state.maxPermissionLevel);
|
|
184
|
+
const reqIdx = levels.indexOf(effectivePermissionLevel);
|
|
185
|
+
// QA-009: Unknown levels (-1) are capped to the server ceiling
|
|
186
|
+
if (reqIdx === -1 || reqIdx > maxIdx) {
|
|
187
|
+
effectivePermissionLevel = state.maxPermissionLevel;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Stream text
|
|
191
|
+
try {
|
|
192
|
+
ctx.streamText({
|
|
193
|
+
message: msg.message,
|
|
194
|
+
model,
|
|
195
|
+
provider,
|
|
196
|
+
tools: msg.options?.tools,
|
|
197
|
+
maxSteps: msg.options?.maxSteps ?? 10,
|
|
198
|
+
skills: msg.options?.skills,
|
|
199
|
+
sessionId: state.sessionId ?? undefined,
|
|
200
|
+
permissionLevel: effectivePermissionLevel,
|
|
201
|
+
approveTool: createServerApproveTool(ws),
|
|
202
|
+
signal: abortController.signal,
|
|
203
|
+
onText: (delta) => {
|
|
204
|
+
safeSend(ws, {
|
|
205
|
+
type: "text",
|
|
206
|
+
delta,
|
|
207
|
+
serverMsgId,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
onToolCall: (info) => {
|
|
211
|
+
safeSend(ws, {
|
|
212
|
+
type: "tool_call",
|
|
213
|
+
callId: info.callId,
|
|
214
|
+
name: info.name,
|
|
215
|
+
args: info.args,
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
onToolResult: (info) => {
|
|
219
|
+
safeSend(ws, {
|
|
220
|
+
type: "tool_result",
|
|
221
|
+
callId: info.callId,
|
|
222
|
+
output: info.output,
|
|
223
|
+
success: info.success,
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
onStep: (step) => {
|
|
227
|
+
// Estimate progress — we don't know totalSteps ahead of time
|
|
228
|
+
safeSend(ws, {
|
|
229
|
+
type: "progress",
|
|
230
|
+
step: 0,
|
|
231
|
+
totalSteps: 0,
|
|
232
|
+
percentage: 0,
|
|
233
|
+
activity: step.content ?? step.type,
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
onError: (error) => {
|
|
237
|
+
safeSend(ws, {
|
|
238
|
+
type: "error",
|
|
239
|
+
code: error.code || "STREAM_ERROR",
|
|
240
|
+
retryable: error.code === "PROVIDER_ERROR",
|
|
241
|
+
message: error.message,
|
|
242
|
+
provider: error.provider,
|
|
243
|
+
tool: error.tool,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
onDone: (result) => {
|
|
247
|
+
safeSend(ws, {
|
|
248
|
+
type: "done",
|
|
249
|
+
serverMsgId,
|
|
250
|
+
usage: result.usage,
|
|
251
|
+
finishReason: result.finishReason,
|
|
252
|
+
});
|
|
253
|
+
// Add assistant message to session
|
|
254
|
+
if (state.sessionId) {
|
|
255
|
+
const assistantMsg = {
|
|
256
|
+
id: serverMsgId,
|
|
257
|
+
role: "assistant",
|
|
258
|
+
content: result.text,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
};
|
|
261
|
+
ctx.sessionManager.addMessage(state.sessionId, assistantMsg);
|
|
262
|
+
}
|
|
263
|
+
state.currentAbortController = null;
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
const message = err instanceof Error ? err.message : "Stream failed";
|
|
269
|
+
safeSend(ws, {
|
|
270
|
+
type: "error",
|
|
271
|
+
code: "STREAM_ERROR",
|
|
272
|
+
retryable: false,
|
|
273
|
+
message,
|
|
274
|
+
});
|
|
275
|
+
state.currentAbortController = null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ── Abort handler ────────────────────────────────────────────────────
|
|
279
|
+
function handleAbort(ws, _msg, state) {
|
|
280
|
+
if (state.currentAbortController) {
|
|
281
|
+
state.currentAbortController.abort();
|
|
282
|
+
state.currentAbortController = null;
|
|
283
|
+
safeSend(ws, {
|
|
284
|
+
type: "error",
|
|
285
|
+
code: "ABORTED",
|
|
286
|
+
retryable: false,
|
|
287
|
+
message: "Request aborted by client",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ── Tool approval handler ────────────────────────────────────────────
|
|
292
|
+
const APPROVAL_TIMEOUT_MS = 30_000; // 30 seconds
|
|
293
|
+
/**
|
|
294
|
+
* Create an `approveTool` callback for the server adapter.
|
|
295
|
+
* Sends a `tool_approval_request` to the client and waits for a
|
|
296
|
+
* `tool_approval_response`. Falls back to auto-deny on timeout.
|
|
297
|
+
*/
|
|
298
|
+
export function createServerApproveTool(ws) {
|
|
299
|
+
return async (call) => {
|
|
300
|
+
const callId = crypto.randomUUID();
|
|
301
|
+
safeSend(ws, {
|
|
302
|
+
type: "tool_approval_request",
|
|
303
|
+
callId,
|
|
304
|
+
name: call.name,
|
|
305
|
+
args: call.args,
|
|
306
|
+
});
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
pendingApprovals.delete(callId);
|
|
310
|
+
resolve(false); // Timeout → deny
|
|
311
|
+
}, APPROVAL_TIMEOUT_MS);
|
|
312
|
+
pendingApprovals.set(callId, { resolve, timer, ws, toolName: call.name, createdAt: Date.now() });
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function handleToolApprovalResponse(ws, msg) {
|
|
317
|
+
const pending = pendingApprovals.get(msg.callId);
|
|
318
|
+
if (!pending)
|
|
319
|
+
return;
|
|
320
|
+
// QA-001: Only the originating connection may resolve the approval
|
|
321
|
+
if (pending.ws !== ws)
|
|
322
|
+
return;
|
|
323
|
+
// Defense-in-depth: verify the tool name matches the pending request
|
|
324
|
+
if (msg.name !== pending.toolName)
|
|
325
|
+
return;
|
|
326
|
+
// Reject expired approvals (defense-in-depth, timer should have fired)
|
|
327
|
+
if (Date.now() - pending.createdAt > APPROVAL_TIMEOUT_MS) {
|
|
328
|
+
clearTimeout(pending.timer);
|
|
329
|
+
pendingApprovals.delete(msg.callId);
|
|
330
|
+
pending.resolve(false);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
clearTimeout(pending.timer);
|
|
334
|
+
pendingApprovals.delete(msg.callId);
|
|
335
|
+
pending.resolve(msg.approved);
|
|
336
|
+
}
|
|
337
|
+
// ── Resume handler ───────────────────────────────────────────────────
|
|
338
|
+
async function handleResume(ws, msg, state, ctx) {
|
|
339
|
+
const session = await ctx.sessionManager.getSession(msg.sessionId, state.apiKeyHash);
|
|
340
|
+
if (!session) {
|
|
341
|
+
safeSend(ws, {
|
|
342
|
+
type: "error",
|
|
343
|
+
code: "SESSION_NOT_FOUND",
|
|
344
|
+
retryable: false,
|
|
345
|
+
message: `Session ${msg.sessionId} not found or expired`,
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
state.sessionId = msg.sessionId;
|
|
350
|
+
safeSend(ws, {
|
|
351
|
+
type: "session_resumed",
|
|
352
|
+
sessionId: msg.sessionId,
|
|
353
|
+
messages: session.messages,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// ── Reconnect handler ────────────────────────────────────────────────
|
|
357
|
+
async function handleReconnect(ws, msg, state, ctx) {
|
|
358
|
+
const session = await ctx.sessionManager.getSession(msg.sessionId, state.apiKeyHash);
|
|
359
|
+
if (!session) {
|
|
360
|
+
safeSend(ws, {
|
|
361
|
+
type: "error",
|
|
362
|
+
code: "SESSION_NOT_FOUND",
|
|
363
|
+
retryable: false,
|
|
364
|
+
message: `Session ${msg.sessionId} not found or expired`,
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
state.sessionId = msg.sessionId;
|
|
369
|
+
// Replay messages — optionally only those after lastSeenId
|
|
370
|
+
let messages = session.messages;
|
|
371
|
+
if (msg.lastSeenId) {
|
|
372
|
+
const lastIndex = messages.findIndex((m) => m.id === msg.lastSeenId);
|
|
373
|
+
if (lastIndex !== -1) {
|
|
374
|
+
messages = messages.slice(lastIndex + 1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
safeSend(ws, {
|
|
378
|
+
type: "replay",
|
|
379
|
+
messages,
|
|
380
|
+
currentStatus: "ready",
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// ── Switch provider handler ──────────────────────────────────────────
|
|
384
|
+
function handleSwitchProvider(ws, msg, state) {
|
|
385
|
+
state.activeProvider = msg.provider;
|
|
386
|
+
if (msg.model) {
|
|
387
|
+
state.activeModel = msg.model;
|
|
388
|
+
}
|
|
389
|
+
safeSend(ws, {
|
|
390
|
+
type: "ack",
|
|
391
|
+
clientMsgId: "",
|
|
392
|
+
serverMsgId: crypto.randomUUID(),
|
|
393
|
+
timestamp: new Date().toISOString(),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
// ── List models handler ──────────────────────────────────────────────
|
|
397
|
+
function handleListModels(ws, ctx) {
|
|
398
|
+
safeSend(ws, {
|
|
399
|
+
type: "models_list",
|
|
400
|
+
models: ctx.listModels(),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// ── List skills handler ──────────────────────────────────────────────
|
|
404
|
+
function handleListSkills(ws, ctx) {
|
|
405
|
+
safeSend(ws, {
|
|
406
|
+
type: "skills_list",
|
|
407
|
+
skills: ctx.listSkills(),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
// ── Settings dispatch helper ────────────────────────────────────────────
|
|
411
|
+
function handleWsSettingsMessage(ws, _msg, _state, ctx, fn) {
|
|
412
|
+
const sCtx = ctx.settingsHandlerContext;
|
|
413
|
+
if (!sCtx) {
|
|
414
|
+
safeSend(ws, {
|
|
415
|
+
type: "error",
|
|
416
|
+
code: "SERVICE_UNAVAILABLE",
|
|
417
|
+
retryable: false,
|
|
418
|
+
message: "Settings not configured",
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
fn(sCtx);
|
|
423
|
+
}
|
|
424
|
+
// ── WS scope helper ─────────────────────────────────────────────────────
|
|
425
|
+
function requireWsScope(state, scope) {
|
|
426
|
+
return !!state.apiKey && hasScope(state.apiKey, scope);
|
|
427
|
+
}
|
|
428
|
+
// ── WS Settings: list providers ─────────────────────────────────────────
|
|
429
|
+
function handleWsListProviders(msg, ws, state, ctx) {
|
|
430
|
+
if (!requireWsScope(state, "agent:read")) {
|
|
431
|
+
safeSend(ws, { type: "providers_list", id: msg.id, providers: {}, error: { code: "FORBIDDEN", message: "Requires agent:read scope" } });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const providers = {};
|
|
435
|
+
for (const pType of ["openai", "anthropic", "glm", "openai-compatible"]) {
|
|
436
|
+
const prefix = `providers.${pType === "openai-compatible" ? "openai-compat" : pType}`;
|
|
437
|
+
const apiKeyVal = ctx.settingsManager.get(`${prefix}.apiKey`).value;
|
|
438
|
+
if (apiKeyVal != null) {
|
|
439
|
+
providers[pType] = { type: pType };
|
|
440
|
+
const model = ctx.settingsManager.get(`${prefix}.model`).value;
|
|
441
|
+
if (model)
|
|
442
|
+
providers[pType].model = model;
|
|
443
|
+
if (pType === "openai-compatible") {
|
|
444
|
+
const baseUrl = ctx.settingsManager.get(`${prefix}.baseUrl`).value;
|
|
445
|
+
if (baseUrl)
|
|
446
|
+
providers[pType].baseUrl = baseUrl;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
safeSend(ws, { type: "providers_list", id: msg.id, providers });
|
|
451
|
+
}
|
|
452
|
+
// ── WS Settings: set provider ───────────────────────────────────────────
|
|
453
|
+
async function handleWsSetProvider(msg, ws, state, ctx) {
|
|
454
|
+
if (!requireWsScope(state, "admin")) {
|
|
455
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, error: { code: "FORBIDDEN", message: "Requires admin scope" } });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const { type: providerType, apiKey, baseUrl, model } = msg.provider;
|
|
459
|
+
const VALID_PROVIDER_TYPES = new Set(["openai", "anthropic", "glm", "openai-compatible"]);
|
|
460
|
+
if (!providerType || !VALID_PROVIDER_TYPES.has(providerType)) {
|
|
461
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, error: { code: "VALIDATION_ERROR", message: "Invalid provider type" } });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const prefix = `providers.${providerType === "openai-compatible" ? "openai-compat" : providerType}`;
|
|
465
|
+
try {
|
|
466
|
+
if (apiKey)
|
|
467
|
+
await ctx.settingsManager.set(`${prefix}.apiKey`, apiKey);
|
|
468
|
+
if (model)
|
|
469
|
+
await ctx.settingsManager.set(`${prefix}.model`, model);
|
|
470
|
+
if (baseUrl && providerType === "openai-compatible")
|
|
471
|
+
await ctx.settingsManager.set(`${prefix}.baseUrl`, baseUrl);
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, error: { code: "SET_ERROR", message: e.message } });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, applied: { [providerType]: true }, requiresRestart: false, restartAffected: [] });
|
|
478
|
+
}
|
|
479
|
+
// ── WS Settings: remove provider ────────────────────────────────────────
|
|
480
|
+
async function handleWsRemoveProvider(msg, ws, state, ctx) {
|
|
481
|
+
if (!requireWsScope(state, "admin")) {
|
|
482
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, error: { code: "FORBIDDEN", message: "Requires admin scope" } });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const VALID_PROVIDER_TYPES = new Set(["openai", "anthropic", "glm", "openai-compatible"]);
|
|
486
|
+
if (!msg.providerType || !VALID_PROVIDER_TYPES.has(msg.providerType)) {
|
|
487
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, error: { code: "VALIDATION_ERROR", message: "Invalid provider type" } });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const prefix = `providers.${msg.providerType === "openai-compatible" ? "openai-compat" : msg.providerType}`;
|
|
491
|
+
try {
|
|
492
|
+
await ctx.settingsManager.reset(`${prefix}.apiKey`);
|
|
493
|
+
try {
|
|
494
|
+
await ctx.settingsManager.reset(`${prefix}.model`);
|
|
495
|
+
}
|
|
496
|
+
catch { /* may not exist */ }
|
|
497
|
+
try {
|
|
498
|
+
await ctx.settingsManager.reset(`${prefix}.baseUrl`);
|
|
499
|
+
}
|
|
500
|
+
catch { /* may not exist */ }
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, error: { code: "RESET_ERROR", message: e.message } });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
safeSend(ws, { type: "settings_updated", id: msg.id, applied: { removed: msg.providerType }, requiresRestart: false, restartAffected: [] });
|
|
507
|
+
}
|
|
508
|
+
// ── Active connections accessor (for closeWebSocket) ──────────────────
|
|
509
|
+
/**
|
|
510
|
+
* Close all active connections and clear the registry.
|
|
511
|
+
* Used by closeWebSocket() during shutdown.
|
|
512
|
+
*/
|
|
513
|
+
export function closeAllConnections() {
|
|
514
|
+
for (const [ws] of activeConnections) {
|
|
515
|
+
try {
|
|
516
|
+
ws.close(1001, "Server shutting down");
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// Ignore errors during shutdown
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
activeConnections.clear();
|
|
523
|
+
}
|