clay-server 5.2.0-beta.4 → 5.2.0-beta.5
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/account-auth-socket.js +1 -0
- package/lib/builtin-mates.js +17 -14
- package/lib/canonical-clay-record-authority.js +15 -0
- package/lib/canonical-clay-scheduling-authority.js +19 -0
- package/lib/claude-hook-installer.js +28 -0
- package/lib/clay-mate-chat-receipts.js +26 -0
- package/lib/clay-mate-chat.js +87 -0
- package/lib/clay-mate-knowledge-input.js +26 -0
- package/lib/clay-mate-knowledge.js +95 -0
- package/lib/clay-mate-lifecycle.js +95 -0
- package/lib/clay-product-control-input.js +33 -0
- package/lib/clay-product-control-tools.js +51 -0
- package/lib/clay-product-control.js +237 -0
- package/lib/clay-project-lifecycle.js +154 -0
- package/lib/clay-project-record-input.js +44 -0
- package/lib/clay-project-records.js +83 -0
- package/lib/clay-scheduling-input.js +21 -0
- package/lib/clay-scheduling-receipts.js +26 -0
- package/lib/clay-scheduling.js +119 -0
- package/lib/clay-screen-controls.js +96 -0
- package/lib/clay-workspace-controls.js +83 -0
- package/lib/clay-workspace-input.js +17 -0
- package/lib/clay-workspace-receipts.js +26 -0
- package/lib/daemon-project-access.js +11 -0
- package/lib/daemon-project-lifecycle-callbacks.js +15 -0
- package/lib/daemon-project-lifecycle.js +262 -0
- package/lib/daemon-projects.js +1 -1
- package/lib/daemon.js +73 -221
- package/lib/issues-service.js +29 -15
- package/lib/knowledge-db-import.js +5 -4
- package/lib/knowledge-db-service.js +9 -6
- package/lib/knowledge-document-index.js +2 -1
- package/lib/knowledge-product-operations.js +46 -0
- package/lib/mate-creation-mcp-server.js +2 -0
- package/lib/mate-knowledge-canonical-access.js +10 -0
- package/lib/mate-knowledge-service.js +7 -4
- package/lib/mate-lifecycle-service.js +94 -0
- package/lib/mate-message-delivery.js +43 -0
- package/lib/mate-ready-creation.js +9 -1
- package/lib/mate-schedule-input.js +29 -0
- package/lib/mate-schedule-lifecycle.js +151 -0
- package/lib/mate-scheduling.js +103 -0
- package/lib/mates.js +4 -3
- package/lib/notes-checklist.js +50 -0
- package/lib/notes.js +49 -3
- package/lib/os-users.js +20 -5
- package/lib/project-admin-overview.js +51 -0
- package/lib/project-ask-user.js +4 -2
- package/lib/project-delegated-follow-up.js +2 -1
- package/lib/project-directory-claim.js +65 -0
- package/lib/project-file-operations.js +61 -0
- package/lib/project-filesystem.js +13 -87
- package/lib/project-full-access.js +23 -2
- package/lib/project-knowledge.js +32 -18
- package/lib/project-lifecycle-operations.js +98 -0
- package/lib/project-lifecycle-process-witness.js +32 -0
- package/lib/project-lifecycle-runtime.js +50 -0
- package/lib/project-linux-access.js +22 -0
- package/lib/project-logs-canonical-clay.js +27 -0
- package/lib/project-logs-clay-query.js +16 -0
- package/lib/project-logs-schema.js +4 -2
- package/lib/project-logs-service.js +8 -7
- package/lib/project-logs.js +1 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-mate-chat-control.js +55 -0
- package/lib/project-mate-creation-proposal.js +18 -3
- package/lib/project-models.js +14 -31
- package/lib/project-notifications.js +74 -16
- package/lib/project-ownership.js +77 -0
- package/lib/project-pair-result-delivery.js +14 -3
- package/lib/project-pending-message-queue.js +1 -0
- package/lib/project-product-control.js +87 -0
- package/lib/project-product-session-queue.js +46 -0
- package/lib/project-push-access.js +28 -0
- package/lib/project-scheduled-message-queue.js +30 -0
- package/lib/project-scheduled-messages.js +63 -64
- package/lib/project-scheduled-task-control.js +83 -0
- package/lib/project-scheduled-tasks.js +13 -2
- package/lib/project-scheduling-access.js +10 -0
- package/lib/project-session-actions.js +65 -0
- package/lib/project-session-delete.js +8 -1
- package/lib/project-session-github.js +47 -12
- package/lib/project-session-history-actions.js +165 -0
- package/lib/project-session-runtime-actions.js +63 -0
- package/lib/project-session-vendor.js +54 -0
- package/lib/project-sessions.js +64 -241
- package/lib/project-user-message.js +30 -1
- package/lib/project-workspace-operations.js +126 -0
- package/lib/project-workspace-query.js +9 -2
- package/lib/project.js +95 -3
- package/lib/public/css/clay-tool-activity.css +51 -0
- package/lib/public/css/home-mate-creation.css +5 -0
- package/lib/public/css/overlays.css +4 -2
- package/lib/public/css/project-admin-overview.css +13 -0
- package/lib/public/css/session-folders.css +11 -0
- package/lib/public/css/session-github.css +3 -0
- package/lib/public/css/sticky-notes.css +61 -1
- package/lib/public/index.html +1 -1
- package/lib/public/modules/admin-settings-projects.js +6 -2
- package/lib/public/modules/app-connection.js +8 -1
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +18 -3
- package/lib/public/modules/app-projects.js +5 -0
- package/lib/public/modules/clay-owner-navigation.js +27 -0
- package/lib/public/modules/clay-tool-activity.js +140 -0
- package/lib/public/modules/clay-tool-presentation.js +338 -0
- package/lib/public/modules/command-palette.js +1 -1
- package/lib/public/modules/default-ai.js +11 -3
- package/lib/public/modules/home-debate-planning.js +5 -5
- package/lib/public/modules/home-mate-chat.js +8 -6
- package/lib/public/modules/home-mate-creation.js +73 -1
- package/lib/public/modules/personal-settings-dialog.js +7 -1
- package/lib/public/modules/project-access-recovery.js +17 -0
- package/lib/public/modules/project-admin-overview.js +21 -0
- package/lib/public/modules/session-folder-layout.js +1 -1
- package/lib/public/modules/session-folder-view-menu.js +9 -0
- package/lib/public/modules/session-github.js +65 -8
- package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
- package/lib/public/modules/sticky-note-markdown.js +72 -6
- package/lib/public/modules/sticky-notes-actions.js +30 -0
- package/lib/public/modules/sticky-notes-browser.js +9 -2
- package/lib/public/modules/sticky-notes-card.js +23 -7
- package/lib/public/modules/sticky-notes-checklist.js +71 -0
- package/lib/public/modules/sticky-notes-editor.js +108 -44
- package/lib/public/modules/sticky-notes-shared.js +50 -2
- package/lib/public/modules/sticky-notes-state.js +66 -0
- package/lib/public/modules/sticky-notes.js +34 -51
- package/lib/public/modules/thinking-lifecycle.js +33 -3
- package/lib/public/modules/thinking-view.js +23 -1
- package/lib/public/modules/tools.js +34 -6
- package/lib/public/modules/user-settings-appearance.js +10 -3
- package/lib/public/modules/user-settings-editors.js +1 -1
- package/lib/public/modules/user-settings-layout.js +11 -2
- package/lib/public/modules/user-settings-preferences.js +6 -2
- package/lib/public/modules/user-settings-profile.js +12 -2
- package/lib/public/modules/user-settings.js +3 -2
- package/lib/public/style.css +2 -0
- package/lib/scheduled-task-records.js +3 -1
- package/lib/scheduler.js +2 -0
- package/lib/sdk-bridge.js +220 -21
- package/lib/sdk-message-processor.js +2 -0
- package/lib/sdk-response-recovery.js +155 -0
- package/lib/server-admin.js +63 -16
- package/lib/server-dm.js +1 -1
- package/lib/server-global-ws.js +10 -20
- package/lib/server-home-chat-events.js +7 -3
- package/lib/server-home-chat.js +4 -16
- package/lib/server-home-mate-creation.js +1 -0
- package/lib/server-home-mcp-consent.js +3 -3
- package/lib/server-mates.js +25 -67
- package/lib/server-owner-ui.js +77 -0
- package/lib/server-path-access.js +38 -0
- package/lib/server-project-removal.js +22 -0
- package/lib/server.js +99 -29
- package/lib/session-config-transition.js +45 -0
- package/lib/session-folders.js +7 -2
- package/lib/session-github.js +9 -2
- package/lib/session-visibility.js +5 -2
- package/lib/sessions.js +10 -0
- package/lib/terminal-manager.js +52 -0
- package/lib/users-permissions.js +2 -8
- package/lib/users.js +4 -0
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +8 -2
- package/lib/yoke/adapters/claude.js +47 -8
- package/lib/yoke/adapters/codex.js +52 -111
- package/lib/yoke/claude-worker-startup.js +28 -0
- package/lib/yoke/codex-idle-recovery.js +81 -1
- package/lib/yoke/codex-recovery-admission.js +164 -0
- package/lib/yoke/session-tool-transport.js +3 -0
- package/package.json +1 -1
package/lib/server-admin.js
CHANGED
|
@@ -18,6 +18,13 @@ function attachAdmin(ctx) {
|
|
|
18
18
|
var onGetProjectAccess = ctx.onGetProjectAccess;
|
|
19
19
|
var onProjectOwnerChanged = ctx.onProjectOwnerChanged;
|
|
20
20
|
|
|
21
|
+
function verifyCurrentOwner(req, route) {
|
|
22
|
+
if (!route || req.method === "GET" || !users.isMultiUser()) return;
|
|
23
|
+
var actor = getMultiUserFromReq(req);
|
|
24
|
+
var access = onGetProjectAccess && onGetProjectAccess(route[1]);
|
|
25
|
+
if (!actor || !access || access.ownerId !== actor.id || !users.findUserById(access.ownerId)) throw new Error("Project owner access required");
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
function handleRequest(req, res, fullUrl) {
|
|
22
29
|
var mateAccessRoute = /^\/api\/admin\/projects\/([^/]+)\/(visibility|owner|users|access)$/.exec(fullUrl);
|
|
23
30
|
if (mateAccessRoute) {
|
|
@@ -31,6 +38,25 @@ function attachAdmin(ctx) {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
|
|
41
|
+
// Project content authority belongs to the valid owner, never the role.
|
|
42
|
+
if (mateAccessRoute && users.isMultiUser()) {
|
|
43
|
+
var actor = getMultiUserFromReq(req);
|
|
44
|
+
var governing = onGetProjectAccess && onGetProjectAccess(mateAccessRoute[1]);
|
|
45
|
+
var validOwner = governing && governing.ownerId && users.findUserById(governing.ownerId);
|
|
46
|
+
var owns = actor && validOwner && actor.id === validOwner.id;
|
|
47
|
+
if (req.method !== "GET" && !owns) {
|
|
48
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
49
|
+
res.end(JSON.stringify({ error: "Only the valid project owner can change access." }));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === "GET" && !owns && actor && actor.role === "admin") {
|
|
53
|
+
var overview = require("./project-admin-overview").metadataFor(actor, target && target.getStatus(), governing, users);
|
|
54
|
+
res.writeHead(overview ? 200 : 404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
55
|
+
res.end(JSON.stringify(overview || { error: "Project not found" }));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
// --- Admin API endpoints (multi-user mode only) ---
|
|
35
61
|
|
|
36
62
|
// List all users (admin only)
|
|
@@ -83,6 +109,16 @@ function attachAdmin(ctx) {
|
|
|
83
109
|
// Look up the user before deletion to get linuxUser for deactivation
|
|
84
110
|
var targetUser = users.findUserById(targetUserId);
|
|
85
111
|
var targetLinuxUser = targetUser ? targetUser.linuxUser : null;
|
|
112
|
+
var owned = false;
|
|
113
|
+
projects.forEach(function (project, slug) {
|
|
114
|
+
var access = onGetProjectAccess && onGetProjectAccess(slug);
|
|
115
|
+
if (access && !access.isMate && access.ownerId === targetUserId) owned = true;
|
|
116
|
+
});
|
|
117
|
+
if (owned) {
|
|
118
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({ error: "The owner must transfer ordinary projects before account deletion." }));
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
86
122
|
var result = users.removeUser(targetUserId);
|
|
87
123
|
if (result.error) {
|
|
88
124
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -118,6 +154,7 @@ function attachAdmin(ctx) {
|
|
|
118
154
|
req.on("end", function () {
|
|
119
155
|
try {
|
|
120
156
|
var data = JSON.parse(body);
|
|
157
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
121
158
|
if (!data.username || typeof data.username !== "string" || data.username.trim().length < 1) {
|
|
122
159
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
123
160
|
res.end('{"error":"Username is required"}');
|
|
@@ -182,6 +219,11 @@ function attachAdmin(ctx) {
|
|
|
182
219
|
res.end('{"error":"User not found"}');
|
|
183
220
|
return true;
|
|
184
221
|
}
|
|
222
|
+
if (mu.id !== targetUserId) {
|
|
223
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
224
|
+
res.end(JSON.stringify({ error: "Use account-owner credential recovery. Administrators cannot impersonate another account." }));
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
185
227
|
var newPin = users.generatePin();
|
|
186
228
|
var pinResult = users.updateUserPin(targetUserId, newPin);
|
|
187
229
|
if (pinResult.error) {
|
|
@@ -222,15 +264,10 @@ function attachAdmin(ctx) {
|
|
|
222
264
|
req.on("data", function(chunk) { body += chunk; });
|
|
223
265
|
req.on("end", function() {
|
|
224
266
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
res.end(JSON.stringify({ error: result.error }));
|
|
230
|
-
} else {
|
|
231
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
232
|
-
res.end('{"ok":true}');
|
|
233
|
-
}
|
|
267
|
+
JSON.parse(body);
|
|
268
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
269
|
+
res.end(JSON.stringify({ error: "OS identity mapping requires server-side verified provisioning." }));
|
|
270
|
+
return;
|
|
234
271
|
} catch (e) {
|
|
235
272
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
236
273
|
res.end('{"error":"Invalid request body"}');
|
|
@@ -363,6 +400,7 @@ function attachAdmin(ctx) {
|
|
|
363
400
|
req.on("end", function () {
|
|
364
401
|
try {
|
|
365
402
|
var data = JSON.parse(body);
|
|
403
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
366
404
|
if (!data.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
367
405
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
368
406
|
res.end('{"error":"Valid email is required"}');
|
|
@@ -429,6 +467,7 @@ function attachAdmin(ctx) {
|
|
|
429
467
|
req.on("end", function () {
|
|
430
468
|
try {
|
|
431
469
|
var data = JSON.parse(body);
|
|
470
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
432
471
|
// Allow clearing SMTP config by sending empty fields
|
|
433
472
|
if (!data.host && !data.user && !data.pass && !data.from) {
|
|
434
473
|
smtp.saveSmtpConfig(null);
|
|
@@ -484,6 +523,7 @@ function attachAdmin(ctx) {
|
|
|
484
523
|
req.on("end", function () {
|
|
485
524
|
try {
|
|
486
525
|
var data = JSON.parse(body);
|
|
526
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
487
527
|
// Use provided config or fall back to saved
|
|
488
528
|
var existingCfg = smtp.getSmtpConfig();
|
|
489
529
|
var pass = data.pass;
|
|
@@ -548,6 +588,7 @@ function attachAdmin(ctx) {
|
|
|
548
588
|
req.on("end", function () {
|
|
549
589
|
try {
|
|
550
590
|
var data = JSON.parse(body);
|
|
591
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
551
592
|
if (data.visibility !== "public" && data.visibility !== "private") {
|
|
552
593
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
553
594
|
res.end('{"error":"Visibility must be public or private"}');
|
|
@@ -582,9 +623,10 @@ function attachAdmin(ctx) {
|
|
|
582
623
|
return true;
|
|
583
624
|
}
|
|
584
625
|
var mu = getMultiUserFromReq(req);
|
|
585
|
-
|
|
626
|
+
var ownerAccess = onGetProjectAccess && onGetProjectAccess(fullUrl.split("/")[4]);
|
|
627
|
+
if (!mu || !ownerAccess || mu.id !== ownerAccess.ownerId) {
|
|
586
628
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
587
|
-
res.end('{"error":"
|
|
629
|
+
res.end('{"error":"Project owner access required"}');
|
|
588
630
|
return true;
|
|
589
631
|
}
|
|
590
632
|
var projSlug = fullUrl.split("/")[4];
|
|
@@ -598,17 +640,21 @@ function attachAdmin(ctx) {
|
|
|
598
640
|
req.on("end", function () {
|
|
599
641
|
try {
|
|
600
642
|
var data = JSON.parse(body);
|
|
643
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
601
644
|
var targetCtx = projects.get(projSlug);
|
|
602
645
|
if (!targetCtx) {
|
|
603
646
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
604
647
|
res.end('{"error":"Project not found"}');
|
|
605
648
|
return;
|
|
606
649
|
}
|
|
607
|
-
var ownerId = data.userId
|
|
650
|
+
var ownerId = data.userId;
|
|
651
|
+
if (typeof ownerId !== "string" || !users.findUserById(ownerId)) throw new Error("A valid owner is required");
|
|
652
|
+
var currentAccess = onGetProjectAccess && onGetProjectAccess(projSlug);
|
|
653
|
+
if (!currentAccess || currentAccess.ownerId !== mu.id) throw new Error("Project owner access required");
|
|
654
|
+
if (!onProjectOwnerChanged) throw new Error("Ownership persistence is unavailable");
|
|
655
|
+
var saved = onProjectOwnerChanged(projSlug, ownerId);
|
|
656
|
+
if (saved && saved.error) throw new Error(saved.error);
|
|
608
657
|
targetCtx.setProjectOwner(ownerId);
|
|
609
|
-
if (onProjectOwnerChanged) {
|
|
610
|
-
onProjectOwnerChanged(projSlug, ownerId);
|
|
611
|
-
}
|
|
612
658
|
// Broadcast to project clients
|
|
613
659
|
var ownerName = null;
|
|
614
660
|
if (ownerId) {
|
|
@@ -653,7 +699,8 @@ function attachAdmin(ctx) {
|
|
|
653
699
|
req.on("end", function () {
|
|
654
700
|
try {
|
|
655
701
|
var data = JSON.parse(body);
|
|
656
|
-
|
|
702
|
+
verifyCurrentOwner(req, mateAccessRoute);
|
|
703
|
+
if (!Array.isArray(data.allowedUsers) || data.allowedUsers.some(function (id) { return typeof id !== "string" || !users.findUserById(id); })) {
|
|
657
704
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
658
705
|
res.end('{"error":"allowedUsers must be an array"}');
|
|
659
706
|
return;
|
package/lib/server-dm.js
CHANGED
|
@@ -49,7 +49,7 @@ function attachDm(ctx) {
|
|
|
49
49
|
var mateCtx2 = mates.buildMateCtx(userId);
|
|
50
50
|
if (mates.isMate(mateCtx2, msg.targetUserId)) {
|
|
51
51
|
var mate = mates.getMate(mateCtx2, msg.targetUserId);
|
|
52
|
-
if (!mate) return true;
|
|
52
|
+
if (!mate || mate.lifecycleDeletion) return true;
|
|
53
53
|
// Ensure mate project is registered (survives server restarts)
|
|
54
54
|
var mateSlug2 = "mate-" + mate.id;
|
|
55
55
|
if (!projects.has(mateSlug2)) {
|
package/lib/server-global-ws.js
CHANGED
|
@@ -63,14 +63,6 @@ function attachGlobalWs(opts) {
|
|
|
63
63
|
if (msg.type === "browse_dir") {
|
|
64
64
|
var rawPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
|
|
65
65
|
var absTarget = path.resolve(rawPath);
|
|
66
|
-
// Multi-user mode: non-admins can only browse their home directory.
|
|
67
|
-
if (osUsers && ws._clayUser && ws._clayUser.role !== "admin") {
|
|
68
|
-
var browseHome = ws._clayUser.linuxUser ? "/home/" + ws._clayUser.linuxUser : null;
|
|
69
|
-
if (!browseHome || (absTarget !== browseHome && (absTarget + "/").indexOf(browseHome + "/") !== 0)) {
|
|
70
|
-
sendTo(ws, { type: "browse_dir_result", path: msg.path, entries: [], error: "Access restricted to your home directory" });
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
66
|
var parentDir, prefix;
|
|
75
67
|
try {
|
|
76
68
|
var stat = fs.statSync(absTarget);
|
|
@@ -87,12 +79,17 @@ function attachGlobalWs(opts) {
|
|
|
87
79
|
parentDir = path.dirname(absTarget);
|
|
88
80
|
prefix = path.basename(absTarget).toLowerCase();
|
|
89
81
|
}
|
|
82
|
+
if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, parentDir)) {
|
|
83
|
+
sendTo(ws, { type: "browse_dir_result", entries: [], error: "Directory access is not permitted" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
90
86
|
try {
|
|
91
87
|
var dirItems = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
92
88
|
var dirEntries = [];
|
|
93
89
|
for (var di = 0; di < dirItems.length; di++) {
|
|
94
90
|
var d = dirItems[di];
|
|
95
91
|
if (!d.isDirectory()) continue;
|
|
92
|
+
if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, path.join(parentDir, d.name))) continue;
|
|
96
93
|
if (d.name.charAt(0) === ".") continue;
|
|
97
94
|
if (IGNORED_DIRS.has(d.name)) continue;
|
|
98
95
|
if (prefix && !d.name.toLowerCase().startsWith(prefix)) continue;
|
|
@@ -110,16 +107,9 @@ function attachGlobalWs(opts) {
|
|
|
110
107
|
if (msg.type === "add_project") {
|
|
111
108
|
var addPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
|
|
112
109
|
var addAbs = path.resolve(addPath);
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
var userHome = "/home/" + ws._clayUser.linuxUser;
|
|
119
|
-
if (addAbs !== userHome && (addAbs + "/").indexOf(userHome + "/") !== 0) {
|
|
120
|
-
sendTo(ws, { type: "add_project_result", ok: false, error: "Path not allowed. You can only add directories under " + userHome });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
110
|
+
if (opts.canUsePath && !opts.canUsePath(ws._clayUser && ws._clayUser.id, addAbs)) {
|
|
111
|
+
sendTo(ws, { type: "add_project_result", ok: false, error: "Directory access is not permitted" });
|
|
112
|
+
return;
|
|
123
113
|
}
|
|
124
114
|
try {
|
|
125
115
|
var addStat = fs.statSync(addAbs);
|
|
@@ -133,7 +123,7 @@ function attachGlobalWs(opts) {
|
|
|
133
123
|
}
|
|
134
124
|
if (typeof onAddProject === "function") {
|
|
135
125
|
var result = onAddProject(addAbs, ws._clayUser);
|
|
136
|
-
sendTo(ws, {
|
|
126
|
+
sendTo(ws, Object.assign({}, result, { type: "add_project_result" }));
|
|
137
127
|
} else {
|
|
138
128
|
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
139
129
|
}
|
|
@@ -159,7 +149,7 @@ function attachGlobalWs(opts) {
|
|
|
159
149
|
}
|
|
160
150
|
if (typeof onCreateProject === "function") {
|
|
161
151
|
var createResult = onCreateProject(createName, ws._clayUser);
|
|
162
|
-
sendTo(ws, {
|
|
152
|
+
sendTo(ws, Object.assign({}, createResult, { type: "add_project_result" }));
|
|
163
153
|
} else {
|
|
164
154
|
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
165
155
|
}
|
|
@@ -125,10 +125,10 @@ function historyToHomeChat(history, debateSetupMode, mateCreationMode, pendingAs
|
|
|
125
125
|
for (var i = 0; i < history.length; i++) {
|
|
126
126
|
var event = history[i];
|
|
127
127
|
if (!event) continue;
|
|
128
|
-
if (event.type === 'tool_executing' && event.name === 'AskUserQuestion' &&
|
|
128
|
+
if (event.type === 'tool_executing' && event.name === 'AskUserQuestion' && ['mcp-import', 'mate-schedule', 'mcp_fallback', 'product-control'].indexOf(event.source) >= 0) {
|
|
129
129
|
flushAssistant();
|
|
130
130
|
var activeConsent = pendingAskUser === undefined || !!(pendingAskUser && pendingAskUser[event.id]);
|
|
131
|
-
messages.push({ role: 'question', flow: 'mcp_import', toolId: event.id, questions: homeDebateQuestions(event.input), status: activeConsent ? 'pending' : 'expired', error: activeConsent ? '' : 'This
|
|
131
|
+
messages.push({ role: 'question', flow: event.source === 'mcp-import' ? 'mcp_import' : 'mate_question', toolId: event.id, questions: homeDebateQuestions(event.input), status: activeConsent ? 'pending' : 'expired', error: activeConsent ? '' : 'This question is no longer active. Ask for a new review.' });
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
134
|
if (event.type === "project_assignment_proposal" || event.type === "project_assignment_status") {
|
|
@@ -192,7 +192,7 @@ function historyToHomeChat(history, debateSetupMode, mateCreationMode, pendingAs
|
|
|
192
192
|
} else if (interviewMode && event.type === "tool_executing" && event.name === "AskUserQuestion" && event.id && event.input) {
|
|
193
193
|
flushAssistant();
|
|
194
194
|
messages.push({ role: "question", flow: mateCreationMode === true ? "mate_creation" : "debate", toolId: event.id, questions: homeDebateQuestions(event.input), status: "pending", error: "" });
|
|
195
|
-
} else if ((event.type === "ask_user_answered" || event.type === "home_debate_question_expired" || event.type === "home_mate_creation_question_expired" || event.type === "mcp_import_consent_expired") && event.toolId) {
|
|
195
|
+
} else if ((event.type === "ask_user_answered" || event.type === "home_debate_question_expired" || event.type === "home_mate_creation_question_expired" || event.type === "mcp_import_consent_expired" || event.type === "mate_schedule_question_expired") && event.toolId) {
|
|
196
196
|
for (var k = messages.length - 1; k >= 0; k--) {
|
|
197
197
|
if (messages[k].role === "question" && messages[k].toolId === event.toolId) {
|
|
198
198
|
messages[k].status = event.type === "ask_user_answered" ? "answered" : "expired";
|
|
@@ -305,7 +305,11 @@ function transformEvent(event, mateId, session, requestId, stableSessionId) {
|
|
|
305
305
|
if (event.type === "debate_proposal_resolved" && event.proposalId) return Object.assign({ type: "home_debate_proposal_resolved", proposalId: event.proposalId, action: event.action || "cancel", error: event.error || "" }, metadata);
|
|
306
306
|
if (session && session.mateCreationMode === true && event.type === "mate_creation_proposal" && event.proposal) return Object.assign({ type: "home_mate_creation_proposal", proposal: event.proposal }, metadata);
|
|
307
307
|
if (session && session.mateCreationMode === true && event.type === "mate_creation_proposal_resolved" && event.proposalId) return Object.assign({ type: "home_mate_creation_proposal_resolved", proposalId: event.proposalId, action: event.action || "cancel", createdMateId: event.mateId || null, mateName: event.mateName || null, error: event.error || "" }, metadata);
|
|
308
|
+
if (!(session && (session.debateSetupMode === true || session.mateCreationMode === true)) && event.type === 'tool_executing' && event.name === 'AskUserQuestion' && ['mate-schedule', 'mcp_fallback', 'product-control'].indexOf(event.source) >= 0) return Object.assign({ type: 'home_mate_question', toolId: event.id, questions: homeDebateQuestions(event.input) }, metadata);
|
|
309
|
+
if (session && session.debateSetupMode !== true && session.mateCreationMode !== true && event.type === 'ask_user_answered' && event.toolId && (session.history || []).some(function (item) { return item.id === event.toolId && ['mate-schedule', 'mcp_fallback', 'product-control'].indexOf(item.source) >= 0; })) return Object.assign({ type: 'home_mate_question_resolved', toolId: event.toolId, status: 'answered', answers: event.answers }, metadata);
|
|
308
310
|
if (event.type === 'tool_executing' && event.name === 'AskUserQuestion' && event.source === 'mcp-import') return Object.assign({ type: 'home_mcp_import_question', toolId: event.id, questions: homeDebateQuestions(event.input) }, metadata);
|
|
311
|
+
if (event.type === 'product_control_question_expired') return Object.assign({ type: 'home_mate_question_resolved', toolId: event.toolId, status: 'expired', error: event.error }, metadata);
|
|
312
|
+
if (event.type === 'mate_schedule_question_expired') return Object.assign({ type: 'home_mate_question_resolved', toolId: event.toolId, status: 'expired', error: event.error }, metadata);
|
|
309
313
|
if (event.type === 'mcp_import_consent_expired') return Object.assign({ type: 'home_mcp_import_question_resolved', toolId: event.toolId, status: 'expired', error: event.error }, metadata);
|
|
310
314
|
if (session && event.type === 'ask_user_answered' && event.toolId && (session.history || []).some(function (item) { return item.id === event.toolId && item.source === 'mcp-import'; })) return Object.assign({ type: 'home_mcp_import_question_resolved', toolId: event.toolId, status: 'answered', answers: event.answers }, metadata);
|
|
311
315
|
if (session && session.debateSetupMode === true && event.type === "tool_executing" && event.name === "AskUserQuestion" && event.id && event.input) {
|
package/lib/server-home-chat.js
CHANGED
|
@@ -31,7 +31,7 @@ function attachHomeChat(deps) {
|
|
|
31
31
|
}
|
|
32
32
|
if (!mateId && allMates[i].builtinKey === "clay") mate = allMates[i];
|
|
33
33
|
}
|
|
34
|
-
if (!mate) return null;
|
|
34
|
+
if (!mate || mate.lifecycleDeletion) return null;
|
|
35
35
|
var slug = "mate-" + mate.id;
|
|
36
36
|
if (!projects.has(slug)) {
|
|
37
37
|
if (!ensureRegistered) return null;
|
|
@@ -404,20 +404,8 @@ function attachHomeChat(deps) {
|
|
|
404
404
|
historyError.sessionId = sessionReference(activeSession);
|
|
405
405
|
throw historyError;
|
|
406
406
|
}
|
|
407
|
-
sessionManager.sendAndRecord(activeSession, { type: "user_message", text: text });
|
|
408
407
|
try {
|
|
409
|
-
|
|
410
|
-
activeSession.isProcessing = true;
|
|
411
|
-
activeSession.sentToolResults = {};
|
|
412
|
-
if (typeof options.onRecorded === "function") options.onRecorded(activeSession);
|
|
413
|
-
if (!activeSession.queryInstance && (!activeSession.worker || activeSession.messageQueue !== "worker")) {
|
|
414
|
-
sdk.startQuery(activeSession, options.queryText || text, null, null);
|
|
415
|
-
} else {
|
|
416
|
-
sdk.pushMessage(activeSession, options.queryText || text, null);
|
|
417
|
-
}
|
|
418
|
-
} else {
|
|
419
|
-
sdk.pushMessage(activeSession, options.queryText || text, null);
|
|
420
|
-
}
|
|
408
|
+
await require('./mate-message-delivery').deliver({ sm: sessionManager, sdk: sdk }, activeSession, text, options);
|
|
421
409
|
} catch (error) {
|
|
422
410
|
error.sessionId = sessionReference(activeSession);
|
|
423
411
|
throw error;
|
|
@@ -456,7 +444,7 @@ function attachHomeChat(deps) {
|
|
|
456
444
|
}
|
|
457
445
|
return true;
|
|
458
446
|
}
|
|
459
|
-
if (msg.type !== "issue_reference_resolve" && msg.type !== "home_debates_list" && msg.type !== "home_clay_ask" && msg.type !== "home_clay_session_resolve" && msg.type !== "home_clay_log_resolve" && msg.type !== "home_mate_present" && msg.type !== "home_mate_open" && msg.type !== "home_mate_sessions_list" && msg.type !== "home_mate_session_open" && msg.type !== "home_mate_send" && msg.type !== "home_mate_new_session" && msg.type !== "home_mate_debate_plan" && msg.type !== "home_debate_proposal_response" && msg.type !== "home_mcp_import_response" && msg.type !== "home_debate_question_response" && msg.type !== "home_debate_control" && msg.type !== "home_mate_creation_plan" && msg.type !== "home_mate_creation_question_response" && msg.type !== "home_mate_creation_proposal_response" && msg.type !== "home_mate_close" && msg.type !== "home_mate_memory_list" && msg.type !== "home_mate_knowledge_list" && msg.type !== "home_mate_models_get" && msg.type !== "home_mate_model_set") {
|
|
447
|
+
if (msg.type !== "issue_reference_resolve" && msg.type !== "home_debates_list" && msg.type !== "home_clay_ask" && msg.type !== "home_clay_session_resolve" && msg.type !== "home_clay_log_resolve" && msg.type !== "home_mate_present" && msg.type !== "home_mate_open" && msg.type !== "home_mate_sessions_list" && msg.type !== "home_mate_session_open" && msg.type !== "home_mate_send" && msg.type !== "home_mate_new_session" && msg.type !== "home_mate_debate_plan" && msg.type !== "home_debate_proposal_response" && msg.type !== "home_mcp_import_response" && msg.type !== "home_mate_question_response" && msg.type !== "home_debate_question_response" && msg.type !== "home_debate_control" && msg.type !== "home_mate_creation_plan" && msg.type !== "home_mate_creation_question_response" && msg.type !== "home_mate_creation_proposal_response" && msg.type !== "home_mate_close" && msg.type !== "home_mate_memory_list" && msg.type !== "home_mate_knowledge_list" && msg.type !== "home_mate_models_get" && msg.type !== "home_mate_model_set") {
|
|
460
448
|
return false;
|
|
461
449
|
}
|
|
462
450
|
|
|
@@ -481,7 +469,7 @@ function attachHomeChat(deps) {
|
|
|
481
469
|
return true;
|
|
482
470
|
}
|
|
483
471
|
|
|
484
|
-
if (msg.type === "home_mcp_import_response") { homeMcpConsent.respond(ws, userId, msg); return true; }
|
|
472
|
+
if (msg.type === "home_mcp_import_response" || msg.type === "home_mate_question_response") { homeMcpConsent.respond(ws, userId, msg); return true; }
|
|
485
473
|
if (msg.type === "issue_reference_resolve") { resolveIssueLink(ws, msg); return true; }
|
|
486
474
|
if (homeDebates.handle(ws, userId, msg)) return true;
|
|
487
475
|
if (msg.type === "home_clay_ask") { homeClayEntry.start(ws, userId, msg); return true; }
|
|
@@ -18,6 +18,7 @@ var INITIATION_PROMPT = [
|
|
|
18
18
|
"Before the user answers again, your first and only action must be the next most useful AskUserQuestion. Before presenting it, do not use Bash, Read, Glob, Grep, workspace history, general knowledge, or any other inspection tool, and do not add a preface.",
|
|
19
19
|
"When that tool returns the user's answer, continue the interview normally. Inspect only bounded context that is directly relevant to what the user supplied, and do not bulk-read knowledge files.",
|
|
20
20
|
"Then explore the working relationship, recurring activities and context, communication preferences, autonomy, boundaries, and escalation behavior. Use grounded choices only when helpful. Every three to five exchanges, briefly summarize what you understand inside the next question and let the user correct it.",
|
|
21
|
+
"If the user explicitly asks the future Mate to send a scheduled message, collect the message and exact local date/time, timezone, and repetition only through one focused structured question at a time. Treat it as optional schedule intent: never create or register a job during this interview; include it only in propose_mate after reviewing it with the user.",
|
|
21
22
|
"Choose the Mate's name last. Do not write files and do not create or update a Mate directly.",
|
|
22
23
|
"When the identity is complete, end only by calling propose_mate with a concise public bio, structured preferences, and a complete first-person identityMarkdown. Do not emit a normal assistant preface or conclusion.",
|
|
23
24
|
].join("\n");
|
|
@@ -4,10 +4,10 @@ function attachHomeMcpConsent(ctx) {
|
|
|
4
4
|
var found = ctx.findMateProject(userId, msg.mateId || null, false);
|
|
5
5
|
var session = found && ctx.resolveHomeSession(found, userId, msg.sessionId);
|
|
6
6
|
var tap = ctx.messageTap(ws, msg), pending = session && session.pendingAskUser && session.pendingAskUser[msg.toolId];
|
|
7
|
-
if (!ctx.isActorLive(ws, userId) || !found || !session || !tap || tap.mateSlug !== found.slug || tap.sessionId !== session.localId || (tap.requestId || null) !== (msg.requestId || null) || !pending || !pending.request || pending.request.source
|
|
8
|
-
ctx.sendError(ws, found && found.mate.id, 'This owner
|
|
7
|
+
if (!ctx.isActorLive(ws, userId) || !found || !session || !tap || tap.mateSlug !== found.slug || tap.sessionId !== session.localId || (tap.requestId || null) !== (msg.requestId || null) || !pending || !pending.request || ['mcp-import', 'mate-schedule', 'mcp_fallback', 'product-control'].indexOf(pending.request.source) < 0) {
|
|
8
|
+
ctx.sendError(ws, found && found.mate.id, 'This owner question is unavailable or expired.', msg.requestId || null, msg.sessionId || null, 'question_expired', { toolId: msg.toolId }); return;
|
|
9
9
|
}
|
|
10
|
-
if (!found.ctx.handleHomeAskUserResponse({ type: 'ask_user_response', toolId: msg.toolId, answers: msg.answers || {} }, session, ws)) ctx.sendError(ws, found.mate.id, 'This
|
|
10
|
+
if (!found.ctx.handleHomeAskUserResponse({ type: 'ask_user_response', toolId: msg.toolId, answers: msg.answers || {} }, session, ws)) ctx.sendError(ws, found.mate.id, 'This question could not be submitted.', msg.requestId || null, msg.sessionId, 'question_refused', { toolId: msg.toolId });
|
|
11
11
|
}
|
|
12
12
|
return { respond: respond };
|
|
13
13
|
}
|
package/lib/server-mates.js
CHANGED
|
@@ -74,12 +74,26 @@ function attachMates(ctx) {
|
|
|
74
74
|
var mateSlug = "mate-" + mate.id;
|
|
75
75
|
var mateName = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
|
|
76
76
|
var registered = false;
|
|
77
|
+
var boundScheduleJob = null;
|
|
77
78
|
var favorites;
|
|
78
79
|
try {
|
|
79
80
|
registered = addProject(mateDir, mateSlug, mateName, null, mate.createdBy || effectiveUserId, null, { isMate: true, mateDisplayName: mateName }) !== false;
|
|
80
81
|
if (!registered) throw new Error("The Mate project could not be registered.");
|
|
82
|
+
var intent = mate.seedData && mate.seedData.scheduleIntent;
|
|
83
|
+
if (intent && intent.message && intent.scheduleRule) {
|
|
84
|
+
var targetProject = projects.get(mateSlug);
|
|
85
|
+
var targetManager = targetProject && targetProject.getSessionManager();
|
|
86
|
+
if (!targetProject || !targetManager || typeof targetProject.bindMateSchedule !== "function") throw new Error("The Mate schedule could not be bound to the new Mate.");
|
|
87
|
+
var targetSession = targetManager.createSessionRaw({ ownerId: users.isMultiUser() ? effectiveUserId : null, vendor: mate.vendor, model: mate.model });
|
|
88
|
+
boundScheduleJob = targetProject.bindMateSchedule(targetSession, intent, "creation-" + mate.id);
|
|
89
|
+
if (!boundScheduleJob) throw new Error("The Mate schedule could not be persisted.");
|
|
90
|
+
mates.updateMate(mateCtx, mate.id, { seedData: Object.assign({}, mate.seedData, { scheduleIntent: null }) });
|
|
91
|
+
}
|
|
81
92
|
favorites = users.addDmFavorite(effectiveUserId, mate.id);
|
|
82
93
|
} catch (error) {
|
|
94
|
+
if (targetSession && targetSession.mateSchedules && targetProject && typeof targetProject.cancelMateSchedule === "function") {
|
|
95
|
+
try { targetSession.mateSchedules.forEach(function (record) { targetProject.cancelMateSchedule(targetSession, record); }); } catch (cancelError) {}
|
|
96
|
+
}
|
|
83
97
|
if (registered) try { removeProject(mateSlug); } catch (removeError) {}
|
|
84
98
|
try { mates.deleteMate(mateCtx, mate.id); } catch (deleteError) {}
|
|
85
99
|
throw error;
|
|
@@ -97,6 +111,10 @@ function attachMates(ctx) {
|
|
|
97
111
|
return mate;
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
var lifecycle = require("./mate-lifecycle-service").attachMateLifecycleService({
|
|
115
|
+
users: users, mates: mates, projects: projects, removeProject: removeProject, refresh: refreshTeamSections,
|
|
116
|
+
});
|
|
117
|
+
|
|
100
118
|
// --- Mate message handlers ---
|
|
101
119
|
|
|
102
120
|
function handleMessage(ws, msg) {
|
|
@@ -168,7 +186,7 @@ function attachMates(ctx) {
|
|
|
168
186
|
for (var mi = 0; mi < mateList.length; mi++) {
|
|
169
187
|
var m = mateList[mi];
|
|
170
188
|
var mSlug = "mate-" + m.id;
|
|
171
|
-
if (!projects.has(mSlug)) {
|
|
189
|
+
if (!m.lifecycleDeletion && !projects.has(mSlug)) {
|
|
172
190
|
var mDir = mates.getMateDir(mateCtx5, m.id);
|
|
173
191
|
fs.mkdirSync(mDir, { recursive: true });
|
|
174
192
|
var mName = (m.profile && m.profile.displayName) || m.name || "New Mate";
|
|
@@ -200,45 +218,9 @@ function attachMates(ctx) {
|
|
|
200
218
|
|
|
201
219
|
if (msg.type === "mate_delete") {
|
|
202
220
|
if (!msg.mateId) return true;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (mateToDelete && mateToDelete.builtinKey) {
|
|
207
|
-
users.addDeletedBuiltinKey(userId, mateToDelete.builtinKey);
|
|
208
|
-
}
|
|
209
|
-
var result = mates.deleteMate(mateCtx6, msg.mateId);
|
|
210
|
-
if (result.error) {
|
|
211
|
-
ws.send(JSON.stringify({ type: "mate_error", error: result.error }));
|
|
212
|
-
} else {
|
|
213
|
-
removeProject("mate-" + msg.mateId);
|
|
214
|
-
// Build updated available builtins list
|
|
215
|
-
var builtinDefs3 = require("./builtin-mates");
|
|
216
|
-
var missingKeys3 = mates.getMissingBuiltinKeys(mateCtx6);
|
|
217
|
-
var availableBuiltins3 = [];
|
|
218
|
-
for (var abk3 = 0; abk3 < missingKeys3.length; abk3++) {
|
|
219
|
-
var bDef3 = builtinDefs3.getBuiltinByKey(missingKeys3[abk3]);
|
|
220
|
-
if (bDef3) {
|
|
221
|
-
availableBuiltins3.push({
|
|
222
|
-
key: bDef3.key,
|
|
223
|
-
displayName: bDef3.displayName,
|
|
224
|
-
bio: bDef3.bio,
|
|
225
|
-
avatarCustom: bDef3.avatarCustom || "",
|
|
226
|
-
avatarStyle: bDef3.avatarStyle || "imprint",
|
|
227
|
-
avatarColor: bDef3.avatarColor || "",
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
ws.send(JSON.stringify({ type: "mate_deleted", mateId: msg.mateId, availableBuiltins: availableBuiltins3 }));
|
|
232
|
-
// Broadcast to all clients so strips update
|
|
233
|
-
projects.forEach(function (pCtx) {
|
|
234
|
-
pCtx.forEachClient(function (otherWs) {
|
|
235
|
-
if (otherWs === ws) return;
|
|
236
|
-
if (users.isMultiUser() && (!otherWs._clayUser || otherWs._clayUser.id !== userId)) return;
|
|
237
|
-
if (otherWs.readyState !== 1) return;
|
|
238
|
-
otherWs.send(JSON.stringify({ type: "mate_deleted", mateId: msg.mateId, availableBuiltins: availableBuiltins3 }));
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
}
|
|
221
|
+
lifecycle.remove(userId, msg.mateId, null, ws).then(function () {}, function (error) {
|
|
222
|
+
ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, error: error.message }));
|
|
223
|
+
});
|
|
242
224
|
return true;
|
|
243
225
|
}
|
|
244
226
|
|
|
@@ -303,33 +285,8 @@ function attachMates(ctx) {
|
|
|
303
285
|
ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, error: "Use the Mate model selector to change this setting." }));
|
|
304
286
|
return true;
|
|
305
287
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
try { updated = mates.updateMate(mateCtx7, msg.mateId, msg.updates); }
|
|
309
|
-
catch (error) {
|
|
310
|
-
ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, requestId: msg.requestId, error: "Could not save this Mate. Your changes were not acknowledged; try again." }));
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
|
-
if (updated && updated.error) {
|
|
314
|
-
ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, requestId: msg.requestId, error: updated.error }));
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
if (updated) {
|
|
318
|
-
ws.send(JSON.stringify({ type: "mate_updated", mate: updated, requestId: msg.requestId }));
|
|
319
|
-
// Broadcast update
|
|
320
|
-
projects.forEach(function (pCtx) {
|
|
321
|
-
pCtx.forEachClient(function (otherWs) {
|
|
322
|
-
if (otherWs === ws) return;
|
|
323
|
-
if (users.isMultiUser() && (!otherWs._clayUser || otherWs._clayUser.id !== userId)) return;
|
|
324
|
-
if (otherWs.readyState !== 1) return;
|
|
325
|
-
otherWs.send(JSON.stringify({ type: "mate_updated", mate: updated }));
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
// Re-enforce team sections across all mate projects so roster stays current
|
|
329
|
-
refreshTeamSections(mateCtx7);
|
|
330
|
-
} else {
|
|
331
|
-
ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, requestId: msg.requestId, error: "Mate not found" }));
|
|
332
|
-
}
|
|
288
|
+
try { lifecycle.update(userId, msg.mateId, msg.updates, ws, msg.requestId); }
|
|
289
|
+
catch (error) { ws.send(JSON.stringify({ type: "mate_error", mateId: msg.mateId, requestId: msg.requestId, error: error.message })); }
|
|
333
290
|
return true;
|
|
334
291
|
}
|
|
335
292
|
|
|
@@ -338,6 +295,7 @@ function attachMates(ctx) {
|
|
|
338
295
|
|
|
339
296
|
return {
|
|
340
297
|
handleMessage: handleMessage,
|
|
298
|
+
lifecycle: lifecycle,
|
|
341
299
|
scheduleRegistryRefresh: scheduleRegistryRefresh,
|
|
342
300
|
createReadyMateFromInterview: createReadyMateFromInterview,
|
|
343
301
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
// Only authenticated application sockets enumerated by the server are targets.
|
|
3
|
+
function attachOwnerUi(ctx) {
|
|
4
|
+
var identities = new WeakMap(), capabilities = new WeakMap(), deliveries = new Map();
|
|
5
|
+
function alive(ws, owner) {
|
|
6
|
+
return ws && ws.readyState === 1 && (!ws._clayAuthCheck || ws._clayAuthCheck()) &&
|
|
7
|
+
(ctx.isMultiUser() ? !!owner && ws._clayUser && ws._clayUser.id === owner && !!ctx.findUser(owner) : !ws._clayUser);
|
|
8
|
+
}
|
|
9
|
+
function sockets(owner) {
|
|
10
|
+
var all = new Map();
|
|
11
|
+
ctx.forEachClient(function (ws, slug) {
|
|
12
|
+
if (!alive(ws, owner) || all.has(ws)) return;
|
|
13
|
+
if (!identities.has(ws)) identities.set(ws, 'viewer:' + crypto.randomUUID());
|
|
14
|
+
all.set(ws, { ws: ws, ref: identities.get(ws), slug: slug || null, sessionId: ws._clayActiveSession || null,
|
|
15
|
+
supported: capabilities.get(ws) === 1 && !ws._clayPane });
|
|
16
|
+
});
|
|
17
|
+
return Array.from(all.values());
|
|
18
|
+
}
|
|
19
|
+
function get(owner, ref) { return sockets(owner).find(function (item) { return item.ref === ref; }); }
|
|
20
|
+
function inspect(owner, ref) {
|
|
21
|
+
var viewer = get(owner, ref);
|
|
22
|
+
if (!viewer) throw new Error('The exact owner UI connection is absent or revoked.');
|
|
23
|
+
return viewer;
|
|
24
|
+
}
|
|
25
|
+
function check(owner, original) {
|
|
26
|
+
var live = inspect(owner, original.ref);
|
|
27
|
+
if (live.ws !== original.ws || live.slug !== original.slug || live.sessionId !== original.sessionId || live.supported !== original.supported) throw new Error('The selected viewer context changed. Inspect again.');
|
|
28
|
+
return live;
|
|
29
|
+
}
|
|
30
|
+
function deliver(owner, original, target, revalidate) {
|
|
31
|
+
revalidate(); var viewer = check(owner, original);
|
|
32
|
+
if (!viewer.supported) return { status: 'unsupported', delivered: false, acknowledged: false, visible: 'unknown' };
|
|
33
|
+
if (deliveries.size >= 100) {
|
|
34
|
+
deliveries.forEach(function (item, key) { if (Date.now() - item.created > 300000) deliveries.delete(key); });
|
|
35
|
+
if (deliveries.size >= 100) throw new Error('UI delivery capacity is full; wait for earlier attempts to expire.');
|
|
36
|
+
}
|
|
37
|
+
var id = 'navigation:' + crypto.randomUUID();
|
|
38
|
+
var record = { owner: owner, viewer: viewer, target: target, revalidate: revalidate, created: Date.now(), claimed: false,
|
|
39
|
+
result: { status: 'delivered', deliveryId: id, delivered: false, acknowledged: false, visible: 'unknown' } };
|
|
40
|
+
deliveries.set(id, record);
|
|
41
|
+
try {
|
|
42
|
+
viewer.ws.send(JSON.stringify({ type: 'clay_navigation_offer', deliveryId: id }));
|
|
43
|
+
if (!alive(viewer.ws, owner)) throw new Error('Viewer authorization or connection ended during delivery.');
|
|
44
|
+
record.result.delivered = true;
|
|
45
|
+
} catch (error) { record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; }
|
|
46
|
+
return Object.assign({}, record.result);
|
|
47
|
+
}
|
|
48
|
+
function read(owner, id) {
|
|
49
|
+
var record = deliveries.get(id);
|
|
50
|
+
if (!record || record.owner !== owner) throw new Error('No delivery exists for this owner.');
|
|
51
|
+
record.revalidate();
|
|
52
|
+
return Object.assign({}, record.result);
|
|
53
|
+
}
|
|
54
|
+
function handleMessage(ws, msg) {
|
|
55
|
+
if (['clay_ui_ready', 'clay_navigation_claim', 'clay_navigation_ack'].indexOf(msg.type) < 0) return false;
|
|
56
|
+
var owner = ctx.isMultiUser() ? ws._clayUser && ws._clayUser.id : null;
|
|
57
|
+
if (!sockets(owner).some(function (item) { return item.ws === ws; })) return true;
|
|
58
|
+
if (msg.type === 'clay_ui_ready') { if (msg.version === 1) capabilities.set(ws, 1); return true; }
|
|
59
|
+
var record = deliveries.get(msg.deliveryId);
|
|
60
|
+
if (!record || record.viewer.ws !== ws || record.owner !== owner || Date.now() - record.created > 30000) return true;
|
|
61
|
+
try {
|
|
62
|
+
record.revalidate();
|
|
63
|
+
if (msg.type === 'clay_navigation_claim') {
|
|
64
|
+
if (record.claimed) return true;
|
|
65
|
+
check(owner, record.viewer); record.claimed = true;
|
|
66
|
+
ws.send(JSON.stringify({ type: 'clay_navigation_apply', deliveryId: msg.deliveryId, target: record.target }));
|
|
67
|
+
} else if (record.claimed && !record.result.acknowledged && ['accepted', 'refused', 'failed'].indexOf(msg.status) >= 0) {
|
|
68
|
+
record.result.acknowledged = true;
|
|
69
|
+
// Accepted only confirms invocation of the existing navigator, never rendering.
|
|
70
|
+
record.result.clientStatus = msg.status;
|
|
71
|
+
}
|
|
72
|
+
} catch (error) { record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; }
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return { list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage };
|
|
76
|
+
}
|
|
77
|
+
module.exports = { attachOwnerUi: attachOwnerUi };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Directory pickers and project registration must not become content-access
|
|
2
|
+
// bypasses through another spelling, symlink or nested directory.
|
|
3
|
+
var fs = require("fs");
|
|
4
|
+
var path = require("path");
|
|
5
|
+
function inside(root, target) {
|
|
6
|
+
var relative = path.relative(root, target);
|
|
7
|
+
return relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
|
|
8
|
+
}
|
|
9
|
+
function attachPathAccess(ctx) {
|
|
10
|
+
return function (userId, requested) {
|
|
11
|
+
if (!ctx.users.isMultiUser()) return true;
|
|
12
|
+
var user = userId && ctx.users.findUserById(userId);
|
|
13
|
+
if (!user) return false;
|
|
14
|
+
var target;
|
|
15
|
+
try { target = fs.realpathSync(requested); } catch (error) { return false; }
|
|
16
|
+
var allowed = false;
|
|
17
|
+
var denied = false;
|
|
18
|
+
ctx.projects.forEach(function (project, slug) {
|
|
19
|
+
var status = project.getStatus();
|
|
20
|
+
var root;
|
|
21
|
+
try { root = fs.realpathSync(status.path); } catch (error) { return; }
|
|
22
|
+
if (inside(root, target) || inside(target, root)) {
|
|
23
|
+
if (!ctx.canAccess(userId, slug)) denied = true;
|
|
24
|
+
else if (inside(root, target)) allowed = true;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if (denied) return false;
|
|
28
|
+
if (ctx.osUsers) {
|
|
29
|
+
if (!user.linuxUser) return false;
|
|
30
|
+
try {
|
|
31
|
+
var identity = ctx.resolveIdentity(user.linuxUser);
|
|
32
|
+
return inside(fs.realpathSync(identity.home), target);
|
|
33
|
+
} catch (error) { return false; }
|
|
34
|
+
}
|
|
35
|
+
return allowed || ctx.users.getAllUsers().length === 1;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
module.exports = { attachPathAccess: attachPathAccess };
|