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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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 () {
|
|
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
|
-
|
|
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:
|
|
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);
|
package/lib/project-sessions.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
1170
|
-
var
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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"
|
|
1235
|
-
|
|
1236
|
-
var
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
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,
|
|
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({
|
|
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, {
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
-
|
|
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",
|