clay-server 2.47.0-beta.2 → 2.47.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/yoke/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  var iface = require("./interface");
5
5
  var instructions = require("./instructions");
6
+ var vendorRegistry = require("./vendor-registry");
6
7
  var createClaudeAdapter = require("./adapters/claude").createClaudeAdapter;
7
8
  var createCodexAdapter = require("./adapters/codex").createCodexAdapter;
8
9
  var createKiroAdapter = require("./adapters/kiro").createKiroAdapter;
@@ -45,6 +46,8 @@ function wrapCreateQuery(adapter, defaultCwd) {
45
46
  * @returns {Adapter}
46
47
  */
47
48
  function createAdapter(opts) {
49
+ // Adding a vendor here also requires a registry entry and an update to the
50
+ // completeness list in test/yoke-vendor-registry.test.js.
48
51
  var vendor = (opts && opts.vendor) || "claude";
49
52
  var adapter;
50
53
  if (vendor === "claude") {
@@ -284,7 +287,12 @@ function createAdapters(opts) {
284
287
  var auth = { claude: false, codex: false, kiro: false };
285
288
  var adapters = {};
286
289
 
287
- if (installed.claude) {
290
+ function supportsConfiguredIsolation(vendor) {
291
+ var info = vendorRegistry.getVendorInfo(vendor);
292
+ return !opts.osUsers || !info || info.osUserIsolation;
293
+ }
294
+
295
+ if (installed.claude && supportsConfiguredIsolation("claude")) {
288
296
  try {
289
297
  if (!_sharedClaudeAdapter) {
290
298
  _sharedClaudeAdapter = createAdapter({ vendor: "claude", cwd: opts.cwd });
@@ -301,7 +309,7 @@ function createAdapters(opts) {
301
309
  }
302
310
  }
303
311
 
304
- if (installed.codex) {
312
+ if (installed.codex && supportsConfiguredIsolation("codex")) {
305
313
  try {
306
314
  adapters.codex = createAdapter({ vendor: "codex", cwd: opts.cwd, slug: opts.slug });
307
315
  auth.codex = true;
@@ -311,7 +319,8 @@ function createAdapters(opts) {
311
319
  }
312
320
  }
313
321
 
314
- if (installed.kiro && !opts.osUsers) {
322
+ var kiroInfo = vendorRegistry.getVendorInfo("kiro");
323
+ if (installed.kiro && supportsConfiguredIsolation("kiro")) {
315
324
  try {
316
325
  adapters.kiro = createAdapter({ vendor: "kiro", cwd: opts.cwd, slug: opts.slug });
317
326
  auth.kiro = true;
@@ -320,7 +329,7 @@ function createAdapters(opts) {
320
329
  console.error("[yoke] Failed to create adapter for kiro:", e.message);
321
330
  }
322
331
  } else if (installed.kiro && opts.osUsers) {
323
- console.log("[yoke] Kiro adapter disabled: OS-user isolation requires per-user ACP spawning");
332
+ console.log("[yoke] " + kiroInfo.displayName + " adapter disabled: OS-user isolation requires per-user spawning");
324
333
  }
325
334
 
326
335
  return { adapters: adapters, auth: auth };
@@ -334,8 +343,9 @@ function createAdapters(opts) {
334
343
  async function lazyCreateAdapter(adapters, vendor, opts) {
335
344
  opts = opts || {};
336
345
 
337
- if (vendor === "kiro" && (opts.osUsers || opts.linuxUser)) {
338
- console.log("[yoke] Refusing lazy Kiro adapter creation for OS-isolated user " + (opts.linuxUser || "unknown"));
346
+ var vendorInfo = vendorRegistry.getVendorInfo(vendor);
347
+ if (vendorInfo && !vendorInfo.osUserIsolation && (opts.osUsers || opts.linuxUser)) {
348
+ console.log("[yoke] Refusing lazy " + vendorInfo.displayName + " adapter creation for OS-isolated user " + (opts.linuxUser || "unknown"));
339
349
  return null;
340
350
  }
341
351
 
@@ -365,6 +375,8 @@ module.exports = {
365
375
  checkAuth: checkAuth,
366
376
  checkInstalled: checkInstalled,
367
377
  invalidateAuthCache: invalidateAuthCache,
378
+ VENDOR_REGISTRY: vendorRegistry.VENDOR_REGISTRY,
379
+ getVendorInfo: vendorRegistry.getVendorInfo,
368
380
  TOOL_POLICIES: iface.TOOL_POLICIES,
369
381
  validateAdapter: iface.validateAdapter,
370
382
  validateQueryHandle: iface.validateQueryHandle,
@@ -0,0 +1,55 @@
1
+ // Static, init-free facts about each vendor YOKE supports. Host code that
2
+ // needs metadata before adapter initialization should read it from here.
3
+ // Do not import adapters into this module.
4
+
5
+ var VENDOR_REGISTRY = {
6
+ claude: {
7
+ displayName: "Claude Code",
8
+ loginCommand: "claude login",
9
+ binaryName: "claude",
10
+ avatar: "/claude-code-avatar.png",
11
+ sessionModes: ["gui", "tui"],
12
+ osUserIsolation: true,
13
+ usageDashboard: {
14
+ icon: "/claude-code-avatar.png",
15
+ alt: "Claude Code",
16
+ href: "https://claude.ai/settings/usage",
17
+ title: "Check usage on claude.ai",
18
+ },
19
+ rateLimitTracking: true,
20
+ },
21
+ codex: {
22
+ displayName: "Codex",
23
+ loginCommand: "codex login --device-auth",
24
+ binaryName: "codex",
25
+ avatar: "/codex-avatar.png",
26
+ sessionModes: ["gui"],
27
+ osUserIsolation: true,
28
+ usageDashboard: {
29
+ icon: "/codex-avatar.png",
30
+ alt: "Codex",
31
+ href: "https://chatgpt.com/admin/usage",
32
+ title: "Check usage on ChatGPT",
33
+ },
34
+ rateLimitTracking: true,
35
+ },
36
+ kiro: {
37
+ displayName: "Kiro CLI",
38
+ loginCommand: "kiro-cli login",
39
+ binaryName: "kiro-cli",
40
+ avatar: "/kiro-avatar.svg",
41
+ sessionModes: ["gui"],
42
+ osUserIsolation: false,
43
+ usageDashboard: null,
44
+ rateLimitTracking: false,
45
+ },
46
+ };
47
+
48
+ function getVendorInfo(vendor) {
49
+ return VENDOR_REGISTRY[vendor] || null;
50
+ }
51
+
52
+ module.exports = {
53
+ VENDOR_REGISTRY: VENDOR_REGISTRY,
54
+ getVendorInfo: getVendorInfo,
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.47.0-beta.2",
3
+ "version": "2.47.0-beta.4",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -49,9 +49,9 @@
49
49
  "homepage": "https://github.com/chadbyte/clay#readme",
50
50
  "author": "Chad",
51
51
  "dependencies": {
52
- "@anthropic-ai/claude-agent-sdk": "^0.3.198",
52
+ "@anthropic-ai/claude-agent-sdk": "^0.3.233",
53
53
  "@lydell/node-pty": "^1.2.0-beta.3",
54
- "@openai/codex": "^0.142.4",
54
+ "@openai/codex": "^0.147.0",
55
55
  "imapflow": "^1.3.1",
56
56
  "nodemailer": "^6.10.1",
57
57
  "qrcode-terminal": "^0.12.0",
@@ -1,359 +0,0 @@
1
- var fs = require("fs");
2
- var path = require("path");
3
- var config = require("./config");
4
-
5
- var sqlite;
6
- var _availabilityError = null;
7
- try {
8
- sqlite = require("node:sqlite");
9
- } catch (e) {
10
- sqlite = null;
11
- _availabilityError = "Mate datastores require Node 22.13.0 or newer with node:sqlite available.";
12
- }
13
-
14
- function parseNodeVersion(version) {
15
- var parts = String(version || "").split(".");
16
- return {
17
- major: parseInt(parts[0] || "0", 10) || 0,
18
- minor: parseInt(parts[1] || "0", 10) || 0,
19
- patch: parseInt(parts[2] || "0", 10) || 0,
20
- };
21
- }
22
-
23
- function assertNodeVersion() {
24
- var v = parseNodeVersion(process.versions.node);
25
- if (v.major < 22 || (v.major === 22 && v.minor < 13)) {
26
- throw new Error("Mate datastores require Node 22.13.0 or newer.");
27
- }
28
- }
29
- try {
30
- assertNodeVersion();
31
- } catch (e) {
32
- if (!_availabilityError) _availabilityError = e.message;
33
- }
34
-
35
- var DatabaseSync = sqlite ? sqlite.DatabaseSync : null;
36
-
37
- var MAX_ROWS = 200;
38
- var MAX_RESULT_BYTES = 1024 * 1024;
39
- var DB_SIZE_WARNING_BYTES = 100 * 1024 * 1024;
40
- var PRAGMA_BUSY_TIMEOUT = 5000;
41
- var CLAY_META_VERSION = "1";
42
-
43
- var _dbCache = {};
44
-
45
- function isMateDatastoreAvailable() {
46
- return !!DatabaseSync && !_availabilityError;
47
- }
48
-
49
- function getMateDatastoreAvailabilityError() {
50
- return _availabilityError || null;
51
- }
52
-
53
- function assertAvailable() {
54
- if (!isMateDatastoreAvailable()) {
55
- throw new Error(getMateDatastoreAvailabilityError() || "Mate datastore is unavailable.");
56
- }
57
- }
58
-
59
- function getMateDbPath(opts) {
60
- if (opts && opts.dbPath) return String(opts.dbPath);
61
- if (opts && opts.mateDir) return path.join(String(opts.mateDir), "store.db");
62
- var userId = opts && opts.userId ? String(opts.userId) : "default";
63
- var mateId = opts && opts.mateId ? String(opts.mateId) : null;
64
- if (!mateId) throw new Error("Mate datastore requires a mateId.");
65
- return path.join(config.CONFIG_DIR, "mates", userId, mateId, "store.db");
66
- }
67
-
68
- function ensureParentDir(filePath) {
69
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
70
- }
71
-
72
- function getDbSizeBytes(dbPath) {
73
- try {
74
- return fs.statSync(dbPath).size;
75
- } catch (e) {
76
- return 0;
77
- }
78
- }
79
-
80
- function openDatabase(dbPath) {
81
- var db = new DatabaseSync(dbPath);
82
- try { db.exec("PRAGMA journal_mode = WAL"); } catch (e) {}
83
- try { db.exec("PRAGMA foreign_keys = ON"); } catch (e2) {}
84
- try { db.exec("PRAGMA busy_timeout = " + PRAGMA_BUSY_TIMEOUT); } catch (e3) {}
85
- return db;
86
- }
87
-
88
- function initClayMeta(db) {
89
- var now = new Date().toISOString();
90
- db.exec("CREATE TABLE IF NOT EXISTS clay_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
91
- var stmt = db.prepare("INSERT OR IGNORE INTO clay_meta (key, value) VALUES (?, ?)");
92
- stmt.run("clay_meta_version", CLAY_META_VERSION);
93
- stmt.run("created_at", now);
94
- stmt.run("last_opened_at", now);
95
- db.prepare("UPDATE clay_meta SET value = ? WHERE key = ?").run(now, "last_opened_at");
96
- }
97
-
98
- function ensureMateDatastore(opts) {
99
- assertAvailable();
100
- var dbPath = getMateDbPath(opts);
101
- if (_dbCache[dbPath] && _dbCache[dbPath].db) return _dbCache[dbPath];
102
- ensureParentDir(dbPath);
103
- var db = openDatabase(dbPath);
104
- initClayMeta(db);
105
- var wrapper = {
106
- db: db,
107
- dbPath: dbPath,
108
- sizeBytes: getDbSizeBytes(dbPath),
109
- warning: getDbSizeBytes(dbPath) > DB_SIZE_WARNING_BYTES ? "Mate datastore exceeds 100 MB soft warning threshold." : null,
110
- };
111
- _dbCache[dbPath] = wrapper;
112
- return wrapper;
113
- }
114
-
115
- function openMateDatastore(opts) {
116
- return ensureMateDatastore(opts);
117
- }
118
-
119
- function closeMateDatastore(handle) {
120
- var wrapper = unwrapHandle(handle);
121
- if (!wrapper || !wrapper.db) return;
122
- try {
123
- wrapper.db.close();
124
- } catch (e) {}
125
- if (wrapper.dbPath && _dbCache[wrapper.dbPath]) delete _dbCache[wrapper.dbPath];
126
- }
127
-
128
- function closeAllMateDatastores() {
129
- var keys = Object.keys(_dbCache);
130
- for (var i = 0; i < keys.length; i++) {
131
- closeMateDatastore(_dbCache[keys[i]]);
132
- }
133
- }
134
-
135
- function unwrapHandle(handle) {
136
- if (!handle) return null;
137
- if (handle.db && handle.dbPath) return handle;
138
- return { db: handle, dbPath: null, sizeBytes: 0, warning: null };
139
- }
140
-
141
- function normalizeSql(sql) {
142
- var text = String(sql || "");
143
- text = text.replace(/^\s*(?:--[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*/g, "");
144
- return text.trim();
145
- }
146
-
147
- function getFirstKeyword(sql) {
148
- var text = normalizeSql(sql);
149
- var match = text.match(/^([A-Za-z]+)/);
150
- return match ? match[1].toUpperCase() : "";
151
- }
152
-
153
- function hasMultipleStatements(sql) {
154
- var text = normalizeSql(sql);
155
- if (text.indexOf(";") === -1) return false;
156
- return !/;\s*$/.test(text) || text.slice(0, -1).indexOf(";") !== -1;
157
- }
158
-
159
- function isForbiddenSql(sql) {
160
- var text = normalizeSql(sql).toUpperCase();
161
- var banned = [
162
- "ATTACH DATABASE",
163
- "DETACH DATABASE",
164
- "LOAD_EXTENSION",
165
- "LOAD EXTENSION",
166
- ];
167
- for (var i = 0; i < banned.length; i++) {
168
- if (text.indexOf(banned[i]) !== -1) return banned[i];
169
- }
170
- if (/PRAGMA\s+(?!table_info|table_xinfo|index_list|index_info|foreign_key_list)/i.test(text)) {
171
- return "PRAGMA";
172
- }
173
- return "";
174
- }
175
-
176
- function isReadOnlyQuery(sql) {
177
- var kw = getFirstKeyword(sql);
178
- if (kw === "SELECT" || kw === "WITH") return true;
179
- return false;
180
- }
181
-
182
- function isAllowedExec(sql) {
183
- var kw = getFirstKeyword(sql);
184
- return kw === "CREATE" || kw === "ALTER" || kw === "DROP" || kw === "INSERT" || kw === "UPDATE" || kw === "DELETE";
185
- }
186
-
187
- function bindParams(stmt, params) {
188
- if (!params) return stmt;
189
- if (!Array.isArray(params)) return stmt;
190
- return stmt.run.apply(stmt, params);
191
- }
192
-
193
- function runQuery(handle, sql, params, limits) {
194
- var wrapper = unwrapHandle(handle);
195
- if (!wrapper || !wrapper.db) {
196
- return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
197
- }
198
- var text = normalizeSql(sql);
199
- if (!text) return makeError("SQLITE_QUERY_REJECTED", "Query SQL is required.");
200
- if (hasMultipleStatements(text)) return makeError("SQLITE_QUERY_REJECTED", "Multiple statements are not allowed in query mode.");
201
- var forbidden = isForbiddenSql(text);
202
- if (forbidden) return makeError("SQLITE_FORBIDDEN", forbidden + " is not allowed in Mate datastores.");
203
- if (!isReadOnlyQuery(text)) return makeError("SQLITE_QUERY_REJECTED", "Only SELECT and WITH queries are allowed.");
204
-
205
- try {
206
- var stmt = wrapper.db.prepare(text);
207
- var rows = Array.isArray(params) ? stmt.all.apply(stmt, params) : stmt.all();
208
- rows = sanitizeValue(rows);
209
- var rowCount = rows.length;
210
- var maxRows = limits && typeof limits.maxRows === "number" ? limits.maxRows : MAX_ROWS;
211
- var truncated = false;
212
- if (rows.length > maxRows) {
213
- rows = rows.slice(0, maxRows);
214
- truncated = true;
215
- }
216
- var result = {
217
- ok: true,
218
- rows: rows,
219
- rowCount: rowCount,
220
- truncated: truncated,
221
- };
222
- if (wrapper.warning) result.warning = wrapper.warning;
223
- if (limits && limits.includeSizeInfo) {
224
- result.sizeBytes = wrapper.sizeBytes;
225
- }
226
- if (Buffer.byteLength(JSON.stringify(result), "utf8") > (limits && limits.maxBytes ? limits.maxBytes : MAX_RESULT_BYTES)) {
227
- return makeError("SQLITE_RESULT_TOO_LARGE", "Query result exceeds the 1 MB response limit.");
228
- }
229
- return result;
230
- } catch (e) {
231
- return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Query failed.");
232
- }
233
- }
234
-
235
- function runExec(handle, sql, params, limits) {
236
- var wrapper = unwrapHandle(handle);
237
- if (!wrapper || !wrapper.db) {
238
- return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
239
- }
240
- var text = normalizeSql(sql);
241
- if (!text) return makeError("MATE_DATASTORE_BAD_INPUT", "SQL is required.");
242
- if (hasMultipleStatements(text)) return makeError("SQLITE_EXEC_FAILED", "Multiple statements are not allowed in exec mode.");
243
- var forbidden = isForbiddenSql(text);
244
- if (forbidden) return makeError("SQLITE_FORBIDDEN", forbidden + " is not allowed in Mate datastores.");
245
- if (!isAllowedExec(text)) return makeError("SQLITE_EXEC_FAILED", "Only CREATE, ALTER, DROP, INSERT, UPDATE, and DELETE are allowed in exec mode.");
246
-
247
- try {
248
- var stmt = wrapper.db.prepare(text);
249
- var runResult = Array.isArray(params) ? stmt.run.apply(stmt, params) : stmt.run();
250
- var result = {
251
- ok: true,
252
- changes: typeof runResult.changes === "number" ? runResult.changes : 0,
253
- lastInsertRowid: typeof runResult.lastInsertRowid !== "undefined" ? sanitizeValue(runResult.lastInsertRowid) : null,
254
- };
255
- if (wrapper.warning) result.warning = wrapper.warning;
256
- if (limits && limits.includeSizeInfo) {
257
- result.sizeBytes = getDbSizeBytes(wrapper.dbPath);
258
- }
259
- if (Buffer.byteLength(JSON.stringify(result), "utf8") > (limits && limits.maxBytes ? limits.maxBytes : MAX_RESULT_BYTES)) {
260
- return makeError("SQLITE_RESULT_TOO_LARGE", "Execution result exceeds the 1 MB response limit.");
261
- }
262
- wrapper.sizeBytes = getDbSizeBytes(wrapper.dbPath);
263
- if (wrapper.sizeBytes > DB_SIZE_WARNING_BYTES) {
264
- wrapper.warning = "Mate datastore exceeds 100 MB soft warning threshold.";
265
- result.warning = wrapper.warning;
266
- }
267
- return result;
268
- } catch (e) {
269
- return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Execution failed.");
270
- }
271
- }
272
-
273
- function listSchemaObjects(handle) {
274
- var wrapper = unwrapHandle(handle);
275
- if (!wrapper || !wrapper.db) {
276
- return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
277
- }
278
- try {
279
- var rows = wrapper.db.prepare(
280
- "SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE 'clay_%' ORDER BY type, name"
281
- ).all();
282
- return { ok: true, objects: rows };
283
- } catch (e) {
284
- return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Failed to list schema objects.");
285
- }
286
- }
287
-
288
- function describeTable(handle, tableName) {
289
- var wrapper = unwrapHandle(handle);
290
- if (!wrapper || !wrapper.db) {
291
- return makeError("MATE_DATASTORE_UNAVAILABLE", "Mate datastore is not available.");
292
- }
293
- if (!isSafeIdentifier(tableName)) {
294
- return makeError("MATE_DATASTORE_BAD_INPUT", "Table name is invalid.");
295
- }
296
- try {
297
- var info = wrapper.db.prepare("SELECT name, type, sql FROM sqlite_master WHERE name = ? AND type IN ('table', 'view')").get(tableName);
298
- if (!info) return makeError("SQLITE_TABLE_NOT_FOUND", "Table not found.");
299
- var columns = wrapper.db.prepare("PRAGMA table_info(" + quoteIdentifier(tableName) + ")").all();
300
- var indexes = wrapper.db.prepare("PRAGMA index_list(" + quoteIdentifier(tableName) + ")").all();
301
- return {
302
- ok: true,
303
- table: tableName,
304
- columns: columns,
305
- indexes: indexes,
306
- createSql: info.sql || null,
307
- };
308
- } catch (e) {
309
- return normalizeSqliteError(e, "SQLITE_EXEC_FAILED", "Failed to describe table.");
310
- }
311
- }
312
-
313
- function isSafeIdentifier(name) {
314
- return typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
315
- }
316
-
317
- function quoteIdentifier(name) {
318
- return '"' + String(name).replace(/"/g, '""') + '"';
319
- }
320
-
321
- function makeError(code, message) {
322
- return { ok: false, code: code, message: message };
323
- }
324
-
325
- function sanitizeValue(value) {
326
- if (typeof value === "bigint") return value.toString();
327
- if (Array.isArray(value)) return value.map(sanitizeValue);
328
- if (value && typeof value === "object") {
329
- var out = {};
330
- var keys = Object.keys(value);
331
- for (var i = 0; i < keys.length; i++) {
332
- out[keys[i]] = sanitizeValue(value[keys[i]]);
333
- }
334
- return out;
335
- }
336
- return value;
337
- }
338
-
339
- function normalizeSqliteError(err, fallbackCode, fallbackMessage) {
340
- var message = err && err.message ? String(err.message) : fallbackMessage;
341
- var code = fallbackCode;
342
- if (message.indexOf("no such table") !== -1) code = "SQLITE_TABLE_NOT_FOUND";
343
- if (message.indexOf("no such column") !== -1) code = "SQLITE_QUERY_REJECTED";
344
- return { ok: false, code: code, message: message };
345
- }
346
-
347
- module.exports = {
348
- isMateDatastoreAvailable: isMateDatastoreAvailable,
349
- getMateDatastoreAvailabilityError: getMateDatastoreAvailabilityError,
350
- openMateDatastore: openMateDatastore,
351
- ensureMateDatastore: ensureMateDatastore,
352
- listSchemaObjects: listSchemaObjects,
353
- describeTable: describeTable,
354
- runQuery: runQuery,
355
- runExec: runExec,
356
- closeMateDatastore: closeMateDatastore,
357
- closeAllMateDatastores: closeAllMateDatastores,
358
- getMateDbPath: getMateDbPath,
359
- };