libre-webui 0.26.0 → 0.28.0

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 (395) hide show
  1. package/README.md +57 -29
  2. package/backend/dist/index.d.ts.map +1 -1
  3. package/backend/dist/index.js +7 -0
  4. package/backend/dist/index.js.map +1 -1
  5. package/backend/dist/persistence/postgresAutomationWorkTargetMigration.d.ts +9 -0
  6. package/backend/dist/persistence/postgresAutomationWorkTargetMigration.d.ts.map +1 -0
  7. package/backend/dist/persistence/postgresAutomationWorkTargetMigration.js +44 -0
  8. package/backend/dist/persistence/postgresAutomationWorkTargetMigration.js.map +1 -0
  9. package/backend/dist/persistence/postgresMfaPushRecoveryMigration.d.ts +9 -0
  10. package/backend/dist/persistence/postgresMfaPushRecoveryMigration.d.ts.map +1 -0
  11. package/backend/dist/persistence/postgresMfaPushRecoveryMigration.js +107 -0
  12. package/backend/dist/persistence/postgresMfaPushRecoveryMigration.js.map +1 -0
  13. package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
  14. package/backend/dist/persistence/postgresMigrationRegistry.js +8 -0
  15. package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
  16. package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
  17. package/backend/dist/persistence/postgresResourceRepositories.js +179 -4
  18. package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
  19. package/backend/dist/persistence/postgresSecurityRepositories.d.ts.map +1 -1
  20. package/backend/dist/persistence/postgresSecurityRepositories.js +154 -0
  21. package/backend/dist/persistence/postgresSecurityRepositories.js.map +1 -1
  22. package/backend/dist/persistence/postgresWorkComputerMigration.d.ts +8 -0
  23. package/backend/dist/persistence/postgresWorkComputerMigration.d.ts.map +1 -0
  24. package/backend/dist/persistence/postgresWorkComputerMigration.js +37 -0
  25. package/backend/dist/persistence/postgresWorkComputerMigration.js.map +1 -0
  26. package/backend/dist/persistence/postgresWorkTakeoverMigration.d.ts +9 -0
  27. package/backend/dist/persistence/postgresWorkTakeoverMigration.d.ts.map +1 -0
  28. package/backend/dist/persistence/postgresWorkTakeoverMigration.js +38 -0
  29. package/backend/dist/persistence/postgresWorkTakeoverMigration.js.map +1 -0
  30. package/backend/dist/persistence/resourceTypes.d.ts +64 -0
  31. package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
  32. package/backend/dist/persistence/resourceTypes.js.map +1 -1
  33. package/backend/dist/persistence/securityTypes.d.ts +60 -0
  34. package/backend/dist/persistence/securityTypes.d.ts.map +1 -1
  35. package/backend/dist/persistence/sqliteMigrations.d.ts +4 -0
  36. package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
  37. package/backend/dist/persistence/sqliteMigrations.js +267 -0
  38. package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
  39. package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
  40. package/backend/dist/persistence/sqliteResourceRepositories.js +151 -7
  41. package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
  42. package/backend/dist/persistence/sqliteSecurityRepositories.d.ts.map +1 -1
  43. package/backend/dist/persistence/sqliteSecurityRepositories.js +140 -0
  44. package/backend/dist/persistence/sqliteSecurityRepositories.js.map +1 -1
  45. package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
  46. package/backend/dist/persistence/sqliteToPostgresMigration.js +45 -0
  47. package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
  48. package/backend/dist/platform/jobs/domainJobContracts.d.ts +2 -0
  49. package/backend/dist/platform/jobs/domainJobContracts.d.ts.map +1 -1
  50. package/backend/dist/platform/jobs/domainJobContracts.js +2 -0
  51. package/backend/dist/platform/jobs/domainJobContracts.js.map +1 -1
  52. package/backend/dist/platform/jobs/domainJobHandlers.d.ts.map +1 -1
  53. package/backend/dist/platform/jobs/domainJobHandlers.js +152 -2
  54. package/backend/dist/platform/jobs/domainJobHandlers.js.map +1 -1
  55. package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts.map +1 -1
  56. package/backend/dist/platform/workPersistence/postgresWorkPersistence.js +7 -3
  57. package/backend/dist/platform/workPersistence/postgresWorkPersistence.js.map +1 -1
  58. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts.map +1 -1
  59. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js +8 -4
  60. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js.map +1 -1
  61. package/backend/dist/platform/workPersistence/types.d.ts +2 -0
  62. package/backend/dist/platform/workPersistence/types.d.ts.map +1 -1
  63. package/backend/dist/platform/workPersistence/types.js.map +1 -1
  64. package/backend/dist/routes/auth.d.ts.map +1 -1
  65. package/backend/dist/routes/auth.js +427 -5
  66. package/backend/dist/routes/auth.js.map +1 -1
  67. package/backend/dist/routes/automations.d.ts.map +1 -1
  68. package/backend/dist/routes/automations.js +5 -0
  69. package/backend/dist/routes/automations.js.map +1 -1
  70. package/backend/dist/routes/documents.d.ts.map +1 -1
  71. package/backend/dist/routes/documents.js +4 -0
  72. package/backend/dist/routes/documents.js.map +1 -1
  73. package/backend/dist/routes/push.d.ts +3 -0
  74. package/backend/dist/routes/push.d.ts.map +1 -0
  75. package/backend/dist/routes/push.js +105 -0
  76. package/backend/dist/routes/push.js.map +1 -0
  77. package/backend/dist/routes/recovery.d.ts +3 -0
  78. package/backend/dist/routes/recovery.d.ts.map +1 -0
  79. package/backend/dist/routes/recovery.js +83 -0
  80. package/backend/dist/routes/recovery.js.map +1 -0
  81. package/backend/dist/routes/users.d.ts.map +1 -1
  82. package/backend/dist/routes/users.js +35 -0
  83. package/backend/dist/routes/users.js.map +1 -1
  84. package/backend/dist/routes/work.d.ts.map +1 -1
  85. package/backend/dist/routes/work.js +205 -1
  86. package/backend/dist/routes/work.js.map +1 -1
  87. package/backend/dist/services/authService.d.ts +8 -0
  88. package/backend/dist/services/authService.d.ts.map +1 -1
  89. package/backend/dist/services/authService.js +29 -0
  90. package/backend/dist/services/authService.js.map +1 -1
  91. package/backend/dist/services/automationSchedulerService.d.ts.map +1 -1
  92. package/backend/dist/services/automationSchedulerService.js +31 -0
  93. package/backend/dist/services/automationSchedulerService.js.map +1 -1
  94. package/backend/dist/services/automationService.d.ts +4 -1
  95. package/backend/dist/services/automationService.d.ts.map +1 -1
  96. package/backend/dist/services/automationService.js +27 -0
  97. package/backend/dist/services/automationService.js.map +1 -1
  98. package/backend/dist/services/dataArchiveService.d.ts.map +1 -1
  99. package/backend/dist/services/dataArchiveService.js +3 -0
  100. package/backend/dist/services/dataArchiveService.js.map +1 -1
  101. package/backend/dist/services/documentMediaExtractionService.d.ts +40 -0
  102. package/backend/dist/services/documentMediaExtractionService.d.ts.map +1 -0
  103. package/backend/dist/services/documentMediaExtractionService.js +157 -0
  104. package/backend/dist/services/documentMediaExtractionService.js.map +1 -0
  105. package/backend/dist/services/documentService.d.ts +6 -0
  106. package/backend/dist/services/documentService.d.ts.map +1 -1
  107. package/backend/dist/services/documentService.js +58 -4
  108. package/backend/dist/services/documentService.js.map +1 -1
  109. package/backend/dist/services/encryptionService.d.ts +6 -0
  110. package/backend/dist/services/encryptionService.d.ts.map +1 -1
  111. package/backend/dist/services/encryptionService.js +12 -0
  112. package/backend/dist/services/encryptionService.js.map +1 -1
  113. package/backend/dist/services/mfaService.d.ts +64 -0
  114. package/backend/dist/services/mfaService.d.ts.map +1 -0
  115. package/backend/dist/services/mfaService.js +262 -0
  116. package/backend/dist/services/mfaService.js.map +1 -0
  117. package/backend/dist/services/notificationService.d.ts +5 -0
  118. package/backend/dist/services/notificationService.d.ts.map +1 -1
  119. package/backend/dist/services/notificationService.js +33 -1
  120. package/backend/dist/services/notificationService.js.map +1 -1
  121. package/backend/dist/services/preferencesService.d.ts +1 -1
  122. package/backend/dist/services/preferencesService.d.ts.map +1 -1
  123. package/backend/dist/services/preferencesService.js +1 -0
  124. package/backend/dist/services/preferencesService.js.map +1 -1
  125. package/backend/dist/services/recoveryDrillService.d.ts +57 -0
  126. package/backend/dist/services/recoveryDrillService.d.ts.map +1 -0
  127. package/backend/dist/services/recoveryDrillService.js +363 -0
  128. package/backend/dist/services/recoveryDrillService.js.map +1 -0
  129. package/backend/dist/services/webPushService.d.ts +71 -0
  130. package/backend/dist/services/webPushService.d.ts.map +1 -0
  131. package/backend/dist/services/webPushService.js +328 -0
  132. package/backend/dist/services/webPushService.js.map +1 -0
  133. package/backend/dist/services/webauthnService.d.ts +58 -0
  134. package/backend/dist/services/webauthnService.d.ts.map +1 -0
  135. package/backend/dist/services/webauthnService.js +301 -0
  136. package/backend/dist/services/webauthnService.js.map +1 -0
  137. package/backend/dist/services/websocketTicketService.d.ts +1 -1
  138. package/backend/dist/services/websocketTicketService.d.ts.map +1 -1
  139. package/backend/dist/services/websocketTicketService.js +3 -1
  140. package/backend/dist/services/websocketTicketService.js.map +1 -1
  141. package/backend/dist/services/workAgentGuidance.d.ts +14 -0
  142. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
  143. package/backend/dist/services/workAgentGuidance.js +38 -2
  144. package/backend/dist/services/workAgentGuidance.js.map +1 -1
  145. package/backend/dist/services/workAgentService.d.ts +9 -0
  146. package/backend/dist/services/workAgentService.d.ts.map +1 -1
  147. package/backend/dist/services/workAgentService.js +503 -13
  148. package/backend/dist/services/workAgentService.js.map +1 -1
  149. package/backend/dist/services/workComputerSetupService.d.ts +33 -0
  150. package/backend/dist/services/workComputerSetupService.d.ts.map +1 -0
  151. package/backend/dist/services/workComputerSetupService.js +164 -0
  152. package/backend/dist/services/workComputerSetupService.js.map +1 -0
  153. package/backend/dist/services/workComputerTeachService.d.ts +105 -0
  154. package/backend/dist/services/workComputerTeachService.d.ts.map +1 -0
  155. package/backend/dist/services/workComputerTeachService.js +513 -0
  156. package/backend/dist/services/workComputerTeachService.js.map +1 -0
  157. package/backend/dist/services/workKubernetesDriver.d.ts +8 -0
  158. package/backend/dist/services/workKubernetesDriver.d.ts.map +1 -1
  159. package/backend/dist/services/workKubernetesDriver.js +22 -0
  160. package/backend/dist/services/workKubernetesDriver.js.map +1 -1
  161. package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
  162. package/backend/dist/services/workModelProviderService.js +102 -8
  163. package/backend/dist/services/workModelProviderService.js.map +1 -1
  164. package/backend/dist/services/workPolicyService.d.ts +8 -0
  165. package/backend/dist/services/workPolicyService.d.ts.map +1 -1
  166. package/backend/dist/services/workPolicyService.js +25 -0
  167. package/backend/dist/services/workPolicyService.js.map +1 -1
  168. package/backend/dist/services/workRuntimeDriver.d.ts +32 -0
  169. package/backend/dist/services/workRuntimeDriver.d.ts.map +1 -1
  170. package/backend/dist/services/workRuntimeDriver.js +54 -8
  171. package/backend/dist/services/workRuntimeDriver.js.map +1 -1
  172. package/backend/dist/services/workRuntimeService.d.ts +212 -1
  173. package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
  174. package/backend/dist/services/workRuntimeService.js +1184 -9
  175. package/backend/dist/services/workRuntimeService.js.map +1 -1
  176. package/backend/dist/services/workRuntimeShared.d.ts +11 -1
  177. package/backend/dist/services/workRuntimeShared.d.ts.map +1 -1
  178. package/backend/dist/services/workRuntimeShared.js +24 -1
  179. package/backend/dist/services/workRuntimeShared.js.map +1 -1
  180. package/backend/dist/services/workScreenControlService.d.ts +60 -0
  181. package/backend/dist/services/workScreenControlService.d.ts.map +1 -0
  182. package/backend/dist/services/workScreenControlService.js +201 -0
  183. package/backend/dist/services/workScreenControlService.js.map +1 -0
  184. package/backend/dist/services/workTaskService.d.ts +11 -0
  185. package/backend/dist/services/workTaskService.d.ts.map +1 -1
  186. package/backend/dist/services/workTaskService.js +31 -0
  187. package/backend/dist/services/workTaskService.js.map +1 -1
  188. package/backend/dist/types/index.d.ts +10 -2
  189. package/backend/dist/types/index.d.ts.map +1 -1
  190. package/backend/dist/types/index.js.map +1 -1
  191. package/backend/dist/types/work.d.ts +13 -0
  192. package/backend/dist/types/work.d.ts.map +1 -1
  193. package/backend/dist/utils/documentExtraction.d.ts +31 -3
  194. package/backend/dist/utils/documentExtraction.d.ts.map +1 -1
  195. package/backend/dist/utils/documentExtraction.js +106 -0
  196. package/backend/dist/utils/documentExtraction.js.map +1 -1
  197. package/backend/dist/utils/toolEgress.d.ts +2 -1
  198. package/backend/dist/utils/toolEgress.d.ts.map +1 -1
  199. package/backend/dist/utils/toolEgress.js +14 -2
  200. package/backend/dist/utils/toolEgress.js.map +1 -1
  201. package/backend/dist/utils/totp.d.ts +19 -0
  202. package/backend/dist/utils/totp.d.ts.map +1 -0
  203. package/backend/dist/utils/totp.js +117 -0
  204. package/backend/dist/utils/totp.js.map +1 -0
  205. package/backend/dist/utils/webauthn.d.ts +47 -0
  206. package/backend/dist/utils/webauthn.d.ts.map +1 -0
  207. package/backend/dist/utils/webauthn.js +323 -0
  208. package/backend/dist/utils/webauthn.js.map +1 -0
  209. package/backend/dist/websocketServer.d.ts.map +1 -1
  210. package/backend/dist/websocketServer.js +9 -0
  211. package/backend/dist/websocketServer.js.map +1 -1
  212. package/backend/dist/workScreenServer.d.ts +12 -0
  213. package/backend/dist/workScreenServer.d.ts.map +1 -0
  214. package/backend/dist/workScreenServer.js +291 -0
  215. package/backend/dist/workScreenServer.js.map +1 -0
  216. package/backend/package.json +1 -1
  217. package/deploy/work-computer/Dockerfile +48 -0
  218. package/deploy/work-computer/README.md +93 -0
  219. package/deploy/work-computer/fonts/fonts.css +335 -0
  220. package/deploy/work-computer/fonts/v16-aFTR7PB1QTsUX8KYvumzEY2tbYf-Vlh3uA.woff2 +0 -0
  221. package/deploy/work-computer/fonts/v16-aFTR7PB1QTsUX8KYvumzEYOtbYf-Vlg.woff2 +0 -0
  222. package/deploy/work-computer/fonts/v16-aFTU7PB1QTsUX8KYthSQBK6PYK3EXw.woff2 +0 -0
  223. package/deploy/work-computer/fonts/v16-aFTU7PB1QTsUX8KYthqQBK6PYK0.woff2 +0 -0
  224. package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggOxSvfedN62Zw.woff2 +0 -0
  225. package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggSxSvfedN62Zw.woff2 +0 -0
  226. package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggexSvfedN4.woff2 +0 -0
  227. package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggixSvfedN62Zw.woff2 +0 -0
  228. package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggmxSvfedN62Zw.woff2 +0 -0
  229. package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggqxSvfedN62Zw.woff2 +0 -0
  230. package/deploy/work-computer/fonts/v22-V8mDoQDjQSkFtoMM3T6r8E7mPb54C_k3HqUtEw.woff2 +0 -0
  231. package/deploy/work-computer/fonts/v22-V8mDoQDjQSkFtoMM3T6r8E7mPb94C_k3HqUtEw.woff2 +0 -0
  232. package/deploy/work-computer/fonts/v22-V8mDoQDjQSkFtoMM3T6r8E7mPbF4C_k3HqU.woff2 +0 -0
  233. package/deploy/work-computer/start-computer.sh +155 -0
  234. package/deploy/work-computer/start-page.html +251 -0
  235. package/deploy/work-computer/tint2rc +76 -0
  236. package/deploy/work-computer/wallpaper.png +0 -0
  237. package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
  238. package/frontend/dist/assets/index-DGy6mo0B.js +33 -0
  239. package/frontend/dist/assets/index-DqLJKWF-.css +1 -0
  240. package/frontend/dist/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  241. package/frontend/dist/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
  242. package/frontend/dist/fonts/jetbrains-mono.css +24 -0
  243. package/frontend/dist/index.html +40 -18
  244. package/frontend/dist/js/ArtifactContainer-DvIMxleC.js +1 -0
  245. package/frontend/dist/js/{ArtifactDemoPage-D0JEIAbZ.js → ArtifactDemoPage-BqYskFqc.js} +1 -1
  246. package/frontend/dist/js/ArtifactSandboxFrame-9wuufHn6.js +111 -0
  247. package/frontend/dist/js/ArtifactSlideOutPanel-DXESMtPh.js +1 -0
  248. package/frontend/dist/js/AutomationsPage-DByxhIKj.js +1 -0
  249. package/frontend/dist/js/AvatarUpload-DnCGQ46o.js +1 -0
  250. package/frontend/dist/js/{Button-C0r3J9G_.js → Button-CpobxJ2P.js} +1 -1
  251. package/frontend/dist/js/CalendarPage-JywVP-9y.js +1 -0
  252. package/frontend/dist/js/ChannelsPage-B31rcARd.js +1 -0
  253. package/frontend/dist/js/ChatPage-CHXJY8m3.js +14 -0
  254. package/frontend/dist/js/{EvaluationsPage-Bm8FRoeX.js → EvaluationsPage-CcmB5wdF.js} +1 -1
  255. package/frontend/dist/js/FirstTimeSetup-DDU7SowM.js +1 -0
  256. package/frontend/dist/js/GalleryPage-XfX_7iCf.js +1 -0
  257. package/frontend/dist/js/HomePage-DAZAkoAV.js +1 -0
  258. package/frontend/dist/js/LibreClawPage-De0gZmbF.js +1 -0
  259. package/frontend/dist/js/Logo-yKM43xGa.js +1 -0
  260. package/frontend/dist/js/LogoMark-PmpKQOre.js +1 -0
  261. package/frontend/dist/js/{ModelManagerModals-DX9eT5r2.js → ModelManagerModals-hz2cZZ4a.js} +1 -1
  262. package/frontend/dist/js/ModelSelector-BoFehQy-.js +1 -0
  263. package/frontend/dist/js/NotesPage-BGPI3gG6.js +3 -0
  264. package/frontend/dist/js/OptimizedSyntaxHighlighter-DrejdNUm.js +1 -0
  265. package/frontend/dist/js/OptimizedSyntaxHighlighter-hNqZPLiB.js +1 -0
  266. package/frontend/dist/js/PasswordStrengthMeter-RwModpZ1.js +1 -0
  267. package/frontend/dist/js/PersonasPage-BkNRD2wO.js +13 -0
  268. package/frontend/dist/js/PluginUsagePage-Cqt7V2SQ.js +1 -0
  269. package/frontend/dist/js/RichMessageContent-B2QSyX-l.js +1 -0
  270. package/frontend/dist/js/RichMessageContent-CYEhT6uU.js +3 -0
  271. package/frontend/dist/js/SettingsModal-IbIvVyss.js +19 -0
  272. package/frontend/dist/js/{SettingsToggle-BIVyfGjl.js → SettingsToggle-3GxA96E0.js} +1 -1
  273. package/frontend/dist/js/ShareDialog-BoE4OVhV.js +1 -0
  274. package/frontend/dist/js/SystemPage-B5ecz5H3.js +1 -0
  275. package/frontend/dist/js/TurnstileWidget-DHU9IUM1.js +1 -0
  276. package/frontend/dist/js/UserManagementPage-C0VUfbMk.js +1 -0
  277. package/frontend/dist/js/WorkPage-EweYI5Dx.js +74 -0
  278. package/frontend/dist/js/api-Bbh2AHHN.js +1 -0
  279. package/frontend/dist/js/api-F0CnlVH8.js +14 -0
  280. package/frontend/dist/js/appNavigation-DqV69ohR.js +1 -0
  281. package/frontend/dist/js/appStore-SLLBamo2.js +2 -0
  282. package/frontend/dist/js/appVersion-46bF9UkF.js +1 -0
  283. package/frontend/dist/js/ar-C4yo7x5J.js +2 -0
  284. package/frontend/dist/js/artifactHtml-DDmfpcB0.js +41 -0
  285. package/frontend/dist/js/authStore-CcfL8T6T.js +1 -0
  286. package/frontend/dist/js/authStore-DlSQ1TcP.js +4 -0
  287. package/frontend/dist/js/axios-_ZpUYd3e.js +1 -0
  288. package/frontend/dist/js/bn-BeZ6d2qF.js +2 -0
  289. package/frontend/dist/js/chatStore-DI79tEJ-.js +3 -0
  290. package/frontend/dist/js/classPrivateMethodInitSpec-B31Q4d2M.js +1 -0
  291. package/frontend/dist/js/{config-8KdgjtEx.js → config-0QgGZCrI.js} +1 -1
  292. package/frontend/dist/js/cs-9K5LILI5.js +2 -0
  293. package/frontend/dist/js/da-BWbBhLK8.js +2 -0
  294. package/frontend/dist/js/de-BXD9t2uY.js +2 -0
  295. package/frontend/dist/js/es-Dv4wZKlU.js +2 -0
  296. package/frontend/dist/js/{estree-BU5-LV_1.js → estree-WDwviHUZ.js} +1 -1
  297. package/frontend/dist/js/evaluationsApi-wjf0nh5J.js +1 -0
  298. package/frontend/dist/js/fr-BkXGyEei.js +2 -0
  299. package/frontend/dist/js/hi-BS1ldbWk.js +2 -0
  300. package/frontend/dist/js/{html-DeoAy2bo.js → html-Cj0HNcRt.js} +1 -1
  301. package/frontend/dist/js/id-Cr1njx5M.js +2 -0
  302. package/frontend/dist/js/is-BqpNCtDd.js +2 -0
  303. package/frontend/dist/js/it-Dp-iwH27.js +2 -0
  304. package/frontend/dist/js/ja-_e4v_6Mn.js +2 -0
  305. package/frontend/dist/js/ko-CjXf2pIX.js +2 -0
  306. package/frontend/dist/js/{logger-DSXjF4ye.js → logger-CpCQz3Vi.js} +1 -1
  307. package/frontend/dist/js/{markdown-DOLsYkBY.js → markdown-DTahdtRF.js} +1 -1
  308. package/frontend/dist/js/{messageCodeStyles-DhVgizF-.js → messageCodeStyles-COJFoq4P.js} +2 -2
  309. package/frontend/dist/js/ms-xpn-zACj.js +2 -0
  310. package/frontend/dist/js/nl-Baf0a2X0.js +2 -0
  311. package/frontend/dist/js/pl-BEOTrcUv.js +2 -0
  312. package/frontend/dist/js/pt-B5FUaihM.js +2 -0
  313. package/frontend/dist/js/rfb-bJaPz33m.js +5 -0
  314. package/frontend/dist/js/ru-BjzgMmkJ.js +2 -0
  315. package/frontend/dist/js/{standalone-BO5pzI8_.js → standalone-C04jT26m.js} +1 -1
  316. package/frontend/dist/js/sv-DDX_gP1z.js +2 -0
  317. package/frontend/dist/js/{syntax-highlight-DcR9Vhb7.js → syntax-highlight-P2zo9o06.js} +1 -1
  318. package/frontend/dist/js/teamEventStream-CgUkgjFC.js +8 -0
  319. package/frontend/dist/js/th-BXr3KovE.js +2 -0
  320. package/frontend/dist/js/tr-CdqxHEM8.js +2 -0
  321. package/frontend/dist/js/ttsBatching-BfhNT3c5.js +2 -0
  322. package/frontend/dist/js/{typescript-6GJ8APmn.js → typescript-FtGI7Y5P.js} +1 -1
  323. package/frontend/dist/js/ui-DFa2W4QC.js +2 -0
  324. package/frontend/dist/js/ui-vendor-CRZXLK0Q.js +177 -0
  325. package/frontend/dist/js/uk-CotKDP2Q.js +2 -0
  326. package/frontend/dist/js/useChat-BawGm7ua.js +10 -0
  327. package/frontend/dist/js/useQuery-BboI5V5G.js +1 -0
  328. package/frontend/dist/js/{useTranslation-5b5UhAc9.js → useTranslation-D7uLySn1.js} +1 -1
  329. package/frontend/dist/js/{utils-BGrdX9ie.js → utils-sD-qFh8D.js} +1 -1
  330. package/frontend/dist/js/utils-vendor-DTLgmsuc.js +9 -0
  331. package/frontend/dist/js/vi-F2NC1HaJ.js +2 -0
  332. package/frontend/dist/js/webauthnClient-CPSq528Q.js +1 -0
  333. package/frontend/dist/js/workApi-8mBuh_-J.js +6 -0
  334. package/frontend/dist/js/workApi-DJtLOslf.js +1 -0
  335. package/frontend/dist/js/workStatus-CcO9Fg41.js +1 -0
  336. package/frontend/dist/js/zh-DZ-kpEEA.js +2 -0
  337. package/frontend/dist/manifest.webmanifest +32 -0
  338. package/frontend/dist/sw.js +125 -0
  339. package/package.json +4 -2
  340. package/frontend/dist/assets/index-BPsH9FII.js +0 -80
  341. package/frontend/dist/assets/index-BtXF0Oxa.css +0 -1
  342. package/frontend/dist/js/ArtifactContainer-CIoFoMv8.js +0 -1
  343. package/frontend/dist/js/ArtifactSandboxFrame-REazyIrx.js +0 -151
  344. package/frontend/dist/js/ArtifactSlideOutPanel-A-i94ghF.js +0 -1
  345. package/frontend/dist/js/AutomationsPage-Dow_lbIf.js +0 -1
  346. package/frontend/dist/js/CalendarPage-DLjasZOd.js +0 -1
  347. package/frontend/dist/js/ChannelsPage-C8hvTD7J.js +0 -1
  348. package/frontend/dist/js/ChatPage-BAgBHUES.js +0 -16
  349. package/frontend/dist/js/FirstTimeSetup-BxJapNul.js +0 -1
  350. package/frontend/dist/js/GalleryPage-DHz9DkK7.js +0 -1
  351. package/frontend/dist/js/HomePage-BqNy-FWX.js +0 -1
  352. package/frontend/dist/js/LibreClawPage-BDzDILjY.js +0 -1
  353. package/frontend/dist/js/ModelSelector-ol8mK2ja.js +0 -1
  354. package/frontend/dist/js/NotesPage-DzHEx2O4.js +0 -3
  355. package/frontend/dist/js/OptimizedSyntaxHighlighter-CZNPwN0i.js +0 -1
  356. package/frontend/dist/js/PersonasPage-M_WKH2a9.js +0 -13
  357. package/frontend/dist/js/PluginUsagePage-Ctoka6JZ.js +0 -1
  358. package/frontend/dist/js/RichMessageContent-BqgafH4p.js +0 -3
  359. package/frontend/dist/js/SettingsModal-R9EDadAV.js +0 -19
  360. package/frontend/dist/js/SystemPage-aOkrSuY7.js +0 -1
  361. package/frontend/dist/js/UserManagementPage-BOuaX3ay.js +0 -1
  362. package/frontend/dist/js/WorkPage-DxZISerQ.js +0 -6
  363. package/frontend/dist/js/api-6bSMtSNh.js +0 -14
  364. package/frontend/dist/js/appStore-CdObhQBb.js +0 -2
  365. package/frontend/dist/js/ar-DT_VLBO3.js +0 -2
  366. package/frontend/dist/js/authStore-DbqqrfZv.js +0 -4
  367. package/frontend/dist/js/bn-BrvEqhGA.js +0 -2
  368. package/frontend/dist/js/chatStore-DbZPJNsu.js +0 -3
  369. package/frontend/dist/js/cs-BthnM_YI.js +0 -2
  370. package/frontend/dist/js/da-DCth-6_j.js +0 -2
  371. package/frontend/dist/js/de-BBWt0gJ_.js +0 -2
  372. package/frontend/dist/js/es-DuslOPqM.js +0 -2
  373. package/frontend/dist/js/evaluationsApi-CwZJsgeG.js +0 -1
  374. package/frontend/dist/js/fr-DZAeg46S.js +0 -2
  375. package/frontend/dist/js/hi-ijDSiY77.js +0 -2
  376. package/frontend/dist/js/id-BS6qS0oq.js +0 -2
  377. package/frontend/dist/js/is-CO2PMDte.js +0 -2
  378. package/frontend/dist/js/it-ChhxT4hs.js +0 -2
  379. package/frontend/dist/js/ja-cyeqUeJN.js +0 -2
  380. package/frontend/dist/js/ko-CfqHTLXj.js +0 -2
  381. package/frontend/dist/js/ms-DZ5pACe1.js +0 -2
  382. package/frontend/dist/js/nl-BsRsnOVp.js +0 -2
  383. package/frontend/dist/js/pl-DXSTAW4M.js +0 -2
  384. package/frontend/dist/js/pt-DmDx0JxB.js +0 -2
  385. package/frontend/dist/js/ru-d2g8EWfY.js +0 -2
  386. package/frontend/dist/js/sv-CIhYUTVQ.js +0 -2
  387. package/frontend/dist/js/th-B3wSxanB.js +0 -2
  388. package/frontend/dist/js/tr-R4UGgPOY.js +0 -2
  389. package/frontend/dist/js/ui-CV0LYmkM.js +0 -2
  390. package/frontend/dist/js/ui-vendor-CWjIBW-z.js +0 -177
  391. package/frontend/dist/js/uk-BzUF4Vkt.js +0 -2
  392. package/frontend/dist/js/utils-vendor-9bQ62WeD.js +0 -9
  393. package/frontend/dist/js/vi-CKc6Fg4h.js +0 -2
  394. package/frontend/dist/js/workApi-BruWTlHM.js +0 -6
  395. package/frontend/dist/js/zh-CXL-kDe8.js +0 -2
@@ -31,10 +31,286 @@ import { WorkRuntimeError, workRuntimeConfig as config, } from './workRuntimeSha
31
31
  // The public surface of the Work runtime is unchanged by the driver split:
32
32
  // consumers and tests keep importing everything from this module.
33
33
  export { WORK_RUNTIME_ADMISSION_DEFAULTS, WORK_RUNTIME_DEFAULTS, WorkRuntimeError, parseDnsServers, } from './workRuntimeShared.js';
34
- export { DockerWorkRuntimeDriver, buildWorkContainerRunArgs, describeDockerUnavailable, formatPreviewHost, parseManagedContainerList, parsePublishedPort, } from './workRuntimeDriver.js';
34
+ export { DockerWorkRuntimeDriver, buildWorkContainerRunArgs, describeDockerUnavailable, formatPreviewHost, parseManagedContainerList, parsePublishedPort, resolvePublishedEndpointHost, } from './workRuntimeDriver.js';
35
35
  const logger = createLogger('services:work-runtime');
36
36
  const PREVIEW_READY_TIMEOUT_MS = 15000;
37
37
  const PREVIEW_POLL_INTERVAL_MS = 250;
38
+ /** Most actions a single computer_act batch may carry (rakazo-scale). */
39
+ export const WORK_COMPUTER_ACTION_LIMIT = 24;
40
+ const WORK_COMPUTER_TEXT_LIMIT = 4000;
41
+ const WORK_COMPUTER_WAIT_LIMIT_MS = 5000;
42
+ const WORK_COMPUTER_WAIT_TOTAL_LIMIT_MS = 30000;
43
+ const WORK_COMPUTER_COORDINATE_LIMIT = 10000;
44
+ const WORK_COMPUTER_FOCUS_LIMIT = 200;
45
+ const WORK_COMPUTER_EXPECT_TEXT_LIMIT = 300;
46
+ const WORK_COMPUTER_EXPECT_WAIT_LIMIT_MS = 15000;
47
+ /** xdotool key syntax: chords like ctrl+shift+t, keysyms like Return. */
48
+ const WORK_COMPUTER_KEYS_PATTERN = /^[A-Za-z0-9_]+(\+[A-Za-z0-9_]+)*$/;
49
+ /**
50
+ * Validate a computer_act batch before anything reaches the sandbox. The
51
+ * output is plain data safe to JSON-encode into the in-container action
52
+ * interpreter; every rejected batch names the exact offending action.
53
+ */
54
+ export function validateWorkComputerActions(actions) {
55
+ if (!Array.isArray(actions) || actions.length === 0) {
56
+ throw new WorkRuntimeError('computer_act requires a non-empty "actions" array.', 400, 'WORK_COMPUTER_INVALID_ACTION');
57
+ }
58
+ if (actions.length > WORK_COMPUTER_ACTION_LIMIT) {
59
+ throw new WorkRuntimeError(`computer_act accepts at most ${WORK_COMPUTER_ACTION_LIMIT} actions per call.`, 400, 'WORK_COMPUTER_INVALID_ACTION');
60
+ }
61
+ const invalid = (index, reason) => new WorkRuntimeError(`computer_act action ${index + 1} is invalid: ${reason}`, 400, 'WORK_COMPUTER_INVALID_ACTION');
62
+ const coordinate = (value, index, name) => {
63
+ if (typeof value !== 'number' ||
64
+ !Number.isInteger(value) ||
65
+ value < 0 ||
66
+ value > WORK_COMPUTER_COORDINATE_LIMIT) {
67
+ throw invalid(index, `"${name}" must be an integer between 0 and ${WORK_COMPUTER_COORDINATE_LIMIT}.`);
68
+ }
69
+ return value;
70
+ };
71
+ const focusAssertion = (action, index) => {
72
+ if (action.focus === undefined)
73
+ return {};
74
+ if (typeof action.focus !== 'string' ||
75
+ action.focus.trim().length === 0 ||
76
+ action.focus.length > WORK_COMPUTER_FOCUS_LIMIT) {
77
+ throw invalid(index, `"focus" must be a string of 1 to ${WORK_COMPUTER_FOCUS_LIMIT} characters.`);
78
+ }
79
+ return { focus: action.focus.trim() };
80
+ };
81
+ let totalWaitMs = 0;
82
+ const validated = [];
83
+ for (const [index, value] of actions.entries()) {
84
+ const action = value && typeof value === 'object' && !Array.isArray(value)
85
+ ? value
86
+ : undefined;
87
+ if (!action || typeof action.type !== 'string') {
88
+ throw invalid(index, 'each action must be an object with a "type".');
89
+ }
90
+ switch (action.type) {
91
+ case 'move':
92
+ validated.push({
93
+ type: 'move',
94
+ x: coordinate(action.x, index, 'x'),
95
+ y: coordinate(action.y, index, 'y'),
96
+ });
97
+ break;
98
+ case 'click':
99
+ case 'double_click':
100
+ case 'right_click': {
101
+ const hasX = action.x !== undefined;
102
+ const hasY = action.y !== undefined;
103
+ if (hasX !== hasY) {
104
+ throw invalid(index, 'provide both "x" and "y" or neither.');
105
+ }
106
+ validated.push({
107
+ type: action.type,
108
+ ...(hasX
109
+ ? {
110
+ x: coordinate(action.x, index, 'x'),
111
+ y: coordinate(action.y, index, 'y'),
112
+ }
113
+ : {}),
114
+ });
115
+ break;
116
+ }
117
+ case 'type': {
118
+ if (typeof action.text !== 'string' ||
119
+ action.text.length === 0 ||
120
+ action.text.length > WORK_COMPUTER_TEXT_LIMIT) {
121
+ throw invalid(index, `"text" must be a string of 1 to ${WORK_COMPUTER_TEXT_LIMIT} characters.`);
122
+ }
123
+ validated.push({
124
+ type: 'type',
125
+ text: action.text,
126
+ ...focusAssertion(action, index),
127
+ });
128
+ break;
129
+ }
130
+ case 'key': {
131
+ if (typeof action.keys !== 'string' ||
132
+ !WORK_COMPUTER_KEYS_PATTERN.test(action.keys) ||
133
+ action.keys.length > 64) {
134
+ throw invalid(index, '"keys" must be an xdotool key name or chord such as "Return" or "ctrl+l".');
135
+ }
136
+ validated.push({
137
+ type: 'key',
138
+ keys: action.keys,
139
+ ...focusAssertion(action, index),
140
+ });
141
+ break;
142
+ }
143
+ case 'scroll': {
144
+ if (action.direction !== 'up' && action.direction !== 'down') {
145
+ throw invalid(index, '"direction" must be "up" or "down".');
146
+ }
147
+ const amount = action.amount ?? 3;
148
+ if (typeof amount !== 'number' ||
149
+ !Number.isInteger(amount) ||
150
+ amount < 1 ||
151
+ amount > 20) {
152
+ throw invalid(index, '"amount" must be an integer between 1 and 20.');
153
+ }
154
+ const hasX = action.x !== undefined;
155
+ const hasY = action.y !== undefined;
156
+ if (hasX !== hasY) {
157
+ throw invalid(index, 'provide both "x" and "y" or neither.');
158
+ }
159
+ validated.push({
160
+ type: 'scroll',
161
+ direction: action.direction,
162
+ amount,
163
+ ...(hasX
164
+ ? {
165
+ x: coordinate(action.x, index, 'x'),
166
+ y: coordinate(action.y, index, 'y'),
167
+ }
168
+ : {}),
169
+ });
170
+ break;
171
+ }
172
+ case 'scroll_until': {
173
+ if (action.direction !== 'up' && action.direction !== 'down') {
174
+ throw invalid(index, '"direction" must be "up" or "down".');
175
+ }
176
+ const target = action.target &&
177
+ typeof action.target === 'object' &&
178
+ !Array.isArray(action.target)
179
+ ? action.target
180
+ : undefined;
181
+ const hasText = target?.text !== undefined;
182
+ const hasEdge = target?.edge !== undefined;
183
+ if (!target || hasText === hasEdge) {
184
+ throw invalid(index, '"target" must be an object with exactly one of "text" or "edge".');
185
+ }
186
+ let validatedTarget;
187
+ if (hasText) {
188
+ if (typeof target.text !== 'string' ||
189
+ target.text.trim().length === 0 ||
190
+ target.text.length > 200) {
191
+ throw invalid(index, '"target.text" must be a string of 1 to 200 characters.');
192
+ }
193
+ validatedTarget = { text: target.text.trim() };
194
+ }
195
+ else {
196
+ if (target.edge !== 'top' && target.edge !== 'bottom') {
197
+ throw invalid(index, '"target.edge" must be "top" or "bottom".');
198
+ }
199
+ validatedTarget = { edge: target.edge };
200
+ }
201
+ if (action.maxAmount !== undefined) {
202
+ if (typeof action.maxAmount !== 'number' ||
203
+ !Number.isInteger(action.maxAmount) ||
204
+ action.maxAmount < 1 ||
205
+ action.maxAmount > 30) {
206
+ throw invalid(index, '"maxAmount" must be an integer between 1 and 30.');
207
+ }
208
+ }
209
+ const hasX = action.x !== undefined;
210
+ const hasY = action.y !== undefined;
211
+ if (hasX !== hasY) {
212
+ throw invalid(index, 'provide both "x" and "y" or neither.');
213
+ }
214
+ validated.push({
215
+ type: 'scroll_until',
216
+ direction: action.direction,
217
+ target: validatedTarget,
218
+ ...(action.maxAmount !== undefined
219
+ ? { maxAmount: action.maxAmount }
220
+ : {}),
221
+ ...(hasX
222
+ ? {
223
+ x: coordinate(action.x, index, 'x'),
224
+ y: coordinate(action.y, index, 'y'),
225
+ }
226
+ : {}),
227
+ });
228
+ break;
229
+ }
230
+ case 'wait': {
231
+ if (typeof action.ms !== 'number' ||
232
+ !Number.isInteger(action.ms) ||
233
+ action.ms < 1 ||
234
+ action.ms > WORK_COMPUTER_WAIT_LIMIT_MS) {
235
+ throw invalid(index, `"ms" must be an integer between 1 and ${WORK_COMPUTER_WAIT_LIMIT_MS}.`);
236
+ }
237
+ totalWaitMs += action.ms;
238
+ if (totalWaitMs > WORK_COMPUTER_WAIT_TOTAL_LIMIT_MS) {
239
+ throw invalid(index, `total wait time per call is limited to ${WORK_COMPUTER_WAIT_TOTAL_LIMIT_MS} ms.`);
240
+ }
241
+ validated.push({ type: 'wait', ms: action.ms });
242
+ break;
243
+ }
244
+ default:
245
+ throw invalid(index, `unknown action type "${action.type}".`);
246
+ }
247
+ }
248
+ return validated;
249
+ }
250
+ /**
251
+ * Validate a computer_act "expect" declaration: the outcome the batch is
252
+ * supposed to produce, evaluated by the runtime after the actions settle so
253
+ * "input delivered" cannot be mistaken for "task succeeded".
254
+ */
255
+ export function validateWorkComputerExpectation(value) {
256
+ if (value === undefined || value === null)
257
+ return undefined;
258
+ const invalid = (reason) => new WorkRuntimeError(`computer_act "expect" is invalid: ${reason}`, 400, 'WORK_COMPUTER_INVALID_ACTION');
259
+ if (typeof value !== 'object' || Array.isArray(value)) {
260
+ throw invalid('it must be an object.');
261
+ }
262
+ const raw = value;
263
+ const expectation = {};
264
+ for (const key of ['titleContains', 'urlContains']) {
265
+ if (raw[key] === undefined)
266
+ continue;
267
+ if (typeof raw[key] !== 'string' ||
268
+ raw[key].trim().length === 0 ||
269
+ raw[key].length > WORK_COMPUTER_EXPECT_TEXT_LIMIT) {
270
+ throw invalid(`"${key}" must be a string of 1 to ${WORK_COMPUTER_EXPECT_TEXT_LIMIT} characters.`);
271
+ }
272
+ expectation[key] = raw[key].trim();
273
+ }
274
+ if (raw.regionChanged !== undefined) {
275
+ const region = raw.regionChanged &&
276
+ typeof raw.regionChanged === 'object' &&
277
+ !Array.isArray(raw.regionChanged)
278
+ ? raw.regionChanged
279
+ : undefined;
280
+ const bounded = (field, name, minimum) => {
281
+ if (typeof field !== 'number' ||
282
+ !Number.isInteger(field) ||
283
+ field < minimum ||
284
+ field > WORK_COMPUTER_COORDINATE_LIMIT) {
285
+ throw invalid(`"regionChanged.${name}" must be an integer between ${minimum} and ${WORK_COMPUTER_COORDINATE_LIMIT}.`);
286
+ }
287
+ return field;
288
+ };
289
+ if (!region)
290
+ throw invalid('"regionChanged" must be an object.');
291
+ expectation.regionChanged = {
292
+ x: bounded(region.x, 'x', 0),
293
+ y: bounded(region.y, 'y', 0),
294
+ width: bounded(region.width, 'width', 1),
295
+ height: bounded(region.height, 'height', 1),
296
+ };
297
+ }
298
+ if (raw.withinMs !== undefined) {
299
+ if (typeof raw.withinMs !== 'number' ||
300
+ !Number.isInteger(raw.withinMs) ||
301
+ raw.withinMs < 1 ||
302
+ raw.withinMs > WORK_COMPUTER_EXPECT_WAIT_LIMIT_MS) {
303
+ throw invalid(`"withinMs" must be an integer between 1 and ${WORK_COMPUTER_EXPECT_WAIT_LIMIT_MS}.`);
304
+ }
305
+ expectation.withinMs = raw.withinMs;
306
+ }
307
+ if (expectation.titleContains === undefined &&
308
+ expectation.urlContains === undefined &&
309
+ expectation.regionChanged === undefined) {
310
+ throw invalid('declare at least one of "titleContains", "urlContains", or "regionChanged".');
311
+ }
312
+ return expectation;
313
+ }
38
314
  /** How long a run waits for the shared runtime lease before conflicting. */
39
315
  const runLeaseWaitMs = () => {
40
316
  const parsed = Number.parseInt(process.env.WORK_RUN_LEASE_WAIT_MS ?? '', 10);
@@ -85,6 +361,12 @@ export class WorkRuntimeService {
85
361
  this.runtimeLeases = new Map();
86
362
  this.previewLeaseReleases = new Map();
87
363
  this.terminalHolds = new Map();
364
+ this.screenHolds = new Map();
365
+ // Cross-replica mirror of screenHolds: presence members that tell another
366
+ // replica's teardown (a run ending on the durable worker) that a viewer
367
+ // here still owns the container.
368
+ this.viewerPresenceCounts = new Map();
369
+ this.viewerPresenceTimers = new Map();
88
370
  this.recoveryTasks = new Map();
89
371
  this.recoveryOrphans = new Map();
90
372
  // Sweeps left for the empty-inventory case before giving up on a runtime
@@ -394,6 +676,90 @@ export class WorkRuntimeService {
394
676
  terminalSessionCount(taskId) {
395
677
  return this.terminalHolds.get(taskId) ?? 0;
396
678
  }
679
+ /**
680
+ * A connected Screen viewer owns the running container the way an
681
+ * attached terminal does: while someone watches the Work Computer,
682
+ * workspace-helper teardown and run-end cleanup must not stop the
683
+ * sandbox out from under the GUI session. Callers hold the returned
684
+ * release for the lifetime of the viewer connection.
685
+ */
686
+ async beginScreenSession(task) {
687
+ // Deliberately NOT the runtime lease: in team mode the task's run holds
688
+ // that lease on the durable worker for its whole duration, and a viewer
689
+ // must neither fail against it nor block the next run by holding it.
690
+ // Local holds stop this process's teardown; the presence member stops
691
+ // every other replica's.
692
+ await this.assertTaskIsActive(task);
693
+ const releasePresence = await this.acquireViewerPresence(task.id);
694
+ this.screenHolds.set(task.id, (this.screenHolds.get(task.id) ?? 0) + 1);
695
+ let released = false;
696
+ return async () => {
697
+ if (released)
698
+ return;
699
+ released = true;
700
+ this.noteTaskActivity(task.id);
701
+ const remaining = (this.screenHolds.get(task.id) ?? 1) - 1;
702
+ if (remaining <= 0) {
703
+ this.screenHolds.delete(task.id);
704
+ }
705
+ else {
706
+ this.screenHolds.set(task.id, remaining);
707
+ }
708
+ releasePresence();
709
+ try {
710
+ if (remaining <= 0 && !this.shuttingDown) {
711
+ await this.withLifecycleLock(task.id, (_assertHeld, signal) => this.stopContainerIfIdleWithLock(task, signal));
712
+ }
713
+ }
714
+ catch (error) {
715
+ logger.warn(`Could not idle Work container ${task.containerName} after a screen session:`, error);
716
+ }
717
+ };
718
+ }
719
+ /**
720
+ * Register this process as an active viewer of the task in the shared
721
+ * presence scope, refreshed for as long as at least one viewer remains.
722
+ * Solo mode needs no cross-replica signal and returns a no-op.
723
+ */
724
+ async acquireViewerPresence(taskId) {
725
+ if (getPlatformRuntimeConfig().mode !== 'team') {
726
+ return () => undefined;
727
+ }
728
+ const member = `${this.activityMemberId}:viewer`;
729
+ const refresh = () => withCoordinationTimeout(getCoordinator().setPresence(`work-task-active:${taskId}`, member, 30000), SHARED_COORDINATION_OPERATION_TIMEOUT_MS);
730
+ await refresh();
731
+ const count = this.viewerPresenceCounts.get(taskId) ?? 0;
732
+ if (count === 0) {
733
+ const timer = setInterval(() => {
734
+ void refresh().catch(error => logger.warn(`Could not refresh Work viewer presence for ${taskId}:`, error));
735
+ }, 10000);
736
+ timer.unref?.();
737
+ this.viewerPresenceTimers.set(taskId, timer);
738
+ }
739
+ this.viewerPresenceCounts.set(taskId, count + 1);
740
+ let released = false;
741
+ return () => {
742
+ if (released)
743
+ return;
744
+ released = true;
745
+ const remaining = (this.viewerPresenceCounts.get(taskId) ?? 1) - 1;
746
+ if (remaining > 0) {
747
+ this.viewerPresenceCounts.set(taskId, remaining);
748
+ return;
749
+ }
750
+ this.viewerPresenceCounts.delete(taskId);
751
+ const timer = this.viewerPresenceTimers.get(taskId);
752
+ if (timer)
753
+ clearInterval(timer);
754
+ this.viewerPresenceTimers.delete(taskId);
755
+ void getCoordinator()
756
+ .clearPresence(`work-task-active:${taskId}`, member)
757
+ .catch(error => logger.warn(`Could not clear Work viewer presence for ${taskId}:`, error));
758
+ };
759
+ }
760
+ screenSessionCount(taskId) {
761
+ return this.screenHolds.get(taskId) ?? 0;
762
+ }
397
763
  beginShutdown() {
398
764
  this.shuttingDown = true;
399
765
  if (this.recoveryTimer) {
@@ -574,6 +940,7 @@ export class WorkRuntimeService {
574
940
  }
575
941
  const busy = this.activeCommands.has(task.id) ||
576
942
  (this.terminalHolds.get(task.id) ?? 0) > 0 ||
943
+ (this.screenHolds.get(task.id) ?? 0) > 0 ||
577
944
  (this.runtimeLeases.has(task.id) &&
578
945
  !this.previewLeaseReleases.has(task.id));
579
946
  const sharedActivity = getPlatformRuntimeConfig().mode === 'team'
@@ -864,26 +1231,60 @@ export class WorkRuntimeService {
864
1231
  releaseLease();
865
1232
  }
866
1233
  }
867
- async stopContainerIfIdleWithLock(task, signal) {
868
- if (this.activeCommands.has(task.id))
869
- return;
1234
+ /** Returns true when the container was actually stopped. */
1235
+ async stopContainerIfIdleWithLock(task, signal, options = {}) {
1236
+ if (!options.ignoreActiveCommands && this.activeCommands.has(task.id)) {
1237
+ return false;
1238
+ }
870
1239
  // An attached terminal session owns the running container exactly like a
871
1240
  // ready preview does.
872
1241
  if ((this.terminalHolds.get(task.id) ?? 0) > 0)
873
- return;
1242
+ return false;
1243
+ // So does a watched Work Computer screen: without this, every Files
1244
+ // refresh would destroy the GUI session mid-view.
1245
+ if ((this.screenHolds.get(task.id) ?? 0) > 0)
1246
+ return false;
1247
+ // Holds on other replicas are only visible through the presence scope:
1248
+ // a screen viewer on the app process must survive the durable worker's
1249
+ // run-end teardown, and a run on the worker must survive an app-side
1250
+ // helper's teardown. Own members are excluded — this process's holds
1251
+ // were checked directly above, and a run's own presence must not block
1252
+ // its own cleanup. When coordination is unreachable, keep the container;
1253
+ // the idle sweep is the backstop.
1254
+ if (getPlatformRuntimeConfig().mode === 'team') {
1255
+ try {
1256
+ const members = await withCoordinationTimeout(getCoordinator().listPresence(`work-task-active:${task.id}`), SHARED_COORDINATION_OPERATION_TIMEOUT_MS);
1257
+ if (members.some(member => !member.startsWith(this.activityMemberId))) {
1258
+ return false;
1259
+ }
1260
+ }
1261
+ catch (error) {
1262
+ logger.warn(`Could not check cross-replica activity before idling Work container ${task.containerName}; keeping it:`, error);
1263
+ return false;
1264
+ }
1265
+ }
874
1266
  try {
875
1267
  if ((await this.previewProcessCheckWithLock(task, signal)) === 'ready' &&
876
1268
  this.previewLeaseReleases.has(task.id)) {
877
- return;
1269
+ return false;
878
1270
  }
879
1271
  }
880
1272
  catch (error) {
881
1273
  logger.warn(`Could not verify preview state before idling Work container ${task.containerName}; stopping it:`, error);
882
1274
  }
1275
+ // The preview probe above awaits a container exec; a terminal or screen
1276
+ // hold can land during that window. Re-check synchronously so a viewer
1277
+ // who just attached does not lose the session to a stale decision.
1278
+ if ((!options.ignoreActiveCommands && this.activeCommands.has(task.id)) ||
1279
+ (this.terminalHolds.get(task.id) ?? 0) > 0 ||
1280
+ (this.screenHolds.get(task.id) ?? 0) > 0) {
1281
+ return false;
1282
+ }
883
1283
  await this.stopContainerWithLock(task, signal);
884
1284
  this.releasePreviewLease(task.id);
885
1285
  await this.markPreviewStopped(task.id);
886
1286
  this.completeRecoveryTask(task.id);
1287
+ return true;
887
1288
  }
888
1289
  async readFile(task, requestedPath) {
889
1290
  const workspacePath = validateWorkspacePath(requestedPath, false);
@@ -1158,9 +1559,11 @@ export class WorkRuntimeService {
1158
1559
  if (commandRegistered) {
1159
1560
  // Keep the distributed lifecycle fence through teardown. A
1160
1561
  // second replica must never stop or recreate this sandbox in
1161
- // the gap between preparation and Docker exec.
1162
- await this.stopContainerWithLock(task, signal);
1163
- containerStopped = true;
1562
+ // the gap between preparation and Docker exec. The managed
1563
+ // command script already reaped the command's process group,
1564
+ // so a container held by a screen viewer, terminal, or ready
1565
+ // preview may keep running.
1566
+ containerStopped = await this.stopContainerIfIdleWithLock(task, signal, { ignoreActiveCommands: true });
1164
1567
  }
1165
1568
  }
1166
1569
  });
@@ -1179,6 +1582,284 @@ export class WorkRuntimeService {
1179
1582
  }
1180
1583
  }
1181
1584
  }
1585
+ /**
1586
+ * Start (or confirm) the Work Computer GUI session in a task's sandbox:
1587
+ * virtual display, window manager, browser, and the localhost-only
1588
+ * VNC-to-WebSocket bridge. Idempotent — the in-container script exits 0
1589
+ * when a session is already running. Requires a policy with the GUI
1590
+ * enabled and a networked task (the screen is reached over the published
1591
+ * loopback port, and a computer without network is not useful anyway).
1592
+ */
1593
+ async startComputer(task) {
1594
+ await this.withComputerSession(task, async () => undefined);
1595
+ }
1596
+ /**
1597
+ * The teach recorder's anchor probe: which interactive element sits under
1598
+ * a screen coordinate right now, plus the page URL. Read-only, leaseless,
1599
+ * and best-effort — a missing anchor degrades a demonstration to bare
1600
+ * coordinates, never fails the recording.
1601
+ */
1602
+ async computerAnchorAt(task, x, y) {
1603
+ if (!(await this.computerToolsAvailable(task)))
1604
+ return {};
1605
+ if ((await this.driver.runtimeState(task)) !== 'running')
1606
+ return {};
1607
+ const result = await this.driver.exec(task, ['node', '-e', COMPUTER_ANCHOR_SCRIPT, '--', String(x), String(y)], { timeoutMs: 8000, maxOutputChars: 10000, acceptFailure: true });
1608
+ if (result.exitCode !== 0)
1609
+ return {};
1610
+ try {
1611
+ const parsed = JSON.parse(result.stdout.trim() || '{}');
1612
+ return {
1613
+ ...(typeof parsed.anchor === 'string' && parsed.anchor
1614
+ ? { anchor: parsed.anchor.slice(0, 200) }
1615
+ : {}),
1616
+ ...(typeof parsed.url === 'string' && parsed.url
1617
+ ? { url: parsed.url.slice(0, 300) }
1618
+ : {}),
1619
+ };
1620
+ }
1621
+ catch {
1622
+ return {};
1623
+ }
1624
+ }
1625
+ /** Whether this task may use the agent computer tools. */
1626
+ async computerToolsAvailable(task) {
1627
+ return (task.networkEnabled &&
1628
+ (await workPolicyService.resolve(task.policyId)).guiEnabled === true);
1629
+ }
1630
+ /**
1631
+ * The session's VNC passwords, generated in-container at session start.
1632
+ * `view` goes to every authorized watcher; `control` goes only to the
1633
+ * current takeover-lease holder — the VNC server itself keeps everyone
1634
+ * else's input inert. Undefined on a pre-takeover GUI image (no passwd
1635
+ * file), where the session is view-only for everyone.
1636
+ */
1637
+ async computerCredentials(task) {
1638
+ return this.withComputerSession(task, async (signal) => {
1639
+ const result = await this.driver.exec(task, [
1640
+ '/bin/sh',
1641
+ '-c',
1642
+ 'cat "${LIBRE_COMPUTER_STATE_DIR:-/tmp/libre-computer}/passwd"',
1643
+ ], {
1644
+ timeoutMs: 10000,
1645
+ maxOutputChars: 10000,
1646
+ acceptFailure: true,
1647
+ abortSignal: signal,
1648
+ });
1649
+ if (result.exitCode !== 0)
1650
+ return undefined;
1651
+ const lines = result.stdout
1652
+ .split('\n')
1653
+ .map(line => line.trim())
1654
+ .filter(Boolean);
1655
+ const marker = lines.indexOf('__BEGIN_VIEWONLY__');
1656
+ const control = lines[0];
1657
+ const view = marker > 0 ? lines[marker + 1] : undefined;
1658
+ if (!control || control === '__BEGIN_VIEWONLY__' || !view) {
1659
+ return undefined;
1660
+ }
1661
+ return { control, view };
1662
+ });
1663
+ }
1664
+ /**
1665
+ * The agent's eyes: current screenshot plus cursor and window state from
1666
+ * the task's Work Computer session. Starts the session when needed.
1667
+ */
1668
+ async computerObserve(task) {
1669
+ return this.withComputerSession(task, signal => this.runComputerScript(task, COMPUTER_OBSERVE_SCRIPT, [], 30000, signal));
1670
+ }
1671
+ /**
1672
+ * The agent's hands: a validated action batch executed with xdotool, then
1673
+ * a settle-and-observe so the caller sees the resulting screen in the same
1674
+ * round trip.
1675
+ */
1676
+ async computerAct(task, actions, expect) {
1677
+ const validated = validateWorkComputerActions(actions);
1678
+ const expectation = validateWorkComputerExpectation(expect);
1679
+ // Waits, slow synthetic typing, and expectation polling all extend the
1680
+ // exec deadline; the batch must never be killed mid-gesture by a fixed
1681
+ // timeout.
1682
+ const extraBudgetMs = validated.reduce((total, action) => total +
1683
+ (action.type === 'wait' ? action.ms : 0) +
1684
+ (action.type === 'type' ? action.text.length * 15 : 0) +
1685
+ (action.type === 'scroll_until' ? (action.maxAmount ?? 15) * 400 : 0), 0) +
1686
+ (expectation ? (expectation.withinMs ?? 5000) : 0) +
1687
+ // Adaptive settle polling and per-click receipt captures.
1688
+ 5000;
1689
+ return this.withComputerSession(task, signal => this.runComputerScript(task, COMPUTER_ACT_SCRIPT, [JSON.stringify(validated), JSON.stringify(expectation ?? null)], 60000 + extraBudgetMs, signal));
1690
+ }
1691
+ async runComputerScript(task, script, args, timeoutMs, signal) {
1692
+ const result = await this.driver.exec(task, ['node', '-e', script, '--', ...args], {
1693
+ timeoutMs,
1694
+ maxOutputChars: 4000000,
1695
+ acceptFailure: true,
1696
+ abortSignal: signal,
1697
+ });
1698
+ if (result.exitCode !== 0) {
1699
+ throw new WorkRuntimeError(`The Work Computer action failed: ${(result.stderr ||
1700
+ result.stdout ||
1701
+ 'unknown error')
1702
+ .trim()
1703
+ .slice(0, 300)}`, 500, 'WORK_COMPUTER_ACTION_FAILED');
1704
+ }
1705
+ const parsed = parseJsonOutput(result.stdout);
1706
+ if (typeof parsed.width !== 'number' ||
1707
+ typeof parsed.height !== 'number' ||
1708
+ typeof parsed.cursorX !== 'number' ||
1709
+ typeof parsed.cursorY !== 'number' ||
1710
+ typeof parsed.image !== 'string' ||
1711
+ parsed.image.length === 0) {
1712
+ throw new WorkRuntimeError('The Work Computer returned an invalid observation.', 500, 'WORK_COMPUTER_ACTION_FAILED');
1713
+ }
1714
+ const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value)
1715
+ ? value
1716
+ : {};
1717
+ const fence = parsed.fence && typeof parsed.fence === 'object'
1718
+ ? parsed.fence
1719
+ : undefined;
1720
+ const expect = parsed.expect && typeof parsed.expect === 'object'
1721
+ ? parsed.expect
1722
+ : undefined;
1723
+ return {
1724
+ width: parsed.width,
1725
+ height: parsed.height,
1726
+ cursorX: parsed.cursorX,
1727
+ cursorY: parsed.cursorY,
1728
+ window: typeof parsed.window === 'string' ? parsed.window : '',
1729
+ ...(typeof parsed.windowId === 'number' &&
1730
+ Number.isFinite(parsed.windowId)
1731
+ ? { windowId: parsed.windowId }
1732
+ : {}),
1733
+ ...(typeof parsed.windowCount === 'number'
1734
+ ? { windowCount: parsed.windowCount }
1735
+ : {}),
1736
+ ...(typeof parsed.url === 'string' && parsed.url
1737
+ ? { url: parsed.url.slice(0, 500) }
1738
+ : {}),
1739
+ ...(typeof parsed.pageFocus === 'boolean'
1740
+ ? { pageFocus: parsed.pageFocus }
1741
+ : {}),
1742
+ ...(typeof parsed.focusedElement === 'string' && parsed.focusedElement
1743
+ ? { focusedElement: parsed.focusedElement.slice(0, 200) }
1744
+ : {}),
1745
+ ...(typeof parsed.screenshotSha256 === 'string' && parsed.screenshotSha256
1746
+ ? { screenshotSha256: parsed.screenshotSha256 }
1747
+ : {}),
1748
+ ...(fence &&
1749
+ typeof fence.afterAction === 'number' &&
1750
+ (fence.reason === 'focus_assertion_failed' ||
1751
+ fence.reason === 'context_changed') &&
1752
+ typeof fence.detail === 'string'
1753
+ ? {
1754
+ fence: {
1755
+ afterAction: fence.afterAction,
1756
+ reason: fence.reason,
1757
+ detail: fence.detail.slice(0, 600),
1758
+ },
1759
+ }
1760
+ : {}),
1761
+ ...(expect &&
1762
+ (expect.outcome === 'passed' || expect.outcome === 'pending')
1763
+ ? {
1764
+ expect: {
1765
+ outcome: expect.outcome,
1766
+ ...(Array.isArray(expect.unmet)
1767
+ ? { unmet: expect.unmet.map(String).slice(0, 8) }
1768
+ : {}),
1769
+ },
1770
+ }
1771
+ : {}),
1772
+ ...(Array.isArray(parsed.clickReceipts)
1773
+ ? {
1774
+ clickReceipts: parsed.clickReceipts
1775
+ .slice(0, WORK_COMPUTER_ACTION_LIMIT)
1776
+ .map(asRecord)
1777
+ .filter(receipt => typeof receipt.action === 'number' &&
1778
+ typeof receipt.changed === 'boolean')
1779
+ .map(receipt => ({
1780
+ action: receipt.action,
1781
+ type: String(receipt.type ?? 'click'),
1782
+ x: Number(receipt.x ?? 0),
1783
+ y: Number(receipt.y ?? 0),
1784
+ changed: receipt.changed,
1785
+ })),
1786
+ }
1787
+ : {}),
1788
+ ...(Array.isArray(parsed.scrollReceipts)
1789
+ ? {
1790
+ scrollReceipts: parsed.scrollReceipts
1791
+ .slice(0, WORK_COMPUTER_ACTION_LIMIT)
1792
+ .map(asRecord)
1793
+ .filter(receipt => typeof receipt.action === 'number')
1794
+ .map(receipt => ({
1795
+ action: receipt.action,
1796
+ found: typeof receipt.found === 'boolean' ? receipt.found : null,
1797
+ visible: typeof receipt.visible === 'boolean' ? receipt.visible : null,
1798
+ scrolledUnits: Number(receipt.scrolledUnits ?? 0),
1799
+ })),
1800
+ }
1801
+ : {}),
1802
+ screenshotBase64: parsed.image,
1803
+ };
1804
+ }
1805
+ async withComputerSession(task, fn) {
1806
+ if (!task.networkEnabled) {
1807
+ throw new WorkRuntimeError('The Work Computer requires network access. Enable network access for this Work task first.', 409, 'WORK_COMPUTER_REQUIRES_NETWORK');
1808
+ }
1809
+ const policy = await workPolicyService.resolve(task.policyId);
1810
+ if (policy.guiEnabled !== true) {
1811
+ throw new WorkRuntimeError("This task's Work policy does not enable the Work Computer.", 409, 'WORK_COMPUTER_NOT_ENABLED');
1812
+ }
1813
+ let releaseLease;
1814
+ try {
1815
+ releaseLease = await this.acquireRuntimeLease(task);
1816
+ }
1817
+ catch (error) {
1818
+ // In team mode the lease holder is usually this deployment's own
1819
+ // durable worker executing the task's run — exactly when a human
1820
+ // wants to see the screen. When the sandbox is already running,
1821
+ // attach to it the way workspace helpers do: start-computer is
1822
+ // idempotent, and lifecycle transitions stay lease-guarded.
1823
+ if (error instanceof WorkRuntimeError &&
1824
+ error.code === 'WORK_RUNTIME_LEASE_CONFLICT') {
1825
+ await this.assertTaskIsActive(task);
1826
+ if ((await this.driver.runtimeState(task)) === 'running') {
1827
+ await this.startComputerInContainer(task);
1828
+ const value = await fn();
1829
+ this.noteTaskActivity(task.id);
1830
+ return value;
1831
+ }
1832
+ }
1833
+ throw error;
1834
+ }
1835
+ try {
1836
+ await this.ensureImage(task);
1837
+ await this.assertTaskIsActive(task);
1838
+ return await this.withLifecycleLock(task.id, async (assertHeld, signal) => {
1839
+ await this.assertTaskIsActive(task);
1840
+ this.assertCurrentNetworkPolicy(task);
1841
+ await assertHeld();
1842
+ await this.prepareWithLock(task, signal);
1843
+ await this.startComputerInContainer(task, signal);
1844
+ const value = await fn(signal);
1845
+ this.noteTaskActivity(task.id);
1846
+ return value;
1847
+ });
1848
+ }
1849
+ finally {
1850
+ releaseLease();
1851
+ }
1852
+ }
1853
+ async startComputerInContainer(task, signal) {
1854
+ const result = await this.driver.exec(task, ['/usr/local/bin/start-computer'], { timeoutMs: 60000, ...(signal ? { abortSignal: signal } : {}) });
1855
+ if (result.exitCode !== 0) {
1856
+ const detail = (result.stderr || result.stdout || '')
1857
+ .trim()
1858
+ .slice(0, 300);
1859
+ throw new WorkRuntimeError(`The Work Computer could not start${detail ? `: ${detail}` : '.'} ` +
1860
+ 'The policy image must include the Work Computer GUI stack.', 500, 'WORK_COMPUTER_START_FAILED');
1861
+ }
1862
+ }
1182
1863
  async startPreview(task, command, hooks = {}) {
1183
1864
  if (!task.networkEnabled) {
1184
1865
  throw new WorkRuntimeError('Preview requires network access. Enable network access for this Work task first.', 409, 'WORK_PREVIEW_REQUIRES_NETWORK');
@@ -2364,6 +3045,500 @@ const walk = directory => {
2364
3045
  walk(targetReal);
2365
3046
  process.stdout.write(results.join('\\n'));
2366
3047
  `;
3048
+ /**
3049
+ * Shared observation core for the Work Computer scripts: cursor, active
3050
+ * window, display geometry, a full-screen PNG captured with ImageMagick from
3051
+ * the session's Xvfb display, plus best-effort semantic context — window
3052
+ * identity and count from xdotool, and the active tab's URL, page focus, and
3053
+ * focused-element descriptor from Chromium's loopback DevTools endpoint.
3054
+ * Every semantic signal degrades to absent rather than failing observation.
3055
+ */
3056
+ const COMPUTER_OBSERVE_COMMON = String.raw `
3057
+ const {execFileSync} = require('node:child_process');
3058
+ const {createHash} = require('node:crypto');
3059
+ process.env.DISPLAY = ':' + (process.env.LIBRE_COMPUTER_DISPLAY || '1');
3060
+ const CDP_BASE =
3061
+ 'http://127.0.0.1:' + (process.env.LIBRE_COMPUTER_CDP_PORT || '9222');
3062
+ const out = (cmd, args, timeout = 10000) =>
3063
+ execFileSync(cmd, args, {encoding: 'utf8', timeout});
3064
+ const attempt = fn => {
3065
+ try { return fn(); } catch { return undefined; }
3066
+ };
3067
+ // Cheap synchronous context signals: enough to detect window, modal, and
3068
+ // title transitions between actions without capturing a screenshot.
3069
+ const frame = () => {
3070
+ const windowId = attempt(() =>
3071
+ Number(out('xdotool', ['getactivewindow']).trim())
3072
+ );
3073
+ const windowName =
3074
+ attempt(() => out('xdotool', ['getactivewindow', 'getwindowname']).trim()) || '';
3075
+ const windowCount = attempt(
3076
+ () =>
3077
+ out('xdotool', ['search', '--onlyvisible', '--name', '.'])
3078
+ .split('\n')
3079
+ .filter(Boolean).length
3080
+ );
3081
+ return {windowId, windowName, windowCount};
3082
+ };
3083
+ // Never returns page content or field values — only the identity of the
3084
+ // focused element and whether the page (vs the browser UI) holds focus.
3085
+ const FOCUS_EXPRESSION =
3086
+ '(() => { if (!document.hasFocus()) return {focus: false}; ' +
3087
+ 'const e = document.activeElement; ' +
3088
+ 'if (!e || e === document.body) return {focus: true, el: ""}; ' +
3089
+ 'const a = n => e.getAttribute(n) || ""; ' +
3090
+ 'const label = (a("aria-label") || (e.labels && e.labels[0] && e.labels[0].textContent) || a("placeholder") || a("name") || "").trim().slice(0, 80); ' +
3091
+ 'let d = e.tagName.toLowerCase(); ' +
3092
+ 'if (e.id) d += "#" + e.id; ' +
3093
+ 'if (a("type")) d += "[type=" + a("type") + "]"; ' +
3094
+ 'if (label) d += " (" + label + ")"; ' +
3095
+ 'return {focus: true, el: d}; })()';
3096
+ const cdpActiveTarget = async windowName => {
3097
+ const listResponse = await fetch(CDP_BASE + '/json/list', {
3098
+ signal: AbortSignal.timeout(700),
3099
+ });
3100
+ const targets = (await listResponse.json()).filter(
3101
+ t => t.type === 'page' && t.url && !String(t.url).startsWith('devtools:')
3102
+ );
3103
+ if (!targets.length) return undefined;
3104
+ return (
3105
+ targets.find(
3106
+ t => t.title && windowName && windowName.startsWith(String(t.title).slice(0, 80))
3107
+ ) || targets[0]
3108
+ );
3109
+ };
3110
+ // Evaluate one expression in the active page; undefined on any failure.
3111
+ const cdpEvaluate = async (target, expression) => {
3112
+ if (!target || !target.webSocketDebuggerUrl) return undefined;
3113
+ return new Promise(resolve => {
3114
+ const socket = new WebSocket(target.webSocketDebuggerUrl);
3115
+ const timer = setTimeout(() => {
3116
+ try { socket.close(); } catch {}
3117
+ resolve(undefined);
3118
+ }, 900);
3119
+ socket.onerror = () => { clearTimeout(timer); resolve(undefined); };
3120
+ socket.onopen = () =>
3121
+ socket.send(
3122
+ JSON.stringify({
3123
+ id: 1,
3124
+ method: 'Runtime.evaluate',
3125
+ params: {expression, returnByValue: true},
3126
+ })
3127
+ );
3128
+ socket.onmessage = event => {
3129
+ clearTimeout(timer);
3130
+ try { socket.close(); } catch {}
3131
+ try {
3132
+ resolve(JSON.parse(event.data).result.result.value);
3133
+ } catch { resolve(undefined); }
3134
+ };
3135
+ });
3136
+ };
3137
+ const cdpProbe = async windowName => {
3138
+ const result = {url: '', pageFocus: undefined, focusedElement: ''};
3139
+ try {
3140
+ const active = await cdpActiveTarget(windowName);
3141
+ if (!active) return result;
3142
+ result.url = String(active.url).slice(0, 500);
3143
+ const probe = await cdpEvaluate(active, FOCUS_EXPRESSION);
3144
+ if (probe && typeof probe === 'object') {
3145
+ result.pageFocus = probe.focus === true;
3146
+ result.focusedElement =
3147
+ probe.focus === true ? String(probe.el || '').slice(0, 200) : '';
3148
+ }
3149
+ } catch {}
3150
+ return result;
3151
+ };
3152
+ const observe = async () => {
3153
+ const cursor = {x: 0, y: 0};
3154
+ for (const line of out('xdotool', ['getmouselocation', '--shell']).split('\n')) {
3155
+ const [key, value] = line.split('=');
3156
+ if (key === 'X') cursor.x = Number(value);
3157
+ if (key === 'Y') cursor.y = Number(value);
3158
+ }
3159
+ const current = frame();
3160
+ const geometry = out('xdotool', ['getdisplaygeometry']).trim().split(' ');
3161
+ const png = execFileSync('import', ['-window', 'root', '-silent', 'png:-'], {
3162
+ timeout: 20000,
3163
+ maxBuffer: 32 * 1024 * 1024,
3164
+ });
3165
+ const cdp = await cdpProbe(current.windowName);
3166
+ return {
3167
+ width: Number(geometry[0]),
3168
+ height: Number(geometry[1]),
3169
+ cursorX: cursor.x,
3170
+ cursorY: cursor.y,
3171
+ window: current.windowName.slice(0, 300),
3172
+ ...(current.windowId !== undefined && Number.isFinite(current.windowId)
3173
+ ? {windowId: current.windowId}
3174
+ : {}),
3175
+ ...(current.windowCount !== undefined
3176
+ ? {windowCount: current.windowCount}
3177
+ : {}),
3178
+ ...(cdp.url ? {url: cdp.url} : {}),
3179
+ ...(cdp.pageFocus !== undefined ? {pageFocus: cdp.pageFocus} : {}),
3180
+ ...(cdp.focusedElement ? {focusedElement: cdp.focusedElement} : {}),
3181
+ screenshotSha256: createHash('sha256').update(png).digest('hex'),
3182
+ image: png.toString('base64'),
3183
+ };
3184
+ };
3185
+ `;
3186
+ export const COMPUTER_OBSERVE_SCRIPT = String.raw `${COMPUTER_OBSERVE_COMMON}
3187
+ observe().then(observation => {
3188
+ process.stdout.write(JSON.stringify(observation));
3189
+ }).catch(error => {
3190
+ console.error(error && error.message ? error.message : String(error));
3191
+ process.exit(1);
3192
+ });
3193
+ `;
3194
+ /**
3195
+ * Interpreter for a backend-validated computer_act batch. Input is trusted
3196
+ * JSON (validateWorkComputerActions runs first); every gesture goes through
3197
+ * xdotool argv — no shell interpolation anywhere.
3198
+ */
3199
+ export const COMPUTER_ACT_SCRIPT = String.raw `${COMPUTER_OBSERVE_COMMON}
3200
+ const actions = JSON.parse(process.argv[1] || '[]');
3201
+ const expectation = JSON.parse(process.argv[2] || 'null');
3202
+ const xdotool = args => out('xdotool', args, 120000);
3203
+ const wait = ms =>
3204
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
3205
+ const moveTo = action => {
3206
+ if (action.x !== undefined) {
3207
+ xdotool(['mousemove', '--sync', String(action.x), String(action.y)]);
3208
+ }
3209
+ };
3210
+ const perform = action => {
3211
+ if (action.type === 'move') {
3212
+ xdotool(['mousemove', '--sync', String(action.x), String(action.y)]);
3213
+ } else if (action.type === 'click') {
3214
+ moveTo(action);
3215
+ xdotool(['click', '1']);
3216
+ } else if (action.type === 'double_click') {
3217
+ moveTo(action);
3218
+ xdotool(['click', '--repeat', '2', '--delay', '150', '1']);
3219
+ } else if (action.type === 'right_click') {
3220
+ moveTo(action);
3221
+ xdotool(['click', '3']);
3222
+ } else if (action.type === 'type') {
3223
+ xdotool(['type', '--clearmodifiers', '--delay', '15', '--', action.text]);
3224
+ } else if (action.type === 'key') {
3225
+ xdotool(['key', '--clearmodifiers', '--', action.keys]);
3226
+ } else if (action.type === 'scroll') {
3227
+ moveTo(action);
3228
+ xdotool([
3229
+ 'click',
3230
+ '--repeat',
3231
+ String(action.amount),
3232
+ '--delay',
3233
+ '50',
3234
+ action.direction === 'up' ? '4' : '5',
3235
+ ]);
3236
+ } else if (action.type === 'wait') {
3237
+ wait(action.ms);
3238
+ }
3239
+ }
3240
+ const regionHash = region =>
3241
+ attempt(() => {
3242
+ const crop =
3243
+ String(region.width) + 'x' + String(region.height) +
3244
+ '+' + String(region.x) + '+' + String(region.y);
3245
+ const png = execFileSync(
3246
+ 'import',
3247
+ ['-window', 'root', '-crop', crop, '-silent', 'png:-'],
3248
+ {timeout: 20000, maxBuffer: 32 * 1024 * 1024}
3249
+ );
3250
+ return createHash('sha256').update(png).digest('hex');
3251
+ });
3252
+ const contextChanged = (action, before, after) => {
3253
+ const mouse =
3254
+ action.type === 'move' ||
3255
+ action.type === 'click' ||
3256
+ action.type === 'double_click' ||
3257
+ action.type === 'right_click';
3258
+ if (before.windowCount !== after.windowCount) {
3259
+ return 'a window appeared or closed (modal, popup, or dialog)';
3260
+ }
3261
+ if (before.windowId !== after.windowId) {
3262
+ // A mouse action activating the window under the cursor is the action
3263
+ // doing its job, not context loss; anything else changing the active
3264
+ // window is.
3265
+ return mouse ? '' : 'the active window changed';
3266
+ }
3267
+ if (before.windowName !== after.windowName) return 'the window title changed';
3268
+ return '';
3269
+ };
3270
+ const screenHash = () =>
3271
+ attempt(() => {
3272
+ const png = execFileSync('import', ['-window', 'root', '-silent', 'png:-'], {
3273
+ timeout: 20000,
3274
+ maxBuffer: 32 * 1024 * 1024,
3275
+ });
3276
+ return createHash('sha256').update(png).digest('hex');
3277
+ });
3278
+ // Adaptive settle: two identical consecutive captures mean the screen has
3279
+ // stopped changing; the deadline bounds pages that never go quiet.
3280
+ const settleScreen = () => {
3281
+ const deadline = Date.now() + 2500;
3282
+ let previous = screenHash();
3283
+ wait(250);
3284
+ for (;;) {
3285
+ const current = screenHash();
3286
+ if (
3287
+ (previous && current && previous === current) ||
3288
+ Date.now() >= deadline
3289
+ ) {
3290
+ return;
3291
+ }
3292
+ previous = current;
3293
+ wait(300);
3294
+ }
3295
+ };
3296
+ const CLICK_RECEIPT_SIZE = 120;
3297
+ const clickReceiptRegion = action => ({
3298
+ x: Math.max(0, action.x - CLICK_RECEIPT_SIZE / 2),
3299
+ y: Math.max(0, action.y - CLICK_RECEIPT_SIZE / 2),
3300
+ width: CLICK_RECEIPT_SIZE,
3301
+ height: CLICK_RECEIPT_SIZE,
3302
+ });
3303
+ // Goal-directed scrolling: advance in small steps until the target text is
3304
+ // visible (or the page edge is reached), verified through the page itself.
3305
+ // Without a semantic signal it degrades to one bounded plain scroll.
3306
+ const scrollUntil = async action => {
3307
+ const receipt = {action: 0, found: null, visible: null, scrolledUnits: 0};
3308
+ const probe = async () => {
3309
+ try {
3310
+ const active = await cdpActiveTarget(frame().windowName);
3311
+ if (!active) return undefined;
3312
+ let expression;
3313
+ if (action.target.text !== undefined) {
3314
+ expression =
3315
+ '(() => { const needle = ' +
3316
+ JSON.stringify(String(action.target.text).toLowerCase()) +
3317
+ '; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); ' +
3318
+ 'let node; let host = null; ' +
3319
+ 'while ((node = walker.nextNode())) { ' +
3320
+ 'if (String(node.nodeValue).toLowerCase().includes(needle)) { host = node.parentElement; break; } } ' +
3321
+ 'if (!host) return {found: false, visible: false}; ' +
3322
+ 'const r = host.getBoundingClientRect(); ' +
3323
+ 'return {found: true, visible: r.top >= 0 && r.bottom <= window.innerHeight && r.height > 0}; })()';
3324
+ } else {
3325
+ expression =
3326
+ '(() => { const el = document.scrollingElement || document.documentElement; ' +
3327
+ 'return {found: true, visible: ' +
3328
+ (action.target.edge === 'bottom'
3329
+ ? 'Math.ceil(el.scrollTop + window.innerHeight) >= el.scrollHeight'
3330
+ : 'el.scrollTop <= 0') +
3331
+ '}; })()';
3332
+ }
3333
+ return await cdpEvaluate(active, expression);
3334
+ } catch {
3335
+ return undefined;
3336
+ }
3337
+ };
3338
+ const maxUnits = action.maxAmount || 15;
3339
+ const button = action.direction === 'up' ? '4' : '5';
3340
+ moveTo(action);
3341
+ for (;;) {
3342
+ const state = await probe();
3343
+ if (state === undefined) {
3344
+ const remaining = maxUnits - receipt.scrolledUnits;
3345
+ if (remaining > 0) {
3346
+ xdotool(['click', '--repeat', String(remaining), '--delay', '50', button]);
3347
+ receipt.scrolledUnits += remaining;
3348
+ }
3349
+ return receipt;
3350
+ }
3351
+ receipt.found = state.found === true;
3352
+ receipt.visible = state.visible === true;
3353
+ if (
3354
+ (receipt.found && receipt.visible) ||
3355
+ receipt.scrolledUnits >= maxUnits
3356
+ ) {
3357
+ return receipt;
3358
+ }
3359
+ const units = Math.min(3, maxUnits - receipt.scrolledUnits);
3360
+ xdotool(['click', '--repeat', String(units), '--delay', '50', button]);
3361
+ receipt.scrolledUnits += units;
3362
+ wait(200);
3363
+ }
3364
+ };
3365
+ const main = async () => {
3366
+ const beforeRegion =
3367
+ expectation && expectation.regionChanged
3368
+ ? regionHash(expectation.regionChanged)
3369
+ : undefined;
3370
+ let fence;
3371
+ const clickReceipts = [];
3372
+ const scrollReceipts = [];
3373
+ for (let index = 0; index < actions.length; index++) {
3374
+ const action = actions[index];
3375
+ if ((action.type === 'type' || action.type === 'key') && action.focus) {
3376
+ const current = frame();
3377
+ const cdp = await cdpProbe(current.windowName);
3378
+ const browserChrome =
3379
+ cdp.pageFocus === false && /chromium/i.test(current.windowName);
3380
+ const haystack = [
3381
+ current.windowName,
3382
+ cdp.url,
3383
+ cdp.focusedElement,
3384
+ browserChrome ? 'browser-chrome' : '',
3385
+ ]
3386
+ .filter(Boolean)
3387
+ .join(' | ');
3388
+ const needle = action.focus.toLowerCase();
3389
+ const matches = haystack.toLowerCase().includes(needle);
3390
+ if (!matches || (browserChrome && needle !== 'browser-chrome')) {
3391
+ fence = {
3392
+ afterAction: index,
3393
+ reason: 'focus_assertion_failed',
3394
+ detail:
3395
+ 'Action ' + String(index + 1) + ' required keyboard focus matching "' +
3396
+ action.focus + '" but the current focus is: ' +
3397
+ (haystack || 'unknown') +
3398
+ (browserChrome
3399
+ ? '. The page does NOT hold keyboard focus — typed text would go to the browser UI.'
3400
+ : '') +
3401
+ ' This and the remaining action(s) did not run.',
3402
+ };
3403
+ break;
3404
+ }
3405
+ }
3406
+ const before = frame();
3407
+ const receiptRegion =
3408
+ (action.type === 'click' ||
3409
+ action.type === 'double_click' ||
3410
+ action.type === 'right_click') &&
3411
+ action.x !== undefined
3412
+ ? clickReceiptRegion(action)
3413
+ : undefined;
3414
+ if (action.type === 'scroll_until') {
3415
+ const receipt = await scrollUntil(action);
3416
+ receipt.action = index + 1;
3417
+ scrollReceipts.push(receipt);
3418
+ } else if (receiptRegion) {
3419
+ const beforeHash = regionHash(receiptRegion);
3420
+ perform(action);
3421
+ wait(150);
3422
+ const afterHash = regionHash(receiptRegion);
3423
+ // "Pixels near the click changed" — a signal, not a verdict: a dead
3424
+ // click on empty space reads changed:false even though it delivered.
3425
+ clickReceipts.push({
3426
+ action: index + 1,
3427
+ type: action.type,
3428
+ x: action.x,
3429
+ y: action.y,
3430
+ changed: Boolean(beforeHash && afterHash && beforeHash !== afterHash),
3431
+ });
3432
+ } else {
3433
+ perform(action);
3434
+ }
3435
+ if (index < actions.length - 1) {
3436
+ const changed = contextChanged(action, before, frame());
3437
+ if (changed) {
3438
+ fence = {
3439
+ afterAction: index + 1,
3440
+ reason: 'context_changed',
3441
+ detail:
3442
+ 'After action ' + String(index + 1) + ', ' + changed +
3443
+ '; the remaining ' + String(actions.length - index - 1) +
3444
+ ' action(s) did not run because their coordinates targeted the previous screen. Observe before continuing.',
3445
+ };
3446
+ break;
3447
+ }
3448
+ }
3449
+ }
3450
+ settleScreen();
3451
+ let expectResult;
3452
+ if (expectation && !fence) {
3453
+ const deadline = Date.now() + (expectation.withinMs || 5000);
3454
+ let unmet = [];
3455
+ for (;;) {
3456
+ unmet = [];
3457
+ const current = frame();
3458
+ if (
3459
+ expectation.titleContains !== undefined &&
3460
+ !current.windowName.toLowerCase().includes(expectation.titleContains.toLowerCase())
3461
+ ) {
3462
+ unmet.push('titleContains');
3463
+ }
3464
+ if (expectation.urlContains !== undefined) {
3465
+ const cdp = await cdpProbe(current.windowName);
3466
+ if (!cdp.url.toLowerCase().includes(expectation.urlContains.toLowerCase())) {
3467
+ unmet.push('urlContains');
3468
+ }
3469
+ }
3470
+ if (expectation.regionChanged) {
3471
+ const currentRegion = regionHash(expectation.regionChanged);
3472
+ if (!beforeRegion || !currentRegion || currentRegion === beforeRegion) {
3473
+ unmet.push('regionChanged');
3474
+ }
3475
+ }
3476
+ if (!unmet.length || Date.now() >= deadline) break;
3477
+ wait(500);
3478
+ }
3479
+ expectResult = unmet.length
3480
+ ? {outcome: 'pending', unmet}
3481
+ : {outcome: 'passed'};
3482
+ }
3483
+ const observation = await observe();
3484
+ process.stdout.write(
3485
+ JSON.stringify({
3486
+ ...observation,
3487
+ ...(fence ? {fence} : {}),
3488
+ ...(expectResult ? {expect: expectResult} : {}),
3489
+ ...(clickReceipts.length ? {clickReceipts} : {}),
3490
+ ...(scrollReceipts.length ? {scrollReceipts} : {}),
3491
+ })
3492
+ );
3493
+ };
3494
+ main().catch(error => {
3495
+ console.error(error && error.message ? error.message : String(error));
3496
+ process.exit(1);
3497
+ });
3498
+ `;
3499
+ /**
3500
+ * Resolve the page element under a screen coordinate — the teach recorder's
3501
+ * anchor probe. Runs during a human demonstration, so it must never inject
3502
+ * input or steal focus: it only reads. Returns {} outside the browser
3503
+ * viewport, on non-browser windows, or on pre-CDP GUI images.
3504
+ */
3505
+ export const COMPUTER_ANCHOR_SCRIPT = String.raw `${COMPUTER_OBSERVE_COMMON}
3506
+ const pointX = Number(process.argv[1]);
3507
+ const pointY = Number(process.argv[2]);
3508
+ const main = async () => {
3509
+ const result = {};
3510
+ try {
3511
+ const active = await cdpActiveTarget(frame().windowName);
3512
+ if (active) {
3513
+ result.url = String(active.url).slice(0, 300);
3514
+ const expression =
3515
+ '(() => { const sx = ' + String(pointX) + '; const sy = ' + String(pointY) + '; ' +
3516
+ 'const px = sx - (window.screenX + (window.outerWidth - window.innerWidth) / 2); ' +
3517
+ 'const py = sy - (window.screenY + (window.outerHeight - window.innerHeight)); ' +
3518
+ 'if (px < 0 || py < 0 || px > window.innerWidth || py > window.innerHeight) return null; ' +
3519
+ 'const el = document.elementFromPoint(px, py); if (!el) return null; ' +
3520
+ 'const host = el.closest("button, a, input, select, textarea, label, summary, [role]") || el; ' +
3521
+ 'const a = n => host.getAttribute(n) || ""; ' +
3522
+ 'const text = (host.innerText || host.value || "").split("\\n").join(" ").split("\\t").join(" ").trim().slice(0, 60); ' +
3523
+ 'const label = (a("aria-label") || a("placeholder") || a("name") || text).trim(); ' +
3524
+ 'let d = host.tagName.toLowerCase(); ' +
3525
+ 'if (host.id) d += "#" + host.id; ' +
3526
+ 'if (a("type")) d += "[type=" + a("type") + "]"; ' +
3527
+ 'if (label) d += " (" + label + ")"; ' +
3528
+ 'return d; })()';
3529
+ const anchor = await cdpEvaluate(active, expression);
3530
+ if (typeof anchor === 'string' && anchor) {
3531
+ result.anchor = anchor.slice(0, 200);
3532
+ }
3533
+ }
3534
+ } catch {}
3535
+ process.stdout.write(JSON.stringify(result));
3536
+ };
3537
+ main().catch(error => {
3538
+ console.error(error && error.message ? error.message : String(error));
3539
+ process.exit(1);
3540
+ });
3541
+ `;
2367
3542
  /**
2368
3543
  * Select the runtime backend for this deployment. Docker remains the
2369
3544
  * default; WORK_RUNTIME_BACKEND=kubernetes runs sandboxes as Pods in a