libre-webui 0.19.6 → 0.21.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 (230) hide show
  1. package/README.md +32 -22
  2. package/backend/dist/db.d.ts.map +1 -1
  3. package/backend/dist/db.js +31 -0
  4. package/backend/dist/db.js.map +1 -1
  5. package/backend/dist/index.d.ts.map +1 -1
  6. package/backend/dist/index.js +14 -10
  7. package/backend/dist/index.js.map +1 -1
  8. package/backend/dist/routes/chat.d.ts.map +1 -1
  9. package/backend/dist/routes/chat.js +97 -10
  10. package/backend/dist/routes/chat.js.map +1 -1
  11. package/backend/dist/routes/libreClaw.d.ts.map +1 -1
  12. package/backend/dist/routes/libreClaw.js +41 -0
  13. package/backend/dist/routes/libreClaw.js.map +1 -1
  14. package/backend/dist/routes/ollama.d.ts.map +1 -1
  15. package/backend/dist/routes/ollama.js +52 -2
  16. package/backend/dist/routes/ollama.js.map +1 -1
  17. package/backend/dist/routes/search.d.ts +3 -0
  18. package/backend/dist/routes/search.d.ts.map +1 -0
  19. package/backend/dist/routes/search.js +143 -0
  20. package/backend/dist/routes/search.js.map +1 -0
  21. package/backend/dist/routes/work.d.ts.map +1 -1
  22. package/backend/dist/routes/work.js +138 -4
  23. package/backend/dist/routes/work.js.map +1 -1
  24. package/backend/dist/services/agentAccessService.d.ts +6 -0
  25. package/backend/dist/services/agentAccessService.d.ts.map +1 -0
  26. package/backend/dist/services/agentAccessService.js +58 -0
  27. package/backend/dist/services/agentAccessService.js.map +1 -0
  28. package/backend/dist/services/agentCliService.d.ts.map +1 -1
  29. package/backend/dist/services/agentCliService.js +2 -1
  30. package/backend/dist/services/agentCliService.js.map +1 -1
  31. package/backend/dist/services/authService.d.ts +1 -0
  32. package/backend/dist/services/authService.d.ts.map +1 -1
  33. package/backend/dist/services/authService.js +2 -0
  34. package/backend/dist/services/authService.js.map +1 -1
  35. package/backend/dist/services/documentService.d.ts +2 -0
  36. package/backend/dist/services/documentService.d.ts.map +1 -1
  37. package/backend/dist/services/documentService.js +51 -23
  38. package/backend/dist/services/documentService.js.map +1 -1
  39. package/backend/dist/services/modelAccessService.d.ts +12 -0
  40. package/backend/dist/services/modelAccessService.d.ts.map +1 -0
  41. package/backend/dist/services/modelAccessService.js +66 -0
  42. package/backend/dist/services/modelAccessService.js.map +1 -0
  43. package/backend/dist/services/systemDiagnosticsService.d.ts +4 -3
  44. package/backend/dist/services/systemDiagnosticsService.d.ts.map +1 -1
  45. package/backend/dist/services/systemDiagnosticsService.js +35 -34
  46. package/backend/dist/services/systemDiagnosticsService.js.map +1 -1
  47. package/backend/dist/services/webSearchService.d.ts +64 -0
  48. package/backend/dist/services/webSearchService.d.ts.map +1 -0
  49. package/backend/dist/services/webSearchService.js +230 -0
  50. package/backend/dist/services/webSearchService.js.map +1 -0
  51. package/backend/dist/services/workAccessService.d.ts +15 -0
  52. package/backend/dist/services/workAccessService.d.ts.map +1 -0
  53. package/backend/dist/services/workAccessService.js +67 -0
  54. package/backend/dist/services/workAccessService.js.map +1 -0
  55. package/backend/dist/services/workAdminService.d.ts +67 -0
  56. package/backend/dist/services/workAdminService.d.ts.map +1 -0
  57. package/backend/dist/services/workAdminService.js +92 -0
  58. package/backend/dist/services/workAdminService.js.map +1 -0
  59. package/backend/dist/services/workAgentGuidance.d.ts +1 -0
  60. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
  61. package/backend/dist/services/workAgentGuidance.js +3 -0
  62. package/backend/dist/services/workAgentGuidance.js.map +1 -1
  63. package/backend/dist/services/workAgentService.d.ts +6 -0
  64. package/backend/dist/services/workAgentService.d.ts.map +1 -1
  65. package/backend/dist/services/workAgentService.js +199 -11
  66. package/backend/dist/services/workAgentService.js.map +1 -1
  67. package/backend/dist/services/workKubernetesDriver.d.ts +74 -0
  68. package/backend/dist/services/workKubernetesDriver.d.ts.map +1 -0
  69. package/backend/dist/services/workKubernetesDriver.js +659 -0
  70. package/backend/dist/services/workKubernetesDriver.js.map +1 -0
  71. package/backend/dist/services/workPolicyService.d.ts +58 -0
  72. package/backend/dist/services/workPolicyService.d.ts.map +1 -0
  73. package/backend/dist/services/workPolicyService.js +299 -0
  74. package/backend/dist/services/workPolicyService.js.map +1 -0
  75. package/backend/dist/services/workPreviewProxyService.d.ts +7 -1
  76. package/backend/dist/services/workPreviewProxyService.d.ts.map +1 -1
  77. package/backend/dist/services/workPreviewProxyService.js +36 -8
  78. package/backend/dist/services/workPreviewProxyService.js.map +1 -1
  79. package/backend/dist/services/workRuntimeDriver.d.ts +166 -0
  80. package/backend/dist/services/workRuntimeDriver.d.ts.map +1 -0
  81. package/backend/dist/services/workRuntimeDriver.js +737 -0
  82. package/backend/dist/services/workRuntimeDriver.js.map +1 -0
  83. package/backend/dist/services/workRuntimeService.d.ts +60 -51
  84. package/backend/dist/services/workRuntimeService.d.ts.map +1 -1
  85. package/backend/dist/services/workRuntimeService.js +310 -658
  86. package/backend/dist/services/workRuntimeService.js.map +1 -1
  87. package/backend/dist/services/workRuntimeShared.d.ts +83 -0
  88. package/backend/dist/services/workRuntimeShared.d.ts.map +1 -0
  89. package/backend/dist/services/workRuntimeShared.js +136 -0
  90. package/backend/dist/services/workRuntimeShared.js.map +1 -0
  91. package/backend/dist/services/workTaskService.d.ts +6 -1
  92. package/backend/dist/services/workTaskService.d.ts.map +1 -1
  93. package/backend/dist/services/workTaskService.js +18 -3
  94. package/backend/dist/services/workTaskService.js.map +1 -1
  95. package/backend/dist/services/workTerminalService.d.ts +2 -22
  96. package/backend/dist/services/workTerminalService.d.ts.map +1 -1
  97. package/backend/dist/services/workTerminalService.js +10 -127
  98. package/backend/dist/services/workTerminalService.js.map +1 -1
  99. package/backend/dist/storage.d.ts.map +1 -1
  100. package/backend/dist/storage.js +3 -3
  101. package/backend/dist/storage.js.map +1 -1
  102. package/backend/dist/storageMappers.d.ts +1 -0
  103. package/backend/dist/storageMappers.d.ts.map +1 -1
  104. package/backend/dist/storageMappers.js +1 -0
  105. package/backend/dist/storageMappers.js.map +1 -1
  106. package/backend/dist/types/index.d.ts +1 -0
  107. package/backend/dist/types/index.d.ts.map +1 -1
  108. package/backend/dist/types/index.js.map +1 -1
  109. package/backend/dist/types/work.d.ts +4 -0
  110. package/backend/dist/types/work.d.ts.map +1 -1
  111. package/backend/dist/utils/chatDocumentContext.d.ts +7 -0
  112. package/backend/dist/utils/chatDocumentContext.d.ts.map +1 -1
  113. package/backend/dist/utils/chatDocumentContext.js +38 -30
  114. package/backend/dist/utils/chatDocumentContext.js.map +1 -1
  115. package/backend/dist/utils/dockerEndpoint.d.ts +37 -0
  116. package/backend/dist/utils/dockerEndpoint.d.ts.map +1 -0
  117. package/backend/dist/utils/dockerEndpoint.js +72 -0
  118. package/backend/dist/utils/dockerEndpoint.js.map +1 -0
  119. package/backend/dist/websocketServer.d.ts.map +1 -1
  120. package/backend/dist/websocketServer.js +79 -15
  121. package/backend/dist/websocketServer.js.map +1 -1
  122. package/backend/dist/workTerminalServer.d.ts.map +1 -1
  123. package/backend/dist/workTerminalServer.js +6 -5
  124. package/backend/dist/workTerminalServer.js.map +1 -1
  125. package/backend/package.json +2 -1
  126. package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
  127. package/frontend/dist/assets/index-0wfi-EpC.js +40 -0
  128. package/frontend/dist/assets/index-D7m1zWWg.css +1 -0
  129. package/frontend/dist/index.html +12 -11
  130. package/frontend/dist/js/ArtifactContainer-DPRPcuES.js +1 -0
  131. package/frontend/dist/js/{ArtifactDemoPage-Big00RwH.js → ArtifactDemoPage-DzwPlz9J.js} +1 -1
  132. package/frontend/dist/js/{ArtifactSandboxFrame-C_S4j076.js → ArtifactSandboxFrame-CKi4mSTH.js} +2 -2
  133. package/frontend/dist/js/{ArtifactSlideOutPanel-B4-D3HkB.js → ArtifactSlideOutPanel-BskRCzP0.js} +1 -1
  134. package/frontend/dist/js/ChatPage-DRutCyJ5.js +16 -0
  135. package/frontend/dist/js/FirstTimeSetup-DU50YWrE.js +1 -0
  136. package/frontend/dist/js/GalleryPage-DxXIUesf.js +1 -0
  137. package/frontend/dist/js/HomePage-Bew9kMKx.js +1 -0
  138. package/frontend/dist/js/{LibreClawPage-B14Go5Mn.js → LibreClawPage-Cy9y3YlF.js} +1 -1
  139. package/frontend/dist/js/{ModelManagerModals-D9j9RECi.js → ModelManagerModals-D-NxeVvV.js} +1 -1
  140. package/frontend/dist/js/ModelSelector-DXDcDmDR.js +1 -0
  141. package/frontend/dist/js/ModelsPage-Bj2reiF9.js +2 -0
  142. package/frontend/dist/js/{NotesPage-BwEMW-Z3.js → NotesPage-CJKEbj3J.js} +1 -1
  143. package/frontend/dist/js/{PersonasPage-e-CbTxP3.js → PersonasPage-bgWF9S7b.js} +2 -2
  144. package/frontend/dist/js/{PluginUsagePage-CvZMJJwv.js → PluginUsagePage-CRkD1Tz6.js} +1 -1
  145. package/frontend/dist/js/{RichMessageContent-DNl3_CIw.js → RichMessageContent-DeNjCiyN.js} +1 -1
  146. package/frontend/dist/js/SettingsModal-BUUlkZcv.js +1 -0
  147. package/frontend/dist/js/SettingsToggle-bZ5W4CCf.js +1 -0
  148. package/frontend/dist/js/SystemPage-DcO51urv.js +1 -0
  149. package/frontend/dist/js/UserManagementPage-DLiYw6xG.js +1 -0
  150. package/frontend/dist/js/WorkPage-MMQ0Y0U5.js +5 -0
  151. package/frontend/dist/js/api-CZURQtuc.js +14 -0
  152. package/frontend/dist/js/{appStore-BpoXhleX.js → appStore-sGaWMSDr.js} +2 -2
  153. package/frontend/dist/js/ar-Bm27TCMQ.js +2 -0
  154. package/frontend/dist/js/authStore-uaj0hswV.js +4 -0
  155. package/frontend/dist/js/bn-C7CHC42H.js +2 -0
  156. package/frontend/dist/js/{chatStore-CWqfsOjc.js → chatStore-tRx0Ulq0.js} +2 -2
  157. package/frontend/dist/js/{config-CuXlcWP1.js → config-D-jxRG52.js} +1 -1
  158. package/frontend/dist/js/cs-zGEtwQn_.js +2 -0
  159. package/frontend/dist/js/da-B-0sHNTu.js +2 -0
  160. package/frontend/dist/js/de-UZNE6t2h.js +2 -0
  161. package/frontend/dist/js/es-CPLnFYIw.js +2 -0
  162. package/frontend/dist/js/{estree-BuWOCMW0.js → estree-CRuerC6a.js} +1 -1
  163. package/frontend/dist/js/fr-Br1o0WVz.js +2 -0
  164. package/frontend/dist/js/hi-BcEvNEpb.js +2 -0
  165. package/frontend/dist/js/{html-D7tW7UvW.js → html-BYKlz3Jp.js} +1 -1
  166. package/frontend/dist/js/i18n-DJTg6xWw.js +2 -0
  167. package/frontend/dist/js/id-C30L2DoT.js +2 -0
  168. package/frontend/dist/js/is-BAEhfi5Z.js +2 -0
  169. package/frontend/dist/js/it-HWnfQXZY.js +2 -0
  170. package/frontend/dist/js/ja-Dvux1mon.js +2 -0
  171. package/frontend/dist/js/ko-agDQ__KP.js +2 -0
  172. package/frontend/dist/js/{markdown-CBSxWEtK.js → markdown-DFy8bILU.js} +1 -1
  173. package/frontend/dist/js/{messageCodeStyles-BlTi46wI.js → messageCodeStyles-_gGirbe-.js} +2 -2
  174. package/frontend/dist/js/ms-aNRa2QX7.js +2 -0
  175. package/frontend/dist/js/nl-BtkiDUn-.js +2 -0
  176. package/frontend/dist/js/pl-SEO0lBnX.js +2 -0
  177. package/frontend/dist/js/pt-BYtjmvq1.js +2 -0
  178. package/frontend/dist/js/ru-B-ynj2hU.js +2 -0
  179. package/frontend/dist/js/{standalone-CaXys7q3.js → standalone-jOqWoOmp.js} +1 -1
  180. package/frontend/dist/js/sv-CliKR15U.js +2 -0
  181. package/frontend/dist/js/th-DJQyDr9J.js +2 -0
  182. package/frontend/dist/js/tr-BBGKZ1la.js +2 -0
  183. package/frontend/dist/js/{typescript-DQ5zi5zo.js → typescript-TwGXpgeF.js} +1 -1
  184. package/frontend/dist/js/{ui-DFQiHo_k.js → ui-DSFUl2vT.js} +2 -2
  185. package/frontend/dist/js/{ui-vendor-D_pYT79f.js → ui-vendor-xUzKqAXI.js} +1 -1
  186. package/frontend/dist/js/uk-C1GcFWxE.js +2 -0
  187. package/frontend/dist/js/vi-CIxgICOQ.js +2 -0
  188. package/frontend/dist/js/workApi-kClHFVkJ.js +6 -0
  189. package/frontend/dist/js/zh-Cm7vsQ9s.js +2 -0
  190. package/package.json +4 -3
  191. package/frontend/dist/assets/index-Cd0_AoYV.js +0 -23
  192. package/frontend/dist/assets/index-DtkGuqvC.css +0 -1
  193. package/frontend/dist/js/ArtifactContainer-Dy_1KKWg.js +0 -1
  194. package/frontend/dist/js/ChatPage-BkFd1iac.js +0 -16
  195. package/frontend/dist/js/FirstTimeSetup-C6YxrwR7.js +0 -1
  196. package/frontend/dist/js/GalleryPage-BtcPs7SO.js +0 -1
  197. package/frontend/dist/js/HomePage-5PhpAt14.js +0 -1
  198. package/frontend/dist/js/ModelSelector-C1wN91tl.js +0 -1
  199. package/frontend/dist/js/ModelsPage-Cc3L0ur8.js +0 -2
  200. package/frontend/dist/js/SettingsModal-C8KJJEud.js +0 -1
  201. package/frontend/dist/js/SystemPage-8kEAS71H.js +0 -1
  202. package/frontend/dist/js/UserManagementPage-BSrZ0ucu.js +0 -1
  203. package/frontend/dist/js/WorkPage-spf92PVN.js +0 -5
  204. package/frontend/dist/js/api-MySpmAK8.js +0 -19
  205. package/frontend/dist/js/ar-ClMsdoTH.js +0 -2
  206. package/frontend/dist/js/authStore-DmrydYuS.js +0 -3
  207. package/frontend/dist/js/bn-LyPol_AY.js +0 -2
  208. package/frontend/dist/js/cs-D98fZaKF.js +0 -2
  209. package/frontend/dist/js/da-M7ND0GQR.js +0 -2
  210. package/frontend/dist/js/de-BV0q1kzb.js +0 -2
  211. package/frontend/dist/js/es-C7FvsVoN.js +0 -2
  212. package/frontend/dist/js/fr-5SloN0ok.js +0 -2
  213. package/frontend/dist/js/hi-C6VeRgeR.js +0 -2
  214. package/frontend/dist/js/i18n-CLd1FZPx.js +0 -2
  215. package/frontend/dist/js/id-CkVp7Z1Z.js +0 -2
  216. package/frontend/dist/js/is-DqxnBX4W.js +0 -2
  217. package/frontend/dist/js/it-CiW4_O5V.js +0 -2
  218. package/frontend/dist/js/ja-BZ3Sdund.js +0 -2
  219. package/frontend/dist/js/ko-CS7LAhfJ.js +0 -2
  220. package/frontend/dist/js/ms-URvcYqgz.js +0 -2
  221. package/frontend/dist/js/nl-CQJpW7wK.js +0 -2
  222. package/frontend/dist/js/pl-DDMgPMAz.js +0 -2
  223. package/frontend/dist/js/pt-JCqqmGn_.js +0 -2
  224. package/frontend/dist/js/ru-C_dwMbiK.js +0 -2
  225. package/frontend/dist/js/sv-CW3c9NGx.js +0 -2
  226. package/frontend/dist/js/th-9ARgClBr.js +0 -2
  227. package/frontend/dist/js/tr-DNY5_DAg.js +0 -2
  228. package/frontend/dist/js/uk-XDh4_7IL.js +0 -2
  229. package/frontend/dist/js/vi-BaoJBXnt.js +0 -2
  230. package/frontend/dist/js/zh-B202vYJt.js +0 -2
@@ -0,0 +1,737 @@
1
+ /*
2
+ * Libre WebUI
3
+ * Copyright (C) 2025 Kroonen AI, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { spawn } from 'node:child_process';
18
+ import http from 'node:http';
19
+ import { dockerEndpointRequestOptions, resolveDockerEndpoint, } from '../utils/dockerEndpoint.js';
20
+ import { createLogger } from '../utils/logger.js';
21
+ import workPolicyService from './workPolicyService.js';
22
+ import { WorkRuntimeError, WORK_RUNTIME_DEFAULTS, computePolicyFingerprint, defaultRuntimePolicy, workRuntimeConfig as config, } from './workRuntimeShared.js';
23
+ const logger = createLogger('services:work-runtime');
24
+ const activeDockerProcesses = new Set();
25
+ export class DockerWorkRuntimeDriver {
26
+ constructor() {
27
+ this.image = config.image;
28
+ /**
29
+ * The terminal talks to the Docker Engine API directly because a TTY exec
30
+ * needs a hijacked bidirectional stream, which the docker CLI only offers
31
+ * to an interactive controlling terminal. The connection is the resolved
32
+ * Docker endpoint: the Unix socket, or a plain-HTTP tcp:// endpoint such
33
+ * as a socket proxy holding the socket on the app's behalf — the hijacked
34
+ * stream survives an HTTP-aware proxy because it rides a standard
35
+ * Connection: Upgrade tunnel. Endpoints this client cannot speak to
36
+ * (ssh://, TLS-verified tcp) make the terminal report itself unavailable
37
+ * instead of guessing.
38
+ */
39
+ this.terminalEndpoint = resolveDockerEndpoint(process.env.WORK_DOCKER_SOCKET, process.env.DOCKER_HOST, process.env.DOCKER_TLS_VERIFY);
40
+ }
41
+ async probe() {
42
+ try {
43
+ await this.docker(['info', '--format', '{{.ServerVersion}}'], {
44
+ timeoutMs: 5000,
45
+ });
46
+ }
47
+ catch (error) {
48
+ throw new WorkRuntimeError(describeDockerUnavailable(error, config.dockerCommand), 503, 'WORK_DOCKER_UNAVAILABLE');
49
+ }
50
+ }
51
+ shutdown() {
52
+ for (const child of activeDockerProcesses) {
53
+ child.kill('SIGKILL');
54
+ }
55
+ }
56
+ async ensureImage(image = config.image) {
57
+ const inspected = await this.docker(['image', 'inspect', image], {
58
+ timeoutMs: 10000,
59
+ acceptFailure: true,
60
+ });
61
+ if (inspected.exitCode === 0)
62
+ return;
63
+ logger.info(`Pulling Work runtime image ${image}`);
64
+ await this.docker(['pull', image], { timeoutMs: 900000 });
65
+ }
66
+ async ensureWorkspace(task) {
67
+ if (await this.volumeExists(task.volumeName)) {
68
+ await this.assertManagedVolume(task);
69
+ return;
70
+ }
71
+ await this.docker([
72
+ 'volume',
73
+ 'create',
74
+ '--label',
75
+ 'ai.libre-webui.managed=true',
76
+ '--label',
77
+ `ai.libre-webui.task=${task.id}`,
78
+ task.volumeName,
79
+ ]);
80
+ // Docker returns an existing volume from `volume create` if another
81
+ // process wins the name race, so prove ownership before mounting it.
82
+ await this.assertManagedVolume(task);
83
+ try {
84
+ await this.initializeVolume(task, workPolicyService.resolve(task.policyId).image);
85
+ }
86
+ catch (error) {
87
+ await this.docker(['volume', 'rm', task.volumeName], {
88
+ acceptFailure: true,
89
+ });
90
+ throw error;
91
+ }
92
+ }
93
+ async ensureRuntime(task) {
94
+ const policy = workPolicyService.resolve(task.policyId);
95
+ await this.ensureWorkNetwork(task);
96
+ if (await this.containerExists(task.containerName)) {
97
+ await this.assertManagedContainer(task);
98
+ if (!(await this.containerMatchesTaskPolicy(task, policy))) {
99
+ logger.warn(`Recreating Work container ${task.containerName} because its isolation policy is stale.`);
100
+ await this.docker(['rm', '-f', task.containerName]);
101
+ await this.docker(buildWorkContainerRunArgs(task, policy));
102
+ return;
103
+ }
104
+ const state = await this.docker([
105
+ 'inspect',
106
+ '--format',
107
+ '{{.State.Running}}',
108
+ task.containerName,
109
+ ]);
110
+ if (state.stdout.trim() !== 'true') {
111
+ await this.docker(['start', task.containerName]);
112
+ }
113
+ return;
114
+ }
115
+ await this.docker(buildWorkContainerRunArgs(task, policy));
116
+ }
117
+ async runtimeState(task) {
118
+ if (!(await this.containerExists(task.containerName)))
119
+ return 'absent';
120
+ await this.assertManagedContainer(task);
121
+ return (await this.containerIsRunning(task.containerName))
122
+ ? 'running'
123
+ : 'stopped';
124
+ }
125
+ async stopRuntime(task) {
126
+ if (!(await this.containerExists(task.containerName)))
127
+ return;
128
+ await this.assertManagedContainer(task);
129
+ await this.docker(['stop', '--time', '1', task.containerName], {
130
+ timeoutMs: 10000,
131
+ });
132
+ }
133
+ async removeRuntime(task) {
134
+ if (!(await this.containerExists(task.containerName)))
135
+ return;
136
+ await this.assertManagedContainer(task);
137
+ await this.docker(['rm', '-f', task.containerName]);
138
+ }
139
+ async removeTaskResources(task) {
140
+ const [hasContainer, hasVolume] = await Promise.all([
141
+ this.containerExists(task.containerName),
142
+ this.volumeExists(task.volumeName),
143
+ ]);
144
+ // Validate every destructive target before removing either one, so a
145
+ // conflicting unmanaged Docker resource cannot cause partial cleanup.
146
+ if (hasContainer) {
147
+ await this.assertManagedContainer(task);
148
+ }
149
+ if (hasVolume) {
150
+ await this.assertManagedVolume(task);
151
+ }
152
+ if (hasContainer) {
153
+ await this.docker(['rm', '-f', task.containerName], {
154
+ timeoutMs: 15000,
155
+ });
156
+ }
157
+ if (hasVolume) {
158
+ await this.docker(['volume', 'rm', task.volumeName], {
159
+ timeoutMs: 15000,
160
+ });
161
+ }
162
+ }
163
+ async exec(task, command, options = {}) {
164
+ const args = [
165
+ 'exec',
166
+ ...(options.input !== undefined ? ['--interactive'] : []),
167
+ '--user',
168
+ '1000:1000',
169
+ '--workdir',
170
+ options.workdir ?? '/workspace',
171
+ task.containerName,
172
+ ...command,
173
+ ];
174
+ return this.docker(args, options);
175
+ }
176
+ async previewEndpoint(task) {
177
+ const portResult = await this.docker([
178
+ 'port',
179
+ task.containerName,
180
+ `${config.previewPort}/tcp`,
181
+ ]);
182
+ const port = parsePublishedPort(portResult.stdout, config.previewPort);
183
+ // No host: the proxy targets its configured upstream (publish) interface.
184
+ return port === undefined ? undefined : { port };
185
+ }
186
+ /**
187
+ * Every container the Work runtime has ever created, in one daemon query,
188
+ * identified by the managed label stamped at `docker run` time.
189
+ */
190
+ async listManaged() {
191
+ const result = await this.docker([
192
+ 'ps',
193
+ '--all',
194
+ '--no-trunc',
195
+ '--filter',
196
+ 'label=ai.libre-webui.managed=true',
197
+ '--format',
198
+ '{{.Names}}\t{{.State}}\t{{.Label "ai.libre-webui.task"}}',
199
+ ], { timeoutMs: 15000 });
200
+ return parseManagedContainerList(result.stdout);
201
+ }
202
+ async removeOrphan(name) {
203
+ await this.docker(['rm', '--force', name], { timeoutMs: 15000 });
204
+ }
205
+ terminalUnavailableReason() {
206
+ if (!this.terminalEndpoint) {
207
+ return 'The Work terminal needs a reachable Docker Engine API. Set WORK_DOCKER_SOCKET to the Unix socket path, or point DOCKER_HOST at a plain-HTTP tcp:// endpoint such as a socket proxy (DOCKER_HOST currently names an endpoint this client cannot speak to).';
208
+ }
209
+ return null;
210
+ }
211
+ async openTerminal(containerName) {
212
+ const created = await this.terminalApi('POST', `/containers/${encodeURIComponent(containerName)}/exec`, buildExecCreatePayload());
213
+ const execId = parseExecId(created);
214
+ const stream = await this.startExecStream(execId);
215
+ return {
216
+ stream,
217
+ resize: async (cols, rows) => {
218
+ await this.terminalApi('POST', `/exec/${encodeURIComponent(execId)}/resize?w=${cols}&h=${rows}`);
219
+ },
220
+ };
221
+ }
222
+ /**
223
+ * Transport options for the resolved endpoint. A Unix socket needs an
224
+ * explicit Host header (there is no authority in the request target); a
225
+ * tcp endpoint gets the real host:port so an HTTP-aware proxy can route
226
+ * on it.
227
+ */
228
+ terminalTransport() {
229
+ const endpoint = this.terminalEndpoint;
230
+ if (!endpoint) {
231
+ throw new WorkRuntimeError(this.terminalUnavailableReason() ?? 'The Work terminal is unavailable.', 503, 'WORK_TERMINAL_UNAVAILABLE');
232
+ }
233
+ return {
234
+ options: dockerEndpointRequestOptions(endpoint),
235
+ hostHeader: endpoint.kind === 'unix' ? { Host: 'docker' } : {},
236
+ };
237
+ }
238
+ terminalApi(method, path, payload) {
239
+ return new Promise((resolve, reject) => {
240
+ const body = payload === undefined ? undefined : JSON.stringify(payload);
241
+ const { options, hostHeader } = this.terminalTransport();
242
+ const request = http.request({
243
+ ...options,
244
+ method,
245
+ path,
246
+ headers: {
247
+ ...hostHeader,
248
+ 'Content-Type': 'application/json',
249
+ 'Content-Length': body ? Buffer.byteLength(body) : 0,
250
+ },
251
+ }, response => {
252
+ const chunks = [];
253
+ response.on('data', chunk => chunks.push(chunk));
254
+ response.on('end', () => {
255
+ resolve({
256
+ status: response.statusCode ?? 0,
257
+ body: Buffer.concat(chunks).toString('utf8'),
258
+ });
259
+ });
260
+ });
261
+ request.setTimeout(10000, () => {
262
+ request.destroy(new Error('Docker Engine API request timed out.'));
263
+ });
264
+ request.on('error', error => {
265
+ reject(new WorkRuntimeError(`Could not reach the Docker Engine socket for the terminal: ${error.message}`, 503, 'WORK_TERMINAL_UNAVAILABLE'));
266
+ });
267
+ if (body)
268
+ request.write(body);
269
+ request.end();
270
+ });
271
+ }
272
+ startExecStream(execId) {
273
+ return new Promise((resolve, reject) => {
274
+ const body = JSON.stringify({ Detach: false, Tty: true });
275
+ const { options, hostHeader } = this.terminalTransport();
276
+ const request = http.request({
277
+ ...options,
278
+ method: 'POST',
279
+ path: `/exec/${encodeURIComponent(execId)}/start`,
280
+ headers: {
281
+ ...hostHeader,
282
+ 'Content-Type': 'application/json',
283
+ 'Content-Length': Buffer.byteLength(body),
284
+ Connection: 'Upgrade',
285
+ Upgrade: 'tcp',
286
+ },
287
+ });
288
+ request.on('upgrade', (_response, socket, head) => {
289
+ // With Tty:true the hijacked stream is raw terminal bytes in both
290
+ // directions; no stream-multiplexing frames to parse.
291
+ if (head.length > 0)
292
+ socket.unshift(head);
293
+ resolve(socket);
294
+ });
295
+ request.on('response', response => {
296
+ const chunks = [];
297
+ response.on('data', chunk => chunks.push(chunk));
298
+ response.on('end', () => {
299
+ reject(new WorkRuntimeError(`The Docker Engine refused the terminal stream (HTTP ${response.statusCode}): ${Buffer.concat(chunks).toString('utf8').trim()}`, 503, 'WORK_TERMINAL_UNAVAILABLE'));
300
+ });
301
+ });
302
+ request.on('error', error => {
303
+ reject(new WorkRuntimeError(`Could not open the terminal stream: ${error.message}`, 503, 'WORK_TERMINAL_UNAVAILABLE'));
304
+ });
305
+ request.write(body);
306
+ request.end();
307
+ });
308
+ }
309
+ async ensureWorkNetwork(task) {
310
+ if (!task.networkEnabled)
311
+ return;
312
+ const inspect = await this.docker([
313
+ 'network',
314
+ 'inspect',
315
+ '--format',
316
+ '{{index .Labels "ai.libre-webui.managed"}} {{index .Options "com.docker.network.bridge.enable_icc"}}',
317
+ config.networkName,
318
+ ], { acceptFailure: true });
319
+ if (inspect.exitCode === 0) {
320
+ const [managed, icc] = inspect.stdout.trim().split(' ');
321
+ if (managed !== 'true' || icc !== 'false') {
322
+ throw new WorkRuntimeError(`Docker network "${config.networkName}" exists but is not the managed Work sandbox network (label ai.libre-webui.managed=true with inter-container communication disabled). Remove it or point WORK_NETWORK_NAME at an unused name.`, 503, 'WORK_NETWORK_CONFLICT');
323
+ }
324
+ return;
325
+ }
326
+ const created = await this.docker([
327
+ 'network',
328
+ 'create',
329
+ '--label',
330
+ 'ai.libre-webui.managed=true',
331
+ '--opt',
332
+ 'com.docker.network.bridge.enable_icc=false',
333
+ config.networkName,
334
+ ], { acceptFailure: true });
335
+ if (created.exitCode !== 0 && !/already exists/i.test(created.stderr)) {
336
+ throw new WorkRuntimeError(`Could not create the Work sandbox network "${config.networkName}": ${created.stderr.trim()}`, 503, 'WORK_NETWORK_UNAVAILABLE');
337
+ }
338
+ }
339
+ async initializeVolume(task, image = this.image) {
340
+ await this.docker([
341
+ 'run',
342
+ '--rm',
343
+ '--network',
344
+ 'none',
345
+ '--read-only',
346
+ '--tmpfs',
347
+ '/tmp:rw,noexec,nosuid,size=64m',
348
+ '--cap-drop',
349
+ 'ALL',
350
+ '--cap-add',
351
+ 'CHOWN',
352
+ '--mount',
353
+ `type=volume,src=${task.volumeName},dst=/workspace`,
354
+ image,
355
+ 'chown',
356
+ '-R',
357
+ '1000:1000',
358
+ '/workspace',
359
+ ]);
360
+ }
361
+ async volumeExists(name) {
362
+ const result = await this.docker(['volume', 'inspect', name], {
363
+ timeoutMs: 5000,
364
+ acceptFailure: true,
365
+ });
366
+ if (result.exitCode === 0)
367
+ return true;
368
+ if (/no such volume/i.test(`${result.stderr}\n${result.stdout}`)) {
369
+ return false;
370
+ }
371
+ throw new WorkRuntimeError(`Could not inspect Work volume "${name}": ${result.stderr.trim() || result.stdout.trim() || `Docker exited with code ${result.exitCode}.`}`, 503, 'WORK_DOCKER_INSPECT_FAILED');
372
+ }
373
+ async containerExists(name) {
374
+ const result = await this.docker(['container', 'inspect', name], {
375
+ timeoutMs: 5000,
376
+ acceptFailure: true,
377
+ });
378
+ if (result.exitCode === 0)
379
+ return true;
380
+ if (/no such (?:container|object)/i.test(`${result.stderr}\n${result.stdout}`)) {
381
+ return false;
382
+ }
383
+ throw new WorkRuntimeError(`Could not inspect Work container "${name}": ${result.stderr.trim() || result.stdout.trim() || `Docker exited with code ${result.exitCode}.`}`, 503, 'WORK_DOCKER_INSPECT_FAILED');
384
+ }
385
+ async containerIsRunning(name) {
386
+ const result = await this.docker([
387
+ 'inspect',
388
+ '--format',
389
+ '{{.State.Running}}',
390
+ name,
391
+ ]);
392
+ const state = result.stdout.trim();
393
+ if (state === 'true')
394
+ return true;
395
+ if (state === 'false')
396
+ return false;
397
+ throw new WorkRuntimeError(`Docker returned an invalid state for Work container "${name}".`, 503, 'WORK_DOCKER_INSPECT_FAILED');
398
+ }
399
+ async assertManagedContainer(task) {
400
+ const result = await this.docker([
401
+ 'inspect',
402
+ '--format',
403
+ '{{ index .Config.Labels "ai.libre-webui.task" }}',
404
+ task.containerName,
405
+ ]);
406
+ if (result.stdout.trim() !== task.id) {
407
+ throw new WorkRuntimeError(`Refusing to operate unmanaged container "${task.containerName}".`, 409, 'WORK_CONTAINER_NAME_CONFLICT');
408
+ }
409
+ }
410
+ async containerMatchesTaskPolicy(task, policy = defaultRuntimePolicy) {
411
+ const result = await this.docker([
412
+ 'inspect',
413
+ '--format',
414
+ '{{json .}}',
415
+ task.containerName,
416
+ ]);
417
+ let inspected;
418
+ try {
419
+ inspected = JSON.parse(result.stdout);
420
+ }
421
+ catch {
422
+ return false;
423
+ }
424
+ const containerConfig = objectRecord(inspected.Config);
425
+ const hostConfig = objectRecord(inspected.HostConfig);
426
+ const labels = objectRecord(containerConfig.Labels);
427
+ const env = stringArray(containerConfig.Env);
428
+ const capDrop = stringArray(hostConfig.CapDrop);
429
+ const securityOpt = stringArray(hostConfig.SecurityOpt);
430
+ const command = stringArray(containerConfig.Cmd);
431
+ const mounts = Array.isArray(inspected.Mounts)
432
+ ? inspected.Mounts.map(objectRecord)
433
+ : [];
434
+ const workspaceMount = mounts.find(mount => mount.Destination === '/workspace');
435
+ const expectedNetwork = task.networkEnabled ? config.networkName : 'none';
436
+ const portBindings = objectRecord(hostConfig.PortBindings);
437
+ const previewBindings = Array.isArray(portBindings[`${config.previewPort}/tcp`])
438
+ ? portBindings[`${config.previewPort}/tcp`].map(objectRecord)
439
+ : [];
440
+ const portPolicyMatches = task.networkEnabled
441
+ ? Object.keys(portBindings).length === 1 &&
442
+ previewBindings.length === 1 &&
443
+ previewBindings[0]?.HostIp === config.previewBind
444
+ : Object.keys(portBindings).length === 0;
445
+ return (labels['ai.libre-webui.managed'] === 'true' &&
446
+ labels['ai.libre-webui.task'] === task.id &&
447
+ labels['ai.libre-webui.policy'] === computePolicyFingerprint(policy) &&
448
+ containerConfig.Image === policy.image &&
449
+ containerConfig.User === '1000:1000' &&
450
+ containerConfig.WorkingDir === '/workspace' &&
451
+ command.join('\0') === ['tail', '-f', '/dev/null'].join('\0') &&
452
+ env.includes('HOME=/tmp') &&
453
+ env.includes('NPM_CONFIG_CACHE=/tmp/npm-cache') &&
454
+ hostConfig.ReadonlyRootfs === true &&
455
+ hostConfig.Privileged === false &&
456
+ capDrop.includes('ALL') &&
457
+ securityOpt.includes('no-new-privileges') &&
458
+ Number(hostConfig.PidsLimit) === policy.pidsLimit &&
459
+ Number(hostConfig.Memory) > 0 &&
460
+ Number(hostConfig.MemorySwap) === Number(hostConfig.Memory) &&
461
+ Number(hostConfig.NanoCpus) > 0 &&
462
+ hostConfig.NetworkMode === expectedNetwork &&
463
+ portPolicyMatches &&
464
+ workspaceMount?.Type === 'volume' &&
465
+ workspaceMount.Name === task.volumeName &&
466
+ workspaceMount.RW === true);
467
+ }
468
+ async assertManagedVolume(task) {
469
+ const result = await this.docker([
470
+ 'volume',
471
+ 'inspect',
472
+ '--format',
473
+ '{{ index .Labels "ai.libre-webui.task" }}',
474
+ task.volumeName,
475
+ ]);
476
+ if (result.stdout.trim() !== task.id) {
477
+ throw new WorkRuntimeError(`Refusing to operate unmanaged volume "${task.volumeName}".`, 409, 'WORK_VOLUME_NAME_CONFLICT');
478
+ }
479
+ }
480
+ async docker(args, options = {}) {
481
+ try {
482
+ return await runProcess(config.dockerCommand, args, options);
483
+ }
484
+ catch (error) {
485
+ if (error instanceof WorkRuntimeError)
486
+ throw error;
487
+ throw new WorkRuntimeError(error instanceof Error ? error.message : 'Docker command failed.');
488
+ }
489
+ }
490
+ }
491
+ export function buildExecCreatePayload(shell = ['/bin/bash', '-l']) {
492
+ // Mirrors the container policy: the shell runs as the same unprivileged
493
+ // user, in the workspace, inside the already-hardened container. Nothing
494
+ // about the sandbox weakens because a human is typing instead of the model.
495
+ return {
496
+ AttachStdin: true,
497
+ AttachStdout: true,
498
+ AttachStderr: true,
499
+ Tty: true,
500
+ User: '1000:1000',
501
+ WorkingDir: '/workspace',
502
+ Env: ['TERM=xterm-256color'],
503
+ Cmd: [...shell],
504
+ };
505
+ }
506
+ function parseExecId(response) {
507
+ if (response.status < 200 || response.status >= 300) {
508
+ throw new WorkRuntimeError(`The Docker Engine rejected the terminal exec (HTTP ${response.status}): ${response.body.trim()}`, 503, 'WORK_TERMINAL_UNAVAILABLE');
509
+ }
510
+ try {
511
+ const parsed = JSON.parse(response.body);
512
+ if (typeof parsed.Id === 'string' && parsed.Id)
513
+ return parsed.Id;
514
+ }
515
+ catch {
516
+ // Fall through to the error below.
517
+ }
518
+ throw new WorkRuntimeError('The Docker Engine returned an unexpected exec-create response.', 503, 'WORK_TERMINAL_UNAVAILABLE');
519
+ }
520
+ /** Parse `docker ps` lines of `name\tstate\ttask-label` into containers. */
521
+ export function parseManagedContainerList(stdout) {
522
+ const containers = [];
523
+ for (const line of stdout.split('\n')) {
524
+ if (!line.trim())
525
+ continue;
526
+ const [name = '', state = '', taskId = ''] = line
527
+ .split('\t')
528
+ .map(field => field.trim());
529
+ if (!name || !state)
530
+ continue;
531
+ containers.push({
532
+ name,
533
+ taskId,
534
+ // paused and restarting containers still hold execution state; only
535
+ // created/exited/dead containers are safely at rest.
536
+ running: state === 'running' || state === 'restarting' || state === 'paused',
537
+ });
538
+ }
539
+ return containers;
540
+ }
541
+ /** Bracket a bare IPv6 literal so it can carry a port in a URL. */
542
+ export function formatPreviewHost(host) {
543
+ return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
544
+ }
545
+ /**
546
+ * Turn a failed `docker info` into the change an operator has to make. These
547
+ * are the three ways a containerized Libre WebUI fails to reach the daemon:
548
+ * the image has no CLI, the bind-mounted socket is owned by a group the
549
+ * backend user is not in, or no daemon is listening.
550
+ */
551
+ export function describeDockerUnavailable(error, dockerCommand = config.dockerCommand) {
552
+ const message = error instanceof Error ? error.message : String(error);
553
+ if (/ENOENT/.test(message)) {
554
+ return `The "${dockerCommand}" CLI is not installed in the Libre WebUI runtime. Run an image that ships the Docker CLI and mount the host Docker socket, or point WORK_DOCKER_COMMAND at the CLI path.`;
555
+ }
556
+ if (/EACCES/.test(message) || /permission denied/i.test(message)) {
557
+ return 'The Docker socket is mounted but the Libre WebUI user cannot open it. Add the backend user to the group that owns /var/run/docker.sock (Compose: group_add with the socket GID).';
558
+ }
559
+ if (/cannot connect to the docker daemon/i.test(message) ||
560
+ /is the docker daemon running/i.test(message) ||
561
+ /no such file or directory/i.test(message)) {
562
+ return 'No Docker daemon is reachable. Start Docker, mount the host socket into the Libre WebUI container with -v /var/run/docker.sock:/var/run/docker.sock, or point DOCKER_HOST at a reachable Docker API endpoint such as a socket proxy.';
563
+ }
564
+ return message;
565
+ }
566
+ export function buildWorkContainerRunArgs(task, policy = defaultRuntimePolicy) {
567
+ const args = [
568
+ 'run',
569
+ '--detach',
570
+ '--name',
571
+ task.containerName,
572
+ '--init',
573
+ '--label',
574
+ 'ai.libre-webui.managed=true',
575
+ '--label',
576
+ `ai.libre-webui.task=${task.id}`,
577
+ '--label',
578
+ `ai.libre-webui.policy=${computePolicyFingerprint(policy)}`,
579
+ '--user',
580
+ '1000:1000',
581
+ '--workdir',
582
+ '/workspace',
583
+ '--read-only',
584
+ '--tmpfs',
585
+ '/tmp:rw,nosuid,size=512m',
586
+ '--env',
587
+ 'HOME=/tmp',
588
+ '--env',
589
+ 'NPM_CONFIG_CACHE=/tmp/npm-cache',
590
+ '--cap-drop',
591
+ 'ALL',
592
+ '--security-opt',
593
+ 'no-new-privileges',
594
+ '--pids-limit',
595
+ String(policy.pidsLimit),
596
+ '--memory',
597
+ policy.memoryLimit,
598
+ // Same value as --memory: the memory cap cannot be sidestepped via swap.
599
+ '--memory-swap',
600
+ policy.memoryLimit,
601
+ '--cpus',
602
+ policy.cpuLimit,
603
+ '--network',
604
+ task.networkEnabled ? config.networkName : 'none',
605
+ ];
606
+ if (task.networkEnabled) {
607
+ args.push('--publish', `${config.previewBind}::${config.previewPort}`);
608
+ for (const server of config.dnsServers) {
609
+ args.push('--dns', server);
610
+ }
611
+ }
612
+ args.push('--mount', task.hostPath
613
+ ? // Host-folder workspaces are opt-in per deployment and validated
614
+ // against an allowlist before ever reaching this point.
615
+ `type=bind,src=${task.hostPath},dst=/workspace`
616
+ : `type=volume,src=${task.volumeName},dst=/workspace,volume-nocopy`, policy.image, 'tail', '-f', '/dev/null');
617
+ return args;
618
+ }
619
+ export function parsePublishedPort(output, _containerPort = config.previewPort, bindAddress = config.previewBind) {
620
+ // Only a binding on the interface this deployment asked for counts. The
621
+ // default stays loopback, so a stray wildcard binding is still rejected.
622
+ const allowed = new Set(bindAddress === WORK_RUNTIME_DEFAULTS.previewBind
623
+ ? ['127.0.0.1', '[::1]']
624
+ : [bindAddress, formatPreviewHost(bindAddress)]);
625
+ for (const line of output.trim().split(/\r?\n/)) {
626
+ const match = line.match(/^(.*):(\d+)$/);
627
+ if (!match || !allowed.has(match[1]))
628
+ continue;
629
+ const port = Number(match[2]);
630
+ if (Number.isInteger(port) && port > 0 && port <= 65535)
631
+ return port;
632
+ }
633
+ return undefined;
634
+ }
635
+ function objectRecord(value) {
636
+ return value && typeof value === 'object'
637
+ ? value
638
+ : {};
639
+ }
640
+ function stringArray(value) {
641
+ return Array.isArray(value)
642
+ ? value.filter((item) => typeof item === 'string')
643
+ : [];
644
+ }
645
+ async function runProcess(command, args, options) {
646
+ const timeoutMs = options.timeoutMs ?? config.commandTimeoutMs;
647
+ const maxOutputChars = options.maxOutputChars ?? config.maxOutputChars;
648
+ return new Promise((resolve, reject) => {
649
+ const child = spawn(command, args, {
650
+ stdio: ['pipe', 'pipe', 'pipe'],
651
+ env: process.env,
652
+ });
653
+ activeDockerProcesses.add(child);
654
+ let stdout = '';
655
+ let stderr = '';
656
+ let truncated = false;
657
+ let settled = false;
658
+ let stdinError;
659
+ let timer;
660
+ const claimSettlement = () => {
661
+ if (settled)
662
+ return false;
663
+ settled = true;
664
+ if (timer)
665
+ clearTimeout(timer);
666
+ return true;
667
+ };
668
+ const append = (current, chunk) => {
669
+ const next = current + chunk.toString('utf8');
670
+ if (next.length <= maxOutputChars)
671
+ return next;
672
+ truncated = true;
673
+ const half = Math.floor(maxOutputChars / 2);
674
+ return `${next.slice(0, half)}\n... output truncated ...\n${next.slice(-half)}`;
675
+ };
676
+ child.stdout.on('data', chunk => {
677
+ stdout = append(stdout, chunk);
678
+ });
679
+ child.stderr.on('data', chunk => {
680
+ stderr = append(stderr, chunk);
681
+ });
682
+ child.stdin.on('error', error => {
683
+ // A helper may reject the request and close stdin before a large input
684
+ // has finished writing. EPIPE is then expected; the process exit carries
685
+ // the useful error. Other stdin failures are reported when it closes.
686
+ if (error.code !== 'EPIPE') {
687
+ stdinError = error;
688
+ }
689
+ });
690
+ child.on('error', error => {
691
+ activeDockerProcesses.delete(child);
692
+ if (!claimSettlement())
693
+ return;
694
+ reject(new WorkRuntimeError(`Could not start ${command}: ${error.message}`, 503, 'WORK_DOCKER_UNAVAILABLE'));
695
+ });
696
+ child.on('close', (code, signal) => {
697
+ activeDockerProcesses.delete(child);
698
+ const result = {
699
+ exitCode: code ?? -1,
700
+ stdout,
701
+ stderr,
702
+ truncated,
703
+ signal: signal || undefined,
704
+ };
705
+ if (stdinError && !options.acceptFailure) {
706
+ if (!claimSettlement())
707
+ return;
708
+ reject(new WorkRuntimeError(`Could not write command input: ${stdinError.message}`, 503, 'WORK_DOCKER_STDIN_FAILED'));
709
+ return;
710
+ }
711
+ if (result.exitCode !== 0 && !options.acceptFailure) {
712
+ if (!claimSettlement())
713
+ return;
714
+ reject(new WorkRuntimeError(stderr.trim() ||
715
+ stdout.trim() ||
716
+ `${command} exited with code ${result.exitCode}.`, 503, 'WORK_DOCKER_COMMAND_FAILED'));
717
+ return;
718
+ }
719
+ if (claimSettlement())
720
+ resolve(result);
721
+ });
722
+ timer = setTimeout(() => {
723
+ child.kill('SIGKILL');
724
+ if (!claimSettlement())
725
+ return;
726
+ reject(new WorkRuntimeError(`Command timed out after ${timeoutMs}ms.`, 504, 'WORK_COMMAND_TIMEOUT'));
727
+ }, timeoutMs);
728
+ timer.unref();
729
+ if (options.input !== undefined) {
730
+ child.stdin.end(options.input);
731
+ }
732
+ else {
733
+ child.stdin.end();
734
+ }
735
+ });
736
+ }
737
+ //# sourceMappingURL=workRuntimeDriver.js.map