clay-server 4.1.0 → 4.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/daemon-project-access.js +14 -0
  2. package/lib/daemon.js +7 -0
  3. package/lib/project-connection.js +5 -2
  4. package/lib/project-full-access.js +69 -56
  5. package/lib/project-issue-launch.js +3 -1
  6. package/lib/project-sessions.js +158 -32
  7. package/lib/project-user-message.js +5 -2
  8. package/lib/project.js +5 -0
  9. package/lib/public/app.js +7 -0
  10. package/lib/public/css/command-palette.css +51 -33
  11. package/lib/public/css/filebrowser.css +20 -0
  12. package/lib/public/css/issues.css +4 -4
  13. package/lib/public/css/messages.css +27 -0
  14. package/lib/public/css/overlays.css +60 -0
  15. package/lib/public/css/pane.css +21 -4
  16. package/lib/public/css/pending-message-queue.css +5 -0
  17. package/lib/public/index.html +18 -3
  18. package/lib/public/modules/app-connection.js +4 -0
  19. package/lib/public/modules/app-header.js +18 -25
  20. package/lib/public/modules/app-messages.js +24 -7
  21. package/lib/public/modules/app-panels.js +22 -11
  22. package/lib/public/modules/clay-file-links.js +7 -0
  23. package/lib/public/modules/clay-issue-links.js +1 -0
  24. package/lib/public/modules/clay-log-links.js +5 -0
  25. package/lib/public/modules/github-links.js +115 -0
  26. package/lib/public/modules/markdown.js +9 -2
  27. package/lib/public/modules/mcp-ui.js +154 -17
  28. package/lib/public/modules/pane-bridge.js +2 -0
  29. package/lib/public/modules/pane-file-bridge.js +69 -0
  30. package/lib/public/modules/pending-message-queue-model.js +6 -0
  31. package/lib/public/modules/pending-message-queue-recovery.js +73 -0
  32. package/lib/public/modules/pending-message-queue.js +67 -64
  33. package/lib/public/modules/permission-control.js +187 -0
  34. package/lib/public/modules/project-session-visibility-settings.js +138 -0
  35. package/lib/public/modules/project-settings.js +5 -0
  36. package/lib/public/modules/sidebar-projects.js +14 -3
  37. package/lib/public/modules/split-view.js +29 -29
  38. package/lib/sdk-bridge.js +95 -40
  39. package/lib/server.js +8 -1
  40. package/lib/session-permission-mode.js +42 -0
  41. package/lib/session-visibility.js +62 -1
  42. package/lib/sessions.js +16 -1
  43. package/lib/ws-schema.js +4 -0
  44. package/lib/yoke/adapters/claude-worker-policy.js +67 -0
  45. package/lib/yoke/adapters/claude-worker.js +47 -7
  46. package/lib/yoke/adapters/claude.js +114 -6
  47. package/package.json +1 -1
package/lib/ws-schema.js CHANGED
@@ -27,6 +27,8 @@ var schema = {
27
27
  "reorder_session_bookmarks": { direction: "c2s", handler: "lib/project-sessions.js", description: "Reorder favorited sessions within the favorites area" },
28
28
  "bulk_delete_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a group of sessions at once" },
29
29
  "set_session_visibility": { direction: "c2s", handler: "lib/project-sessions.js", description: "Show or hide a session in the sidebar" },
30
+ "set_project_session_visibility_default": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set private/shared default for new ordinary project sessions" },
31
+ "set_project_session_visibility_default_result": { direction: "s2c", handler: "lib/public/modules/project-session-visibility-settings.js", description: "Correlated project session visibility default update result" },
30
32
  "search_sessions": { direction: "c2s", handler: "lib/project-sessions.js", description: "Search session titles" },
31
33
  "search_session_content": { direction: "c2s", handler: "lib/project-sessions.js", description: "Full-text search within a session" },
32
34
  "load_more_history": { direction: "c2s", handler: "lib/project-sessions.js", description: "Request older history entries for the current session" },
@@ -250,6 +252,7 @@ var schema = {
250
252
  "set_server_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default model" },
251
253
  "set_project_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the project default model" },
252
254
  "set_permission_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set permission mode for the current session" },
255
+ "permission_mode_result": { direction: "s2c", handler: "lib/public/modules/permission-control.js", description: "Correlated result for an exact project/session permission mode change" },
253
256
  "set_session_full_access": { direction: "c2s", handler: "lib/project-sessions.js", description: "Toggle Clay-managed permission skipping for a GUI session" },
254
257
  "session_full_access_changed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "A session's full-access state changed" },
255
258
  "session_full_access_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of toggling session full access" },
@@ -267,6 +270,7 @@ var schema = {
267
270
  "autonomous_run_state": { direction: "s2c", handler: "lib/public/modules/autonomous-run.js", description: "Synchronized Until complete run state" },
268
271
  "reload_skills": { direction: "c2s", handler: "lib/project-sessions.js", description: "Hot-reload skills/MCP/agents on the active query without a restart" },
269
272
  "set_mcp_permission_mode_override": { direction: "c2s", handler: "lib/project-sessions.js", description: "Override permission mode (default/auto/null) for one MCP server" },
273
+ "mcp_permission_mode_result": { direction: "s2c", handler: "lib/public/modules/mcp-ui.js", description: "Correlated result for one session MCP AI review policy change" },
270
274
  "set_server_default_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default permission mode" },
271
275
  "set_project_default_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the project default permission mode" },
272
276
  "set_effort": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set effort level for the current session" },
@@ -0,0 +1,67 @@
1
+ // Bounded fail-closed lifecycle for worker PreToolUse policy IPC requests.
2
+
3
+ function denied(message) {
4
+ return { behavior: "deny", message: message };
5
+ }
6
+
7
+ function createPolicyBroker(options) {
8
+ options = options || {};
9
+ var send = options.send;
10
+ var timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000;
11
+ var pending = {};
12
+
13
+ function settle(requestId, result) {
14
+ var record = pending[requestId];
15
+ if (!record) return false;
16
+ delete pending[requestId];
17
+ clearTimeout(record.timer);
18
+ if (record.signal && record.onAbort && typeof record.signal.removeEventListener === "function") {
19
+ record.signal.removeEventListener("abort", record.onAbort);
20
+ }
21
+ record.resolve(result);
22
+ return true;
23
+ }
24
+
25
+ function request(message, signal) {
26
+ return new Promise(function(resolve) {
27
+ if (signal && signal.aborted) {
28
+ resolve(denied("Permission policy cancelled."));
29
+ return;
30
+ }
31
+ var requestId = message.requestId;
32
+ var record = { resolve: resolve, signal: signal || null, onAbort: null, timer: null };
33
+ pending[requestId] = record;
34
+ record.onAbort = function() {
35
+ settle(requestId, denied("Permission policy cancelled."));
36
+ };
37
+ if (signal && typeof signal.addEventListener === "function") {
38
+ signal.addEventListener("abort", record.onAbort, { once: true });
39
+ }
40
+ record.timer = setTimeout(function() {
41
+ settle(requestId, denied("Permission policy timed out."));
42
+ }, timeoutMs);
43
+ var sent = false;
44
+ try { sent = send(message) === true; }
45
+ catch (error) { sent = false; }
46
+ if (!sent) settle(requestId, denied("Permission policy IPC is unavailable."));
47
+ });
48
+ }
49
+
50
+ function respond(message) {
51
+ var result = message.error ? denied(message.error) : message.result;
52
+ return settle(message.requestId, result || null);
53
+ }
54
+
55
+ function clear(message) {
56
+ var ids = Object.keys(pending);
57
+ for (var i = 0; i < ids.length; i++) settle(ids[i], denied(message || "Permission policy IPC closed."));
58
+ }
59
+
60
+ function pendingCount() {
61
+ return Object.keys(pending).length;
62
+ }
63
+
64
+ return { request: request, respond: respond, clear: clear, pendingCount: pendingCount };
65
+ }
66
+
67
+ module.exports = { createPolicyBroker: createPolicyBroker };
@@ -18,6 +18,7 @@ var crypto = require("crypto");
18
18
  var path = require("path");
19
19
  var claudeContextUsage = require(path.join(__dirname, "../claude-context-usage"));
20
20
  var claudeModelSwitch = require(path.join(__dirname, "../claude-model-switch"));
21
+ var createPolicyBroker = require("./claude-worker-policy").createPolicyBroker;
21
22
  var fs = require("fs");
22
23
 
23
24
  var socketPath = process.argv[2];
@@ -244,14 +245,18 @@ function buildMcpServersFromDescriptors(descriptors, sdk) {
244
245
 
245
246
  // --- IPC helpers ---
246
247
  function sendToDaemon(msg) {
247
- if (!conn || conn.destroyed) return;
248
+ if (!conn || conn.destroyed) return false;
248
249
  try {
249
250
  conn.write(JSON.stringify(msg) + "\n");
251
+ return true;
250
252
  } catch (e) {
251
253
  console.error("[sdk-worker] Failed to send message:", e.message);
254
+ return false;
252
255
  }
253
256
  }
254
257
 
258
+ var preToolPolicyBroker = createPolicyBroker({ send: sendToDaemon, timeoutMs: 10000 });
259
+
255
260
  function handleMessage(msg) {
256
261
  try { require("fs").writeSync(2, "[sdk-worker] MSG: " + msg.type + "\n"); } catch (e) {}
257
262
  switch (msg.type) {
@@ -294,6 +299,9 @@ function handleMessage(msg) {
294
299
  case "permission_response":
295
300
  handlePermissionResponse(msg);
296
301
  break;
302
+ case "pre_tool_policy_response":
303
+ handlePreToolPolicyResponse(msg);
304
+ break;
297
305
  case "ask_user_response":
298
306
  handleAskUserResponse(msg);
299
307
  break;
@@ -394,6 +402,16 @@ function handlePermissionResponse(msg) {
394
402
  }
395
403
  }
396
404
 
405
+ function handlePreToolPolicyResponse(msg) {
406
+ preToolPolicyBroker.respond(msg);
407
+ }
408
+
409
+ function preToolUsePolicy(toolName, input, toolUseId, signal) {
410
+ var requestId = crypto.randomUUID();
411
+ return preToolPolicyBroker.request({ type: "pre_tool_policy_request", requestId: requestId, toolName: toolName,
412
+ input: input || {}, toolUseId: toolUseId || "" }, signal);
413
+ }
414
+
397
415
  function handleAskUserResponse(msg) {
398
416
  var resolve = pendingAskUser[msg.toolUseId];
399
417
  if (resolve) {
@@ -515,6 +533,22 @@ async function handleQueryStart(msg) {
515
533
  };
516
534
  var nativeUserInput = options.userInputMode !== "fallback";
517
535
  delete options.userInputMode;
536
+ var usePreToolPolicy = options.clayPreToolUsePolicy === true;
537
+ delete options.clayPreToolUsePolicy;
538
+ if (usePreToolPolicy) {
539
+ options.hooks = options.hooks || {};
540
+ options.hooks.PreToolUse = (options.hooks.PreToolUse || []).concat([{
541
+ hooks: [async function(input, toolUseId, hookOpts) {
542
+ input = input || {};
543
+ var decision = await preToolUsePolicy(input.tool_name, input.tool_input, toolUseId || input.tool_use_id, hookOpts && hookOpts.signal);
544
+ if (!decision) return { continue: true };
545
+ return { hookSpecificOutput: { hookEventName: "PreToolUse",
546
+ permissionDecision: decision.behavior === "deny" ? "deny" : "allow",
547
+ permissionDecisionReason: decision.message || "Clay session policy",
548
+ updatedInput: decision.updatedInput } };
549
+ }],
550
+ }]);
551
+ }
518
552
  options.canUseTool = function(toolName, input, toolOpts) {
519
553
  // AskUserQuestion is handled specially: we send it as a separate IPC type
520
554
  // so the daemon can use its own AskUserQuestion handling logic
@@ -626,6 +660,7 @@ async function handleQueryStart(msg) {
626
660
  messageQueue = null;
627
661
  abortController = null;
628
662
  pendingPermissions = {};
663
+ preToolPolicyBroker.clear("Permission policy query ended.");
629
664
  pendingAskUser = {};
630
665
  pendingElicitations = {};
631
666
  }
@@ -657,12 +692,12 @@ async function handleSetEffort(msg) {
657
692
  }
658
693
 
659
694
  async function handleSetPermissionMode(msg) {
660
- if (!queryInstance) return;
695
+ if (!queryInstance) { sendToDaemon({ type: "permission_mode_result", requestId: msg.requestId, error: "No active query." }); return; }
661
696
  try {
662
697
  await queryInstance.setPermissionMode(msg.mode);
663
- sendToDaemon({ type: "permission_mode_changed", mode: msg.mode });
698
+ sendToDaemon({ type: "permission_mode_result", requestId: msg.requestId });
664
699
  } catch (e) {
665
- sendToDaemon({ type: "worker_error", error: "Failed to set permission mode: " + (e.message || e) });
700
+ sendToDaemon({ type: "permission_mode_result", requestId: msg.requestId, error: "Failed to set permission mode: " + (e.message || e) });
666
701
  }
667
702
  }
668
703
 
@@ -705,11 +740,15 @@ async function handleReloadSkills(msg) {
705
740
  }
706
741
 
707
742
  async function handleSetMcpPermissionModeOverride(msg) {
708
- if (!queryInstance || typeof queryInstance.setMcpPermissionModeOverride !== "function") return;
743
+ if (!queryInstance || typeof queryInstance.setMcpPermissionModeOverride !== "function") {
744
+ sendToDaemon({ type: "mcp_permission_mode_result", requestId: msg.requestId, error: "MCP permission controls are unavailable." });
745
+ return;
746
+ }
709
747
  try {
710
- await queryInstance.setMcpPermissionModeOverride(msg.serverName, msg.mode);
748
+ var result = await queryInstance.setMcpPermissionModeOverride(msg.serverName, msg.mode);
749
+ sendToDaemon({ type: "mcp_permission_mode_result", requestId: msg.requestId, result: result || null });
711
750
  } catch (e) {
712
- console.error("[sdk-worker] setMcpPermissionModeOverride error:", e.message);
751
+ sendToDaemon({ type: "mcp_permission_mode_result", requestId: msg.requestId, error: e.message || String(e) });
713
752
  }
714
753
  }
715
754
 
@@ -789,6 +828,7 @@ async function handleWarmup(msg) {
789
828
  // --- Cleanup ---
790
829
  var _exitScheduled = false;
791
830
  function cleanup() {
831
+ preToolPolicyBroker.clear("Permission policy worker closed.");
792
832
  if (_keepAlive) {
793
833
  try { clearInterval(_keepAlive); } catch (e) {}
794
834
  }
@@ -67,6 +67,29 @@ function applySystemPromptSnapshot(systemPrompt, enabled) {
67
67
  return systemPrompt;
68
68
  }
69
69
 
70
+ function attachPreToolUsePolicy(options, policy) {
71
+ if (typeof policy !== "function") return;
72
+ options.hooks = options.hooks || {};
73
+ options.hooks.PreToolUse = (options.hooks.PreToolUse || []).concat([{
74
+ hooks: [async function(input, toolUseId, hookOpts) {
75
+ input = input || {};
76
+ var decision = await policy(input.tool_name, input.tool_input || {}, {
77
+ toolUseID: toolUseId || input.tool_use_id,
78
+ signal: hookOpts && hookOpts.signal,
79
+ });
80
+ if (!decision) return { continue: true };
81
+ return {
82
+ hookSpecificOutput: {
83
+ hookEventName: "PreToolUse",
84
+ permissionDecision: decision.behavior === "deny" ? "deny" : "allow",
85
+ permissionDecisionReason: decision.message || "Clay session policy",
86
+ updatedInput: decision.updatedInput,
87
+ },
88
+ };
89
+ }],
90
+ }]);
91
+ }
92
+
70
93
  function getSharedSkillPlugins(homeDir, existingPlugins, cwd, env) {
71
94
  var plugins = Array.isArray(existingPlugins) ? existingPlugins.slice() : [];
72
95
  // undefined means normal in-process Claude and may use the configured
@@ -122,6 +145,7 @@ function buildWorkerQueryOptions(queryOpts, claudeOpts, workerCwd) {
122
145
  if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
123
146
  queryOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
124
147
  if (queryOpts.onModelSwitch) queryOptions.reportModelSwitches = true;
148
+ if (queryOpts.preToolUsePolicy) queryOptions.clayPreToolUsePolicy = true;
125
149
  if (queryOpts.model) queryOptions.model = queryOpts.model;
126
150
  if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
127
151
  if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
@@ -316,11 +340,13 @@ function flattenEvent(raw) {
316
340
  base.skills = raw.skills;
317
341
  base.slashCommands = raw.slash_commands;
318
342
  base.fastModeState = raw.fast_mode_state || null;
343
+ base.permissionMode = raw.permissionMode || raw.permission_mode || null;
319
344
  return base;
320
345
  }
321
346
  if (raw.subtype === "status") {
322
347
  base.yokeType = "status";
323
348
  base.status = raw.status;
349
+ base.permissionMode = raw.permissionMode || raw.permission_mode || null;
324
350
  return base;
325
351
  }
326
352
  // API retry notices are transient transport state, not conversation
@@ -1015,7 +1041,7 @@ function serializeWorkerValue(value, seen) {
1015
1041
  // in-process QueryHandle. This allows processQueryStream to iterate a worker
1016
1042
  // query identically to an in-process query.
1017
1043
 
1018
- function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool, onUserDialog, onUserInputRequest, onModelSwitch) {
1044
+ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool, onUserDialog, onUserInputRequest, onModelSwitch, preToolUsePolicy) {
1019
1045
  var contextRequests = claudeContextUsage.createRequests(worker);
1020
1046
  var correlation = claudeCorrelation.createCorrelation();
1021
1047
  // Async iterable state
@@ -1028,6 +1054,23 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1028
1054
  // result from the worker (e.g. rewindFiles). Each entry is keyed by a
1029
1055
  // requestId and holds { resolve, reject } of the in-flight Promise.
1030
1056
  var pendingRewinds = {};
1057
+ var pendingPermissionModes = {};
1058
+ var pendingMcpPermissionModes = {};
1059
+
1060
+ function rejectPendingPermissionModes(error) {
1061
+ Object.keys(pendingPermissionModes).forEach(function(id) {
1062
+ var pending = pendingPermissionModes[id];
1063
+ delete pendingPermissionModes[id];
1064
+ clearTimeout(pending.timer);
1065
+ pending.reject(error);
1066
+ });
1067
+ Object.keys(pendingMcpPermissionModes).forEach(function(id) {
1068
+ var pending = pendingMcpPermissionModes[id];
1069
+ delete pendingMcpPermissionModes[id];
1070
+ clearTimeout(pending.timer);
1071
+ pending.reject(error);
1072
+ });
1073
+ }
1031
1074
 
1032
1075
  function pushToIter(value) {
1033
1076
  if (iterEnded) return;
@@ -1087,6 +1130,20 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1087
1130
  }
1088
1131
  break;
1089
1132
 
1133
+ case "pre_tool_policy_request":
1134
+ if (preToolUsePolicy) {
1135
+ Promise.resolve().then(function() {
1136
+ return preToolUsePolicy(msg.toolName, msg.input || {}, { toolUseID: msg.toolUseId });
1137
+ }).then(function(result) {
1138
+ worker.send({ type: "pre_tool_policy_response", requestId: msg.requestId, result: result || null });
1139
+ }).catch(function(error) {
1140
+ worker.send({ type: "pre_tool_policy_response", requestId: msg.requestId, error: error.message || String(error) });
1141
+ });
1142
+ } else {
1143
+ worker.send({ type: "pre_tool_policy_response", requestId: msg.requestId, result: null });
1144
+ }
1145
+ break;
1146
+
1090
1147
  case "ask_user_request":
1091
1148
  if (onUserInputRequest) {
1092
1149
  claudeUserInput.ask(onUserInputRequest, msg.input, {
@@ -1164,6 +1221,24 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1164
1221
  case "context_usage_response":
1165
1222
  contextRequests.receive(msg);
1166
1223
  break;
1224
+ case "permission_mode_result": {
1225
+ var modePending = pendingPermissionModes[msg.requestId];
1226
+ if (!modePending) break;
1227
+ delete pendingPermissionModes[msg.requestId];
1228
+ clearTimeout(modePending.timer);
1229
+ if (msg.error) modePending.reject(new Error(msg.error));
1230
+ else modePending.resolve();
1231
+ break;
1232
+ }
1233
+ case "mcp_permission_mode_result": {
1234
+ var mcpPending = pendingMcpPermissionModes[msg.requestId];
1235
+ if (!mcpPending) break;
1236
+ delete pendingMcpPermissionModes[msg.requestId];
1237
+ clearTimeout(mcpPending.timer);
1238
+ if (msg.error) mcpPending.reject(new Error(msg.error));
1239
+ else mcpPending.resolve(msg.result || null);
1240
+ break;
1241
+ }
1167
1242
  case "context_usage":
1168
1243
  case "model_changed":
1169
1244
  case "effort_changed":
@@ -1184,12 +1259,14 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1184
1259
  }
1185
1260
 
1186
1261
  case "query_done":
1262
+ rejectPendingPermissionModes(new Error("Worker query ended while changing permission mode"));
1187
1263
  console.log("[yoke/claude] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
1188
1264
  worker._queryEnded = true;
1189
1265
  endIter();
1190
1266
  break;
1191
1267
 
1192
1268
  case "query_error": {
1269
+ rejectPendingPermissionModes(new Error(msg.error || "Worker exited while changing permission mode"));
1193
1270
  console.log("[yoke/claude] IPC query_error received, pid=" + (worker.process ? worker.process.pid : "?") + " _fallback=" + !!msg._fallback + " _queryEnded=" + worker._queryEnded + " error=" + (msg.error || "").substring(0, 100));
1194
1271
  // Skip fallback errors from exit handler if we already handled the real one
1195
1272
  if (msg._fallback && worker._queryEnded) break;
@@ -1283,8 +1360,22 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1283
1360
  },
1284
1361
 
1285
1362
  setPermissionMode: function(mode) {
1286
- worker.send({ type: "set_permission_mode", mode: mode });
1287
- return Promise.resolve();
1363
+ return new Promise(function(resolve, reject) {
1364
+ var requestId = crypto.randomUUID();
1365
+ var timer = setTimeout(function() {
1366
+ if (!pendingPermissionModes[requestId]) return;
1367
+ delete pendingPermissionModes[requestId];
1368
+ reject(new Error("Timed out changing permission mode in worker."));
1369
+ }, worker.permissionModeTimeoutMs || 10000);
1370
+ pendingPermissionModes[requestId] = { resolve: resolve, reject: reject, timer: timer };
1371
+ try {
1372
+ if (worker.send({ type: "set_permission_mode", mode: mode, requestId: requestId }) !== true) {
1373
+ delete pendingPermissionModes[requestId]; clearTimeout(timer); reject(new Error("Worker is unavailable."));
1374
+ }
1375
+ } catch (error) {
1376
+ delete pendingPermissionModes[requestId]; clearTimeout(timer); reject(error);
1377
+ }
1378
+ });
1288
1379
  },
1289
1380
 
1290
1381
  stopTask: function(taskId) {
@@ -1298,8 +1389,22 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1298
1389
  },
1299
1390
 
1300
1391
  setMcpPermissionModeOverride: function(serverName, mode) {
1301
- worker.send({ type: "set_mcp_permission_mode_override", serverName: serverName, mode: mode });
1302
- return Promise.resolve(null);
1392
+ return new Promise(function(resolve, reject) {
1393
+ var requestId = crypto.randomUUID();
1394
+ var timer = setTimeout(function() {
1395
+ if (!pendingMcpPermissionModes[requestId]) return;
1396
+ delete pendingMcpPermissionModes[requestId];
1397
+ reject(new Error("Timed out changing MCP permission mode in worker."));
1398
+ }, worker.permissionModeTimeoutMs || 10000);
1399
+ pendingMcpPermissionModes[requestId] = { resolve: resolve, reject: reject, timer: timer };
1400
+ try {
1401
+ if (worker.send({ type: "set_mcp_permission_mode_override", serverName: serverName, mode: mode, requestId: requestId }) !== true) {
1402
+ delete pendingMcpPermissionModes[requestId]; clearTimeout(timer); reject(new Error("Worker is unavailable."));
1403
+ }
1404
+ } catch (error) {
1405
+ delete pendingMcpPermissionModes[requestId]; clearTimeout(timer); reject(error);
1406
+ }
1407
+ });
1303
1408
  },
1304
1409
 
1305
1410
  getContextUsage: function(options) {
@@ -1326,6 +1431,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1326
1431
  },
1327
1432
 
1328
1433
  close: function() {
1434
+ rejectPendingPermissionModes(new Error("Worker query closed while changing permission mode"));
1329
1435
  // End the iterator
1330
1436
  endIter();
1331
1437
  // Send end_messages to worker
@@ -1625,6 +1731,7 @@ function createClaudeAdapter(opts) {
1625
1731
  if (queryOpts.canUseTool || nativeUserInputHandler) {
1626
1732
  sdkOptions.canUseTool = claudeUserInput.canUseTool(Object.assign({}, queryOpts, { onUserInputRequest: nativeUserInputHandler }));
1627
1733
  }
1734
+ attachPreToolUsePolicy(sdkOptions, queryOpts.preToolUsePolicy);
1628
1735
  if (nativeUserInputHandler) {
1629
1736
  sdkOptions.onElicitation = function (request, elicitOpts) { return claudeUserInput.elicitation(nativeUserInputHandler, request, elicitOpts); };
1630
1737
  } else if (queryOpts.userInputMode !== "fallback" && queryOpts.onElicitation) sdkOptions.onElicitation = queryOpts.onElicitation;
@@ -1795,7 +1902,7 @@ function createClaudeAdapter(opts) {
1795
1902
  // Create the worker query handle (sets up message handler on worker)
1796
1903
  var workerUserInputHandler = queryOpts.userInputMode === "fallback" ? null : queryOpts.onUserInputRequest;
1797
1904
  var workerElicitationHandler = queryOpts.userInputMode === "fallback" ? null : queryOpts.onElicitation;
1798
- var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, workerElicitationHandler, queryOpts.callMcpTool, queryOpts.onUserDialog, workerUserInputHandler, queryOpts.onModelSwitch);
1905
+ var handle = createWorkerQueryHandle(worker, queryOpts.canUseTool, workerElicitationHandler, queryOpts.callMcpTool, queryOpts.onUserDialog, workerUserInputHandler, queryOpts.onModelSwitch, queryOpts.preToolUsePolicy);
1799
1906
 
1800
1907
  // Wait for worker to be ready before sending query_start
1801
1908
  if (!reusingWorker) {
@@ -2063,5 +2170,6 @@ module.exports = {
2063
2170
  canReuseWorker: canReuseWorker,
2064
2171
  buildWorkerQueryOptions: buildWorkerQueryOptions,
2065
2172
  normalizeEvent: flattenEvent,
2173
+ attachPreToolUsePolicy: attachPreToolUsePolicy,
2066
2174
  },
2067
2175
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0",
3
+ "version": "4.2.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"