clay-server 2.20.0 → 2.20.1-beta.10
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/README.md +1 -1
- package/bin/cli.js +170 -55
- package/lib/config.js +23 -13
- package/lib/daemon.js +47 -64
- package/lib/ipc.js +3 -2
- package/lib/os-users.js +24 -0
- package/lib/users.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<p align="center"><img src="media/hero.png" alt="Clay workspace" /></p>
|
|
10
10
|
|
|
11
|
-
AI teammates who remember your decisions, challenge your thinking, and grow with your codebase. Runs on your machine.
|
|
11
|
+
A team workspace built on Claude Code. AI teammates who remember your decisions, challenge your thinking, and grow with your codebase. Runs on your machine.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npx clay-server
|
package/bin/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ var crypto = require("crypto");
|
|
|
36
36
|
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo, REAL_HOME } = require("../lib/config");
|
|
37
37
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
38
38
|
var { generateAuthToken } = require("../lib/server");
|
|
39
|
-
var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
|
|
39
|
+
var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser, getSetupCode } = require("../lib/users");
|
|
40
40
|
|
|
41
41
|
function openUrl(url) {
|
|
42
42
|
try {
|
|
@@ -1417,16 +1417,15 @@ function setup(callback) {
|
|
|
1417
1417
|
}
|
|
1418
1418
|
var isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1419
1419
|
if (!isRoot) {
|
|
1420
|
-
//
|
|
1421
|
-
var
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
saveConfig(partialConfig);
|
|
1420
|
+
// Merge into existing config (preserve projects, TLS, etc.)
|
|
1421
|
+
var existingCfg = loadConfig() || {};
|
|
1422
|
+
existingCfg.port = port;
|
|
1423
|
+
existingCfg.host = host;
|
|
1424
|
+
existingCfg.mode = "multi";
|
|
1425
|
+
existingCfg.osUsers = true;
|
|
1426
|
+
existingCfg.setupCompleted = true;
|
|
1427
|
+
if (dangerouslySkipPermissions) existingCfg.dangerouslySkipPermissions = true;
|
|
1428
|
+
saveConfig(existingCfg);
|
|
1430
1429
|
log(sym.bar);
|
|
1431
1430
|
log(sym.warn + " " + a.yellow + "OS user isolation requires root." + a.reset);
|
|
1432
1431
|
log(sym.bar + " Run:");
|
|
@@ -1999,6 +1998,7 @@ function showMainMenu(config, ip, setupCode) {
|
|
|
1999
1998
|
parts.push(a.reset + a.yellow + a.bold + totalAwaiting + a.reset + a.yellow + " awaiting" + a.reset + a.dim);
|
|
2000
1999
|
}
|
|
2001
2000
|
log(" " + a.dim + parts.join(a.reset + a.dim + " · ") + a.reset);
|
|
2001
|
+
log(" " + a.dim + "~/.clay → " + path.join(REAL_HOME, ".clay") + a.reset);
|
|
2002
2002
|
log(" Press " + a.bold + "o" + a.reset + " to open in browser");
|
|
2003
2003
|
log("");
|
|
2004
2004
|
|
|
@@ -2009,8 +2009,10 @@ function showMainMenu(config, ip, setupCode) {
|
|
|
2009
2009
|
log("");
|
|
2010
2010
|
}
|
|
2011
2011
|
|
|
2012
|
-
if (
|
|
2013
|
-
|
|
2012
|
+
// Always show setup code if one exists (persists until admin is created)
|
|
2013
|
+
var displayCode = setupCode || getSetupCode();
|
|
2014
|
+
if (displayCode) {
|
|
2015
|
+
log(" " + a.yellow + sym.warn + " Setup code: " + a.bold + displayCode + a.reset);
|
|
2014
2016
|
log(" " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
|
|
2015
2017
|
log("");
|
|
2016
2018
|
}
|
|
@@ -2373,6 +2375,9 @@ function showSettingsMenu(config, ip) {
|
|
|
2373
2375
|
} else {
|
|
2374
2376
|
items.push({ label: "Enable multi-user mode", value: "multi_user" });
|
|
2375
2377
|
}
|
|
2378
|
+
if (muEnabled) {
|
|
2379
|
+
items.push({ label: "Show setup code", value: "show_setup_code" });
|
|
2380
|
+
}
|
|
2376
2381
|
if (muEnabled && hasAdmin()) {
|
|
2377
2382
|
items.push({ label: "Recover admin password", value: "recover_admin" });
|
|
2378
2383
|
}
|
|
@@ -2478,7 +2483,9 @@ function showSettingsMenu(config, ip) {
|
|
|
2478
2483
|
}
|
|
2479
2484
|
if (process.getuid() !== 0) {
|
|
2480
2485
|
log(sym.bar);
|
|
2481
|
-
log(sym.bar + " " + a.red + "
|
|
2486
|
+
log(sym.bar + " " + a.red + sym.warn + " OS user isolation requires root." + a.reset);
|
|
2487
|
+
log(sym.bar + " " + a.dim + "Shut down this server, then restart with:" + a.reset);
|
|
2488
|
+
log(sym.bar + " " + a.bold + "sudo npx clay-server" + a.reset);
|
|
2482
2489
|
log(sym.bar);
|
|
2483
2490
|
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2484
2491
|
showSettingsMenu(config, ip);
|
|
@@ -2527,33 +2534,95 @@ function showSettingsMenu(config, ip) {
|
|
|
2527
2534
|
log(sym.bar);
|
|
2528
2535
|
log(sym.bar + " " + a.dim + "Then try enabling OS user isolation again." + a.reset);
|
|
2529
2536
|
log(sym.bar);
|
|
2530
|
-
|
|
2531
|
-
|
|
2537
|
+
showSettingsMenu(config, ip);
|
|
2538
|
+
return;
|
|
2539
|
+
} else if (res.error) {
|
|
2532
2540
|
log(sym.bar);
|
|
2533
|
-
log(sym.
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2541
|
+
log(sym.bar + " " + a.red + sym.warn + " Failed to enable OS users: " + res.error + a.reset);
|
|
2542
|
+
log(sym.bar);
|
|
2543
|
+
showSettingsMenu(config, ip);
|
|
2544
|
+
return;
|
|
2545
|
+
} else if (!res.ok) {
|
|
2546
|
+
log(sym.bar);
|
|
2547
|
+
log(sym.bar + " " + a.red + sym.warn + " Unexpected response from daemon." + a.reset);
|
|
2548
|
+
log(sym.bar + " " + a.dim + JSON.stringify(res) + a.reset);
|
|
2549
|
+
log(sym.bar);
|
|
2550
|
+
showSettingsMenu(config, ip);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
// Daemon saved the flag. Now provision from CLI with live progress.
|
|
2554
|
+
config.osUsers = true;
|
|
2555
|
+
log(sym.bar);
|
|
2556
|
+
log(sym.done + " " + a.green + "OS-level user isolation enabled." + a.reset);
|
|
2557
|
+
log(sym.bar);
|
|
2558
|
+
|
|
2559
|
+
// Provision Linux accounts from CLI (we have root + terminal)
|
|
2560
|
+
var osUsersLib = require("../lib/os-users");
|
|
2561
|
+
var usersLib = require("../lib/users");
|
|
2562
|
+
|
|
2563
|
+
try { osUsersLib.ensureProjectsDir(); } catch (e) {
|
|
2564
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Failed to create projects dir: " + e.message + a.reset);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
var allUsers = usersLib.getAllUsers();
|
|
2568
|
+
if (allUsers.length === 0) {
|
|
2569
|
+
log(sym.bar + " " + a.dim + "No users to provision yet. Accounts will be created when users register." + a.reset);
|
|
2570
|
+
} else {
|
|
2571
|
+
log(sym.bar + " " + a.dim + "Provisioning " + allUsers.length + " user(s)..." + a.reset);
|
|
2572
|
+
for (var ui = 0; ui < allUsers.length; ui++) {
|
|
2573
|
+
var usr = allUsers[ui];
|
|
2574
|
+
if (usr.linuxUser && osUsersLib.linuxUserExists(usr.linuxUser)) {
|
|
2575
|
+
log(sym.bar + " " + a.dim + sym.done + " " + usr.username + " -> " + usr.linuxUser + " (exists)" + a.reset);
|
|
2576
|
+
continue;
|
|
2542
2577
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2578
|
+
log(sym.bar + " " + a.dim + "Creating Linux account for " + usr.username + "..." + a.reset);
|
|
2579
|
+
var provision = osUsersLib.provisionLinuxUser(usr.username);
|
|
2580
|
+
if (provision.ok) {
|
|
2581
|
+
usersLib.updateLinuxUser(usr.id, provision.linuxUser);
|
|
2582
|
+
log(sym.bar + " " + a.green + sym.done + " " + usr.username + " -> " + provision.linuxUser + a.reset);
|
|
2583
|
+
} else {
|
|
2584
|
+
log(sym.bar + " " + a.red + sym.warn + " " + usr.username + ": " + (provision.error || "unknown error") + a.reset);
|
|
2545
2585
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// Set up ACLs for existing projects
|
|
2590
|
+
var cfg = loadConfig() || {};
|
|
2591
|
+
var cfgProjects = cfg.projects || [];
|
|
2592
|
+
if (cfgProjects.length > 0) {
|
|
2593
|
+
log(sym.bar);
|
|
2594
|
+
log(sym.bar + " " + a.dim + "Setting ACLs for " + cfgProjects.length + " project(s)..." + a.reset);
|
|
2595
|
+
for (var pi = 0; pi < cfgProjects.length; pi++) {
|
|
2596
|
+
var proj = cfgProjects[pi];
|
|
2597
|
+
if (osUsersLib.isHomeDirectory(proj.path)) {
|
|
2598
|
+
log(sym.bar + " " + a.dim + "~ " + (proj.slug || proj.path) + " (home dir, skipped)" + a.reset);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
try {
|
|
2602
|
+
if (proj.visibility === "public") {
|
|
2603
|
+
osUsersLib.grantAllUsersAccess(proj.path, usersLib);
|
|
2604
|
+
}
|
|
2605
|
+
if (proj.ownerId) {
|
|
2606
|
+
var ownerUser = usersLib.findUserById(proj.ownerId);
|
|
2607
|
+
if (ownerUser && ownerUser.linuxUser) {
|
|
2608
|
+
osUsersLib.grantProjectAccess(proj.path, ownerUser.linuxUser);
|
|
2609
|
+
}
|
|
2551
2610
|
}
|
|
2611
|
+
log(sym.bar + " " + a.dim + sym.done + " " + (proj.slug || proj.path) + a.reset);
|
|
2612
|
+
} catch (aclErr) {
|
|
2613
|
+
log(sym.bar + " " + a.yellow + sym.warn + " " + (proj.slug || proj.path) + ": " + aclErr.message + a.reset);
|
|
2552
2614
|
}
|
|
2553
2615
|
}
|
|
2554
|
-
log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
|
|
2555
|
-
log(sym.bar);
|
|
2556
2616
|
}
|
|
2617
|
+
|
|
2618
|
+
log(sym.bar);
|
|
2619
|
+
log(sym.bar + " " + a.dim + "Restart the daemon for full effect." + a.reset);
|
|
2620
|
+
log(sym.bar);
|
|
2621
|
+
showSettingsMenu(config, ip);
|
|
2622
|
+
}).catch(function (err) {
|
|
2623
|
+
log(sym.bar);
|
|
2624
|
+
log(sym.bar + " " + a.red + sym.warn + " IPC error: " + (err.message || err) + a.reset);
|
|
2625
|
+
log(sym.bar);
|
|
2557
2626
|
showSettingsMenu(config, ip);
|
|
2558
2627
|
});
|
|
2559
2628
|
} else {
|
|
@@ -2603,33 +2672,48 @@ function showSettingsMenu(config, ip) {
|
|
|
2603
2672
|
{ label: "Cancel", value: "cancel" },
|
|
2604
2673
|
], function (confirmChoice) {
|
|
2605
2674
|
if (confirmChoice === "confirm") {
|
|
2606
|
-
//
|
|
2675
|
+
// Save old PID before clearing, so we can force-kill if needed
|
|
2607
2676
|
var cfg = loadConfig() || {};
|
|
2677
|
+
var oldPid = cfg.pid;
|
|
2678
|
+
// Clear setupCompleted so setup() runs fresh
|
|
2608
2679
|
delete cfg.setupCompleted;
|
|
2609
2680
|
delete cfg.mode;
|
|
2610
2681
|
cfg.pid = null;
|
|
2611
2682
|
saveConfig(cfg);
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2683
|
+
|
|
2684
|
+
// Helper: wait for port to be free, force-kill if needed
|
|
2685
|
+
function waitForPortFree(cb) {
|
|
2686
|
+
var attempts = 0;
|
|
2687
|
+
var maxAttempts = 12; // 6 seconds total
|
|
2688
|
+
function check() {
|
|
2689
|
+
isPortFree(port).then(function (free) {
|
|
2690
|
+
if (free) return cb();
|
|
2691
|
+
attempts++;
|
|
2692
|
+
if (attempts >= maxAttempts) {
|
|
2693
|
+
// Port still busy, force-kill old daemon
|
|
2694
|
+
if (oldPid) {
|
|
2695
|
+
try { process.kill(oldPid, "SIGKILL"); } catch (e) {}
|
|
2696
|
+
}
|
|
2697
|
+
// Wait a bit more after SIGKILL
|
|
2698
|
+
setTimeout(function () {
|
|
2699
|
+
isPortFree(port).then(function (free2) {
|
|
2700
|
+
if (!free2) {
|
|
2701
|
+
log(sym.warn + " " + a.yellow + "Port " + port + " still in use. Kill the process manually:" + a.reset);
|
|
2702
|
+
log(sym.bar + " " + a.bold + "lsof -ti:" + port + " | xargs kill -9" + a.reset);
|
|
2703
|
+
}
|
|
2704
|
+
cb();
|
|
2705
|
+
});
|
|
2706
|
+
}, 1000);
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
setTimeout(check, 500);
|
|
2620
2710
|
});
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2628
|
-
log("");
|
|
2629
|
-
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2630
|
-
}
|
|
2631
|
-
});
|
|
2632
|
-
}).catch(function () {
|
|
2711
|
+
}
|
|
2712
|
+
check();
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// Helper: run setup wizard after daemon is dead
|
|
2716
|
+
function proceedWithSetup() {
|
|
2633
2717
|
clearStaleConfig();
|
|
2634
2718
|
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2635
2719
|
var rc = loadClayrc();
|
|
@@ -2647,6 +2731,17 @@ function showSettingsMenu(config, ip) {
|
|
|
2647
2731
|
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2648
2732
|
}
|
|
2649
2733
|
});
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// Shut down the daemon, then wait for port to be free
|
|
2737
|
+
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
2738
|
+
waitForPortFree(proceedWithSetup);
|
|
2739
|
+
}).catch(function () {
|
|
2740
|
+
// IPC failed, daemon may be unresponsive. Try SIGTERM, then wait.
|
|
2741
|
+
if (oldPid) {
|
|
2742
|
+
try { process.kill(oldPid, "SIGTERM"); } catch (e) {}
|
|
2743
|
+
}
|
|
2744
|
+
waitForPortFree(proceedWithSetup);
|
|
2650
2745
|
});
|
|
2651
2746
|
} else {
|
|
2652
2747
|
showSettingsMenu(config, ip);
|
|
@@ -2654,6 +2749,26 @@ function showSettingsMenu(config, ip) {
|
|
|
2654
2749
|
});
|
|
2655
2750
|
break;
|
|
2656
2751
|
|
|
2752
|
+
case "show_setup_code":
|
|
2753
|
+
// getSetupCode() auto-generates if multi-user is on and no code exists
|
|
2754
|
+
var currentCode = getSetupCode();
|
|
2755
|
+
log(sym.bar);
|
|
2756
|
+
if (currentCode) {
|
|
2757
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Setup code: " + a.bold + currentCode + a.reset);
|
|
2758
|
+
if (hasAdmin()) {
|
|
2759
|
+
log(sym.bar + " " + a.dim + "Admin account exists. This code is for adding the next admin." + a.reset);
|
|
2760
|
+
} else {
|
|
2761
|
+
log(sym.bar + " " + a.dim + "Enter this code in the browser to create the admin account." + a.reset);
|
|
2762
|
+
}
|
|
2763
|
+
} else {
|
|
2764
|
+
log(sym.bar + " " + a.dim + "Multi-user mode is not enabled." + a.reset);
|
|
2765
|
+
}
|
|
2766
|
+
log(sym.bar);
|
|
2767
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2768
|
+
showSettingsMenu(config, ip);
|
|
2769
|
+
});
|
|
2770
|
+
break;
|
|
2771
|
+
|
|
2657
2772
|
case "logs":
|
|
2658
2773
|
console.clear();
|
|
2659
2774
|
log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
|
package/lib/config.js
CHANGED
|
@@ -6,22 +6,28 @@ var net = require("net");
|
|
|
6
6
|
// When running under sudo, resolve the real user's home directory
|
|
7
7
|
// so that ~/.clay/ points to the original user's data, not /root/.clay/
|
|
8
8
|
function getRealHome() {
|
|
9
|
-
|
|
9
|
+
var sudoUser = process.env.SUDO_USER;
|
|
10
|
+
if (sudoUser && sudoUser !== "root") {
|
|
11
|
+
// 1. Try getent passwd (works on most Linux, may fail with some NSS configs)
|
|
10
12
|
try {
|
|
11
13
|
var entry = require("child_process")
|
|
12
|
-
.execSync("getent passwd " +
|
|
14
|
+
.execSync("getent passwd " + sudoUser, { encoding: "utf8", timeout: 3000 })
|
|
13
15
|
.trim();
|
|
14
16
|
var home = entry.split(":")[5];
|
|
15
|
-
if (home) return home;
|
|
16
|
-
} catch (e) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
if (home && fs.existsSync(home)) return home;
|
|
18
|
+
} catch (e) {}
|
|
19
|
+
// 2. Try shell expansion of ~USER
|
|
20
|
+
try {
|
|
21
|
+
var home = require("child_process")
|
|
22
|
+
.execSync("eval echo ~" + sudoUser, { encoding: "utf8", timeout: 3000 })
|
|
23
|
+
.trim();
|
|
24
|
+
if (home && home !== "~" + sudoUser && fs.existsSync(home)) return home;
|
|
25
|
+
} catch (e2) {}
|
|
26
|
+
// 3. Direct path fallback (GCE, cloud VMs)
|
|
27
|
+
var directHome = "/home/" + sudoUser;
|
|
28
|
+
if (fs.existsSync(directHome)) return directHome;
|
|
29
|
+
// 4. SUDO_USER's original HOME (some sudo configs preserve it)
|
|
30
|
+
if (process.env.SUDO_HOME && fs.existsSync(process.env.SUDO_HOME)) return process.env.SUDO_HOME;
|
|
25
31
|
}
|
|
26
32
|
return os.homedir();
|
|
27
33
|
}
|
|
@@ -29,7 +35,11 @@ function getRealHome() {
|
|
|
29
35
|
var REAL_HOME = getRealHome();
|
|
30
36
|
|
|
31
37
|
// v3: ~/.clay/ (v2 was ~/.claude-relay/, v1 was {cwd}/.claude-relay/)
|
|
32
|
-
|
|
38
|
+
// If CLAY_CONFIG is set (daemon mode), derive CLAY_HOME from it so that
|
|
39
|
+
// daemon.json, users.json, sessions/, etc. all live in the same directory.
|
|
40
|
+
var CLAY_HOME = process.env.CLAY_HOME
|
|
41
|
+
|| (process.env.CLAY_CONFIG ? path.dirname(process.env.CLAY_CONFIG) : null)
|
|
42
|
+
|| path.join(REAL_HOME, ".clay");
|
|
33
43
|
var LEGACY_HOME = path.join(REAL_HOME, ".claude-relay");
|
|
34
44
|
|
|
35
45
|
// Auto-migrate v2 -> v3: rename ~/.claude-relay/ to ~/.clay/ (once, before anything reads)
|
package/lib/daemon.js
CHANGED
|
@@ -33,6 +33,7 @@ var usersModule = require("./users");
|
|
|
33
33
|
var { scanWorktrees, createWorktree, removeWorktree, isWorktree } = require("./worktree");
|
|
34
34
|
var mates = require("./mates");
|
|
35
35
|
|
|
36
|
+
var daemonVersion = require("../package.json").version;
|
|
36
37
|
var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
|
|
37
38
|
var config;
|
|
38
39
|
|
|
@@ -42,6 +43,12 @@ try {
|
|
|
42
43
|
console.error("[daemon] Failed to read config:", e.message);
|
|
43
44
|
process.exit(1);
|
|
44
45
|
}
|
|
46
|
+
console.log("[daemon] v" + daemonVersion + " PID " + process.pid + " config " + configFile);
|
|
47
|
+
|
|
48
|
+
console.log("[daemon] Config: " + configFile);
|
|
49
|
+
console.log("[daemon] Users: " + usersModule.USERS_FILE);
|
|
50
|
+
if (process.env.SUDO_USER) console.log("[daemon] SUDO_USER: " + process.env.SUDO_USER);
|
|
51
|
+
console.log("[daemon] UID: " + (typeof process.getuid === "function" ? process.getuid() : "N/A"));
|
|
45
52
|
|
|
46
53
|
// --- OS users mode: check required system dependencies ---
|
|
47
54
|
if (config.osUsers) {
|
|
@@ -1020,6 +1027,7 @@ if (existingConfig && existingConfig.pid && existingConfig.pid !== process.pid)
|
|
|
1020
1027
|
}
|
|
1021
1028
|
}
|
|
1022
1029
|
var ipc = createIPCServer(socketPath(), function (msg) {
|
|
1030
|
+
console.log("[daemon] IPC:", msg.cmd);
|
|
1023
1031
|
switch (msg.cmd) {
|
|
1024
1032
|
case "add_project": {
|
|
1025
1033
|
if (!msg.path) return { ok: false, error: "missing path" };
|
|
@@ -1129,45 +1137,8 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1129
1137
|
config.osUsers = enableOsUsers;
|
|
1130
1138
|
saveConfig(config);
|
|
1131
1139
|
console.log("[daemon] OS users:", enableOsUsers);
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
try { ensureProjectsDir(); } catch (e) {
|
|
1135
|
-
console.error("[daemon] Failed to create projects dir:", e.message);
|
|
1136
|
-
}
|
|
1137
|
-
// Auto-provision Linux accounts for all existing users
|
|
1138
|
-
var provisionResult = provisionAllUsers(usersModule);
|
|
1139
|
-
console.log("[daemon] Provisioning result: " +
|
|
1140
|
-
provisionResult.provisioned.length + " provisioned, " +
|
|
1141
|
-
provisionResult.skipped.length + " skipped, " +
|
|
1142
|
-
provisionResult.errors.length + " errors");
|
|
1143
|
-
// Set up ACLs for all existing projects
|
|
1144
|
-
for (var pi = 0; pi < config.projects.length; pi++) {
|
|
1145
|
-
var proj = config.projects[pi];
|
|
1146
|
-
var projPath = proj.path;
|
|
1147
|
-
var projVisibility = proj.visibility || "public";
|
|
1148
|
-
// Grant ACL to project owner
|
|
1149
|
-
if (proj.ownerId) {
|
|
1150
|
-
var ownerUser = usersModule.findUserById(proj.ownerId);
|
|
1151
|
-
if (ownerUser && ownerUser.linuxUser) {
|
|
1152
|
-
grantProjectAccess(projPath, ownerUser.linuxUser);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
// Public projects: grant ACL to all users
|
|
1156
|
-
if (projVisibility === "public") {
|
|
1157
|
-
grantAllUsersAccess(projPath, usersModule);
|
|
1158
|
-
} else {
|
|
1159
|
-
// Private projects: grant ACL to allowedUsers
|
|
1160
|
-
var projAllowed = proj.allowedUsers || [];
|
|
1161
|
-
for (var ai = 0; ai < projAllowed.length; ai++) {
|
|
1162
|
-
var allowedUser = usersModule.findUserById(projAllowed[ai]);
|
|
1163
|
-
if (allowedUser && allowedUser.linuxUser) {
|
|
1164
|
-
grantProjectAccess(projPath, allowedUser.linuxUser);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
return { ok: true, provisioning: provisionResult };
|
|
1170
|
-
}
|
|
1140
|
+
// Provisioning is handled by CLI (which has terminal access for progress).
|
|
1141
|
+
// Daemon only saves the flag. On next restart, daemon will pick it up.
|
|
1171
1142
|
return { ok: true };
|
|
1172
1143
|
}
|
|
1173
1144
|
|
|
@@ -1292,37 +1263,49 @@ function startListening() {
|
|
|
1292
1263
|
config.pid = process.pid;
|
|
1293
1264
|
saveConfig(config);
|
|
1294
1265
|
|
|
1295
|
-
// Auto-provision Linux accounts on startup if OS users mode is enabled
|
|
1266
|
+
// Auto-provision Linux accounts on startup if OS users mode is enabled.
|
|
1267
|
+
// Run in setTimeout so the event loop stays free for IPC during startup.
|
|
1296
1268
|
if (config.osUsers) {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
console.error("[daemon] Failed to provision " + provResult.errors.length + " account(s)");
|
|
1304
|
-
}
|
|
1305
|
-
// Set up ACLs for all existing projects on startup
|
|
1306
|
-
for (var pi = 0; pi < config.projects.length; pi++) {
|
|
1307
|
-
var proj = config.projects[pi];
|
|
1308
|
-
if (proj.ownerId) {
|
|
1309
|
-
var ownerUser = usersModule.findUserById(proj.ownerId);
|
|
1310
|
-
if (ownerUser && ownerUser.linuxUser) {
|
|
1311
|
-
grantProjectAccess(proj.path, ownerUser.linuxUser);
|
|
1269
|
+
setTimeout(function () {
|
|
1270
|
+
try { ensureProjectsDir(); } catch (e) {}
|
|
1271
|
+
try {
|
|
1272
|
+
var provResult = provisionAllUsers(usersModule);
|
|
1273
|
+
if (provResult.provisioned.length > 0) {
|
|
1274
|
+
console.log("[daemon] Auto-provisioned " + provResult.provisioned.length + " Linux account(s) on startup");
|
|
1312
1275
|
}
|
|
1276
|
+
if (provResult.errors.length > 0) {
|
|
1277
|
+
console.error("[daemon] Failed to provision " + provResult.errors.length + " account(s)");
|
|
1278
|
+
}
|
|
1279
|
+
} catch (provErr) {
|
|
1280
|
+
console.error("[daemon] Startup provisioning error:", provErr.message);
|
|
1313
1281
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1282
|
+
// Set up ACLs for all existing projects on startup
|
|
1283
|
+
for (var pi = 0; pi < config.projects.length; pi++) {
|
|
1284
|
+
var proj = config.projects[pi];
|
|
1285
|
+
try {
|
|
1286
|
+
if (proj.ownerId) {
|
|
1287
|
+
var ownerUser = usersModule.findUserById(proj.ownerId);
|
|
1288
|
+
if (ownerUser && ownerUser.linuxUser) {
|
|
1289
|
+
grantProjectAccess(proj.path, ownerUser.linuxUser);
|
|
1290
|
+
}
|
|
1322
1291
|
}
|
|
1292
|
+
if ((proj.visibility || "public") === "public") {
|
|
1293
|
+
grantAllUsersAccess(proj.path, usersModule);
|
|
1294
|
+
} else {
|
|
1295
|
+
var projAllowed = proj.allowedUsers || [];
|
|
1296
|
+
for (var ai = 0; ai < projAllowed.length; ai++) {
|
|
1297
|
+
var allowedUser = usersModule.findUserById(projAllowed[ai]);
|
|
1298
|
+
if (allowedUser && allowedUser.linuxUser) {
|
|
1299
|
+
grantProjectAccess(proj.path, allowedUser.linuxUser);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
} catch (aclErr) {
|
|
1304
|
+
console.error("[daemon] Startup ACL error for " + proj.path + ":", aclErr.message);
|
|
1323
1305
|
}
|
|
1324
1306
|
}
|
|
1325
|
-
|
|
1307
|
+
console.log("[daemon] Startup OS users provisioning complete.");
|
|
1308
|
+
}, 100);
|
|
1326
1309
|
}
|
|
1327
1310
|
|
|
1328
1311
|
// Check for crash info from a previous crash and notify clients
|
package/lib/ipc.js
CHANGED
|
@@ -74,7 +74,8 @@ function createIPCServer(sockPath, handler) {
|
|
|
74
74
|
* Send a command to the daemon IPC server and wait for response.
|
|
75
75
|
* Returns a Promise resolving to the parsed response.
|
|
76
76
|
*/
|
|
77
|
-
function sendIPCCommand(sockPath, message) {
|
|
77
|
+
function sendIPCCommand(sockPath, message, timeout) {
|
|
78
|
+
var timeoutMs = timeout || 3000;
|
|
78
79
|
return new Promise(function (resolve) {
|
|
79
80
|
var client = net.connect(sockPath);
|
|
80
81
|
var buffer = "";
|
|
@@ -86,7 +87,7 @@ function sendIPCCommand(sockPath, message) {
|
|
|
86
87
|
client.destroy();
|
|
87
88
|
resolve({ ok: false, error: "timeout" });
|
|
88
89
|
}
|
|
89
|
-
},
|
|
90
|
+
}, timeoutMs);
|
|
90
91
|
|
|
91
92
|
client.on("connect", function () {
|
|
92
93
|
client.write(JSON.stringify(message) + "\n");
|
package/lib/os-users.js
CHANGED
|
@@ -157,11 +157,30 @@ function checkAclSupport() {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Check if a path is a user's home directory (e.g. /home/chad, /root).
|
|
162
|
+
* Running recursive setfacl on a home dir is dangerous and slow.
|
|
163
|
+
*/
|
|
164
|
+
function isHomeDirectory(dirPath) {
|
|
165
|
+
var resolved = path.resolve(dirPath);
|
|
166
|
+
// /root
|
|
167
|
+
if (resolved === "/root") return true;
|
|
168
|
+
// /home/username (exactly two levels)
|
|
169
|
+
var parts = resolved.split("/");
|
|
170
|
+
if (parts.length === 3 && parts[0] === "" && parts[1] === "home" && parts[2]) return true;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
160
174
|
/**
|
|
161
175
|
* Grant a Linux user ACL access (rwX) to a project directory.
|
|
162
176
|
* Uses setfacl to add recursive + default ACL entries.
|
|
177
|
+
* Skips home directories to avoid slow recursive ACL on large trees.
|
|
163
178
|
*/
|
|
164
179
|
function grantProjectAccess(projectPath, linuxUser) {
|
|
180
|
+
if (isHomeDirectory(projectPath)) {
|
|
181
|
+
console.log("[os-users] Skipping ACL for home directory: " + projectPath);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
165
184
|
try {
|
|
166
185
|
// Recursive ACL for existing files
|
|
167
186
|
execSync("setfacl -R -m u:" + linuxUser + ":rwX " + JSON.stringify(projectPath), {
|
|
@@ -190,6 +209,10 @@ function grantProjectAccess(projectPath, linuxUser) {
|
|
|
190
209
|
* Revoke a Linux user's ACL access from a project directory.
|
|
191
210
|
*/
|
|
192
211
|
function revokeProjectAccess(projectPath, linuxUser) {
|
|
212
|
+
if (isHomeDirectory(projectPath)) {
|
|
213
|
+
console.log("[os-users] Skipping ACL revoke for home directory: " + projectPath);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
193
216
|
try {
|
|
194
217
|
execSync("setfacl -R -x u:" + linuxUser + " " + JSON.stringify(projectPath), {
|
|
195
218
|
encoding: "utf8",
|
|
@@ -424,4 +447,5 @@ module.exports = {
|
|
|
424
447
|
installClaudeCli: installClaudeCli,
|
|
425
448
|
deactivateLinuxUser: deactivateLinuxUser,
|
|
426
449
|
ensureProjectsDir: ensureProjectsDir,
|
|
450
|
+
isHomeDirectory: isHomeDirectory,
|
|
427
451
|
};
|
package/lib/users.js
CHANGED
|
@@ -115,7 +115,15 @@ function generateSetupCode() {
|
|
|
115
115
|
|
|
116
116
|
function getSetupCode() {
|
|
117
117
|
var data = loadUsers();
|
|
118
|
-
|
|
118
|
+
if (data.setupCode) return data.setupCode;
|
|
119
|
+
// Defensive: if multi-user is on, no admin, and no code, auto-generate one
|
|
120
|
+
if (data.multiUser && !findAdmin(data)) {
|
|
121
|
+
var code = generateSetupCode();
|
|
122
|
+
data.setupCode = code;
|
|
123
|
+
saveUsers(data);
|
|
124
|
+
return code;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
function clearSetupCode() {
|