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
@@ -41,6 +41,7 @@ function getProjectAccess(config, slug, osUsers) {
41
41
  visibility: visibility,
42
42
  allowedUsers: parentAllowedUsers,
43
43
  ownerId: project.ownerId || null,
44
+ sessionVisibilityDefault: project.sessionVisibilityDefault === "shared" ? "shared" : "private",
44
45
  isWorktree: false,
45
46
  };
46
47
  }
@@ -52,6 +53,7 @@ function getProjectAccess(config, slug, osUsers) {
52
53
  visibility: visibility,
53
54
  allowedUsers: uniqueUsers(parentAllowedUsers.concat(worktreeAllowedUsers)),
54
55
  ownerId: project.ownerId || null,
56
+ sessionVisibilityDefault: project.sessionVisibilityDefault === "shared" ? "shared" : "private",
55
57
  isWorktree: true,
56
58
  parentSlug: parentSlug,
57
59
  parentAllowedUsers: parentAllowedUsers,
@@ -61,6 +63,7 @@ function getProjectAccess(config, slug, osUsers) {
61
63
  visibility: visibility,
62
64
  allowedUsers: parentAllowedUsers,
63
65
  ownerId: project.ownerId || null,
66
+ sessionVisibilityDefault: project.sessionVisibilityDefault === "shared" ? "shared" : "private",
64
67
  },
65
68
  };
66
69
  }
@@ -84,6 +87,16 @@ function setAllowedUsers(config, slug, allowedUsers) {
84
87
  return { ok: true, isWorktree: true, allowedUsers: clean };
85
88
  }
86
89
 
90
+ function setSessionVisibilityDefault(config, slug, visibility) {
91
+ if (visibility !== "private" && visibility !== "shared") return { error: "Invalid session visibility default" };
92
+ if (parentSlugFor(slug)) return { error: "Worktrees inherit this setting from their parent project." };
93
+ var project = findProject(config, slug);
94
+ if (!project) return { error: "Project not found" };
95
+ if (visibility === "shared") project.sessionVisibilityDefault = "shared";
96
+ else delete project.sessionVisibilityDefault;
97
+ return { ok: true, visibility: visibility };
98
+ }
99
+
87
100
  function clearWorktreeGrant(config, slug) {
88
101
  var parentSlug = parentSlugFor(slug);
89
102
  var project = parentSlug ? findProject(config, parentSlug) : null;
@@ -113,6 +126,7 @@ module.exports = {
113
126
  parentSlugFor: parentSlugFor,
114
127
  getProjectAccess: getProjectAccess,
115
128
  setAllowedUsers: setAllowedUsers,
129
+ setSessionVisibilityDefault: setSessionVisibilityDefault,
116
130
  clearWorktreeGrant: clearWorktreeGrant,
117
131
  reconcileWorktreeGrants: reconcileWorktreeGrants,
118
132
  };
package/lib/daemon.js CHANGED
@@ -556,6 +556,13 @@ var relay = createServer({
556
556
  });
557
557
  return { ok: true };
558
558
  },
559
+ onSetProjectSessionVisibilityDefault: function (slug, visibility) {
560
+ var result = daemonProjectAccess.setSessionVisibilityDefault(config, slug, visibility);
561
+ if (result.error) return { ok: false, error: result.error };
562
+ saveConfig(config);
563
+ relay.broadcastAll({ type: "projects_updated", projects: relay.getProjects(), projectCount: config.projects.length });
564
+ return result;
565
+ },
559
566
  onProjectOwnerChanged: function (slug, ownerId) {
560
567
  console.log("[daemon] onProjectOwnerChanged:", slug, "→", ownerId);
561
568
  var oldOwnerId = null;
@@ -5,6 +5,7 @@ var userPresence = require("./user-presence");
5
5
  var emailAccounts = require("./email-accounts");
6
6
  var { getCodexConfig } = require("./codex-defaults");
7
7
  var yoke = require("./yoke");
8
+ var permissionModes = require("./session-permission-mode");
8
9
 
9
10
  function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
10
11
  try {
@@ -195,7 +196,8 @@ function attachConnection(ctx) {
195
196
  // model. Always send it so reconnects cannot fall back to "Not installed."
196
197
  var initialModelInfo = buildInitialModelInfo(sm, initialVendor, initialModel, initialModels, restoredActive ? restoredActive.localId : null);
197
198
  sendTo(ws, initialModelInfo);
198
- sendTo(ws, { type: "config_state", model: initialModelInfo.model, vendor: initialVendor, sessionId: restoredActive ? restoredActive.localId : null, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
199
+ var initialPermissionState = permissionModes.clientState(restoredActive, sm.currentPermissionMode, sm.defaultVendor);
200
+ sendTo(ws, { type: "config_state", model: initialModelInfo.model, vendor: initialVendor, sessionId: restoredActive ? restoredActive.localId : null, mode: initialPermissionState.requestedPermissionMode, effectivePermissionMode: initialPermissionState.effectivePermissionMode, permissionCapabilities: initialPermissionState.permissionCapabilities, mcpPermissionModeOverrides: initialPermissionState.mcpPermissionModeOverrides, effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
199
201
  sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
200
202
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
201
203
  sendTo(ws, { type: "term_list", terminals: tm.list() });
@@ -270,7 +272,8 @@ function attachConnection(ctx) {
270
272
  try { resolveSessionForView(active, ws); } catch (e) {}
271
273
  }
272
274
  var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
273
- sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: active.permissionMode || null });
275
+ var activePermissionState = permissionModes.clientState(active, sm.currentPermissionMode, sm.defaultVendor);
276
+ sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: activePermissionState.requestedPermissionMode, effectivePermissionMode: activePermissionState.effectivePermissionMode, permissionCapabilities: activePermissionState.permissionCapabilities, mcpPermissionModeOverrides: activePermissionState.mcpPermissionModeOverrides });
274
277
  if (ctx.autonomousRun) sendTo(ws, { type: "autonomous_run_state", sessionId: active.localId, run: ctx.autonomousRun.project(active.autonomousRun) });
275
278
  // Send per-session context sources
276
279
  var sessionSources = loadContextSources(slug, active.localId);
@@ -1,86 +1,99 @@
1
- // Shared, transactional control for Clay-managed per-session Skip Permissions.
1
+ // Serialized, transactional owner for every per-session permission selection.
2
+ // Skip is Clay-managed and deliberately maps the SDK to default.
2
3
 
3
4
  function createFullAccessService(ctx) {
4
5
  var sm = ctx.sm;
5
6
  var transitions = new Map();
6
-
7
7
  function serialize(session, action) {
8
8
  var prior = transitions.get(session.localId) || Promise.resolve();
9
9
  var next = prior.catch(function () {}).then(action);
10
10
  transitions.set(session.localId, next);
11
- return next.finally(function () {
12
- if (transitions.get(session.localId) === next) transitions.delete(session.localId);
13
- });
11
+ return next.finally(function () { if (transitions.get(session.localId) === next) transitions.delete(session.localId); });
14
12
  }
15
-
16
13
  function snapshot(session) {
17
- return {
18
- permissionMode: session.permissionMode || null,
19
- permissionModeBeforeFullAccess: session.permissionModeBeforeFullAccess || null,
20
- enabled: session.permissionMode === "bypassPermissions",
21
- };
14
+ return { permissionMode: session.permissionMode || null, permissionModeBeforeFullAccess: session.permissionModeBeforeFullAccess || null,
15
+ effectivePermissionMode: session.effectivePermissionMode || null, permissionModeFallbackReason: session.permissionModeFallbackReason || null,
16
+ enabled: session.permissionMode === "bypassPermissions" };
22
17
  }
23
-
24
- function notify(session) {
18
+ function persist(session) {
19
+ sm.saveSessionFile(session);
20
+ sm.broadcastSessionList();
25
21
  if (typeof sm.sendToSession === "function") sm.sendToSession(session, { type: "session_full_access_changed", id: session.localId,
26
22
  enabled: session.permissionMode === "bypassPermissions", permissionMode: session.permissionMode });
27
23
  }
28
-
29
- async function applyNativeMode(session, enabled) {
24
+ async function setNative(session, mode) {
30
25
  if (!session.queryInstance || typeof session.queryInstance.setPermissionMode !== "function") return;
31
- // Clay's common permission callback remains the approval authority.
32
- await session.queryInstance.setPermissionMode(enabled ? "default" : session.permissionMode);
26
+ await session.queryInstance.setPermissionMode(mode === "bypassPermissions" ? "default" : mode);
33
27
  }
34
-
35
- async function applyEnabled(session, enabled) {
28
+ async function applyMode(session, mode) {
36
29
  var previous = snapshot(session);
37
- if (previous.enabled === !!enabled) return previous;
38
- if (enabled) {
39
- session.permissionModeBeforeFullAccess = session.permissionMode || sm.currentPermissionMode || "default";
40
- session.permissionMode = "bypassPermissions";
41
- } else {
42
- session.permissionMode = session.permissionModeBeforeFullAccess || "default";
43
- session.permissionModeBeforeFullAccess = null;
44
- }
30
+ if (previous.permissionMode === mode) return previous;
31
+ // Clear stale confirmation before the setter. An init/status event may
32
+ // arrive while it is pending; do not clear that newer observation later.
33
+ session.effectivePermissionMode = null;
34
+ session.permissionModeFallbackReason = null;
45
35
  try {
46
- await applyNativeMode(session, !!enabled);
47
- sm.saveSessionFile(session);
48
- sm.broadcastSessionList();
49
- notify(session);
50
- return previous;
36
+ await setNative(session, mode);
51
37
  } catch (error) {
38
+ session.effectivePermissionMode = previous.effectivePermissionMode;
39
+ session.permissionModeFallbackReason = previous.permissionModeFallbackReason;
40
+ throw error;
41
+ }
42
+ session.permissionMode = mode;
43
+ session.permissionModeBeforeFullAccess = mode === "bypassPermissions" ? (previous.permissionMode || sm.currentPermissionMode || "default") : null;
44
+ try { persist(session); return previous; }
45
+ catch (error) {
52
46
  session.permissionMode = previous.permissionMode;
53
47
  session.permissionModeBeforeFullAccess = previous.permissionModeBeforeFullAccess;
48
+ session.effectivePermissionMode = previous.effectivePermissionMode;
49
+ session.permissionModeFallbackReason = previous.permissionModeFallbackReason;
50
+ try { await setNative(session, previous.permissionMode || "default"); }
51
+ catch (rollbackError) {
52
+ session.effectivePermissionMode = null;
53
+ session.permissionModeFallbackReason = "Permission rollback failed: " + (rollbackError.message || rollbackError);
54
+ }
54
55
  throw error;
55
56
  }
56
57
  }
57
-
58
+ function setMode(session, mode) { return serialize(session, function () { return applyMode(session, mode); }); }
58
59
  function setEnabled(session, enabled) {
59
- return serialize(session, function () { return applyEnabled(session, enabled); });
60
- }
61
-
62
- async function applyRestore(session, prior) {
63
- if (!prior) return;
64
- var current = snapshot(session);
65
- session.permissionMode = prior.permissionMode;
66
- session.permissionModeBeforeFullAccess = prior.permissionModeBeforeFullAccess;
67
- try {
68
- await applyNativeMode(session, prior.enabled);
69
- sm.saveSessionFile(session);
70
- sm.broadcastSessionList();
71
- notify(session);
72
- } catch (error) {
73
- session.permissionMode = current.permissionMode;
74
- session.permissionModeBeforeFullAccess = current.permissionModeBeforeFullAccess;
75
- throw error;
76
- }
60
+ return serialize(session, function () {
61
+ return applyMode(session, enabled ? "bypassPermissions" : (session.permissionModeBeforeFullAccess || "default"));
62
+ });
77
63
  }
78
-
79
64
  function restore(session, prior) {
80
- return serialize(session, function () { return applyRestore(session, prior); });
65
+ if (!prior) return Promise.resolve();
66
+ return serialize(session, async function () {
67
+ var current = snapshot(session);
68
+ // Runtime-effective state is an observation, not part of the selection
69
+ // snapshot. Clear it before the native setter and preserve any newer
70
+ // init/status event that arrives while the acknowledgement is pending.
71
+ session.effectivePermissionMode = null;
72
+ session.permissionModeFallbackReason = null;
73
+ try {
74
+ await setNative(session, prior.permissionMode || "default");
75
+ } catch (error) {
76
+ session.effectivePermissionMode = current.effectivePermissionMode;
77
+ session.permissionModeFallbackReason = current.permissionModeFallbackReason;
78
+ throw error;
79
+ }
80
+ session.permissionMode = prior.permissionMode;
81
+ session.permissionModeBeforeFullAccess = prior.permissionModeBeforeFullAccess;
82
+ try { persist(session); }
83
+ catch (error) {
84
+ session.permissionMode = current.permissionMode;
85
+ session.permissionModeBeforeFullAccess = current.permissionModeBeforeFullAccess;
86
+ session.effectivePermissionMode = null;
87
+ session.permissionModeFallbackReason = null;
88
+ try { await setNative(session, current.permissionMode || "default"); }
89
+ catch (rollbackError) {
90
+ session.effectivePermissionMode = null;
91
+ session.permissionModeFallbackReason = "Permission rollback failed: " + (rollbackError.message || rollbackError);
92
+ }
93
+ throw error;
94
+ }
95
+ });
81
96
  }
82
-
83
- return { restore: restore, setEnabled: setEnabled, snapshot: snapshot };
97
+ return { restore: restore, setEnabled: setEnabled, setMode: setMode, snapshot: snapshot };
84
98
  }
85
-
86
99
  module.exports = { createFullAccessService: createFullAccessService };
@@ -2,6 +2,7 @@
2
2
 
3
3
  var yoke = require("./yoke");
4
4
  var provenance = require("./session-provenance");
5
+ var sessionVisibility = require("./session-visibility");
5
6
 
6
7
  function attachIssueLaunch(ctx) {
7
8
  var requests = new Map();
@@ -79,8 +80,9 @@ function attachIssueLaunch(ctx) {
79
80
  var liveLinuxUser = typeof ctx.getLinuxUserForSession === "function" ? ctx.getLinuxUserForSession({ ownerId: actorId }) : null;
80
81
  if (osUsersEnabled() && liveLinuxUser !== expectedLinuxUser) throw new Error("The OS identity changed while issue work was being prepared.");
81
82
 
83
+ var projectAccess = typeof ctx.getProjectAccess === "function" ? ctx.getProjectAccess() : null;
82
84
  var session = ctx.sm.createSessionRaw({ vendor: runtime.vendor, model: runtime.model, effort: runtime.effort || null,
83
- mode: "gui", ownerId: actor && actor.id || null, sessionVisibility: "private" });
85
+ mode: "gui", ownerId: actor && actor.id || null, sessionVisibility: sessionVisibility.defaultForProject(projectAccess) });
84
86
  provenance.ensureOrigin(session);
85
87
  if (session.hidden === true || session.delegated === true || provenance.isWorker(session)) {
86
88
  ctx.sm.deleteSession(session.localId);
@@ -88,6 +88,7 @@ function attachSessions(ctx) {
88
88
  var slug = ctx.slug;
89
89
  var isMate = ctx.isMate;
90
90
  var osUsers = ctx.osUsers;
91
+ var dangerouslySkipPermissions = ctx.dangerouslySkipPermissions === true;
91
92
  var currentVersion = ctx.currentVersion;
92
93
  var sm = ctx.sm;
93
94
  var sdk = ctx.sdk;
@@ -98,6 +99,7 @@ function attachSessions(ctx) {
98
99
  var sendToAdmins = ctx.sendToAdmins;
99
100
  var broadcastUpdateAvailable = ctx.broadcastUpdateAvailable || function () {};
100
101
  var broadcastUpdateStarted = ctx.broadcastUpdateStarted || function () {};
102
+ var isDriverOperatedSession = ctx.isDriverOperatedSession || function () { return false; };
101
103
  var sendToUpdateUser = ctx.sendToUpdateUser || function () {};
102
104
  var sendToSession = ctx.sendToSession;
103
105
  var sendToSessionOthers = ctx.sendToSessionOthers;
@@ -555,7 +557,7 @@ function attachSessions(ctx) {
555
557
  }
556
558
  sm.sweepBlankSessions();
557
559
  var sessionOpts = {
558
- sessionVisibility: sessionVisibility.normalize(msg.sessionVisibility),
560
+ sessionVisibility: "private",
559
561
  vendor: prepared.runtime.vendor,
560
562
  model: prepared.runtime.model,
561
563
  effort: prepared.runtime.effort || null,
@@ -619,7 +621,15 @@ function attachSessions(ctx) {
619
621
  sm.sweepBlankSessions();
620
622
  var sessionOpts = {};
621
623
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
622
- sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
624
+ // Only ordinary project conversations may opt into this setting.
625
+ // Scheduled interviews and background/worker creation paths remain explicit.
626
+ var visibilityChoice = sessionVisibility.resolveNewSessionVisibility(getProjectAccess(), msg, isMate);
627
+ if (visibilityChoice.error) {
628
+ scheduledRequestError(ws, msg, visibilityChoice.error);
629
+ if (!msg.requestId) sendTo(ws, { type: "error", text: visibilityChoice.error });
630
+ return true;
631
+ }
632
+ sessionOpts.sessionVisibility = visibilityChoice.visibility;
623
633
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
624
634
  // Mode resolution: vendors without a TUI session mode are always GUI.
625
635
  // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
@@ -701,8 +711,7 @@ function attachSessions(ctx) {
701
711
  if (reusable) {
702
712
  if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
703
713
  if (!reusable.effort) reusable.effort = sessionOpts.effort;
704
- sm.setSessionVisibility(reusable.localId, sessionOpts.sessionVisibility);
705
- sessionVisibility.revokeViewers(ctx, reusable);
714
+ // Reusable blanks already exist; do not retroactively change them.
706
715
  sm.switchSession(reusable.localId, ws);
707
716
  newSess = reusable;
708
717
  } else {
@@ -735,6 +744,14 @@ function attachSessions(ctx) {
735
744
  return true;
736
745
  }
737
746
 
747
+ if (msg.type === "set_project_session_visibility_default") {
748
+ sessionVisibility.handleDefaultChange({
749
+ slug: slug, isMate: isMate, osUsers: osUsers, usersModule: usersModule, getProjectAccess: getProjectAccess,
750
+ opts: opts, sendTo: sendTo,
751
+ }, ws, msg);
752
+ return true;
753
+ }
754
+
738
755
  if (msg.type === "set_session_bookmark") {
739
756
  if (typeof msg.sessionId === "number") {
740
757
  var bookmarkTarget = sm.sessions.get(msg.sessionId);
@@ -1166,13 +1183,62 @@ function attachSessions(ctx) {
1166
1183
  return true;
1167
1184
  }
1168
1185
 
1169
- if (msg.type === "set_mcp_permission_mode_override" && msg.serverName) {
1170
- var session = getSessionForWs(ws);
1171
- if (session && sdk.setMcpPermissionModeOverride) {
1172
- // mode: "default" | "auto" | null (null clears the override)
1173
- var mcpMode = (msg.mode === "auto" || msg.mode === "default") ? msg.mode : null;
1174
- sdk.setMcpPermissionModeOverride(session, msg.serverName, mcpMode);
1186
+ if (msg.type === "set_mcp_permission_mode_override") {
1187
+ var mcpRequestId = typeof msg.requestId === "string" && msg.requestId.length <= 128 ? msg.requestId : null;
1188
+ var mcpProjectSlug = typeof msg.projectSlug === "string" && msg.projectSlug.length <= 256 ? msg.projectSlug : null;
1189
+ var mcpSessionId = Number(msg.sessionId);
1190
+ var mcpServerName = typeof msg.serverName === "string" && msg.serverName.length > 0 && msg.serverName.length <= 256 ? msg.serverName : null;
1191
+ var mcpSession = Number.isSafeInteger(mcpSessionId) ? sm.sessions.get(mcpSessionId) : null;
1192
+ function mcpPermissionResult(ok, error, overrides) {
1193
+ sendTo(ws, {
1194
+ type: "mcp_permission_mode_result",
1195
+ ok: ok === true,
1196
+ requestId: mcpRequestId,
1197
+ projectSlug: mcpProjectSlug,
1198
+ sessionId: Number.isSafeInteger(mcpSessionId) ? mcpSessionId : null,
1199
+ serverName: mcpServerName,
1200
+ mode: ok ? mcpMode : null,
1201
+ mcpPermissionModeOverrides: ok ? overrides || {} : {},
1202
+ error: error || null,
1203
+ });
1204
+ }
1205
+ var mcpMode = msg.mode === "auto" || msg.mode === "default" ? msg.mode : null;
1206
+ var mcpModeValid = msg.mode == null || msg.mode === "" || msg.mode === "auto" || msg.mode === "default";
1207
+ if (mcpProjectSlug !== slug) {
1208
+ mcpPermissionResult(false, "The AI review request belongs to a different project.");
1209
+ return true;
1210
+ }
1211
+ if (!mcpServerName || !mcpModeValid) {
1212
+ mcpPermissionResult(false, "The AI review request is invalid.");
1213
+ return true;
1175
1214
  }
1215
+ var mcpRuntimeMode = mcpSession && (mcpSession.runtimeMode || mcpSession.mode || "gui");
1216
+ if (!mcpSession || !ownsSearchSession(ws, mcpSession) || mcpRuntimeMode !== "gui" || isDriverOperatedSession(mcpSession)) {
1217
+ mcpPermissionResult(false, "AI review controls are only available on an accessible Driver GUI session.");
1218
+ return true;
1219
+ }
1220
+ var mcpApplicable = (mcpSession.effectivePermissionMode === "auto" || mcpSession.effectivePermissionMode === "bypassPermissions") &&
1221
+ mcpSession.permissionCapabilities && mcpSession.permissionCapabilities.mcpOverride === true && typeof sdk.setMcpPermissionModeOverride === "function";
1222
+ if (!mcpApplicable) {
1223
+ mcpPermissionResult(false, "AI review controls are not available for this session.");
1224
+ return true;
1225
+ }
1226
+ (async function () {
1227
+ try {
1228
+ var result = await sdk.setMcpPermissionModeOverride(mcpSession, mcpServerName, mcpMode);
1229
+ if (!result || result.ok === false) {
1230
+ mcpPermissionResult(false, result && result.error || "Failed to change AI review policy.");
1231
+ return;
1232
+ }
1233
+ mcpSession.mcpPermissionModeOverrides = mcpSession.mcpPermissionModeOverrides || {};
1234
+ if (mcpMode) mcpSession.mcpPermissionModeOverrides[mcpServerName] = mcpMode;
1235
+ else delete mcpSession.mcpPermissionModeOverrides[mcpServerName];
1236
+ if (typeof sm.saveSessionFile === "function") sm.saveSessionFile(mcpSession);
1237
+ mcpPermissionResult(true, null, mcpSession.mcpPermissionModeOverrides);
1238
+ } catch (error) {
1239
+ mcpPermissionResult(false, "Failed to change AI review policy: " + (error.message || error));
1240
+ }
1241
+ })();
1176
1242
  return true;
1177
1243
  }
1178
1244
 
@@ -1189,6 +1255,10 @@ function attachSessions(ctx) {
1189
1255
  " is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
1190
1256
  } else {
1191
1257
  vendorSession.vendor = msg.vendor;
1258
+ if (vendorSession.permissionMode === "auto" && msg.vendor !== "claude") {
1259
+ vendorSession.permissionMode = "default";
1260
+ vendorSession.effectivePermissionMode = null;
1261
+ }
1192
1262
  if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
1193
1263
  sm.saveSessionFile(vendorSession);
1194
1264
  sm.broadcastSessionList();
@@ -1231,16 +1301,62 @@ function attachSessions(ctx) {
1231
1301
  return true;
1232
1302
  }
1233
1303
 
1234
- if (msg.type === "set_permission_mode" && msg.mode) {
1235
- sm.currentPermissionMode = msg.mode;
1236
- var session = getSessionForWs(ws);
1237
- if (session) {
1238
- session.permissionMode = msg.mode;
1239
- sm.saveSessionFile(session);
1240
- sm.broadcastSessionList();
1241
- sdk.setPermissionMode(session, msg.mode);
1304
+ if (msg.type === "set_permission_mode") {
1305
+ var permissionModes = require("./session-permission-mode");
1306
+ var requestId = typeof msg.requestId === "string" && msg.requestId.length <= 128 ? msg.requestId : null;
1307
+ var requestedProjectSlug = typeof msg.projectSlug === "string" && msg.projectSlug.length <= 256 ? msg.projectSlug : null;
1308
+ var requestedSessionId = Number(msg.sessionId);
1309
+ var session = Number.isSafeInteger(requestedSessionId) ? sm.sessions.get(requestedSessionId) : null;
1310
+ function permissionResult(ok, error, includeState) {
1311
+ var state = includeState && session ? permissionModes.clientState(session, sm.currentPermissionMode, sm.defaultVendor) : null;
1312
+ sendTo(ws, {
1313
+ type: "permission_mode_result",
1314
+ ok: ok === true,
1315
+ requestId: requestId,
1316
+ projectSlug: requestedProjectSlug,
1317
+ sessionId: Number.isSafeInteger(requestedSessionId) ? requestedSessionId : null,
1318
+ mode: state ? state.requestedPermissionMode : null,
1319
+ effectivePermissionMode: state ? state.effectivePermissionMode : null,
1320
+ permissionCapabilities: state ? state.permissionCapabilities : null,
1321
+ mcpPermissionModeOverrides: state ? state.mcpPermissionModeOverrides : {},
1322
+ error: error || null,
1323
+ });
1242
1324
  }
1243
- send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1325
+ if (requestedProjectSlug !== slug) {
1326
+ permissionResult(false, "The permission request belongs to a different project.", false);
1327
+ return true;
1328
+ }
1329
+ if (!session || !ownsSearchSession(ws, session) || isDriverOperatedSession(session)) {
1330
+ permissionResult(false, "Permissions can only be changed on an accessible Driver session.", false);
1331
+ return true;
1332
+ }
1333
+ var permissionRuntimeMode = session.runtimeMode || session.mode || "gui";
1334
+ if (permissionRuntimeMode !== "gui") {
1335
+ permissionResult(false, "Permissions can only be changed on a GUI session.", true);
1336
+ return true;
1337
+ }
1338
+ if (dangerouslySkipPermissions) {
1339
+ permissionResult(false, "Skip permissions is forced by the server setting.", true);
1340
+ return true;
1341
+ }
1342
+ var valid = permissionModes.validate(session, msg.mode, sm.defaultVendor);
1343
+ if (!valid.ok) { permissionResult(false, valid.error, true); return true; }
1344
+ (async function() {
1345
+ try {
1346
+ if (session.permissionMode === "bypassPermissions" && valid.mode !== "bypassPermissions" && autonomousRun) {
1347
+ await autonomousRun.stopForPermissionChange(session);
1348
+ }
1349
+ await fullAccessService.setMode(session, valid.mode);
1350
+ var state = permissionModes.clientState(session, sm.currentPermissionMode, sm.defaultVendor);
1351
+ sm.sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: session.vendor || sm.defaultVendor || "claude", mode: state.requestedPermissionMode,
1352
+ effectivePermissionMode: state.effectivePermissionMode, permissionRequestId: requestId,
1353
+ permissionCapabilities: state.permissionCapabilities, mcpPermissionModeOverrides: state.mcpPermissionModeOverrides,
1354
+ effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1355
+ permissionResult(true, null, true);
1356
+ } catch (error) {
1357
+ permissionResult(false, "Failed to change permission mode: " + (error.message || error), true);
1358
+ }
1359
+ })();
1244
1360
  return true;
1245
1361
  }
1246
1362
 
@@ -1248,12 +1364,12 @@ function attachSessions(ctx) {
1248
1364
  var fullAccessSession = msg.id ? sm.sessions.get(Number(msg.id)) : getSessionForWs(ws);
1249
1365
  var fullAccessEnabled = msg.enabled === true;
1250
1366
  var fullAccessMode = fullAccessSession && (fullAccessSession.runtimeMode || fullAccessSession.mode || "gui");
1251
- if (!fullAccessSession || fullAccessMode !== "gui") {
1367
+ if (!fullAccessSession || fullAccessMode !== "gui" || isDriverOperatedSession(fullAccessSession)) {
1252
1368
  sendTo(ws, { type: "session_full_access_result", ok: false, id: msg.id || null, error: "Skip Permissions is only available for GUI sessions." });
1253
1369
  return true;
1254
1370
  }
1255
1371
  if (usersModule.isMultiUser() && ws._clayUser &&
1256
- !usersModule.canAccessSession(ws._clayUser.id, fullAccessSession, { visibility: "public" })) {
1372
+ !usersModule.canAccessSession(ws._clayUser.id, fullAccessSession, getProjectAccess())) {
1257
1373
  sendTo(ws, { type: "session_full_access_result", ok: false, id: fullAccessSession.localId, error: "You do not have access to this session." });
1258
1374
  return true;
1259
1375
  }
@@ -1493,7 +1609,11 @@ function attachSessions(ctx) {
1493
1609
  } else {
1494
1610
  forkHistory = session.history.slice();
1495
1611
  }
1496
- var forked = sm.createSession({ vendor: session.vendor, ownerId: session.ownerId || null }, ws);
1612
+ var forked = sm.createSession({
1613
+ vendor: session.vendor,
1614
+ ownerId: session.ownerId || null,
1615
+ sessionVisibility: sessionVisibility.normalize(session.sessionVisibility),
1616
+ }, ws);
1497
1617
  forked.cliSessionId = result.sessionId;
1498
1618
  forked.title = forkTitle;
1499
1619
  forked.history = forkHistory;
@@ -1511,7 +1631,14 @@ function attachSessions(ctx) {
1511
1631
  // Read history from CLI session files
1512
1632
  var cliSess = require("./cli-sessions");
1513
1633
  return cliSess.readCliSessionHistory(resolveSessionHome(session), cwd, result.sessionId).then(function(history) {
1514
- var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
1634
+ var forked = sm.resumeSession(result.sessionId, {
1635
+ history: history,
1636
+ title: forkTitle,
1637
+ vendor: session.vendor,
1638
+ ownerId: session.ownerId || null,
1639
+ sessionVisibility: sessionVisibility.normalize(session.sessionVisibility),
1640
+ sessionVisibilityExplicit: session.sessionVisibilityExplicit === true,
1641
+ }, ws);
1515
1642
  if (forked) {
1516
1643
  ws._clayActiveSession = forked.localId;
1517
1644
  sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
@@ -1577,11 +1704,13 @@ function attachSessions(ctx) {
1577
1704
 
1578
1705
  // --- Plan approval: "allow_accept_edits" -- approve + switch to acceptEdits mode ---
1579
1706
  if (decision === "allow_accept_edits") {
1580
- sdk.setPermissionMode(session, "acceptEdits");
1581
- sm.currentPermissionMode = "acceptEdits";
1582
- send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1583
- pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1584
- sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
1707
+ fullAccessService.setMode(session, "acceptEdits").then(function() {
1708
+ pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1709
+ sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
1710
+ }).catch(function(error) {
1711
+ pending.resolve({ behavior: "deny", message: "Could not enter the accepted-edits workflow." });
1712
+ sendTo(ws, { type: "error", text: "Failed to change permission mode: " + (error.message || error) });
1713
+ });
1585
1714
  return true;
1586
1715
  }
1587
1716
 
@@ -1606,10 +1735,6 @@ function attachSessions(ctx) {
1606
1735
  }
1607
1736
  });
1608
1737
 
1609
- // Update permission mode for the new session
1610
- sm.currentPermissionMode = "acceptEdits";
1611
- send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1612
-
1613
1738
  // Build prompt from plan content (sent from client) or plan file path
1614
1739
  var clientPlanContent = msg.planContent || "";
1615
1740
  var planPrompt;
@@ -1628,6 +1753,7 @@ function attachSessions(ctx) {
1628
1753
  ]).then(function () {
1629
1754
  try {
1630
1755
  var newSession = sm.createSession(null, ws);
1756
+ newSession.permissionMode = "acceptEdits";
1631
1757
  // Send the plan as the first user message (with planContent for UI rendering)
1632
1758
  var userMsg = { type: "user_message", text: planPrompt, planContent: clientPlanContent || null };
1633
1759
  newSession.history.push(userMsg);
@@ -122,10 +122,13 @@ function attachUserMessage(ctx) {
122
122
  function handleUserMessage(ws, msg, forcedSession, internal) {
123
123
  if (msg.type === "pending_message_get" || msg.type === "pending_message_edit" || msg.type === "pending_message_cancel" || msg.type === "pending_message_reorder" || msg.type === "pending_message_resume") {
124
124
  var queueSession = forcedSession || getSessionForWs(ws);
125
- if (!queueSession || !pendingMessageQueue) return true;
126
125
  var queueActor = ws && ws._clayUser;
127
126
  var queueResult;
128
- if (msg.projectSlug !== slug) queueResult = { ok: false, error: "Project changed", sessionId: queueSession.localId, projectSlug: slug };
127
+ var requestedQueue = { ok: false, projectSlug: msg.projectSlug || slug, sessionId: msg.sessionId == null ? null : msg.sessionId, revision: 0, paused: false, items: [] };
128
+ if (!pendingMessageQueue) queueResult = Object.assign({}, requestedQueue, { error: "The message queue is unavailable." });
129
+ else if (!queueSession) queueResult = Object.assign({}, requestedQueue, { error: "The session is no longer available." });
130
+ else if (msg.projectSlug !== slug) queueResult = Object.assign({}, requestedQueue, { error: "Project changed" });
131
+ else if (msg.sessionId == null || String(msg.sessionId) !== String(queueSession.localId)) queueResult = Object.assign({}, requestedQueue, { error: "Session changed" });
129
132
  else if (msg.type === "pending_message_resume") {
130
133
  queueResult = pendingMessageQueue.resume(queueSession, queueActor, msg.revision, msg.sessionId);
131
134
  if (queueResult.ok && !queueResult.paused) {
package/lib/project.js CHANGED
@@ -794,6 +794,10 @@ function createProjectContext(opts) {
794
794
  var _issueWork = require("./project-issue-launch").attachIssueLaunch({
795
795
  sm: sm, resolveDefaultAi: opts.resolveDefaultAi, getSdk: function () { return sdk; },
796
796
  getLinuxUserForSession: getLinuxUserForSession, osUsers: osUsers,
797
+ getProjectAccess: function () {
798
+ if (isMate) return { visibility: "private", ownerId: projectOwnerId || null, allowedUsers: [] };
799
+ return opts.getProjectAccess ? opts.getProjectAccess() : { visibility: "public", ownerId: projectOwnerId || null };
800
+ },
797
801
  ensureProjectAccessForSession: ensureProjectAccessForSession,
798
802
  canStartWork: function (ws, actor) { return clients.has(ws) && ws.readyState === 1 && ws._clayUser === actor; },
799
803
  onProcessingChanged: onProcessingChanged, sendTo: sendTo,
@@ -1941,6 +1945,7 @@ function createProjectContext(opts) {
1941
1945
  adapter: adapter,
1942
1946
  fullAccessService: _fullAccess,
1943
1947
  autonomousRun: _autonomousRun,
1948
+ isDriverOperatedSession: function (session) { return !isMate && _sessionPair.workerPermission.isDriverOperated(session); },
1944
1949
  getProjectList: getProjectList,
1945
1950
  getProjectCount: getProjectCount,
1946
1951
  getScheduleCount: getScheduleCount,
package/lib/public/app.js CHANGED
@@ -333,6 +333,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
333
333
  cursorSharingHydrated: false,
334
334
  pendingOutboundMessages: [],
335
335
  pendingMessageQueue: { projectSlug: null, sessionId: null, revision: 0, paused: false, items: [], loading: false, error: "" },
336
+ pendingMessageQueueRecovery: false,
336
337
  pendingMessageQueueRequests: {},
337
338
  pendingMessageQueueEdit: null,
338
339
  pendingMessageQueueDrag: null,
@@ -432,6 +433,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
432
433
  defaultAiDraft: { vendor: "", model: "", effort: "" },
433
434
  defaultAiDraftDirty: false,
434
435
  defaultVendorState: { loading: false, saving: false, getRequestId: null, saveRequestId: null, preference: null, preferencePresent: false, installedVendors: [], accountId: null, projectSlug: null, serverEpoch: null, canonicalRevision: 0, error: "" },
436
+ projectSessionVisibilityState: { active: null, pending: null },
435
437
 
436
438
  // dm
437
439
  dmTargetUser: null,
@@ -492,6 +494,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
492
494
  // new-session menu doesn't flash the TUI-only entry.
493
495
  claudeOpenMode: "gui",
494
496
  currentMode: "default",
497
+ effectivePermissionMode: null,
498
+ permissionCapabilities: { auto: false, mcpOverride: false },
499
+ permissionModePendingByTarget: {},
500
+ mcpPermissionModePendingByTarget: {},
501
+ mcpPermissionModeOverrides: {},
495
502
  currentEffort: "medium",
496
503
  currentBetas: [],
497
504
  currentThinking: "adaptive",