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.
- package/bin/cli.js +9 -91
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +2 -2
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +2 -0
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +170 -446
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +10 -7
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +56 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +2 -3
- package/lib/startup-projects.js +40 -0
- package/lib/storage-usage.js +127 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/adapters/codex.js +8 -45
- package/lib/yoke/codex-model-catalog.js +40 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- 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
|
|
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
|
|
1463
|
-
|
|
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
|
|
1638
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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();
|
package/lib/linear-store.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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, ...).
|
|
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
|
|
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 (
|
|
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 (
|
|
64
|
+
if (!_warnedNoSetpriv) {
|
|
74
65
|
_warnedNoSetpriv = true;
|
|
75
|
-
console.warn("[os-users]
|
|
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 = /^(
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
278
|
-
|
|
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
|
|
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
|
-
//
|
|
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 };
|