clay-server 5.2.0-beta.3 → 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.
Files changed (339) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +19 -16
  3. package/lib/bundled-skills/clay-agent-browser/SKILL.md +6 -2
  4. package/lib/canonical-clay-record-authority.js +15 -0
  5. package/lib/canonical-clay-scheduling-authority.js +19 -0
  6. package/lib/claude-hook-installer.js +28 -0
  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-workspace-controls.js +83 -0
  23. package/lib/clay-workspace-input.js +17 -0
  24. package/lib/clay-workspace-receipts.js +26 -0
  25. package/lib/daemon-project-access.js +11 -0
  26. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  27. package/lib/daemon-project-lifecycle.js +262 -0
  28. package/lib/daemon-projects.js +1 -1
  29. package/lib/daemon.js +73 -221
  30. package/lib/issues-service.js +29 -15
  31. package/lib/knowledge-db-import.js +5 -4
  32. package/lib/knowledge-db-service.js +9 -6
  33. package/lib/knowledge-document-index.js +2 -1
  34. package/lib/knowledge-file-store.js +7 -1
  35. package/lib/knowledge-product-operations.js +46 -0
  36. package/lib/mate-avatar-catalog.js +12 -0
  37. package/lib/mate-avatar-migration.js +41 -0
  38. package/lib/mate-creation-mcp-server.js +2 -0
  39. package/lib/mate-knowledge-canonical-access.js +10 -0
  40. package/lib/mate-knowledge-service.js +7 -4
  41. package/lib/mate-lifecycle-service.js +94 -0
  42. package/lib/mate-message-delivery.js +43 -0
  43. package/lib/mate-ready-creation.js +9 -1
  44. package/lib/mate-schedule-input.js +29 -0
  45. package/lib/mate-schedule-lifecycle.js +151 -0
  46. package/lib/mate-scheduling.js +103 -0
  47. package/lib/mates.js +18 -6
  48. package/lib/mcp-agent-tools.js +117 -0
  49. package/lib/mcp-connection-store.js +119 -15
  50. package/lib/mcp-import-credentials.js +31 -0
  51. package/lib/mcp-local-transport.js +13 -0
  52. package/lib/mcp-oauth.js +8 -6
  53. package/lib/mcp-query-bridge.js +30 -0
  54. package/lib/mcp-remote-connections.js +74 -20
  55. package/lib/mcp-source-adapters.js +91 -0
  56. package/lib/mcp-source-context.js +42 -0
  57. package/lib/mcp-source-discovery.js +169 -0
  58. package/lib/mcp-source-plugins.js +68 -0
  59. package/lib/mcp-source-restrictions.js +47 -0
  60. package/lib/mcp-tool-policy.js +34 -0
  61. package/lib/notes-checklist.js +50 -0
  62. package/lib/notes.js +49 -3
  63. package/lib/os-users.js +20 -5
  64. package/lib/project-admin-overview.js +51 -0
  65. package/lib/project-ask-user.js +8 -4
  66. package/lib/project-debate.js +1 -0
  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-knowledge.js +32 -18
  73. package/lib/project-lifecycle-operations.js +98 -0
  74. package/lib/project-lifecycle-process-witness.js +32 -0
  75. package/lib/project-lifecycle-runtime.js +50 -0
  76. package/lib/project-linux-access.js +22 -0
  77. package/lib/project-logs-canonical-clay.js +27 -0
  78. package/lib/project-logs-clay-query.js +16 -0
  79. package/lib/project-logs-schema.js +4 -2
  80. package/lib/project-logs-service.js +8 -7
  81. package/lib/project-logs.js +1 -0
  82. package/lib/project-loop.js +1 -0
  83. package/lib/project-mate-chat-control.js +55 -0
  84. package/lib/project-mate-creation-proposal.js +18 -3
  85. package/lib/project-mate-interaction.js +8 -0
  86. package/lib/project-mcp.js +54 -16
  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-pending-message-queue.js +1 -0
  92. package/lib/project-product-control.js +87 -0
  93. package/lib/project-product-session-queue.js +46 -0
  94. package/lib/project-push-access.js +28 -0
  95. package/lib/project-scheduled-message-queue.js +30 -0
  96. package/lib/project-scheduled-messages.js +63 -64
  97. package/lib/project-scheduled-task-control.js +83 -0
  98. package/lib/project-scheduled-tasks.js +13 -2
  99. package/lib/project-scheduling-access.js +10 -0
  100. package/lib/project-session-actions.js +65 -0
  101. package/lib/project-session-delete.js +8 -1
  102. package/lib/project-session-document.js +8 -3
  103. package/lib/project-session-github.js +63 -19
  104. package/lib/project-session-history-actions.js +165 -0
  105. package/lib/project-session-linear.js +14 -6
  106. package/lib/project-session-notes.js +7 -3
  107. package/lib/project-session-runtime-actions.js +63 -0
  108. package/lib/project-session-vendor.js +54 -0
  109. package/lib/project-sessions.js +68 -241
  110. package/lib/project-shared-browser.js +56 -12
  111. package/lib/project-user-message.js +30 -1
  112. package/lib/project-workspace-operations.js +126 -0
  113. package/lib/project-workspace-query.js +9 -2
  114. package/lib/project.js +144 -37
  115. package/lib/public/app.js +5 -0
  116. package/lib/public/appearance-bootstrap.js +14 -0
  117. package/lib/public/css/clay-tool-activity.css +51 -0
  118. package/lib/public/css/clay-workspace.css +29 -0
  119. package/lib/public/css/home-debate-planning.css +2 -0
  120. package/lib/public/css/home-mate-creation.css +5 -0
  121. package/lib/public/css/mate-avatar-gallery.css +12 -0
  122. package/lib/public/css/mate-conversation.css +10 -5
  123. package/lib/public/css/overlays.css +4 -2
  124. package/lib/public/css/project-admin-overview.css +13 -0
  125. package/lib/public/css/session-folders.css +11 -0
  126. package/lib/public/css/session-github.css +46 -1
  127. package/lib/public/css/shared-browser.css +35 -1
  128. package/lib/public/css/sticky-notes.css +61 -1
  129. package/lib/public/index.html +11 -5
  130. package/lib/public/mate-avatars/B01.png +0 -0
  131. package/lib/public/mate-avatars/B02.png +0 -0
  132. package/lib/public/mate-avatars/B03.png +0 -0
  133. package/lib/public/mate-avatars/B04.png +0 -0
  134. package/lib/public/mate-avatars/B05.png +0 -0
  135. package/lib/public/mate-avatars/B06.png +0 -0
  136. package/lib/public/mate-avatars/B07.png +0 -0
  137. package/lib/public/mate-avatars/B08.png +0 -0
  138. package/lib/public/mate-avatars/B09.png +0 -0
  139. package/lib/public/mate-avatars/B10.png +0 -0
  140. package/lib/public/mate-avatars/B11.png +0 -0
  141. package/lib/public/mate-avatars/B12.png +0 -0
  142. package/lib/public/mate-avatars/B13.png +0 -0
  143. package/lib/public/mate-avatars/B14.png +0 -0
  144. package/lib/public/mate-avatars/B15.png +0 -0
  145. package/lib/public/mate-avatars/B16.png +0 -0
  146. package/lib/public/mate-avatars/B17.png +0 -0
  147. package/lib/public/mate-avatars/B18.png +0 -0
  148. package/lib/public/mate-avatars/B19.png +0 -0
  149. package/lib/public/mate-avatars/B20.png +0 -0
  150. package/lib/public/mate-avatars/B21.png +0 -0
  151. package/lib/public/mate-avatars/B22.png +0 -0
  152. package/lib/public/mate-avatars/B23.png +0 -0
  153. package/lib/public/mate-avatars/B24.png +0 -0
  154. package/lib/public/mate-avatars/B25.png +0 -0
  155. package/lib/public/mate-avatars/B26.png +0 -0
  156. package/lib/public/mate-avatars/B27.png +0 -0
  157. package/lib/public/mate-avatars/B28.png +0 -0
  158. package/lib/public/mate-avatars/B29.png +0 -0
  159. package/lib/public/mate-avatars/B30.png +0 -0
  160. package/lib/public/mate-avatars/B31.png +0 -0
  161. package/lib/public/mate-avatars/B32.png +0 -0
  162. package/lib/public/mate-avatars/B33.png +0 -0
  163. package/lib/public/mate-avatars/B34.png +0 -0
  164. package/lib/public/mate-avatars/B35.png +0 -0
  165. package/lib/public/mate-avatars/B36.png +0 -0
  166. package/lib/public/mate-avatars/B37.png +0 -0
  167. package/lib/public/mate-avatars/B38.png +0 -0
  168. package/lib/public/mate-avatars/B39.png +0 -0
  169. package/lib/public/mate-avatars/B40.png +0 -0
  170. package/lib/public/mate-avatars/B41.png +0 -0
  171. package/lib/public/mate-avatars/B42.png +0 -0
  172. package/lib/public/mate-avatars/B43.png +0 -0
  173. package/lib/public/mate-avatars/B44.png +0 -0
  174. package/lib/public/mate-avatars/B45.png +0 -0
  175. package/lib/public/mate-avatars/B46.png +0 -0
  176. package/lib/public/mate-avatars/B47.png +0 -0
  177. package/lib/public/mate-avatars/B48.png +0 -0
  178. package/lib/public/mate-avatars/C01.png +0 -0
  179. package/lib/public/mate-avatars/C02.png +0 -0
  180. package/lib/public/mate-avatars/C03.png +0 -0
  181. package/lib/public/mate-avatars/C04.png +0 -0
  182. package/lib/public/mate-avatars/C05.png +0 -0
  183. package/lib/public/mate-avatars/C06.png +0 -0
  184. package/lib/public/mate-avatars/C07.png +0 -0
  185. package/lib/public/mate-avatars/C08.png +0 -0
  186. package/lib/public/mate-avatars/C09.png +0 -0
  187. package/lib/public/mate-avatars/C10.png +0 -0
  188. package/lib/public/mate-avatars/C11.png +0 -0
  189. package/lib/public/mate-avatars/C12.png +0 -0
  190. package/lib/public/mate-avatars/C13.png +0 -0
  191. package/lib/public/mate-avatars/C14.png +0 -0
  192. package/lib/public/mate-avatars/C15.png +0 -0
  193. package/lib/public/mate-avatars/C16.png +0 -0
  194. package/lib/public/mate-avatars/C17.png +0 -0
  195. package/lib/public/mate-avatars/C18.png +0 -0
  196. package/lib/public/mate-avatars/C19.png +0 -0
  197. package/lib/public/mate-avatars/C20.png +0 -0
  198. package/lib/public/mate-avatars/C21.png +0 -0
  199. package/lib/public/mate-avatars/C22.png +0 -0
  200. package/lib/public/mate-avatars/C23.png +0 -0
  201. package/lib/public/mate-avatars/C24.png +0 -0
  202. package/lib/public/mate-avatars/C25.png +0 -0
  203. package/lib/public/mate-avatars/C26.png +0 -0
  204. package/lib/public/mate-avatars/C27.png +0 -0
  205. package/lib/public/mate-avatars/C28.png +0 -0
  206. package/lib/public/mate-avatars/C29.png +0 -0
  207. package/lib/public/mate-avatars/C30.png +0 -0
  208. package/lib/public/mate-avatars/C31.png +0 -0
  209. package/lib/public/mate-avatars/C32.png +0 -0
  210. package/lib/public/mate-avatars/C33.png +0 -0
  211. package/lib/public/mate-avatars/C34.png +0 -0
  212. package/lib/public/mate-avatars/C35.png +0 -0
  213. package/lib/public/mate-avatars/C36.png +0 -0
  214. package/lib/public/mate-avatars/C37.png +0 -0
  215. package/lib/public/mate-avatars/C38.png +0 -0
  216. package/lib/public/mate-avatars/C39.png +0 -0
  217. package/lib/public/mate-avatars/C40.png +0 -0
  218. package/lib/public/mate-avatars/C41.png +0 -0
  219. package/lib/public/mate-avatars/C42.png +0 -0
  220. package/lib/public/mate-avatars/C43.png +0 -0
  221. package/lib/public/mate-avatars/C44.png +0 -0
  222. package/lib/public/mate-avatars/C45.png +0 -0
  223. package/lib/public/mate-avatars/C46.png +0 -0
  224. package/lib/public/mate-avatars/C47.png +0 -0
  225. package/lib/public/mate-avatars/C48.png +0 -0
  226. package/lib/public/mate-avatars/catalog.json +482 -0
  227. package/lib/public/mate-avatars/lead-clay.png +0 -0
  228. package/lib/public/modules/admin-settings-projects.js +6 -2
  229. package/lib/public/modules/app-connection.js +8 -1
  230. package/lib/public/modules/app-message-router.js +4 -0
  231. package/lib/public/modules/app-messages.js +31 -5
  232. package/lib/public/modules/app-projects.js +5 -0
  233. package/lib/public/modules/app-rendering.js +1 -1
  234. package/lib/public/modules/avatar.js +15 -41
  235. package/lib/public/modules/chat-render-runtime.js +10 -1
  236. package/lib/public/modules/clay-browser-links.js +47 -0
  237. package/lib/public/modules/clay-log-links.js +1 -1
  238. package/lib/public/modules/clay-owner-navigation.js +27 -0
  239. package/lib/public/modules/clay-tool-activity.js +140 -0
  240. package/lib/public/modules/clay-tool-presentation.js +338 -0
  241. package/lib/public/modules/command-palette.js +2 -2
  242. package/lib/public/modules/debate.js +1 -0
  243. package/lib/public/modules/default-ai.js +11 -3
  244. package/lib/public/modules/history-replay-batch.js +5 -1
  245. package/lib/public/modules/home-debate-live.js +2 -0
  246. package/lib/public/modules/home-debate-planning.js +7 -7
  247. package/lib/public/modules/home-mate-avatar-editor.js +127 -208
  248. package/lib/public/modules/home-mate-chat.js +8 -6
  249. package/lib/public/modules/home-mate-creation.js +73 -1
  250. package/lib/public/modules/home-mate-settings.js +19 -12
  251. package/lib/public/modules/markdown.js +4 -0
  252. package/lib/public/modules/mate-avatar-catalog.js +97 -0
  253. package/lib/public/modules/mate-management.js +40 -7
  254. package/lib/public/modules/mcp-connection-view.js +31 -13
  255. package/lib/public/modules/mcp-discovery-view.js +31 -0
  256. package/lib/public/modules/mcp-ui.js +23 -4
  257. package/lib/public/modules/mention.js +3 -0
  258. package/lib/public/modules/pane-browser-bridge.js +20 -0
  259. package/lib/public/modules/personal-settings-dialog.js +7 -1
  260. package/lib/public/modules/profile.js +1 -238
  261. package/lib/public/modules/project-access-recovery.js +17 -0
  262. package/lib/public/modules/project-admin-overview.js +21 -0
  263. package/lib/public/modules/project-mate-navigation.js +99 -14
  264. package/lib/public/modules/search-clay-chat.js +16 -2
  265. package/lib/public/modules/session-folder-layout.js +1 -1
  266. package/lib/public/modules/session-folder-view-menu.js +9 -0
  267. package/lib/public/modules/session-github.js +85 -14
  268. package/lib/public/modules/session-work-primary.js +51 -0
  269. package/lib/public/modules/shared-browser-settings.js +112 -0
  270. package/lib/public/modules/shared-browser-tabs.js +13 -3
  271. package/lib/public/modules/shared-browser.js +27 -6
  272. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  273. package/lib/public/modules/split-view.js +2 -0
  274. package/lib/public/modules/sticky-note-markdown.js +72 -6
  275. package/lib/public/modules/sticky-notes-actions.js +30 -0
  276. package/lib/public/modules/sticky-notes-browser.js +9 -2
  277. package/lib/public/modules/sticky-notes-card.js +23 -7
  278. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  279. package/lib/public/modules/sticky-notes-editor.js +108 -44
  280. package/lib/public/modules/sticky-notes-shared.js +50 -2
  281. package/lib/public/modules/sticky-notes-state.js +66 -0
  282. package/lib/public/modules/sticky-notes.js +34 -51
  283. package/lib/public/modules/theme.js +8 -1
  284. package/lib/public/modules/thinking-lifecycle.js +33 -3
  285. package/lib/public/modules/thinking-view.js +23 -1
  286. package/lib/public/modules/tools.js +36 -7
  287. package/lib/public/modules/user-settings-appearance.js +10 -3
  288. package/lib/public/modules/user-settings-editors.js +1 -1
  289. package/lib/public/modules/user-settings-layout.js +11 -2
  290. package/lib/public/modules/user-settings-preferences.js +6 -2
  291. package/lib/public/modules/user-settings-profile.js +12 -2
  292. package/lib/public/modules/user-settings.js +3 -2
  293. package/lib/public/style.css +3 -0
  294. package/lib/public/sw.js +3 -1
  295. package/lib/scheduled-task-records.js +3 -1
  296. package/lib/scheduler.js +2 -0
  297. package/lib/sdk-bridge.js +228 -23
  298. package/lib/sdk-message-processor.js +2 -0
  299. package/lib/sdk-response-recovery.js +155 -0
  300. package/lib/server-admin.js +63 -16
  301. package/lib/server-appearance-settings.js +13 -1
  302. package/lib/server-dm.js +2 -1
  303. package/lib/server-global-ws.js +10 -20
  304. package/lib/server-home-chat-events.js +18 -4
  305. package/lib/server-home-chat.js +12 -16
  306. package/lib/server-home-mate-creation.js +1 -0
  307. package/lib/server-home-mcp-consent.js +14 -0
  308. package/lib/server-mates.js +25 -62
  309. package/lib/server-owner-ui.js +77 -0
  310. package/lib/server-path-access.js +38 -0
  311. package/lib/server-project-removal.js +22 -0
  312. package/lib/server-settings.js +4 -1
  313. package/lib/server.js +144 -41
  314. package/lib/session-config-transition.js +45 -0
  315. package/lib/session-folders.js +7 -2
  316. package/lib/session-github.js +9 -2
  317. package/lib/session-visibility.js +5 -2
  318. package/lib/session-work-primary.js +50 -0
  319. package/lib/sessions.js +13 -0
  320. package/lib/shared-browser-cookie-sync.js +32 -0
  321. package/lib/shared-browser-cookies.js +79 -0
  322. package/lib/shared-browser-runtime.js +25 -9
  323. package/lib/shared-browser-storage-sync.js +43 -0
  324. package/lib/shared-browser-storage.js +101 -0
  325. package/lib/shared-browser-tools.js +2 -1
  326. package/lib/shared-browser-worker.js +46 -10
  327. package/lib/terminal-manager.js +52 -0
  328. package/lib/users-permissions.js +2 -8
  329. package/lib/users.js +4 -0
  330. package/lib/workspace-query-access.js +4 -1
  331. package/lib/ws-schema.js +8 -2
  332. package/lib/yoke/acp-agent-profiles.js +2 -2
  333. package/lib/yoke/adapters/claude.js +47 -8
  334. package/lib/yoke/adapters/codex.js +52 -111
  335. package/lib/yoke/claude-worker-startup.js +28 -0
  336. package/lib/yoke/codex-idle-recovery.js +81 -1
  337. package/lib/yoke/codex-recovery-admission.js +164 -0
  338. package/lib/yoke/session-tool-transport.js +3 -0
  339. package/package.json +5 -1
@@ -0,0 +1,151 @@
1
+ var crypto = require('crypto');
2
+ var rule = require('./schedule-rule');
3
+
4
+ function copy(value) { return JSON.parse(JSON.stringify(value)); }
5
+ function attachMateScheduleLifecycle(ctx) {
6
+ var now = ctx.now || Date.now;
7
+ function save(session, records) {
8
+ var previous = session.mateSchedules;
9
+ session.mateSchedules = records;
10
+ if (ctx.sm.saveSessionFile(session) !== true) {
11
+ session.mateSchedules = previous;
12
+ throw new Error('The Mate schedule could not be saved.');
13
+ }
14
+ }
15
+ function records(session) { return copy(session.mateSchedules || []); }
16
+ function find(session, id) {
17
+ return records(session).find(function (record) { return record.id === id; }) || null;
18
+ }
19
+ function replace(session, record) {
20
+ var all = records(session);
21
+ var index = all.findIndex(function (item) { return item.id === record.id; });
22
+ if (index < 0) all.push(record); else all[index] = record;
23
+ save(session, all);
24
+ }
25
+ function jobFor(record) {
26
+ return ctx.scheduler.listJobs().reverse().find(function (job) {
27
+ return job.projectId === ctx.slug && job.payload && job.payload.mateScheduleId === record.id &&
28
+ job.payload.logicalRevision === record.revision && job.payload.occurrence === record.occurrence;
29
+ });
30
+ }
31
+ function current(session, job) {
32
+ var record = job && job.payload && find(session, job.payload.mateScheduleId);
33
+ if (record && record.createdBy && record.createdBy.type === 'canonicalClay') { try { (ctx.authorizeRuntime || ctx.authorize)(session); } catch (error) { return false; } }
34
+ return !!(record && record.state === 'active' && record.revision === job.payload.logicalRevision &&
35
+ record.occurrence === job.payload.occurrence && record.ownerId === job.ownerId);
36
+ }
37
+ function admit(session, record) {
38
+ if (record.state !== 'active') return null;
39
+ var existing = jobFor(record);
40
+ if (existing && !(existing.state === 'cancelled' && existing.cancelledReason === 'queue_admission_failed')) {
41
+ if (['failed', 'cancelled', 'interrupted'].indexOf(existing.state) >= 0) throw new Error('The occurrence requires recovery; it was not scheduled.');
42
+ return existing;
43
+ }
44
+ var job = ctx.schedule(session, record.message, Math.max(record.runAt, now() + 1),
45
+ { id: record.ownerId }, null, null, 'mate-' + record.id + '-' + record.revision + '-' + record.occurrence,
46
+ null, { exact: true, scheduleRule: record.rule, mateScheduleId: record.id,
47
+ logicalScheduleId: record.id, logicalRevision: record.revision, occurrence: record.occurrence });
48
+ if (!job) throw new Error('The occurrence could not be admitted. The saved schedule needs retry.');
49
+ return job;
50
+ }
51
+ function create(session, value, id, control) {
52
+ var actor = ctx.authorize(session, control);
53
+ var existing = find(session, id);
54
+ if (existing) { if (existing.message !== value.message || JSON.stringify(existing.rule) !== JSON.stringify(value.rule)) throw new Error('Schedule request already names another intent.'); admit(session, existing); return existing; }
55
+ var runAt = rule.nextScheduleRuleTime(value.rule, now(), [], {});
56
+ if (!Number.isFinite(runAt)) throw new Error('The schedule time has expired.');
57
+ var record = { id: id, revision: crypto.randomUUID(), ownerId: ctx.owner(session), message: value.message,
58
+ createdBy: actor || null, updatedBy: actor || null, rule: value.rule, state: 'active', occurrence: 0, count: 0, dateCounts: {}, runAt: runAt };
59
+ replace(session, record);
60
+ admit(session, record);
61
+ return record;
62
+ }
63
+ function checked(session, args, control) {
64
+ ctx.authorize(session, control);
65
+ var record = find(session, args.id);
66
+ if (!record || record.ownerId !== ctx.owner(session)) throw new Error('Schedule not found.');
67
+ if (args.revision && args.revision !== record.revision) throw new Error('The schedule revision is stale.');
68
+ return record;
69
+ }
70
+ function cancel(session, args, control) {
71
+ var record = checked(session, args, control);
72
+ var actor = ctx.authorize(session, control);
73
+ if (record.state === 'cancelled') return record;
74
+ // Persist the tombstone before touching occurrences. Dispatch rechecks it.
75
+ record.state = 'cancelled';
76
+ if (actor) record.updatedBy = actor;
77
+ replace(session, record);
78
+ var job = jobFor(record);
79
+ if (job && job.state === 'queued' && !ctx.cancelJob(session, job)) throw new Error('Cancellation was saved, but occurrence cleanup failed.');
80
+ return record;
81
+ }
82
+ function update(session, args, value, control) {
83
+ var previous = checked(session, args, control);
84
+ var actor = ctx.authorize(session, control);
85
+ if (previous.state !== 'active') throw new Error('Only active schedules can be updated.');
86
+ var runAt = rule.nextScheduleRuleTime(value.rule, now(), [], {});
87
+ if (!Number.isFinite(runAt)) throw new Error('The schedule time has expired.');
88
+ var record = Object.assign({}, previous, { revision: crypto.randomUUID(), updatedBy: actor || previous.updatedBy || null, message: value.message,
89
+ rule: value.rule, count: 0, dateCounts: {}, occurrence: 0, runAt: runAt });
90
+ replace(session, record);
91
+ var oldJob = jobFor(previous);
92
+ if (oldJob && oldJob.state === 'queued' && !ctx.cancelJob(session, oldJob)) throw new Error('Update saved, but previous occurrence cleanup failed.');
93
+ admit(session, record);
94
+ return record;
95
+ }
96
+ function consumed(session, job) {
97
+ if (!current(session, job)) return false;
98
+ var record = find(session, job.payload.mateScheduleId);
99
+ record.count++;
100
+ var date = new Date(record.runAt);
101
+ var key = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0');
102
+ record.dateCounts[key] = Number(record.dateCounts[key] || 0) + 1;
103
+ // Coalesce missed times: deliver the admitted occurrence once, then choose
104
+ // the next future time. Counts represent deliveries, not missed slots.
105
+ record.runAt = rule.nextScheduleRuleTime(record.rule, Math.max(record.runAt, now()), [],
106
+ { startCount: record.count, dateStartCounts: record.dateCounts });
107
+ record.occurrence++;
108
+ if (!Number.isFinite(record.runAt)) record.state = 'completed';
109
+ replace(session, record);
110
+ admit(session, record);
111
+ return true;
112
+ }
113
+ function reconcile(session) {
114
+ records(session).forEach(function (record) {
115
+ if (record.state !== 'active') return;
116
+ (ctx.authorizeRuntime || ctx.authorize)(session);
117
+ var job = jobFor(record);
118
+ var item = job && ctx.queueItem(session, job);
119
+ if (item && item.state === 'consumed') consumed(session, job);
120
+ else if (item && (item.state === 'cancelled' || item.state === 'failed')) {
121
+ record.state = item.state;
122
+ replace(session, record);
123
+ } else if (job && ['failed', 'cancelled', 'interrupted'].indexOf(job.state) >= 0 && (!item || item.state !== 'pending')) {
124
+ record.state = 'interrupted';
125
+ replace(session, record);
126
+ } else admit(session, record);
127
+ });
128
+ }
129
+ function onConsumed(session, job) {
130
+ try { return consumed(session, job); }
131
+ catch (error) {
132
+ // Queue consumption is already durable. Keep the accepted provider turn
133
+ // running; reconciliation repairs the saved next occurrence after restart.
134
+ ctx.sm.sendAndRecord(session, { type: 'error', text: 'The scheduled message was delivered, but its next occurrence needs recovery: ' + error.message });
135
+ return false;
136
+ }
137
+ }
138
+ return { create: create, read: checked, list: function (session, control) { ctx.authorize(session, control); return records(session); },
139
+ update: update, cancel: cancel, failed: function (session, job, error) {
140
+ if (!current(session, job)) return;
141
+ var record = find(session, job.payload.mateScheduleId);
142
+ record.state = 'failed'; record.error = error.message;
143
+ replace(session, record);
144
+ }, cancelled: function (session, job) {
145
+ if (!current(session, job)) return;
146
+ var record = find(session, job.payload.mateScheduleId);
147
+ record.state = 'cancelled';
148
+ replace(session, record);
149
+ }, current: current, consumed: onConsumed, reconcile: reconcile };
150
+ }
151
+ module.exports = { attachMateScheduleLifecycle: attachMateScheduleLifecycle };
@@ -0,0 +1,103 @@
1
+ var crypto = require('crypto');
2
+ var userInput = require('./yoke/user-input');
3
+ var input = require('./mate-schedule-input');
4
+ var personal = require('./mcp-session-access');
5
+
6
+ function attachMateScheduling(ctx) {
7
+ function owner(session) { return ctx.users.isMultiUser() ? session.ownerId : 'local-user'; }
8
+ function privateSession(session) {
9
+ if (!ctx.isMate || !session || ctx.sm.sessions.get(session.localId) !== session ||
10
+ session.destroying || session.mateCreationMode || session.taskStopRequested || session._mcpForeignInput ||
11
+ session.sessionVisibility === 'shared' || session.delegated || session._delegatedBy || session._pairDelegation) throw new Error('An active private Mate conversation is required.');
12
+ if (ctx.users.isMultiUser() && (session.ownerId !== ctx.projectOwnerId || !personal.canUsePersonalConnections(session, ctx.sm.sessions))) throw new Error('Only the Mate owner can schedule messages.');
13
+ if (!ctx.users.isMultiUser() && (session.ownerId || (session.history || []).some(function (event) { return event.type === 'user_message' && event.from && event.from !== 'default'; }))) throw new Error('Foreign input cannot authorize Mate schedules.');
14
+ ctx.authorizeAccess(session, owner(session));
15
+ }
16
+ function authorize(session) {
17
+ if (ctx.isHostAgent) throw new Error('Canonical Clay scheduling requires its separate owner-reviewed controls.');
18
+ privateSession(session);
19
+ }
20
+ function authorizeControl(session, control) {
21
+ privateSession(session);
22
+ return require('./canonical-clay-scheduling-authority').validate(control, ctx.slug, session);
23
+ }
24
+ function authorizeLifecycle(session) {
25
+ privateSession(session);
26
+ require('./canonical-clay-scheduling-authority').assertMate(ctx.projectOwnerId, ctx.mateId, ctx.users.isMultiUser());
27
+ if (ctx.isHostAgent) require('./canonical-clay-scheduling-authority').assertHost(ctx.projectOwnerId, ctx.mateId, ctx.users.isMultiUser());
28
+ }
29
+ function persistDraft(session, draft) {
30
+ var previous = session.mateScheduleDraft;
31
+ session.mateScheduleDraft = draft;
32
+ if (ctx.sm.saveSessionFile(session) !== true) {
33
+ session.mateScheduleDraft = previous;
34
+ throw new Error('The schedule draft could not be saved.');
35
+ }
36
+ }
37
+ function result(value) { return { content: [{ type: 'text', text: JSON.stringify(value) }] }; }
38
+ function getToolDefs(session) {
39
+ try { authorize(session); } catch (error) { return []; }
40
+ var generation = Number(session._sdkQueryGeneration || 0);
41
+ var originalOwner = owner(session);
42
+ function current() {
43
+ authorize(session);
44
+ if (generation !== Number(session._sdkQueryGeneration || 0) || owner(session) !== originalOwner) throw new Error('The schedule query is stale.');
45
+ }
46
+ function tool(name, description, properties, required, handler) {
47
+ return { name: name, queryBound: true, inputSchema: { type: 'object', properties: properties, required: required, additionalProperties: false },
48
+ description: description, handler: async function (args) {
49
+ try { current(); return result(await handler(args || {})); }
50
+ catch (error) { return result({ status: 'rejected', reason: error.message }); }
51
+ } };
52
+ }
53
+ var str = { type: 'string' };
54
+ var fields = { message: str, date: str, time: str, scheduleRule: { type: ['object', 'null'] } };
55
+ async function approve(draft, question, label, other) {
56
+ persistDraft(session, draft);
57
+ var reply = await userInput.dispatchUserInput(ctx.userInput(session), { questions: [{
58
+ id: 'approve', header: 'Mate schedule', question: question,
59
+ options: [{ label: label }, { label: other || 'Cancel' }], allowOther: false,
60
+ }] }, { source: 'mate-schedule', signal: session.abortController && session.abortController.signal });
61
+ current();
62
+ if (!session.mateScheduleDraft || session.mateScheduleDraft.id !== draft.id || session.mateScheduleDraft.revision !== draft.revision) throw new Error('The schedule draft is stale.');
63
+ var accepted = reply.status === 'submitted' && reply.answers.approve && reply.answers.approve.length === 1 && reply.answers.approve[0] === label;
64
+ if (!accepted) persistDraft(session, null);
65
+ return accepted;
66
+ }
67
+ async function review(args, update) {
68
+ var previous = update ? ctx.lifecycle.read(session, args) : null;
69
+ var value = input.normalize(args, previous);
70
+ var draft = { id: crypto.randomUUID(), revision: crypto.randomUUID(), value: value,
71
+ targetId: previous && previous.id, targetRevision: previous && previous.revision };
72
+ if (!await approve(draft, (update ? 'Update this schedule?\n' : 'Create this schedule?\n') + input.review(value), update ? 'Update schedule' : 'Create schedule')) return { status: 'cancelled' };
73
+ var record = update ? ctx.lifecycle.update(session, { id: draft.targetId, revision: draft.targetRevision }, value) : ctx.lifecycle.create(session, value, draft.id);
74
+ persistDraft(session, null);
75
+ return { status: update ? 'updated' : 'created', schedule: record, timezone: input.timezone() };
76
+ }
77
+ async function cancel(args) {
78
+ var record = ctx.lifecycle.read(session, args);
79
+ if (record.state === 'cancelled') return { status: 'cancelled', schedule: record };
80
+ var draft = { id: crypto.randomUUID(), revision: crypto.randomUUID(), targetId: record.id, targetRevision: record.revision };
81
+ if (!await approve(draft, 'Cancel this schedule?\n' + input.review({ message: record.message, rule: record.rule }), 'Cancel schedule', 'Keep schedule')) return { status: 'unchanged' };
82
+ record = ctx.lifecycle.cancel(session, { id: draft.targetId, revision: draft.targetRevision });
83
+ persistDraft(session, null);
84
+ return { status: 'cancelled', schedule: record };
85
+ }
86
+ var begin = tool('begin_mate_schedule_interview', 'Begin scheduling for this Mate. Ask exactly one structured question at a time. Never assume repetition.', {}, [], function () {
87
+ return { status: 'interview_started', timezone: input.timezone(), guidance: 'Collect the message, date/time, and explicitly requested repetition one question at a time. Then propose the complete schedule for the human approval card.' };
88
+ });
89
+ begin.structuredQuestionLimit = function () { try { current(); return 1; } catch (error) { return null; } };
90
+ return [begin,
91
+ tool('propose_mate_schedule', 'Review a complete message schedule in a server-owned approval card. Only authenticated human approval persists a schedule.', fields, ['message'], function (args) { return review(args, false); }),
92
+ tool('list_mate_schedules', 'List logical schedules and their stable IDs, revisions and lifecycle states in this private conversation.', {}, [], function () { return { schedules: ctx.lifecycle.list(session) }; }),
93
+ tool('read_mate_schedule', 'Read one logical schedule using its listed ID.', { id: str }, ['id'], function (args) { return { schedule: ctx.lifecycle.read(session, args) }; }),
94
+ tool('update_mate_schedule', 'Propose changes to an active schedule by listed ID and revision, with a fresh human approval card.', Object.assign({ id: str, revision: str }, fields), ['id', 'revision'], function (args) { return review(args, true); }),
95
+ tool('cancel_mate_schedule', 'Review cancellation by listed ID and revision in an authenticated human approval card. Cancellation persists across restart.', { id: str, revision: str }, ['id', 'revision'], cancel),
96
+ ];
97
+ }
98
+ return { authorize: authorize, authorizeControl: authorizeControl, authorizeLifecycle: authorizeLifecycle, owner: owner, getToolDefs: getToolDefs, getSystemPrompt: function (session) {
99
+ try { authorize(session); } catch (error) { return ''; }
100
+ return 'When scheduling for this Mate, call begin_mate_schedule_interview, ask exactly one structured question at a time, and clarify the message, local date/time in ' + input.timezone() + ', and explicitly requested repetition. Use scheduleRule for recurrence and count/until limits. propose_mate_schedule and update_mate_schedule present the exact authenticated human approval card. Chat text and model flags never approve schedules. Use listed logical IDs and revisions to read, update or cancel. Scheduled output uses this private Mate runtime and tools. Project Scheduled Tasks Driver/Worker tools remain unavailable.';
101
+ } };
102
+ }
103
+ module.exports = { attachMateScheduling: attachMateScheduling };
package/lib/mates.js CHANGED
@@ -4,6 +4,8 @@ var path = require("path");
4
4
  var crypto = require("crypto");
5
5
  var config = require("./config");
6
6
  var workspaceColor = require("./mate-workspace-color");
7
+ var avatarCatalog = require("./mate-avatar-catalog");
8
+ var avatarMigration = require("./mate-avatar-migration");
7
9
 
8
10
  var crisisSafety = require("./crisis-safety");
9
11
  var matesPrompts = require("./mates-prompts");
@@ -98,10 +100,12 @@ function loadMates(ctx) {
98
100
  var raw = fs.readFileSync(matesFilePath(ctx), "utf8");
99
101
  var data = JSON.parse(raw);
100
102
  if (!data.mates) data.mates = [];
101
- return data;
102
103
  } catch (e) {
103
104
  return defaultData();
104
105
  }
106
+ // Migration write failures must propagate, never masquerade as an empty registry.
107
+ if (avatarMigration.migrate(data)) saveMates(ctx, data);
108
+ return data;
105
109
  }
106
110
 
107
111
  function saveMates(ctx, data) {
@@ -135,6 +139,7 @@ function createMate(ctx, seedData) {
135
139
  displayName: null,
136
140
  avatarColor: "#5857fc",
137
141
  avatarStyle: "imprint",
142
+ avatarCatalogId: "B" + String(crypto.randomInt(1, 49)).padStart(2, "0"),
138
143
  avatarSeed: crypto.randomBytes(4).toString("hex"),
139
144
  },
140
145
  bio: null,
@@ -142,6 +147,7 @@ function createMate(ctx, seedData) {
142
147
  interviewProjectPath: null,
143
148
  };
144
149
 
150
+ avatarMigration.normalizeMate(mate);
145
151
  data.mates.push(mate);
146
152
  saveMates(ctx, data);
147
153
 
@@ -205,6 +211,8 @@ function updateMate(ctx, id, updates) {
205
211
  if (data.mates[i].primary || data.mates[i].builtinKey === "clay") delete updates.workspaceColor;
206
212
  else if (!workspaceColor.valid(updates.workspaceColor)) return { error: "Choose a supported workspace color." };
207
213
  }
214
+ var avatarError = avatarCatalog.validate(updates.profile, data.mates[i]);
215
+ if (avatarError) return { error: avatarError };
208
216
  // Primary mates: protect name and core identity fields
209
217
  if (data.mates[i].primary) {
210
218
  delete updates.name;
@@ -219,6 +227,7 @@ function updateMate(ctx, id, updates) {
219
227
  // Locked built-in artwork is server-managed. Preserve it even when a
220
228
  // stale or crafted client submits a replacement profile object.
221
229
  if (data.mates[i].profile && data.mates[i].profile.avatarLocked && updates.profile) {
230
+ updates.profile.avatarCatalogId = data.mates[i].profile.avatarCatalogId || "";
222
231
  updates.profile.avatarStyle = data.mates[i].profile.avatarStyle;
223
232
  updates.profile.avatarSeed = data.mates[i].profile.avatarSeed;
224
233
  updates.profile.avatarColor = data.mates[i].profile.avatarColor;
@@ -229,6 +238,7 @@ function updateMate(ctx, id, updates) {
229
238
  for (var j = 0; j < keys.length; j++) {
230
239
  data.mates[i][keys[j]] = updates[keys[j]];
231
240
  }
241
+ avatarMigration.normalizeMate(data.mates[i]);
232
242
  saveMates(ctx, data);
233
243
  return data.mates[i];
234
244
  }
@@ -236,7 +246,7 @@ function updateMate(ctx, id, updates) {
236
246
  return null;
237
247
  }
238
248
 
239
- function deleteMate(ctx, id) {
249
+ function deleteMate(ctx, id, options) {
240
250
  var data = loadMates(ctx);
241
251
  // Primary mates cannot be deleted (they are system infrastructure)
242
252
  for (var di = 0; di < data.mates.length; di++) {
@@ -249,15 +259,16 @@ function deleteMate(ctx, id) {
249
259
  return m.id !== id;
250
260
  });
251
261
  if (data.mates.length === before) return { error: "Mate not found" };
252
- saveMates(ctx, data);
262
+ if (!options || !options.strictCleanup) saveMates(ctx, data);
253
263
 
254
264
  // Remove mate directory
255
265
  var mateDir = getMateDir(ctx, id);
256
266
  try {
257
267
  fs.rmSync(mateDir, { recursive: true, force: true });
258
268
  } catch (e) {
259
- // Directory may not exist
269
+ if (options && options.strictCleanup) throw e;
260
270
  }
271
+ if (options && options.strictCleanup) saveMates(ctx, data);
261
272
 
262
273
  return { ok: true };
263
274
  }
@@ -506,6 +517,7 @@ function createBuiltinMate(ctx, builtinKey) {
506
517
  interviewProjectPath: null,
507
518
  };
508
519
 
520
+ avatarMigration.normalizeMate(mate);
509
521
  data.mates.push(mate);
510
522
  saveMates(ctx, data);
511
523
 
@@ -661,8 +673,8 @@ function syncPrimaryMates(ctx) {
661
673
  }
662
674
  // Locked avatars are code-managed — refresh them from the def so a
663
675
  // built-in's avatar can be updated by changing the source file
664
- // (e.g. Clay flipping from a stale Ally asset to the app icon).
665
- if (def.avatarLocked) {
676
+ // User uploads are preserved while canonical library artwork follows the definition.
677
+ if (def.avatarLocked && !avatarMigration.hasCustom(mate)) {
666
678
  if (def.avatarCustom != null && mate.profile.avatarCustom !== def.avatarCustom) {
667
679
  mate.profile.avatarCustom = def.avatarCustom; changed = true;
668
680
  }
@@ -0,0 +1,117 @@
1
+ // Agent import consent is server-owned, owner-only, exact-query and single-use.
2
+ var crypto = require('crypto');
3
+ var userInput = require('./yoke/user-input');
4
+ var createProxy = require('./mcp-proxy').createProxy;
5
+ var SERVER = 'clay-mcp-connections';
6
+ function result(value, error) { return { content: [{ type: 'text', text: JSON.stringify(value) }], isError: !!error }; }
7
+ function attachMcpAgentTools(ctx) {
8
+ var viewers = new Map(), pending = new Map(), used = new WeakMap(), closed = false;
9
+ function ownerOf(session) { return ctx.isMultiUser() ? String(session.ownerId) : 'default'; }
10
+ function authorized(session, owner, generation) {
11
+ if (closed || !session || session.youCurator || session._mcpForeignInput || session.sessionVisibility === 'shared' || !ctx.canUseSession(session) || ownerOf(session) !== owner || Number(session._sdkQueryGeneration || 0) !== generation || session.taskStopRequested || session.abortController && session.abortController.signal.aborted) throw new Error('MCP management requires the current authorized private query.');
12
+ ctx.authorizeOwner(owner);
13
+ }
14
+ function canRespond(ws, session, owner) {
15
+ try {
16
+ if (!ws || ws.readyState !== 1 || ctx.ownerFor(ws) !== owner) return false;
17
+ return ctx.clients.has(ws) && ws._clayActiveSession === session.localId || viewers.has(ws) && viewers.get(ws).has(session);
18
+ } catch (error) { return false; }
19
+ }
20
+ function humans(session, owner) {
21
+ return Array.from(ctx.clients).concat(Array.from(viewers.keys())).some(function (ws) { return canRespond(ws, session, owner); });
22
+ }
23
+ function registerViewer(ws, session) {
24
+ var owner = ownerOf(session);
25
+ if (closed || !session || !ctx.canUseSession(session) || session._mcpForeignInput || session.sessionVisibility === 'shared') throw new Error('Consent viewer requires an authorized private session.');
26
+ ctx.authorizeOwner(owner);
27
+ if (ctx.ownerFor(ws) !== owner || ws.readyState !== 1) throw new Error('Consent viewer must be the authenticated owner.');
28
+ var sessions = viewers.get(ws) || new Map(), detached = false;
29
+ sessions.set(session, (sessions.get(session) || 0) + 1); viewers.set(ws, sessions);
30
+ return function () {
31
+ if (detached) return; detached = true;
32
+ var count = sessions.get(session) || 0;
33
+ if (count <= 1) sessions.delete(session); else sessions.set(session, count - 1);
34
+ if (!sessions.size && viewers.get(ws) === sessions) viewers.delete(ws);
35
+ };
36
+ }
37
+ function review(candidate, owner, config) {
38
+ var restrictions = config.enabledTools === null ? 'Tool allowlist: unrestricted' : 'Tool allowlist: ' + JSON.stringify(config.enabledTools || []);
39
+ restrictions += '\nExcluded tools: ' + JSON.stringify(config.disabledTools || []);
40
+ if (config.toolPatterns && config.toolPatterns.length) restrictions += '\nExcluded vendor patterns: ' + JSON.stringify(config.toolPatterns);
41
+ var question = 'Import "' + candidate.name + '" from ' + candidate.vendor + ' (' + candidate.scope + ') ?\nSource: ' + candidate.file + '\nTransport: ' + candidate.transport + '\nDependency: ' + candidate.dependency +
42
+ '\nDestination: owner ' + owner + ' — private MCP catalog shared across your Clay sessions, project sessions and Mates; other accounts cannot use it.\n' + restrictions + '\nRequired credential names: ' + (candidate.references.join(', ') || 'None') +
43
+ '\nThe imported copy will be DISABLED. This approval only saves that exact copy. Enable, Test and Sign in require separate actions. Original vendor files stay unchanged. Enter credentials only in MCP management, never in chat.';
44
+ if (question.length > 2000) throw new Error('This candidate needs a larger policy review than the consent card supports. Review it in MCP management; no import was made.');
45
+ return question;
46
+ }
47
+ function definitions(session) {
48
+ var owner = ownerOf(session), generation = Number(session._sdkQueryGeneration || 0);
49
+ function authorize() { authorized(session, owner, generation); }
50
+ async function discover(args) {
51
+ authorize();
52
+ if (Object.keys(args).some(function (key) { return key !== 'vendor' && key !== 'file'; })) throw new Error('Unsupported discovery input.');
53
+ if (!!args.file !== !!args.vendor) throw new Error('Configured discovery requires both vendor and absolute file path.');
54
+ var scan = ctx.discovery.scan(owner, session, args.file ? { vendor: args.vendor, file: args.file } : null);
55
+ authorize(); return result(Object.assign({ status: 'discovered', imported: false }, scan));
56
+ }
57
+ async function propose(args) {
58
+ authorize();
59
+ if (Object.keys(args).some(function (key) { return key !== 'scanId' && key !== 'candidateId'; })) throw new Error('Import approval cannot be supplied by an agent.');
60
+ var selected = ctx.discovery.selected(owner, session, args);
61
+ var key = JSON.stringify([owner, session.localId, generation, args.scanId, args.candidateId]);
62
+ var attempts = used.get(session);
63
+ if (!attempts || attempts.scanId !== args.scanId || attempts.generation !== generation) { attempts = { scanId: args.scanId, generation: generation, candidates: new Set() }; used.set(session, attempts); }
64
+ if (attempts.candidates.has(args.candidateId) || pending.has(key)) throw new Error('This import proposal has already been requested. Scan again for a new review.');
65
+ var candidate = ctx.discovery.candidate(owner, session, args);
66
+ if (!humans(session, owner) || session.loop && session.loop.active && session.loop.role !== 'crafting') return result({ status: 'consent_unavailable', imported: false, message: 'An authenticated owner must open this conversation to approve an import. Scheduled or unattended runs cannot approve for the user.' });
67
+ var question = review(candidate, owner, selected.config), requestId = 'mcp_import_' + crypto.randomUUID();
68
+ var controller = new AbortController(), timer, timedOut = false, sessionSignal = session.abortController && session.abortController.signal;
69
+ var entry = { controller: controller, requestId: requestId };
70
+ pending.set(key, entry); attempts.candidates.add(args.candidateId);
71
+ var parentAbort = function () { controller.abort(); };
72
+ if (sessionSignal) sessionSignal.addEventListener('abort', parentAbort, { once: true });
73
+ try {
74
+ timer = setTimeout(function () { timedOut = true; controller.abort(); }, Math.min(ctx.consentTimeout || 45000, 45000));
75
+ var handler = ctx.userInput && ctx.userInput(session);
76
+ var answer = await userInput.dispatchUserInput(function (request, respond) {
77
+ request.authorizeResponse = function (ws) {
78
+ try { authorize(); return pending.get(key) === entry && canRespond(ws, session, owner); } catch (error) { return false; }
79
+ };
80
+ if (typeof handler !== 'function') return respond.cancel('Structured owner consent is unavailable.');
81
+ return handler(request, respond);
82
+ }, { questions: [{ id: 'mcp_import', header: 'Import MCP', question: question, allowOther: false, options: [{ label: 'Keep unchanged' }, { label: 'Import disabled copy' }] }] }, { source: 'mcp-import', requestId: requestId, signal: controller.signal });
83
+ if (answer.status !== 'submitted' || !answer.answers.mcp_import || answer.answers.mcp_import.length !== 1 || answer.answers.mcp_import[0] !== 'Import disabled copy') return result({ status: timedOut ? 'expired' : answer.status === 'submitted' ? 'declined' : answer.status, imported: false, message: 'No connection was imported.' });
84
+ authorize();
85
+ if (pending.get(key) !== entry || !humans(session, owner)) throw new Error('Import consent is no longer available.');
86
+ // Consume before entering the queue. Queued mutation revalidates everything again.
87
+ pending.delete(key);
88
+ function authorizeImport() { authorize(); if (controller.signal.aborted) throw new Error('Import consent expired before the copy could be saved. Scan again.'); }
89
+ var id = await ctx.importConnection(owner, function () {
90
+ authorizeImport();
91
+ if (!humans(session, owner)) throw new Error('The owner disconnected before import.');
92
+ var live = ctx.discovery.selected(owner, session, args);
93
+ if (live.signature !== selected.signature || live.revision !== selected.revision) throw new Error('The reviewed candidate changed. Scan again.');
94
+ return live;
95
+ }, authorizeImport);
96
+ return result({ status: 'imported', imported: true, id: id, enabled: false, label: candidate.name, message: 'Saved a disabled copy in your private catalog. Configure credentials in MCP management; activation, testing and login are separate actions.' });
97
+ } finally {
98
+ clearTimeout(timer); pending.delete(key);
99
+ if (sessionSignal) sessionSignal.removeEventListener('abort', parentAbort);
100
+ }
101
+ }
102
+ function wrap(handler) { return function (args) { return Promise.resolve().then(function () { return handler(args || {}); }).catch(function (error) { return result({ status: 'refused', imported: false, message: error.message }, true); }); }; }
103
+ return [{ name: 'discover_mcp_connections', description: 'Read-only inspection of your vendor MCP sources. Returns redacted candidate IDs and provenance. Never executes commands, imports, activates, logs in or reads vendor OAuth stores. Use credential entry only in MCP management.', inputSchema: { type: 'object', properties: { vendor: { type: 'string' }, file: { type: 'string' } }, additionalProperties: false }, handler: wrap(discover) },
104
+ { name: 'propose_mcp_import', description: 'Present one exact discovered candidate to the authenticated owner for structured approval, then save a disabled copy only after explicit approval. Denial, cancellation, timeout or absent owner causes no import. Pass only scanId and candidateId; agent-provided approval is never accepted.', inputSchema: { type: 'object', properties: { scanId: { type: 'string' }, candidateId: { type: 'string' } }, required: ['scanId', 'candidateId'], additionalProperties: false }, handler: wrap(propose) }];
105
+ }
106
+ function getServer(session) {
107
+ try {
108
+ authorized(session, ownerOf(session), Number(session._sdkQueryGeneration || 0));
109
+ if (ctx.available && !ctx.available(session)) return null;
110
+ var tools = definitions(session);
111
+ return createProxy(SERVER, tools, function (name, args) { var tool = tools.find(function (item) { return item.name === name; }); return tool.handler(args); });
112
+ } catch (error) { return null; }
113
+ }
114
+ function destroy() { closed = true; pending.forEach(function (entry) { entry.controller.abort(); }); pending.clear(); used = new WeakMap(); viewers.clear(); }
115
+ return { getServer: getServer, registerViewer: registerViewer, destroy: destroy };
116
+ }
117
+ module.exports = { attachMcpAgentTools: attachMcpAgentTools, SERVER: SERVER };
@@ -1,39 +1,143 @@
1
- // Owner/project-scoped records with encrypted credentials; never sent to the UI.
1
+ // Owner-scoped remote records and project-scoped computer preferences with encrypted credentials; never sent to the UI.
2
2
  var fs = require('fs');
3
3
  var path = require('path');
4
4
  var crypto = require('crypto');
5
- function createConnectionStore(root, project) {
5
+ var equal = require('util').isDeepStrictEqual;
6
+
7
+ function createConnectionStore(root, project, options) {
6
8
  var directory = path.join(root, 'mcp-connections');
9
+ var shared = !!(options && options.shared);
7
10
  function scope(owner) {
8
11
  if (typeof owner !== 'string' || !owner || owner.length > 160) throw new Error('MCP account owner is required.');
9
- return JSON.stringify([project, owner]);
12
+ return shared ? JSON.stringify([owner]) : JSON.stringify([project, owner]);
10
13
  }
11
14
  function filename(owner) { return path.join(directory, crypto.createHash('sha256').update(scope(owner)).digest('hex') + '.json'); }
15
+ function legacyFilename(owner) { return path.join(directory, crypto.createHash('sha256').update(JSON.stringify([project, owner])).digest('hex') + '.json'); }
12
16
  function key() {
13
17
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
14
- var file = path.join(directory, '.key');
15
- try { fs.writeFileSync(file, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 }); }
16
- catch (error) { if (error.code !== 'EEXIST') throw error; }
17
- return fs.readFileSync(file);
18
+ var file = path.join(directory, '.key'), bytes;
19
+ try { bytes = fs.readFileSync(file); }
20
+ catch (error) {
21
+ if (error.code !== 'ENOENT') throw error;
22
+ var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
23
+ try {
24
+ fs.writeFileSync(temp, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 });
25
+ try { fs.linkSync(temp, file); } catch (publishError) { if (publishError.code !== 'EEXIST') throw publishError; }
26
+ } finally { fs.unlinkSync(temp); }
27
+ bytes = fs.readFileSync(file);
28
+ }
29
+ if (bytes.length !== 32) throw new Error('MCP encryption key is invalid.');
30
+ return bytes;
18
31
  }
19
- function load(owner) {
32
+ function decode(file, aad) {
20
33
  var data;
21
- try { data = JSON.parse(fs.readFileSync(filename(owner), 'utf8')); }
22
- catch (error) { if (error.code === 'ENOENT') return []; throw new Error('Could not read MCP connections.'); }
34
+ try { data = JSON.parse(fs.readFileSync(file, 'utf8')); }
35
+ catch (error) { if (error.code === 'ENOENT') return { records: [], revision: null, migratedProjects: [], retired: [] }; throw new Error('Could not read MCP connections.'); }
23
36
  var parts = data.encrypted.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
24
37
  var cipher = crypto.createDecipheriv('aes-256-gcm', key(), parts[0]);
25
- cipher.setAAD(Buffer.from(scope(owner))); cipher.setAuthTag(parts[1]);
26
- return JSON.parse(Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8'));
38
+ cipher.setAAD(Buffer.from(aad)); cipher.setAuthTag(parts[1]);
39
+ var value = JSON.parse(Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8'));
40
+ if (Array.isArray(value)) return { records: value, revision: null, migratedProjects: [], retired: [] };
41
+ return { records: Array.isArray(value.records) ? value.records : [], revision: value.revision || null, migratedProjects: Array.isArray(value.migratedProjects) ? value.migratedProjects : [], retired: value.retired || [] };
27
42
  }
28
- function save(owner, records) {
43
+ function encode(owner, state, expectedRevision) {
44
+ var current = decode(filename(owner), scope(owner));
45
+ if (expectedRevision !== undefined && current.revision !== expectedRevision) throw new Error('MCP connections changed. Please retry.');
46
+ var next = { records: state.records, revision: crypto.randomBytes(16).toString('hex'), migratedProjects: state.migratedProjects || [], retired: state.retired || [] };
29
47
  var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', key(), iv);
30
48
  cipher.setAAD(Buffer.from(scope(owner)));
31
- var bytes = Buffer.concat([cipher.update(JSON.stringify(records), 'utf8'), cipher.final()]);
49
+ var bytes = Buffer.concat([cipher.update(JSON.stringify(next), 'utf8'), cipher.final()]);
32
50
  var value = { encrypted: [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.') };
33
51
  var file = filename(owner), temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
34
52
  try { fs.writeFileSync(temp, JSON.stringify(value), { mode: 0o600 }); fs.renameSync(temp, file); }
35
53
  finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
54
+ return next.revision;
36
55
  }
37
- return { load: load, save: save };
56
+ function visible(records) { if (!shared) return records; return records.filter(function (record) { return record.source !== 'computer' || record.project === project; }); }
57
+ function connectionConflict(left, right) { return left.id && left.id === right.id || (left.url && right.url && left.url === right.url); }
58
+ function migrate(owner, state) {
59
+ if (!shared || state.migratedProjects.indexOf(project) >= 0) return state;
60
+ var legacy = decode(legacyFilename(owner), JSON.stringify([project, owner])).records;
61
+ var merged = state.records.slice();
62
+ legacy.forEach(function (record) {
63
+ var copy = Object.assign({}, record);
64
+ if (copy.source === 'computer') copy.project = project;
65
+ else if ((state.retired || []).some(function (removed) { return connectionConflict(removed, copy); })) return;
66
+ var existing = merged.filter(function (current) { return current.source === copy.source && (copy.source === 'computer' ? current.project === project && current.name === copy.name : connectionConflict(current, copy)); });
67
+ if (existing.length && existing.some(function (current) { return !equal(current, copy); })) throw new Error('MCP connection migration conflict. Resolve the legacy project connection before retrying.');
68
+ if (!existing.length) merged.push(copy);
69
+ });
70
+ state.records = merged;
71
+ state.migratedProjects = state.migratedProjects.concat([project]);
72
+ return state;
73
+ }
74
+ function readUnlocked(owner) {
75
+ var state = decode(filename(owner), scope(owner));
76
+ if (shared) {
77
+ var unique = [];
78
+ state.records.forEach(function (record) {
79
+ var existing = unique.find(function (current) {
80
+ return record.source === 'computer' ? current.source === 'computer' && record.project === current.project && record.name === current.name : current.source !== 'computer' && connectionConflict(record, current);
81
+ });
82
+ if (existing && !equal(existing, record)) throw new Error('Conflicting duplicate MCP records require resolution.');
83
+ if (!existing) unique.push(record);
84
+ });
85
+ if (unique.length !== state.records.length) { state.records = unique; state.revision = encode(owner, state, state.revision); }
86
+ }
87
+ if (shared && state.migratedProjects.indexOf(project) < 0) { state = migrate(owner, state); state.revision = encode(owner, state, state.revision); }
88
+ return state;
89
+ }
90
+ // Fail closed on contention. The lock covers migration and the full CAS read/write,
91
+ // including callers in a second process using the same storage root.
92
+ function locked(owner, operation) {
93
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
94
+ var file = filename(owner) + '.lock', fd;
95
+ try { fd = fs.openSync(file, 'wx', 0o600); }
96
+ catch (error) { if (error.code === 'EEXIST') throw new Error('MCP connections are busy. Please retry.'); throw error; }
97
+ try { fs.writeFileSync(fd, String(process.pid)); return operation(); }
98
+ finally { fs.closeSync(fd); fs.unlinkSync(file); }
99
+ }
100
+ function snapshot(owner) {
101
+ return locked(owner, function () {
102
+ var state = readUnlocked(owner);
103
+ return { records: visible(state.records), revision: state.revision };
104
+ });
105
+ }
106
+ function load(owner) { return snapshot(owner).records; }
107
+ function revision(owner) { return snapshot(owner).revision; }
108
+ function validate(records) {
109
+ if (!Array.isArray(records)) throw new Error('MCP connection snapshot must be an array.');
110
+ records.forEach(function (record, index) {
111
+ if (!record || typeof record !== 'object') throw new Error('Invalid MCP connection record.');
112
+ if (records.slice(0, index).some(function (previous) {
113
+ return record.id && previous.id === record.id ||
114
+ record.source === 'computer' && previous.source === 'computer' && record.name === previous.name ||
115
+ record.source !== 'computer' && previous.source !== 'computer' && record.url && record.url === previous.url;
116
+ })) throw new Error('Duplicate MCP connection in snapshot.');
117
+ if (shared && record.source === 'computer' && record.project && record.project !== project) throw new Error('Foreign project computer preference.');
118
+ });
119
+ }
120
+ // Remote rows are the complete owner snapshot. Computer rows replace only this
121
+ // project's preferences; omitted remote rows are deleted, never merged back.
122
+ function save(owner, records, expectedRevision) {
123
+ validate(records);
124
+ return locked(owner, function () {
125
+ var state = readUnlocked(owner);
126
+ if (expectedRevision !== undefined && state.revision !== expectedRevision) throw new Error('MCP connections changed. Please retry.');
127
+ if (!shared) return encode(owner, { records: records, migratedProjects: [] }, state.revision);
128
+ var incomingRemote = records.filter(function (record) { return record.source !== 'computer'; });
129
+ var currentComputer = records.filter(function (record) { return record.source === 'computer'; }).map(function (record) { return Object.assign({}, record, { project: project }); });
130
+ var otherComputer = state.records.filter(function (record) { return record.source === 'computer' && record.project !== project; });
131
+ var removed = state.records.filter(function (record) {
132
+ return record.source !== 'computer' && !incomingRemote.some(function (incoming) { return connectionConflict(record, incoming); });
133
+ }).map(function (record) { return { id: record.id, url: record.url }; });
134
+ state.retired = (state.retired || []).concat(removed).filter(function (record, index, all) {
135
+ return all.findIndex(function (other) { return other.id === record.id && other.url === record.url; }) === index;
136
+ });
137
+ state.records = incomingRemote.concat(otherComputer, currentComputer);
138
+ return encode(owner, state, state.revision);
139
+ });
140
+ }
141
+ return { load: load, save: save, revision: revision, snapshot: snapshot, namespace: fs.existsSync(root) ? fs.realpathSync(root) : path.resolve(root) };
38
142
  }
39
143
  module.exports = { createConnectionStore: createConnectionStore };