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.
- package/README.md +57 -29
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +7 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/persistence/postgresAutomationWorkTargetMigration.d.ts +9 -0
- package/backend/dist/persistence/postgresAutomationWorkTargetMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresAutomationWorkTargetMigration.js +44 -0
- package/backend/dist/persistence/postgresAutomationWorkTargetMigration.js.map +1 -0
- package/backend/dist/persistence/postgresMfaPushRecoveryMigration.d.ts +9 -0
- package/backend/dist/persistence/postgresMfaPushRecoveryMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresMfaPushRecoveryMigration.js +107 -0
- package/backend/dist/persistence/postgresMfaPushRecoveryMigration.js.map +1 -0
- package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
- package/backend/dist/persistence/postgresMigrationRegistry.js +8 -0
- package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.js +179 -4
- package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/postgresSecurityRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresSecurityRepositories.js +154 -0
- package/backend/dist/persistence/postgresSecurityRepositories.js.map +1 -1
- package/backend/dist/persistence/postgresWorkComputerMigration.d.ts +8 -0
- package/backend/dist/persistence/postgresWorkComputerMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresWorkComputerMigration.js +37 -0
- package/backend/dist/persistence/postgresWorkComputerMigration.js.map +1 -0
- package/backend/dist/persistence/postgresWorkTakeoverMigration.d.ts +9 -0
- package/backend/dist/persistence/postgresWorkTakeoverMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresWorkTakeoverMigration.js +38 -0
- package/backend/dist/persistence/postgresWorkTakeoverMigration.js.map +1 -0
- package/backend/dist/persistence/resourceTypes.d.ts +64 -0
- package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
- package/backend/dist/persistence/resourceTypes.js.map +1 -1
- package/backend/dist/persistence/securityTypes.d.ts +60 -0
- package/backend/dist/persistence/securityTypes.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.d.ts +4 -0
- package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.js +267 -0
- package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.js +151 -7
- package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteSecurityRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteSecurityRepositories.js +140 -0
- package/backend/dist/persistence/sqliteSecurityRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.js +45 -0
- package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
- package/backend/dist/platform/jobs/domainJobContracts.d.ts +2 -0
- package/backend/dist/platform/jobs/domainJobContracts.d.ts.map +1 -1
- package/backend/dist/platform/jobs/domainJobContracts.js +2 -0
- package/backend/dist/platform/jobs/domainJobContracts.js.map +1 -1
- package/backend/dist/platform/jobs/domainJobHandlers.d.ts.map +1 -1
- package/backend/dist/platform/jobs/domainJobHandlers.js +152 -2
- package/backend/dist/platform/jobs/domainJobHandlers.js.map +1 -1
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts.map +1 -1
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.js +7 -3
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.js.map +1 -1
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts.map +1 -1
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js +8 -4
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js.map +1 -1
- package/backend/dist/platform/workPersistence/types.d.ts +2 -0
- package/backend/dist/platform/workPersistence/types.d.ts.map +1 -1
- package/backend/dist/platform/workPersistence/types.js.map +1 -1
- package/backend/dist/routes/auth.d.ts.map +1 -1
- package/backend/dist/routes/auth.js +427 -5
- package/backend/dist/routes/auth.js.map +1 -1
- package/backend/dist/routes/automations.d.ts.map +1 -1
- package/backend/dist/routes/automations.js +5 -0
- package/backend/dist/routes/automations.js.map +1 -1
- package/backend/dist/routes/documents.d.ts.map +1 -1
- package/backend/dist/routes/documents.js +4 -0
- package/backend/dist/routes/documents.js.map +1 -1
- package/backend/dist/routes/push.d.ts +3 -0
- package/backend/dist/routes/push.d.ts.map +1 -0
- package/backend/dist/routes/push.js +105 -0
- package/backend/dist/routes/push.js.map +1 -0
- package/backend/dist/routes/recovery.d.ts +3 -0
- package/backend/dist/routes/recovery.d.ts.map +1 -0
- package/backend/dist/routes/recovery.js +83 -0
- package/backend/dist/routes/recovery.js.map +1 -0
- package/backend/dist/routes/users.d.ts.map +1 -1
- package/backend/dist/routes/users.js +35 -0
- package/backend/dist/routes/users.js.map +1 -1
- package/backend/dist/routes/work.d.ts.map +1 -1
- package/backend/dist/routes/work.js +205 -1
- package/backend/dist/routes/work.js.map +1 -1
- package/backend/dist/services/authService.d.ts +8 -0
- package/backend/dist/services/authService.d.ts.map +1 -1
- package/backend/dist/services/authService.js +29 -0
- package/backend/dist/services/authService.js.map +1 -1
- package/backend/dist/services/automationSchedulerService.d.ts.map +1 -1
- package/backend/dist/services/automationSchedulerService.js +31 -0
- package/backend/dist/services/automationSchedulerService.js.map +1 -1
- package/backend/dist/services/automationService.d.ts +4 -1
- package/backend/dist/services/automationService.d.ts.map +1 -1
- package/backend/dist/services/automationService.js +27 -0
- package/backend/dist/services/automationService.js.map +1 -1
- package/backend/dist/services/dataArchiveService.d.ts.map +1 -1
- package/backend/dist/services/dataArchiveService.js +3 -0
- package/backend/dist/services/dataArchiveService.js.map +1 -1
- package/backend/dist/services/documentMediaExtractionService.d.ts +40 -0
- package/backend/dist/services/documentMediaExtractionService.d.ts.map +1 -0
- package/backend/dist/services/documentMediaExtractionService.js +157 -0
- package/backend/dist/services/documentMediaExtractionService.js.map +1 -0
- package/backend/dist/services/documentService.d.ts +6 -0
- package/backend/dist/services/documentService.d.ts.map +1 -1
- package/backend/dist/services/documentService.js +58 -4
- package/backend/dist/services/documentService.js.map +1 -1
- package/backend/dist/services/encryptionService.d.ts +6 -0
- package/backend/dist/services/encryptionService.d.ts.map +1 -1
- package/backend/dist/services/encryptionService.js +12 -0
- package/backend/dist/services/encryptionService.js.map +1 -1
- package/backend/dist/services/mfaService.d.ts +64 -0
- package/backend/dist/services/mfaService.d.ts.map +1 -0
- package/backend/dist/services/mfaService.js +262 -0
- package/backend/dist/services/mfaService.js.map +1 -0
- package/backend/dist/services/notificationService.d.ts +5 -0
- package/backend/dist/services/notificationService.d.ts.map +1 -1
- package/backend/dist/services/notificationService.js +33 -1
- package/backend/dist/services/notificationService.js.map +1 -1
- package/backend/dist/services/preferencesService.d.ts +1 -1
- package/backend/dist/services/preferencesService.d.ts.map +1 -1
- package/backend/dist/services/preferencesService.js +1 -0
- package/backend/dist/services/preferencesService.js.map +1 -1
- package/backend/dist/services/recoveryDrillService.d.ts +57 -0
- package/backend/dist/services/recoveryDrillService.d.ts.map +1 -0
- package/backend/dist/services/recoveryDrillService.js +363 -0
- package/backend/dist/services/recoveryDrillService.js.map +1 -0
- package/backend/dist/services/webPushService.d.ts +71 -0
- package/backend/dist/services/webPushService.d.ts.map +1 -0
- package/backend/dist/services/webPushService.js +328 -0
- package/backend/dist/services/webPushService.js.map +1 -0
- package/backend/dist/services/webauthnService.d.ts +58 -0
- package/backend/dist/services/webauthnService.d.ts.map +1 -0
- package/backend/dist/services/webauthnService.js +301 -0
- package/backend/dist/services/webauthnService.js.map +1 -0
- package/backend/dist/services/websocketTicketService.d.ts +1 -1
- package/backend/dist/services/websocketTicketService.d.ts.map +1 -1
- package/backend/dist/services/websocketTicketService.js +3 -1
- package/backend/dist/services/websocketTicketService.js.map +1 -1
- package/backend/dist/services/workAgentGuidance.d.ts +14 -0
- package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
- package/backend/dist/services/workAgentGuidance.js +38 -2
- package/backend/dist/services/workAgentGuidance.js.map +1 -1
- package/backend/dist/services/workAgentService.d.ts +9 -0
- package/backend/dist/services/workAgentService.d.ts.map +1 -1
- package/backend/dist/services/workAgentService.js +503 -13
- package/backend/dist/services/workAgentService.js.map +1 -1
- package/backend/dist/services/workComputerSetupService.d.ts +33 -0
- package/backend/dist/services/workComputerSetupService.d.ts.map +1 -0
- package/backend/dist/services/workComputerSetupService.js +164 -0
- package/backend/dist/services/workComputerSetupService.js.map +1 -0
- package/backend/dist/services/workComputerTeachService.d.ts +105 -0
- package/backend/dist/services/workComputerTeachService.d.ts.map +1 -0
- package/backend/dist/services/workComputerTeachService.js +513 -0
- package/backend/dist/services/workComputerTeachService.js.map +1 -0
- package/backend/dist/services/workKubernetesDriver.d.ts +8 -0
- package/backend/dist/services/workKubernetesDriver.d.ts.map +1 -1
- package/backend/dist/services/workKubernetesDriver.js +22 -0
- package/backend/dist/services/workKubernetesDriver.js.map +1 -1
- package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
- package/backend/dist/services/workModelProviderService.js +102 -8
- package/backend/dist/services/workModelProviderService.js.map +1 -1
- package/backend/dist/services/workPolicyService.d.ts +8 -0
- package/backend/dist/services/workPolicyService.d.ts.map +1 -1
- package/backend/dist/services/workPolicyService.js +25 -0
- package/backend/dist/services/workPolicyService.js.map +1 -1
- package/backend/dist/services/workRuntimeDriver.d.ts +32 -0
- package/backend/dist/services/workRuntimeDriver.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeDriver.js +54 -8
- package/backend/dist/services/workRuntimeDriver.js.map +1 -1
- package/backend/dist/services/workRuntimeService.d.ts +212 -1
- package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeService.js +1184 -9
- package/backend/dist/services/workRuntimeService.js.map +1 -1
- package/backend/dist/services/workRuntimeShared.d.ts +11 -1
- package/backend/dist/services/workRuntimeShared.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeShared.js +24 -1
- package/backend/dist/services/workRuntimeShared.js.map +1 -1
- package/backend/dist/services/workScreenControlService.d.ts +60 -0
- package/backend/dist/services/workScreenControlService.d.ts.map +1 -0
- package/backend/dist/services/workScreenControlService.js +201 -0
- package/backend/dist/services/workScreenControlService.js.map +1 -0
- package/backend/dist/services/workTaskService.d.ts +11 -0
- package/backend/dist/services/workTaskService.d.ts.map +1 -1
- package/backend/dist/services/workTaskService.js +31 -0
- package/backend/dist/services/workTaskService.js.map +1 -1
- package/backend/dist/types/index.d.ts +10 -2
- package/backend/dist/types/index.d.ts.map +1 -1
- package/backend/dist/types/index.js.map +1 -1
- package/backend/dist/types/work.d.ts +13 -0
- package/backend/dist/types/work.d.ts.map +1 -1
- package/backend/dist/utils/documentExtraction.d.ts +31 -3
- package/backend/dist/utils/documentExtraction.d.ts.map +1 -1
- package/backend/dist/utils/documentExtraction.js +106 -0
- package/backend/dist/utils/documentExtraction.js.map +1 -1
- package/backend/dist/utils/toolEgress.d.ts +2 -1
- package/backend/dist/utils/toolEgress.d.ts.map +1 -1
- package/backend/dist/utils/toolEgress.js +14 -2
- package/backend/dist/utils/toolEgress.js.map +1 -1
- package/backend/dist/utils/totp.d.ts +19 -0
- package/backend/dist/utils/totp.d.ts.map +1 -0
- package/backend/dist/utils/totp.js +117 -0
- package/backend/dist/utils/totp.js.map +1 -0
- package/backend/dist/utils/webauthn.d.ts +47 -0
- package/backend/dist/utils/webauthn.d.ts.map +1 -0
- package/backend/dist/utils/webauthn.js +323 -0
- package/backend/dist/utils/webauthn.js.map +1 -0
- package/backend/dist/websocketServer.d.ts.map +1 -1
- package/backend/dist/websocketServer.js +9 -0
- package/backend/dist/websocketServer.js.map +1 -1
- package/backend/dist/workScreenServer.d.ts +12 -0
- package/backend/dist/workScreenServer.d.ts.map +1 -0
- package/backend/dist/workScreenServer.js +291 -0
- package/backend/dist/workScreenServer.js.map +1 -0
- package/backend/package.json +1 -1
- package/deploy/work-computer/Dockerfile +48 -0
- package/deploy/work-computer/README.md +93 -0
- package/deploy/work-computer/fonts/fonts.css +335 -0
- package/deploy/work-computer/fonts/v16-aFTR7PB1QTsUX8KYvumzEY2tbYf-Vlh3uA.woff2 +0 -0
- package/deploy/work-computer/fonts/v16-aFTR7PB1QTsUX8KYvumzEYOtbYf-Vlg.woff2 +0 -0
- package/deploy/work-computer/fonts/v16-aFTU7PB1QTsUX8KYthSQBK6PYK3EXw.woff2 +0 -0
- package/deploy/work-computer/fonts/v16-aFTU7PB1QTsUX8KYthqQBK6PYK0.woff2 +0 -0
- package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggOxSvfedN62Zw.woff2 +0 -0
- package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggSxSvfedN62Zw.woff2 +0 -0
- package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggexSvfedN4.woff2 +0 -0
- package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggixSvfedN62Zw.woff2 +0 -0
- package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggmxSvfedN62Zw.woff2 +0 -0
- package/deploy/work-computer/fonts/v20-xn7gYHE41ni1AdIRggqxSvfedN62Zw.woff2 +0 -0
- package/deploy/work-computer/fonts/v22-V8mDoQDjQSkFtoMM3T6r8E7mPb54C_k3HqUtEw.woff2 +0 -0
- package/deploy/work-computer/fonts/v22-V8mDoQDjQSkFtoMM3T6r8E7mPb94C_k3HqUtEw.woff2 +0 -0
- package/deploy/work-computer/fonts/v22-V8mDoQDjQSkFtoMM3T6r8E7mPbF4C_k3HqU.woff2 +0 -0
- package/deploy/work-computer/start-computer.sh +155 -0
- package/deploy/work-computer/start-page.html +251 -0
- package/deploy/work-computer/tint2rc +76 -0
- package/deploy/work-computer/wallpaper.png +0 -0
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/assets/index-DGy6mo0B.js +33 -0
- package/frontend/dist/assets/index-DqLJKWF-.css +1 -0
- package/frontend/dist/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/frontend/dist/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
- package/frontend/dist/fonts/jetbrains-mono.css +24 -0
- package/frontend/dist/index.html +40 -18
- package/frontend/dist/js/ArtifactContainer-DvIMxleC.js +1 -0
- package/frontend/dist/js/{ArtifactDemoPage-D0JEIAbZ.js → ArtifactDemoPage-BqYskFqc.js} +1 -1
- package/frontend/dist/js/ArtifactSandboxFrame-9wuufHn6.js +111 -0
- package/frontend/dist/js/ArtifactSlideOutPanel-DXESMtPh.js +1 -0
- package/frontend/dist/js/AutomationsPage-DByxhIKj.js +1 -0
- package/frontend/dist/js/AvatarUpload-DnCGQ46o.js +1 -0
- package/frontend/dist/js/{Button-C0r3J9G_.js → Button-CpobxJ2P.js} +1 -1
- package/frontend/dist/js/CalendarPage-JywVP-9y.js +1 -0
- package/frontend/dist/js/ChannelsPage-B31rcARd.js +1 -0
- package/frontend/dist/js/ChatPage-CHXJY8m3.js +14 -0
- package/frontend/dist/js/{EvaluationsPage-Bm8FRoeX.js → EvaluationsPage-CcmB5wdF.js} +1 -1
- package/frontend/dist/js/FirstTimeSetup-DDU7SowM.js +1 -0
- package/frontend/dist/js/GalleryPage-XfX_7iCf.js +1 -0
- package/frontend/dist/js/HomePage-DAZAkoAV.js +1 -0
- package/frontend/dist/js/LibreClawPage-De0gZmbF.js +1 -0
- package/frontend/dist/js/Logo-yKM43xGa.js +1 -0
- package/frontend/dist/js/LogoMark-PmpKQOre.js +1 -0
- package/frontend/dist/js/{ModelManagerModals-DX9eT5r2.js → ModelManagerModals-hz2cZZ4a.js} +1 -1
- package/frontend/dist/js/ModelSelector-BoFehQy-.js +1 -0
- package/frontend/dist/js/NotesPage-BGPI3gG6.js +3 -0
- package/frontend/dist/js/OptimizedSyntaxHighlighter-DrejdNUm.js +1 -0
- package/frontend/dist/js/OptimizedSyntaxHighlighter-hNqZPLiB.js +1 -0
- package/frontend/dist/js/PasswordStrengthMeter-RwModpZ1.js +1 -0
- package/frontend/dist/js/PersonasPage-BkNRD2wO.js +13 -0
- package/frontend/dist/js/PluginUsagePage-Cqt7V2SQ.js +1 -0
- package/frontend/dist/js/RichMessageContent-B2QSyX-l.js +1 -0
- package/frontend/dist/js/RichMessageContent-CYEhT6uU.js +3 -0
- package/frontend/dist/js/SettingsModal-IbIvVyss.js +19 -0
- package/frontend/dist/js/{SettingsToggle-BIVyfGjl.js → SettingsToggle-3GxA96E0.js} +1 -1
- package/frontend/dist/js/ShareDialog-BoE4OVhV.js +1 -0
- package/frontend/dist/js/SystemPage-B5ecz5H3.js +1 -0
- package/frontend/dist/js/TurnstileWidget-DHU9IUM1.js +1 -0
- package/frontend/dist/js/UserManagementPage-C0VUfbMk.js +1 -0
- package/frontend/dist/js/WorkPage-EweYI5Dx.js +74 -0
- package/frontend/dist/js/api-Bbh2AHHN.js +1 -0
- package/frontend/dist/js/api-F0CnlVH8.js +14 -0
- package/frontend/dist/js/appNavigation-DqV69ohR.js +1 -0
- package/frontend/dist/js/appStore-SLLBamo2.js +2 -0
- package/frontend/dist/js/appVersion-46bF9UkF.js +1 -0
- package/frontend/dist/js/ar-C4yo7x5J.js +2 -0
- package/frontend/dist/js/artifactHtml-DDmfpcB0.js +41 -0
- package/frontend/dist/js/authStore-CcfL8T6T.js +1 -0
- package/frontend/dist/js/authStore-DlSQ1TcP.js +4 -0
- package/frontend/dist/js/axios-_ZpUYd3e.js +1 -0
- package/frontend/dist/js/bn-BeZ6d2qF.js +2 -0
- package/frontend/dist/js/chatStore-DI79tEJ-.js +3 -0
- package/frontend/dist/js/classPrivateMethodInitSpec-B31Q4d2M.js +1 -0
- package/frontend/dist/js/{config-8KdgjtEx.js → config-0QgGZCrI.js} +1 -1
- package/frontend/dist/js/cs-9K5LILI5.js +2 -0
- package/frontend/dist/js/da-BWbBhLK8.js +2 -0
- package/frontend/dist/js/de-BXD9t2uY.js +2 -0
- package/frontend/dist/js/es-Dv4wZKlU.js +2 -0
- package/frontend/dist/js/{estree-BU5-LV_1.js → estree-WDwviHUZ.js} +1 -1
- package/frontend/dist/js/evaluationsApi-wjf0nh5J.js +1 -0
- package/frontend/dist/js/fr-BkXGyEei.js +2 -0
- package/frontend/dist/js/hi-BS1ldbWk.js +2 -0
- package/frontend/dist/js/{html-DeoAy2bo.js → html-Cj0HNcRt.js} +1 -1
- package/frontend/dist/js/id-Cr1njx5M.js +2 -0
- package/frontend/dist/js/is-BqpNCtDd.js +2 -0
- package/frontend/dist/js/it-Dp-iwH27.js +2 -0
- package/frontend/dist/js/ja-_e4v_6Mn.js +2 -0
- package/frontend/dist/js/ko-CjXf2pIX.js +2 -0
- package/frontend/dist/js/{logger-DSXjF4ye.js → logger-CpCQz3Vi.js} +1 -1
- package/frontend/dist/js/{markdown-DOLsYkBY.js → markdown-DTahdtRF.js} +1 -1
- package/frontend/dist/js/{messageCodeStyles-DhVgizF-.js → messageCodeStyles-COJFoq4P.js} +2 -2
- package/frontend/dist/js/ms-xpn-zACj.js +2 -0
- package/frontend/dist/js/nl-Baf0a2X0.js +2 -0
- package/frontend/dist/js/pl-BEOTrcUv.js +2 -0
- package/frontend/dist/js/pt-B5FUaihM.js +2 -0
- package/frontend/dist/js/rfb-bJaPz33m.js +5 -0
- package/frontend/dist/js/ru-BjzgMmkJ.js +2 -0
- package/frontend/dist/js/{standalone-BO5pzI8_.js → standalone-C04jT26m.js} +1 -1
- package/frontend/dist/js/sv-DDX_gP1z.js +2 -0
- package/frontend/dist/js/{syntax-highlight-DcR9Vhb7.js → syntax-highlight-P2zo9o06.js} +1 -1
- package/frontend/dist/js/teamEventStream-CgUkgjFC.js +8 -0
- package/frontend/dist/js/th-BXr3KovE.js +2 -0
- package/frontend/dist/js/tr-CdqxHEM8.js +2 -0
- package/frontend/dist/js/ttsBatching-BfhNT3c5.js +2 -0
- package/frontend/dist/js/{typescript-6GJ8APmn.js → typescript-FtGI7Y5P.js} +1 -1
- package/frontend/dist/js/ui-DFa2W4QC.js +2 -0
- package/frontend/dist/js/ui-vendor-CRZXLK0Q.js +177 -0
- package/frontend/dist/js/uk-CotKDP2Q.js +2 -0
- package/frontend/dist/js/useChat-BawGm7ua.js +10 -0
- package/frontend/dist/js/useQuery-BboI5V5G.js +1 -0
- package/frontend/dist/js/{useTranslation-5b5UhAc9.js → useTranslation-D7uLySn1.js} +1 -1
- package/frontend/dist/js/{utils-BGrdX9ie.js → utils-sD-qFh8D.js} +1 -1
- package/frontend/dist/js/utils-vendor-DTLgmsuc.js +9 -0
- package/frontend/dist/js/vi-F2NC1HaJ.js +2 -0
- package/frontend/dist/js/webauthnClient-CPSq528Q.js +1 -0
- package/frontend/dist/js/workApi-8mBuh_-J.js +6 -0
- package/frontend/dist/js/workApi-DJtLOslf.js +1 -0
- package/frontend/dist/js/workStatus-CcO9Fg41.js +1 -0
- package/frontend/dist/js/zh-DZ-kpEEA.js +2 -0
- package/frontend/dist/manifest.webmanifest +32 -0
- package/frontend/dist/sw.js +125 -0
- package/package.json +4 -2
- package/frontend/dist/assets/index-BPsH9FII.js +0 -80
- package/frontend/dist/assets/index-BtXF0Oxa.css +0 -1
- package/frontend/dist/js/ArtifactContainer-CIoFoMv8.js +0 -1
- package/frontend/dist/js/ArtifactSandboxFrame-REazyIrx.js +0 -151
- package/frontend/dist/js/ArtifactSlideOutPanel-A-i94ghF.js +0 -1
- package/frontend/dist/js/AutomationsPage-Dow_lbIf.js +0 -1
- package/frontend/dist/js/CalendarPage-DLjasZOd.js +0 -1
- package/frontend/dist/js/ChannelsPage-C8hvTD7J.js +0 -1
- package/frontend/dist/js/ChatPage-BAgBHUES.js +0 -16
- package/frontend/dist/js/FirstTimeSetup-BxJapNul.js +0 -1
- package/frontend/dist/js/GalleryPage-DHz9DkK7.js +0 -1
- package/frontend/dist/js/HomePage-BqNy-FWX.js +0 -1
- package/frontend/dist/js/LibreClawPage-BDzDILjY.js +0 -1
- package/frontend/dist/js/ModelSelector-ol8mK2ja.js +0 -1
- package/frontend/dist/js/NotesPage-DzHEx2O4.js +0 -3
- package/frontend/dist/js/OptimizedSyntaxHighlighter-CZNPwN0i.js +0 -1
- package/frontend/dist/js/PersonasPage-M_WKH2a9.js +0 -13
- package/frontend/dist/js/PluginUsagePage-Ctoka6JZ.js +0 -1
- package/frontend/dist/js/RichMessageContent-BqgafH4p.js +0 -3
- package/frontend/dist/js/SettingsModal-R9EDadAV.js +0 -19
- package/frontend/dist/js/SystemPage-aOkrSuY7.js +0 -1
- package/frontend/dist/js/UserManagementPage-BOuaX3ay.js +0 -1
- package/frontend/dist/js/WorkPage-DxZISerQ.js +0 -6
- package/frontend/dist/js/api-6bSMtSNh.js +0 -14
- package/frontend/dist/js/appStore-CdObhQBb.js +0 -2
- package/frontend/dist/js/ar-DT_VLBO3.js +0 -2
- package/frontend/dist/js/authStore-DbqqrfZv.js +0 -4
- package/frontend/dist/js/bn-BrvEqhGA.js +0 -2
- package/frontend/dist/js/chatStore-DbZPJNsu.js +0 -3
- package/frontend/dist/js/cs-BthnM_YI.js +0 -2
- package/frontend/dist/js/da-DCth-6_j.js +0 -2
- package/frontend/dist/js/de-BBWt0gJ_.js +0 -2
- package/frontend/dist/js/es-DuslOPqM.js +0 -2
- package/frontend/dist/js/evaluationsApi-CwZJsgeG.js +0 -1
- package/frontend/dist/js/fr-DZAeg46S.js +0 -2
- package/frontend/dist/js/hi-ijDSiY77.js +0 -2
- package/frontend/dist/js/id-BS6qS0oq.js +0 -2
- package/frontend/dist/js/is-CO2PMDte.js +0 -2
- package/frontend/dist/js/it-ChhxT4hs.js +0 -2
- package/frontend/dist/js/ja-cyeqUeJN.js +0 -2
- package/frontend/dist/js/ko-CfqHTLXj.js +0 -2
- package/frontend/dist/js/ms-DZ5pACe1.js +0 -2
- package/frontend/dist/js/nl-BsRsnOVp.js +0 -2
- package/frontend/dist/js/pl-DXSTAW4M.js +0 -2
- package/frontend/dist/js/pt-DmDx0JxB.js +0 -2
- package/frontend/dist/js/ru-d2g8EWfY.js +0 -2
- package/frontend/dist/js/sv-CIhYUTVQ.js +0 -2
- package/frontend/dist/js/th-B3wSxanB.js +0 -2
- package/frontend/dist/js/tr-R4UGgPOY.js +0 -2
- package/frontend/dist/js/ui-CV0LYmkM.js +0 -2
- package/frontend/dist/js/ui-vendor-CWjIBW-z.js +0 -177
- package/frontend/dist/js/uk-BzUF4Vkt.js +0 -2
- package/frontend/dist/js/utils-vendor-9bQ62WeD.js +0 -9
- package/frontend/dist/js/vi-CKc6Fg4h.js +0 -2
- package/frontend/dist/js/workApi-BruWTlHM.js +0 -6
- 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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
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
|