@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451

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 (165) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +70 -22
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +192 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +29 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. package/runtime/windows-job-supervisor.ps1 +380 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.3-dev.fbe15023cdfe",
3
+ "version": "0.1.4-dev.41da3780c451",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -32,16 +32,17 @@
32
32
  "url": "https://github.com/coleman399/Forge/issues"
33
33
  },
34
34
  "engines": {
35
- "node": ">=22.13.0"
35
+ "node": "^22.13.0 || >=24.0.0"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",
42
- "test": "vitest run",
42
+ "test": "node scripts/run-owned-tests.mjs",
43
+ "test:residue": "node scripts/run-owned-residue-gate.mjs",
43
44
  "test:watch": "vitest",
44
- "smoke:package": "npm run build && node scripts/smoke-package.mjs",
45
+ "smoke:package": "tsc -p tsconfig.json && node scripts/smoke-package.mjs",
45
46
  "lint": "eslint \"src/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\" \"vitest.setup.ts\""
46
47
  },
47
48
  "dependencies": {
@@ -55,6 +56,7 @@
55
56
  "globals": "^17.6.0",
56
57
  "typescript": "^5.8.3",
57
58
  "typescript-eslint": "^8.61.1",
58
- "vitest": "^3.2.3"
59
+ "vite": "^7.3.7",
60
+ "vitest": "^4.1.12"
59
61
  }
60
62
  }
@@ -6,6 +6,10 @@ param(
6
6
  [Parameter(Mandatory = $true)]
7
7
  [string]$PayloadPath,
8
8
 
9
+ [Parameter(Mandatory = $true)]
10
+ [ValidatePattern("^[0-9a-f]{64}$")]
11
+ [string]$PayloadSha256,
12
+
9
13
  [string]$GateName
10
14
  )
11
15
 
@@ -13,8 +17,238 @@ $ErrorActionPreference = "Stop"
13
17
 
14
18
  Add-Type -TypeDefinition @"
15
19
  using System;
20
+ using System.IO;
16
21
  using System.Runtime.InteropServices;
17
22
  using System.Text;
23
+ using Microsoft.Win32.SafeHandles;
24
+
25
+ // A provider created suspended exists (so its exact identity can be recorded)
26
+ // but runs no code until Resume. Providers such as Claude stop waiting for
27
+ // stdin after a few seconds, so the start grant must gate execution, not input.
28
+ public sealed class ForgeSuspendedProcess : IDisposable
29
+ {
30
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
31
+ private struct STARTUPINFO
32
+ {
33
+ public int cb;
34
+ public string lpReserved;
35
+ public string lpDesktop;
36
+ public string lpTitle;
37
+ public int dwX;
38
+ public int dwY;
39
+ public int dwXSize;
40
+ public int dwYSize;
41
+ public int dwXCountChars;
42
+ public int dwYCountChars;
43
+ public int dwFillAttribute;
44
+ public int dwFlags;
45
+ public short wShowWindow;
46
+ public short cbReserved2;
47
+ public IntPtr lpReserved2;
48
+ public IntPtr hStdInput;
49
+ public IntPtr hStdOutput;
50
+ public IntPtr hStdError;
51
+ }
52
+
53
+ [StructLayout(LayoutKind.Sequential)]
54
+ private struct PROCESS_INFORMATION
55
+ {
56
+ public IntPtr hProcess;
57
+ public IntPtr hThread;
58
+ public int dwProcessId;
59
+ public int dwThreadId;
60
+ }
61
+
62
+ [StructLayout(LayoutKind.Sequential)]
63
+ private struct SECURITY_ATTRIBUTES
64
+ {
65
+ public int nLength;
66
+ public IntPtr lpSecurityDescriptor;
67
+ public bool bInheritHandle;
68
+ }
69
+
70
+ private const uint CREATE_SUSPENDED = 0x00000004;
71
+ private const uint CREATE_NO_WINDOW = 0x08000000;
72
+ private const int STARTF_USESTDHANDLES = 0x00000100;
73
+ private const int HANDLE_FLAG_INHERIT = 0x00000001;
74
+ private const uint INFINITE = 0xFFFFFFFF;
75
+
76
+ [DllImport("kernel32.dll", SetLastError = true)]
77
+ private static extern bool CreatePipe(
78
+ out SafeFileHandle readPipe,
79
+ out SafeFileHandle writePipe,
80
+ ref SECURITY_ATTRIBUTES attributes,
81
+ int size);
82
+
83
+ [DllImport("kernel32.dll", SetLastError = true)]
84
+ private static extern bool SetHandleInformation(SafeFileHandle handle, int mask, int flags);
85
+
86
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
87
+ private static extern bool CreateProcess(
88
+ string applicationName,
89
+ StringBuilder commandLine,
90
+ IntPtr processAttributes,
91
+ IntPtr threadAttributes,
92
+ bool inheritHandles,
93
+ uint creationFlags,
94
+ IntPtr environment,
95
+ string currentDirectory,
96
+ ref STARTUPINFO startupInfo,
97
+ out PROCESS_INFORMATION processInformation);
98
+
99
+ [DllImport("kernel32.dll", SetLastError = true)]
100
+ private static extern uint ResumeThread(IntPtr thread);
101
+
102
+ [DllImport("kernel32.dll", SetLastError = true)]
103
+ private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
104
+
105
+ [DllImport("kernel32.dll", SetLastError = true)]
106
+ private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
107
+
108
+ [DllImport("kernel32.dll", SetLastError = true)]
109
+ private static extern bool TerminateProcess(IntPtr process, uint exitCode);
110
+
111
+ [DllImport("kernel32.dll", SetLastError = true)]
112
+ private static extern bool CloseHandle(IntPtr handle);
113
+
114
+ private IntPtr process;
115
+ private IntPtr thread;
116
+
117
+ private ForgeSuspendedProcess(PROCESS_INFORMATION information, Stream input, Stream output, Stream error)
118
+ {
119
+ process = information.hProcess;
120
+ thread = information.hThread;
121
+ Id = information.dwProcessId;
122
+ StandardInput = input;
123
+ StandardOutput = output;
124
+ StandardError = error;
125
+ }
126
+
127
+ public int Id { get; private set; }
128
+ public Stream StandardInput { get; private set; }
129
+ public Stream StandardOutput { get; private set; }
130
+ public Stream StandardError { get; private set; }
131
+
132
+ // Builds the same command line System.Diagnostics.Process does, so command
133
+ // resolution is unchanged from the unsuspended launch.
134
+ public static ForgeSuspendedProcess Start(string fileName, string arguments, string workingDirectory)
135
+ {
136
+ SafeFileHandle inputRead = null, inputWrite = null;
137
+ SafeFileHandle outputRead = null, outputWrite = null;
138
+ SafeFileHandle errorRead = null, errorWrite = null;
139
+ bool started = false;
140
+ try
141
+ {
142
+ CreateChildPipe(out inputRead, out inputWrite, false);
143
+ CreateChildPipe(out outputRead, out outputWrite, true);
144
+ CreateChildPipe(out errorRead, out errorWrite, true);
145
+
146
+ var startupInfo = new STARTUPINFO();
147
+ startupInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));
148
+ startupInfo.dwFlags = STARTF_USESTDHANDLES;
149
+ startupInfo.hStdInput = inputRead.DangerousGetHandle();
150
+ startupInfo.hStdOutput = outputWrite.DangerousGetHandle();
151
+ startupInfo.hStdError = errorWrite.DangerousGetHandle();
152
+
153
+ string trimmed = fileName.Trim();
154
+ bool quoted = trimmed.StartsWith("\"", StringComparison.Ordinal) && trimmed.EndsWith("\"", StringComparison.Ordinal);
155
+ var commandLine = new StringBuilder();
156
+ if (!quoted) commandLine.Append('"');
157
+ commandLine.Append(trimmed);
158
+ if (!quoted) commandLine.Append('"');
159
+ if (!String.IsNullOrEmpty(arguments)) commandLine.Append(' ').Append(arguments);
160
+
161
+ PROCESS_INFORMATION information;
162
+ if (!CreateProcess(
163
+ null,
164
+ commandLine,
165
+ IntPtr.Zero,
166
+ IntPtr.Zero,
167
+ true,
168
+ CREATE_SUSPENDED | CREATE_NO_WINDOW,
169
+ IntPtr.Zero,
170
+ String.IsNullOrEmpty(workingDirectory) ? null : workingDirectory,
171
+ ref startupInfo,
172
+ out information))
173
+ {
174
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "CreateProcess failed.");
175
+ }
176
+
177
+ started = true;
178
+ return new ForgeSuspendedProcess(
179
+ information,
180
+ new FileStream(inputWrite, FileAccess.Write, 4096, false),
181
+ new FileStream(outputRead, FileAccess.Read, 4096, false),
182
+ new FileStream(errorRead, FileAccess.Read, 4096, false));
183
+ }
184
+ finally
185
+ {
186
+ // The child owns its ends now; keeping them open would hide EOF.
187
+ if (inputRead != null) inputRead.Dispose();
188
+ if (outputWrite != null) outputWrite.Dispose();
189
+ if (errorWrite != null) errorWrite.Dispose();
190
+ if (!started)
191
+ {
192
+ if (inputWrite != null) inputWrite.Dispose();
193
+ if (outputRead != null) outputRead.Dispose();
194
+ if (errorRead != null) errorRead.Dispose();
195
+ }
196
+ }
197
+ }
198
+
199
+ // Only the child's end is inheritable; the supervisor's end stays private.
200
+ private static void CreateChildPipe(out SafeFileHandle read, out SafeFileHandle write, bool parentReads)
201
+ {
202
+ var attributes = new SECURITY_ATTRIBUTES();
203
+ attributes.nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
204
+ attributes.bInheritHandle = true;
205
+ if (!CreatePipe(out read, out write, ref attributes, 0))
206
+ {
207
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "CreatePipe failed.");
208
+ }
209
+ SafeFileHandle parentEnd = parentReads ? read : write;
210
+ if (!SetHandleInformation(parentEnd, HANDLE_FLAG_INHERIT, 0))
211
+ {
212
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "SetHandleInformation failed.");
213
+ }
214
+ }
215
+
216
+ public void Resume()
217
+ {
218
+ if (ResumeThread(thread) == UInt32.MaxValue)
219
+ {
220
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "ResumeThread failed.");
221
+ }
222
+ }
223
+
224
+ public int WaitForExit()
225
+ {
226
+ if (WaitForSingleObject(process, INFINITE) != 0)
227
+ {
228
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "WaitForSingleObject failed.");
229
+ }
230
+ uint exitCode;
231
+ if (!GetExitCodeProcess(process, out exitCode))
232
+ {
233
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "GetExitCodeProcess failed.");
234
+ }
235
+ return unchecked((int)exitCode);
236
+ }
237
+
238
+ public void Kill()
239
+ {
240
+ TerminateProcess(process, 1);
241
+ }
242
+
243
+ public void Dispose()
244
+ {
245
+ StandardInput.Dispose();
246
+ StandardOutput.Dispose();
247
+ StandardError.Dispose();
248
+ if (thread != IntPtr.Zero) { CloseHandle(thread); thread = IntPtr.Zero; }
249
+ if (process != IntPtr.Zero) { CloseHandle(process); process = IntPtr.Zero; }
250
+ }
251
+ }
18
252
 
19
253
  public static class ForgeWindowsJob
20
254
  {
@@ -73,6 +307,14 @@ public static class ForgeWindowsJob
73
307
  [DllImport("kernel32.dll", SetLastError = true)]
74
308
  public static extern bool TerminateJobObject(IntPtr job, uint exitCode);
75
309
 
310
+ [DllImport("kernel32.dll", SetLastError = true)]
311
+ private static extern bool QueryInformationJobObject(
312
+ IntPtr job,
313
+ int informationClass,
314
+ IntPtr information,
315
+ uint informationLength,
316
+ out uint returnLength);
317
+
76
318
  [DllImport("kernel32.dll", SetLastError = true)]
77
319
  public static extern bool CloseHandle(IntPtr handle);
78
320
 
@@ -109,6 +351,28 @@ public static class ForgeWindowsJob
109
351
  return job;
110
352
  }
111
353
 
354
+ public static ulong PeakJobMemoryUsed(IntPtr job)
355
+ {
356
+ int size = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
357
+ IntPtr pointer = Marshal.AllocHGlobal(size);
358
+ try
359
+ {
360
+ uint returned;
361
+ if (!QueryInformationJobObject(job, JobObjectExtendedLimitInformation, pointer, (uint)size, out returned))
362
+ {
363
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "QueryInformationJobObject failed.");
364
+ }
365
+ var information = (JOBOBJECT_EXTENDED_LIMIT_INFORMATION)Marshal.PtrToStructure(
366
+ pointer,
367
+ typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
368
+ return information.PeakJobMemoryUsed.ToUInt64();
369
+ }
370
+ finally
371
+ {
372
+ Marshal.FreeHGlobal(pointer);
373
+ }
374
+ }
375
+
112
376
  public static string BuildArguments(object[] arguments)
113
377
  {
114
378
  var result = new StringBuilder();
@@ -162,7 +426,19 @@ function Read-ForgeSupervisorPayload {
162
426
  if (-not [IO.Path]::IsPathRooted($PayloadPath)) {
163
427
  throw "The supervisor payload path must be absolute."
164
428
  }
165
- $json = [IO.File]::ReadAllText($PayloadPath, [Text.Encoding]::UTF8)
429
+ $bytes = [IO.File]::ReadAllBytes($PayloadPath)
430
+ $hasher = [Security.Cryptography.SHA256]::Create()
431
+ try {
432
+ $actualSha256 = -join ($hasher.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
433
+ }
434
+ finally {
435
+ $hasher.Dispose()
436
+ }
437
+ if (-not [string]::Equals($actualSha256, $PayloadSha256, [StringComparison]::Ordinal)) {
438
+ throw "The supervisor payload does not match its expected SHA-256 identity."
439
+ }
440
+ $utf8 = New-Object Text.UTF8Encoding($false, $true)
441
+ $json = $utf8.GetString($bytes)
166
442
  return $json | ConvertFrom-Json
167
443
  }
168
444
 
@@ -208,7 +484,32 @@ function New-ForgeChildProcess(
208
484
  }
209
485
  }
210
486
 
211
- function Write-ForgeProviderReady([object]$request, [Diagnostics.Process]$provider) {
487
+ function New-ForgeSuspendedProvider([string]$command, [object[]]$arguments, [string]$workingDirectory) {
488
+ $maximumAttempts = 3
489
+ for ($attempt = 1; $attempt -le $maximumAttempts; $attempt += 1) {
490
+ try {
491
+ $provider = [ForgeSuspendedProcess]::Start(
492
+ $command,
493
+ [ForgeWindowsJob]::BuildArguments($arguments),
494
+ $workingDirectory)
495
+ $provider | Add-Member -NotePropertyName ForgeStartAttempt -NotePropertyValue $attempt
496
+ return $provider
497
+ }
498
+ catch {
499
+ $nativeError = $_.Exception
500
+ while ($nativeError.InnerException) {
501
+ $nativeError = $nativeError.InnerException
502
+ }
503
+ $retryable = $nativeError -is [ComponentModel.Win32Exception] -and $nativeError.NativeErrorCode -in @(5, 32, 33)
504
+ if (-not $retryable -or $attempt -eq $maximumAttempts) {
505
+ throw
506
+ }
507
+ Start-Sleep -Milliseconds (100 * $attempt)
508
+ }
509
+ }
510
+ }
511
+
512
+ function Write-ForgeProviderReady([object]$request, [object]$provider) {
212
513
  $readyPath = [string]$request.readyPath
213
514
  if ([string]::IsNullOrWhiteSpace($readyPath) -or -not [IO.Path]::IsPathRooted($readyPath)) {
214
515
  throw "The provider-ready path must be absolute."
@@ -219,14 +520,17 @@ function Write-ForgeProviderReady([object]$request, [Diagnostics.Process]$provid
219
520
  throw "The provider-ready path must be beside the supervisor payload."
220
521
  }
221
522
 
523
+ # The supervisor holds the provider's process handle, so this PID cannot be
524
+ # reused before its start time is read.
525
+ $providerStartTime = (Get-Process -Id $provider.Id -ErrorAction Stop).StartTime.ToUniversalTime()
222
526
  $ready = @{
223
527
  schemaVersion = 1
224
528
  supervisorId = [string]$request.supervisorId
225
529
  rootPid = [int]$env:FORGE_SUPERVISOR_ROOT_PID
226
530
  rootIdentity = [string]$env:FORGE_SUPERVISOR_ROOT_IDENTITY
227
531
  providerPid = $provider.Id
228
- providerCreatedAt = $provider.StartTime.ToUniversalTime().ToString("O")
229
- providerIdentity = "win32:$($provider.StartTime.ToUniversalTime().Ticks)"
532
+ providerCreatedAt = $providerStartTime.ToString("O")
533
+ providerIdentity = "win32:$($providerStartTime.Ticks)"
230
534
  startAttempt = [int]$provider.ForgeStartAttempt
231
535
  } | ConvertTo-Json -Compress
232
536
  $temporaryPath = "$readyPath.$([Guid]::NewGuid().ToString('N')).tmp"
@@ -253,13 +557,17 @@ function Wait-ForgeProviderStartGrant([object]$request) {
253
557
  throw "Timed out waiting for the provider start grant."
254
558
  }
255
559
 
256
- function Start-ForgeOutputCopies([Diagnostics.Process]$child) {
560
+ function Start-ForgeStreamCopies([IO.Stream]$output, [IO.Stream]$errorOutput) {
257
561
  return @(
258
- $child.StandardOutput.BaseStream.CopyToAsync([Console]::OpenStandardOutput()),
259
- $child.StandardError.BaseStream.CopyToAsync([Console]::OpenStandardError())
562
+ $output.CopyToAsync([Console]::OpenStandardOutput()),
563
+ $errorOutput.CopyToAsync([Console]::OpenStandardError())
260
564
  )
261
565
  }
262
566
 
567
+ function Start-ForgeOutputCopies([Diagnostics.Process]$child) {
568
+ return Start-ForgeStreamCopies $child.StandardOutput.BaseStream $child.StandardError.BaseStream
569
+ }
570
+
263
571
  function Write-ForgeStandardInput([Diagnostics.Process]$child, [string]$inputText) {
264
572
  if ($inputText.Length -gt 0) {
265
573
  $child.StandardInput.Write($inputText)
@@ -267,6 +575,45 @@ function Write-ForgeStandardInput([Diagnostics.Process]$child, [string]$inputTex
267
575
  $child.StandardInput.Close()
268
576
  }
269
577
 
578
+ function Write-ForgeMemoryTelemetry([object]$request, [IntPtr]$job) {
579
+ $telemetryPath = [string]$request.memoryTelemetryPath
580
+ if ([string]::IsNullOrWhiteSpace($telemetryPath) -or -not [IO.Path]::IsPathRooted($telemetryPath)) {
581
+ throw "The memory telemetry path must be absolute."
582
+ }
583
+ $payloadDirectory = [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($PayloadPath))
584
+ $telemetryDirectory = [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($telemetryPath))
585
+ if (-not [string]::Equals($payloadDirectory, $telemetryDirectory, [StringComparison]::OrdinalIgnoreCase)) {
586
+ throw "The memory telemetry path must be beside the supervisor payload."
587
+ }
588
+ $telemetry = @{
589
+ schemaVersion = 1
590
+ peakMemoryBytes = [ForgeWindowsJob]::PeakJobMemoryUsed($job)
591
+ sampleCount = 1
592
+ source = "windows_job_object_peak_job_memory"
593
+ observedAt = [DateTimeOffset]::UtcNow.ToString("O")
594
+ } | ConvertTo-Json -Compress
595
+ $temporaryPath = "$telemetryPath.$([Guid]::NewGuid().ToString('N')).tmp"
596
+ try {
597
+ [IO.File]::WriteAllText($temporaryPath, "$telemetry`n", (New-Object Text.UTF8Encoding($false)))
598
+ [IO.File]::Move($temporaryPath, $telemetryPath)
599
+ }
600
+ finally {
601
+ if ([IO.File]::Exists($temporaryPath)) {
602
+ try { [IO.File]::Delete($temporaryPath) } catch { }
603
+ }
604
+ }
605
+ }
606
+
607
+ function Write-ForgeMemoryTelemetryBestEffort([object]$request, [IntPtr]$job) {
608
+ try {
609
+ Write-ForgeMemoryTelemetry $request $job
610
+ }
611
+ catch {
612
+ # Telemetry is optional. Provider output, descendant cleanup, and the real
613
+ # provider exit code remain authoritative if querying or persisting it fails.
614
+ }
615
+ }
616
+
270
617
  if ($Mode -eq "Inner") {
271
618
  if ([string]::IsNullOrWhiteSpace($GateName)) {
272
619
  throw "The inner supervisor requires a gate name."
@@ -284,17 +631,33 @@ if ($Mode -eq "Inner") {
284
631
  $gate.Dispose()
285
632
  }
286
633
 
287
- $provider = New-ForgeChildProcess ([string]$request.command) @($request.args) ([string]$request.cwd) $true
288
- Write-ForgeProviderReady $request $provider
289
- $copies = Start-ForgeOutputCopies $provider
290
- Wait-ForgeProviderStartGrant $request
291
- Write-ForgeStandardInput $provider $inputText
292
- $provider.WaitForExit()
634
+ # The provider exists for the ready record but runs nothing until the start
635
+ # grant. Its input is written only after Resume: a prompt larger than the pipe
636
+ # buffer would otherwise block against a reader that cannot run.
637
+ $provider = New-ForgeSuspendedProvider ([string]$request.command) @($request.args) ([string]$request.cwd)
638
+ try {
639
+ Write-ForgeProviderReady $request $provider
640
+ $copies = Start-ForgeStreamCopies $provider.StandardOutput $provider.StandardError
641
+ Wait-ForgeProviderStartGrant $request
642
+ $provider.Resume()
643
+ }
644
+ catch {
645
+ $provider.Kill()
646
+ $provider.Dispose()
647
+ throw
648
+ }
649
+ # Same encoding System.Diagnostics.Process uses for redirected input.
650
+ $inputWriter = New-Object IO.StreamWriter($provider.StandardInput, [Console]::InputEncoding, 4096)
651
+ if ($inputText.Length -gt 0) {
652
+ $inputWriter.Write($inputText)
653
+ }
654
+ $inputWriter.Close()
655
+ $exitCode = $provider.WaitForExit()
293
656
  if (-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]$copies, 1000)) {
294
657
  $provider.StandardOutput.Dispose()
295
658
  $provider.StandardError.Dispose()
296
659
  }
297
- exit $provider.ExitCode
660
+ exit $exitCode
298
661
  }
299
662
 
300
663
  $inputText = [Console]::In.ReadToEnd()
@@ -323,6 +686,8 @@ try {
323
686
  "Inner",
324
687
  "-PayloadPath",
325
688
  $PayloadPath,
689
+ "-PayloadSha256",
690
+ $PayloadSha256,
326
691
  "-GateName",
327
692
  $gateNameValue
328
693
  )
@@ -343,6 +708,7 @@ try {
343
708
  }
344
709
 
345
710
  $exitCode = $inner.ExitCode
711
+ Write-ForgeMemoryTelemetryBestEffort $request $job
346
712
  [ForgeWindowsJob]::TerminateJobObject($job, 1) | Out-Null
347
713
  [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]$copies, 2000) | Out-Null
348
714
  exit $exitCode