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.
- package/lib/daemon-project-access.js +14 -0
- package/lib/daemon.js +7 -0
- package/lib/project-connection.js +5 -2
- package/lib/project-full-access.js +69 -56
- package/lib/project-issue-launch.js +3 -1
- package/lib/project-sessions.js +158 -32
- package/lib/project-user-message.js +5 -2
- package/lib/project.js +5 -0
- package/lib/public/app.js +7 -0
- package/lib/public/css/command-palette.css +51 -33
- package/lib/public/css/filebrowser.css +20 -0
- package/lib/public/css/issues.css +4 -4
- package/lib/public/css/messages.css +27 -0
- package/lib/public/css/overlays.css +60 -0
- package/lib/public/css/pane.css +21 -4
- package/lib/public/css/pending-message-queue.css +5 -0
- package/lib/public/index.html +18 -3
- package/lib/public/modules/app-connection.js +4 -0
- package/lib/public/modules/app-header.js +18 -25
- package/lib/public/modules/app-messages.js +24 -7
- package/lib/public/modules/app-panels.js +22 -11
- package/lib/public/modules/clay-file-links.js +7 -0
- package/lib/public/modules/clay-issue-links.js +1 -0
- package/lib/public/modules/clay-log-links.js +5 -0
- package/lib/public/modules/github-links.js +115 -0
- package/lib/public/modules/markdown.js +9 -2
- package/lib/public/modules/mcp-ui.js +154 -17
- package/lib/public/modules/pane-bridge.js +2 -0
- package/lib/public/modules/pane-file-bridge.js +69 -0
- package/lib/public/modules/pending-message-queue-model.js +6 -0
- package/lib/public/modules/pending-message-queue-recovery.js +73 -0
- package/lib/public/modules/pending-message-queue.js +67 -64
- package/lib/public/modules/permission-control.js +187 -0
- package/lib/public/modules/project-session-visibility-settings.js +138 -0
- package/lib/public/modules/project-settings.js +5 -0
- package/lib/public/modules/sidebar-projects.js +14 -3
- package/lib/public/modules/split-view.js +29 -29
- package/lib/sdk-bridge.js +95 -40
- package/lib/server.js +8 -1
- package/lib/session-permission-mode.js +42 -0
- package/lib/session-visibility.js +62 -1
- package/lib/sessions.js +16 -1
- package/lib/ws-schema.js +4 -0
- package/lib/yoke/adapters/claude-worker-policy.js +67 -0
- package/lib/yoke/adapters/claude-worker.js +47 -7
- package/lib/yoke/adapters/claude.js +114 -6
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sidebar-projects.js - Project icon strip, context menus, emoji picker, drag-and-drop, worktree modal
|
|
2
2
|
// Extracted from sidebar.js (PR-36)
|
|
3
3
|
|
|
4
|
-
import { escapeHtml } from './utils.js';
|
|
4
|
+
import { escapeHtml, showToast } from './utils.js';
|
|
5
5
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
6
6
|
import { openProjectSettings } from './project-settings.js';
|
|
7
7
|
import { triggerShare } from './qrcode.js';
|
|
@@ -415,6 +415,11 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
415
415
|
closeProjectCtxMenu();
|
|
416
416
|
if (closeUserCtxMenu) closeUserCtxMenu();
|
|
417
417
|
closeEmojiPicker();
|
|
418
|
+
var projects = getCachedProjects();
|
|
419
|
+
var targetProject = null;
|
|
420
|
+
for (var projectIndex = 0; projectIndex < projects.length; projectIndex++) {
|
|
421
|
+
if (projects[projectIndex].slug === slug) { targetProject = projects[projectIndex]; break; }
|
|
422
|
+
}
|
|
418
423
|
|
|
419
424
|
var menu = document.createElement("div");
|
|
420
425
|
menu.className = "project-ctx-menu";
|
|
@@ -438,7 +443,13 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
438
443
|
settingsItem.addEventListener("click", function (e) {
|
|
439
444
|
e.stopPropagation();
|
|
440
445
|
closeProjectCtxMenu();
|
|
441
|
-
|
|
446
|
+
if (!targetProject) {
|
|
447
|
+
showToast("Project details are unavailable. Refresh and try again.", "error");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
openProjectSettings(slug, Object.assign({}, targetProject, {
|
|
451
|
+
name: targetProject.title || targetProject.project || name,
|
|
452
|
+
}));
|
|
442
453
|
});
|
|
443
454
|
menu.appendChild(settingsItem);
|
|
444
455
|
}
|
|
@@ -460,7 +471,7 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
460
471
|
|
|
461
472
|
// --- Manage Access ---
|
|
462
473
|
if (store.get('isMultiUserMode')) {
|
|
463
|
-
var isProjectOwner = store.get('myUserId') &&
|
|
474
|
+
var isProjectOwner = store.get('myUserId') && targetProject && targetProject.projectOwnerId && store.get('myUserId') === targetProject.projectOwnerId;
|
|
464
475
|
var isAdmin = store.get('permissions') && store.get('permissions').projectSettings !== false;
|
|
465
476
|
if (isProjectOwner || isAdmin) {
|
|
466
477
|
var accessItem = document.createElement("button");
|
|
@@ -8,13 +8,15 @@ import { detachTuiView } from './session-tui-view.js';
|
|
|
8
8
|
import { formatTokens } from './app-panels.js';
|
|
9
9
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
10
10
|
import { groupedSessionIds, findSplitGroup, isConfiguredWorker } from './split-group-helpers.js';
|
|
11
|
-
import { showConfirm } from './app-misc.js';
|
|
12
11
|
import { syncPairChrome } from './split-pair-ui.js';
|
|
13
12
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
14
13
|
import { autoStartLoginIfNeeded } from './vendor-login.js';
|
|
15
14
|
import { isCurrentProjectSessionReady } from './split-session-boundary.js';
|
|
16
15
|
import { splitPanesMatchProject, hasCurrentSplitGroup, reconcileRestoredSplit } from './project-activation.js';
|
|
17
16
|
import { handlePaneIssueMessage } from './pane-issue-bridge.js';
|
|
17
|
+
import { handlePaneFileMessage, revealFilesPanel } from './pane-file-bridge.js';
|
|
18
|
+
import { openFile } from './filebrowser.js';
|
|
19
|
+
import { bindPermissionControl, createPermissionControl, renderPermissionControl } from './permission-control.js';
|
|
18
20
|
|
|
19
21
|
var host = null;
|
|
20
22
|
var nativeApp = null;
|
|
@@ -161,25 +163,28 @@ export function syncPaneTitles() {
|
|
|
161
163
|
syncPairChrome(host, split);
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
function
|
|
165
|
-
if (!button) return;
|
|
166
|
+
function panePermissionControlState(session) {
|
|
166
167
|
var mode = session && (session.runtimeMode || session.mode || "gui");
|
|
167
168
|
var worker = !!session && isConfiguredWorker(store.get('splitGroups'), session.id);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
return {
|
|
170
|
+
projectSlug: store.get('currentSlug'),
|
|
171
|
+
sessionId: session && session.id,
|
|
172
|
+
vendor: session && session.vendor || store.get('currentVendor') || "claude",
|
|
173
|
+
permissionMode: session && session.permissionMode || "default",
|
|
174
|
+
effectivePermissionMode: session && session.effectivePermissionMode || null,
|
|
175
|
+
permissionCapabilities: session && session.permissionCapabilities || { auto: false, mcpOverride: false },
|
|
176
|
+
connected: store.get('connected'),
|
|
177
|
+
globalPermissionModeForced: store.get('skipPermsEnabled') === true,
|
|
178
|
+
visible: !!session && !worker && mode === "gui",
|
|
179
|
+
locked: worker,
|
|
180
|
+
};
|
|
176
181
|
}
|
|
177
182
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
function updatePaneFullAccessButton(button, session) {
|
|
184
|
+
if (!button) return;
|
|
185
|
+
// Permission policy belongs to the Driver. A configured Worker inherits it
|
|
186
|
+
// server-side and must not present a second, misleading control.
|
|
187
|
+
renderPermissionControl(button, panePermissionControlState(session));
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
function createPane(pane, index) {
|
|
@@ -208,6 +213,8 @@ function createPane(pane, index) {
|
|
|
208
213
|
|
|
209
214
|
var frame = document.createElement("iframe");
|
|
210
215
|
frame.className = "split-pane-frame";
|
|
216
|
+
frame.dataset.projectSlug = pane.slug;
|
|
217
|
+
frame.dataset.sessionId = String(pane.sessionId);
|
|
211
218
|
frame.src = paneUrl(pane);
|
|
212
219
|
frame.title = pane.title;
|
|
213
220
|
|
|
@@ -225,21 +232,11 @@ function createPane(pane, index) {
|
|
|
225
232
|
});
|
|
226
233
|
header.appendChild(ctxChip);
|
|
227
234
|
|
|
228
|
-
var fullAccess =
|
|
229
|
-
fullAccess.type = "button";
|
|
230
|
-
fullAccess.className = "session-full-access split-pane-full-access hidden";
|
|
231
|
-
fullAccess.setAttribute("role", "switch");
|
|
232
|
-
fullAccess.innerHTML = '<span class="session-full-access-label">Skip Permissions</span><span class="session-full-access-track" aria-hidden="true"><span></span></span>';
|
|
235
|
+
var fullAccess = createPermissionControl("split-pane-full-access hidden", "Driver session permission mode");
|
|
233
236
|
updatePaneFullAccessButton(fullAccess, session);
|
|
234
|
-
fullAccess
|
|
237
|
+
bindPermissionControl(fullAccess, function() {
|
|
235
238
|
var current = sessionById(pane.sessionId);
|
|
236
|
-
|
|
237
|
-
setPaneFullAccess(pane.sessionId, false);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
showConfirm("Skip permission prompts for this session? Tool requests will be approved automatically.", function () {
|
|
241
|
-
setPaneFullAccess(pane.sessionId, true);
|
|
242
|
-
}, "Skip Permissions", true);
|
|
239
|
+
return panePermissionControlState(current);
|
|
243
240
|
});
|
|
244
241
|
header.appendChild(fullAccess);
|
|
245
242
|
// Permission state belongs to the session identity on the left. Keep it
|
|
@@ -293,6 +290,7 @@ function handlePaneMessage(event) {
|
|
|
293
290
|
return;
|
|
294
291
|
}
|
|
295
292
|
if (handlePaneIssueMessage(event, host)) return;
|
|
293
|
+
if (handlePaneFileMessage(event, host, openFile, revealFilesPanel)) return;
|
|
296
294
|
if (msg.type !== "clay-pane-context") return;
|
|
297
295
|
var frames = host.querySelectorAll(".split-pane-frame");
|
|
298
296
|
for (var i = 0; i < frames.length; i++) {
|
|
@@ -527,6 +525,8 @@ export function initSplitView() {
|
|
|
527
525
|
document.addEventListener("click", handleSidebarSessionClick, true);
|
|
528
526
|
store.subscribe(function (state, prev) {
|
|
529
527
|
if (state.splitPanes !== prev.splitPanes) renderSplit(state.splitPanes);
|
|
528
|
+
if (state.permissionModePendingByTarget !== prev.permissionModePendingByTarget || state.connected !== prev.connected ||
|
|
529
|
+
state.skipPermsEnabled !== prev.skipPermsEnabled) syncPaneTitles();
|
|
530
530
|
// Switching to a session outside the open split (new_session, palette,
|
|
531
531
|
// notification click) closes the split UI; the group itself persists.
|
|
532
532
|
if (state.activeSessionId !== prev.activeSessionId && state.splitPanes && state.splitPanes.panes) {
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -788,61 +788,61 @@ function createSDKBridge(opts) {
|
|
|
788
788
|
return null; // Not whitelisted
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
-
function
|
|
791
|
+
function resolveSessionPermissionMode(session) {
|
|
792
|
+
var mode = session.permissionMode || sm.currentPermissionMode;
|
|
793
|
+
var router = getWorkerPermissionRouter();
|
|
794
|
+
if (router && typeof router.inheritedPermissionMode === "function") {
|
|
795
|
+
var inherited = router.inheritedPermissionMode(session);
|
|
796
|
+
if (inherited) mode = inherited;
|
|
797
|
+
}
|
|
798
|
+
return mode;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function handlePreToolUsePolicy(session, toolName, input) {
|
|
792
802
|
var debateDecision = homeDebateToolPolicy.initialToolDecision(session, toolName, input);
|
|
793
|
-
if (debateDecision) return
|
|
803
|
+
if (debateDecision) return debateDecision;
|
|
794
804
|
// The dynamic proposal tool is bound to this exact query session. It only
|
|
795
805
|
// posts the inline proposal; starting the debate still requires the user.
|
|
796
806
|
if (session.homeDebatePlanning === true && toolName === "propose_debate") {
|
|
797
|
-
return
|
|
807
|
+
return { behavior: "allow", updatedInput: input };
|
|
798
808
|
}
|
|
799
809
|
// Mate creation proposals only present the exact-session approval card;
|
|
800
810
|
// creating the Mate still requires the user's explicit action there.
|
|
801
811
|
if (session.mateCreationMode === true && toolName === "propose_mate") {
|
|
802
|
-
return
|
|
803
|
-
}
|
|
804
|
-
// Full-auto mode: auto-approve everything except AskUserQuestion
|
|
805
|
-
// (which still needs to go through the user interaction flow).
|
|
806
|
-
var clayPermissionMode = session.permissionMode || sm.currentPermissionMode;
|
|
807
|
-
// A configured Split Worker is Driver-operated, so it runs under its
|
|
808
|
-
// Driver's permission mode, re-resolved from the live pair on every single
|
|
809
|
-
// decision. Nothing is copied at creation and the Worker's own persisted
|
|
810
|
-
// setting is never written, so toggling skip permissions on the Driver
|
|
811
|
-
// applies to the Worker's next call in either direction, mid-turn included.
|
|
812
|
-
var _wpMode = getWorkerPermissionRouter();
|
|
813
|
-
if (_wpMode && typeof _wpMode.inheritedPermissionMode === "function") {
|
|
814
|
-
var inherited = _wpMode.inheritedPermissionMode(session);
|
|
815
|
-
if (inherited) clayPermissionMode = inherited;
|
|
816
|
-
}
|
|
817
|
-
// AskUserQuestion is genuine user input, not an authorization, so it is
|
|
818
|
-
// excluded here exactly as it always was: a Driver in skip mode still
|
|
819
|
-
// cannot silently answer a question addressed to the person.
|
|
820
|
-
if (clayPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
|
|
821
|
-
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
812
|
+
return { behavior: "allow", updatedInput: input };
|
|
822
813
|
}
|
|
823
|
-
|
|
824
814
|
// Ralph Loop execution: auto-approve all tools, deny interactive ones.
|
|
825
815
|
// Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
|
|
826
816
|
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
827
817
|
if (toolName === "AskUserQuestion") {
|
|
828
|
-
return
|
|
818
|
+
return { behavior: "deny", message: "Autonomous mode. Make your own decision." };
|
|
829
819
|
}
|
|
830
820
|
if (toolName === "EnterPlanMode") {
|
|
831
|
-
return
|
|
821
|
+
return { behavior: "deny", message: "Do not enter plan mode. Execute directly." };
|
|
832
822
|
}
|
|
833
|
-
return
|
|
823
|
+
return { behavior: "allow", updatedInput: input };
|
|
834
824
|
}
|
|
835
825
|
|
|
836
826
|
// Check shared whitelist (read-only tools, safe browser tools, safe bash commands)
|
|
837
827
|
var whitelisted = checkToolWhitelist(toolName, input);
|
|
838
828
|
if (whitelisted) {
|
|
839
|
-
return
|
|
829
|
+
return whitelisted;
|
|
840
830
|
}
|
|
841
831
|
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function handleCanUseTool(session, toolName, input, opts) {
|
|
836
|
+
var initialDecision = handlePreToolUsePolicy(session, toolName, input);
|
|
837
|
+
if (initialDecision) return Promise.resolve(initialDecision);
|
|
838
|
+
// Skip permissions removes only the remaining Clay prompt. The SDK's
|
|
839
|
+
// deny/ask rules and Auto classifier have already run before this callback.
|
|
840
|
+
var skipRemainingPermissions = resolveSessionPermissionMode(session) === "bypassPermissions" && toolName !== "AskUserQuestion";
|
|
841
|
+
|
|
842
|
+
if (skipRemainingPermissions) return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
843
|
+
|
|
842
844
|
// Auto-approve if tool was previously allowed for session
|
|
843
|
-
if (session.allowedTools && session.allowedTools[toolName]) {
|
|
844
|
-
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
845
|
-
}
|
|
845
|
+
if (session.allowedTools && session.allowedTools[toolName]) return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
846
846
|
|
|
847
847
|
// A visible Split Worker's remaining prompts belong to its paired Driver,
|
|
848
848
|
// not to the Worker pane. This sits below every auto-decision above, so
|
|
@@ -988,12 +988,32 @@ function createSDKBridge(opts) {
|
|
|
988
988
|
}
|
|
989
989
|
}
|
|
990
990
|
|
|
991
|
+
function ownsQueryRuntime(session, queryInstance, queryGeneration) {
|
|
992
|
+
return session.queryInstance === queryInstance
|
|
993
|
+
&& Number(session._sdkQueryGeneration || 0) === Number(queryGeneration)
|
|
994
|
+
&& (!sm.sessions || typeof sm.sessions.get !== "function" || sm.sessions.get(session.localId) === session);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function recordRuntimeEffectivePermissionMode(session, queryInstance, queryGeneration, message) {
|
|
998
|
+
if (!message || (message.yokeType !== "init" && message.yokeType !== "status") || !message.permissionMode) return false;
|
|
999
|
+
if (!ownsQueryRuntime(session, queryInstance, queryGeneration)) return false;
|
|
1000
|
+
session.effectivePermissionMode = message.permissionMode;
|
|
1001
|
+
session.permissionModeFallbackReason = null;
|
|
1002
|
+
sm.saveSessionFile(session);
|
|
1003
|
+
sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: session.vendor || "claude",
|
|
1004
|
+
mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: message.permissionMode,
|
|
1005
|
+
permissionCapabilities: session.permissionCapabilities || { auto: false, mcpOverride: false },
|
|
1006
|
+
mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {} });
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
991
1010
|
async function processQueryStream(session, codexCatalogCandidate) {
|
|
992
1011
|
// Capture references at start so we only clean up OUR resources in finally,
|
|
993
1012
|
// not resources from a newer query that may have been created after an abort.
|
|
994
1013
|
var myQueryInstance = session.queryInstance;
|
|
995
1014
|
var myAbortController = session.abortController;
|
|
996
1015
|
var myMessageQueue = session.messageQueue;
|
|
1016
|
+
var myQueryGeneration = Number(session._sdkQueryGeneration || 0);
|
|
997
1017
|
console.log("[sdk-bridge] processQueryStream: starting for-await loop, vendor=" + (session.vendor || adapter.vendor));
|
|
998
1018
|
try {
|
|
999
1019
|
for await (var msg of myQueryInstance) {
|
|
@@ -1023,8 +1043,10 @@ function createSDKBridge(opts) {
|
|
|
1023
1043
|
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
|
|
1024
1044
|
break;
|
|
1025
1045
|
case "permission_mode_changed":
|
|
1026
|
-
|
|
1027
|
-
|
|
1046
|
+
// Setter notifications are acknowledgements, not a new project
|
|
1047
|
+
// default and not proof of the runtime-effective mode. Only an
|
|
1048
|
+
// init/status event below may confirm effectivePermissionMode.
|
|
1049
|
+
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: session.effectivePermissionMode || null, permissionCapabilities: session.permissionCapabilities || { auto: false, mcpOverride: false }, mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {}, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1028
1050
|
break;
|
|
1029
1051
|
case "worker_error":
|
|
1030
1052
|
send({ type: "error", text: metaData.error });
|
|
@@ -1033,6 +1055,10 @@ function createSDKBridge(opts) {
|
|
|
1033
1055
|
continue;
|
|
1034
1056
|
}
|
|
1035
1057
|
processSDKMessage(session, msg);
|
|
1058
|
+
// SDK init/status events are the only authority for runtime-effective
|
|
1059
|
+
// permission state. Setter acknowledgement deliberately does not
|
|
1060
|
+
// change this field.
|
|
1061
|
+
recordRuntimeEffectivePermissionMode(session, myQueryInstance, myQueryGeneration, msg);
|
|
1036
1062
|
if (codexCatalogCandidate && msg && msg.yokeType === "session_started" && msg.sessionId
|
|
1037
1063
|
&& session.queryInstance === myQueryInstance
|
|
1038
1064
|
&& Number(session._sdkQueryGeneration || 0) === codexCatalogCandidate.generation
|
|
@@ -1843,12 +1869,20 @@ function createSDKBridge(opts) {
|
|
|
1843
1869
|
abortController: linuxUser ? undefined : session.abortController,
|
|
1844
1870
|
canUseTool: function(toolName, input, toolOpts) {
|
|
1845
1871
|
var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
|
|
1846
|
-
if (nativeQuestionError) return
|
|
1872
|
+
if (nativeQuestionError) return { behavior: "deny", message: nativeQuestionError };
|
|
1847
1873
|
var permissionName = toolName;
|
|
1848
1874
|
var permissionToolDef = findSessionToolDef(toolName);
|
|
1849
1875
|
if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
|
|
1850
1876
|
return handleCanUseTool(session, permissionName, input, toolOpts);
|
|
1851
1877
|
},
|
|
1878
|
+
preToolUsePolicy: function(toolName, input) {
|
|
1879
|
+
var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
|
|
1880
|
+
if (nativeQuestionError) return Promise.resolve({ behavior: "deny", message: nativeQuestionError });
|
|
1881
|
+
var permissionName = toolName;
|
|
1882
|
+
var permissionToolDef = findSessionToolDef(toolName);
|
|
1883
|
+
if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
|
|
1884
|
+
return handlePreToolUsePolicy(session, permissionName, input);
|
|
1885
|
+
},
|
|
1852
1886
|
onUserInputRequest: function(request, respond) {
|
|
1853
1887
|
return sessionUserInputHandler(request, respond);
|
|
1854
1888
|
},
|
|
@@ -1948,6 +1982,24 @@ function createSDKBridge(opts) {
|
|
|
1948
1982
|
|
|
1949
1983
|
// Store QueryHandle on session for iteration and control.
|
|
1950
1984
|
session.queryInstance = handle;
|
|
1985
|
+
// Capabilities come from the installed query handle, never a model-name
|
|
1986
|
+
// table. `acceptEdits` and MCP overrides are Claude SDK semantics; a
|
|
1987
|
+
// vendor may expose the generic setter without implementing either.
|
|
1988
|
+
session.permissionCapabilities = require("./session-permission-mode").capabilities(session, sm.defaultVendor);
|
|
1989
|
+
session.permissionCapabilities.mcpOverride = sessionAdapter.vendor === "claude" && typeof handle.setMcpPermissionModeOverride === "function";
|
|
1990
|
+
session.effectivePermissionMode = null;
|
|
1991
|
+
sm.saveSessionFile(session);
|
|
1992
|
+
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: null, permissionCapabilities: session.permissionCapabilities, mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {}, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1993
|
+
|
|
1994
|
+
if (session.permissionMode === "auto" && session.permissionCapabilities.mcpOverride) {
|
|
1995
|
+
var overrideNames = Object.keys(session.mcpPermissionModeOverrides || {});
|
|
1996
|
+
for (var oi = 0; oi < overrideNames.length; oi++) {
|
|
1997
|
+
try { await handle.setMcpPermissionModeOverride(overrideNames[oi], session.mcpPermissionModeOverrides[overrideNames[oi]]); }
|
|
1998
|
+
catch (overrideError) {
|
|
1999
|
+
sendToSession(session, { type: "error", text: "Failed to restore the AI review policy for " + overrideNames[oi] + ": " + (overrideError.message || overrideError) });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
1951
2003
|
|
|
1952
2004
|
if (typeof beforePush === "function" && beforePush() !== true) {
|
|
1953
2005
|
try { handle.close(); } catch (e) {}
|
|
@@ -2358,17 +2410,15 @@ function createSDKBridge(opts) {
|
|
|
2358
2410
|
async function setPermissionMode(session, mode) {
|
|
2359
2411
|
if (!session.queryInstance) {
|
|
2360
2412
|
// No active query — just store the mode for next startQuery
|
|
2361
|
-
|
|
2362
|
-
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
2363
|
-
return;
|
|
2413
|
+
return { ok: true, effectiveMode: mode };
|
|
2364
2414
|
}
|
|
2365
2415
|
try {
|
|
2366
2416
|
// Route through QueryHandle (works for both in-process and worker paths)
|
|
2367
2417
|
await session.queryInstance.setPermissionMode(mode);
|
|
2368
|
-
|
|
2369
|
-
sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
2418
|
+
return { ok: true, effectiveMode: mode };
|
|
2370
2419
|
} catch (e) {
|
|
2371
2420
|
sendToSession(session, { type: "error", text: "Failed to set permission mode: " + (e.message || e) });
|
|
2421
|
+
return { ok: false, error: e.message || String(e) };
|
|
2372
2422
|
}
|
|
2373
2423
|
}
|
|
2374
2424
|
|
|
@@ -2404,12 +2454,14 @@ function createSDKBridge(opts) {
|
|
|
2404
2454
|
|
|
2405
2455
|
// Override the permission mode for one MCP server on the active query.
|
|
2406
2456
|
async function setMcpPermissionModeOverride(session, serverName, mode) {
|
|
2407
|
-
if (!session || !session.queryInstance) return;
|
|
2457
|
+
if (!session || !session.queryInstance || typeof session.queryInstance.setMcpPermissionModeOverride !== "function") return { ok: false, error: "MCP permission controls are unavailable for this session." };
|
|
2408
2458
|
try {
|
|
2409
2459
|
// Route through QueryHandle (works for both in-process and worker paths)
|
|
2410
2460
|
await session.queryInstance.setMcpPermissionModeOverride(serverName, mode);
|
|
2461
|
+
return { ok: true };
|
|
2411
2462
|
} catch (e) {
|
|
2412
2463
|
send({ type: "error", text: "Failed to set MCP permission mode: " + (e.message || e) });
|
|
2464
|
+
return { ok: false, error: e.message || String(e) };
|
|
2413
2465
|
}
|
|
2414
2466
|
}
|
|
2415
2467
|
|
|
@@ -2583,6 +2635,9 @@ function createSDKBridge(opts) {
|
|
|
2583
2635
|
createMessageQueue: createMessageQueue,
|
|
2584
2636
|
processSDKMessage: processSDKMessage,
|
|
2585
2637
|
checkToolWhitelist: checkToolWhitelist,
|
|
2638
|
+
handlePreToolUsePolicy: handlePreToolUsePolicy,
|
|
2639
|
+
ownsQueryRuntime: ownsQueryRuntime,
|
|
2640
|
+
recordRuntimeEffectivePermissionMode: recordRuntimeEffectivePermissionMode,
|
|
2586
2641
|
handleCanUseTool: handleCanUseTool,
|
|
2587
2642
|
handleElicitation: handleElicitation,
|
|
2588
2643
|
processQueryStream: processQueryStream,
|
package/lib/server.js
CHANGED
|
@@ -158,6 +158,7 @@ function createServer(opts) {
|
|
|
158
158
|
var onReorderProjects = opts.onReorderProjects || null;
|
|
159
159
|
var onSetProjectTitle = opts.onSetProjectTitle || null;
|
|
160
160
|
var onSetProjectIcon = opts.onSetProjectIcon || null;
|
|
161
|
+
var onSetProjectSessionVisibilityDefault = opts.onSetProjectSessionVisibilityDefault || null;
|
|
161
162
|
var onProjectOwnerChanged = opts.onProjectOwnerChanged || null;
|
|
162
163
|
var onGetServerDefaultEffort = opts.onGetServerDefaultEffort || null;
|
|
163
164
|
var onSetServerDefaultEffort = opts.onSetServerDefaultEffort || null;
|
|
@@ -1458,6 +1459,7 @@ function createServer(opts) {
|
|
|
1458
1459
|
onReorderProjects: onReorderProjects,
|
|
1459
1460
|
onSetProjectTitle: onSetProjectTitle,
|
|
1460
1461
|
onSetProjectIcon: onSetProjectIcon,
|
|
1462
|
+
onSetProjectSessionVisibilityDefault: onSetProjectSessionVisibilityDefault,
|
|
1461
1463
|
onProjectOwnerChanged: onProjectOwnerChanged,
|
|
1462
1464
|
onGetServerDefaultEffort: onGetServerDefaultEffort,
|
|
1463
1465
|
onSetServerDefaultEffort: onSetServerDefaultEffort,
|
|
@@ -1707,7 +1709,12 @@ function createServer(opts) {
|
|
|
1707
1709
|
var visible = [];
|
|
1708
1710
|
projects.forEach(function (ctx, slug) {
|
|
1709
1711
|
if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, slug))) return;
|
|
1710
|
-
|
|
1712
|
+
var status = ctx.getStatus();
|
|
1713
|
+
if (!status.isMate && onGetProjectAccess) {
|
|
1714
|
+
var access = onGetProjectAccess(slug);
|
|
1715
|
+
status.sessionVisibilityDefault = access && access.sessionVisibilityDefault === "shared" ? "shared" : "private";
|
|
1716
|
+
}
|
|
1717
|
+
visible.push(status);
|
|
1711
1718
|
});
|
|
1712
1719
|
return visible;
|
|
1713
1720
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Canonical, server-owned permission selection validation. The requested mode
|
|
2
|
+
// is persisted separately from the SDK mode because Clay keeps the final
|
|
3
|
+
// canUseTool decision for GUI sessions.
|
|
4
|
+
|
|
5
|
+
function normalize(mode) {
|
|
6
|
+
if (mode === "default" || mode === "auto" || mode === "plan" || mode === "acceptEdits" || mode === "bypassPermissions") return mode;
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function capabilities(session, defaultVendor) {
|
|
11
|
+
var vendor = session && session.vendor || defaultVendor || "claude";
|
|
12
|
+
return {
|
|
13
|
+
auto: vendor === "claude",
|
|
14
|
+
mcpOverride: vendor === "claude" && !!(session && session.queryInstance &&
|
|
15
|
+
typeof session.queryInstance.setMcpPermissionModeOverride === "function"),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function supportsAuto(session, defaultVendor) {
|
|
20
|
+
return capabilities(session, defaultVendor).auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validate(session, mode, defaultVendor) {
|
|
24
|
+
var requested = normalize(mode);
|
|
25
|
+
if (!requested) return { ok: false, error: "Unknown permission mode." };
|
|
26
|
+
if (requested === "auto" && !supportsAuto(session, defaultVendor)) {
|
|
27
|
+
return { ok: false, error: "Auto permissions are not available for this session." };
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, mode: requested };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clientState(session, fallback, defaultVendor) {
|
|
33
|
+
var requested = session && session.permissionMode || fallback || "default";
|
|
34
|
+
return {
|
|
35
|
+
requestedPermissionMode: requested,
|
|
36
|
+
effectivePermissionMode: session && session.effectivePermissionMode || null,
|
|
37
|
+
permissionCapabilities: capabilities(session, defaultVendor),
|
|
38
|
+
mcpPermissionModeOverrides: session && session.mcpPermissionModeOverrides || {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { validate: validate, clientState: clientState, supportsAuto: supportsAuto, capabilities: capabilities };
|
|
@@ -7,6 +7,66 @@ function restore(meta) {
|
|
|
7
7
|
return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function defaultForProject(access) {
|
|
11
|
+
return access && access.sessionVisibilityDefault === "shared" ? "shared" : "private";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveNewSessionVisibility(access, msg, isMate) {
|
|
15
|
+
if (isMate === true) return { visibility: "private" };
|
|
16
|
+
if (!msg || !Object.prototype.hasOwnProperty.call(msg, "sessionVisibility")) {
|
|
17
|
+
return { visibility: defaultForProject(access) };
|
|
18
|
+
}
|
|
19
|
+
if (msg.sessionVisibility === "private" || msg.sessionVisibility === "shared") {
|
|
20
|
+
return { visibility: msg.sessionVisibility };
|
|
21
|
+
}
|
|
22
|
+
return { error: "Session visibility must be private or shared." };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function defaultChangeResult(ctx, ws, msg, result) {
|
|
26
|
+
var requestedSlug = typeof msg.slug === "string" && msg.slug.length > 0 && msg.slug.length <= 200 ? msg.slug : ctx.slug;
|
|
27
|
+
ctx.sendTo(ws, Object.assign({
|
|
28
|
+
type: "set_project_session_visibility_default_result",
|
|
29
|
+
slug: requestedSlug,
|
|
30
|
+
requestId: typeof msg.requestId === "string" ? msg.requestId.substring(0, 200) : null,
|
|
31
|
+
}, result));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleDefaultChange(ctx, ws, msg) {
|
|
35
|
+
if (!msg || (msg.visibility !== "shared" && msg.visibility !== "private")) {
|
|
36
|
+
defaultChangeResult(ctx, ws, msg || {}, { ok: false, error: "Session visibility default must be private or shared." });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (msg.slug && msg.slug !== ctx.slug) {
|
|
40
|
+
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (ctx.isMate === true || !ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
|
|
44
|
+
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
var user = ws && ws._clayUser && ctx.usersModule.findUserById(ws._clayUser.id);
|
|
48
|
+
var access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
|
|
49
|
+
if (access && access.isWorktree === true) {
|
|
50
|
+
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Worktrees inherit this setting from their parent project." });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
var permissions = user && ctx.usersModule.getEffectivePermissions
|
|
54
|
+
? ctx.usersModule.getEffectivePermissions(user, ctx.osUsers) : {};
|
|
55
|
+
var isOwner = !!(user && access && access.ownerId && access.ownerId === user.id);
|
|
56
|
+
var isAdmin = !!(user && user.role === "admin");
|
|
57
|
+
if (!user || !permissions.projectSettings || (!isOwner && !isAdmin)) {
|
|
58
|
+
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (typeof ctx.opts.onSetProjectSessionVisibilityDefault !== "function") {
|
|
62
|
+
defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
var result = ctx.opts.onSetProjectSessionVisibilityDefault(ctx.slug, msg.visibility);
|
|
66
|
+
defaultChangeResult(ctx, ws, msg, { ok: !!(result && result.ok), visibility: result && result.visibility, error: result && result.error });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
10
70
|
function handleChange(ctx, ws, msg) {
|
|
11
71
|
if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
|
|
12
72
|
var session = ctx.sm.sessions.get(msg.sessionId);
|
|
@@ -38,4 +98,5 @@ function revokeViewers(ctx, session) {
|
|
|
38
98
|
}
|
|
39
99
|
}
|
|
40
100
|
|
|
41
|
-
module.exports = { normalize: normalize, restore: restore,
|
|
101
|
+
module.exports = { normalize: normalize, restore: restore, defaultForProject: defaultForProject, resolveNewSessionVisibility: resolveNewSessionVisibility,
|
|
102
|
+
handleChange: handleChange, handleDefaultChange: handleDefaultChange, revokeViewers: revokeViewers };
|
package/lib/sessions.js
CHANGED
|
@@ -177,6 +177,7 @@ function createSessionManager(opts) {
|
|
|
177
177
|
// so lazy-resume (`claude --resume`) re-spawns with the same flag.
|
|
178
178
|
if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
|
|
179
179
|
if (session.permissionMode) metaObj.permissionMode = session.permissionMode;
|
|
180
|
+
if (session.mcpPermissionModeOverrides) metaObj.mcpPermissionModeOverrides = session.mcpPermissionModeOverrides;
|
|
180
181
|
if (session.permissionModeBeforeFullAccess) metaObj.permissionModeBeforeFullAccess = session.permissionModeBeforeFullAccess;
|
|
181
182
|
if (session.autonomousRun) metaObj.autonomousRun = session.autonomousRun;
|
|
182
183
|
if (session.loopInterviewBrief) metaObj.loopInterviewBrief = session.loopInterviewBrief;
|
|
@@ -348,6 +349,10 @@ function createSessionManager(opts) {
|
|
|
348
349
|
// Clay's own per-session permission mode.
|
|
349
350
|
session.dangerouslySkipPermissions = session.mode === "tui" && !!m.dangerouslySkipPermissions;
|
|
350
351
|
session.permissionMode = m.permissionMode || (session.mode === "gui" && m.dangerouslySkipPermissions ? "bypassPermissions" : null);
|
|
352
|
+
// Runtime-effective state is observed from the live SDK only. A daemon
|
|
353
|
+
// restore has no active runtime to confirm the old value.
|
|
354
|
+
session.effectivePermissionMode = null;
|
|
355
|
+
session.mcpPermissionModeOverrides = m.mcpPermissionModeOverrides && typeof m.mcpPermissionModeOverrides === "object" ? m.mcpPermissionModeOverrides : {};
|
|
351
356
|
session.permissionModeBeforeFullAccess = m.permissionModeBeforeFullAccess || null;
|
|
352
357
|
session.autonomousRun = restoreStoredRun(m.autonomousRun);
|
|
353
358
|
session.loopInterviewBrief = m.loopInterviewBrief && typeof m.loopInterviewBrief === "object" ? m.loopInterviewBrief : null;
|
|
@@ -587,6 +592,9 @@ function createSessionManager(opts) {
|
|
|
587
592
|
runtimeTerminalId: typeof s.runtimeTerminalId === "number" ? s.runtimeTerminalId : null,
|
|
588
593
|
dangerouslySkipPermissions: !!s.dangerouslySkipPermissions,
|
|
589
594
|
permissionMode: s.permissionMode || null,
|
|
595
|
+
effectivePermissionMode: s.effectivePermissionMode || null,
|
|
596
|
+
permissionCapabilities: require("./session-permission-mode").capabilities(s, defaultVendor),
|
|
597
|
+
mcpPermissionModeOverrides: s.mcpPermissionModeOverrides || {},
|
|
590
598
|
autonomousRun: s.autonomousRun ? {
|
|
591
599
|
id: s.autonomousRun.id,
|
|
592
600
|
state: s.autonomousRun.state,
|
|
@@ -676,6 +684,8 @@ function createSessionManager(opts) {
|
|
|
676
684
|
mode: (sessionOpts && sessionOpts.mode === "tui") ? "tui" : "gui",
|
|
677
685
|
dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.mode === "tui" && sessionOpts.dangerouslySkipPermissions),
|
|
678
686
|
permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
|
|
687
|
+
effectivePermissionMode: null,
|
|
688
|
+
mcpPermissionModeOverrides: {},
|
|
679
689
|
permissionModeBeforeFullAccess: null,
|
|
680
690
|
autonomousRun: null,
|
|
681
691
|
loopInterviewBrief: null,
|
|
@@ -725,6 +735,8 @@ function createSessionManager(opts) {
|
|
|
725
735
|
mode: (sessionOpts && sessionOpts.mode === "tui") ? "tui" : "gui",
|
|
726
736
|
dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.mode === "tui" && sessionOpts.dangerouslySkipPermissions),
|
|
727
737
|
permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
|
|
738
|
+
effectivePermissionMode: null,
|
|
739
|
+
mcpPermissionModeOverrides: {},
|
|
728
740
|
permissionModeBeforeFullAccess: null,
|
|
729
741
|
autonomousRun: null,
|
|
730
742
|
loopInterviewBrief: null,
|
|
@@ -826,7 +838,8 @@ function createSessionManager(opts) {
|
|
|
826
838
|
var _capsByVendor = capabilitiesByVendor || {};
|
|
827
839
|
var _sessionVendor = session.vendor || defaultVendor || "claude";
|
|
828
840
|
var _vendorCaps = _capsByVendor[_sessionVendor] || {};
|
|
829
|
-
|
|
841
|
+
var _permissionState = require("./session-permission-mode").clientState(session, "default", defaultVendor);
|
|
842
|
+
_send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: _permissionState.requestedPermissionMode, effectivePermissionMode: _permissionState.effectivePermissionMode, permissionCapabilities: _permissionState.permissionCapabilities, mcpPermissionModeOverrides: _permissionState.mcpPermissionModeOverrides, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
|
|
830
843
|
// Send vendor-specific slash commands
|
|
831
844
|
var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
|
|
832
845
|
|| session.slashCommands
|
|
@@ -1100,6 +1113,8 @@ function createSessionManager(opts) {
|
|
|
1100
1113
|
lastActivity: Date.now(),
|
|
1101
1114
|
history: cliHistory,
|
|
1102
1115
|
messageUUIDs: [],
|
|
1116
|
+
sessionVisibility: sessionVisibility.normalize(opts && opts.sessionVisibility),
|
|
1117
|
+
sessionVisibilityExplicit: !!(opts && opts.sessionVisibilityExplicit === true),
|
|
1103
1118
|
bookmarked: false,
|
|
1104
1119
|
favoriteOrder: null,
|
|
1105
1120
|
};
|