clay-server 5.1.0-beta.2 → 6.0.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.
Files changed (83) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/daemon-settings.js +11 -0
  3. package/lib/daemon.js +8 -37
  4. package/lib/linear-store.js +35 -8
  5. package/lib/os-users.js +5 -16
  6. package/lib/project-connection.js +0 -7
  7. package/lib/project-filesystem.js +21 -39
  8. package/lib/project-mate-interaction.js +2 -2
  9. package/lib/project-server-settings.js +31 -0
  10. package/lib/project-sessions.js +57 -184
  11. package/lib/public/app.js +6 -37
  12. package/lib/public/css/input.css +3 -1
  13. package/lib/public/css/project-settings-modal.css +56 -0
  14. package/lib/public/css/server-settings-system.css +125 -0
  15. package/lib/public/css/session-github.css +25 -16
  16. package/lib/public/css/settings-ui.css +247 -22
  17. package/lib/public/css/shared-browser.css +2 -0
  18. package/lib/public/css/title-bar.css +5 -1
  19. package/lib/public/index.html +170 -446
  20. package/lib/public/modules/account-island.js +9 -7
  21. package/lib/public/modules/admin-settings-email.js +197 -0
  22. package/lib/public/modules/admin-settings-invitations.js +183 -0
  23. package/lib/public/modules/admin-settings-projects.js +198 -0
  24. package/lib/public/modules/admin-settings-ui.js +100 -0
  25. package/lib/public/modules/admin-settings-users.js +331 -0
  26. package/lib/public/modules/admin.js +19 -949
  27. package/lib/public/modules/app-dm.js +0 -2
  28. package/lib/public/modules/app-messages.js +13 -52
  29. package/lib/public/modules/app-rate-limit.js +70 -12
  30. package/lib/public/modules/context-sources.js +27 -18
  31. package/lib/public/modules/default-ai.js +14 -7
  32. package/lib/public/modules/header-tui-font.js +10 -20
  33. package/lib/public/modules/linear-settings.js +3 -2
  34. package/lib/public/modules/markdown.js +5 -6
  35. package/lib/public/modules/notification-settings.js +81 -0
  36. package/lib/public/modules/notifications.js +44 -28
  37. package/lib/public/modules/personal-settings-dialog.js +51 -0
  38. package/lib/public/modules/profile.js +15 -6
  39. package/lib/public/modules/project-settings-environment.js +106 -0
  40. package/lib/public/modules/project-settings.js +72 -628
  41. package/lib/public/modules/server-settings-controls.js +316 -0
  42. package/lib/public/modules/server-settings-environment.js +194 -0
  43. package/lib/public/modules/server-settings-navigation.js +39 -0
  44. package/lib/public/modules/server-settings-state.js +54 -0
  45. package/lib/public/modules/server-settings.js +116 -788
  46. package/lib/public/modules/server-storage-settings.js +84 -0
  47. package/lib/public/modules/session-github.js +2 -1
  48. package/lib/public/modules/session-presence-ui.js +1 -1
  49. package/lib/public/modules/settings-env-values.js +34 -0
  50. package/lib/public/modules/settings-select.js +121 -0
  51. package/lib/public/modules/shared-browser.js +10 -7
  52. package/lib/public/modules/stt-picker.js +14 -5
  53. package/lib/public/modules/terminal-prefs.js +22 -0
  54. package/lib/public/modules/theme.js +88 -52
  55. package/lib/public/modules/user-settings-account.js +318 -0
  56. package/lib/public/modules/user-settings-appearance.js +55 -0
  57. package/lib/public/modules/user-settings-editors.js +24 -0
  58. package/lib/public/modules/user-settings-integrations.js +104 -0
  59. package/lib/public/modules/user-settings-layout.js +80 -0
  60. package/lib/public/modules/user-settings-preferences.js +48 -0
  61. package/lib/public/modules/user-settings-profile.js +53 -0
  62. package/lib/public/modules/user-settings-themes.js +38 -0
  63. package/lib/public/modules/user-settings-voice.js +75 -0
  64. package/lib/public/modules/user-settings.js +53 -480
  65. package/lib/public/style.css +4 -0
  66. package/lib/sdk-message-processor.js +16 -4
  67. package/lib/sdk-quota-snapshot.js +37 -0
  68. package/lib/server-appearance-settings.js +71 -0
  69. package/lib/server-linear.js +14 -8
  70. package/lib/server-notification-settings.js +67 -0
  71. package/lib/server-settings.js +4 -0
  72. package/lib/server-storage.js +56 -0
  73. package/lib/server.js +10 -2
  74. package/lib/sessions.js +2 -3
  75. package/lib/startup-projects.js +40 -0
  76. package/lib/storage-usage.js +127 -0
  77. package/lib/users-preferences.js +0 -74
  78. package/lib/users.js +0 -4
  79. package/lib/ws-schema.js +2 -4
  80. package/lib/yoke/adapters/codex.js +8 -45
  81. package/lib/yoke/codex-model-catalog.js +40 -0
  82. package/lib/yoke/codex-rate-limits.js +26 -0
  83. package/package.json +3 -3
package/bin/cli.js CHANGED
@@ -37,6 +37,7 @@ var { generateAuthToken } = require("../lib/server");
37
37
  var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser, getSetupCode } = require("../lib/users");
38
38
  var clayStudioCert = require("../lib/clay-studio-cert");
39
39
  var cliWordmark = require("../lib/cli-wordmark");
40
+ var buildStartupProjects = require("../lib/startup-projects").buildStartupProjects;
40
41
 
41
42
  function openUrl(url) {
42
43
  try {
@@ -446,9 +447,6 @@ async function restartDaemonFromConfig() {
446
447
  var newConfig = Object.assign({}, lastConfig, {
447
448
  pid: null,
448
449
  port: targetPort,
449
- projects: (lastConfig.projects || []).filter(function (p) {
450
- return fs.existsSync(p.path);
451
- })
452
450
  });
453
451
 
454
452
  ensureConfigDir();
@@ -1445,56 +1443,15 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1445
1443
  return;
1446
1444
  }
1447
1445
 
1448
- var allProjects = [];
1449
- var usedSlugs = [];
1450
-
1451
- // Load previous config to preserve per-project settings (visibility, allowedUsers)
1452
1446
  var prevConfig = loadConfig();
1453
- var prevProjectMap = {};
1454
- if (prevConfig && prevConfig.projects) {
1455
- for (var pi = 0; pi < prevConfig.projects.length; pi++) {
1456
- prevProjectMap[prevConfig.projects[pi].path] = prevConfig.projects[pi];
1457
- }
1458
- }
1459
-
1460
- // Only include cwd if explicitly requested
1447
+ var recentProjects = extraProjects || [];
1461
1448
  if (addCwd) {
1462
- var slug = generateSlug(cwd, []);
1463
- var cwdEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1464
- // Restore title/icon from .clayrc if available
1465
- var cwdRc = loadClayrc();
1466
- var cwdRecent = cwdRc.recentProjects || [];
1467
- for (var cr = 0; cr < cwdRecent.length; cr++) {
1468
- if (cwdRecent[cr].path === cwd) {
1469
- if (cwdRecent[cr].title) cwdEntry.title = cwdRecent[cr].title;
1470
- if (cwdRecent[cr].icon) cwdEntry.icon = cwdRecent[cr].icon;
1471
- break;
1472
- }
1473
- }
1474
- // Restore project-level settings from previous config
1475
- if (prevProjectMap[cwd]) {
1476
- cwdEntry = Object.assign({}, prevProjectMap[cwd], cwdEntry);
1477
- }
1478
- allProjects.push(cwdEntry);
1479
- usedSlugs.push(slug);
1480
- }
1481
-
1482
- // Add restored projects (from ~/.clayrc)
1483
- if (extraProjects && extraProjects.length > 0) {
1484
- for (var ep = 0; ep < extraProjects.length; ep++) {
1485
- var rp = extraProjects[ep];
1486
- if (rp.path === cwd) continue; // skip if same as cwd
1487
- if (!fs.existsSync(rp.path)) continue; // skip missing directories
1488
- var rpSlug = generateSlug(rp.path, usedSlugs);
1489
- usedSlugs.push(rpSlug);
1490
- var rpEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1491
- // Restore project-level settings from previous config
1492
- if (prevProjectMap[rp.path]) {
1493
- rpEntry = Object.assign({}, prevProjectMap[rp.path], rpEntry);
1494
- }
1495
- allProjects.push(rpEntry);
1496
- }
1449
+ var cwdRecent = (loadClayrc().recentProjects || []).filter(function (p) { return p.path === cwd; });
1450
+ recentProjects = cwdRecent.concat(recentProjects);
1497
1451
  }
1452
+ var allProjects = buildStartupProjects(prevConfig, recentProjects, {
1453
+ cwd: cwd, addCwd: addCwd, existsSync: fs.existsSync, generateSlug: generateSlug,
1454
+ });
1498
1455
 
1499
1456
  var config = Object.assign({}, prevConfig || {}, {
1500
1457
  pid: null,
@@ -1629,49 +1586,10 @@ async function devMode(mode, keepAwake, existingPinHash, wantOsUsers) {
1629
1586
  return;
1630
1587
  }
1631
1588
 
1632
- var slug = generateSlug(cwd, []);
1633
- var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1634
-
1635
- // Load previous config to preserve per-project settings (visibility, allowedUsers)
1636
1589
  var prevDevConfig = loadConfig();
1637
- var prevDevProjectMap = {};
1638
- if (prevDevConfig && prevDevConfig.projects) {
1639
- for (var pdi = 0; pdi < prevDevConfig.projects.length; pdi++) {
1640
- prevDevProjectMap[prevDevConfig.projects[pdi].path] = prevDevConfig.projects[pdi];
1641
- }
1642
- }
1643
-
1644
- // Restore previous projects
1645
- var rc = loadClayrc();
1646
- var restorable = (rc.recentProjects || []).filter(function (p) {
1647
- return p.path !== cwd && fs.existsSync(p.path);
1590
+ var allProjects = buildStartupProjects(prevDevConfig, loadClayrc().recentProjects || [], {
1591
+ cwd: cwd, addCwd: true, existsSync: fs.existsSync, generateSlug: generateSlug,
1648
1592
  });
1649
- // Restore title/icon for cwd from .clayrc
1650
- var rcAll = rc.recentProjects || [];
1651
- for (var ci = 0; ci < rcAll.length; ci++) {
1652
- if (rcAll[ci].path === cwd) {
1653
- if (rcAll[ci].title) cwdDevEntry.title = rcAll[ci].title;
1654
- if (rcAll[ci].icon) cwdDevEntry.icon = rcAll[ci].icon;
1655
- break;
1656
- }
1657
- }
1658
- // Restore access settings for cwd from previous config
1659
- if (prevDevProjectMap[cwd]) {
1660
- cwdDevEntry = Object.assign({}, prevDevProjectMap[cwd], cwdDevEntry);
1661
- }
1662
- var allProjects = [cwdDevEntry];
1663
- var usedSlugs = [slug];
1664
- for (var ri = 0; ri < restorable.length; ri++) {
1665
- var rp = restorable[ri];
1666
- var rpSlug = generateSlug(rp.path, usedSlugs);
1667
- usedSlugs.push(rpSlug);
1668
- var rpDevEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1669
- // Restore project-level settings from previous config
1670
- if (prevDevProjectMap[rp.path]) {
1671
- rpDevEntry = Object.assign({}, prevDevProjectMap[rp.path], rpDevEntry);
1672
- }
1673
- allProjects.push(rpDevEntry);
1674
- }
1675
1593
 
1676
1594
  var config = Object.assign({}, prevDevConfig || {}, {
1677
1595
  pid: null,
@@ -0,0 +1,11 @@
1
+ // Persist before publishing a setting to live daemon state.
2
+ function saveDaemonSetting(config, key, value, saveConfig) {
3
+ var overrides = {}; overrides[key] = value;
4
+ var next = Object.assign({}, config, overrides);
5
+ if (value === undefined) delete next[key];
6
+ saveConfig(next);
7
+ if (value === undefined) delete config[key];
8
+ Object.assign(config, next);
9
+ return next;
10
+ }
11
+ module.exports = { saveDaemonSetting: saveDaemonSetting };
package/lib/daemon.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ var saveDaemonSetting = require("./daemon-settings").saveDaemonSetting;
2
3
 
3
4
  // --- Node version check ---
4
5
  var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
@@ -72,9 +73,7 @@ if (projectLogContext.initializeProjectKnowledge(config)) {
72
73
  }
73
74
 
74
75
  // --- OS users mode: check required system dependencies ---
75
- // Supplementary-group inheritance (issue #367): default on so sessions behave
76
- // like a normal login; operators can disable via config / the settings toggle.
77
- osUsersMod.setInheritGroups(config.inheritGroups !== false);
76
+ // User-switched processes inherit the account groups, like a normal login.
78
77
  if (config.osUsers) {
79
78
  var checkExec = require("child_process").execFileSync;
80
79
  var missing = [];
@@ -86,11 +85,7 @@ if (config.osUsers) {
86
85
  console.error("[daemon] Install with: sudo apt install " + missing.map(function (m) { return m.split(" ")[0]; }).join(" "));
87
86
  process.exit(78); // EX_CONFIG
88
87
  }
89
- if (config.inheritGroups !== false) {
90
- console.log("[daemon] Sessions inherit each user's supplementary groups (docker, etc.); disable in Settings > User Groups.");
91
- } else {
92
- console.log("[daemon] Sessions run with primary group only (supplementary groups dropped); enable in Settings > User Groups.");
93
- }
88
+ console.log("[daemon] Sessions inherit each user's supplementary groups when setpriv is available.");
94
89
  }
95
90
 
96
91
  // --- TLS ---
@@ -637,12 +632,7 @@ var relay = createServer({
637
632
  return { envrc: config.sharedEnv || "" };
638
633
  },
639
634
  onSetSharedEnv: function (envrc) {
640
- if (envrc) {
641
- config.sharedEnv = envrc;
642
- } else {
643
- delete config.sharedEnv;
644
- }
645
- saveConfig(config);
635
+ saveDaemonSetting(config, "sharedEnv", envrc || undefined, saveConfig);
646
636
  return { ok: true };
647
637
  },
648
638
  onGetServerDefaultEffort: function () {
@@ -812,7 +802,6 @@ var relay = createServer({
812
802
  keepAwake: !!config.keepAwake,
813
803
  autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
814
804
  osUsers: !!config.osUsers,
815
- inheritGroups: config.inheritGroups !== false,
816
805
  chatLayout: config.chatLayout || "channel",
817
806
  matesEnabled: config.matesEnabled === true,
818
807
  capsulesEnabled: config.capsulesEnabled === true,
@@ -827,25 +816,18 @@ var relay = createServer({
827
816
  onSetImageRetention: function (days) {
828
817
  var d = parseInt(days, 10);
829
818
  if (isNaN(d) || d < 0) d = 7;
830
- config.imageRetentionDays = d;
831
- saveConfig(config);
819
+ saveDaemonSetting(config, "imageRetentionDays", d, saveConfig);
832
820
  console.log("[daemon] Image retention:", d === 0 ? "forever" : d + " days");
833
821
  return { ok: true, days: d };
834
822
  },
835
823
  onSetUpdateChannel: function (channel) {
836
- config.updateChannel = channel === "beta" ? "beta" : "stable";
837
- saveConfig(config);
824
+ saveDaemonSetting(config, "updateChannel", channel === "beta" ? "beta" : "stable", saveConfig);
838
825
  console.log("[daemon] Update channel:", config.updateChannel, "(web)");
839
826
  return { ok: true, updateChannel: config.updateChannel };
840
827
  },
841
828
  onSetPin: function (pin) {
842
- if (pin) {
843
- config.pinHash = generateAuthToken(pin);
844
- } else {
845
- config.pinHash = null;
846
- }
829
+ saveDaemonSetting(config, "pinHash", pin ? generateAuthToken(pin) : null, saveConfig);
847
830
  relay.setAuthToken(config.pinHash);
848
- saveConfig(config);
849
831
  console.log("[daemon] PIN", pin ? "set" : "removed", "(web)");
850
832
  return { ok: true, pinEnabled: !!config.pinHash };
851
833
  },
@@ -921,8 +903,7 @@ var relay = createServer({
921
903
  },
922
904
  onSetKeepAwake: function (value) {
923
905
  var want = !!value;
924
- config.keepAwake = want;
925
- saveConfig(config);
906
+ saveDaemonSetting(config, "keepAwake", want, saveConfig);
926
907
  if (want && !caffeinateProc && process.platform === "darwin") {
927
908
  try {
928
909
  var { spawn: spawnCaff } = require("child_process");
@@ -936,16 +917,6 @@ var relay = createServer({
936
917
  console.log("[daemon] Keep awake:", want, "(web)");
937
918
  return { ok: true, keepAwake: want };
938
919
  },
939
- onSetInheritGroups: function (value) {
940
- var want = !!value;
941
- config.inheritGroups = want;
942
- saveConfig(config);
943
- // Apply live so subsequent session/terminal spawns pick it up without a
944
- // daemon restart; already-running sessions keep their current groups.
945
- osUsersMod.setInheritGroups(want);
946
- console.log("[daemon] Inherit supplementary groups:", want, "(web)");
947
- return { ok: true, inheritGroups: want };
948
- },
949
920
  onShutdown: function () {
950
921
  console.log("[daemon] Shutdown requested via web UI");
951
922
  gracefulShutdown();
@@ -19,34 +19,61 @@ function attachLinearStore(root) {
19
19
  catch (error) { if (error.code !== 'EEXIST') throw error; }
20
20
  return fs.readFileSync(file);
21
21
  }
22
- function key(owner) {
22
+ function record(owner, id) {
23
23
  var data = read(owner);
24
+ return !id || id === "default" ? data : Object.hasOwn(data.connections || {}, id) ? data.connections[id] : {};
25
+ }
26
+ function key(owner, id) {
27
+ var data = record(owner, id);
24
28
  if (!data.secret) return null;
25
29
  var parts = data.secret.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
26
30
  var cipher = crypto.createDecipheriv('aes-256-gcm', masterKey(), parts[0]);
27
- cipher.setAAD(Buffer.from(owner)); cipher.setAuthTag(parts[1]);
31
+ cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner)); cipher.setAuthTag(parts[1]);
28
32
  return Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8');
29
33
  }
30
- function view(owner) {
31
- var data = read(owner);
34
+ function view(owner, id) {
35
+ var data = record(owner, id);
32
36
  return { connected: !!data.secret, workspace: data.workspace || null, revision: data.revision || null };
33
37
  }
34
- function save(owner, secret, workspace) {
38
+ function save(owner, secret, workspace, id, name) {
39
+ if (id && (!/^[a-zA-Z0-9_-]{1,64}$/.test(id) || ['__proto__', 'constructor', 'prototype'].indexOf(id) >= 0)) throw new Error("Invalid connection.");
40
+ var previous = read(owner);
35
41
  var file = filename(owner), data = { revision: crypto.randomBytes(16).toString('hex') };
36
42
  if (secret !== null) {
37
43
  if (typeof secret !== 'string' || !secret.trim() || secret.length > 4096 || /[\r\n]/.test(secret)) throw new Error('Enter a valid Linear API key.');
38
44
  var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', masterKey(), iv);
39
- cipher.setAAD(Buffer.from(owner));
45
+ cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner));
40
46
  var bytes = Buffer.concat([cipher.update(secret.trim(), 'utf8'), cipher.final()]);
41
47
  data.secret = [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.');
42
48
  data.workspace = workspace;
43
49
  }
50
+ if (name !== undefined) {
51
+ if (typeof name !== 'string' || !name.trim() || name.trim().length > 80) throw new Error('Enter a connection name of up to 80 characters.');
52
+ data.name = name.trim();
53
+ } else data.name = record(owner, id).name;
54
+ if (id && id !== 'default') {
55
+ previous.connections = previous.connections || {};
56
+ if (secret === null) delete previous.connections[id];
57
+ else {
58
+ if (!previous.connections[id] && Object.keys(previous.connections).length >= 30) throw new Error('Connection limit reached.');
59
+ previous.connections[id] = data;
60
+ }
61
+ data = previous;
62
+ } else data.connections = previous.connections;
44
63
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
45
64
  var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
46
65
  try { fs.writeFileSync(temp, JSON.stringify(data), { mode: 0o600 }); fs.renameSync(temp, file); }
47
66
  finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
48
- return view(owner);
67
+ return view(owner, id);
68
+ }
69
+ function list(owner) {
70
+ var data = read(owner), entries = [];
71
+ if (data.secret) entries.push(Object.assign({ id: 'default', name: data.name || 'Existing connection', legacy: true }, view(owner)));
72
+ Object.keys(data.connections || {}).forEach(function (id) {
73
+ entries.push(Object.assign({ id: id, name: data.connections[id].name || 'Linear', legacy: false }, view(owner, id)));
74
+ });
75
+ return entries;
49
76
  }
50
- return { key: key, view: view, save: save };
77
+ return { key: key, view: view, save: save, list: list };
51
78
  }
52
79
  module.exports = { attachLinearStore: attachLinearStore };
package/lib/os-users.js CHANGED
@@ -17,20 +17,11 @@ function isSafeLinuxUsername(username) {
17
17
  // --- Supplementary group inheritance (issue #367) ---
18
18
  // Node's child_process { uid, gid } options call setuid/setgid but never
19
19
  // initgroups(), so spawned sessions drop all supplementary groups (docker,
20
- // video, wheel, ...). When inheritGroups is enabled we route the spawn through
20
+ // video, wheel, ...). Route user-switched spawns through
21
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
22
  var _setprivAvail = null;
24
23
  var _warnedNoSetpriv = false;
25
24
 
26
- function setInheritGroups(value) {
27
- _inheritGroups = !!value;
28
- }
29
-
30
- function getInheritGroups() {
31
- return _inheritGroups;
32
- }
33
-
34
25
  function setprivAvailable() {
35
26
  if (_setprivAvail !== null) return _setprivAvail;
36
27
  try {
@@ -50,7 +41,7 @@ function setprivAvailable() {
50
41
  * spawn()/execFileSync()/pty.spawn().
51
42
  *
52
43
  * - Same-user spawns (no uid/gid in options) are returned unchanged.
53
- * - When inheritGroups is disabled, or setpriv is unavailable, falls back to
44
+ * - When setpriv is unavailable, falls back to
54
45
  * Node's uid/gid options (primary group only) so behavior degrades safely.
55
46
  * Requires the parent process to be root, which is the case in os-users mode.
56
47
  */
@@ -62,7 +53,7 @@ function wrapSpawnAsUser(command, args, options, setprivCheck) {
62
53
  if (uid == null || gid == null) return { command: command, args: args, options: options };
63
54
 
64
55
  var hasSetpriv = typeof setprivCheck === "function" ? setprivCheck() : setprivAvailable();
65
- if (_inheritGroups && hasSetpriv) {
56
+ if (hasSetpriv) {
66
57
  var wrappedArgs = ["--reuid", String(uid), "--regid", String(gid), "--init-groups", "--", command].concat(args);
67
58
  var newOpts = Object.assign({}, options);
68
59
  delete newOpts.uid;
@@ -70,9 +61,9 @@ function wrapSpawnAsUser(command, args, options, setprivCheck) {
70
61
  return { command: "setpriv", args: wrappedArgs, options: newOpts };
71
62
  }
72
63
 
73
- if (_inheritGroups && !_warnedNoSetpriv) {
64
+ if (!_warnedNoSetpriv) {
74
65
  _warnedNoSetpriv = true;
75
- console.warn("[os-users] inheritGroups enabled but `setpriv` not found; supplementary groups will be dropped. Install util-linux to restore them.");
66
+ console.warn("[os-users] `setpriv` not found; supplementary groups will be dropped. Install util-linux to restore them.");
76
67
  }
77
68
  return { command: command, args: args, options: options };
78
69
  }
@@ -673,8 +664,6 @@ module.exports = {
673
664
  grantAllUsersAccess: grantAllUsersAccess,
674
665
  installClaudeCli: installClaudeCli,
675
666
  claudeAvailableForUser: claudeAvailableForUser,
676
- setInheritGroups: setInheritGroups,
677
- getInheritGroups: getInheritGroups,
678
667
  wrapSpawnAsUser: wrapSpawnAsUser,
679
668
  deactivateLinuxUser: deactivateLinuxUser,
680
669
  ensureProjectsDir: ensureProjectsDir,
@@ -246,13 +246,6 @@ function attachConnection(ctx) {
246
246
  sendTo(ws, { type: "email_accounts_list", accounts: emailAccountsList, providers: emailAccounts.PROVIDER_PRESETS });
247
247
  sendTo(ws, { type: "notes_list", notes: nm.list() });
248
248
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
249
- // Initial per-user preference: how to render Claude sessions.
250
- if (usersModule && typeof usersModule.getClaudeOpenMode === "function") {
251
- var _comUid = (wsUser && wsUser.id) || null;
252
- var _comVal = _comUid ? usersModule.getClaudeOpenMode(_comUid) : "gui";
253
- sendTo(ws, { type: "claude_open_mode_changed", claudeOpenMode: _comVal || "gui" });
254
- }
255
-
256
249
  _loop.sendConnectionState(ws);
257
250
  if (_mcp) _mcp.sendConnectionState(ws);
258
251
  if (_notifications) _notifications.sendConnectionState(ws, sendTo);
@@ -204,26 +204,28 @@ function attachFilesystem(ctx) {
204
204
 
205
205
  // --- Project settings permission gate ---
206
206
  if (msg.type === "get_project_env" || msg.type === "set_project_env" ||
207
- msg.type === "read_global_claude_md" || msg.type === "write_global_claude_md" ||
208
207
  msg.type === "get_shared_env" || msg.type === "set_shared_env" ||
209
208
  msg.type === "transfer_project_owner") {
210
- var globalSetting = /^(read_global_claude_md|write_global_claude_md|get_shared_env|set_shared_env)$/.test(msg.type);
209
+ var globalSetting = /^(get_shared_env|set_shared_env)$/.test(msg.type);
211
210
  var targetSlug = msg.slug || slug;
212
211
  if (!access.hasPermission(ws, "projectSettings") ||
213
212
  (globalSetting ? !access.isAdmin(ws) : !access.canAccessProject(ws, targetSlug))) {
214
- sendTo(ws, { type: "error", text: "Project settings access is not permitted" });
213
+ sendTo(ws, { type: globalSetting ? (msg.type === 'get_shared_env' ? 'shared_env_result' : 'set_shared_env_result') : (msg.type === 'get_project_env' ? 'project_env_result' : msg.type === 'set_project_env' ? 'set_project_env_result' : 'error'), slug: targetSlug, requestId: msg.requestId, ok: false, error: 'Project settings access is not permitted', text: "Project settings access is not permitted" });
215
214
  return true;
216
215
  }
217
216
  }
218
217
 
219
218
  // --- Project environment variables ---
220
219
  if (msg.type === "get_project_env") {
220
+ if (typeof opts.onGetProjectEnv !== "function") { sendTo(ws, { type: "project_env_result", slug: targetSlug, requestId: msg.requestId, error: "Project environment is unavailable in this runtime." }); return true; }
221
221
  var envrc = "";
222
222
  if (typeof opts.onGetProjectEnv === "function") {
223
- var envResult = opts.onGetProjectEnv(targetSlug);
223
+ var envResult;
224
+ try { envResult = opts.onGetProjectEnv(targetSlug); }
225
+ catch (error) { sendTo(ws, { type: "project_env_result", slug: targetSlug, requestId: msg.requestId, error: error.message }); return true; }
224
226
  envrc = envResult.envrc || "";
225
227
  }
226
- sendTo(ws, { type: "project_env_result", slug: msg.slug, envrc: envrc });
228
+ sendTo(ws, { type: "project_env_result", slug: targetSlug, requestId: msg.requestId, envrc: envrc });
227
229
  return true;
228
230
  }
229
231
 
@@ -231,41 +233,16 @@ function attachFilesystem(ctx) {
231
233
  if (typeof opts.onSetProjectEnv === "function") {
232
234
  var envError = validateEnvString(msg.envrc || "");
233
235
  if (envError) {
234
- sendTo(ws, { type: "set_project_env_result", ok: false, slug: msg.slug, error: envError });
236
+ sendTo(ws, { type: "set_project_env_result", ok: false, slug: targetSlug, requestId: msg.requestId, error: envError });
235
237
  return true;
236
238
  }
237
- var setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || "");
239
+ var setResult;
240
+ try { setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || ""); }
241
+ catch (error) { sendTo(ws, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, ok: false, error: error.message }); return true; }
238
242
  if (setResult.ok) onEnvironmentChanged();
239
- sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: msg.slug, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
243
+ sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: targetSlug, requestId: msg.requestId, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
240
244
  } else {
241
- sendTo(ws, { type: "set_project_env_result", ok: false, error: "Not supported" });
242
- }
243
- return true;
244
- }
245
-
246
- // --- Global CLAUDE.md ---
247
- if (msg.type === "read_global_claude_md") {
248
- var globalMdPath = path.join(require("./config").REAL_HOME, ".claude", "CLAUDE.md");
249
- try {
250
- var globalMdContent = fs.readFileSync(globalMdPath, "utf8");
251
- sendTo(ws, { type: "global_claude_md_result", content: globalMdContent });
252
- } catch (e) {
253
- sendTo(ws, { type: "global_claude_md_result", error: e.message });
254
- }
255
- return true;
256
- }
257
-
258
- if (msg.type === "write_global_claude_md") {
259
- var globalMdDir = path.join(require("./config").REAL_HOME, ".claude");
260
- var globalMdWritePath = path.join(globalMdDir, "CLAUDE.md");
261
- try {
262
- if (!fs.existsSync(globalMdDir)) {
263
- fs.mkdirSync(globalMdDir, { recursive: true });
264
- }
265
- fs.writeFileSync(globalMdWritePath, msg.content || "", "utf8");
266
- sendTo(ws, { type: "write_global_claude_md_result", ok: true });
267
- } catch (e) {
268
- sendTo(ws, { type: "write_global_claude_md_result", ok: false, error: e.message });
245
+ sendTo(ws, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, ok: false, error: "Not supported" });
269
246
  }
270
247
  return true;
271
248
  }
@@ -274,9 +251,12 @@ function attachFilesystem(ctx) {
274
251
  if (msg.type === "get_shared_env") {
275
252
  var sharedEnvrc = "";
276
253
  if (typeof opts.onGetSharedEnv === "function") {
277
- var sharedResult = opts.onGetSharedEnv();
278
- sharedEnvrc = sharedResult.envrc || "";
254
+ try {
255
+ var sharedResult = opts.onGetSharedEnv();
256
+ sharedEnvrc = sharedResult.envrc || "";
257
+ } catch (error) { sendTo(ws, { type: "shared_env_result", error: error.message }); return true; }
279
258
  }
259
+ if (typeof opts.onGetSharedEnv !== "function") { sendTo(ws, { type: "shared_env_result", error: "Shared environment is unavailable in this runtime." }); return true; }
280
260
  sendTo(ws, { type: "shared_env_result", envrc: sharedEnvrc });
281
261
  return true;
282
262
  }
@@ -288,7 +268,9 @@ function attachFilesystem(ctx) {
288
268
  sendTo(ws, { type: "set_shared_env_result", ok: false, error: sharedEnvError });
289
269
  return true;
290
270
  }
291
- var sharedSetResult = opts.onSetSharedEnv(msg.envrc || "");
271
+ var sharedSetResult;
272
+ try { sharedSetResult = opts.onSetSharedEnv(msg.envrc || ""); }
273
+ catch (error) { sendTo(ws, { type: "set_shared_env_result", ok: false, error: error.message }); return true; }
292
274
  if (sharedSetResult.ok) onEnvironmentChanged();
293
275
  sendTo(ws, { type: "set_shared_env_result", ok: sharedSetResult.ok, error: sharedSetResult.error, timing: sharedSetResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
294
276
  } else {
@@ -758,8 +758,8 @@ function attachMateInteraction(ctx) {
758
758
  onDone: mentionCallbacks.onDone,
759
759
  onError: mentionCallbacks.onError,
760
760
  canUseTool: function (toolName, input, toolOpts) {
761
- // Full-auto mode: auto-approve everything except AskUserQuestion
762
- if (sm.currentPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
761
+ // Read the caller's live mode so reused mentions follow session changes.
762
+ if ((session.permissionMode || sm.currentPermissionMode) === "bypassPermissions" && toolName !== "AskUserQuestion") {
763
763
  return Promise.resolve({ behavior: "allow", updatedInput: input });
764
764
  }
765
765
  // Use the shared whitelist from sdk-bridge (read-only tools + safe bash commands)
@@ -0,0 +1,31 @@
1
+ // Server administration messages. Every mutation returns an acknowledgement.
2
+ var SETTINGS = {
3
+ set_pin: { callback: 'onSetPin', field: 'pin', valid: function (value) { return value === null || typeof value === 'string' && /^\d{6}$/.test(value); }, timing: 'Applies to new sign-ins.' },
4
+ set_keep_awake: { callback: 'onSetKeepAwake', field: 'value', timing: 'Applies while the daemon is running.' },
5
+ set_inherit_groups: { callback: 'onSetInheritGroups', field: 'value', timing: 'Applies to newly spawned agent and terminal processes.' },
6
+ set_image_retention: { callback: 'onSetImageRetention', field: 'days', valid: function (value) { return Number.isInteger(value) && value >= 0 && value <= 36500; }, timing: 'The next cleanup applies this period to existing images.' }
7
+ };
8
+ function handleServerSettings(ctx, ws, msg) {
9
+ var entry = SETTINGS[msg.type];
10
+ if (!entry) return false;
11
+ function reply(data) { ctx.sendTo(ws, Object.assign({ type: msg.type + '_result' }, data)); }
12
+ if (ctx.usersModule.isMultiUser()) {
13
+ var id = ws._clayUser && ws._clayUser.id;
14
+ var user = id && ctx.usersModule.findUserById(id);
15
+ if (!user || user.role !== 'admin') { reply({ ok: false, error: 'Administrator access required.' }); return true; }
16
+ }
17
+ var value = msg[entry.field];
18
+ if (entry.valid ? !entry.valid(value) : typeof value !== 'boolean') { reply({ ok: false, error: 'Invalid setting value.' }); return true; }
19
+ var opts = ctx.opts;
20
+ try {
21
+ var config = opts.onGetDaemonConfig ? opts.onGetDaemonConfig() : {};
22
+ if (msg.type === 'set_keep_awake' && config.platform !== 'darwin') throw new Error('Keep awake requires macOS.');
23
+ if (msg.type === 'set_inherit_groups') throw new Error('Group inheritance is automatic in OS-user mode and is no longer configurable.');
24
+ if (typeof opts[entry.callback] !== 'function') throw new Error('This runtime does not support saving this setting.');
25
+ var result = opts[entry.callback](value);
26
+ reply(Object.assign({}, result, { timing: entry.timing }));
27
+ if (result.ok && msg.type === 'set_keep_awake') ctx.send({ type: 'keep_awake_changed', keepAwake: result.keepAwake });
28
+ } catch (error) { reply({ ok: false, error: error.message || 'Could not save this setting.' }); }
29
+ return true;
30
+ }
31
+ module.exports = { handleServerSettings: handleServerSettings };