clay-server 2.21.0-beta.2 → 2.21.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon.js +14 -27
- package/lib/sdk-bridge.js +55 -66
- package/lib/sdk-worker.js +0 -25
- package/lib/sessions.js +16 -4
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -1264,7 +1264,8 @@ function startListening() {
|
|
|
1264
1264
|
saveConfig(config);
|
|
1265
1265
|
|
|
1266
1266
|
// Auto-provision Linux accounts on startup if OS users mode is enabled.
|
|
1267
|
-
//
|
|
1267
|
+
// ACLs are NOT re-applied on every startup (too slow with recursive setfacl).
|
|
1268
|
+
// ACLs are set when: projects are added, users are added, or visibility changes.
|
|
1268
1269
|
if (config.osUsers) {
|
|
1269
1270
|
setTimeout(function () {
|
|
1270
1271
|
try { ensureProjectsDir(); } catch (e) {}
|
|
@@ -1272,6 +1273,17 @@ function startListening() {
|
|
|
1272
1273
|
var provResult = provisionAllUsers(usersModule);
|
|
1273
1274
|
if (provResult.provisioned.length > 0) {
|
|
1274
1275
|
console.log("[daemon] Auto-provisioned " + provResult.provisioned.length + " Linux account(s) on startup");
|
|
1276
|
+
// Only set ACLs for newly provisioned users (not all users on all projects)
|
|
1277
|
+
for (var pi = 0; pi < config.projects.length; pi++) {
|
|
1278
|
+
var proj = config.projects[pi];
|
|
1279
|
+
if ((proj.visibility || "public") === "public") {
|
|
1280
|
+
for (var ni = 0; ni < provResult.provisioned.length; ni++) {
|
|
1281
|
+
try {
|
|
1282
|
+
grantProjectAccess(proj.path, provResult.provisioned[ni].linuxUser);
|
|
1283
|
+
} catch (e) {}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1275
1287
|
}
|
|
1276
1288
|
if (provResult.errors.length > 0) {
|
|
1277
1289
|
console.error("[daemon] Failed to provision " + provResult.errors.length + " account(s)");
|
|
@@ -1279,32 +1291,7 @@ function startListening() {
|
|
|
1279
1291
|
} catch (provErr) {
|
|
1280
1292
|
console.error("[daemon] Startup provisioning error:", provErr.message);
|
|
1281
1293
|
}
|
|
1282
|
-
|
|
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
|
-
}
|
|
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);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
console.log("[daemon] Startup OS users provisioning complete.");
|
|
1294
|
+
console.log("[daemon] Startup OS users check complete.");
|
|
1308
1295
|
}, 100);
|
|
1309
1296
|
}
|
|
1310
1297
|
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -814,9 +814,36 @@ function createSDKBridge(opts) {
|
|
|
814
814
|
* Mirrors the in-process startQuery flow but delegates SDK execution to the worker.
|
|
815
815
|
*/
|
|
816
816
|
async function startQueryViaWorker(session, text, images, linuxUser) {
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
817
|
+
// Pre-copy CLI session file BEFORE spawning worker.
|
|
818
|
+
// Must happen before spawn so execSync doesn't block the event loop
|
|
819
|
+
// while worker is alive (which causes ready/exit race conditions).
|
|
820
|
+
if (session.cliSessionId && linuxUser) {
|
|
821
|
+
try {
|
|
822
|
+
var configMod = require("./config");
|
|
823
|
+
var osUsersMod = require("./os-users");
|
|
824
|
+
var originalHome = configMod.REAL_HOME || require("os").homedir();
|
|
825
|
+
var linuxUserHome = osUsersMod.getLinuxUserHome(linuxUser);
|
|
826
|
+
if (originalHome !== linuxUserHome) {
|
|
827
|
+
var projectSlug = (cwd || "").replace(/\//g, "-");
|
|
828
|
+
var sessionFileName = session.cliSessionId + ".jsonl";
|
|
829
|
+
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
|
|
830
|
+
var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
|
|
831
|
+
var dstFile = path.join(dstDir, sessionFileName);
|
|
832
|
+
if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
|
|
833
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
834
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
835
|
+
var uid = osUsersMod.getLinuxUserUid(linuxUser);
|
|
836
|
+
if (uid != null) {
|
|
837
|
+
try { require("child_process").execSync("chown " + uid + " " + JSON.stringify(dstFile)); } catch (e2) {}
|
|
838
|
+
}
|
|
839
|
+
console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} catch (copyErr) {
|
|
843
|
+
console.log("[sdk-bridge] Session pre-copy skipped:", copyErr.message);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
820
847
|
var worker;
|
|
821
848
|
try {
|
|
822
849
|
worker = spawnWorker(linuxUser);
|
|
@@ -952,7 +979,7 @@ function createSDKBridge(opts) {
|
|
|
952
979
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
953
980
|
sm.broadcastSessionList();
|
|
954
981
|
}
|
|
955
|
-
cleanupSessionWorker(session);
|
|
982
|
+
cleanupSessionWorker(session, worker);
|
|
956
983
|
if (session.onQueryComplete) {
|
|
957
984
|
try { session.onQueryComplete(session); } catch (err) {
|
|
958
985
|
console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
|
|
@@ -965,63 +992,10 @@ function createSDKBridge(opts) {
|
|
|
965
992
|
var qerrLower = (msg.error || "").toLowerCase();
|
|
966
993
|
var isSessionNotFound = qerrLower.indexOf("no conversation found") !== -1
|
|
967
994
|
|| qerrLower.indexOf("session not found") !== -1;
|
|
968
|
-
if (isSessionNotFound
|
|
969
|
-
//
|
|
970
|
-
|
|
971
|
-
session.
|
|
972
|
-
// Try to copy the CLI session file from original user to OS user
|
|
973
|
-
var migratedOk = false;
|
|
974
|
-
if (session.cliSessionId && session.lastLinuxUser) {
|
|
975
|
-
try {
|
|
976
|
-
var configMod = require("./config");
|
|
977
|
-
var osUsersMod = require("./os-users");
|
|
978
|
-
var originalHome = configMod.REAL_HOME || require("os").homedir();
|
|
979
|
-
var linuxUserHome = osUsersMod.getLinuxUserHome ? osUsersMod.getLinuxUserHome(session.lastLinuxUser) : "/home/" + session.lastLinuxUser;
|
|
980
|
-
var projectDir = session.cwd || "";
|
|
981
|
-
var projectSlug = projectDir.replace(/\//g, "-");
|
|
982
|
-
var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, session.cliSessionId + ".jsonl");
|
|
983
|
-
var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
|
|
984
|
-
var dstFile = path.join(dstDir, session.cliSessionId + ".jsonl");
|
|
985
|
-
if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
|
|
986
|
-
fs.mkdirSync(dstDir, { recursive: true });
|
|
987
|
-
fs.copyFileSync(srcFile, dstFile);
|
|
988
|
-
// Fix ownership so the OS user can read/write it
|
|
989
|
-
var uid = osUsersMod.getLinuxUserUid ? osUsersMod.getLinuxUserUid(session.lastLinuxUser) : null;
|
|
990
|
-
if (uid != null) {
|
|
991
|
-
try { require("child_process").execSync("chown -R " + uid + " " + JSON.stringify(dstDir)); } catch (e2) {}
|
|
992
|
-
}
|
|
993
|
-
console.log("[sdk-bridge] Copied CLI session " + session.cliSessionId + " to " + dstFile);
|
|
994
|
-
migratedOk = true;
|
|
995
|
-
} else if (fs.existsSync(dstFile)) {
|
|
996
|
-
migratedOk = true; // already there
|
|
997
|
-
}
|
|
998
|
-
} catch (copyErr) {
|
|
999
|
-
console.error("[sdk-bridge] Failed to copy CLI session:", copyErr.message);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
// Detach old worker so its exit event doesn't interfere with the new one
|
|
1003
|
-
if (session.worker) {
|
|
1004
|
-
session.worker.messageHandlers = [];
|
|
1005
|
-
session.worker.kill();
|
|
1006
|
-
session.worker = null;
|
|
1007
|
-
}
|
|
1008
|
-
session.queryInstance = null;
|
|
1009
|
-
session.messageQueue = null;
|
|
1010
|
-
session.abortController = null;
|
|
1011
|
-
// Ensure client knows we're still processing (re-send status)
|
|
1012
|
-
session.isProcessing = true;
|
|
1013
|
-
onProcessingChanged();
|
|
1014
|
-
send({ type: "status", status: "processing" });
|
|
1015
|
-
// If migration failed, clear cliSessionId so it starts fresh
|
|
1016
|
-
if (!migratedOk) {
|
|
1017
|
-
session.cliSessionId = null;
|
|
1018
|
-
}
|
|
1019
|
-
// Re-run the query (with resume if migration succeeded)
|
|
1020
|
-
var retryText = session._lastQueryText || "";
|
|
1021
|
-
var retryImages = session._lastQueryImages || undefined;
|
|
1022
|
-
var retryLinuxUser = session.lastLinuxUser || null;
|
|
1023
|
-
startQuery(session, retryText, retryImages, retryLinuxUser);
|
|
1024
|
-
break;
|
|
995
|
+
if (isSessionNotFound) {
|
|
996
|
+
// Pre-copy should have handled this. Clear stale cliSessionId
|
|
997
|
+
// so next message starts a fresh conversation in the same UI session.
|
|
998
|
+
session.cliSessionId = null;
|
|
1025
999
|
}
|
|
1026
1000
|
if (session.isProcessing) {
|
|
1027
1001
|
session.isProcessing = false;
|
|
@@ -1086,7 +1060,7 @@ function createSDKBridge(opts) {
|
|
|
1086
1060
|
}
|
|
1087
1061
|
sm.broadcastSessionList();
|
|
1088
1062
|
}
|
|
1089
|
-
cleanupSessionWorker(session);
|
|
1063
|
+
cleanupSessionWorker(session, worker);
|
|
1090
1064
|
// Auto-continue on rate limit (scheduler sessions, or user setting)
|
|
1091
1065
|
var workerDidScheduleAC = false;
|
|
1092
1066
|
var workerACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
@@ -1154,7 +1128,13 @@ function createSDKBridge(opts) {
|
|
|
1154
1128
|
});
|
|
1155
1129
|
}
|
|
1156
1130
|
|
|
1157
|
-
function cleanupSessionWorker(session) {
|
|
1131
|
+
function cleanupSessionWorker(session, fromWorker) {
|
|
1132
|
+
// If called from a specific worker's exit/error handler, only cleanup if
|
|
1133
|
+
// that worker is still the session's current worker. Prevents stale
|
|
1134
|
+
// worker exit events from killing a newer worker.
|
|
1135
|
+
if (fromWorker && session.worker && session.worker !== fromWorker) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1158
1138
|
session.queryInstance = null;
|
|
1159
1139
|
session.messageQueue = null;
|
|
1160
1140
|
session.abortController = null;
|
|
@@ -1254,9 +1234,9 @@ function createSDKBridge(opts) {
|
|
|
1254
1234
|
// Auto-approve safe Bash commands (read-only, non-destructive)
|
|
1255
1235
|
if (toolName === "Bash" && input && input.command) {
|
|
1256
1236
|
var cmd = input.command.trim();
|
|
1257
|
-
var firstWord = cmd.split(/[\s;|&]/)[0];
|
|
1258
1237
|
var safeBashCommands = {
|
|
1259
|
-
|
|
1238
|
+
// Navigation (harmless on its own, checked in compound commands below)
|
|
1239
|
+
cd: true, pushd: true, popd: true,
|
|
1260
1240
|
// File/dir inspection
|
|
1261
1241
|
ls: true, cat: true, head: true, tail: true, wc: true, file: true,
|
|
1262
1242
|
stat: true, find: true, tree: true, du: true, df: true,
|
|
@@ -1293,7 +1273,16 @@ function createSDKBridge(opts) {
|
|
|
1293
1273
|
nslookup: true, host: true, ping: true, traceroute: true,
|
|
1294
1274
|
curl: true, wget: true, http: true,
|
|
1295
1275
|
};
|
|
1296
|
-
|
|
1276
|
+
// Split compound commands (&&, ||, ;, |) and check ALL segments
|
|
1277
|
+
var segments = cmd.split(/\s*(?:&&|\|\||[;|])\s*/);
|
|
1278
|
+
var allSafe = true;
|
|
1279
|
+
for (var si = 0; si < segments.length; si++) {
|
|
1280
|
+
var seg = segments[si].trim();
|
|
1281
|
+
if (!seg) continue;
|
|
1282
|
+
var firstWord = seg.split(/\s/)[0];
|
|
1283
|
+
if (!safeBashCommands[firstWord]) { allSafe = false; break; }
|
|
1284
|
+
}
|
|
1285
|
+
if (allSafe) {
|
|
1297
1286
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1298
1287
|
}
|
|
1299
1288
|
}
|
package/lib/sdk-worker.js
CHANGED
|
@@ -220,31 +220,6 @@ async function handleQueryStart(msg) {
|
|
|
220
220
|
messageQueue.push(msg.prompt);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
// If resuming a session that doesn't exist for this OS user, try to migrate
|
|
224
|
-
// the session file from the original user's home directory.
|
|
225
|
-
if (msg.options && msg.options.resume && msg.originalHome && msg.projectPath) {
|
|
226
|
-
try {
|
|
227
|
-
var fs = require("fs");
|
|
228
|
-
var homePath = require("os").homedir();
|
|
229
|
-
if (homePath !== msg.originalHome) {
|
|
230
|
-
var projDirName = msg.projectPath.replace(/\//g, "-");
|
|
231
|
-
var sessionFile = msg.options.resume + ".jsonl";
|
|
232
|
-
var destDir = path.join(homePath, ".claude", "projects", projDirName);
|
|
233
|
-
var destFile = path.join(destDir, sessionFile);
|
|
234
|
-
if (!fs.existsSync(destFile)) {
|
|
235
|
-
var srcFile = path.join(msg.originalHome, ".claude", "projects", projDirName, sessionFile);
|
|
236
|
-
if (fs.existsSync(srcFile)) {
|
|
237
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
238
|
-
fs.copyFileSync(srcFile, destFile);
|
|
239
|
-
try { fs.writeSync(2, "[sdk-worker] Migrated session " + msg.options.resume + " from " + msg.originalHome + "\n"); } catch (e2) {}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
} catch (migrateErr) {
|
|
244
|
-
try { fs.writeSync(2, "[sdk-worker] Session migration failed: " + migrateErr.message + "\n"); } catch (e2) {}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
223
|
// Build query options (callbacks are local, everything else from daemon)
|
|
249
224
|
var options = msg.options || {};
|
|
250
225
|
options.abortController = abortController;
|
package/lib/sessions.js
CHANGED
|
@@ -627,10 +627,12 @@ function createSessionManager(opts) {
|
|
|
627
627
|
return { hits: hits, total: history.length };
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
var _migrationFailedIds = {};
|
|
630
631
|
function migrateSessionTitles(getSDK, migrateCwd) {
|
|
631
632
|
var candidates = [];
|
|
632
633
|
sessions.forEach(function(s) {
|
|
633
|
-
if (s.cliSessionId && s.title && s.title !== "New Session" && s.title !== "Resumed session"
|
|
634
|
+
if (s.cliSessionId && s.title && s.title !== "New Session" && s.title !== "Resumed session"
|
|
635
|
+
&& !_migrationFailedIds[s.cliSessionId]) {
|
|
634
636
|
candidates.push({ cliSessionId: s.cliSessionId, title: s.title });
|
|
635
637
|
}
|
|
636
638
|
});
|
|
@@ -647,18 +649,28 @@ function createSessionManager(opts) {
|
|
|
647
649
|
return sdkTitles[item.cliSessionId] !== item.title;
|
|
648
650
|
});
|
|
649
651
|
if (toMigrate.length === 0) return;
|
|
652
|
+
var migrated = 0;
|
|
653
|
+
var failed = 0;
|
|
650
654
|
var chain = Promise.resolve();
|
|
651
655
|
for (var j = 0; j < toMigrate.length; j++) {
|
|
652
656
|
(function(item) {
|
|
653
657
|
chain = chain.then(function() {
|
|
654
|
-
return sdkMod.renameSession(item.cliSessionId, item.title, { dir: migrateCwd }).
|
|
655
|
-
|
|
658
|
+
return sdkMod.renameSession(item.cliSessionId, item.title, { dir: migrateCwd }).then(function() {
|
|
659
|
+
migrated++;
|
|
660
|
+
}).catch(function(e) {
|
|
661
|
+
failed++;
|
|
662
|
+
_migrationFailedIds[item.cliSessionId] = true;
|
|
656
663
|
});
|
|
657
664
|
});
|
|
658
665
|
})(toMigrate[j]);
|
|
659
666
|
}
|
|
660
667
|
chain.then(function() {
|
|
661
|
-
|
|
668
|
+
if (migrated > 0) {
|
|
669
|
+
console.log("[session] Migrated " + migrated + " session title(s) to SDK format");
|
|
670
|
+
}
|
|
671
|
+
if (failed > 0) {
|
|
672
|
+
console.log("[session] Skipped " + failed + " session(s) (CLI session not found for current user)");
|
|
673
|
+
}
|
|
662
674
|
}).catch(function(e) {
|
|
663
675
|
console.error("[session] Migration chain failed:", e.message || e);
|
|
664
676
|
});
|