clay-server 2.10.0 → 2.11.0-beta.10
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/bin/cli.js +157 -1
- package/lib/daemon.js +341 -2
- package/lib/dm.js +135 -0
- package/lib/os-users.js +301 -0
- package/lib/pages.js +36 -0
- package/lib/project.js +386 -67
- package/lib/public/app.js +675 -17
- package/lib/public/css/admin.css +99 -10
- package/lib/public/css/filebrowser.css +22 -0
- package/lib/public/css/icon-strip.css +162 -1
- package/lib/public/css/menus.css +23 -0
- package/lib/public/css/messages.css +245 -0
- package/lib/public/css/overlays.css +88 -0
- package/lib/public/css/server-settings.css +30 -2
- package/lib/public/css/sidebar.css +4 -0
- package/lib/public/index.html +140 -66
- package/lib/public/modules/admin.js +179 -12
- package/lib/public/modules/input.js +13 -2
- package/lib/public/modules/notifications.js +3 -1
- package/lib/public/modules/project-settings.js +154 -168
- package/lib/public/modules/server-settings.js +78 -189
- package/lib/public/modules/settings-defaults.js +243 -0
- package/lib/public/modules/sidebar.js +112 -6
- package/lib/public/modules/terminal.js +48 -10
- package/lib/public/modules/tools.js +214 -1
- package/lib/sdk-bridge.js +634 -6
- package/lib/sdk-worker.js +446 -0
- package/lib/server.js +335 -3
- package/lib/sessions.js +26 -0
- package/lib/terminal-manager.js +2 -2
- package/lib/terminal.js +20 -4
- package/lib/updater.js +38 -11
- package/lib/users.js +79 -0
- package/package.json +2 -2
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// sdk-worker.js — Standalone worker process for OS-level user isolation.
|
|
2
|
+
// Runs as a target Linux user, loads the Claude Agent SDK, and communicates
|
|
3
|
+
// with the main Clay daemon over a Unix domain socket using JSON lines.
|
|
4
|
+
//
|
|
5
|
+
// Usage: node sdk-worker.js <socket-path>
|
|
6
|
+
|
|
7
|
+
var net = require("net");
|
|
8
|
+
var crypto = require("crypto");
|
|
9
|
+
var path = require("path");
|
|
10
|
+
|
|
11
|
+
var socketPath = process.argv[2];
|
|
12
|
+
if (!socketPath) {
|
|
13
|
+
console.error("[sdk-worker] Missing socket path argument");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- State ---
|
|
18
|
+
var sdkModule = null;
|
|
19
|
+
var queryInstance = null;
|
|
20
|
+
var messageQueue = null;
|
|
21
|
+
var abortController = null;
|
|
22
|
+
var pendingPermissions = {}; // requestId -> resolve
|
|
23
|
+
var pendingAskUser = {}; // toolUseId -> resolve
|
|
24
|
+
var pendingElicitations = {}; // requestId -> resolve
|
|
25
|
+
var conn = null;
|
|
26
|
+
var buffer = "";
|
|
27
|
+
|
|
28
|
+
// --- Message queue (same implementation as sdk-bridge.js) ---
|
|
29
|
+
function createMessageQueue() {
|
|
30
|
+
var queue = [];
|
|
31
|
+
var waiting = null;
|
|
32
|
+
var ended = false;
|
|
33
|
+
return {
|
|
34
|
+
push: function(msg) {
|
|
35
|
+
if (waiting) {
|
|
36
|
+
var resolve = waiting;
|
|
37
|
+
waiting = null;
|
|
38
|
+
resolve({ value: msg, done: false });
|
|
39
|
+
} else {
|
|
40
|
+
queue.push(msg);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
end: function() {
|
|
44
|
+
ended = true;
|
|
45
|
+
if (waiting) {
|
|
46
|
+
var resolve = waiting;
|
|
47
|
+
waiting = null;
|
|
48
|
+
resolve({ value: undefined, done: true });
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[Symbol.asyncIterator]: function() {
|
|
52
|
+
return {
|
|
53
|
+
next: function() {
|
|
54
|
+
if (queue.length > 0) {
|
|
55
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
56
|
+
}
|
|
57
|
+
if (ended) {
|
|
58
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
59
|
+
}
|
|
60
|
+
return new Promise(function(resolve) {
|
|
61
|
+
waiting = resolve;
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- SDK loader ---
|
|
70
|
+
function getSDK() {
|
|
71
|
+
if (!sdkModule) sdkModule = import("@anthropic-ai/claude-agent-sdk");
|
|
72
|
+
return sdkModule;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- IPC helpers ---
|
|
76
|
+
function sendToDaemon(msg) {
|
|
77
|
+
if (!conn || conn.destroyed) return;
|
|
78
|
+
try {
|
|
79
|
+
conn.write(JSON.stringify(msg) + "\n");
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error("[sdk-worker] Failed to send message:", e.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleMessage(msg) {
|
|
86
|
+
switch (msg.type) {
|
|
87
|
+
case "query_start":
|
|
88
|
+
handleQueryStart(msg);
|
|
89
|
+
break;
|
|
90
|
+
case "push_message":
|
|
91
|
+
handlePushMessage(msg);
|
|
92
|
+
break;
|
|
93
|
+
case "end_messages":
|
|
94
|
+
if (messageQueue) messageQueue.end();
|
|
95
|
+
break;
|
|
96
|
+
case "abort":
|
|
97
|
+
if (abortController) abortController.abort();
|
|
98
|
+
break;
|
|
99
|
+
case "set_model":
|
|
100
|
+
handleSetModel(msg);
|
|
101
|
+
break;
|
|
102
|
+
case "set_effort":
|
|
103
|
+
handleSetEffort(msg);
|
|
104
|
+
break;
|
|
105
|
+
case "set_permission_mode":
|
|
106
|
+
handleSetPermissionMode(msg);
|
|
107
|
+
break;
|
|
108
|
+
case "stop_task":
|
|
109
|
+
handleStopTask(msg);
|
|
110
|
+
break;
|
|
111
|
+
case "permission_response":
|
|
112
|
+
handlePermissionResponse(msg);
|
|
113
|
+
break;
|
|
114
|
+
case "ask_user_response":
|
|
115
|
+
handleAskUserResponse(msg);
|
|
116
|
+
break;
|
|
117
|
+
case "elicitation_response":
|
|
118
|
+
handleElicitationResponse(msg);
|
|
119
|
+
break;
|
|
120
|
+
case "warmup":
|
|
121
|
+
handleWarmup(msg);
|
|
122
|
+
break;
|
|
123
|
+
case "shutdown":
|
|
124
|
+
cleanup();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
console.error("[sdk-worker] Unknown message type:", msg.type);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- canUseTool: delegates to daemon via IPC ---
|
|
133
|
+
function canUseTool(toolName, input, opts) {
|
|
134
|
+
var requestId = crypto.randomUUID();
|
|
135
|
+
sendToDaemon({
|
|
136
|
+
type: "permission_request",
|
|
137
|
+
requestId: requestId,
|
|
138
|
+
toolName: toolName,
|
|
139
|
+
input: input,
|
|
140
|
+
toolUseId: opts.toolUseID || "",
|
|
141
|
+
decisionReason: opts.decisionReason || "",
|
|
142
|
+
});
|
|
143
|
+
return new Promise(function(resolve) {
|
|
144
|
+
pendingPermissions[requestId] = resolve;
|
|
145
|
+
if (opts.signal) {
|
|
146
|
+
opts.signal.addEventListener("abort", function() {
|
|
147
|
+
delete pendingPermissions[requestId];
|
|
148
|
+
resolve({ behavior: "deny", message: "Cancelled" });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- onElicitation: delegates to daemon via IPC ---
|
|
155
|
+
function onElicitation(request, opts) {
|
|
156
|
+
var requestId = crypto.randomUUID();
|
|
157
|
+
sendToDaemon({
|
|
158
|
+
type: "elicitation_request",
|
|
159
|
+
requestId: requestId,
|
|
160
|
+
serverName: request.serverName,
|
|
161
|
+
message: request.message,
|
|
162
|
+
mode: request.mode || "form",
|
|
163
|
+
url: request.url || null,
|
|
164
|
+
elicitationId: request.elicitationId || null,
|
|
165
|
+
requestedSchema: request.requestedSchema || null,
|
|
166
|
+
});
|
|
167
|
+
return new Promise(function(resolve) {
|
|
168
|
+
pendingElicitations[requestId] = resolve;
|
|
169
|
+
if (opts.signal) {
|
|
170
|
+
opts.signal.addEventListener("abort", function() {
|
|
171
|
+
delete pendingElicitations[requestId];
|
|
172
|
+
resolve({ action: "reject" });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handlePermissionResponse(msg) {
|
|
179
|
+
var resolve = pendingPermissions[msg.requestId];
|
|
180
|
+
if (resolve) {
|
|
181
|
+
delete pendingPermissions[msg.requestId];
|
|
182
|
+
resolve(msg.result);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handleAskUserResponse(msg) {
|
|
187
|
+
var resolve = pendingAskUser[msg.toolUseId];
|
|
188
|
+
if (resolve) {
|
|
189
|
+
delete pendingAskUser[msg.toolUseId];
|
|
190
|
+
resolve(msg.result);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleElicitationResponse(msg) {
|
|
195
|
+
var resolve = pendingElicitations[msg.requestId];
|
|
196
|
+
if (resolve) {
|
|
197
|
+
delete pendingElicitations[msg.requestId];
|
|
198
|
+
resolve(msg.result);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Query handling ---
|
|
203
|
+
async function handleQueryStart(msg) {
|
|
204
|
+
var sdk;
|
|
205
|
+
try {
|
|
206
|
+
sdk = await getSDK();
|
|
207
|
+
} catch (e) {
|
|
208
|
+
sendToDaemon({ type: "query_error", error: "Failed to load SDK: " + (e.message || e), exitCode: null, stderr: null });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
messageQueue = createMessageQueue();
|
|
213
|
+
abortController = new AbortController();
|
|
214
|
+
|
|
215
|
+
// Push the initial user message
|
|
216
|
+
if (msg.prompt) {
|
|
217
|
+
messageQueue.push(msg.prompt);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Build query options (callbacks are local, everything else from daemon)
|
|
221
|
+
var options = msg.options || {};
|
|
222
|
+
options.abortController = abortController;
|
|
223
|
+
options.canUseTool = function(toolName, input, toolOpts) {
|
|
224
|
+
// AskUserQuestion is handled specially: we send it as a separate IPC type
|
|
225
|
+
// so the daemon can use its own AskUserQuestion handling logic
|
|
226
|
+
if (toolName === "AskUserQuestion") {
|
|
227
|
+
var toolUseId = toolOpts.toolUseID || "";
|
|
228
|
+
sendToDaemon({
|
|
229
|
+
type: "ask_user_request",
|
|
230
|
+
toolUseId: toolUseId,
|
|
231
|
+
input: input,
|
|
232
|
+
});
|
|
233
|
+
return new Promise(function(resolve) {
|
|
234
|
+
pendingAskUser[toolUseId] = resolve;
|
|
235
|
+
if (toolOpts.signal) {
|
|
236
|
+
toolOpts.signal.addEventListener("abort", function() {
|
|
237
|
+
delete pendingAskUser[toolUseId];
|
|
238
|
+
resolve({ behavior: "deny", message: "Cancelled" });
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return canUseTool(toolName, input, toolOpts);
|
|
244
|
+
};
|
|
245
|
+
options.onElicitation = function(request, elicitOpts) {
|
|
246
|
+
return onElicitation(request, elicitOpts);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
queryInstance = sdk.query({
|
|
251
|
+
prompt: messageQueue,
|
|
252
|
+
options: options,
|
|
253
|
+
});
|
|
254
|
+
} catch (e) {
|
|
255
|
+
sendToDaemon({ type: "query_error", error: "Failed to create query: " + (e.message || e), exitCode: null, stderr: null });
|
|
256
|
+
queryInstance = null;
|
|
257
|
+
messageQueue = null;
|
|
258
|
+
abortController = null;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// If single-turn, end the message queue immediately
|
|
263
|
+
if (msg.singleTurn) {
|
|
264
|
+
messageQueue.end();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Stream events to daemon
|
|
268
|
+
try {
|
|
269
|
+
for await (var event of queryInstance) {
|
|
270
|
+
sendToDaemon({ type: "sdk_event", event: event });
|
|
271
|
+
}
|
|
272
|
+
sendToDaemon({ type: "query_done" });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
var errMsg = err.message || String(err);
|
|
275
|
+
sendToDaemon({
|
|
276
|
+
type: "query_error",
|
|
277
|
+
error: errMsg,
|
|
278
|
+
exitCode: err.exitCode != null ? err.exitCode : null,
|
|
279
|
+
stderr: err.stderr || null,
|
|
280
|
+
});
|
|
281
|
+
} finally {
|
|
282
|
+
queryInstance = null;
|
|
283
|
+
messageQueue = null;
|
|
284
|
+
abortController = null;
|
|
285
|
+
pendingPermissions = {};
|
|
286
|
+
pendingAskUser = {};
|
|
287
|
+
pendingElicitations = {};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function handlePushMessage(msg) {
|
|
292
|
+
if (!messageQueue) return;
|
|
293
|
+
messageQueue.push(msg.content);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function handleSetModel(msg) {
|
|
297
|
+
if (!queryInstance) return;
|
|
298
|
+
try {
|
|
299
|
+
await queryInstance.setModel(msg.model);
|
|
300
|
+
sendToDaemon({ type: "model_changed", model: msg.model });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
sendToDaemon({ type: "worker_error", error: "Failed to set model: " + (e.message || e) });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function handleSetEffort(msg) {
|
|
307
|
+
if (!queryInstance) return;
|
|
308
|
+
try {
|
|
309
|
+
await queryInstance.setEffort(msg.effort);
|
|
310
|
+
sendToDaemon({ type: "effort_changed", effort: msg.effort });
|
|
311
|
+
} catch (e) {
|
|
312
|
+
sendToDaemon({ type: "worker_error", error: "Failed to set effort: " + (e.message || e) });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function handleSetPermissionMode(msg) {
|
|
317
|
+
if (!queryInstance) return;
|
|
318
|
+
try {
|
|
319
|
+
await queryInstance.setPermissionMode(msg.mode);
|
|
320
|
+
sendToDaemon({ type: "permission_mode_changed", mode: msg.mode });
|
|
321
|
+
} catch (e) {
|
|
322
|
+
sendToDaemon({ type: "worker_error", error: "Failed to set permission mode: " + (e.message || e) });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function handleStopTask(msg) {
|
|
327
|
+
if (!queryInstance) return;
|
|
328
|
+
try {
|
|
329
|
+
await queryInstance.stopTask(msg.taskId);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.error("[sdk-worker] stopTask error:", e.message);
|
|
332
|
+
}
|
|
333
|
+
// Also abort as fallback (matches daemon behavior)
|
|
334
|
+
if (abortController) {
|
|
335
|
+
abortController.abort();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- Warmup ---
|
|
340
|
+
async function handleWarmup(msg) {
|
|
341
|
+
var sdk;
|
|
342
|
+
try {
|
|
343
|
+
sdk = await getSDK();
|
|
344
|
+
} catch (e) {
|
|
345
|
+
sendToDaemon({ type: "warmup_error", error: "Failed to load SDK: " + (e.message || e) });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
var ac = new AbortController();
|
|
350
|
+
var mq = createMessageQueue();
|
|
351
|
+
mq.push({ type: "user", message: { role: "user", content: [{ type: "text", text: "hi" }] } });
|
|
352
|
+
mq.end();
|
|
353
|
+
|
|
354
|
+
var warmupOptions = msg.options || {};
|
|
355
|
+
warmupOptions.abortController = ac;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
var stream = sdk.query({
|
|
359
|
+
prompt: mq,
|
|
360
|
+
options: warmupOptions,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
for await (var event of stream) {
|
|
364
|
+
if (event.type === "system" && event.subtype === "init") {
|
|
365
|
+
var result = {
|
|
366
|
+
slashCommands: event.slash_commands || [],
|
|
367
|
+
model: event.model || "",
|
|
368
|
+
skills: event.skills || [],
|
|
369
|
+
fastModeState: event.fast_mode_state || null,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Fetch available models before aborting
|
|
373
|
+
try {
|
|
374
|
+
var models = await stream.supportedModels();
|
|
375
|
+
result.models = models || [];
|
|
376
|
+
} catch (e) {
|
|
377
|
+
result.models = [];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
sendToDaemon({ type: "warmup_done", result: result });
|
|
381
|
+
ac.abort();
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch (e) {
|
|
386
|
+
if (e && e.name !== "AbortError" && !(e.message && e.message.indexOf("aborted") !== -1)) {
|
|
387
|
+
sendToDaemon({ type: "warmup_error", error: "Warmup failed: " + (e.message || e) });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// --- Cleanup ---
|
|
393
|
+
function cleanup() {
|
|
394
|
+
if (abortController) {
|
|
395
|
+
try { abortController.abort(); } catch (e) {}
|
|
396
|
+
}
|
|
397
|
+
if (messageQueue) {
|
|
398
|
+
try { messageQueue.end(); } catch (e) {}
|
|
399
|
+
}
|
|
400
|
+
if (conn && !conn.destroyed) {
|
|
401
|
+
try { conn.end(); } catch (e) {}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Connect to daemon socket ---
|
|
406
|
+
conn = net.connect(socketPath, function() {
|
|
407
|
+
sendToDaemon({ type: "ready" });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
conn.on("data", function(chunk) {
|
|
411
|
+
buffer += chunk.toString();
|
|
412
|
+
var lines = buffer.split("\n");
|
|
413
|
+
buffer = lines.pop(); // keep incomplete line in buffer
|
|
414
|
+
for (var i = 0; i < lines.length; i++) {
|
|
415
|
+
if (!lines[i].trim()) continue;
|
|
416
|
+
try {
|
|
417
|
+
var msg = JSON.parse(lines[i]);
|
|
418
|
+
handleMessage(msg);
|
|
419
|
+
} catch (e) {
|
|
420
|
+
console.error("[sdk-worker] Failed to parse message:", e.message);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
conn.on("error", function(err) {
|
|
426
|
+
console.error("[sdk-worker] Socket error:", err.message);
|
|
427
|
+
cleanup();
|
|
428
|
+
process.exit(1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
conn.on("close", function() {
|
|
432
|
+
console.log("[sdk-worker] Socket closed, shutting down");
|
|
433
|
+
cleanup();
|
|
434
|
+
process.exit(0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Handle process signals
|
|
438
|
+
process.on("SIGTERM", function() {
|
|
439
|
+
cleanup();
|
|
440
|
+
process.exit(0);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
process.on("SIGINT", function() {
|
|
444
|
+
cleanup();
|
|
445
|
+
process.exit(0);
|
|
446
|
+
});
|