clay-server 2.11.0-beta.2 → 2.11.0-beta.20
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 +386 -86
- package/lib/daemon.js +421 -31
- package/lib/os-users.js +358 -0
- package/lib/pages.js +39 -1
- package/lib/project.js +271 -51
- package/lib/public/app.js +441 -28
- package/lib/public/css/admin.css +99 -10
- package/lib/public/css/filebrowser.css +28 -0
- package/lib/public/css/icon-strip.css +123 -1
- package/lib/public/css/messages.css +63 -0
- package/lib/public/css/mobile-nav.css +17 -0
- package/lib/public/css/overlays.css +137 -0
- package/lib/public/css/server-settings.css +30 -2
- package/lib/public/css/sidebar.css +26 -0
- package/lib/public/index.html +126 -66
- package/lib/public/modules/admin.js +232 -17
- package/lib/public/modules/notifications.js +3 -1
- package/lib/public/modules/project-settings.js +154 -168
- package/lib/public/modules/server-settings.js +78 -189
- package/lib/public/modules/settings-defaults.js +243 -0
- package/lib/public/modules/sidebar.js +306 -24
- package/lib/public/modules/sticky-notes.js +6 -2
- package/lib/public/modules/terminal.js +175 -26
- package/lib/sdk-bridge.js +609 -11
- package/lib/sdk-worker.js +446 -0
- package/lib/server.js +385 -19
- package/lib/sessions.js +48 -7
- package/lib/terminal-manager.js +2 -2
- package/lib/terminal.js +21 -4
- package/lib/updater.js +38 -11
- package/lib/users.js +162 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -62,6 +62,7 @@ var headlessMode = false;
|
|
|
62
62
|
var watchMode = false;
|
|
63
63
|
var host = null;
|
|
64
64
|
var multiUserMode = false;
|
|
65
|
+
var osUsersMode = false;
|
|
65
66
|
|
|
66
67
|
for (var i = 0; i < args.length; i++) {
|
|
67
68
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -108,6 +109,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
108
109
|
dangerouslySkipPermissions = true;
|
|
109
110
|
} else if (args[i] === "--multi-user") {
|
|
110
111
|
multiUserMode = true;
|
|
112
|
+
} else if (args[i] === "--os-users") {
|
|
113
|
+
osUsersMode = true;
|
|
111
114
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
112
115
|
console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
|
|
113
116
|
console.log(" clay-server --add <path> Add a project to the running daemon");
|
|
@@ -128,7 +131,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
128
131
|
console.log(" --remove <path> Remove a project directory");
|
|
129
132
|
console.log(" --list List all registered projects");
|
|
130
133
|
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
131
|
-
console.log(" --multi-user
|
|
134
|
+
console.log(" --multi-user Start in multi-user mode (use with --yes for headless)");
|
|
135
|
+
console.log(" --os-users Enable OS-level user isolation (Linux, requires root + --multi-user)");
|
|
132
136
|
console.log(" --dangerously-skip-permissions");
|
|
133
137
|
console.log(" Bypass all permission prompts");
|
|
134
138
|
process.exit(0);
|
|
@@ -267,33 +271,8 @@ if (listMode) {
|
|
|
267
271
|
return;
|
|
268
272
|
}
|
|
269
273
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
var muResult = enableMultiUser();
|
|
273
|
-
if (muResult.alreadyEnabled && muResult.hasAdmin) {
|
|
274
|
-
console.log("");
|
|
275
|
-
console.log("Multi-user mode is already enabled and an admin account exists.");
|
|
276
|
-
console.log("No changes made.");
|
|
277
|
-
console.log("");
|
|
278
|
-
} else if (muResult.setupCode) {
|
|
279
|
-
console.log("");
|
|
280
|
-
console.log("\x1b[33m⚠ Experimental Feature\x1b[0m");
|
|
281
|
-
console.log("");
|
|
282
|
-
console.log(" Multi-user mode is experimental and may change in future releases.");
|
|
283
|
-
console.log(" Sharing access to AI-powered tools may be subject to your provider's");
|
|
284
|
-
console.log(" terms of service. Please review the applicable usage policies before");
|
|
285
|
-
console.log(" granting access to other users.");
|
|
286
|
-
console.log("");
|
|
287
|
-
console.log("\x1b[32mMulti-user mode enabled.\x1b[0m");
|
|
288
|
-
console.log("");
|
|
289
|
-
console.log("Setup code: \x1b[1m" + muResult.setupCode + "\x1b[0m");
|
|
290
|
-
console.log("");
|
|
291
|
-
console.log("Open Clay in your browser and enter this code to create the admin account.");
|
|
292
|
-
console.log("The code is single-use and will be cleared once the admin is set up.");
|
|
293
|
-
console.log("");
|
|
294
|
-
}
|
|
295
|
-
process.exit(0);
|
|
296
|
-
}
|
|
274
|
+
// --multi-user / --os-users are now handled in the main entry flow (setup wizard or repeat run)
|
|
275
|
+
// Flags are parsed above and applied during forkDaemon()
|
|
297
276
|
|
|
298
277
|
var cwd = process.cwd();
|
|
299
278
|
|
|
@@ -460,9 +439,11 @@ async function restartDaemonFromConfig() {
|
|
|
460
439
|
debug: lastConfig.debug || false,
|
|
461
440
|
keepAwake: lastConfig.keepAwake || false,
|
|
462
441
|
dangerouslySkipPermissions: lastConfig.dangerouslySkipPermissions || false,
|
|
442
|
+
osUsers: lastConfig.osUsers || false,
|
|
463
443
|
projects: (lastConfig.projects || []).filter(function (p) {
|
|
464
444
|
return fs.existsSync(p.path);
|
|
465
445
|
}),
|
|
446
|
+
removedProjects: lastConfig.removedProjects || [],
|
|
466
447
|
};
|
|
467
448
|
|
|
468
449
|
ensureConfigDir();
|
|
@@ -1243,13 +1224,29 @@ function setup(callback) {
|
|
|
1243
1224
|
log("");
|
|
1244
1225
|
log(sym.pointer + " " + a.bold + "Clay" + a.reset + a.dim + " · Unofficial, open-source project" + a.reset);
|
|
1245
1226
|
log(sym.bar);
|
|
1246
|
-
log(sym.bar + " " + a.
|
|
1247
|
-
log(sym.bar
|
|
1248
|
-
log(sym.bar + " " + a.dim + "
|
|
1227
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Disclaimer" + a.reset);
|
|
1228
|
+
log(sym.bar);
|
|
1229
|
+
log(sym.bar + " " + a.dim + "This is an independent project and is not affiliated with Anthropic." + a.reset);
|
|
1230
|
+
log(sym.bar + " " + a.dim + "Claude is a trademark of Anthropic." + a.reset);
|
|
1231
|
+
log(sym.bar);
|
|
1232
|
+
log(sym.bar + " " + a.dim + "Clay is provided \"as is\" without warranty of any kind. Users are" + a.reset);
|
|
1233
|
+
log(sym.bar + " " + a.dim + "responsible for complying with the terms of service of underlying AI" + a.reset);
|
|
1234
|
+
log(sym.bar + " " + a.dim + "providers (e.g., Anthropic, OpenAI) and all applicable terms of any" + a.reset);
|
|
1235
|
+
log(sym.bar + " " + a.dim + "third-party services." + a.reset);
|
|
1236
|
+
log(sym.bar);
|
|
1237
|
+
log(sym.bar + " " + a.dim + "Features such as multi-user mode are experimental and may involve" + a.reset);
|
|
1238
|
+
log(sym.bar + " " + a.dim + "sharing access to API-based services. Before enabling such features," + a.reset);
|
|
1239
|
+
log(sym.bar + " " + a.dim + "review your provider's usage policies regarding account sharing," + a.reset);
|
|
1240
|
+
log(sym.bar + " " + a.dim + "acceptable use, and any applicable rate limits or restrictions." + a.reset);
|
|
1241
|
+
log(sym.bar);
|
|
1242
|
+
log(sym.bar + " " + a.dim + "The authors assume no liability for misuse or violations arising" + a.reset);
|
|
1243
|
+
log(sym.bar + " " + a.dim + "from the use of this software." + a.reset);
|
|
1244
|
+
log(sym.bar);
|
|
1245
|
+
log(sym.bar + " Type " + a.bold + "agree" + a.reset + " to accept and continue.");
|
|
1249
1246
|
log(sym.bar);
|
|
1250
1247
|
|
|
1251
|
-
|
|
1252
|
-
if (!
|
|
1248
|
+
promptText("", "", function (val) {
|
|
1249
|
+
if (!val || val.trim().toLowerCase() !== "agree") {
|
|
1253
1250
|
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
1254
1251
|
log("");
|
|
1255
1252
|
process.exit(0);
|
|
@@ -1279,44 +1276,99 @@ function setup(callback) {
|
|
|
1279
1276
|
}
|
|
1280
1277
|
port = p;
|
|
1281
1278
|
log(sym.bar);
|
|
1279
|
+
askMode();
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1282
1283
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
askPin();
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
afterPin(pin);
|
|
1299
|
-
});
|
|
1300
|
-
} else {
|
|
1301
|
-
afterPin(pin);
|
|
1302
|
-
}
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1284
|
+
function askMode() {
|
|
1285
|
+
promptSelect("How will you use Clay?", [
|
|
1286
|
+
{ label: "Just me (single user)", value: "single" },
|
|
1287
|
+
{ label: "Multiple users", value: "multi" },
|
|
1288
|
+
], function (mode) {
|
|
1289
|
+
if (mode === "single") {
|
|
1290
|
+
finishSetup(mode, false);
|
|
1291
|
+
} else {
|
|
1292
|
+
askOsUsers(mode);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1305
1296
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1297
|
+
function askOsUsers(mode) {
|
|
1298
|
+
// Only offer OS user isolation on Linux
|
|
1299
|
+
if (process.platform !== "linux") {
|
|
1300
|
+
finishSetup(mode, false);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
log(sym.bar);
|
|
1304
|
+
promptSelect("Enable OS-level user isolation?", [
|
|
1305
|
+
{ label: "Yes", value: "yes" },
|
|
1306
|
+
{ label: "No", value: "no" },
|
|
1307
|
+
], function (choice) {
|
|
1308
|
+
if (choice !== "yes") {
|
|
1309
|
+
finishSetup(mode, false);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
log(sym.bar);
|
|
1313
|
+
log(sym.bar + " " + a.yellow + sym.warn + " OS-Level User Isolation" + a.reset);
|
|
1314
|
+
log(sym.bar);
|
|
1315
|
+
log(sym.bar + " " + a.dim + "This feature maps each Clay user to a Linux OS user account." + a.reset);
|
|
1316
|
+
log(sym.bar + " " + a.dim + "The daemon must run as root and will spawn processes (SDK workers," + a.reset);
|
|
1317
|
+
log(sym.bar + " " + a.dim + "terminals, file operations) as the mapped Linux user." + a.reset);
|
|
1318
|
+
log(sym.bar);
|
|
1319
|
+
log(sym.bar + " " + a.dim + "What this means:" + a.reset);
|
|
1320
|
+
log(sym.bar + " " + a.dim + "- Each mapped user uses their own ~/.claude/ credentials" + a.reset);
|
|
1321
|
+
log(sym.bar + " " + a.dim + "- Terminals and file access follow Linux permissions" + a.reset);
|
|
1322
|
+
log(sym.bar + " " + a.dim + "- Linux user accounts are created automatically (clay-username)" + a.reset);
|
|
1323
|
+
log(sym.bar);
|
|
1324
|
+
log(sym.bar + " " + a.dim + "Recommended: Run on a dedicated Clay server or cloud instance," + a.reset);
|
|
1325
|
+
log(sym.bar + " " + a.dim + "not on a personal computer or general-purpose server." + a.reset);
|
|
1326
|
+
log(sym.bar);
|
|
1327
|
+
promptSelect("Confirm", [
|
|
1328
|
+
{ label: "Enable OS-level user isolation", value: "confirm" },
|
|
1329
|
+
{ label: "Cancel", value: "cancel" },
|
|
1330
|
+
], function (confirmChoice) {
|
|
1331
|
+
if (confirmChoice !== "confirm") {
|
|
1332
|
+
finishSetup(mode, false);
|
|
1333
|
+
return;
|
|
1314
1334
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1335
|
+
var isRoot = typeof process.getuid === "function" && process.getuid() === 0;
|
|
1336
|
+
if (!isRoot) {
|
|
1337
|
+
// Save config so sudo clay can pick it up
|
|
1338
|
+
var partialConfig = {
|
|
1339
|
+
port: port,
|
|
1340
|
+
host: host,
|
|
1341
|
+
mode: "multi",
|
|
1342
|
+
osUsers: true,
|
|
1343
|
+
setupCompleted: true,
|
|
1344
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1345
|
+
};
|
|
1346
|
+
saveConfig(partialConfig);
|
|
1347
|
+
log(sym.bar);
|
|
1348
|
+
log(sym.warn + " " + a.yellow + "OS user isolation requires root." + a.reset);
|
|
1349
|
+
log(sym.bar + " Run:");
|
|
1350
|
+
log(sym.bar + " " + a.bold + "sudo npx clay-server" + a.reset);
|
|
1351
|
+
log(sym.end);
|
|
1352
|
+
log("");
|
|
1353
|
+
process.exit(0);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
finishSetup(mode, true);
|
|
1317
1357
|
});
|
|
1318
1358
|
});
|
|
1319
1359
|
}
|
|
1360
|
+
|
|
1361
|
+
function finishSetup(mode, wantOsUsers) {
|
|
1362
|
+
if (process.platform === "darwin") {
|
|
1363
|
+
log(sym.bar);
|
|
1364
|
+
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
1365
|
+
callback(mode, keepAwake, wantOsUsers);
|
|
1366
|
+
});
|
|
1367
|
+
} else {
|
|
1368
|
+
callback(mode, false, wantOsUsers);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1320
1372
|
askPort();
|
|
1321
1373
|
});
|
|
1322
1374
|
}
|
|
@@ -1324,7 +1376,7 @@ function setup(callback) {
|
|
|
1324
1376
|
// ==============================
|
|
1325
1377
|
// Fork the daemon process
|
|
1326
1378
|
// ==============================
|
|
1327
|
-
async function forkDaemon(
|
|
1379
|
+
async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
1328
1380
|
var ip = getLocalIP();
|
|
1329
1381
|
var hasTls = false;
|
|
1330
1382
|
|
|
@@ -1403,11 +1455,14 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1403
1455
|
pid: null,
|
|
1404
1456
|
port: port,
|
|
1405
1457
|
host: host,
|
|
1406
|
-
pinHash:
|
|
1458
|
+
pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
|
|
1407
1459
|
tls: hasTls,
|
|
1408
1460
|
debug: debugMode,
|
|
1409
1461
|
keepAwake: keepAwake,
|
|
1410
1462
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1463
|
+
osUsers: wantOsUsers || osUsersMode,
|
|
1464
|
+
mode: mode || "single",
|
|
1465
|
+
setupCompleted: true,
|
|
1411
1466
|
projects: allProjects,
|
|
1412
1467
|
};
|
|
1413
1468
|
|
|
@@ -1449,6 +1504,18 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1449
1504
|
return;
|
|
1450
1505
|
}
|
|
1451
1506
|
|
|
1507
|
+
// Enable multi-user mode if requested
|
|
1508
|
+
if (config.mode === "multi") {
|
|
1509
|
+
var muResult = enableMultiUser();
|
|
1510
|
+
if (muResult.setupCode) {
|
|
1511
|
+
log("");
|
|
1512
|
+
log(sym.done + " " + a.green + "Multi-user mode enabled." + a.reset);
|
|
1513
|
+
log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
|
|
1514
|
+
log(sym.bar + " Open Clay in your browser and enter this code to create the admin account.");
|
|
1515
|
+
log("");
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1452
1519
|
// Headless mode — print status and exit immediately
|
|
1453
1520
|
if (headlessMode) {
|
|
1454
1521
|
var protocol = config.tls ? "https" : "http";
|
|
@@ -1467,7 +1534,7 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
|
1467
1534
|
// ==============================
|
|
1468
1535
|
// Dev mode — foreground daemon with file watching
|
|
1469
1536
|
// ==============================
|
|
1470
|
-
async function devMode(
|
|
1537
|
+
async function devMode(mode, keepAwake, existingPinHash) {
|
|
1471
1538
|
var ip = getLocalIP();
|
|
1472
1539
|
var hasTls = false;
|
|
1473
1540
|
|
|
@@ -1533,11 +1600,13 @@ async function devMode(pin, keepAwake, existingPinHash) {
|
|
|
1533
1600
|
pid: null,
|
|
1534
1601
|
port: port,
|
|
1535
1602
|
host: host,
|
|
1536
|
-
pinHash: existingPinHash ||
|
|
1603
|
+
pinHash: existingPinHash || null,
|
|
1537
1604
|
tls: hasTls,
|
|
1538
1605
|
debug: true,
|
|
1539
1606
|
keepAwake: keepAwake || false,
|
|
1540
1607
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1608
|
+
mode: mode || "single",
|
|
1609
|
+
setupCompleted: true,
|
|
1541
1610
|
projects: allProjects,
|
|
1542
1611
|
};
|
|
1543
1612
|
|
|
@@ -2268,6 +2337,7 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
2268
2337
|
function showSettingsMenu(config, ip) {
|
|
2269
2338
|
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
2270
2339
|
var isAwake = status && status.keepAwake;
|
|
2340
|
+
var isOsUsers = status && status.osUsers;
|
|
2271
2341
|
|
|
2272
2342
|
console.clear();
|
|
2273
2343
|
printLogo();
|
|
@@ -2304,8 +2374,19 @@ function showSettingsMenu(config, ip) {
|
|
|
2304
2374
|
? a.green + "Enabled" + a.reset
|
|
2305
2375
|
: a.dim + "Off" + a.reset;
|
|
2306
2376
|
|
|
2377
|
+
var modeLabel = config.mode === "multi" ? "Multi-user" : "Single user";
|
|
2378
|
+
var modeStatus = config.mode === "multi"
|
|
2379
|
+
? a.clay + modeLabel + a.reset
|
|
2380
|
+
: a.dim + modeLabel + a.reset;
|
|
2381
|
+
log(sym.bar + " Mode " + modeStatus);
|
|
2307
2382
|
log(sym.bar + " PIN " + pinStatus);
|
|
2308
2383
|
log(sym.bar + " Multi-user " + muStatus);
|
|
2384
|
+
var osUsersStatus = isOsUsers
|
|
2385
|
+
? a.green + "Enabled" + a.reset
|
|
2386
|
+
: a.dim + "Off" + a.reset;
|
|
2387
|
+
if (muEnabled) {
|
|
2388
|
+
log(sym.bar + " OS users " + osUsersStatus);
|
|
2389
|
+
}
|
|
2309
2390
|
if (process.platform === "darwin") {
|
|
2310
2391
|
log(sym.bar + " Keep awake " + awakeStatus);
|
|
2311
2392
|
}
|
|
@@ -2324,6 +2405,11 @@ function showSettingsMenu(config, ip) {
|
|
|
2324
2405
|
}
|
|
2325
2406
|
if (muEnabled) {
|
|
2326
2407
|
items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
|
|
2408
|
+
if (isOsUsers) {
|
|
2409
|
+
items.push({ label: "Disable OS-level user isolation", value: "disable_os_users" });
|
|
2410
|
+
} else {
|
|
2411
|
+
items.push({ label: "Enable OS-level user isolation", value: "os_users" });
|
|
2412
|
+
}
|
|
2327
2413
|
} else {
|
|
2328
2414
|
items.push({ label: "Enable multi-user mode", value: "multi_user" });
|
|
2329
2415
|
}
|
|
@@ -2331,6 +2417,7 @@ function showSettingsMenu(config, ip) {
|
|
|
2331
2417
|
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
2332
2418
|
}
|
|
2333
2419
|
items.push({ label: "View logs", value: "logs" });
|
|
2420
|
+
items.push({ label: "Re-run setup wizard", value: "rerun_setup" });
|
|
2334
2421
|
items.push({ label: "Back", value: "back" });
|
|
2335
2422
|
|
|
2336
2423
|
promptSelect("Select", items, function (choice) {
|
|
@@ -2416,6 +2503,182 @@ function showSettingsMenu(config, ip) {
|
|
|
2416
2503
|
});
|
|
2417
2504
|
break;
|
|
2418
2505
|
|
|
2506
|
+
case "os_users":
|
|
2507
|
+
if (process.platform === "win32") {
|
|
2508
|
+
log(sym.bar);
|
|
2509
|
+
log(sym.bar + " " + a.red + "OS-level user isolation is not supported on Windows." + a.reset);
|
|
2510
|
+
log(sym.bar);
|
|
2511
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2512
|
+
showSettingsMenu(config, ip);
|
|
2513
|
+
});
|
|
2514
|
+
break;
|
|
2515
|
+
}
|
|
2516
|
+
if (process.getuid() !== 0) {
|
|
2517
|
+
log(sym.bar);
|
|
2518
|
+
log(sym.bar + " " + a.red + "Requires running as root." + a.reset);
|
|
2519
|
+
log(sym.bar);
|
|
2520
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2521
|
+
showSettingsMenu(config, ip);
|
|
2522
|
+
});
|
|
2523
|
+
break;
|
|
2524
|
+
}
|
|
2525
|
+
if (process.platform !== "linux") {
|
|
2526
|
+
log(sym.bar);
|
|
2527
|
+
log(sym.bar + " " + a.red + sym.warn + " OS-level user isolation requires Linux." + a.reset);
|
|
2528
|
+
log(sym.bar + " " + a.dim + "This feature depends on setfacl, getent, and uid/gid process spawning." + a.reset);
|
|
2529
|
+
log(sym.bar + " " + a.dim + "Use Docker or a Linux VM to run Clay with OS user isolation." + a.reset);
|
|
2530
|
+
log(sym.bar);
|
|
2531
|
+
showSettingsMenu(config, ip);
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
log(sym.bar);
|
|
2535
|
+
log(sym.bar + " " + a.yellow + sym.warn + " OS-Level User Isolation" + a.reset);
|
|
2536
|
+
log(sym.bar);
|
|
2537
|
+
log(sym.bar + " " + a.dim + "This feature maps each Clay user to a Linux OS user account." + a.reset);
|
|
2538
|
+
log(sym.bar + " " + a.dim + "The daemon must run as root and will spawn processes (SDK workers," + a.reset);
|
|
2539
|
+
log(sym.bar + " " + a.dim + "terminals, file operations) as the mapped Linux user." + a.reset);
|
|
2540
|
+
log(sym.bar);
|
|
2541
|
+
log(sym.bar + " " + a.dim + "What this means:" + a.reset);
|
|
2542
|
+
log(sym.bar + " " + a.dim + "- Each mapped user uses their own ~/.claude/ credentials" + a.reset);
|
|
2543
|
+
log(sym.bar + " " + a.dim + "- Terminals and file access follow Linux permissions" + a.reset);
|
|
2544
|
+
log(sym.bar + " " + a.dim + "- Linux user accounts are created automatically (clay-username)" + a.reset);
|
|
2545
|
+
log(sym.bar);
|
|
2546
|
+
log(sym.bar + " " + a.dim + "Recommended: Run on a dedicated Clay server or cloud instance," + a.reset);
|
|
2547
|
+
log(sym.bar + " " + a.dim + "not on a personal computer or general-purpose server." + a.reset);
|
|
2548
|
+
log(sym.bar);
|
|
2549
|
+
promptSelect("Select", [
|
|
2550
|
+
{ label: "Enable OS-level user isolation", value: "confirm" },
|
|
2551
|
+
{ label: "Cancel", value: "cancel" },
|
|
2552
|
+
], function (confirmChoice) {
|
|
2553
|
+
if (confirmChoice === "confirm") {
|
|
2554
|
+
sendIPCCommand(socketPath(), { cmd: "set_os_users", value: true }).then(function (res) {
|
|
2555
|
+
if (res.ok) {
|
|
2556
|
+
config.osUsers = true;
|
|
2557
|
+
log(sym.bar);
|
|
2558
|
+
log(sym.done + " " + a.green + "OS-level user isolation enabled." + a.reset);
|
|
2559
|
+
if (res.provisioning) {
|
|
2560
|
+
var p = res.provisioning;
|
|
2561
|
+
if (p.provisioned.length > 0) {
|
|
2562
|
+
log(sym.bar);
|
|
2563
|
+
log(sym.bar + " " + a.green + "Provisioned " + p.provisioned.length + " Linux account(s):" + a.reset);
|
|
2564
|
+
for (var pi = 0; pi < p.provisioned.length; pi++) {
|
|
2565
|
+
log(sym.bar + " " + a.dim + p.provisioned[pi].username + " -> " + p.provisioned[pi].linuxUser + a.reset);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (p.skipped.length > 0) {
|
|
2569
|
+
log(sym.bar + " " + a.dim + p.skipped.length + " user(s) already mapped." + a.reset);
|
|
2570
|
+
}
|
|
2571
|
+
if (p.errors.length > 0) {
|
|
2572
|
+
log(sym.bar);
|
|
2573
|
+
log(sym.bar + " " + a.red + p.errors.length + " user(s) failed to provision:" + a.reset);
|
|
2574
|
+
for (var ei = 0; ei < p.errors.length; ei++) {
|
|
2575
|
+
log(sym.bar + " " + a.red + p.errors[ei].username + ": " + p.errors[ei].error + a.reset);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
|
|
2580
|
+
log(sym.bar);
|
|
2581
|
+
}
|
|
2582
|
+
showSettingsMenu(config, ip);
|
|
2583
|
+
});
|
|
2584
|
+
} else {
|
|
2585
|
+
showSettingsMenu(config, ip);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
break;
|
|
2589
|
+
|
|
2590
|
+
case "disable_os_users":
|
|
2591
|
+
log(sym.bar);
|
|
2592
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Disable OS-level user isolation?" + a.reset);
|
|
2593
|
+
log(sym.bar);
|
|
2594
|
+
log(sym.bar + " " + a.dim + "Processes will no longer be spawned as mapped Linux users." + a.reset);
|
|
2595
|
+
log(sym.bar + " " + a.dim + "User mappings will be preserved and restored if re-enabled." + a.reset);
|
|
2596
|
+
log(sym.bar);
|
|
2597
|
+
promptSelect("Confirm", [
|
|
2598
|
+
{ label: "Disable OS-level user isolation", value: "confirm" },
|
|
2599
|
+
{ label: "Cancel", value: "cancel" },
|
|
2600
|
+
], function (confirmChoice) {
|
|
2601
|
+
if (confirmChoice === "confirm") {
|
|
2602
|
+
sendIPCCommand(socketPath(), { cmd: "set_os_users", value: false }).then(function (res) {
|
|
2603
|
+
if (res.ok) {
|
|
2604
|
+
config.osUsers = false;
|
|
2605
|
+
log(sym.bar);
|
|
2606
|
+
log(sym.done + " " + a.green + "OS-level user isolation disabled." + a.reset);
|
|
2607
|
+
log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
|
|
2608
|
+
log(sym.bar);
|
|
2609
|
+
}
|
|
2610
|
+
showSettingsMenu(config, ip);
|
|
2611
|
+
});
|
|
2612
|
+
} else {
|
|
2613
|
+
showSettingsMenu(config, ip);
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
break;
|
|
2617
|
+
|
|
2618
|
+
case "rerun_setup":
|
|
2619
|
+
log(sym.bar);
|
|
2620
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Re-run setup wizard?" + a.reset);
|
|
2621
|
+
log(sym.bar);
|
|
2622
|
+
log(sym.bar + " " + a.dim + "This will shut down the running daemon, reset your setup" + a.reset);
|
|
2623
|
+
log(sym.bar + " " + a.dim + "preferences (mode, port), and walk you through the wizard again." + a.reset);
|
|
2624
|
+
log(sym.bar + " " + a.dim + "Your projects and user accounts will be preserved." + a.reset);
|
|
2625
|
+
log(sym.bar);
|
|
2626
|
+
promptSelect("Confirm", [
|
|
2627
|
+
{ label: "Re-run setup wizard", value: "confirm" },
|
|
2628
|
+
{ label: "Cancel", value: "cancel" },
|
|
2629
|
+
], function (confirmChoice) {
|
|
2630
|
+
if (confirmChoice === "confirm") {
|
|
2631
|
+
// Clear setupCompleted so setup() runs fresh
|
|
2632
|
+
var cfg = loadConfig() || {};
|
|
2633
|
+
delete cfg.setupCompleted;
|
|
2634
|
+
delete cfg.mode;
|
|
2635
|
+
cfg.pid = null;
|
|
2636
|
+
saveConfig(cfg);
|
|
2637
|
+
// Shut down the daemon
|
|
2638
|
+
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
2639
|
+
clearStaleConfig();
|
|
2640
|
+
// Run the setup wizard, then fork a new daemon
|
|
2641
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2642
|
+
var rc = loadClayrc();
|
|
2643
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
2644
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
2645
|
+
});
|
|
2646
|
+
if (restorable.length > 0) {
|
|
2647
|
+
promptRestoreProjects(restorable, function (selected) {
|
|
2648
|
+
forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
|
|
2649
|
+
});
|
|
2650
|
+
} else {
|
|
2651
|
+
log(sym.bar);
|
|
2652
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2653
|
+
log("");
|
|
2654
|
+
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
}).catch(function () {
|
|
2658
|
+
clearStaleConfig();
|
|
2659
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2660
|
+
var rc = loadClayrc();
|
|
2661
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
2662
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
2663
|
+
});
|
|
2664
|
+
if (restorable.length > 0) {
|
|
2665
|
+
promptRestoreProjects(restorable, function (selected) {
|
|
2666
|
+
forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
|
|
2667
|
+
});
|
|
2668
|
+
} else {
|
|
2669
|
+
log(sym.bar);
|
|
2670
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2671
|
+
log("");
|
|
2672
|
+
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
});
|
|
2676
|
+
} else {
|
|
2677
|
+
showSettingsMenu(config, ip);
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2680
|
+
break;
|
|
2681
|
+
|
|
2419
2682
|
case "logs":
|
|
2420
2683
|
console.clear();
|
|
2421
2684
|
log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
|
|
@@ -2477,14 +2740,14 @@ var currentVersion = require("../package.json").version;
|
|
|
2477
2740
|
if (devConfig.pid) clearStaleConfig();
|
|
2478
2741
|
devConfig = null;
|
|
2479
2742
|
}
|
|
2480
|
-
// No config — go through setup (disclaimer, port,
|
|
2743
|
+
// No config — go through setup (disclaimer, port, mode, etc.)
|
|
2481
2744
|
if (!devConfig) {
|
|
2482
|
-
setup(function (
|
|
2483
|
-
devMode(
|
|
2745
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2746
|
+
devMode(mode, keepAwake, null);
|
|
2484
2747
|
});
|
|
2485
2748
|
} else {
|
|
2486
|
-
// Reuse existing
|
|
2487
|
-
await devMode(
|
|
2749
|
+
// Reuse existing config (repeat run)
|
|
2750
|
+
await devMode(devConfig.mode || "single", devConfig.keepAwake || false, devConfig.pinHash || null);
|
|
2488
2751
|
}
|
|
2489
2752
|
return;
|
|
2490
2753
|
}
|
|
@@ -2561,19 +2824,55 @@ var currentVersion = require("../package.json").version;
|
|
|
2561
2824
|
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2562
2825
|
}
|
|
2563
2826
|
} else {
|
|
2564
|
-
// No daemon running —
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2827
|
+
// No daemon running — check for saved config (repeat run)
|
|
2828
|
+
var savedConfig = loadConfig();
|
|
2829
|
+
var isRepeatRun = savedConfig && savedConfig.setupCompleted;
|
|
2830
|
+
|
|
2831
|
+
// --multi-user / --os-users CLI flags set config directly for headless/scripted usage
|
|
2832
|
+
if (multiUserMode) {
|
|
2833
|
+
if (!savedConfig) savedConfig = {};
|
|
2834
|
+
savedConfig.mode = "multi";
|
|
2835
|
+
savedConfig.setupCompleted = true;
|
|
2836
|
+
}
|
|
2837
|
+
if (osUsersMode) {
|
|
2838
|
+
if (!savedConfig) savedConfig = {};
|
|
2839
|
+
savedConfig.osUsers = true;
|
|
2840
|
+
savedConfig.mode = "multi";
|
|
2841
|
+
savedConfig.setupCompleted = true;
|
|
2842
|
+
}
|
|
2843
|
+
isRepeatRun = savedConfig && savedConfig.setupCompleted;
|
|
2844
|
+
|
|
2845
|
+
if (isRepeatRun || autoYes) {
|
|
2846
|
+
// Repeat run or --yes: skip wizard, reuse saved config
|
|
2847
|
+
var savedMode = (savedConfig && savedConfig.mode) || "single";
|
|
2848
|
+
var savedKeepAwake = (savedConfig && savedConfig.keepAwake) || false;
|
|
2849
|
+
var savedOsUsers = (savedConfig && savedConfig.osUsers) || false;
|
|
2850
|
+
|
|
2851
|
+
// os-users requires root
|
|
2852
|
+
if (savedOsUsers && typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
2853
|
+
console.error(a.red + "OS user isolation requires root." + a.reset);
|
|
2854
|
+
console.error("Run: " + a.bold + "sudo npx clay-server" + a.reset);
|
|
2855
|
+
process.exit(1);
|
|
2856
|
+
return;
|
|
2571
2857
|
}
|
|
2858
|
+
|
|
2859
|
+
if (savedConfig && savedConfig.port) port = savedConfig.port;
|
|
2860
|
+
if (savedConfig && savedConfig.host) host = savedConfig.host;
|
|
2861
|
+
if (savedConfig && savedConfig.dangerouslySkipPermissions) dangerouslySkipPermissions = true;
|
|
2862
|
+
|
|
2863
|
+
if (autoYes) {
|
|
2864
|
+
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
2865
|
+
console.log(" " + sym.done + " Mode: " + savedMode);
|
|
2866
|
+
if (dangerouslySkipPermissions) {
|
|
2867
|
+
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2572
2871
|
var autoRc = loadClayrc();
|
|
2573
2872
|
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
2574
2873
|
return p.path !== cwd && fs.existsSync(p.path);
|
|
2575
2874
|
});
|
|
2576
|
-
if (autoRestorable.length > 0) {
|
|
2875
|
+
if (autoRestorable.length > 0 && autoYes) {
|
|
2577
2876
|
console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
|
|
2578
2877
|
}
|
|
2579
2878
|
// Add cwd if it has history in .clayrc, or if there are no other projects to restore
|
|
@@ -2581,9 +2880,10 @@ var currentVersion = require("../package.json").version;
|
|
|
2581
2880
|
return p.path === cwd;
|
|
2582
2881
|
});
|
|
2583
2882
|
var addCwd = cwdInRc || autoRestorable.length === 0;
|
|
2584
|
-
await forkDaemon(
|
|
2883
|
+
await forkDaemon(savedMode, savedKeepAwake, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd, savedOsUsers);
|
|
2585
2884
|
} else {
|
|
2586
|
-
|
|
2885
|
+
// First run: interactive wizard
|
|
2886
|
+
setup(function (mode, keepAwake, wantOsUsers) {
|
|
2587
2887
|
// Check ~/.clayrc for previous projects to restore
|
|
2588
2888
|
var rc = loadClayrc();
|
|
2589
2889
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
@@ -2592,13 +2892,13 @@ var currentVersion = require("../package.json").version;
|
|
|
2592
2892
|
|
|
2593
2893
|
if (restorable.length > 0) {
|
|
2594
2894
|
promptRestoreProjects(restorable, function (selected) {
|
|
2595
|
-
forkDaemon(
|
|
2895
|
+
forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
|
|
2596
2896
|
});
|
|
2597
2897
|
} else {
|
|
2598
2898
|
log(sym.bar);
|
|
2599
2899
|
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2600
2900
|
log("");
|
|
2601
|
-
forkDaemon(
|
|
2901
|
+
forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
|
|
2602
2902
|
}
|
|
2603
2903
|
});
|
|
2604
2904
|
}
|