@openclaw/crabline 0.1.11 → 0.1.13

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 (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,10 +1,512 @@
1
- import { spawn } from "node:child_process";
1
+ import { execFile, spawn, } from "node:child_process";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
2
4
  import path from "node:path";
3
5
  import { z } from "zod";
4
6
  import { CrablineError, ensureErrorMessage } from "../../core/errors.js";
5
7
  const MAX_SCRIPT_OUTPUT_BYTES = 1024 * 1024;
6
8
  const SCRIPT_WAIT_EXIT_GRACE_MS = 250;
7
- const WINDOWS_TERMINATION_TIMEOUT_MS = 5_000;
9
+ const CHILD_CLOSE_TIMEOUT_MS = 1_000;
10
+ const WINDOWS_TERMINATION_COMMAND_TIMEOUT_MS = 2_500;
11
+ const WINDOWS_JOB_COMMAND_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_COMMAND";
12
+ const WINDOWS_JOB_SHELL_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_SHELL";
13
+ const WINDOWS_JOB_HELPER_SOURCE_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_HELPER_SOURCE";
14
+ const WINDOWS_JOB_HELPER_OUTPUT_ENV = "CRABLINE_INTERNAL_SCRIPT_JOB_HELPER_OUTPUT";
15
+ const WINDOWS_JOB_HELPER_SOURCE = [
16
+ "using System;",
17
+ "using System.ComponentModel;",
18
+ "using System.Runtime.InteropServices;",
19
+ "using System.Text;",
20
+ "public static class CrablineScriptJob{",
21
+ "private const uint CreateSuspended=0x00000004;",
22
+ "private const uint ExtendedStartupInfoPresent=0x00080000;",
23
+ "private const uint Infinite=0xffffffff;",
24
+ "private const uint JobObjectLimitKillOnJobClose=0x00002000;",
25
+ "private const int ProcThreadAttributeJobList=0x0002000d;",
26
+ "private const uint StartfUseStdHandles=0x00000100;",
27
+ "private const uint DuplicateSameAccess=0x00000002;",
28
+ "private const uint ResumeThreadFailed=0xffffffff;",
29
+ "private const int JobObjectExtendedLimitInformationClass=9;",
30
+ "private const int StdInputHandle=-10;",
31
+ "private const int StdOutputHandle=-11;",
32
+ "private const int StdErrorHandle=-12;",
33
+ "[StructLayout(LayoutKind.Sequential)]",
34
+ "private struct ProcessInformation{",
35
+ "public IntPtr Process;",
36
+ "public IntPtr Thread;",
37
+ "public uint ProcessId;",
38
+ "public uint ThreadId;",
39
+ "}",
40
+ "[StructLayout(LayoutKind.Sequential)]",
41
+ "private struct StartupInfo{",
42
+ "public int Size;",
43
+ "public IntPtr Reserved;",
44
+ "public IntPtr Desktop;",
45
+ "public IntPtr Title;",
46
+ "public int X;",
47
+ "public int Y;",
48
+ "public int XSize;",
49
+ "public int YSize;",
50
+ "public int XCountChars;",
51
+ "public int YCountChars;",
52
+ "public int FillAttribute;",
53
+ "public uint Flags;",
54
+ "public short ShowWindow;",
55
+ "public short Reserved2Size;",
56
+ "public IntPtr Reserved2;",
57
+ "public IntPtr StandardInput;",
58
+ "public IntPtr StandardOutput;",
59
+ "public IntPtr StandardError;",
60
+ "}",
61
+ "[StructLayout(LayoutKind.Sequential)]",
62
+ "private struct StartupInfoEx{",
63
+ "public StartupInfo StartupInfo;",
64
+ "public IntPtr AttributeList;",
65
+ "}",
66
+ "[StructLayout(LayoutKind.Sequential)]",
67
+ "private struct JobObjectBasicLimitInformation{",
68
+ "public long PerProcessUserTimeLimit;",
69
+ "public long PerJobUserTimeLimit;",
70
+ "public uint LimitFlags;",
71
+ "public UIntPtr MinimumWorkingSetSize;",
72
+ "public UIntPtr MaximumWorkingSetSize;",
73
+ "public uint ActiveProcessLimit;",
74
+ "public IntPtr Affinity;",
75
+ "public uint PriorityClass;",
76
+ "public uint SchedulingClass;",
77
+ "}",
78
+ "[StructLayout(LayoutKind.Sequential)]",
79
+ "private struct IoCounters{",
80
+ "public ulong ReadOperationCount;",
81
+ "public ulong WriteOperationCount;",
82
+ "public ulong OtherOperationCount;",
83
+ "public ulong ReadTransferCount;",
84
+ "public ulong WriteTransferCount;",
85
+ "public ulong OtherTransferCount;",
86
+ "}",
87
+ "[StructLayout(LayoutKind.Sequential)]",
88
+ "private struct JobObjectExtendedLimitInformation{",
89
+ "public JobObjectBasicLimitInformation BasicLimitInformation;",
90
+ "public IoCounters IoInfo;",
91
+ "public UIntPtr ProcessMemoryLimit;",
92
+ "public UIntPtr JobMemoryLimit;",
93
+ "public UIntPtr PeakProcessMemoryUsed;",
94
+ "public UIntPtr PeakJobMemoryUsed;",
95
+ "}",
96
+ '[DllImport("kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)]',
97
+ "[return:MarshalAs(UnmanagedType.Bool)]",
98
+ "private static extern bool CreateProcess(string applicationName,StringBuilder commandLine,IntPtr processAttributes,IntPtr threadAttributes,bool inheritHandles,uint creationFlags,IntPtr environment,string currentDirectory,ref StartupInfoEx startupInfo,out ProcessInformation processInformation);",
99
+ '[DllImport("kernel32.dll",SetLastError=true)]',
100
+ "private static extern IntPtr CreateJobObject(IntPtr jobAttributes,string name);",
101
+ '[DllImport("kernel32.dll",SetLastError=true)]',
102
+ "[return:MarshalAs(UnmanagedType.Bool)]",
103
+ "private static extern bool SetInformationJobObject(IntPtr job,int informationClass,ref JobObjectExtendedLimitInformation information,uint informationLength);",
104
+ '[DllImport("kernel32.dll",SetLastError=true)]',
105
+ "[return:MarshalAs(UnmanagedType.Bool)]",
106
+ "private static extern bool InitializeProcThreadAttributeList(IntPtr attributeList,int attributeCount,int flags,ref IntPtr size);",
107
+ '[DllImport("kernel32.dll",SetLastError=true)]',
108
+ "[return:MarshalAs(UnmanagedType.Bool)]",
109
+ "private static extern bool UpdateProcThreadAttribute(IntPtr attributeList,uint flags,IntPtr attribute,IntPtr value,IntPtr size,IntPtr previousValue,IntPtr returnSize);",
110
+ '[DllImport("kernel32.dll")]',
111
+ "private static extern void DeleteProcThreadAttributeList(IntPtr attributeList);",
112
+ '[DllImport("kernel32.dll",SetLastError=true)]',
113
+ "[return:MarshalAs(UnmanagedType.Bool)]",
114
+ "private static extern bool DuplicateHandle(IntPtr sourceProcess,IntPtr sourceHandle,IntPtr targetProcess,out IntPtr targetHandle,uint desiredAccess,bool inheritHandle,uint options);",
115
+ '[DllImport("kernel32.dll")]',
116
+ "private static extern IntPtr GetCurrentProcess();",
117
+ '[DllImport("kernel32.dll",SetLastError=true)]',
118
+ "private static extern IntPtr GetStdHandle(int standardHandle);",
119
+ '[DllImport("kernel32.dll",SetLastError=true)]',
120
+ "private static extern uint ResumeThread(IntPtr thread);",
121
+ '[DllImport("kernel32.dll",SetLastError=true)]',
122
+ "private static extern uint WaitForSingleObject(IntPtr handle,uint milliseconds);",
123
+ '[DllImport("kernel32.dll",SetLastError=true)]',
124
+ "[return:MarshalAs(UnmanagedType.Bool)]",
125
+ "private static extern bool GetExitCodeProcess(IntPtr process,out uint exitCode);",
126
+ '[DllImport("kernel32.dll",SetLastError=true)]',
127
+ "[return:MarshalAs(UnmanagedType.Bool)]",
128
+ "private static extern bool TerminateProcess(IntPtr process,uint exitCode);",
129
+ '[DllImport("kernel32.dll")]',
130
+ "[return:MarshalAs(UnmanagedType.Bool)]",
131
+ "private static extern bool CloseHandle(IntPtr handle);",
132
+ "private static void ThrowLastError(string operation){throw new Win32Exception(Marshal.GetLastWin32Error(),operation);}",
133
+ "private static IntPtr DuplicateStandardHandle(int standardHandle){",
134
+ "IntPtr source=GetStdHandle(standardHandle);",
135
+ "IntPtr duplicate=IntPtr.Zero;",
136
+ "IntPtr process=GetCurrentProcess();",
137
+ 'if(source==IntPtr.Zero||source==new IntPtr(-1)||!DuplicateHandle(process,source,process,out duplicate,0,true,DuplicateSameAccess)){ThrowLastError("Could not duplicate a script standard handle.");}',
138
+ "return duplicate;",
139
+ "}",
140
+ "private static int Run(string applicationName,string commandLine){",
141
+ "IntPtr job=IntPtr.Zero;",
142
+ "IntPtr attributeList=IntPtr.Zero;",
143
+ "IntPtr jobList=IntPtr.Zero;",
144
+ "IntPtr standardInput=IntPtr.Zero;",
145
+ "IntPtr standardOutput=IntPtr.Zero;",
146
+ "IntPtr standardError=IntPtr.Zero;",
147
+ "ProcessInformation processInformation=new ProcessInformation();",
148
+ "bool processCreated=false;",
149
+ "try{",
150
+ "job=CreateJobObject(IntPtr.Zero,null);",
151
+ 'if(job==IntPtr.Zero){ThrowLastError("Could not create the script job object.");}',
152
+ "JobObjectExtendedLimitInformation limits=new JobObjectExtendedLimitInformation();",
153
+ "limits.BasicLimitInformation.LimitFlags=JobObjectLimitKillOnJobClose;",
154
+ 'if(!SetInformationJobObject(job,JobObjectExtendedLimitInformationClass,ref limits,(uint)Marshal.SizeOf(typeof(JobObjectExtendedLimitInformation)))){ThrowLastError("Could not configure the script job object.");}',
155
+ "IntPtr attributeListSize=IntPtr.Zero;",
156
+ "InitializeProcThreadAttributeList(IntPtr.Zero,1,0,ref attributeListSize);",
157
+ "attributeList=Marshal.AllocHGlobal(attributeListSize);",
158
+ 'if(!InitializeProcThreadAttributeList(attributeList,1,0,ref attributeListSize)){ThrowLastError("Could not initialize the script job attribute list.");}',
159
+ "jobList=Marshal.AllocHGlobal(IntPtr.Size);",
160
+ "Marshal.WriteIntPtr(jobList,job);",
161
+ 'if(!UpdateProcThreadAttribute(attributeList,0,new IntPtr(ProcThreadAttributeJobList),jobList,new IntPtr(IntPtr.Size),IntPtr.Zero,IntPtr.Zero)){ThrowLastError("Could not bind the script process to its job object.");}',
162
+ "standardInput=DuplicateStandardHandle(StdInputHandle);",
163
+ "standardOutput=DuplicateStandardHandle(StdOutputHandle);",
164
+ "standardError=DuplicateStandardHandle(StdErrorHandle);",
165
+ "StartupInfoEx startupInfo=new StartupInfoEx();",
166
+ "startupInfo.StartupInfo.Size=Marshal.SizeOf(typeof(StartupInfoEx));",
167
+ "startupInfo.StartupInfo.Flags=StartfUseStdHandles;",
168
+ "startupInfo.StartupInfo.StandardInput=standardInput;",
169
+ "startupInfo.StartupInfo.StandardOutput=standardOutput;",
170
+ "startupInfo.StartupInfo.StandardError=standardError;",
171
+ "startupInfo.AttributeList=attributeList;",
172
+ 'if(!CreateProcess(applicationName,new StringBuilder(commandLine),IntPtr.Zero,IntPtr.Zero,true,CreateSuspended|ExtendedStartupInfoPresent,IntPtr.Zero,Environment.CurrentDirectory,ref startupInfo,out processInformation)){ThrowLastError("Could not start the script command.");}',
173
+ "processCreated=true;",
174
+ 'if(ResumeThread(processInformation.Thread)==ResumeThreadFailed){ThrowLastError("Could not resume the script command.");}',
175
+ 'if(WaitForSingleObject(processInformation.Process,Infinite)==Infinite){ThrowLastError("Could not wait for the script command.");}',
176
+ "uint exitCode;",
177
+ 'if(!GetExitCodeProcess(processInformation.Process,out exitCode)){ThrowLastError("Could not read the script exit code.");}',
178
+ "return unchecked((int)exitCode);",
179
+ "}finally{",
180
+ "if(processCreated&&processInformation.Process!=IntPtr.Zero){TerminateProcess(processInformation.Process,1);}",
181
+ "if(processInformation.Thread!=IntPtr.Zero){CloseHandle(processInformation.Thread);}",
182
+ "if(processInformation.Process!=IntPtr.Zero){CloseHandle(processInformation.Process);}",
183
+ "if(standardInput!=IntPtr.Zero){CloseHandle(standardInput);}",
184
+ "if(standardOutput!=IntPtr.Zero){CloseHandle(standardOutput);}",
185
+ "if(standardError!=IntPtr.Zero){CloseHandle(standardError);}",
186
+ "if(attributeList!=IntPtr.Zero){DeleteProcThreadAttributeList(attributeList);Marshal.FreeHGlobal(attributeList);}",
187
+ "if(jobList!=IntPtr.Zero){Marshal.FreeHGlobal(jobList);}",
188
+ "if(job!=IntPtr.Zero){CloseHandle(job);}",
189
+ "}",
190
+ "}",
191
+ "public static int Main(){",
192
+ "try{",
193
+ `string shellValue=Environment.GetEnvironmentVariable("${WINDOWS_JOB_SHELL_ENV}");`,
194
+ `string commandValue=Environment.GetEnvironmentVariable("${WINDOWS_JOB_COMMAND_ENV}");`,
195
+ `Environment.SetEnvironmentVariable("${WINDOWS_JOB_SHELL_ENV}",null);`,
196
+ `Environment.SetEnvironmentVariable("${WINDOWS_JOB_COMMAND_ENV}",null);`,
197
+ 'if(String.IsNullOrEmpty(shellValue)||String.IsNullOrEmpty(commandValue)){throw new InvalidOperationException("Missing script job configuration.");}',
198
+ "string shell=Encoding.UTF8.GetString(Convert.FromBase64String(shellValue));",
199
+ "string commandLine=Encoding.UTF8.GetString(Convert.FromBase64String(commandValue));",
200
+ "return Run(shell,commandLine);",
201
+ "}catch(Exception error){Console.Error.WriteLine(error.Message);return 1;}",
202
+ "}",
203
+ "}",
204
+ ].join("");
205
+ const WINDOWS_PROCESS_TERMINATOR_SOURCE = [
206
+ "using System;",
207
+ "using System.Runtime.InteropServices;",
208
+ "public static class CrablineProcessTerminator{",
209
+ "private const uint ProcessTerminate=0x0001;",
210
+ "private const uint ProcessQueryLimitedInformation=0x1000;",
211
+ "private const uint Synchronize=0x00100000;",
212
+ "private const uint WaitObject0=0;",
213
+ "private const uint WaitTimeout=258;",
214
+ "private const int ErrorInvalidParameter=87;",
215
+ '[DllImport("kernel32.dll",SetLastError=true)]',
216
+ "private static extern IntPtr OpenProcess(uint access,bool inheritHandle,int processId);",
217
+ '[DllImport("kernel32.dll",SetLastError=true)]',
218
+ "[return:MarshalAs(UnmanagedType.Bool)]",
219
+ "private static extern bool GetProcessTimes(IntPtr process,out long creationTime,out long exitTime,out long kernelTime,out long userTime);",
220
+ '[DllImport("kernel32.dll",SetLastError=true)]',
221
+ "[return:MarshalAs(UnmanagedType.Bool)]",
222
+ "private static extern bool TerminateProcess(IntPtr process,uint exitCode);",
223
+ '[DllImport("kernel32.dll",SetLastError=true)]',
224
+ "private static extern uint WaitForSingleObject(IntPtr handle,uint milliseconds);",
225
+ '[DllImport("kernel32.dll")]',
226
+ "[return:MarshalAs(UnmanagedType.Bool)]",
227
+ "private static extern bool CloseHandle(IntPtr handle);",
228
+ "public static bool TerminateVerified(int processId,long expectedCreationTime){",
229
+ "IntPtr handle=OpenProcess(ProcessTerminate|ProcessQueryLimitedInformation|Synchronize,false,processId);",
230
+ "if(handle==IntPtr.Zero){return Marshal.GetLastWin32Error()==ErrorInvalidParameter;}",
231
+ "try{",
232
+ "long creationTime,exitTime,kernelTime,userTime;",
233
+ "if(!GetProcessTimes(handle,out creationTime,out exitTime,out kernelTime,out userTime)){return false;}",
234
+ "if(creationTime/10!=expectedCreationTime/10){return true;}",
235
+ "uint status=WaitForSingleObject(handle,0);",
236
+ "if(status==WaitObject0){return true;}",
237
+ "if(status!=WaitTimeout){return false;}",
238
+ "if(!TerminateProcess(handle,1)){return WaitForSingleObject(handle,0)==WaitObject0;}",
239
+ "return WaitForSingleObject(handle,1000)==WaitObject0;",
240
+ "}finally{CloseHandle(handle);}",
241
+ "}",
242
+ "}",
243
+ ].join("");
244
+ let windowsJobHelperPath;
245
+ let windowsJobHelperBootstrap;
246
+ function removeWindowsJobHelper(directory) {
247
+ try {
248
+ rmSync(directory, { force: true, recursive: true });
249
+ }
250
+ catch {
251
+ // The helper may still be exiting during process shutdown.
252
+ }
253
+ }
254
+ function runWindowsJobHelperCommand(executable, args, options, signal) {
255
+ if (signal.aborted) {
256
+ return Promise.reject(signal.reason ?? new Error("Windows script helper bootstrap aborted."));
257
+ }
258
+ return new Promise((resolve, reject) => {
259
+ const startedAtMs = Date.now();
260
+ let child;
261
+ let settled = false;
262
+ let aborting = false;
263
+ const finish = (error) => {
264
+ if (settled || aborting) {
265
+ return;
266
+ }
267
+ settled = true;
268
+ signal.removeEventListener("abort", abort);
269
+ if (error) {
270
+ reject(error);
271
+ }
272
+ else {
273
+ resolve();
274
+ }
275
+ };
276
+ const abort = () => {
277
+ if (settled || aborting) {
278
+ return;
279
+ }
280
+ aborting = true;
281
+ signal.removeEventListener("abort", abort);
282
+ void (async () => {
283
+ try {
284
+ await terminateChild(child, startedAtMs, observedAtMs);
285
+ }
286
+ catch {
287
+ // The command still needs to be drained before returning the original abort.
288
+ }
289
+ await waitForChildClose(childClosed);
290
+ settled = true;
291
+ reject(signal.reason ?? new Error("Windows script helper bootstrap aborted."));
292
+ })();
293
+ };
294
+ try {
295
+ child = execFile(executable, args, options, (error) => finish(error ?? undefined));
296
+ }
297
+ catch (error) {
298
+ reject(error);
299
+ return;
300
+ }
301
+ const observedAtMs = Date.now();
302
+ const childClosed = new Promise((resolveClose) => {
303
+ child.once("close", (code, closeSignal) => resolveClose({ code, signal: closeSignal }));
304
+ });
305
+ signal.addEventListener("abort", abort, { once: true });
306
+ if (signal.aborted) {
307
+ abort();
308
+ }
309
+ });
310
+ }
311
+ async function createWindowsJobHelper(signal) {
312
+ if (windowsJobHelperPath !== undefined) {
313
+ return windowsJobHelperPath;
314
+ }
315
+ let directory;
316
+ try {
317
+ directory = mkdtempSync(path.join(tmpdir(), "crabline-script-job-"));
318
+ }
319
+ catch (error) {
320
+ throw new Error("Could not create the Windows script Job Object helper directory.", {
321
+ cause: error,
322
+ });
323
+ }
324
+ const helperPath = path.join(directory, "crabline-script-job.exe");
325
+ try {
326
+ await runWindowsJobHelperCommand("powershell.exe", [
327
+ "-NoLogo",
328
+ "-NoProfile",
329
+ "-NonInteractive",
330
+ "-Command",
331
+ `Add-Type -TypeDefinition $env:${WINDOWS_JOB_HELPER_SOURCE_ENV} -Language CSharp -OutputAssembly $env:${WINDOWS_JOB_HELPER_OUTPUT_ENV} -OutputType ConsoleApplication`,
332
+ ], {
333
+ env: {
334
+ ...process.env,
335
+ [WINDOWS_JOB_HELPER_OUTPUT_ENV]: helperPath,
336
+ [WINDOWS_JOB_HELPER_SOURCE_ENV]: WINDOWS_JOB_HELPER_SOURCE,
337
+ },
338
+ timeout: 15_000,
339
+ windowsHide: true,
340
+ }, signal);
341
+ const probeCommand = windowsShellCommand("exit 0");
342
+ await runWindowsJobHelperCommand(helperPath, [], {
343
+ env: {
344
+ ...process.env,
345
+ [WINDOWS_JOB_COMMAND_ENV]: Buffer.from(probeCommand.commandLine).toString("base64"),
346
+ [WINDOWS_JOB_SHELL_ENV]: Buffer.from(probeCommand.shell).toString("base64"),
347
+ },
348
+ timeout: 5_000,
349
+ windowsHide: true,
350
+ }, signal);
351
+ }
352
+ catch (error) {
353
+ removeWindowsJobHelper(directory);
354
+ throw new Error("Could not initialize the Windows script Job Object helper.", {
355
+ cause: error,
356
+ });
357
+ }
358
+ windowsJobHelperPath = helperPath;
359
+ process.once("exit", () => removeWindowsJobHelper(directory));
360
+ return helperPath;
361
+ }
362
+ function releaseWindowsJobHelperWaiter(bootstrap, reason) {
363
+ bootstrap.waiters -= 1;
364
+ if (bootstrap.waiters !== 0 ||
365
+ windowsJobHelperBootstrap !== bootstrap ||
366
+ windowsJobHelperPath !== undefined) {
367
+ return false;
368
+ }
369
+ windowsJobHelperBootstrap = undefined;
370
+ bootstrap.controller.abort(reason ?? new Error("Windows script helper bootstrap has no active waiters."));
371
+ return true;
372
+ }
373
+ function ensureWindowsJobHelper(signal) {
374
+ if (windowsJobHelperPath !== undefined) {
375
+ return Promise.resolve(windowsJobHelperPath);
376
+ }
377
+ if (signal?.aborted) {
378
+ return Promise.reject(signal.reason ?? new Error("Script command aborted."));
379
+ }
380
+ let bootstrap = windowsJobHelperBootstrap;
381
+ if (!bootstrap) {
382
+ const controller = new AbortController();
383
+ bootstrap = {
384
+ controller,
385
+ promise: Promise.resolve(""),
386
+ waiters: 0,
387
+ };
388
+ bootstrap.promise = createWindowsJobHelper(controller.signal).finally(() => {
389
+ if (windowsJobHelperBootstrap === bootstrap) {
390
+ windowsJobHelperBootstrap = undefined;
391
+ }
392
+ });
393
+ windowsJobHelperBootstrap = bootstrap;
394
+ }
395
+ bootstrap.waiters += 1;
396
+ return new Promise((resolve, reject) => {
397
+ let settled = false;
398
+ const finish = (callback) => {
399
+ if (settled) {
400
+ return;
401
+ }
402
+ settled = true;
403
+ signal?.removeEventListener("abort", abort);
404
+ callback();
405
+ };
406
+ const abort = () => {
407
+ finish(() => {
408
+ const reason = signal?.reason ?? new Error("Script command aborted.");
409
+ const drainsBootstrap = releaseWindowsJobHelperWaiter(bootstrap, reason);
410
+ if (drainsBootstrap) {
411
+ void bootstrap.promise.then(() => reject(reason), () => reject(reason));
412
+ }
413
+ else {
414
+ reject(reason);
415
+ }
416
+ });
417
+ };
418
+ signal?.addEventListener("abort", abort, { once: true });
419
+ void bootstrap.promise.then((value) => {
420
+ finish(() => {
421
+ releaseWindowsJobHelperWaiter(bootstrap);
422
+ resolve(value);
423
+ });
424
+ }, (error) => {
425
+ finish(() => {
426
+ releaseWindowsJobHelperWaiter(bootstrap);
427
+ reject(error);
428
+ });
429
+ });
430
+ if (signal?.aborted) {
431
+ abort();
432
+ }
433
+ });
434
+ }
435
+ function quoteWindowsArgument(value) {
436
+ if (!value) {
437
+ return '""';
438
+ }
439
+ if (!/[\s"]/u.test(value)) {
440
+ return value;
441
+ }
442
+ let quoted = '"';
443
+ let backslashes = 0;
444
+ for (const character of value) {
445
+ if (character === "\\") {
446
+ backslashes += 1;
447
+ continue;
448
+ }
449
+ if (character === '"') {
450
+ quoted += `${"\\".repeat(backslashes * 2 + 1)}"`;
451
+ backslashes = 0;
452
+ continue;
453
+ }
454
+ quoted += "\\".repeat(backslashes) + character;
455
+ backslashes = 0;
456
+ }
457
+ return `${quoted}${"\\".repeat(backslashes * 2)}"`;
458
+ }
459
+ function windowsShellCommand(command, shell) {
460
+ const resolvedShell = shell ?? process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe";
461
+ const shellCommand = quoteWindowsArgument(resolvedShell);
462
+ const shellName = path.win32.basename(resolvedShell).toLowerCase();
463
+ return {
464
+ commandLine: shellName === "cmd" || shellName === "cmd.exe"
465
+ ? `${shellCommand} /d /s /c "${command}"`
466
+ : `${shellCommand} -c ${quoteWindowsArgument(command)}`,
467
+ shell: resolvedShell,
468
+ };
469
+ }
470
+ async function spawnScriptChild(params, signal) {
471
+ const cwd = params.cwd ? path.resolve(params.cwd) : process.cwd();
472
+ let startedAtMs;
473
+ let child;
474
+ if (process.platform === "win32") {
475
+ const helperPath = await ensureWindowsJobHelper(signal);
476
+ if (signal?.aborted) {
477
+ throw signal.reason ?? new Error("Script command aborted.");
478
+ }
479
+ startedAtMs = Date.now();
480
+ const shellCommand = windowsShellCommand(params.command, params.shell);
481
+ child = spawn(helperPath, [], {
482
+ cwd,
483
+ env: {
484
+ ...process.env,
485
+ [WINDOWS_JOB_COMMAND_ENV]: Buffer.from(shellCommand.commandLine).toString("base64"),
486
+ [WINDOWS_JOB_SHELL_ENV]: Buffer.from(shellCommand.shell).toString("base64"),
487
+ },
488
+ shell: false,
489
+ stdio: ["pipe", "pipe", "pipe"],
490
+ windowsHide: true,
491
+ });
492
+ }
493
+ else {
494
+ startedAtMs = Date.now();
495
+ child = spawn(params.command, {
496
+ cwd,
497
+ env: process.env,
498
+ shell: params.shell ?? true,
499
+ detached: true,
500
+ stdio: ["pipe", "pipe", "pipe"],
501
+ });
502
+ }
503
+ const observedAtMs = Date.now();
504
+ if (signal?.aborted) {
505
+ await terminateChild(child, startedAtMs, observedAtMs);
506
+ throw signal.reason ?? new Error("Script command aborted.");
507
+ }
508
+ return { child, observedAtMs, startedAtMs };
509
+ }
8
510
  const ScriptMessageSchema = z.object({
9
511
  author: z.enum(["assistant", "system", "user"]),
10
512
  id: z.string().min(1),
@@ -29,56 +531,196 @@ const ScriptInboundResultSchema = z
29
531
  })
30
532
  .refine((result) => (result.timeout === true && result.message === undefined) ||
31
533
  (result.timeout !== true && result.message !== undefined), { message: "result must contain either a message or timeout: true" });
32
- async function terminateChild(child) {
33
- if (process.platform === "win32" && child.pid) {
34
- await new Promise((resolve) => {
35
- let taskkill;
534
+ function runTerminationCommand(command, args) {
535
+ return new Promise((resolve) => {
536
+ let cleanup;
537
+ try {
538
+ cleanup = spawn(command, args, {
539
+ stdio: "ignore",
540
+ windowsHide: true,
541
+ });
542
+ }
543
+ catch {
544
+ resolve(false);
545
+ return;
546
+ }
547
+ let settled = false;
548
+ const finish = (succeeded) => {
549
+ if (settled) {
550
+ return;
551
+ }
552
+ settled = true;
553
+ clearTimeout(timeout);
554
+ resolve(succeeded);
555
+ };
556
+ const timeout = setTimeout(() => {
36
557
  try {
37
- taskkill = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
38
- stdio: "ignore",
39
- windowsHide: true,
40
- });
558
+ cleanup.kill("SIGKILL");
41
559
  }
42
560
  catch {
43
- resolve();
44
- return;
561
+ // The cleanup process may have exited at the timeout boundary.
45
562
  }
46
- let settled = false;
47
- const finish = () => {
48
- if (settled) {
49
- return;
50
- }
51
- settled = true;
52
- clearTimeout(timeout);
53
- resolve();
54
- };
55
- const timeout = setTimeout(() => {
56
- taskkill.kill("SIGKILL");
57
- finish();
58
- }, WINDOWS_TERMINATION_TIMEOUT_MS);
59
- timeout.unref();
60
- taskkill.once("close", finish);
61
- taskkill.once("error", finish);
62
- });
63
- }
64
- else if (child.pid) {
563
+ finish(false);
564
+ }, WINDOWS_TERMINATION_COMMAND_TIMEOUT_MS);
565
+ timeout.unref();
566
+ cleanup.once("close", (code) => finish(code === 0));
567
+ cleanup.once("error", () => finish(false));
568
+ });
569
+ }
570
+ function windowsProcessTreeTermination(pid, childStartedAtMs, childObservedAtMs, rootExpectedAlive) {
571
+ const rootNotBeforeMs = Math.max(0, Math.floor(childStartedAtMs));
572
+ const rootObservedByMs = Math.max(rootNotBeforeMs, Math.ceil(childObservedAtMs) + 1);
573
+ return [
574
+ "$ErrorActionPreference='Stop'",
575
+ `$NativeSource='${WINDOWS_PROCESS_TERMINATOR_SOURCE}'`,
576
+ "Add-Type -TypeDefinition $NativeSource",
577
+ `$RootProcessId=${pid}`,
578
+ `$RootExpectedAlive=$${rootExpectedAlive ? "true" : "false"}`,
579
+ `$RootNotBefore=[DateTimeOffset]::FromUnixTimeMilliseconds(${rootNotBeforeMs}).UtcDateTime`,
580
+ `$RootObservedBy=[DateTimeOffset]::FromUnixTimeMilliseconds(${rootObservedByMs}).UtcDateTime`,
581
+ "$AllProcesses=@(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate)",
582
+ "$Snapshot=[System.Collections.Generic.List[object]]::new()",
583
+ "$Pending=[System.Collections.Generic.Queue[object]]::new()",
584
+ "$Visited=[System.Collections.Generic.HashSet[string]]::new()",
585
+ "$RootProcess=$AllProcesses | Where-Object { [int]$_.ProcessId -eq $RootProcessId } | Select-Object -First 1",
586
+ "$RootCreated=if($null -ne $RootProcess){([datetime]$RootProcess.CreationDate).ToUniversalTime()}else{$null}",
587
+ "$RootMatches=$null -ne $RootProcess -and $RootCreated -ge $RootNotBefore -and $RootCreated -le $RootObservedBy",
588
+ "$KillRoot=$RootExpectedAlive -and $RootMatches",
589
+ "$CleanupFailed=$RootExpectedAlive -and !$RootMatches",
590
+ "if($KillRoot){",
591
+ "$Snapshot.Add($RootProcess)",
592
+ '$Visited.Add("$([int]$RootProcess.ProcessId)|$(([datetime]$RootProcess.CreationDate).ToFileTimeUtc())") | Out-Null',
593
+ "$Pending.Enqueue($RootProcess)",
594
+ "}",
595
+ "while($Pending.Count -gt 0){",
596
+ "$Parent=$Pending.Dequeue()",
597
+ "foreach($Process in @($AllProcesses | Where-Object { [int]$_.ParentProcessId -eq [int]$Parent.ProcessId })){",
598
+ "$ChildCreated=([datetime]$Process.CreationDate).ToUniversalTime()",
599
+ "$ParentCreated=([datetime]$Parent.CreationDate).ToUniversalTime()",
600
+ "if($ChildCreated -lt $ParentCreated){continue}",
601
+ '$Identity="$([int]$Process.ProcessId)|$($ChildCreated.ToFileTimeUtc())"',
602
+ "if(!$Visited.Add($Identity)){continue}",
603
+ "$Snapshot.Add($Process)",
604
+ "$Pending.Enqueue($Process)",
605
+ "}",
606
+ "}",
607
+ "if($KillRoot){",
608
+ "$Entries=@($Snapshot)",
609
+ "[array]::Reverse($Entries)",
610
+ "foreach($Entry in $Entries){",
611
+ "$CreationTime=([datetime]$Entry.CreationDate).ToFileTimeUtc()",
612
+ "if(![CrablineProcessTerminator]::TerminateVerified([int]$Entry.ProcessId,$CreationTime)){$CleanupFailed=$true}",
613
+ "}",
614
+ "}",
615
+ "if($CleanupFailed){exit 1}",
616
+ ].join(";");
617
+ }
618
+ function destroyChildPipes(child) {
619
+ for (const stream of [child.stdin, child.stdout, child.stderr]) {
65
620
  try {
66
- process.kill(-child.pid, "SIGKILL");
621
+ stream?.destroy();
67
622
  }
68
623
  catch {
69
- // The process group may have exited with the shell.
624
+ // Pipe teardown is best effort after process-tree termination.
625
+ }
626
+ }
627
+ }
628
+ function isChildRunning(child) {
629
+ if (child.exitCode !== null || child.signalCode !== null) {
630
+ return false;
631
+ }
632
+ try {
633
+ return child.kill(0);
634
+ }
635
+ catch {
636
+ return false;
637
+ }
638
+ }
639
+ async function terminateChild(child, childStartedAtMs, childObservedAtMs) {
640
+ try {
641
+ const childRunning = isChildRunning(child);
642
+ if (process.platform === "win32") {
643
+ if (child.pid) {
644
+ const treeTerminated = await runTerminationCommand("powershell.exe", [
645
+ "-NoLogo",
646
+ "-NoProfile",
647
+ "-NonInteractive",
648
+ "-Command",
649
+ windowsProcessTreeTermination(child.pid, childStartedAtMs, childObservedAtMs, childRunning),
650
+ ]);
651
+ if (!treeTerminated && isChildRunning(child)) {
652
+ try {
653
+ child.kill("SIGKILL");
654
+ }
655
+ catch {
656
+ // Closing the helper handle is the bounded fallback for its Job Object.
657
+ }
658
+ }
659
+ }
660
+ }
661
+ else if (child.pid) {
662
+ try {
663
+ process.kill(-child.pid, "SIGKILL");
664
+ }
665
+ catch {
666
+ // The process group may have exited with the shell.
667
+ }
668
+ }
669
+ if (isChildRunning(child)) {
670
+ try {
671
+ child.kill("SIGKILL");
672
+ }
673
+ catch {
674
+ // The process may have exited while its descendants retained the pipes.
675
+ }
70
676
  }
71
677
  }
72
- if (child.exitCode === null && child.signalCode === null) {
73
- child.kill("SIGKILL");
678
+ finally {
679
+ destroyChildPipes(child);
74
680
  }
75
681
  }
682
+ async function waitForChildClose(childClosed) {
683
+ let timeout;
684
+ const exit = await Promise.race([
685
+ childClosed,
686
+ new Promise((resolve) => {
687
+ timeout = setTimeout(() => resolve(undefined), CHILD_CLOSE_TIMEOUT_MS);
688
+ timeout.unref();
689
+ }),
690
+ ]);
691
+ clearTimeout(timeout);
692
+ return exit;
693
+ }
694
+ function waitForChildCloseOrAbort(childClosed, signals) {
695
+ const activeSignals = signals.filter((signal) => signal !== undefined);
696
+ if (activeSignals.some((signal) => signal.aborted)) {
697
+ return Promise.resolve(undefined);
698
+ }
699
+ return new Promise((resolve) => {
700
+ let settled = false;
701
+ const abort = () => finish(undefined);
702
+ const finish = (exit) => {
703
+ if (settled) {
704
+ return;
705
+ }
706
+ settled = true;
707
+ for (const signal of activeSignals) {
708
+ signal.removeEventListener("abort", abort);
709
+ }
710
+ resolve(exit);
711
+ };
712
+ for (const signal of activeSignals) {
713
+ signal.addEventListener("abort", abort, { once: true });
714
+ }
715
+ void childClosed.then((exit) => finish(exit));
716
+ });
717
+ }
76
718
  function formatValidationError(error) {
77
719
  return error.issues
78
720
  .map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "result"}: ${issue.message}`)
79
721
  .join("; ");
80
722
  }
81
- const sensitiveEnvironmentNameFragmentPattern = /(?:AUTH|BEARER|CREDENTIAL|KEY|PASS|PRIVATE|SECRET|TOKEN)/iu;
723
+ const sensitiveEnvironmentNameFragmentPattern = /(?:AUTH|BEARER|CREDENTIAL|JWT|KEY|PASS|PRIVATE|SECRET|TOKEN)/iu;
82
724
  const nonSensitiveWorkingDirectoryNames = new Set(["PWD", "OLDPWD"]);
83
725
  function isSensitiveEnvironmentName(name) {
84
726
  const upperName = name.toUpperCase();
@@ -86,7 +728,20 @@ function isSensitiveEnvironmentName(name) {
86
728
  (!nonSensitiveWorkingDirectoryNames.has(upperName) && upperName.includes("PWD")) ||
87
729
  /PAT(?!H)/u.test(upperName));
88
730
  }
731
+ function redactCredentialSyntax(detail) {
732
+ let redacted = detail.replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^/\s@]+)@/giu, "$1[redacted credentials]@");
733
+ redacted = redacted.replace(/(\bauthorization\b["']?\s*[:=]\s*["']?\s*)(?:(basic|bearer)\s+)?([^\s"',;}]+)/giu, (_match, prefix, scheme) => `${prefix}${scheme ? `${scheme} ` : ""}[redacted credential]`);
734
+ redacted = redacted.replace(/\b([A-Za-z_][A-Za-z0-9_-]*)\s*(\+?=|:)\s*("[^"]*"|'[^']*'|[^,;}\]\r\n]+)/gu, (match, name, operator) => name.toLowerCase() !== "authorization" && isSensitiveEnvironmentName(name)
735
+ ? `${name}${operator}[redacted credential]`
736
+ : match);
737
+ return redacted.replace(/--([A-Za-z0-9][A-Za-z0-9_-]*)(=|\s+)("[^"]*"|'[^']*'|[^\s,;]+)/gu, (match, name, separator) => isSensitiveEnvironmentName(name.replaceAll("-", "_"))
738
+ ? `--${name}${separator}[redacted credential]`
739
+ : match);
740
+ }
89
741
  function commandContainsSensitiveValue(command) {
742
+ if (redactCredentialSyntax(command) !== command) {
743
+ return true;
744
+ }
90
745
  const assignments = [
91
746
  ...command.matchAll(/(?:^|[\s;&|])["']?([A-Za-z_][A-Za-z0-9_]*)\s*\+?=/gu),
92
747
  ...command.matchAll(/\$env:([A-Za-z_][A-Za-z0-9_]*)\s*\+?=/giu),
@@ -97,6 +752,147 @@ function commandContainsSensitiveValue(command) {
97
752
  }
98
753
  return [...command.matchAll(/(?:^|[\s;&|])["']?--([A-Za-z0-9][A-Za-z0-9_-]*)(?:=|\s)/gu)].some((match) => isSensitiveEnvironmentName((match[1] ?? "").replaceAll("-", "_")));
99
754
  }
755
+ function addRedactionRepresentations(values, value) {
756
+ if (!value) {
757
+ return;
758
+ }
759
+ values.add(value);
760
+ const serialized = JSON.stringify(value);
761
+ values.add(serialized);
762
+ values.add(serialized.slice(1, -1));
763
+ }
764
+ function addCommandValueRedactions(substringValues, value) {
765
+ addRedactionRepresentations(substringValues, value);
766
+ }
767
+ function tokenizeLiteralCommand(command) {
768
+ const tokens = [];
769
+ let current = "";
770
+ let quote;
771
+ for (let index = 0; index < command.length; index += 1) {
772
+ const character = command[index];
773
+ if (character === "\n" || character === "\r") {
774
+ return undefined;
775
+ }
776
+ if (process.platform === "win32" && character === "\\" && command[index + 1] === '"') {
777
+ return undefined;
778
+ }
779
+ if (process.platform === "win32" && character === '"' && command[index + 1] === '"') {
780
+ return undefined;
781
+ }
782
+ if (quote) {
783
+ if (character === quote) {
784
+ quote = undefined;
785
+ continue;
786
+ }
787
+ if (quote === '"' && /[$`%!]/u.test(character)) {
788
+ return undefined;
789
+ }
790
+ if (character === "\\" && quote === '"' && process.platform !== "win32") {
791
+ return undefined;
792
+ }
793
+ current += character;
794
+ continue;
795
+ }
796
+ if (character === '"' || (character === "'" && process.platform !== "win32")) {
797
+ quote = character;
798
+ continue;
799
+ }
800
+ if (/\s/u.test(character)) {
801
+ if (current) {
802
+ tokens.push(current);
803
+ current = "";
804
+ }
805
+ continue;
806
+ }
807
+ if (/[;&|<>(){}$`%!*?[\]~]/u.test(character) ||
808
+ (process.platform === "win32" && character === "^")) {
809
+ return undefined;
810
+ }
811
+ if (character === "\\") {
812
+ if (process.platform !== "win32") {
813
+ return undefined;
814
+ }
815
+ }
816
+ current += character;
817
+ }
818
+ if (quote) {
819
+ return undefined;
820
+ }
821
+ if (current) {
822
+ tokens.push(current);
823
+ }
824
+ return tokens;
825
+ }
826
+ function snapshotCommandValues(command) {
827
+ const tokens = tokenizeLiteralCommand(command);
828
+ if (!tokens) {
829
+ return undefined;
830
+ }
831
+ const exactValues = new Set();
832
+ const substringValues = new Set();
833
+ let executableSeen = false;
834
+ for (const token of tokens) {
835
+ if (!executableSeen) {
836
+ const assignment = /^([A-Za-z_][A-Za-z0-9_]*)\+?=(.*)$/u.exec(token);
837
+ if (assignment) {
838
+ addCommandValueRedactions(substringValues, assignment[2] ?? "");
839
+ continue;
840
+ }
841
+ executableSeen = true;
842
+ continue;
843
+ }
844
+ if (/^--[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(token)) {
845
+ const value = token.slice(2);
846
+ if (value.length < 3) {
847
+ return undefined;
848
+ }
849
+ addRedactionRepresentations(exactValues, token);
850
+ addCommandValueRedactions(substringValues, value);
851
+ continue;
852
+ }
853
+ if (/^-[A-Za-z0-9][A-Za-z0-9_-]+$/u.test(token)) {
854
+ const value = token.slice(2);
855
+ if (value.length < 3) {
856
+ return undefined;
857
+ }
858
+ addRedactionRepresentations(exactValues, token);
859
+ addCommandValueRedactions(substringValues, token.slice(1));
860
+ addCommandValueRedactions(substringValues, value);
861
+ continue;
862
+ }
863
+ const option = /^--?[A-Za-z0-9][A-Za-z0-9_-]*=(.*)$/u.exec(token);
864
+ if (option) {
865
+ const value = option[1] ?? "";
866
+ if (value.length < 3) {
867
+ return undefined;
868
+ }
869
+ addCommandValueRedactions(substringValues, value);
870
+ continue;
871
+ }
872
+ if (process.platform === "win32" && token.startsWith("/")) {
873
+ const slashOption = /^\/[A-Za-z0-9][A-Za-z0-9_-]*[:=](.*)$/u.exec(token);
874
+ const value = slashOption?.[1] ?? "";
875
+ if (value.length < 3) {
876
+ return undefined;
877
+ }
878
+ addCommandValueRedactions(substringValues, value);
879
+ continue;
880
+ }
881
+ if (token.length < 3) {
882
+ return undefined;
883
+ }
884
+ if (token.startsWith("-")) {
885
+ return undefined;
886
+ }
887
+ else {
888
+ addRedactionRepresentations(substringValues, token);
889
+ }
890
+ }
891
+ return {
892
+ exactValues: [...exactValues].sort((left, right) => right.length - left.length),
893
+ substringValues: [...substringValues].sort((left, right) => right.length - left.length),
894
+ };
895
+ }
100
896
  function snapshotSensitiveEnvironmentValues() {
101
897
  const representations = new Set();
102
898
  const values = Object.entries(process.env)
@@ -104,19 +900,10 @@ function snapshotSensitiveEnvironmentValues() {
104
900
  .map(([, value]) => value)
105
901
  .sort((left, right) => right.length - left.length);
106
902
  for (const value of values) {
107
- representations.add(value);
108
- representations.add(JSON.stringify(value));
109
- representations.add(JSON.stringify(value).slice(1, -1));
903
+ addRedactionRepresentations(representations, value);
110
904
  }
111
905
  return [...representations].sort((left, right) => right.length - left.length);
112
906
  }
113
- function redactSensitiveEnvironmentValues(detail, sensitiveEnvironmentValues) {
114
- let redacted = detail;
115
- for (const representation of sensitiveEnvironmentValues) {
116
- redacted = redacted.split(representation).join("[redacted environment value]");
117
- }
118
- return redacted;
119
- }
120
907
  function collectSensitivePayloadValues(value, values, seen, sensitive = false) {
121
908
  if (typeof value === "string") {
122
909
  if (sensitive && value.length > 0) {
@@ -157,10 +944,54 @@ function snapshotSensitivePayloadValues(payload) {
157
944
  });
158
945
  return [...values].sort((left, right) => right.length - left.length);
159
946
  }
160
- function redactSensitivePayloadValues(detail, sensitivePayloadValues) {
947
+ function isExactCommandValueBoundary(character) {
948
+ return character === undefined || /[\s"'`=,:;.()[\]{}<>!?]/u.test(character);
949
+ }
950
+ function collectDiagnosticRedactionSpans(detail, values, label, priority, exact = false) {
951
+ const spans = [];
952
+ for (const value of values) {
953
+ let cursor = 0;
954
+ while (cursor < detail.length) {
955
+ const start = detail.indexOf(value, cursor);
956
+ if (start < 0) {
957
+ break;
958
+ }
959
+ const end = start + value.length;
960
+ if (!exact ||
961
+ (isExactCommandValueBoundary(detail[start - 1]) && isExactCommandValueBoundary(detail[end]))) {
962
+ spans.push({ end, label, priority, sourceLength: value.length, start });
963
+ }
964
+ cursor = end;
965
+ }
966
+ }
967
+ return spans;
968
+ }
969
+ function redactDiagnosticValues(detail, diagnostics) {
970
+ const spans = [
971
+ ...collectDiagnosticRedactionSpans(detail, diagnostics.sensitiveEnvironmentValues, "[redacted environment value]", 4),
972
+ ...collectDiagnosticRedactionSpans(detail, diagnostics.configuredCommands, "[configured script command]", 3),
973
+ ...collectDiagnosticRedactionSpans(detail, diagnostics.commandValues, "[redacted command value]", 3),
974
+ ...collectDiagnosticRedactionSpans(detail, diagnostics.exactCommandValues, "[redacted command value]", 3, true),
975
+ ...collectDiagnosticRedactionSpans(detail, diagnostics.sensitivePayloadValues, "[redacted configured value]", 2),
976
+ ].sort((left, right) => left.start - right.start || right.end - left.end || right.priority - left.priority);
977
+ const merged = [];
978
+ for (const span of spans) {
979
+ const previous = merged.at(-1);
980
+ if (!previous || span.start >= previous.end) {
981
+ merged.push({ ...span });
982
+ continue;
983
+ }
984
+ previous.end = Math.max(previous.end, span.end);
985
+ if (span.sourceLength > previous.sourceLength ||
986
+ (span.sourceLength === previous.sourceLength && span.priority > previous.priority)) {
987
+ previous.label = span.label;
988
+ previous.priority = span.priority;
989
+ previous.sourceLength = span.sourceLength;
990
+ }
991
+ }
161
992
  let redacted = detail;
162
- for (const value of sensitivePayloadValues) {
163
- redacted = redacted.split(value).join("[redacted configured value]");
993
+ for (const span of merged.toReversed()) {
994
+ redacted = redacted.slice(0, span.start) + span.label + redacted.slice(span.end);
164
995
  }
165
996
  return redacted;
166
997
  }
@@ -168,17 +999,25 @@ function formatScriptError(summary, detail, command, diagnostics) {
168
999
  if (!detail.trim()) {
169
1000
  return summary;
170
1001
  }
171
- if (commandContainsSensitiveValue(command)) {
1002
+ if (!diagnostics.diagnosticsSafe || commandContainsSensitiveValue(command)) {
172
1003
  return `${summary}\n[script diagnostics redacted]`;
173
1004
  }
174
- let redacted = redactSensitivePayloadValues(redactSensitiveEnvironmentValues(detail, diagnostics.sensitiveEnvironmentValues), diagnostics.sensitivePayloadValues);
175
- for (const configuredCommand of diagnostics.configuredCommands) {
176
- redacted = redacted.split(configuredCommand).join("[configured script command]");
177
- }
1005
+ let redacted = redactDiagnosticValues(detail, diagnostics);
1006
+ redacted = redactCredentialSyntax(redacted);
178
1007
  redacted = redacted.trim();
179
1008
  return redacted ? `${summary}\n${redacted}` : summary;
180
1009
  }
181
- function createScriptDiagnosticsSnapshot(command, serializedPayload) {
1010
+ function usesSupportedDiagnosticShell(shell) {
1011
+ if (shell !== undefined) {
1012
+ return false;
1013
+ }
1014
+ if (process.platform !== "win32") {
1015
+ return true;
1016
+ }
1017
+ const comspec = process.env.ComSpec ?? process.env.COMSPEC;
1018
+ return comspec === undefined || /(?:^|[\\/])cmd(?:\.exe)?$/iu.test(comspec);
1019
+ }
1020
+ function createScriptDiagnosticsSnapshot(command, serializedPayload, shell) {
182
1021
  const payload = JSON.parse(serializedPayload);
183
1022
  const configuredCommands = new Set([command]);
184
1023
  const commands = payload.provider?.config?.script?.commands;
@@ -187,8 +1026,30 @@ function createScriptDiagnosticsSnapshot(command, serializedPayload) {
187
1026
  configuredCommands.add(configuredCommand);
188
1027
  }
189
1028
  }
1029
+ const commandValues = new Set();
1030
+ const exactCommandValues = new Set();
1031
+ let diagnosticsSafe = usesSupportedDiagnosticShell(shell);
1032
+ for (const configuredCommand of configuredCommands) {
1033
+ if (commandContainsSensitiveValue(configuredCommand)) {
1034
+ diagnosticsSafe = false;
1035
+ }
1036
+ const values = snapshotCommandValues(configuredCommand);
1037
+ if (!values) {
1038
+ diagnosticsSafe = false;
1039
+ continue;
1040
+ }
1041
+ for (const value of values.substringValues) {
1042
+ commandValues.add(value);
1043
+ }
1044
+ for (const value of values.exactValues) {
1045
+ exactCommandValues.add(value);
1046
+ }
1047
+ }
190
1048
  return {
1049
+ commandValues: [...commandValues].sort((left, right) => right.length - left.length),
191
1050
  configuredCommands: [...configuredCommands].sort((left, right) => right.length - left.length),
1051
+ diagnosticsSafe,
1052
+ exactCommandValues: [...exactCommandValues].sort((left, right) => right.length - left.length),
192
1053
  sensitiveEnvironmentValues: snapshotSensitiveEnvironmentValues(),
193
1054
  sensitivePayloadValues: snapshotSensitivePayloadValues(payload),
194
1055
  };
@@ -212,34 +1073,20 @@ function runScript(params) {
212
1073
  return Promise.reject(params.signal.reason ?? new Error("Script command aborted."));
213
1074
  }
214
1075
  const serializedPayload = JSON.stringify(params.payload);
215
- const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload);
1076
+ const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload, params.shell);
216
1077
  return new Promise((resolve, reject) => {
217
1078
  let child;
218
- try {
219
- child = spawn(params.command, {
220
- cwd: params.cwd ? path.resolve(params.cwd) : process.cwd(),
221
- env: process.env,
222
- shell: params.shell ?? true,
223
- detached: process.platform !== "win32",
224
- stdio: ["pipe", "pipe", "pipe"],
225
- });
226
- }
227
- catch (error) {
228
- reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
229
- return;
230
- }
1079
+ let childStartedAtMs = 0;
1080
+ let childObservedAtMs = 0;
231
1081
  const stdout = [];
232
1082
  const stderr = [];
233
1083
  let deadlineExceeded = false;
234
1084
  let outputBytes = 0;
235
1085
  let settled = false;
1086
+ let timeout;
236
1087
  let timeoutGrace;
237
- const abort = () => {
238
- finish(async () => {
239
- await terminateChild(child);
240
- reject(params.signal?.reason ?? new Error("Script command aborted."));
241
- });
242
- };
1088
+ const spawnAbortController = new AbortController();
1089
+ let spawnCompletion;
243
1090
  const finish = (callback) => {
244
1091
  if (settled) {
245
1092
  return;
@@ -247,80 +1094,98 @@ function runScript(params) {
247
1094
  settled = true;
248
1095
  clearTimeout(timeout);
249
1096
  clearTimeout(timeoutGrace);
1097
+ spawnAbortController.abort();
250
1098
  params.signal?.removeEventListener("abort", abort);
251
1099
  void callback();
252
1100
  };
1101
+ const stopChild = async () => {
1102
+ if (child) {
1103
+ await terminateChild(child, childStartedAtMs, childObservedAtMs);
1104
+ }
1105
+ else {
1106
+ await spawnCompletion;
1107
+ }
1108
+ };
1109
+ const abort = () => {
1110
+ finish(async () => {
1111
+ await stopChild();
1112
+ reject(params.signal?.reason ?? new Error("Script command aborted."));
1113
+ });
1114
+ };
253
1115
  const failForOutputLimit = () => {
254
1116
  finish(async () => {
255
- await terminateChild(child);
1117
+ await stopChild();
256
1118
  reject(new CrablineError(`Script command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes of output.`, {
257
1119
  kind: "connectivity",
258
1120
  }));
259
1121
  });
260
1122
  };
261
- child.stdout.on("data", (chunk) => {
262
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
263
- outputBytes += buffer.length;
264
- if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
265
- failForOutputLimit();
266
- return;
267
- }
268
- stdout.push(buffer);
269
- });
270
- child.stderr.on("data", (chunk) => {
271
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
272
- outputBytes += buffer.length;
273
- if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
274
- failForOutputLimit();
275
- return;
276
- }
277
- stderr.push(buffer);
278
- });
279
- child.stdin.on("error", () => {
280
- // Child closure is reported through the process error/close handlers.
281
- });
282
- child.once("error", (error) => {
283
- finish(() => {
284
- reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
1123
+ const startChild = (spawnedChild) => {
1124
+ spawnedChild.stdout.on("data", (chunk) => {
1125
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1126
+ outputBytes += buffer.length;
1127
+ if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
1128
+ failForOutputLimit();
1129
+ return;
1130
+ }
1131
+ stdout.push(buffer);
285
1132
  });
286
- });
287
- child.once("close", (code, signal) => {
288
- finish(() => {
289
- const stdoutText = Buffer.concat(stdout).toString("utf8");
290
- const stderrText = Buffer.concat(stderr).toString("utf8");
291
- if (code !== 0) {
292
- reject(new CrablineError(formatScriptError(`Script command failed${signal ? ` (${signal})` : ""}.`, stderrText.trim() ? stderrText : stdoutText, params.command, diagnostics), { kind: "connectivity" }));
1133
+ spawnedChild.stderr.on("data", (chunk) => {
1134
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1135
+ outputBytes += buffer.length;
1136
+ if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
1137
+ failForOutputLimit();
293
1138
  return;
294
1139
  }
295
- try {
296
- const result = parseScriptJson({
297
- command: params.command,
298
- diagnostics,
299
- output: stdoutText,
300
- schema: params.schema,
301
- });
302
- if (deadlineExceeded && !params.acceptResultDuringTimeoutGrace?.(result)) {
303
- reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
304
- kind: "timeout",
305
- }));
1140
+ stderr.push(buffer);
1141
+ });
1142
+ spawnedChild.stdin.on("error", () => {
1143
+ // Child closure is reported through the process error/close handlers.
1144
+ });
1145
+ spawnedChild.once("error", (error) => {
1146
+ finish(() => {
1147
+ reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
1148
+ });
1149
+ });
1150
+ spawnedChild.once("close", (code, signal) => {
1151
+ finish(() => {
1152
+ const stdoutText = Buffer.concat(stdout).toString("utf8");
1153
+ const stderrText = Buffer.concat(stderr).toString("utf8");
1154
+ if (code !== 0) {
1155
+ reject(new CrablineError(formatScriptError(`Script command failed${signal ? ` (${signal})` : ""}.`, stderrText.trim() ? stderrText : stdoutText, params.command, diagnostics), { kind: "connectivity" }));
306
1156
  return;
307
1157
  }
308
- resolve(result);
309
- }
310
- catch (error) {
311
- reject(error);
312
- }
1158
+ try {
1159
+ const result = parseScriptJson({
1160
+ command: params.command,
1161
+ diagnostics,
1162
+ output: stdoutText,
1163
+ schema: params.schema,
1164
+ });
1165
+ if (deadlineExceeded && !params.acceptResultDuringTimeoutGrace?.(result)) {
1166
+ reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
1167
+ kind: "timeout",
1168
+ }));
1169
+ return;
1170
+ }
1171
+ resolve(result);
1172
+ }
1173
+ catch (error) {
1174
+ reject(error);
1175
+ }
1176
+ });
313
1177
  });
314
- });
1178
+ spawnedChild.stdin.end(serializedPayload);
1179
+ };
315
1180
  const failForTimeout = () => {
316
1181
  finish(async () => {
317
- await terminateChild(child);
1182
+ await stopChild();
318
1183
  reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
319
1184
  kind: "timeout",
320
1185
  }));
321
1186
  });
322
1187
  };
323
- const timeout = setTimeout(() => {
1188
+ timeout = setTimeout(() => {
324
1189
  const timeoutGraceMs = params.timeoutGraceMs ?? 0;
325
1190
  if (timeoutGraceMs <= 0) {
326
1191
  failForTimeout();
@@ -331,12 +1196,28 @@ function runScript(params) {
331
1196
  timeoutGrace.unref();
332
1197
  }, params.timeoutMs);
333
1198
  timeout.unref();
1199
+ params.signal?.addEventListener("abort", abort, { once: true });
334
1200
  if (params.signal?.aborted) {
335
1201
  abort();
336
1202
  return;
337
1203
  }
338
- params.signal?.addEventListener("abort", abort, { once: true });
339
- child.stdin.end(serializedPayload);
1204
+ spawnCompletion = spawnScriptChild(params, spawnAbortController.signal).then(async (spawned) => {
1205
+ if (settled) {
1206
+ await terminateChild(spawned.child, spawned.startedAtMs, spawned.observedAtMs);
1207
+ return;
1208
+ }
1209
+ child = spawned.child;
1210
+ childStartedAtMs = spawned.startedAtMs;
1211
+ childObservedAtMs = spawned.observedAtMs;
1212
+ startChild(spawned.child);
1213
+ }, (error) => {
1214
+ if (settled) {
1215
+ return;
1216
+ }
1217
+ finish(() => {
1218
+ reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
1219
+ });
1220
+ });
340
1221
  });
341
1222
  }
342
1223
  function watchScript(params) {
@@ -355,31 +1236,45 @@ function watchScript(params) {
355
1236
  },
356
1237
  };
357
1238
  const serializedPayload = JSON.stringify(payload);
358
- const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload);
1239
+ const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload, params.shell);
359
1240
  let child;
1241
+ let childStartedAtMs;
1242
+ let childObservedAtMs;
1243
+ const spawnSignal = params.context.signal
1244
+ ? AbortSignal.any([params.cancelSignal, params.context.signal])
1245
+ : params.cancelSignal;
360
1246
  try {
361
- child = spawn(params.command, {
362
- cwd: params.cwd ? path.resolve(params.cwd) : process.cwd(),
363
- env: process.env,
364
- shell: params.shell ?? true,
365
- detached: process.platform !== "win32",
366
- stdio: ["pipe", "pipe", "pipe"],
367
- });
1247
+ ({
1248
+ child,
1249
+ observedAtMs: childObservedAtMs,
1250
+ startedAtMs: childStartedAtMs,
1251
+ } = await spawnScriptChild(params, spawnSignal));
368
1252
  }
369
1253
  catch (error) {
1254
+ if (params.cancelSignal.aborted) {
1255
+ return;
1256
+ }
1257
+ if (params.context.signal?.aborted) {
1258
+ throw params.context.signal.reason ?? new Error("Script watch command aborted.");
1259
+ }
370
1260
  throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" });
371
1261
  }
372
1262
  let buffer = "";
373
1263
  let stderr = "";
374
1264
  let childError;
375
1265
  let outputLimitError;
1266
+ let termination;
376
1267
  const stopChild = () => {
377
- void terminateChild(child);
1268
+ termination ??= terminateChild(child, childStartedAtMs, childObservedAtMs);
1269
+ return termination;
378
1270
  };
379
- params.cancelSignal.addEventListener("abort", stopChild, { once: true });
380
- params.context.signal?.addEventListener("abort", stopChild, { once: true });
1271
+ const requestStopChild = () => {
1272
+ void stopChild();
1273
+ };
1274
+ params.cancelSignal.addEventListener("abort", requestStopChild, { once: true });
1275
+ params.context.signal?.addEventListener("abort", requestStopChild, { once: true });
381
1276
  if (params.cancelSignal.aborted || params.context.signal?.aborted) {
382
- stopChild();
1277
+ requestStopChild();
383
1278
  }
384
1279
  child.stdout.setEncoding("utf8");
385
1280
  child.stderr.setEncoding("utf8");
@@ -393,7 +1288,7 @@ function watchScript(params) {
393
1288
  stderr += chunk;
394
1289
  if (Buffer.byteLength(stderr) > MAX_SCRIPT_OUTPUT_BYTES) {
395
1290
  outputLimitError = new CrablineError(`Script watch command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes of stderr.`, { kind: "connectivity" });
396
- void terminateChild(child);
1291
+ requestStopChild();
397
1292
  }
398
1293
  });
399
1294
  child.once("error", (error) => {
@@ -452,13 +1347,29 @@ function watchScript(params) {
452
1347
  provider: params.id,
453
1348
  };
454
1349
  }
455
- const exit = await childClosed;
456
1350
  if (outputLimitError) {
1351
+ await stopChild();
1352
+ await waitForChildClose(childClosed);
457
1353
  throw outputLimitError;
458
1354
  }
459
1355
  if (childError) {
1356
+ await waitForChildClose(childClosed);
460
1357
  throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(childError), params.command, diagnostics), { kind: "connectivity" });
461
1358
  }
1359
+ const exit = await waitForChildCloseOrAbort(childClosed, [
1360
+ params.cancelSignal,
1361
+ params.context.signal,
1362
+ ]);
1363
+ if (!exit) {
1364
+ await stopChild();
1365
+ if (params.cancelSignal.aborted) {
1366
+ return;
1367
+ }
1368
+ if (params.context.signal?.aborted) {
1369
+ throw params.context.signal.reason ?? new Error("Script watch command aborted.");
1370
+ }
1371
+ return;
1372
+ }
462
1373
  if (exit.code !== 0) {
463
1374
  throw new CrablineError(formatScriptError(`Script watch command failed${exit.signal ? ` (${exit.signal})` : ""}.`, stderr, params.command, diagnostics), { kind: "connectivity" });
464
1375
  }
@@ -476,13 +1387,13 @@ function watchScript(params) {
476
1387
  throw error;
477
1388
  }
478
1389
  finally {
479
- params.cancelSignal.removeEventListener("abort", stopChild);
480
- params.context.signal?.removeEventListener("abort", stopChild);
1390
+ params.cancelSignal.removeEventListener("abort", requestStopChild);
1391
+ params.context.signal?.removeEventListener("abort", requestStopChild);
481
1392
  child.stdin.destroy();
482
1393
  if (!childCloseObserved) {
483
- await terminateChild(child);
1394
+ await stopChild();
484
1395
  }
485
- await childClosed;
1396
+ await waitForChildClose(childClosed);
486
1397
  }
487
1398
  })();
488
1399
  }