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
@@ -774,6 +774,7 @@ function attachDebate(ctx) {
774
774
  var identityMate = matesModule.getMate(debate.mateCtx, msg.mateId);
775
775
  var identityProfile = identityMate && identityMate.profile ? identityMate.profile : (identityMate || {});
776
776
  if (!msg.mateName) msg.mateName = identity.name;
777
+ if (!msg.avatarCatalogId) msg.avatarCatalogId = identityProfile.avatarCatalogId || "";
777
778
  if (!msg.avatarStyle) msg.avatarStyle = identity.avatarStyle;
778
779
  if (!msg.avatarSeed) msg.avatarSeed = identity.avatarSeed;
779
780
  if (!msg.avatarColor) msg.avatarColor = identity.avatarColor;
@@ -12,7 +12,7 @@ function unsafeReason(session, isPaired) {
12
12
  if (session.homeDebatePlanning || session.homeDebatePhase || session.debateSetupMode || session.debateState || session._debate) return "Debate sessions cannot receive project follow-ups.";
13
13
  if (session.loop || session.spawn || session.handoff || session.assignment || session.singleTurn || session._pairDelegation || isPaired) return "Orchestrated sessions cannot receive project follow-ups.";
14
14
  if (session.dangerouslySkipPermissions || session.permissionMode === "bypassPermissions") return "Full-access sessions cannot receive project follow-ups.";
15
- if (session.destroying || session.scheduledMessage || session.rateLimitAutoContinuePending || session.taskStopRequested || session._runtimeRefreshRequested || session.pendingPush && session.pendingPush.length) return "Target session is not safely resumable right now.";
15
+ if (session.destroying || session.scheduledMessage || session.rateLimitAutoContinuePending || session.taskStopRequested || session._pendingMessageDrainPaused || session._runtimeRefreshRequested || session.pendingPush && session.pendingPush.length) return "Target session is not safely resumable right now.";
16
16
  return "";
17
17
  }
18
18
 
@@ -36,6 +36,7 @@ function attachDelegatedFollowUp(ctx) {
36
36
  var paired = !!(ctx.isPairedSession && ctx.isPairedSession(session));
37
37
  var reason = unsafeReason(session, paired);
38
38
  if (reason) throw new Error(reason);
39
+ if (ctx.isPendingQueuePaused && ctx.isPendingQueuePaused(session)) throw new Error("The Driver queue is stopped; explicit owner resume is required.");
39
40
  if (!ctx.canResumeVendor(session.vendor)) throw new Error("Target session provider cannot safely resume this conversation.");
40
41
  return { session: session, sessionRef: targetSessionRef, title: session.title || "Existing session" };
41
42
  }
@@ -0,0 +1,65 @@
1
+ // Reserve a new project directory before running Git or changing ownership.
2
+ // Filesystem identity checks also keep failed attempts from deleting targets
3
+ // replaced by another actor. Hostile OS/root mutation is outside this boundary.
4
+ var fs = require("fs");
5
+ var path = require("path");
6
+
7
+ function canonical(candidate) {
8
+ var current = path.resolve(candidate);
9
+ var missing = [];
10
+ while (true) {
11
+ try { return path.join.apply(path, [fs.realpathSync(current)].concat(missing)); }
12
+ catch (error) {
13
+ if (error.code !== "ENOENT") throw error;
14
+ // A dangling symlink is an existing object, not a missing directory.
15
+ try { fs.lstatSync(current); throw new Error("A project path contains a dangling symlink."); }
16
+ catch (statError) { if (statError.code !== "ENOENT") throw statError; }
17
+ var parent = path.dirname(current);
18
+ if (parent === current) throw error;
19
+ missing.unshift(path.basename(current));
20
+ current = parent;
21
+ }
22
+ }
23
+ }
24
+
25
+ function inside(root, candidate) {
26
+ var relative = path.relative(root, candidate);
27
+ return relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
28
+ }
29
+
30
+ function rejectIntersection(target, projects, userId) {
31
+ (projects || []).forEach(function (project) {
32
+ var root = canonical(project.path);
33
+ if (target === root || inside(target, root) || (project.ownerId !== userId && inside(root, target))) {
34
+ throw new Error("The destination intersects a registered project.");
35
+ }
36
+ });
37
+ }
38
+
39
+ function claim(options) {
40
+ var slug = options.slug;
41
+ if (!slug || slug === "." || slug === ".." || path.basename(slug) !== slug) throw new Error("Invalid project directory name.");
42
+ var base = canonical(options.baseDir);
43
+ var target = path.join(base, slug);
44
+ rejectIntersection(target, options.projects, options.userId);
45
+ fs.mkdirSync(base, { recursive: true });
46
+ if (fs.realpathSync(options.baseDir) !== base) throw new Error("The project base directory changed.");
47
+ rejectIntersection(target, options.projects, options.userId);
48
+ // EEXIST rejects directories, files, symlinks and concurrent claims alike.
49
+ fs.mkdirSync(target, { mode: 0o700 });
50
+ var identity = fs.lstatSync(target);
51
+ function assertOwned() {
52
+ var current = fs.lstatSync(target);
53
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino || fs.realpathSync(base) !== base) {
54
+ throw new Error("The claimed project directory changed.");
55
+ }
56
+ }
57
+ function cleanup() {
58
+ try { assertOwned(); } catch (error) { return false; }
59
+ fs.rmSync(target, { recursive: true });
60
+ return true;
61
+ }
62
+ return { path: target, assertOwned: assertOwned, cleanup: cleanup };
63
+ }
64
+
65
+ module.exports = { claim: claim, canonical: canonical };
@@ -0,0 +1,61 @@
1
+ // Shared file-viewer effects. Authority and scope are derived by each caller.
2
+ var fs = require('fs'), path = require('path'), crypto = require('crypto');
3
+ var resolve = require('./project-file-path').resolveFilePath;
4
+ function hash(content) { return crypto.createHash('sha256').update(content).digest('hex'); }
5
+ function attachFileOperations(ctx) {
6
+ function list(scope, requested) {
7
+ var dir = resolve(ctx.cwd, requested || '.', scope);
8
+ var items = scope.identity ? ctx.fsAsUser('list', { dir: dir }, scope.identity) : fs.readdirSync(dir, { withFileTypes: true }).map(function (item) { return { name: item.name, isDir: item.isDirectory() }; });
9
+ return items.filter(function (item) { return !item.isDir || !ctx.IGNORED_DIRS.has(item.name); }).map(function (item) { return { name: item.name, type: item.isDir ? 'dir' : 'file', path: path.relative(ctx.cwd, path.join(dir, item.name)).split(path.sep).join('/') }; });
10
+ }
11
+ function read(scope, requested) {
12
+ var file = resolve(ctx.cwd, requested, scope), identity = scope.identity;
13
+ var stat = identity ? ctx.fsAsUser('stat', { file: file }, identity) : fs.statSync(file);
14
+ if (stat.size > ctx.FS_MAX_SIZE) return { binary: true, size: stat.size, error: 'File too large (' + (stat.size / 1024 / 1024).toFixed(1) + ' MB)' };
15
+ var ext = path.extname(file).toLowerCase();
16
+ if (ctx.BINARY_EXTS.has(ext)) {
17
+ var binary = { binary: true, size: stat.size };
18
+ if (ctx.IMAGE_EXTS.has(ext)) binary.imageUrl = 'api/file?path=' + encodeURIComponent(requested);
19
+ return binary;
20
+ }
21
+ return { content: identity ? ctx.fsAsUser('read', { file: file, readContent: true }, identity).content : fs.readFileSync(file, 'utf8'), size: stat.size };
22
+ }
23
+ function snapshot(scope, requested) {
24
+ var file = resolve(ctx.cwd, requested, scope), stat = fs.lstatSync(file);
25
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536) throw new Error('Select a regular, singly linked text file of at most 64 KB.');
26
+ var result = read(scope, requested);
27
+ if (result.binary || typeof result.content !== 'string' || result.content.indexOf('\0') >= 0) throw new Error('Binary files are unavailable for canonical editing.');
28
+ return { dev: stat.dev, ino: stat.ino, mode: stat.mode, uid: stat.uid, gid: stat.gid, size: stat.size, mtime: stat.mtimeMs, hash: hash(result.content) };
29
+ }
30
+ function write(scope, requested, content, control) {
31
+ var file = resolve(ctx.cwd, requested, scope), attempted = false;
32
+ if (!control) {
33
+ if (scope.identity) ctx.fsAsUser('write', { file: file, content: content }, scope.identity);
34
+ else fs.writeFileSync(file, content, 'utf8');
35
+ return { ok: true };
36
+ }
37
+ if (typeof fs.constants.O_NOFOLLOW !== 'number') throw new Error('Canonical editing requires no-follow file descriptors on this platform.');
38
+ control.revalidate();
39
+ if (JSON.stringify(snapshot(scope, requested)) !== JSON.stringify(control.expected)) throw new Error('The exact file revision changed.');
40
+ try {
41
+ attempted = true;
42
+ if (scope.identity) ctx.fsAsUser('write_exact', { file: file, content: content, expected: control.expected, root: fs.realpathSync(ctx.cwd) }, scope.identity);
43
+ else {
44
+ var fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);
45
+ try {
46
+ var stat = fs.fstatSync(fd), expected = control.expected;
47
+ if (!stat.isFile() || stat.nlink !== 1 || stat.dev !== expected.dev || stat.ino !== expected.ino || stat.mode !== expected.mode || stat.uid !== expected.uid || stat.gid !== expected.gid || stat.size !== expected.size || stat.mtimeMs !== expected.mtime || hash(fs.readFileSync(fd, 'utf8')) !== expected.hash) throw new Error('The open file revision changed.');
48
+ control.revalidate();
49
+ fs.ftruncateSync(fd, 0);
50
+ var bytes = Buffer.from(content, 'utf8'), offset = 0;
51
+ while (offset < bytes.length) { var written = fs.writeSync(fd, bytes, offset, bytes.length - offset, offset); if (!written) throw new Error('File write made no progress.'); offset += written; }
52
+ fs.fsyncSync(fd);
53
+ } finally { fs.closeSync(fd); }
54
+ }
55
+ control.revalidate();
56
+ return { status: 'completed', persisted: true, path: requested };
57
+ } catch (error) { error.productControlResult = { persisted: attempted ? 'unknown' : false, retrySafe: false, error: error.message }; throw error; }
58
+ }
59
+ return { list: list, read: read, write: write, snapshot: snapshot };
60
+ }
61
+ module.exports = { attachFileOperations: attachFileOperations };
@@ -37,6 +37,7 @@ function attachFilesystem(ctx) {
37
37
 
38
38
  var access = ctx.requestAccess || attachRequestAccess(ctx);
39
39
  var fileHistory = attachFileHistory(ctx, access);
40
+ var operations = require("./project-file-operations").attachFileOperations(ctx);
40
41
 
41
42
  function handleFilesystemMessage(ws, msg) {
42
43
  var fileScope;
@@ -57,40 +58,11 @@ function attachFilesystem(ctx) {
57
58
 
58
59
  // --- fs_list ---
59
60
  if (msg.type === "fs_list") {
60
- var fsListIdentity = fileScope.identity;
61
- var fsDir = fileTarget;
62
61
  try {
63
- var fsListUserInfo = fsListIdentity;
64
- var entries = [];
65
- if (fsListUserInfo) {
66
- // Run as target OS user to respect Linux file permissions
67
- var rawEntries = fsAsUser("list", { dir: fsDir }, fsListUserInfo);
68
- for (var fi = 0; fi < rawEntries.length; fi++) {
69
- var re = rawEntries[fi];
70
- if (re.isDir && IGNORED_DIRS.has(re.name)) continue;
71
- entries.push({
72
- name: re.name,
73
- type: re.isDir ? "dir" : "file",
74
- path: path.relative(cwd, path.join(fsDir, re.name)).split(path.sep).join("/"),
75
- });
76
- }
77
- } else {
78
- var items = fs.readdirSync(fsDir, { withFileTypes: true });
79
- for (var fi = 0; fi < items.length; fi++) {
80
- var item = items[fi];
81
- if (item.isDirectory() && IGNORED_DIRS.has(item.name)) continue;
82
- entries.push({
83
- name: item.name,
84
- type: item.isDirectory() ? "dir" : "file",
85
- path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
86
- });
87
- }
88
- }
89
- sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
90
- // Auto-watch the directory for changes
91
- startDirWatch(ws, msg.path || ".");
62
+ sendTo(ws, { type: 'fs_list_result', path: msg.path || '.', entries: operations.list(fileScope, msg.path || '.') });
63
+ startDirWatch(ws, msg.path || '.');
92
64
  } catch (e) {
93
- sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: fileError(e).message, errorCode: fileError(e).code });
65
+ sendTo(ws, { type: 'fs_list_result', path: msg.path, entries: [], error: fileError(e).message, errorCode: fileError(e).code });
94
66
  }
95
67
  return true;
96
68
  }
@@ -143,62 +115,15 @@ function attachFilesystem(ctx) {
143
115
 
144
116
  // --- fs_read ---
145
117
  if (msg.type === "fs_read") {
146
- var fsReadIdentity = fileScope.identity;
147
- var fsFile = fileTarget;
148
- try {
149
- var fsReadUserInfo = fsReadIdentity;
150
- var ext = path.extname(fsFile).toLowerCase();
151
- if (fsReadUserInfo) {
152
- // Run stat and read as target OS user
153
- var statResult = fsAsUser("stat", { file: fsFile }, fsReadUserInfo);
154
- if (statResult.size > FS_MAX_SIZE) {
155
- sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: statResult.size, error: "File too large (" + (statResult.size / 1024 / 1024).toFixed(1) + " MB)" });
156
- return true;
157
- }
158
- if (BINARY_EXTS.has(ext)) {
159
- var result = { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: statResult.size };
160
- if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
161
- sendTo(ws, result);
162
- return true;
163
- }
164
- var readResult = fsAsUser("read", { file: fsFile, readContent: true }, fsReadUserInfo);
165
- sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, content: readResult.content, size: statResult.size });
166
- } else {
167
- var stat = fs.statSync(fsFile);
168
- if (stat.size > FS_MAX_SIZE) {
169
- sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: stat.size, error: "File too large (" + (stat.size / 1024 / 1024).toFixed(1) + " MB)" });
170
- return true;
171
- }
172
- if (BINARY_EXTS.has(ext)) {
173
- var result = { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: stat.size };
174
- if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
175
- sendTo(ws, result);
176
- return true;
177
- }
178
- var content = fs.readFileSync(fsFile, "utf8");
179
- sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, content: content, size: stat.size });
180
- }
181
- } catch (e) {
182
- sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: fileError(e).message, errorCode: fileError(e).code });
183
- }
118
+ var metadata = { type: 'fs_read_result', path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId };
119
+ try { sendTo(ws, Object.assign(metadata, operations.read(fileScope, msg.path))); }
120
+ catch (e) { sendTo(ws, Object.assign(metadata, { error: fileError(e).message, errorCode: fileError(e).code })); }
184
121
  return true;
185
122
  }
186
123
 
187
- // --- fs_write ---
188
- if (msg.type === "fs_write") {
189
- var fsWriteIdentity = fileScope.identity;
190
- var fsWriteFile = fileTarget;
191
- try {
192
- var fsWriteUserInfo = fsWriteIdentity;
193
- if (fsWriteUserInfo) {
194
- fsAsUser("write", { file: fsWriteFile, content: msg.content || "" }, fsWriteUserInfo);
195
- } else {
196
- fs.writeFileSync(fsWriteFile, msg.content || "", "utf8");
197
- }
198
- sendTo(ws, { type: "fs_write_result", path: msg.path, ok: true });
199
- } catch (e) {
200
- sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: fileError(e).message, errorCode: fileError(e).code });
201
- }
124
+ if (msg.type === 'fs_write') {
125
+ try { sendTo(ws, Object.assign({ type: 'fs_write_result', path: msg.path }, operations.write(fileScope, msg.path, msg.content || ''))); }
126
+ catch (e) { sendTo(ws, { type: 'fs_write_result', path: msg.path, ok: false, error: fileError(e).message, errorCode: fileError(e).code }); }
202
127
  return true;
203
128
  }
204
129
 
@@ -239,8 +164,8 @@ function attachFilesystem(ctx) {
239
164
  var setResult;
240
165
  try { setResult = opts.onSetProjectEnv(targetSlug, msg.envrc || ""); }
241
166
  catch (error) { sendTo(ws, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, ok: false, error: error.message }); return true; }
242
- if (setResult.ok) onEnvironmentChanged();
243
- sendTo(ws, { type: "set_project_env_result", ok: setResult.ok, slug: targetSlug, requestId: msg.requestId, error: setResult.error, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined });
167
+ if (setResult.ok && !setResult.runtimeRefreshed) onEnvironmentChanged();
168
+ sendTo(ws, Object.assign({}, setResult, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, timing: setResult.ok ? "Applies to newly created coding-agent processes. Active processes keep their current environment." : undefined }));
244
169
  } else {
245
170
  sendTo(ws, { type: "set_project_env_result", slug: targetSlug, requestId: msg.requestId, ok: false, error: "Not supported" });
246
171
  }
@@ -297,6 +222,7 @@ function attachFilesystem(ctx) {
297
222
 
298
223
  return {
299
224
  handleFilesystemMessage: handleFilesystemMessage,
225
+ productOperations: operations,
300
226
  };
301
227
  }
302
228
 
@@ -16,7 +16,7 @@ function createFullAccessService(ctx) {
16
16
  enabled: session.permissionMode === "bypassPermissions" };
17
17
  }
18
18
  function persist(session) {
19
- sm.saveSessionFile(session);
19
+ if (sm.saveSessionFile(session) === false) throw new Error("Could not persist permission mode.");
20
20
  sm.broadcastSessionList();
21
21
  if (typeof sm.sendToSession === "function") sm.sendToSession(session, { type: "session_full_access_changed", id: session.localId,
22
22
  enabled: session.permissionMode === "bypassPermissions", permissionMode: session.permissionMode });
@@ -55,7 +55,28 @@ function createFullAccessService(ctx) {
55
55
  throw error;
56
56
  }
57
57
  }
58
- function setMode(session, mode) { return serialize(session, function () { return applyMode(session, mode); }); }
58
+ function setMode(session, mode, options) {
59
+ return serialize(session, function () {
60
+ if (!options || typeof options.revalidate !== "function") return applyMode(session, mode);
61
+ options.revalidate();
62
+ var observed = { revision: Number(session._permissionObservationRevision || 0), mode: session.effectivePermissionMode, reason: session.permissionModeFallbackReason };
63
+ return require("./session-config-transition").applySessionConfig({
64
+ sm: sm, session: session, revalidate: options.revalidate, method: "setPermissionMode", nativeValue: mode === "bypassPermissions" ? "default" : mode,
65
+ changes: function () {
66
+ var newer = Number(session._permissionObservationRevision || 0) !== observed.revision || session.effectivePermissionMode !== observed.mode || session.permissionModeFallbackReason !== observed.reason;
67
+ return { permissionMode: mode, permissionModeBeforeFullAccess: mode === "bypassPermissions" ? (session.permissionMode || sm.currentPermissionMode || "default") : null,
68
+ effectivePermissionMode: newer ? session.effectivePermissionMode : null, permissionModeFallbackReason: newer ? session.permissionModeFallbackReason : null };
69
+ },
70
+ publish: function () {
71
+ sm.broadcastSessionList();
72
+ if (typeof sm.sendToSession === "function") {
73
+ sm.sendToSession(session, { type: "session_full_access_changed", id: session.localId, enabled: mode === "bypassPermissions", permissionMode: mode });
74
+ sm.sendToSession(session, { type: "config_state", sessionId: session.localId, mode: mode, effectivePermissionMode: session.effectivePermissionMode || null, vendor: session.vendor || sm.defaultVendor || "claude" });
75
+ }
76
+ },
77
+ });
78
+ });
79
+ }
59
80
  function setEnabled(session, enabled) {
60
81
  return serialize(session, function () {
61
82
  return applyMode(session, enabled ? "bypassPermissions" : (session.permissionModeBeforeFullAccess || "default"));
@@ -44,21 +44,30 @@ function attachKnowledge(ctx) {
44
44
  return { type: user ? "user" : "system", userId: user ? user.id : null, displayName: user ? (user.displayName || user.username) : null };
45
45
  }
46
46
 
47
- function mirror(ws, name) {
47
+ function mirrorActor(who, name) {
48
48
  if (!isMate) return { ok: true, names: [] };
49
- var summary = mateSync.syncMateSource({ mateDir: cwd, fileName: name, baseDir: ctx.knowledgeBaseDir, actor: actor(ws) });
49
+ var summary = mateSync.syncMateSource({ mateDir: cwd, fileName: name, baseDir: ctx.knowledgeBaseDir, actor: who });
50
50
  return { ok: !!summary && summary.failed === 0, names: [name], error: summary && summary.errors[0] ? summary.errors[0].message : "Knowledge index synchronization failed." };
51
51
  }
52
52
 
53
- function mirrorMany(ws, names) {
53
+ function mirror(ws, name) { return mirrorActor(actor(ws), name); }
54
+
55
+ function mirrorManyActor(who, names) {
54
56
  var failures = [];
55
57
  for (var i = 0; i < names.length; i++) {
56
- var mirrored = mirror(ws, names[i]);
58
+ var mirrored = mirrorActor(who, names[i]);
57
59
  if (!mirrored.ok) failures.push(mirrored.error);
58
60
  }
59
61
  return { ok: failures.length === 0, names: names.slice(), error: failures[0] || null };
60
62
  }
61
63
 
64
+ function mirrorMany(ws, names) { return mirrorManyActor(actor(ws), names); }
65
+
66
+ function controlMirror(who, names) {
67
+ try { var state = mirrorManyActor(who, names); return { indexSyncPending: !state.ok, syncNames: names, syncError: state.ok ? null : state.error }; }
68
+ catch (error) { return { indexSyncPending: true, syncNames: names, syncError: error.message }; }
69
+ }
70
+
62
71
  function addSyncState(payload, sync) {
63
72
  if (sync.ok) return payload;
64
73
  return Object.assign(payload, { indexSyncPending: true, syncNames: sync.names, syncError: sync.error });
@@ -188,6 +197,22 @@ function attachKnowledge(ctx) {
188
197
  return databaseReady.then(function () { return combineSearch(databases.listCached({})); });
189
198
  }
190
199
 
200
+ function renameDocument(msg, who) {
201
+ var beforeMoveState = documentIndex(); var beforeMove = beforeMoveState.documents; var moveTarget = findDocument(beforeMove, msg.from, msg.documentId); var linkChanges = moveTarget ? rewriteMoveLinks(beforeMove, moveTarget, msg.name) : [];
202
+ var renamed = files.rename(msg.from, msg.name, msg.expectedRevision || null); var linkUpdateFailures = []; var updatedLinks = 0;
203
+ var updatedDocuments = []; var mirrorNames = [msg.from, renamed.name];
204
+ for (var u = 0; u < linkChanges.length; u++) {
205
+ var change = linkChanges[u]; var writeName = change.id === renamed.id ? renamed.name : change.name; var expected = change.id === renamed.id ? renamed.revision : change.revision;
206
+ try { var rewritten = files.save(writeName, change.content, expected); updatedLinks += change.count; updatedDocuments.push(rewritten); mirrorNames.push(rewritten.name); }
207
+ catch (linkError) { linkUpdateFailures.push({ name: writeName, error: linkError.message }); }
208
+ }
209
+ renamed = files.read(renamed.name);
210
+ var uniqueMirrorNames = mirrorNames.filter(function (name, at) { return mirrorNames.indexOf(name) === at; }); var renameSync = mirrorManyActor(who, uniqueMirrorNames);
211
+ return addSyncState({ type: "knowledge_renamed", id: renamed.id, from: msg.from, name: renamed.name, revision: renamed.revision,
212
+ content: renamed.content, updatedLinks: updatedLinks, linkUpdateFailures: linkUpdateFailures,
213
+ updatedDocuments: updatedDocuments.map(function (item) { return { id: item.id, name: item.name, content: item.content, revision: item.revision }; }) }, renameSync);
214
+ }
215
+
191
216
  function handleKnowledgeMessage(ws, msg) {
192
217
  if (msg.type === "knowledge_list") {
193
218
  try { var listed = listKnowledgeItems(ws); if (listed && typeof listed.then === "function") listed.then(function (items) { response(ws, msg, { type: "knowledge_list", files: items, drafts: drafts.list(ws && ws._clayUser && ws._clayUser.id || "system") }); }).catch(function (error) { failure(ws, msg, error); }); else response(ws, msg, { type: "knowledge_list", files: listed, drafts: drafts.list(ws && ws._clayUser && ws._clayUser.id || "system") }); } catch (error) { failure(ws, msg, error); }
@@ -236,19 +261,7 @@ function attachKnowledge(ctx) {
236
261
  if (msg.type === "knowledge_rename") {
237
262
  if (!requireWrite(ws, msg)) return true;
238
263
  try {
239
- var beforeMoveState = documentIndex(); var beforeMove = beforeMoveState.documents; var moveTarget = findDocument(beforeMove, msg.from, msg.documentId); var linkChanges = moveTarget ? rewriteMoveLinks(beforeMove, moveTarget, msg.name) : [];
240
- var renamed = files.rename(msg.from, msg.name, msg.expectedRevision || null); var linkUpdateFailures = []; var updatedLinks = 0;
241
- var updatedDocuments = []; var mirrorNames = [msg.from, renamed.name];
242
- for (var u = 0; u < linkChanges.length; u++) {
243
- var change = linkChanges[u]; var writeName = change.id === renamed.id ? renamed.name : change.name; var expected = change.id === renamed.id ? renamed.revision : change.revision;
244
- try { var rewritten = files.save(writeName, change.content, expected); updatedLinks += change.count; updatedDocuments.push(rewritten); mirrorNames.push(rewritten.name); }
245
- catch (linkError) { linkUpdateFailures.push({ name: writeName, error: linkError.message }); }
246
- }
247
- renamed = files.read(renamed.name);
248
- var uniqueMirrorNames = mirrorNames.filter(function (name, at) { return mirrorNames.indexOf(name) === at; }); var renameSync = mirrorMany(ws, uniqueMirrorNames);
249
- response(ws, msg, addSyncState({ type: "knowledge_renamed", id: renamed.id, from: msg.from, name: renamed.name, revision: renamed.revision,
250
- content: renamed.content, updatedLinks: updatedLinks, linkUpdateFailures: linkUpdateFailures,
251
- updatedDocuments: updatedDocuments.map(function (item) { return { id: item.id, name: item.name, content: item.content, revision: item.revision }; }) }, renameSync));
264
+ response(ws, msg, renameDocument(msg, actor(ws)));
252
265
  sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
253
266
  } catch (error) { failure(ws, msg, error); }
254
267
  return true;
@@ -357,7 +370,8 @@ function attachKnowledge(ctx) {
357
370
  return false;
358
371
  }
359
372
 
360
- return { handleKnowledgeMessage: handleKnowledgeMessage, listKnowledgeFiles: listKnowledgeFiles, databases: databases };
373
+ var productOperations = require('./knowledge-product-operations').createKnowledgeProductOperations({ files: files, mirror: controlMirror, rename: renameDocument });
374
+ return { productOperations: productOperations, handleKnowledgeMessage: handleKnowledgeMessage, listKnowledgeFiles: listKnowledgeFiles, databases: databases };
361
375
  }
362
376
 
363
377
  module.exports = { attachKnowledge: attachKnowledge };
@@ -0,0 +1,98 @@
1
+ // Durable operation identities and independent closure proofs survive config-save failures.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var crypto = require('crypto');
5
+ function attachLifecycleOperations(ctx) {
6
+ function operations() { return ctx.config().projectLifecycleOperations || {}; }
7
+ function proofFile(id) {
8
+ if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error('Invalid lifecycle operation identity.');
9
+ return path.join(ctx.storageDir, 'project-lifecycle-operations', id + '.json');
10
+ }
11
+ function closed(op) {
12
+ try { var proof = JSON.parse(fs.readFileSync(proofFile(op.id), 'utf8')); return proof.id === op.id && proof.targets === JSON.stringify(op.targets); }
13
+ catch (error) { if (error.code === 'ENOENT') return false; throw error; }
14
+ }
15
+ function confirmClosed(op) {
16
+ var file = proofFile(op.id); fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
17
+ fs.writeFileSync(file + '.tmp', JSON.stringify({ id: op.id, targets: JSON.stringify(op.targets) }), { mode: 0o600 });
18
+ fs.renameSync(file + '.tmp', file);
19
+ }
20
+ function partial(op, error) {
21
+ return { ok: false, status: 'failed', action: op.action, outcome: op.restoration ? 'restoration_publication_pending' : op.action === 'remove' ? 'removal_reserved_runtime_pending' : 'publication_pending', persisted: true,
22
+ retrySafe: false, recoveryAction: 'reconcile', operationId: op.id, slug: op.slug, error: error || 'The saved operation needs explicit owner-reviewed reconciliation.' };
23
+ }
24
+ function begin(next, project, action, result, correlation, targets) {
25
+ var id = crypto.randomUUID();
26
+ if (action === 'remove') { project.lifecycleState = 'removing'; project.lifecycleOperationId = id; }
27
+ var op = { id: id, action: action, slug: project.slug, projectKnowledgeId: project.projectKnowledgeId, ownerId: correlation ? correlation.ownerId : project.ownerId || null,
28
+ sourceKey: correlation && correlation.sourceKey || null, expected: JSON.stringify(project), directory: ctx.directory(project.path), targets: targets || [], result: result,
29
+ correlation: correlation ? { key: correlation.key, fingerprint: correlation.fingerprint, ownerId: correlation.ownerId, sourceKey: correlation.sourceKey } : null, phase: 'pending' };
30
+ var all = Object.assign({}, next.projectLifecycleOperations || {});
31
+ if (Object.keys(all).length >= 5000) throw new Error('Lifecycle operation capacity reached.');
32
+ all[id] = op; next.projectLifecycleOperations = all;
33
+ ctx.record(next, correlation, partial(op), project, action); return op;
34
+ }
35
+ function exact(op) {
36
+ var project = ctx.find(op.slug);
37
+ if (!project || project.projectKnowledgeId !== op.projectKnowledgeId || JSON.stringify(project) !== op.expected || JSON.stringify(ctx.directory(project.path)) !== JSON.stringify(op.directory)) throw new Error('The exact saved registration changed. Recovery will not overwrite or delete it.');
38
+ return project;
39
+ }
40
+ function inspect(slug, id, principal) {
41
+ var op = id ? operations()[id] : Object.values(operations()).reverse().find(function (item) { return item.slug === slug && item.phase === 'pending'; });
42
+ var current = ctx.find(slug);
43
+ if (!op || op.slug !== slug || (op.ownerId !== (principal.userId || null) && (!current || current.ownerId !== principal.userId))) throw new Error('No owned lifecycle operation is available.');
44
+ var conflict = op.phase !== 'completed' && JSON.stringify(current) !== op.expected;
45
+ return { registrationTitle: current && current.ownerId === principal.userId ? current.title || current.slug : undefined, conflict: conflict, operationId: op.id, action: op.action, phase: op.phase, runtimeClosureConfirmed: op.action === 'remove' && closed(op), revision: JSON.stringify([op, current]) };
46
+ }
47
+ function complete(next, op, project) {
48
+ var all = Object.assign({}, next.projectLifecycleOperations);
49
+ all[op.id] = Object.assign({}, op, { phase: 'completed' }); next.projectLifecycleOperations = all;
50
+ ctx.record(next, op.correlation, Object.assign({}, op.result, { operationId: op.id }), project, op.action);
51
+ }
52
+ async function reconcile(slug, id, principal, authorize, validate, restore) {
53
+ var info = inspect(slug, id, principal), op = operations()[info.operationId];
54
+ if (op.phase === 'completed') return Object.assign({}, op.result, { operationId: op.id });
55
+ if (validate) validate();
56
+ try {
57
+ if (op.action === 'remove' && !op.restoration) {
58
+ await ctx.relay().closeProjectLifecycle(op.targets, closed(op));
59
+ if (ctx.relay().getProjectLifecycleTargets(slug).length) throw new Error('A live project or worktree runtime appeared during closure.');
60
+ confirmClosed(op);
61
+ authorize();
62
+ var registration = ctx.find(slug);
63
+ if (restore) {
64
+ if (!registration || registration.ownerId !== principal.userId || registration.projectKnowledgeId !== op.projectKnowledgeId || registration.lifecycleOperationId !== op.id) throw new Error('The saved registration no longer belongs to this recovery operation or owner.');
65
+ } else exact(op);
66
+ var next = ctx.snapshot();
67
+ var project = next.projects.find(function (item) { return item.slug === op.slug; });
68
+ if (restore) {
69
+ delete project.lifecycleState; delete project.lifecycleOperationId; project.lifecycleExecutionPaused = true;
70
+ if (JSON.stringify(ctx.directory(project.path)) !== JSON.stringify(op.directory)) throw new Error('The project directory identity changed; restoration was refused.');
71
+ var restored = Object.assign({}, op, { restoration: true, expected: JSON.stringify(project), result: { ok: true, status: 'superseded', originalAction: 'remove', slug: op.slug, registrationRestored: true, executionPaused: true, semantics: 'Registration restored. Queued and scheduled execution stays paused until the owner explicitly sends a new message.' } });
72
+ next.projectLifecycleOperations = Object.assign({}, next.projectLifecycleOperations); next.projectLifecycleOperations[op.id] = restored; ctx.save(next); op = restored;
73
+ ctx.publishRuntime(project, 'recovered'); authorize(); exact(op);
74
+ next = ctx.snapshot(); complete(next, op, project); ctx.save(next); ctx.publish();
75
+ return Object.assign({}, restored.result, { status: 'completed', action: 'recover_registration', originalOutcome: 'superseded', operationId: op.id });
76
+ }
77
+ next.projects = next.projects.filter(function (item) { return item.slug !== op.slug; });
78
+ next.removedProjects = (next.removedProjects || []).filter(function (item) { return item.path !== project.path; }).concat([{ path: project.path, title: project.title || null, icon: project.icon || null, userId: op.ownerId, projectKnowledgeId: project.projectKnowledgeId, removedAt: Date.now() }]).slice(-20);
79
+ complete(next, op, project); ctx.save(next);
80
+ } else {
81
+ if (restore && !op.restoration) throw new Error('Only a pending removal can restore its registration.');
82
+ authorize(); var current = ctx.find(op.slug), currentRevision = JSON.stringify(current);
83
+ if (currentRevision !== op.expected) {
84
+ if (!current || current.projectKnowledgeId !== op.projectKnowledgeId || current.ownerId !== principal.userId || JSON.stringify(ctx.directory(current.path)) !== JSON.stringify(op.directory)) throw new Error('The saved publication target was replaced or transferred.');
85
+ ctx.publishRuntime(current, 'recovered'); authorize();
86
+ if (JSON.stringify(ctx.find(op.slug)) !== currentRevision) throw new Error('Registration changed during publication reconciliation.');
87
+ op = Object.assign({}, op, { result: { ok: true, status: 'superseded', slug: op.slug, persisted: true, currentRegistrationRetained: true } });
88
+ } else {
89
+ exact(op); ctx.publishRuntime(current, op.restoration ? 'recovered' : op.action); authorize(); exact(op);
90
+ }
91
+ var updated = ctx.snapshot(); complete(updated, op, current); ctx.save(updated);
92
+ }
93
+ ctx.publish(); return Object.assign({}, op.result, { operationId: op.id });
94
+ } catch (error) { error.lifecycleResult = Object.assign({}, error.lifecycleResult || {}, partial(op, error.message)); throw error; }
95
+ }
96
+ return { begin: begin, partial: partial, exact: exact, closed: closed, confirmClosed: confirmClosed, inspect: inspect, complete: complete, reconcile: reconcile };
97
+ }
98
+ module.exports = { attachLifecycleOperations: attachLifecycleOperations };
@@ -0,0 +1,32 @@
1
+ // Bounded, content-free process identities for closure reconciliation after host exit.
2
+ var cp = require('child_process');
3
+ function table() {
4
+ var output = cp.execFileSync('ps', ['-axo', 'pid=,ppid=,lstart='], { encoding: 'utf8', timeout: 5000, maxBuffer: 8 * 1024 * 1024, env: Object.assign({}, process.env, { LC_ALL: 'C', TZ: 'UTC' }) });
5
+ var rows = new Map();
6
+ output.split('\n').forEach(function (line) {
7
+ var match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
8
+ if (match) rows.set(Number(match[1]), { pid: Number(match[1]), parent: Number(match[2]), start: match[3] });
9
+ });
10
+ if (!rows.has(process.pid)) throw new Error("The process identity census was incomplete.");
11
+ return rows;
12
+ }
13
+ function capture() {
14
+ try {
15
+ var rows = table(), host = rows.get(process.pid), children = [], parents = new Set([process.pid]);
16
+ if (!host) return null;
17
+ for (var pass = 0; pass < rows.size; pass++) {
18
+ var changed = false;
19
+ rows.forEach(function (row) { if (parents.has(row.parent) && !parents.has(row.pid)) { parents.add(row.pid); children.push(row); changed = true; } });
20
+ if (!changed) break;
21
+ }
22
+ return { host: host, children: children };
23
+ } catch (error) { return null; }
24
+ }
25
+ function confirmsExit(witness) {
26
+ if (!witness || !witness.host || !Array.isArray(witness.children)) return false;
27
+ var rows = table();
28
+ return [witness.host].concat(witness.children).every(function (previous) {
29
+ var current = rows.get(previous.pid); return !current || current.start !== previous.start;
30
+ });
31
+ }
32
+ module.exports = { capture: capture, confirmsExit: confirmsExit };
@@ -0,0 +1,50 @@
1
+ // Retain failed shutdown handles so an explicitly reviewed recovery can retry them.
2
+ var crypto = require('crypto');
3
+ var witnesses = require('./project-lifecycle-process-witness');
4
+ function attachLifecycleRuntime(ctx) {
5
+ var epoch = crypto.randomUUID(), retired = new Map();
6
+ function target(slug, project) {
7
+ var status = project.getStatus();
8
+ return { slug: slug, path: status.path, projectKnowledgeId: status.projectKnowledgeId || null, epoch: epoch };
9
+ }
10
+ function key(item) { return JSON.stringify([item.slug, item.path, item.projectKnowledgeId]); }
11
+ function matches(item, project) { return key(item) === key(target(item.slug, project)); }
12
+ function targets(slug) {
13
+ var out = [], witness = witnesses.capture();
14
+ ctx.projects.forEach(function (project, candidate) {
15
+ if (candidate === slug || candidate.indexOf(slug + '--') === 0) out.push(Object.assign(target(candidate, project), { witness: witness }));
16
+ });
17
+ return out;
18
+ }
19
+ function stop(item, project) {
20
+ var id = key(item), previous = retired.get(id);
21
+ if (previous && previous.project !== project) {
22
+ if (!previous.closed) throw new Error('An earlier runtime with this identity has not confirmed closure.');
23
+ previous = null;
24
+ }
25
+ if (previous && previous.running) return previous.running;
26
+ if (previous && previous.closed) return Promise.resolve(true);
27
+ var state = previous || { project: project, target: item, closed: false };
28
+ retired.set(id, state);
29
+ state.running = Promise.resolve().then(function () { return state.project.destroy(); }).then(function (result) {
30
+ if (result !== true) throw new Error('Project runtime closure was not acknowledged: ' + item.slug);
31
+ state.closed = true; return true;
32
+ }).finally(function () { state.running = null; });
33
+ return state.running;
34
+ }
35
+ async function close(items, proof) {
36
+ for (var item of items) {
37
+ var live = ctx.projects.get(item.slug), previous = retired.get(key(item));
38
+ if (live) {
39
+ if (!matches(item, live)) throw new Error('A replacement runtime occupies this registration.');
40
+ ctx.projects.delete(item.slug);
41
+ await stop(item, live);
42
+ } else if (previous) await stop(item, previous.project);
43
+ else if (!proof && !witnesses.confirmsExit(item.witness)) throw new Error('Runtime closure evidence is unavailable on this host. Keep the operation pending until its original host confirms closure.');
44
+ }
45
+ return true;
46
+ }
47
+ function shutdown() { return Promise.all(Array.from(retired.values()).filter(function (item) { return !item.closed; }).map(function (item) { return stop(item.target, item.project); })); }
48
+ return { targets: targets, close: close, shutdown: shutdown };
49
+ }
50
+ module.exports = { attachLifecycleRuntime: attachLifecycleRuntime };
@@ -0,0 +1,22 @@
1
+ // Fail closed on unmapped identities, aliases, foreign ownership or unacknowledged ACLs.
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+ var cp = require('child_process');
5
+ var osUsers = require('./os-users');
6
+ function verify(projectPath, user, resolveIdentity) {
7
+ if (!user || !user.linuxUser) throw new Error('A provisioned Linux account is required.');
8
+ var identity = resolveIdentity(user.linuxUser), canonical = fs.realpathSync(projectPath), stat = fs.statSync(canonical);
9
+ if (!Number.isInteger(identity.uid) || identity.uid < 0 || !Number.isInteger(identity.gid) || identity.gid < 0) throw new Error('Linux identity is unavailable.');
10
+ if (canonical !== path.resolve(projectPath) || !stat.isDirectory() || stat.uid !== identity.uid) throw new Error('Registration requires a canonical directory owned by the mapped Linux account.');
11
+ if (osUsers.grantProjectAccess(canonical, user.linuxUser, { force: true }) !== true) {
12
+ var grantError = new Error('Linux filesystem access grant was not acknowledged.'); grantError.lifecycleResult = { persisted: false, retrySafe: true, filesystemAccess: 'grant_unconfirmed' }; throw grantError;
13
+ }
14
+ var script = "var fs = require('fs'); if (process.getuid() !== Number(process.argv[2]) || process.getgid() !== Number(process.argv[3])) throw new Error('Execution identity mismatch'); fs.accessSync(process.argv[1], fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK); fs.readdirSync(process.argv[1]);";
15
+ var wrapped = osUsers.wrapSpawnAsUser(process.execPath, ['-e', script, canonical, String(identity.uid), String(identity.gid)], { uid: identity.uid, gid: identity.gid, stdio: 'pipe', timeout: 10000 });
16
+ try { cp.execFileSync(wrapped.command, wrapped.args, wrapped.options); }
17
+ catch (error) { error.lifecycleResult = { persisted: false, retrySafe: true, filesystemAccess: 'verification_failed' }; throw error; }
18
+ var after = fs.statSync(canonical);
19
+ if (after.dev !== stat.dev || after.ino !== stat.ino || after.uid !== identity.uid || fs.realpathSync(projectPath) !== canonical) throw new Error('The Linux project directory changed during access verification.');
20
+ return true;
21
+ }
22
+ module.exports = { verify: verify };