clay-server 2.46.0-beta.1 → 2.47.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +10 -18
- package/lib/daemon.js +11 -0
- package/lib/migrate-single-user.js +286 -0
- package/lib/public/modules/app-panels.js +1 -0
- package/lib/server-auth.js +14 -5
- package/lib/users.js +14 -0
- package/lib/yoke/adapters/codex.js +7 -4
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1359,24 +1359,16 @@ function setup(callback) {
|
|
|
1359
1359
|
}
|
|
1360
1360
|
port = p;
|
|
1361
1361
|
log(sym.bar);
|
|
1362
|
-
|
|
1362
|
+
// No "single vs multi" question anymore: every deploy runs in the
|
|
1363
|
+
// (general) multi-user model. Solo users get an auto-provisioned
|
|
1364
|
+
// default admin with no PIN and no login wall. OS-level user
|
|
1365
|
+
// isolation stays an opt-in, offered here on Linux and toggleable
|
|
1366
|
+
// later; everything else is just multi-user.
|
|
1367
|
+
askOsUsers("multi");
|
|
1363
1368
|
});
|
|
1364
1369
|
});
|
|
1365
1370
|
}
|
|
1366
1371
|
|
|
1367
|
-
function askMode() {
|
|
1368
|
-
promptSelect("How will you use Clay?", [
|
|
1369
|
-
{ label: "Just me (single user)", value: "single" },
|
|
1370
|
-
{ label: "Multiple users", value: "multi" },
|
|
1371
|
-
], function (mode) {
|
|
1372
|
-
if (mode === "single") {
|
|
1373
|
-
finishSetup(mode, false);
|
|
1374
|
-
} else {
|
|
1375
|
-
askOsUsers(mode);
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
1372
|
function askOsUsers(mode) {
|
|
1381
1373
|
// Only offer OS user isolation on Linux
|
|
1382
1374
|
if (process.platform !== "linux") {
|
|
@@ -1554,7 +1546,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1554
1546
|
keepAwake: keepAwake,
|
|
1555
1547
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1556
1548
|
osUsers: wantOsUsers || osUsersMode,
|
|
1557
|
-
mode: mode || "
|
|
1549
|
+
mode: mode || "multi",
|
|
1558
1550
|
setupCompleted: true,
|
|
1559
1551
|
projects: allProjects,
|
|
1560
1552
|
});
|
|
@@ -1729,7 +1721,7 @@ async function devMode(mode, keepAwake, existingPinHash, wantOsUsers) {
|
|
|
1729
1721
|
debug: true,
|
|
1730
1722
|
keepAwake: keepAwake || false,
|
|
1731
1723
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1732
|
-
mode: mode || "
|
|
1724
|
+
mode: mode || "multi",
|
|
1733
1725
|
setupCompleted: true,
|
|
1734
1726
|
projects: allProjects,
|
|
1735
1727
|
osUsers: wantOsUsers || (prevDevConfig ? (prevDevConfig.osUsers || false) : false),
|
|
@@ -2667,7 +2659,7 @@ var currentVersion = require("../package.json").version;
|
|
|
2667
2659
|
});
|
|
2668
2660
|
} else {
|
|
2669
2661
|
// Reuse existing config (repeat run)
|
|
2670
|
-
await devMode(devConfig.mode || "
|
|
2662
|
+
await devMode(devConfig.mode || "multi", devConfig.keepAwake || false, devConfig.pinHash || null, devConfig.osUsers || false);
|
|
2671
2663
|
}
|
|
2672
2664
|
return;
|
|
2673
2665
|
}
|
|
@@ -2767,7 +2759,7 @@ var currentVersion = require("../package.json").version;
|
|
|
2767
2759
|
|
|
2768
2760
|
if (isRepeatRun || autoYes) {
|
|
2769
2761
|
// Repeat run or --yes: skip wizard, reuse saved config
|
|
2770
|
-
var savedMode = (savedConfig && savedConfig.mode) || "
|
|
2762
|
+
var savedMode = (savedConfig && savedConfig.mode) || "multi";
|
|
2771
2763
|
var savedKeepAwake = (savedConfig && savedConfig.keepAwake) || false;
|
|
2772
2764
|
var savedOsUsers = (savedConfig && savedConfig.osUsers) || false;
|
|
2773
2765
|
|
package/lib/daemon.js
CHANGED
|
@@ -52,6 +52,17 @@ console.log("[daemon] Users: " + usersModule.USERS_FILE);
|
|
|
52
52
|
if (process.env.SUDO_USER) console.log("[daemon] SUDO_USER: " + process.env.SUDO_USER);
|
|
53
53
|
console.log("[daemon] UID: " + (typeof process.getuid === "function" ? process.getuid() : "N/A"));
|
|
54
54
|
|
|
55
|
+
// --- Ensure the multi-user model (there is no single-user runtime anymore):
|
|
56
|
+
// migrate a legacy single-user deploy, or provision a default admin on a fresh
|
|
57
|
+
// one. One-time, idempotent, best-effort. Runs before projects/mates/sessions
|
|
58
|
+
// load so the flip and on-disk backfill are in effect for the rest of boot. ---
|
|
59
|
+
try {
|
|
60
|
+
require("./migrate-single-user").ensureMultiUser();
|
|
61
|
+
config = loadConfig(); // refresh in-memory config to pick up the migration marker
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("[daemon] ensureMultiUser threw (continuing):", e && e.message);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
// --- OS users mode: check required system dependencies ---
|
|
56
67
|
// Supplementary-group inheritance (issue #367): default on so sessions behave
|
|
57
68
|
// like a normal login; operators can disable via config / the settings toggle.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// One-time migration: fold a legacy single-user deploy into the multi-user
|
|
2
|
+
// model by auto-provisioning one "admin" user that inherits the single-user
|
|
3
|
+
// PIN, profile, and settings. After this, the app is always internally
|
|
4
|
+
// multi-user; a solo deploy is just a one-user multi-user deploy (with the
|
|
5
|
+
// solo auto-login safety net so there's no login wall when no PIN was set).
|
|
6
|
+
//
|
|
7
|
+
// Design goals:
|
|
8
|
+
// - Seamless: existing users upgrade and keep working with no manual steps.
|
|
9
|
+
// Had a PIN -> same PIN still logs in. Had none -> no login wall.
|
|
10
|
+
// - Safe: backs up state first, tolerates partial failure (best-effort per
|
|
11
|
+
// step), and is a no-op on fresh installs and already-multi-user deploys.
|
|
12
|
+
// - Idempotent: a marker in daemon config prevents re-running.
|
|
13
|
+
//
|
|
14
|
+
// NOTE: this touches on-disk state (users.json, daemon.json, profile.json,
|
|
15
|
+
// mates/). It runs once at daemon boot, before the server starts listening.
|
|
16
|
+
|
|
17
|
+
var fs = require("fs");
|
|
18
|
+
var path = require("path");
|
|
19
|
+
var config = require("./config");
|
|
20
|
+
var users = require("./users");
|
|
21
|
+
|
|
22
|
+
var SETTING_KEYS = [
|
|
23
|
+
"chatLayout",
|
|
24
|
+
"autoContinueOnRateLimit",
|
|
25
|
+
"matesEnabled",
|
|
26
|
+
"terminalFont",
|
|
27
|
+
"deletedBuiltinKeys",
|
|
28
|
+
"mateOnboardingShown",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function backupFile(filePath, stamp) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(filePath)) return;
|
|
34
|
+
var bak = filePath + ".pre-migrate-" + stamp + ".bak";
|
|
35
|
+
if (fs.existsSync(bak)) return; // don't clobber an existing backup
|
|
36
|
+
fs.copyFileSync(filePath, bak);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error("[migrate] backup failed for " + filePath + ": " + (e.message || e));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Move legacy flat mates (CONFIG_DIR/mates/*) under the per-user directory
|
|
43
|
+
// (CONFIG_DIR/mates/<userId>/). Best-effort; never fatal. Mates data is
|
|
44
|
+
// preserved on disk regardless.
|
|
45
|
+
function migrateMatesToUser(userId) {
|
|
46
|
+
try {
|
|
47
|
+
var matesRoot = path.join(config.CONFIG_DIR, "mates");
|
|
48
|
+
if (!fs.existsSync(matesRoot)) return;
|
|
49
|
+
var userDir = path.join(matesRoot, userId);
|
|
50
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
51
|
+
var entries = fs.readdirSync(matesRoot);
|
|
52
|
+
for (var i = 0; i < entries.length; i++) {
|
|
53
|
+
var name = entries[i];
|
|
54
|
+
// Skip per-user dirs (userId-shaped) and our target dir.
|
|
55
|
+
if (name === userId) continue;
|
|
56
|
+
// Only move legacy flat artifacts: mates.json and mate_* directories.
|
|
57
|
+
var isLegacy = name === "mates.json" || name.indexOf("mate_") === 0;
|
|
58
|
+
if (!isLegacy) continue;
|
|
59
|
+
var src = path.join(matesRoot, name);
|
|
60
|
+
var dst = path.join(userDir, name);
|
|
61
|
+
if (fs.existsSync(dst)) continue; // don't clobber freshly-seeded builtins
|
|
62
|
+
try { fs.renameSync(src, dst); }
|
|
63
|
+
catch (e) { console.error("[migrate] mates move skipped for " + name + ": " + (e.message || e)); }
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error("[migrate] mates migration failed (non-fatal): " + (e.message || e));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Backfill ownerId onto legacy sessions (created before ownership existed) so
|
|
71
|
+
// they belong to the migrated user. Walks CONFIG_DIR/sessions/**/*.jsonl and
|
|
72
|
+
// patches the first "meta" line in place. Best-effort per file.
|
|
73
|
+
function backfillSessionOwners(userId) {
|
|
74
|
+
var count = 0;
|
|
75
|
+
try {
|
|
76
|
+
var root = path.join(config.CONFIG_DIR, "sessions");
|
|
77
|
+
if (!fs.existsSync(root)) return 0;
|
|
78
|
+
var stack = [root];
|
|
79
|
+
while (stack.length > 0) {
|
|
80
|
+
var dir = stack.pop();
|
|
81
|
+
var entries;
|
|
82
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { continue; }
|
|
83
|
+
for (var i = 0; i < entries.length; i++) {
|
|
84
|
+
var ent = entries[i];
|
|
85
|
+
var full = path.join(dir, ent.name);
|
|
86
|
+
if (ent.isDirectory()) { stack.push(full); continue; }
|
|
87
|
+
if (!ent.name.endsWith(".jsonl")) continue;
|
|
88
|
+
try {
|
|
89
|
+
var content = fs.readFileSync(full, "utf8");
|
|
90
|
+
var nl = content.indexOf("\n");
|
|
91
|
+
var firstLine = nl === -1 ? content : content.slice(0, nl);
|
|
92
|
+
var rest = nl === -1 ? "" : content.slice(nl); // includes leading \n
|
|
93
|
+
var meta;
|
|
94
|
+
try { meta = JSON.parse(firstLine); } catch (e) { continue; }
|
|
95
|
+
if (!meta || meta.type !== "meta") continue;
|
|
96
|
+
if (meta.ownerId) continue; // already owned
|
|
97
|
+
meta.ownerId = userId;
|
|
98
|
+
var tmp = full + ".tmp." + Date.now();
|
|
99
|
+
fs.writeFileSync(tmp, JSON.stringify(meta) + rest);
|
|
100
|
+
fs.renameSync(tmp, full);
|
|
101
|
+
count++;
|
|
102
|
+
} catch (e) { /* skip this file */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.error("[migrate] session ownerId backfill failed (non-fatal): " + (e.message || e));
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readProfile() {
|
|
112
|
+
try {
|
|
113
|
+
var p = path.join(config.CONFIG_DIR, "profile.json");
|
|
114
|
+
if (!fs.existsSync(p)) return null;
|
|
115
|
+
var raw = fs.readFileSync(p, "utf8");
|
|
116
|
+
var obj = JSON.parse(raw);
|
|
117
|
+
return (obj && typeof obj === "object") ? obj : null;
|
|
118
|
+
} catch (e) { return null; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Returns { migrated: true, userId } on success, or { skipped: <reason> }.
|
|
122
|
+
function migrateSingleUserToMulti() {
|
|
123
|
+
try {
|
|
124
|
+
// Guard 1: already multi-user -> nothing to do.
|
|
125
|
+
if (users.isMultiUser()) return { skipped: "already-multi-user" };
|
|
126
|
+
|
|
127
|
+
// Guard 2: users already exist (partial prior run) -> don't double-create.
|
|
128
|
+
var existing = users.getAllUsers();
|
|
129
|
+
if (existing && existing.length > 0) return { skipped: "users-exist" };
|
|
130
|
+
|
|
131
|
+
var cfg = config.loadConfig() || {};
|
|
132
|
+
|
|
133
|
+
// Guard 3: idempotency marker.
|
|
134
|
+
if (cfg.singleUserMigratedAt) return { skipped: "already-migrated" };
|
|
135
|
+
|
|
136
|
+
// Guard 4: only migrate deploys that show prior single-user use. A pristine
|
|
137
|
+
// fresh install (no PIN, no settings, no profile) is left for the later
|
|
138
|
+
// default-flip phase, so this migration is strictly an upgrade path.
|
|
139
|
+
var profile = readProfile();
|
|
140
|
+
var hasSettings = SETTING_KEYS.some(function (k) { return cfg[k] !== undefined; });
|
|
141
|
+
var hasPin = !!cfg.pinHash;
|
|
142
|
+
if (!profile && !hasSettings && !hasPin) return { skipped: "fresh-install" };
|
|
143
|
+
|
|
144
|
+
var stamp = String(Date.now());
|
|
145
|
+
console.log("[migrate] Legacy single-user deploy detected; migrating to multi-user (stamp=" + stamp + ")");
|
|
146
|
+
|
|
147
|
+
// --- Backup before touching anything ---
|
|
148
|
+
backupFile(config.configPath(), stamp);
|
|
149
|
+
// users.json / auth-tokens live in CONFIG_DIR; back them up if present.
|
|
150
|
+
backupFile(path.join(config.CONFIG_DIR, "users.json"), stamp);
|
|
151
|
+
backupFile(path.join(config.CONFIG_DIR, "profile.json"), stamp);
|
|
152
|
+
|
|
153
|
+
// --- Flip to multi-user, then provision the sole admin ---
|
|
154
|
+
// Flip first so createUser's built-in mate seeding writes to the per-user
|
|
155
|
+
// mates path (CONFIG_DIR/mates/<userId>/), not the flat legacy path.
|
|
156
|
+
users.enableMultiUser();
|
|
157
|
+
|
|
158
|
+
var displayName = (profile && profile.name) ? String(profile.name) : "Admin";
|
|
159
|
+
var created = users.createUserWithoutPin({
|
|
160
|
+
username: "admin",
|
|
161
|
+
displayName: displayName,
|
|
162
|
+
role: "admin",
|
|
163
|
+
profile: profile || undefined,
|
|
164
|
+
});
|
|
165
|
+
if (!created || !created.user) {
|
|
166
|
+
console.error("[migrate] Failed to create admin user: " + (created && created.error));
|
|
167
|
+
// Roll the flag back so we don't leave a userless multi-user deploy.
|
|
168
|
+
try { users.disableMultiUser(); } catch (e) {}
|
|
169
|
+
return { skipped: "create-failed" };
|
|
170
|
+
}
|
|
171
|
+
var userId = created.user.id;
|
|
172
|
+
|
|
173
|
+
// --- Carry over the PIN hash directly (preserve the existing credential;
|
|
174
|
+
// we have the hash, not the raw PIN, so we can't go through updateUserPin) ---
|
|
175
|
+
if (hasPin) {
|
|
176
|
+
try {
|
|
177
|
+
var data = users.loadUsers();
|
|
178
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
179
|
+
if (data.users[i].id === userId) { data.users[i].pinHash = cfg.pinHash; break; }
|
|
180
|
+
}
|
|
181
|
+
users.saveUsers(data);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error("[migrate] PIN carry-over failed (user will have no PIN): " + (e.message || e));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Carry over settings ---
|
|
188
|
+
try {
|
|
189
|
+
if (cfg.chatLayout !== undefined && users.setChatLayout) users.setChatLayout(userId, cfg.chatLayout);
|
|
190
|
+
if (cfg.autoContinueOnRateLimit !== undefined && users.setAutoContinue) users.setAutoContinue(userId, cfg.autoContinueOnRateLimit);
|
|
191
|
+
if (cfg.matesEnabled !== undefined && users.setMatesEnabled) users.setMatesEnabled(userId, cfg.matesEnabled);
|
|
192
|
+
if (cfg.terminalFont && users.setTerminalFont) {
|
|
193
|
+
users.setTerminalFont(userId, cfg.terminalFont.family, cfg.terminalFont.size);
|
|
194
|
+
}
|
|
195
|
+
// Fields without a dedicated setter: write directly.
|
|
196
|
+
if (cfg.deletedBuiltinKeys !== undefined || cfg.mateOnboardingShown !== undefined) {
|
|
197
|
+
var d2 = users.loadUsers();
|
|
198
|
+
for (var j = 0; j < d2.users.length; j++) {
|
|
199
|
+
if (d2.users[j].id === userId) {
|
|
200
|
+
if (cfg.deletedBuiltinKeys !== undefined) d2.users[j].deletedBuiltinKeys = cfg.deletedBuiltinKeys;
|
|
201
|
+
if (cfg.mateOnboardingShown !== undefined) d2.users[j].mateOnboardingShown = cfg.mateOnboardingShown;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
users.saveUsers(d2);
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.error("[migrate] settings carry-over failed (non-fatal): " + (e.message || e));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Move legacy flat mates under the new user ---
|
|
212
|
+
migrateMatesToUser(userId);
|
|
213
|
+
|
|
214
|
+
// --- Backfill session ownership so legacy sessions belong to the user ---
|
|
215
|
+
var backfilled = backfillSessionOwners(userId);
|
|
216
|
+
if (backfilled > 0) console.log("[migrate] Backfilled ownerId on " + backfilled + " legacy session(s)");
|
|
217
|
+
|
|
218
|
+
// --- Mark done (keep the old daemon-config keys for now; a later phase
|
|
219
|
+
// removes the dual settings path). ---
|
|
220
|
+
try {
|
|
221
|
+
var freshCfg = config.loadConfig() || {};
|
|
222
|
+
freshCfg.singleUserMigratedAt = Date.now();
|
|
223
|
+
freshCfg.singleUserMigratedUserId = userId;
|
|
224
|
+
config.saveConfig(freshCfg);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error("[migrate] Failed to write migration marker: " + (e.message || e));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log("[migrate] Single-user -> multi-user migration complete. admin userId=" + userId + (hasPin ? " (PIN preserved)" : " (no PIN; solo auto-login)"));
|
|
230
|
+
return { migrated: true, userId: userId };
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error("[migrate] Single-user migration aborted (non-fatal): " + (e.message || e));
|
|
233
|
+
return { skipped: "error", error: e.message || String(e) };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Guarantee the app is in the multi-user model with at least one user, so the
|
|
238
|
+
// single-user runtime never exists. Called at daemon boot:
|
|
239
|
+
// - already multi-user with users -> nothing to do
|
|
240
|
+
// - legacy single-user with data -> migrate it (preserves PIN/settings)
|
|
241
|
+
// - fresh/empty -> provision one no-PIN "admin"
|
|
242
|
+
// (solo auto-login: no login wall)
|
|
243
|
+
// OS-user isolation stays an orthogonal, settings/flag-driven toggle.
|
|
244
|
+
function ensureMultiUser() {
|
|
245
|
+
try {
|
|
246
|
+
if (users.isMultiUser()) {
|
|
247
|
+
var have = users.getAllUsers();
|
|
248
|
+
if (have && have.length > 0) return { skipped: "ready" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Try the legacy single-user migration first (handles PIN/settings/mates/
|
|
252
|
+
// session carry-over for existing deploys).
|
|
253
|
+
var mig = migrateSingleUserToMulti();
|
|
254
|
+
if (mig && mig.migrated) return mig;
|
|
255
|
+
|
|
256
|
+
// Fresh or empty deploy: provision a default admin so we're multi-user with
|
|
257
|
+
// one user. No PIN -> the phase-1 solo auto-login skips the login wall.
|
|
258
|
+
var existing = users.getAllUsers();
|
|
259
|
+
if (!existing || existing.length === 0) {
|
|
260
|
+
if (!users.isMultiUser()) users.enableMultiUser();
|
|
261
|
+
var created = users.createUserWithoutPin({
|
|
262
|
+
username: "admin",
|
|
263
|
+
displayName: "Admin",
|
|
264
|
+
role: "admin",
|
|
265
|
+
});
|
|
266
|
+
if (!created || !created.user) {
|
|
267
|
+
console.error("[migrate] Failed to provision default admin: " + (created && created.error));
|
|
268
|
+
return { skipped: "provision-failed" };
|
|
269
|
+
}
|
|
270
|
+
console.log("[migrate] Provisioned default admin (no PIN; solo auto-login). userId=" + created.user.id);
|
|
271
|
+
return { provisioned: true, userId: created.user.id };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Users exist but flag wasn't set (shouldn't normally happen) -> flip it.
|
|
275
|
+
if (!users.isMultiUser()) users.enableMultiUser();
|
|
276
|
+
return { skipped: "flag-fixed" };
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.error("[migrate] ensureMultiUser failed (non-fatal): " + (e.message || e));
|
|
279
|
+
return { skipped: "error", error: e.message || String(e) };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
migrateSingleUserToMulti: migrateSingleUserToMulti,
|
|
285
|
+
ensureMultiUser: ensureMultiUser,
|
|
286
|
+
};
|
package/lib/server-auth.js
CHANGED
|
@@ -102,11 +102,20 @@ function attachAuth(ctx) {
|
|
|
102
102
|
function getMultiUserFromReq(req) {
|
|
103
103
|
var cookies = parseCookies(req);
|
|
104
104
|
var token = cookies[MULTI_USER_COOKIE];
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
if (token) {
|
|
106
|
+
var userId = multiUserTokens[token];
|
|
107
|
+
if (userId) {
|
|
108
|
+
var user = users.findUserById(userId);
|
|
109
|
+
if (user) return user;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Solo auto-login: exactly one user with no PIN => frictionless access, no
|
|
113
|
+
// login wall. Mirrors the old single-user onramp once single-user mode is
|
|
114
|
+
// absorbed into multi-user. Disabled the moment a PIN is set or a second
|
|
115
|
+
// user is added (then the normal token/login flow applies).
|
|
116
|
+
var solo = users.getSoloNoPinUser();
|
|
117
|
+
if (solo) return solo;
|
|
118
|
+
return null;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
function isMultiUserAuthed(req) {
|
package/lib/users.js
CHANGED
|
@@ -165,6 +165,19 @@ function getAllUsers() {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// Solo frictionless access: exactly one user with no PIN. Returns the full
|
|
169
|
+
// user record (callers read id/role/etc.) or null. Used to skip the login wall
|
|
170
|
+
// so an absorbed single-user deploy stays zero-friction; auto-disables the
|
|
171
|
+
// moment a PIN is set or a second user is added.
|
|
172
|
+
function getSoloNoPinUser() {
|
|
173
|
+
if (!isMultiUser()) return null;
|
|
174
|
+
var data = loadUsers();
|
|
175
|
+
if (!data.users || data.users.length !== 1) return null;
|
|
176
|
+
var u = data.users[0];
|
|
177
|
+
if (u.pinHash) return null;
|
|
178
|
+
return u;
|
|
179
|
+
}
|
|
180
|
+
|
|
168
181
|
function getOtherUsers(excludeUserId) {
|
|
169
182
|
return getAllUsers().filter(function (u) {
|
|
170
183
|
return u.id !== excludeUserId;
|
|
@@ -451,6 +464,7 @@ module.exports = {
|
|
|
451
464
|
findUserByEmail: findUserByEmail,
|
|
452
465
|
authenticateUser: authenticateUser,
|
|
453
466
|
getAllUsers: getAllUsers,
|
|
467
|
+
getSoloNoPinUser: getSoloNoPinUser,
|
|
454
468
|
removeUser: removeUser,
|
|
455
469
|
updateUserProfile: updateUserProfile,
|
|
456
470
|
updateUserPin: updateUserPin,
|
|
@@ -620,7 +620,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
620
620
|
done: false,
|
|
621
621
|
aborted: false,
|
|
622
622
|
loopStarted: false,
|
|
623
|
-
model: queryOpts.model || "gpt-5.
|
|
623
|
+
model: queryOpts.model || "gpt-5.6-terra",
|
|
624
624
|
// Track incremental text deltas
|
|
625
625
|
textBlocks: {}, // itemId -> true (text_start sent)
|
|
626
626
|
textLengths: {}, // itemId -> last sent length
|
|
@@ -865,7 +865,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
865
865
|
|
|
866
866
|
// Start or resume thread
|
|
867
867
|
var threadParams = {
|
|
868
|
-
model: queryOpts.model || "gpt-5.
|
|
868
|
+
model: queryOpts.model || "gpt-5.6-terra",
|
|
869
869
|
sandbox: queryOpts.sandboxMode || "workspace-write",
|
|
870
870
|
approvalPolicy: queryOpts.approvalPolicy || "on-failure",
|
|
871
871
|
cwd: queryOpts.cwd,
|
|
@@ -1086,6 +1086,9 @@ function createCodexAdapter(opts) {
|
|
|
1086
1086
|
// slow/failed `initialize` leaves the picker empty and the chip shows the
|
|
1087
1087
|
// previous vendor's model.
|
|
1088
1088
|
var CODEX_MODELS = [
|
|
1089
|
+
"gpt-5.6-terra",
|
|
1090
|
+
"gpt-5.6-sol",
|
|
1091
|
+
"gpt-5.6-luna",
|
|
1089
1092
|
"gpt-5.5",
|
|
1090
1093
|
"gpt-5.4",
|
|
1091
1094
|
"gpt-5.4-mini",
|
|
@@ -1131,7 +1134,7 @@ function createCodexAdapter(opts) {
|
|
|
1131
1134
|
function buildReadyResponse(skillNames) {
|
|
1132
1135
|
return {
|
|
1133
1136
|
models: _cachedModels,
|
|
1134
|
-
defaultModel: "gpt-5.
|
|
1137
|
+
defaultModel: "gpt-5.6-terra",
|
|
1135
1138
|
skills: skillNames || [],
|
|
1136
1139
|
slashCommands: skillNames || [],
|
|
1137
1140
|
fastModeState: null,
|
|
@@ -1428,7 +1431,7 @@ function createCodexAdapter(opts) {
|
|
|
1428
1431
|
throw new Error("[yoke/codex] Adapter not initialized. Call init() first.");
|
|
1429
1432
|
}
|
|
1430
1433
|
|
|
1431
|
-
var model = queryOpts.model || "gpt-5.
|
|
1434
|
+
var model = queryOpts.model || "gpt-5.6-terra";
|
|
1432
1435
|
var ac = queryOpts.abortController || new AbortController();
|
|
1433
1436
|
var activeEntry = {
|
|
1434
1437
|
abort: function() {
|
package/package.json
CHANGED