clay-server 2.44.1-beta.1 → 2.45.0-beta.1
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.js +27 -4
- package/lib/os-users.js +69 -2
- package/lib/project-http.js +4 -2
- package/lib/project-sessions.js +86 -21
- package/lib/public/app.js +1 -1
- package/lib/public/index.html +15 -0
- package/lib/public/modules/app-messages.js +54 -2
- package/lib/public/modules/mcp-ui.js +44 -0
- package/lib/public/modules/profile.js +2 -2
- package/lib/public/modules/server-settings.js +34 -0
- package/lib/public/modules/skills.js +16 -0
- package/lib/public/modules/tools.js +156 -0
- package/lib/public/modules/user-settings.js +2 -2
- package/lib/sdk-bridge.js +76 -0
- package/lib/sdk-message-processor.js +67 -0
- package/lib/server-settings.js +2 -2
- package/lib/server.js +2 -0
- package/lib/terminal.js +4 -1
- package/lib/users-preferences.js +8 -7
- package/lib/ws-schema.js +9 -0
- package/lib/yoke/adapters/claude-worker.js +62 -0
- package/lib/yoke/adapters/claude.js +121 -3
- package/package.json +2 -2
package/lib/daemon.js
CHANGED
|
@@ -28,7 +28,8 @@ var path = require("path");
|
|
|
28
28
|
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, removeFromClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo, isPidAlive, clearStaleConfig, REAL_HOME } = require("./config");
|
|
29
29
|
var { createIPCServer } = require("./ipc");
|
|
30
30
|
var { createServer, generateAuthToken } = require("./server");
|
|
31
|
-
var
|
|
31
|
+
var osUsersMod = require("./os-users");
|
|
32
|
+
var { checkAclSupport, grantProjectAccess, revokeProjectAccess, provisionAllUsers, provisionLinuxUser, grantAllUsersAccess, deactivateLinuxUser, ensureProjectsDir } = osUsersMod;
|
|
32
33
|
var usersModule = require("./users");
|
|
33
34
|
var { createWorktree, removeWorktree, isWorktree } = require("./worktree");
|
|
34
35
|
var mates = require("./mates");
|
|
@@ -52,6 +53,9 @@ if (process.env.SUDO_USER) console.log("[daemon] SUDO_USER: " + process.env.SUDO
|
|
|
52
53
|
console.log("[daemon] UID: " + (typeof process.getuid === "function" ? process.getuid() : "N/A"));
|
|
53
54
|
|
|
54
55
|
// --- OS users mode: check required system dependencies ---
|
|
56
|
+
// Supplementary-group inheritance (issue #367): default on so sessions behave
|
|
57
|
+
// like a normal login; operators can disable via config / the settings toggle.
|
|
58
|
+
osUsersMod.setInheritGroups(config.inheritGroups !== false);
|
|
55
59
|
if (config.osUsers) {
|
|
56
60
|
var checkExec = require("child_process").execFileSync;
|
|
57
61
|
var missing = [];
|
|
@@ -63,6 +67,11 @@ if (config.osUsers) {
|
|
|
63
67
|
console.error("[daemon] Install with: sudo apt install " + missing.map(function (m) { return m.split(" ")[0]; }).join(" "));
|
|
64
68
|
process.exit(78); // EX_CONFIG
|
|
65
69
|
}
|
|
70
|
+
if (config.inheritGroups !== false) {
|
|
71
|
+
console.log("[daemon] Sessions inherit each user's supplementary groups (docker, etc.); disable in Settings > User Groups.");
|
|
72
|
+
} else {
|
|
73
|
+
console.log("[daemon] Sessions run with primary group only (supplementary groups dropped); enable in Settings > User Groups.");
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
// --- TLS ---
|
|
@@ -216,7 +225,8 @@ var relay = createServer({
|
|
|
216
225
|
if (uidGid) {
|
|
217
226
|
fs.chmodSync(targetDir, 0o700);
|
|
218
227
|
execFileSync("chown", ["-R", linuxUser + ":" + linuxUser, targetDir]);
|
|
219
|
-
|
|
228
|
+
var gitInit = osUsersMod.wrapSpawnAsUser("git", ["init"], { cwd: targetDir, uid: uidGid.uid, gid: uidGid.gid, env: { PATH: "/usr/local/bin:/usr/bin:/bin" } });
|
|
229
|
+
execFileSync(gitInit.command, gitInit.args, gitInit.options);
|
|
220
230
|
} else {
|
|
221
231
|
execFileSync("git", ["init"], { cwd: targetDir });
|
|
222
232
|
}
|
|
@@ -296,7 +306,8 @@ var relay = createServer({
|
|
|
296
306
|
return;
|
|
297
307
|
}
|
|
298
308
|
}
|
|
299
|
-
var
|
|
309
|
+
var cloneSpawn = osUsersMod.wrapSpawnAsUser("git", ["clone", cloneUrl, targetDir], spawnOpts);
|
|
310
|
+
var proc = spawn(cloneSpawn.command, cloneSpawn.args, cloneSpawn.options);
|
|
300
311
|
var stderrBuf = "";
|
|
301
312
|
proc.stderr.on("data", function (chunk) { stderrBuf += chunk.toString(); });
|
|
302
313
|
// 5 minute timeout
|
|
@@ -689,8 +700,10 @@ var relay = createServer({
|
|
|
689
700
|
headless: !!config.headless,
|
|
690
701
|
keepAwake: !!config.keepAwake,
|
|
691
702
|
autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
|
|
703
|
+
osUsers: !!config.osUsers,
|
|
704
|
+
inheritGroups: config.inheritGroups !== false,
|
|
692
705
|
chatLayout: config.chatLayout || "channel",
|
|
693
|
-
matesEnabled: config.matesEnabled
|
|
706
|
+
matesEnabled: config.matesEnabled === true,
|
|
694
707
|
pinEnabled: !!config.pinHash,
|
|
695
708
|
platform: process.platform,
|
|
696
709
|
hostname: os2.hostname(),
|
|
@@ -804,6 +817,16 @@ var relay = createServer({
|
|
|
804
817
|
console.log("[daemon] Keep awake:", want, "(web)");
|
|
805
818
|
return { ok: true, keepAwake: want };
|
|
806
819
|
},
|
|
820
|
+
onSetInheritGroups: function (value) {
|
|
821
|
+
var want = !!value;
|
|
822
|
+
config.inheritGroups = want;
|
|
823
|
+
saveConfig(config);
|
|
824
|
+
// Apply live so subsequent session/terminal spawns pick it up without a
|
|
825
|
+
// daemon restart; already-running sessions keep their current groups.
|
|
826
|
+
osUsersMod.setInheritGroups(want);
|
|
827
|
+
console.log("[daemon] Inherit supplementary groups:", want, "(web)");
|
|
828
|
+
return { ok: true, inheritGroups: want };
|
|
829
|
+
},
|
|
807
830
|
onShutdown: function () {
|
|
808
831
|
console.log("[daemon] Shutdown requested via web UI");
|
|
809
832
|
gracefulShutdown();
|
package/lib/os-users.js
CHANGED
|
@@ -14,6 +14,68 @@ function isSafeLinuxUsername(username) {
|
|
|
14
14
|
return typeof username === "string" && /^[a-z_][a-z0-9_-]*[$]?$/.test(username);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// --- Supplementary group inheritance (issue #367) ---
|
|
18
|
+
// Node's child_process { uid, gid } options call setuid/setgid but never
|
|
19
|
+
// initgroups(), so spawned sessions drop all supplementary groups (docker,
|
|
20
|
+
// video, wheel, ...). When inheritGroups is enabled we route the spawn through
|
|
21
|
+
// `setpriv --init-groups` so the child gets the same groups as a normal login.
|
|
22
|
+
var _inheritGroups = true; // default: behave like a normal login shell
|
|
23
|
+
var _setprivAvail = null;
|
|
24
|
+
var _warnedNoSetpriv = false;
|
|
25
|
+
|
|
26
|
+
function setInheritGroups(value) {
|
|
27
|
+
_inheritGroups = !!value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getInheritGroups() {
|
|
31
|
+
return _inheritGroups;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setprivAvailable() {
|
|
35
|
+
if (_setprivAvail !== null) return _setprivAvail;
|
|
36
|
+
try {
|
|
37
|
+
execFileSync("setpriv", ["--help"], { stdio: "ignore", timeout: 5000 });
|
|
38
|
+
_setprivAvail = true;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
_setprivAvail = false;
|
|
41
|
+
}
|
|
42
|
+
return _setprivAvail;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a spawn/exec invocation so the child runs as the target user WITH its
|
|
47
|
+
* supplementary groups initialized (initgroups), which Node's uid/gid options
|
|
48
|
+
* do not do. Accepts (command, args, options) where options may carry
|
|
49
|
+
* uid/gid, and returns a normalized { command, args, options } to pass to
|
|
50
|
+
* spawn()/execFileSync()/pty.spawn().
|
|
51
|
+
*
|
|
52
|
+
* - Same-user spawns (no uid/gid in options) are returned unchanged.
|
|
53
|
+
* - When inheritGroups is disabled, or setpriv is unavailable, falls back to
|
|
54
|
+
* Node's uid/gid options (primary group only) so behavior degrades safely.
|
|
55
|
+
* Requires the parent process to be root, which is the case in os-users mode.
|
|
56
|
+
*/
|
|
57
|
+
function wrapSpawnAsUser(command, args, options) {
|
|
58
|
+
args = args || [];
|
|
59
|
+
options = options || {};
|
|
60
|
+
var uid = options.uid;
|
|
61
|
+
var gid = options.gid;
|
|
62
|
+
if (uid == null || gid == null) return { command: command, args: args, options: options };
|
|
63
|
+
|
|
64
|
+
if (_inheritGroups && setprivAvailable()) {
|
|
65
|
+
var wrappedArgs = ["--reuid", String(uid), "--regid", String(gid), "--init-groups", "--", command].concat(args);
|
|
66
|
+
var newOpts = Object.assign({}, options);
|
|
67
|
+
delete newOpts.uid;
|
|
68
|
+
delete newOpts.gid;
|
|
69
|
+
return { command: "setpriv", args: wrappedArgs, options: newOpts };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (_inheritGroups && !_warnedNoSetpriv) {
|
|
73
|
+
_warnedNoSetpriv = true;
|
|
74
|
+
console.warn("[os-users] inheritGroups enabled but `setpriv` not found; supplementary groups will be dropped. Install util-linux to restore them.");
|
|
75
|
+
}
|
|
76
|
+
return { command: command, args: args, options: options };
|
|
77
|
+
}
|
|
78
|
+
|
|
17
79
|
/**
|
|
18
80
|
* Resolve Linux user info from username via getent passwd.
|
|
19
81
|
* Returns { uid, gid, home, user, shell } or throws on failure.
|
|
@@ -84,13 +146,14 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
84
146
|
"var buf = fs.readFileSync(f);",
|
|
85
147
|
"process.stdout.write(buf.toString('base64'));",
|
|
86
148
|
].join(" ");
|
|
87
|
-
var
|
|
149
|
+
var binSpawn = wrapSpawnAsUser(process.execPath, ["-e", script], {
|
|
88
150
|
encoding: "utf8",
|
|
89
151
|
timeout: 10000,
|
|
90
152
|
uid: osUserInfo.uid,
|
|
91
153
|
gid: osUserInfo.gid,
|
|
92
154
|
maxBuffer: 50 * 1024 * 1024,
|
|
93
155
|
});
|
|
156
|
+
var binOutput = execFileSync(binSpawn.command, binSpawn.args, binSpawn.options);
|
|
94
157
|
return { buffer: Buffer.from(binOutput.trim(), "base64") };
|
|
95
158
|
} else if (op === "write") {
|
|
96
159
|
script = [
|
|
@@ -104,12 +167,13 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
104
167
|
throw new Error("Unknown fsAsUser operation: " + op);
|
|
105
168
|
}
|
|
106
169
|
|
|
107
|
-
var
|
|
170
|
+
var fsSpawn = wrapSpawnAsUser(process.execPath, ["-e", script], {
|
|
108
171
|
encoding: "utf8",
|
|
109
172
|
timeout: 10000,
|
|
110
173
|
uid: osUserInfo.uid,
|
|
111
174
|
gid: osUserInfo.gid,
|
|
112
175
|
});
|
|
176
|
+
var output = execFileSync(fsSpawn.command, fsSpawn.args, fsSpawn.options);
|
|
113
177
|
|
|
114
178
|
return JSON.parse(output.trim());
|
|
115
179
|
}
|
|
@@ -554,6 +618,9 @@ module.exports = {
|
|
|
554
618
|
grantAllUsersAccess: grantAllUsersAccess,
|
|
555
619
|
installClaudeCli: installClaudeCli,
|
|
556
620
|
claudeAvailableForUser: claudeAvailableForUser,
|
|
621
|
+
setInheritGroups: setInheritGroups,
|
|
622
|
+
getInheritGroups: getInheritGroups,
|
|
623
|
+
wrapSpawnAsUser: wrapSpawnAsUser,
|
|
557
624
|
deactivateLinuxUser: deactivateLinuxUser,
|
|
558
625
|
ensureProjectsDir: ensureProjectsDir,
|
|
559
626
|
isHomeDirectory: isHomeDirectory,
|
package/lib/project-http.js
CHANGED
|
@@ -335,7 +335,8 @@ function attachHTTP(ctx) {
|
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
337
|
console.log("[skill-install] spawning: npx skills add " + url + " --skill " + skill + " --yes " + scopeFlag + " (cwd: " + spawnCwd + ")");
|
|
338
|
-
var
|
|
338
|
+
var skillAddSpawn = require("./os-users").wrapSpawnAsUser("npx", ["skills", "add", url, "--skill", skill, "--yes", scopeFlag], skillSpawnOpts);
|
|
339
|
+
var child = spawn(skillAddSpawn.command, skillAddSpawn.args, skillAddSpawn.options);
|
|
339
340
|
var stdoutBuf = "";
|
|
340
341
|
var stderrBuf = "";
|
|
341
342
|
child.stdout.on("data", function (chunk) {
|
|
@@ -427,11 +428,12 @@ function attachHTTP(ctx) {
|
|
|
427
428
|
if (uninstallUserInfo) {
|
|
428
429
|
// Run rm as target user to respect permissions
|
|
429
430
|
var rmScript = "var fs = require('fs'); fs.rmSync(" + JSON.stringify(resolved) + ", { recursive: true, force: true });";
|
|
430
|
-
|
|
431
|
+
var rmSpawn = require("./os-users").wrapSpawnAsUser(process.execPath, ["-e", rmScript], {
|
|
431
432
|
uid: uninstallUserInfo.uid,
|
|
432
433
|
gid: uninstallUserInfo.gid,
|
|
433
434
|
timeout: 10000,
|
|
434
435
|
});
|
|
436
|
+
execFileSync(rmSpawn.command, rmSpawn.args, rmSpawn.options);
|
|
435
437
|
} else {
|
|
436
438
|
fs.rmSync(resolved, { recursive: true, force: true });
|
|
437
439
|
}
|
package/lib/project-sessions.js
CHANGED
|
@@ -169,19 +169,32 @@ function attachSessions(ctx) {
|
|
|
169
169
|
var termId = (typeof s.runtimeTerminalId === "number")
|
|
170
170
|
? s.runtimeTerminalId
|
|
171
171
|
: (typeof s.terminalId === "number" ? s.terminalId : null);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
172
|
+
// Don't banner a session someone is already watching — they can see
|
|
173
|
+
// the reply in the TUI. Gate at the source (server) rather than relying
|
|
174
|
+
// on the client's activeSessionId suppression, which isn't reliable for
|
|
175
|
+
// TUI sessions and lets banners pile up for the session in view.
|
|
176
|
+
var beingViewed = false;
|
|
177
|
+
for (var vws of clients) {
|
|
178
|
+
if (vws.readyState === 1 && vws._clayActiveSession === s.localId) {
|
|
179
|
+
beingViewed = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!beingViewed) {
|
|
184
|
+
try {
|
|
185
|
+
ctx._notifications && ctx._notifications.notify("tui_attention", {
|
|
186
|
+
slug: slug,
|
|
187
|
+
sessionId: s.localId,
|
|
188
|
+
ownerId: s.ownerId || null,
|
|
189
|
+
targetUserId: s.ownerId || null,
|
|
190
|
+
title: "Claude responded",
|
|
191
|
+
body: firstLine,
|
|
192
|
+
terminalId: termId,
|
|
193
|
+
sessionTitle: s.title || "",
|
|
194
|
+
cliSessionId: s.cliSessionId || null,
|
|
195
|
+
});
|
|
196
|
+
} catch (e) {}
|
|
197
|
+
}
|
|
185
198
|
// Re-read the assistant text index and broadcast it so any client
|
|
186
199
|
// with this session in view can wire hover-to-grab onto the new
|
|
187
200
|
// message right away. The transcript is small enough that a full
|
|
@@ -904,6 +917,24 @@ function attachSessions(ctx) {
|
|
|
904
917
|
return true;
|
|
905
918
|
}
|
|
906
919
|
|
|
920
|
+
if (msg.type === "reload_skills") {
|
|
921
|
+
var session = getSessionForWs(ws);
|
|
922
|
+
if (session && sdk.reloadSkills) {
|
|
923
|
+
sdk.reloadSkills(session);
|
|
924
|
+
}
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (msg.type === "set_mcp_permission_mode_override" && msg.serverName) {
|
|
929
|
+
var session = getSessionForWs(ws);
|
|
930
|
+
if (session && sdk.setMcpPermissionModeOverride) {
|
|
931
|
+
// mode: "default" | "auto" | null (null clears the override)
|
|
932
|
+
var mcpMode = (msg.mode === "auto" || msg.mode === "default") ? msg.mode : null;
|
|
933
|
+
sdk.setMcpPermissionModeOverride(session, msg.serverName, mcpMode);
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
|
|
907
938
|
if (msg.type === "set_vendor" && msg.vendor) {
|
|
908
939
|
var vendorSession = getSessionForWs(ws);
|
|
909
940
|
if (vendorSession) {
|
|
@@ -1256,13 +1287,18 @@ function attachSessions(ctx) {
|
|
|
1256
1287
|
sdk.pushMessage(session, answerText);
|
|
1257
1288
|
}
|
|
1258
1289
|
} else {
|
|
1259
|
-
// Claude native AskUserQuestion path (
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1290
|
+
// Claude native AskUserQuestion path (built-in tool, intercepted via
|
|
1291
|
+
// canUseTool). In headless SDK mode the built-in can't render its own
|
|
1292
|
+
// answer UI: resolving "allow" lets it return an EMPTY result, so the
|
|
1293
|
+
// model continues the same turn with no answer ("I don't see an
|
|
1294
|
+
// answer"). Injected user messages also lose the race against that
|
|
1295
|
+
// empty result. Instead, deliver the answer as the tool_result by
|
|
1296
|
+
// denying with the formatted answer text — the SDK surfaces a deny
|
|
1297
|
+
// `message` to the model as the tool result, so the model receives the
|
|
1298
|
+
// user's choice in the same turn. (updatedInput.answers is not read by
|
|
1299
|
+
// the 0.3.x built-in, which is what caused answers to be dropped.)
|
|
1300
|
+
var nativeAnswerText = formatAskUserAnswerAsMessage(pending.input, answers);
|
|
1301
|
+
pending.resolve({ behavior: "deny", message: nativeAnswerText });
|
|
1266
1302
|
}
|
|
1267
1303
|
return true;
|
|
1268
1304
|
}
|
|
@@ -1457,6 +1493,26 @@ function attachSessions(ctx) {
|
|
|
1457
1493
|
return true;
|
|
1458
1494
|
}
|
|
1459
1495
|
|
|
1496
|
+
// --- Host user dialog response (SDK request_user_dialog) ---
|
|
1497
|
+
if (msg.type === "user_dialog_response") {
|
|
1498
|
+
var session = getSessionForWs(ws);
|
|
1499
|
+
if (!session) return true;
|
|
1500
|
+
var pending = session.pendingUserDialogs && session.pendingUserDialogs[msg.requestId];
|
|
1501
|
+
if (!pending) return true;
|
|
1502
|
+
delete session.pendingUserDialogs[msg.requestId];
|
|
1503
|
+
if (msg.behavior === "completed") {
|
|
1504
|
+
pending.resolve({ behavior: "completed", result: msg.result });
|
|
1505
|
+
} else {
|
|
1506
|
+
pending.resolve({ behavior: "cancelled" });
|
|
1507
|
+
}
|
|
1508
|
+
sm.sendAndRecord(session, {
|
|
1509
|
+
type: "user_dialog_resolved",
|
|
1510
|
+
requestId: msg.requestId,
|
|
1511
|
+
behavior: msg.behavior === "completed" ? "completed" : "cancelled",
|
|
1512
|
+
});
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1460
1516
|
// --- Browse directories (for add-project autocomplete) ---
|
|
1461
1517
|
if (msg.type === "browse_dir") {
|
|
1462
1518
|
var rawPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
|
|
@@ -1700,7 +1756,7 @@ function attachSessions(ctx) {
|
|
|
1700
1756
|
|
|
1701
1757
|
// --- Daemon config / server management (admin-only in multi-user mode) ---
|
|
1702
1758
|
if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
|
|
1703
|
-
msg.type === "set_auto_continue" || msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
1759
|
+
msg.type === "set_auto_continue" || msg.type === "set_inherit_groups" || msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
|
|
1704
1760
|
if (usersModule.isMultiUser()) {
|
|
1705
1761
|
var _wsUser = ws._clayUser;
|
|
1706
1762
|
if (!_wsUser || _wsUser.role !== "admin") {
|
|
@@ -1744,6 +1800,15 @@ function attachSessions(ctx) {
|
|
|
1744
1800
|
return true;
|
|
1745
1801
|
}
|
|
1746
1802
|
|
|
1803
|
+
if (msg.type === "set_inherit_groups") {
|
|
1804
|
+
if (typeof opts.onSetInheritGroups === "function") {
|
|
1805
|
+
var igResult = opts.onSetInheritGroups(msg.value);
|
|
1806
|
+
sendTo(ws, { type: "set_inherit_groups_result", ok: igResult.ok, inheritGroups: igResult.inheritGroups });
|
|
1807
|
+
send({ type: "inherit_groups_changed", inheritGroups: igResult.inheritGroups });
|
|
1808
|
+
}
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1747
1812
|
if (msg.type === "get_claude_allow_list") {
|
|
1748
1813
|
var galUid = ws._clayUser ? ws._clayUser.id : null;
|
|
1749
1814
|
var galManaged = [];
|
package/lib/public/app.js
CHANGED
|
@@ -287,7 +287,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
287
287
|
cachedMatesList: [],
|
|
288
288
|
cachedDmFavorites: [],
|
|
289
289
|
cachedAvailableBuiltins: [],
|
|
290
|
-
matesEnabled:
|
|
290
|
+
matesEnabled: false,
|
|
291
291
|
returningFromMateDm: false,
|
|
292
292
|
pendingMateInterview: null,
|
|
293
293
|
pendingTermCommand: null,
|
package/lib/public/index.html
CHANGED
|
@@ -1151,6 +1151,7 @@
|
|
|
1151
1151
|
</optgroup>
|
|
1152
1152
|
<optgroup label="Server">
|
|
1153
1153
|
<option value="keep-awake" class="hidden" id="settings-keep-awake-opt">Keep Awake</option>
|
|
1154
|
+
<option value="inherit-groups" class="hidden" id="settings-inherit-groups-opt">User Groups</option>
|
|
1154
1155
|
<option value="restart">Restart</option>
|
|
1155
1156
|
<option value="shutdown">Shutdown</option>
|
|
1156
1157
|
</optgroup>
|
|
@@ -1177,6 +1178,7 @@
|
|
|
1177
1178
|
<div class="settings-nav-separator"></div>
|
|
1178
1179
|
<div class="settings-nav-category">Server</div>
|
|
1179
1180
|
<button class="settings-nav-item hidden" data-section="keep-awake" id="settings-keep-awake-nav"><span>Keep Awake</span></button>
|
|
1181
|
+
<button class="settings-nav-item hidden settings-admin-only" data-section="inherit-groups" id="settings-inherit-groups-nav"><span>User Groups</span></button>
|
|
1180
1182
|
<button class="settings-nav-item" data-section="restart"><span>Restart</span></button>
|
|
1181
1183
|
<button class="settings-nav-item settings-nav-danger" data-section="shutdown"><span>Shutdown</span></button>
|
|
1182
1184
|
</div>
|
|
@@ -1430,6 +1432,19 @@
|
|
|
1430
1432
|
</label>
|
|
1431
1433
|
</div>
|
|
1432
1434
|
</div>
|
|
1435
|
+
<div class="server-settings-section hidden settings-admin-only" data-section="inherit-groups" id="settings-inherit-groups-section">
|
|
1436
|
+
<h2>User Groups</h2>
|
|
1437
|
+
<div class="settings-card">
|
|
1438
|
+
<label class="settings-toggle-row">
|
|
1439
|
+
<div>
|
|
1440
|
+
<span class="settings-label">Inherit supplementary groups</span>
|
|
1441
|
+
<div class="settings-hint">Give each session the user's full group membership (docker, video, etc.), matching a normal login. Disable to run sessions with only the primary group. Requires <code>setpriv</code> (util-linux). Applies to new sessions.</div>
|
|
1442
|
+
</div>
|
|
1443
|
+
<input type="checkbox" id="settings-inherit-groups">
|
|
1444
|
+
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
|
1445
|
+
</label>
|
|
1446
|
+
</div>
|
|
1447
|
+
</div>
|
|
1433
1448
|
<div class="server-settings-section" data-section="restart">
|
|
1434
1449
|
<h2>Restart</h2>
|
|
1435
1450
|
<p class="settings-hint" style="margin-bottom: 16px;">Restart the Clay daemon process. All active sessions will be briefly interrupted and clients will automatically reconnect.</p>
|
|
@@ -21,11 +21,11 @@ import { renderMemoryList } from './mate-memory.js';
|
|
|
21
21
|
import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
|
|
22
22
|
import { handleFindInSessionResults } from './session-search.js';
|
|
23
23
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
|
|
24
|
-
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved } from './tools.js';
|
|
24
|
+
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
25
25
|
import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
|
|
26
26
|
import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch } from './filebrowser.js';
|
|
27
27
|
import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
|
|
28
|
-
import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
|
|
28
|
+
import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleInheritGroupsChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
|
|
29
29
|
import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
|
|
30
30
|
import { attachTuiView, detachTuiView, setTuiSuspendedView, tuiHandleTermOutput, tuiHandleTermResized, tuiHandleTermExited, tuiHandleTermClosed } from './session-tui-view.js';
|
|
31
31
|
import { handleTuiTranscriptState } from './tui-grab.js';
|
|
@@ -992,6 +992,16 @@ export function processMessage(msg) {
|
|
|
992
992
|
stopUrgentBlink();
|
|
993
993
|
break;
|
|
994
994
|
|
|
995
|
+
case "user_dialog_request":
|
|
996
|
+
renderUserDialogRequest(msg);
|
|
997
|
+
startUrgentBlink();
|
|
998
|
+
break;
|
|
999
|
+
|
|
1000
|
+
case "user_dialog_resolved":
|
|
1001
|
+
markUserDialogResolved(msg.requestId, msg.behavior);
|
|
1002
|
+
stopUrgentBlink();
|
|
1003
|
+
break;
|
|
1004
|
+
|
|
995
1005
|
case "slash_command_result":
|
|
996
1006
|
finalizeAssistantBlock();
|
|
997
1007
|
var cmdBlock = document.createElement("div");
|
|
@@ -1088,6 +1098,43 @@ export function processMessage(msg) {
|
|
|
1088
1098
|
addSystemMessage(msg.text, false);
|
|
1089
1099
|
break;
|
|
1090
1100
|
|
|
1101
|
+
case "thinking_tokens":
|
|
1102
|
+
updateThinkingTokens(msg.estimatedTokens);
|
|
1103
|
+
break;
|
|
1104
|
+
|
|
1105
|
+
case "informational":
|
|
1106
|
+
// level: info | notice | suggestion | warning
|
|
1107
|
+
if (msg.content) addSystemMessage(msg.content, msg.level === "warning");
|
|
1108
|
+
break;
|
|
1109
|
+
|
|
1110
|
+
case "permission_denied": {
|
|
1111
|
+
var deniedText = (msg.toolName ? msg.toolName + ": " : "")
|
|
1112
|
+
+ "tool call denied"
|
|
1113
|
+
+ (msg.reason ? " — " + msg.reason : (msg.message ? " — " + msg.message : ""))
|
|
1114
|
+
+ ".";
|
|
1115
|
+
addSystemMessage(deniedText, false);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
case "model_refusal": {
|
|
1120
|
+
var refusalText;
|
|
1121
|
+
if (msg.refusalKind === "fallback") {
|
|
1122
|
+
refusalText = "The model declined this request and automatically retried"
|
|
1123
|
+
+ (msg.fallbackModel ? " with " + msg.fallbackModel : " with a fallback model")
|
|
1124
|
+
+ (msg.originalModel ? " (originally " + msg.originalModel + ")" : "")
|
|
1125
|
+
+ ".";
|
|
1126
|
+
if (msg.category) refusalText += " Category: " + msg.category + ".";
|
|
1127
|
+
addSystemMessage(refusalText, false);
|
|
1128
|
+
} else {
|
|
1129
|
+
refusalText = "The model declined to respond to this request.";
|
|
1130
|
+
if (msg.explanation) refusalText += " " + msg.explanation;
|
|
1131
|
+
else if (msg.content) refusalText += " " + msg.content;
|
|
1132
|
+
if (msg.category) refusalText += " (Category: " + msg.category + ")";
|
|
1133
|
+
addSystemMessage(refusalText, true);
|
|
1134
|
+
}
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1091
1138
|
case "process_conflict":
|
|
1092
1139
|
removeMatePreThinking();
|
|
1093
1140
|
setActivity(null);
|
|
@@ -1705,6 +1752,11 @@ export function processMessage(msg) {
|
|
|
1705
1752
|
handleKeepAwakeChanged(msg);
|
|
1706
1753
|
break;
|
|
1707
1754
|
|
|
1755
|
+
case "set_inherit_groups_result":
|
|
1756
|
+
case "inherit_groups_changed":
|
|
1757
|
+
handleInheritGroupsChanged(msg);
|
|
1758
|
+
break;
|
|
1759
|
+
|
|
1708
1760
|
case "set_auto_continue_result":
|
|
1709
1761
|
case "auto_continue_changed":
|
|
1710
1762
|
handleAutoContinueChanged(msg);
|
|
@@ -137,6 +137,28 @@ function renderMcpServerList() {
|
|
|
137
137
|
|
|
138
138
|
row.appendChild(cb);
|
|
139
139
|
row.appendChild(info);
|
|
140
|
+
|
|
141
|
+
// Per-server permission mode override (applies to the active session only).
|
|
142
|
+
if (server.projectEnabled) {
|
|
143
|
+
var modeSel = document.createElement("select");
|
|
144
|
+
modeSel.className = "mcp-server-perm-mode";
|
|
145
|
+
modeSel.dataset.serverName = server.name;
|
|
146
|
+
modeSel.title = "Permission mode for this server (this session only)";
|
|
147
|
+
var optAsk = document.createElement("option");
|
|
148
|
+
optAsk.value = "default";
|
|
149
|
+
optAsk.textContent = "Ask";
|
|
150
|
+
var optAuto = document.createElement("option");
|
|
151
|
+
optAuto.value = "auto";
|
|
152
|
+
optAuto.textContent = "Auto-approve";
|
|
153
|
+
modeSel.appendChild(optAsk);
|
|
154
|
+
modeSel.appendChild(optAuto);
|
|
155
|
+
modeSel.value = server.permissionModeOverride === "auto" ? "auto" : "default";
|
|
156
|
+
// The row is a <label>; keep select interactions from toggling the checkbox.
|
|
157
|
+
modeSel.addEventListener("click", function (e) { e.stopPropagation(); e.preventDefault(); });
|
|
158
|
+
modeSel.addEventListener("change", onPermissionModeChange);
|
|
159
|
+
row.appendChild(modeSel);
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
contentEl.appendChild(row);
|
|
141
163
|
}
|
|
142
164
|
refreshIcons(contentEl);
|
|
@@ -293,3 +315,25 @@ function onToggle(e) {
|
|
|
293
315
|
}));
|
|
294
316
|
}
|
|
295
317
|
}
|
|
318
|
+
|
|
319
|
+
function onPermissionModeChange(e) {
|
|
320
|
+
var name = e.target.dataset.serverName;
|
|
321
|
+
var mode = e.target.value; // "default" | "auto"
|
|
322
|
+
|
|
323
|
+
// Remember locally so a re-render keeps the selection.
|
|
324
|
+
for (var i = 0; i < _mcpServers.length; i++) {
|
|
325
|
+
if (_mcpServers[i].name === name) {
|
|
326
|
+
_mcpServers[i].permissionModeOverride = mode;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
var ws = getWs();
|
|
332
|
+
if (ws && ws.readyState === 1) {
|
|
333
|
+
ws.send(JSON.stringify({
|
|
334
|
+
type: "set_mcp_permission_mode_override",
|
|
335
|
+
serverName: name,
|
|
336
|
+
mode: mode,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -590,8 +590,8 @@ export function initProfile(_ctx) {
|
|
|
590
590
|
|
|
591
591
|
// Apply Mates UI gate at boot so the sidebar avatars / DM picker
|
|
592
592
|
// entry / home-hub strip are hidden before the user opens settings.
|
|
593
|
-
// Default
|
|
594
|
-
var matesOn = data.matesEnabled
|
|
593
|
+
// Default OFF: Mates is opt-in, so only enable when explicitly true.
|
|
594
|
+
var matesOn = data.matesEnabled === true;
|
|
595
595
|
store.set({ matesEnabled: matesOn });
|
|
596
596
|
if (document.body) document.body.classList.toggle('mates-disabled', !matesOn);
|
|
597
597
|
}).catch(function(err) {
|
|
@@ -157,6 +157,17 @@ export function initServerSettings(appCtx) {
|
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// Inherit supplementary groups toggle (OS-users mode)
|
|
161
|
+
var inheritGroupsToggle = document.getElementById("settings-inherit-groups");
|
|
162
|
+
if (inheritGroupsToggle) {
|
|
163
|
+
inheritGroupsToggle.addEventListener("change", function () {
|
|
164
|
+
var ws = ctx.ws;
|
|
165
|
+
if (ws && ws.readyState === 1) {
|
|
166
|
+
ws.send(JSON.stringify({ type: "set_inherit_groups", value: this.checked }));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
// Image retention select
|
|
161
172
|
var imageRetentionSelect = document.getElementById("settings-image-retention");
|
|
162
173
|
if (imageRetentionSelect) {
|
|
@@ -482,6 +493,10 @@ export function updateDaemonConfig(config) {
|
|
|
482
493
|
var keepAwakeToggle = document.getElementById("settings-keep-awake");
|
|
483
494
|
if (keepAwakeToggle) keepAwakeToggle.checked = !!config.keepAwake;
|
|
484
495
|
|
|
496
|
+
// Inherit supplementary groups (only relevant in OS-users mode)
|
|
497
|
+
var inheritGroupsToggle = document.getElementById("settings-inherit-groups");
|
|
498
|
+
if (inheritGroupsToggle) inheritGroupsToggle.checked = config.inheritGroups !== false;
|
|
499
|
+
|
|
485
500
|
// Image retention
|
|
486
501
|
var imageRetentionSelect = document.getElementById("settings-image-retention");
|
|
487
502
|
if (imageRetentionSelect && config.imageRetentionDays !== undefined) {
|
|
@@ -517,6 +532,20 @@ export function updateDaemonConfig(config) {
|
|
|
517
532
|
if (keepAwakeNav) keepAwakeNav.classList.add("hidden");
|
|
518
533
|
if (keepAwakeOpt) keepAwakeOpt.classList.add("hidden");
|
|
519
534
|
}
|
|
535
|
+
|
|
536
|
+
// Show inherit-groups section/nav only in OS-users mode
|
|
537
|
+
var inheritGroupsSection = document.getElementById("settings-inherit-groups-section");
|
|
538
|
+
var inheritGroupsNav = document.getElementById("settings-inherit-groups-nav");
|
|
539
|
+
var inheritGroupsOpt = document.getElementById("settings-inherit-groups-opt");
|
|
540
|
+
if (config.osUsers) {
|
|
541
|
+
if (inheritGroupsSection) inheritGroupsSection.classList.remove("hidden");
|
|
542
|
+
if (inheritGroupsNav) inheritGroupsNav.classList.remove("hidden");
|
|
543
|
+
if (inheritGroupsOpt) inheritGroupsOpt.classList.remove("hidden");
|
|
544
|
+
} else {
|
|
545
|
+
if (inheritGroupsSection) inheritGroupsSection.classList.add("hidden");
|
|
546
|
+
if (inheritGroupsNav) inheritGroupsNav.classList.add("hidden");
|
|
547
|
+
if (inheritGroupsOpt) inheritGroupsOpt.classList.add("hidden");
|
|
548
|
+
}
|
|
520
549
|
}
|
|
521
550
|
|
|
522
551
|
export function handleSetPinResult(msg) {
|
|
@@ -532,6 +561,11 @@ export function handleKeepAwakeChanged(msg) {
|
|
|
532
561
|
if (keepAwakeToggle) keepAwakeToggle.checked = !!msg.keepAwake;
|
|
533
562
|
}
|
|
534
563
|
|
|
564
|
+
export function handleInheritGroupsChanged(msg) {
|
|
565
|
+
var inheritGroupsToggle = document.getElementById("settings-inherit-groups");
|
|
566
|
+
if (inheritGroupsToggle) inheritGroupsToggle.checked = msg.inheritGroups !== false;
|
|
567
|
+
}
|
|
568
|
+
|
|
535
569
|
export function handleAutoContinueChanged(msg) {
|
|
536
570
|
// Auto-continue is now per-user; server broadcast no longer updates UI
|
|
537
571
|
}
|