clay-server 5.2.0-beta.4 → 5.2.0-beta.6

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.
Files changed (301) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +1 -239
  6. package/lib/claude-jsonl-watcher.js +1 -204
  7. package/lib/clay-mate-chat-receipts.js +26 -0
  8. package/lib/clay-mate-chat.js +87 -0
  9. package/lib/clay-mate-knowledge-input.js +26 -0
  10. package/lib/clay-mate-knowledge.js +95 -0
  11. package/lib/clay-mate-lifecycle.js +95 -0
  12. package/lib/clay-product-control-input.js +33 -0
  13. package/lib/clay-product-control-tools.js +51 -0
  14. package/lib/clay-product-control.js +237 -0
  15. package/lib/clay-project-lifecycle.js +154 -0
  16. package/lib/clay-project-record-input.js +44 -0
  17. package/lib/clay-project-records.js +83 -0
  18. package/lib/clay-scheduling-input.js +21 -0
  19. package/lib/clay-scheduling-receipts.js +26 -0
  20. package/lib/clay-scheduling.js +119 -0
  21. package/lib/clay-screen-controls.js +96 -0
  22. package/lib/clay-skill-install.js +1 -33
  23. package/lib/clay-workspace-controls.js +83 -0
  24. package/lib/clay-workspace-input.js +17 -0
  25. package/lib/clay-workspace-receipts.js +26 -0
  26. package/lib/cli-sessions.js +1 -300
  27. package/lib/codex-defaults.js +1 -28
  28. package/lib/context-usage.js +3 -2
  29. package/lib/daemon-project-access.js +11 -0
  30. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  31. package/lib/daemon-project-lifecycle.js +262 -0
  32. package/lib/daemon-projects.js +1 -1
  33. package/lib/daemon.js +78 -276
  34. package/lib/default-ai-runtime.js +4 -8
  35. package/lib/email-mcp-server.js +1 -1
  36. package/lib/issues-service.js +29 -15
  37. package/lib/kiro-defaults.js +1 -22
  38. package/lib/knowledge-db-import.js +5 -4
  39. package/lib/knowledge-db-service.js +9 -6
  40. package/lib/knowledge-document-index.js +2 -1
  41. package/lib/knowledge-product-operations.js +46 -0
  42. package/lib/mate-creation-mcp-server.js +2 -0
  43. package/lib/mate-knowledge-canonical-access.js +10 -0
  44. package/lib/mate-knowledge-service.js +7 -4
  45. package/lib/mate-lifecycle-service.js +94 -0
  46. package/lib/mate-message-delivery.js +43 -0
  47. package/lib/mate-ready-creation.js +9 -1
  48. package/lib/mate-schedule-input.js +29 -0
  49. package/lib/mate-schedule-lifecycle.js +151 -0
  50. package/lib/mate-scheduling.js +103 -0
  51. package/lib/mates.js +4 -3
  52. package/lib/mcp-proxy.js +2 -32
  53. package/lib/mcp-query-bridge.js +1 -1
  54. package/lib/mcp-source-adapters.js +1 -91
  55. package/lib/mcp-source-context.js +1 -42
  56. package/lib/mcp-source-discovery.js +1 -169
  57. package/lib/mcp-source-plugins.js +1 -68
  58. package/lib/mcp-source-restrictions.js +1 -47
  59. package/lib/mcp-tool-policy.js +1 -26
  60. package/lib/notes-checklist.js +50 -0
  61. package/lib/notes.js +49 -3
  62. package/lib/os-user-diagnostics.js +2 -4
  63. package/lib/os-users.js +24 -70
  64. package/lib/project-admin-overview.js +51 -0
  65. package/lib/project-ask-user.js +4 -2
  66. package/lib/project-connection.js +8 -7
  67. package/lib/project-delegated-follow-up.js +2 -1
  68. package/lib/project-directory-claim.js +65 -0
  69. package/lib/project-file-operations.js +61 -0
  70. package/lib/project-filesystem.js +13 -87
  71. package/lib/project-full-access.js +23 -2
  72. package/lib/project-http.js +4 -4
  73. package/lib/project-knowledge.js +32 -18
  74. package/lib/project-lifecycle-operations.js +98 -0
  75. package/lib/project-lifecycle-process-witness.js +32 -0
  76. package/lib/project-lifecycle-runtime.js +50 -0
  77. package/lib/project-linux-access.js +22 -0
  78. package/lib/project-logs-canonical-clay.js +27 -0
  79. package/lib/project-logs-clay-query.js +16 -0
  80. package/lib/project-logs-schema.js +4 -2
  81. package/lib/project-logs-service.js +8 -7
  82. package/lib/project-logs.js +1 -0
  83. package/lib/project-loop.js +1 -0
  84. package/lib/project-mate-chat-control.js +55 -0
  85. package/lib/project-mate-creation-proposal.js +18 -3
  86. package/lib/project-mcp.js +2 -2
  87. package/lib/project-models.js +14 -31
  88. package/lib/project-notifications.js +74 -16
  89. package/lib/project-ownership.js +77 -0
  90. package/lib/project-pair-result-delivery.js +14 -3
  91. package/lib/project-pair-usage.js +12 -13
  92. package/lib/project-pending-message-queue.js +4 -1
  93. package/lib/project-product-control.js +87 -0
  94. package/lib/project-product-session-queue.js +46 -0
  95. package/lib/project-push-access.js +28 -0
  96. package/lib/project-scheduled-message-queue.js +30 -0
  97. package/lib/project-scheduled-messages.js +63 -64
  98. package/lib/project-scheduled-task-control.js +83 -0
  99. package/lib/project-scheduled-tasks.js +13 -2
  100. package/lib/project-scheduling-access.js +10 -0
  101. package/lib/project-session-actions.js +65 -0
  102. package/lib/project-session-delete.js +8 -1
  103. package/lib/project-session-github.js +47 -12
  104. package/lib/project-session-history-actions.js +167 -0
  105. package/lib/project-session-pair.js +1 -1
  106. package/lib/project-session-runtime-actions.js +64 -0
  107. package/lib/project-session-spawn.js +2 -3
  108. package/lib/project-session-vendor.js +55 -0
  109. package/lib/project-sessions.js +82 -267
  110. package/lib/project-user-message.js +31 -1
  111. package/lib/project-vendor-login.js +6 -36
  112. package/lib/project-workspace-operations.js +126 -0
  113. package/lib/project-workspace-query.js +9 -2
  114. package/lib/project.js +95 -3
  115. package/lib/public/app.js +1 -0
  116. package/lib/public/css/clay-tool-activity.css +51 -0
  117. package/lib/public/css/home-mate-creation.css +5 -0
  118. package/lib/public/css/messages.css +6 -2
  119. package/lib/public/css/overlays.css +4 -2
  120. package/lib/public/css/project-admin-overview.css +13 -0
  121. package/lib/public/css/session-folders.css +11 -0
  122. package/lib/public/css/session-github.css +3 -0
  123. package/lib/public/css/sticky-notes.css +61 -1
  124. package/lib/public/index.html +1 -1
  125. package/lib/public/modules/admin-settings-projects.js +6 -2
  126. package/lib/public/modules/app-connection.js +8 -1
  127. package/lib/public/modules/app-favicon.js +11 -2
  128. package/lib/public/modules/app-header.js +4 -2
  129. package/lib/public/modules/app-loop-ui.js +2 -0
  130. package/lib/public/modules/app-message-router.js +1 -1
  131. package/lib/public/modules/app-messages.js +54 -8
  132. package/lib/public/modules/app-notifications.js +1 -2
  133. package/lib/public/modules/app-panels.js +61 -123
  134. package/lib/public/modules/app-projects.js +5 -0
  135. package/lib/public/modules/app-rate-limit.js +7 -27
  136. package/lib/public/modules/chat-render-runtime.js +3 -0
  137. package/lib/public/modules/clay-owner-navigation.js +27 -0
  138. package/lib/public/modules/clay-tool-activity.js +140 -0
  139. package/lib/public/modules/clay-tool-presentation.js +338 -0
  140. package/lib/public/modules/command-palette.js +1 -1
  141. package/lib/public/modules/default-ai.js +11 -3
  142. package/lib/public/modules/home-debate-planning.js +5 -5
  143. package/lib/public/modules/home-mate-chat.js +8 -6
  144. package/lib/public/modules/home-mate-creation.js +73 -1
  145. package/lib/public/modules/mcp-connection-view.js +1 -1
  146. package/lib/public/modules/permission-control.js +5 -4
  147. package/lib/public/modules/personal-settings-dialog.js +7 -1
  148. package/lib/public/modules/project-access-recovery.js +17 -0
  149. package/lib/public/modules/project-admin-overview.js +21 -0
  150. package/lib/public/modules/scheduler.js +2 -0
  151. package/lib/public/modules/session-create-form.js +10 -10
  152. package/lib/public/modules/session-folder-layout.js +1 -1
  153. package/lib/public/modules/session-folder-view-menu.js +9 -0
  154. package/lib/public/modules/session-github.js +65 -8
  155. package/lib/public/modules/settings-defaults.js +27 -28
  156. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  157. package/lib/public/modules/sticky-note-markdown.js +72 -6
  158. package/lib/public/modules/sticky-notes-actions.js +30 -0
  159. package/lib/public/modules/sticky-notes-browser.js +9 -2
  160. package/lib/public/modules/sticky-notes-card.js +23 -7
  161. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  162. package/lib/public/modules/sticky-notes-editor.js +108 -44
  163. package/lib/public/modules/sticky-notes-shared.js +50 -2
  164. package/lib/public/modules/sticky-notes-state.js +66 -0
  165. package/lib/public/modules/sticky-notes.js +34 -51
  166. package/lib/public/modules/thinking-lifecycle.js +33 -3
  167. package/lib/public/modules/thinking-view.js +23 -1
  168. package/lib/public/modules/tools.js +36 -7
  169. package/lib/public/modules/tui-grab.js +1 -2
  170. package/lib/public/modules/user-settings-appearance.js +10 -3
  171. package/lib/public/modules/user-settings-editors.js +1 -1
  172. package/lib/public/modules/user-settings-layout.js +11 -2
  173. package/lib/public/modules/user-settings-preferences.js +6 -2
  174. package/lib/public/modules/user-settings-profile.js +12 -2
  175. package/lib/public/modules/user-settings.js +3 -2
  176. package/lib/public/modules/vendor-login-browser.js +5 -3
  177. package/lib/public/modules/vendor-login.js +2 -2
  178. package/lib/public/modules/vendor-priority.js +15 -1
  179. package/lib/public/muse-avatar.svg +4 -0
  180. package/lib/public/style.css +2 -0
  181. package/lib/safe-bash-commands.js +3 -45
  182. package/lib/scheduled-task-records.js +3 -1
  183. package/lib/scheduler.js +2 -0
  184. package/lib/sdk-bridge.js +334 -403
  185. package/lib/sdk-foreground-admission.js +16 -0
  186. package/lib/sdk-mcp-descriptors.js +2 -37
  187. package/lib/sdk-message-processor.js +52 -17
  188. package/lib/sdk-quota-snapshot.js +1 -1
  189. package/lib/sdk-response-recovery.js +1 -0
  190. package/lib/sdk-worker.js +2 -677
  191. package/lib/server-admin.js +63 -16
  192. package/lib/server-dm.js +1 -1
  193. package/lib/server-global-ws.js +10 -20
  194. package/lib/server-home-chat-events.js +7 -3
  195. package/lib/server-home-chat.js +4 -16
  196. package/lib/server-home-mate-creation.js +1 -0
  197. package/lib/server-home-mcp-consent.js +3 -3
  198. package/lib/server-mates.js +25 -67
  199. package/lib/server-owner-ui.js +77 -0
  200. package/lib/server-path-access.js +38 -0
  201. package/lib/server-project-removal.js +22 -0
  202. package/lib/server.js +100 -30
  203. package/lib/session-config-transition.js +45 -0
  204. package/lib/session-folders.js +7 -2
  205. package/lib/session-fork-history.js +21 -0
  206. package/lib/session-github.js +9 -2
  207. package/lib/session-hygiene.js +3 -35
  208. package/lib/session-permission-mode.js +4 -3
  209. package/lib/session-skill-scope.js +11 -0
  210. package/lib/session-visibility.js +5 -2
  211. package/lib/sessions.js +27 -89
  212. package/lib/terminal-manager.js +52 -0
  213. package/lib/tool-llm.js +1 -1
  214. package/lib/tui-transcript-index.js +1 -132
  215. package/lib/users-permissions.js +2 -8
  216. package/lib/users.js +4 -0
  217. package/lib/vendor-login-browser.js +1 -11
  218. package/lib/workspace-query-access.js +4 -1
  219. package/lib/ws-schema.js +8 -2
  220. package/lib/yoke/acp-agent-profiles.js +5 -3
  221. package/lib/yoke/adapters/antigravity.js +3 -14
  222. package/lib/yoke/adapters/claude-query-contract.js +9 -0
  223. package/lib/yoke/adapters/claude.js +54 -50
  224. package/lib/yoke/adapters/codex-query-contract.js +24 -0
  225. package/lib/yoke/adapters/codex.js +53 -111
  226. package/lib/yoke/adapters/kiro.js +3 -1
  227. package/lib/yoke/adapters/muse-query-contract.js +7 -0
  228. package/lib/yoke/adapters/muse.js +122 -0
  229. package/lib/yoke/auth-events.js +17 -0
  230. package/lib/yoke/claude-hook-installer.js +267 -0
  231. package/lib/yoke/claude-jsonl-watcher.js +204 -0
  232. package/lib/yoke/claude-maintenance.js +55 -0
  233. package/lib/yoke/claude-permission-patterns.js +53 -0
  234. package/lib/yoke/claude-response-recovery.js +155 -0
  235. package/lib/yoke/claude-transcript-hygiene.js +34 -0
  236. package/lib/yoke/claude-worker-startup.js +28 -0
  237. package/lib/yoke/clay-skill-install.js +33 -0
  238. package/lib/yoke/cli-sessions.js +300 -0
  239. package/lib/yoke/codex-app-server.js +7 -12
  240. package/lib/yoke/codex-idle-recovery.js +81 -1
  241. package/lib/yoke/codex-recovery-admission.js +164 -0
  242. package/lib/yoke/configuration.js +13 -0
  243. package/lib/yoke/discovery.js +70 -0
  244. package/lib/yoke/errors.js +8 -0
  245. package/lib/yoke/host-environment.js +7 -0
  246. package/lib/yoke/index.js +9 -51
  247. package/lib/yoke/interface.js +116 -0
  248. package/lib/yoke/kiro-acp-server.js +5 -5
  249. package/lib/yoke/legacy-sessions.js +89 -0
  250. package/lib/yoke/login-protocol.js +25 -0
  251. package/lib/yoke/maintenance.js +12 -0
  252. package/lib/yoke/mcp-proxy.js +32 -0
  253. package/lib/yoke/mcp-source-adapters.js +91 -0
  254. package/lib/yoke/mcp-source-context.js +42 -0
  255. package/lib/yoke/mcp-source-discovery.js +169 -0
  256. package/lib/yoke/mcp-source-plugins.js +68 -0
  257. package/lib/yoke/mcp-source-restrictions.js +47 -0
  258. package/lib/yoke/mcp-tool-policy-normalize.js +28 -0
  259. package/lib/yoke/model-preferences.js +6 -0
  260. package/lib/yoke/muse-artifacts.js +36 -0
  261. package/lib/yoke/muse-controls.js +50 -0
  262. package/lib/yoke/muse-input.js +64 -0
  263. package/lib/yoke/muse-items.js +157 -0
  264. package/lib/yoke/muse-policy.js +52 -0
  265. package/lib/yoke/muse-query.js +428 -0
  266. package/lib/yoke/muse-runtime.js +211 -0
  267. package/lib/yoke/muse-services.js +79 -0
  268. package/lib/yoke/muse-skills.js +79 -0
  269. package/lib/yoke/muse-turns.js +71 -0
  270. package/lib/yoke/options/acp.js +2 -0
  271. package/lib/yoke/options/antigravity.js +2 -0
  272. package/lib/yoke/options/claude.js +21 -0
  273. package/lib/yoke/options/codex-defaults.js +29 -0
  274. package/lib/yoke/options/codex-settings.js +21 -0
  275. package/lib/yoke/options/codex.js +6 -0
  276. package/lib/yoke/options/kiro-defaults.js +22 -0
  277. package/lib/yoke/options/kiro.js +5 -0
  278. package/lib/yoke/options/muse.js +5 -0
  279. package/lib/yoke/provider-contract.js +5 -0
  280. package/lib/yoke/providers/antigravity-executable.js +8 -0
  281. package/lib/yoke/providers/claude-executable.js +40 -0
  282. package/lib/yoke/providers/claude-model-descriptions.js +14 -0
  283. package/lib/yoke/providers/claude-process.js +97 -0
  284. package/lib/yoke/providers/claude-provision.js +75 -0
  285. package/lib/yoke/providers/codex-errors.js +8 -0
  286. package/lib/yoke/providers/muse-executable.js +17 -0
  287. package/lib/yoke/provisioning.js +6 -0
  288. package/lib/yoke/query-options.js +10 -0
  289. package/lib/yoke/response-recovery.js +41 -0
  290. package/lib/yoke/sdk-mcp-descriptors.js +37 -0
  291. package/lib/yoke/sdk-server-factory.js +7 -0
  292. package/lib/yoke/session-history.js +8 -0
  293. package/lib/yoke/session-tool-transport.js +34 -13
  294. package/lib/yoke/settings.js +23 -0
  295. package/lib/yoke/source-locations.js +4 -0
  296. package/lib/yoke/title-query.js +18 -7
  297. package/lib/yoke/tui-transcript-index.js +132 -0
  298. package/lib/yoke/usage.js +48 -0
  299. package/lib/yoke/user-input.js +13 -0
  300. package/lib/yoke/vendor-registry.js +81 -1
  301. package/package.json +3 -2
@@ -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
- var parsed = JSON.parse(body);
226
- var result = users.updateLinuxUser(targetUserId, parsed.linuxUser || null);
227
- if (result.error) {
228
- res.writeHead(400, { "Content-Type": "application/json" });
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
- if (!mu || mu.role !== "admin") {
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":"Admin access required"}');
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 || null;
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
- if (!Array.isArray(data.allowedUsers)) {
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)) {
@@ -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 (osUsers && ws._clayUser && ws._clayUser.role !== "admin") {
114
- if (!ws._clayUser.linuxUser) {
115
- sendTo(ws, { type: "add_project_result", ok: false, error: "No Linux user assigned" });
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, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
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, { type: "add_project_result", ok: createResult.ok, slug: createResult.slug, error: createResult.error });
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' && event.source === 'mcp-import') {
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 import consent is no longer active. Ask for a new scan and review.' });
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) {
@@ -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
- if (!activeSession.isProcessing) {
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 !== 'mcp-import') {
8
- ctx.sendError(ws, found && found.mate.id, 'This owner import consent is unavailable or expired.', msg.requestId || null, msg.sessionId || null, 'question_expired', { toolId: msg.toolId }); return;
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 import consent could not be submitted. No import was authorized.', msg.requestId || null, msg.sessionId, 'question_refused', { toolId: msg.toolId });
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
  }
@@ -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
- var mateCtx6 = mates.buildMateCtx(userId);
204
- // Track deleted built-in mate key so it doesn't auto-recreate
205
- var mateToDelete = mates.getMate(mateCtx6, msg.mateId);
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
- var mateCtx7 = mates.buildMateCtx(userId);
307
- var updated;
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 };