intentdna 1.9.0 → 1.9.3

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 (131) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +227 -87
  4. package/dist/cli/commands/run-lifecycle.d.ts +61 -11
  5. package/dist/cli/commands/run-lifecycle.js +184 -50
  6. package/dist/cli/commands/run-observer.d.ts +15 -0
  7. package/dist/cli/commands/run-observer.js +210 -0
  8. package/dist/cli/commands/run.d.ts +16 -20
  9. package/dist/cli/commands/run.js +461 -659
  10. package/dist/cli/commands/sync.js +16 -12
  11. package/dist/cli/index.js +14 -5
  12. package/dist/compiler/compile.d.ts +1 -0
  13. package/dist/compiler/compile.js +1 -1
  14. package/dist/compiler/controller.d.ts +16 -0
  15. package/dist/compiler/controller.js +693 -0
  16. package/dist/compiler/input-resolver.d.ts +2 -0
  17. package/dist/compiler/input-resolver.js +22 -6
  18. package/dist/hooks/cli.d.ts +45 -0
  19. package/dist/hooks/cli.js +263 -31
  20. package/dist/hooks/host-event.d.ts +57 -0
  21. package/dist/hooks/host-event.js +187 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/index.js +1 -0
  24. package/dist/hooks/protocol.d.ts +7 -0
  25. package/dist/hooks/schema.d.ts +1 -0
  26. package/dist/hooks/schema.js +89 -1
  27. package/dist/hooks/state.d.ts +23 -1
  28. package/dist/hooks/state.js +9 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/mcp/index.js +2 -0
  32. package/dist/mcp/tools-run.d.ts +4 -0
  33. package/dist/mcp/tools-run.js +419 -0
  34. package/dist/mcp/tools-state.d.ts +1 -1
  35. package/dist/mcp/tools-state.js +13 -241
  36. package/dist/runtime/artifact-store.d.ts +146 -0
  37. package/dist/runtime/artifact-store.js +1436 -0
  38. package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
  39. package/dist/runtime/canonical-attempt-outcome.js +195 -0
  40. package/dist/runtime/canonical-json.d.ts +10 -0
  41. package/dist/runtime/canonical-json.js +105 -0
  42. package/dist/runtime/canonical-run-application.d.ts +93 -0
  43. package/dist/runtime/canonical-run-application.js +229 -0
  44. package/dist/runtime/canonical-run-service.d.ts +454 -0
  45. package/dist/runtime/canonical-run-service.js +4760 -0
  46. package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
  47. package/dist/runtime/canonical-runtime-composition.js +34 -0
  48. package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
  49. package/dist/runtime/canonical-runtime-projection.js +4 -0
  50. package/dist/runtime/canonical-target-compiler.d.ts +46 -0
  51. package/dist/runtime/canonical-target-compiler.js +514 -0
  52. package/dist/runtime/claude-agent-identity.d.ts +2 -0
  53. package/dist/runtime/claude-agent-identity.js +21 -0
  54. package/dist/runtime/executable-run-plan.d.ts +328 -0
  55. package/dist/runtime/executable-run-plan.js +1485 -0
  56. package/dist/runtime/execution-authority.d.ts +83 -0
  57. package/dist/runtime/execution-authority.js +98 -0
  58. package/dist/runtime/handoff-resolver.js +4 -1
  59. package/dist/runtime/harness-pull-adapter.d.ts +146 -0
  60. package/dist/runtime/harness-pull-adapter.js +315 -0
  61. package/dist/runtime/index.d.ts +39 -14
  62. package/dist/runtime/index.js +29 -7
  63. package/dist/runtime/local-execution-authority.d.ts +194 -0
  64. package/dist/runtime/local-execution-authority.js +2897 -0
  65. package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
  66. package/dist/runtime/local-execution-reconciliation.js +107 -0
  67. package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
  68. package/dist/runtime/local-execution-supervisor-script.js +793 -0
  69. package/dist/runtime/local-provider-sandbox.d.ts +42 -0
  70. package/dist/runtime/local-provider-sandbox.js +154 -0
  71. package/dist/runtime/local-verifier-execution.d.ts +24 -0
  72. package/dist/runtime/local-verifier-execution.js +290 -0
  73. package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
  74. package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
  75. package/dist/runtime/plan-store.d.ts +30 -0
  76. package/dist/runtime/plan-store.js +231 -0
  77. package/dist/runtime/process-tree.d.ts +9 -0
  78. package/dist/runtime/process-tree.js +97 -24
  79. package/dist/runtime/providers/codex.js +2 -2
  80. package/dist/runtime/push-driver.d.ts +184 -0
  81. package/dist/runtime/push-driver.js +1002 -0
  82. package/dist/runtime/result-store.d.ts +34 -3
  83. package/dist/runtime/result-store.js +1073 -2
  84. package/dist/runtime/run-binding.d.ts +68 -0
  85. package/dist/runtime/run-binding.js +278 -0
  86. package/dist/runtime/run-contracts.d.ts +575 -0
  87. package/dist/runtime/run-contracts.js +58 -7
  88. package/dist/runtime/run-controller.d.ts +1 -6
  89. package/dist/runtime/run-controller.js +0 -41
  90. package/dist/runtime/run-store.d.ts +85 -7
  91. package/dist/runtime/run-store.js +2528 -186
  92. package/dist/runtime/skill-adapter.d.ts +18 -7
  93. package/dist/runtime/skill-adapter.js +100 -742
  94. package/dist/runtime/structured-output-validator.d.ts +44 -0
  95. package/dist/runtime/structured-output-validator.js +171 -0
  96. package/dist/runtime/verifier-command-binding.d.ts +22 -0
  97. package/dist/runtime/verifier-command-binding.js +162 -0
  98. package/dist/runtime/verifier.d.ts +46 -6
  99. package/dist/runtime/verifier.js +355 -53
  100. package/dist/runtime/windows-job-keeper.d.ts +47 -0
  101. package/dist/runtime/windows-job-keeper.js +231 -0
  102. package/dist/runtime/worker-executor.d.ts +17 -1
  103. package/dist/runtime/worker-executor.js +26 -5
  104. package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
  105. package/dist/runtime/workflow-plan-adapter.js +502 -6
  106. package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
  107. package/dist/runtime/workflow-runtime-manifest.js +5 -0
  108. package/dist/runtime/workspace-isolation.d.ts +29 -2
  109. package/dist/runtime/workspace-isolation.js +488 -11
  110. package/dist/runtime/workspace-observation.d.ts +44 -0
  111. package/dist/runtime/workspace-observation.js +216 -0
  112. package/dist/schema/controller-registry.d.ts +0 -7
  113. package/dist/schema/controller-registry.js +0 -8
  114. package/dist/schema/types.d.ts +84 -10
  115. package/dist/schema/types.js +2 -0
  116. package/dist/schema/validate.js +151 -1
  117. package/dist/schema/validators/controllers.d.ts +1 -1
  118. package/dist/schema/validators/controllers.js +126 -41
  119. package/dist/schema/workflow-authoring-contract.js +2 -0
  120. package/dist/schema/yaml-parser.js +1 -1
  121. package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
  122. package/dist/templates/flutter-rewrite.dna.yaml +87 -60
  123. package/dist/templates/safe-refactoring.dna.yaml +1 -0
  124. package/native/windows-job-keeper/README.md +80 -0
  125. package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
  126. package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
  127. package/native/windows-job-keeper/keeper.c +979 -0
  128. package/native/windows-job-keeper/manifest.json +33 -0
  129. package/package.json +6 -2
  130. package/scripts/build-windows-job-keeper.mjs +172 -0
  131. package/scripts/verify-windows-job-keeper.mjs +184 -0
@@ -0,0 +1,979 @@
1
+ #define WIN32_LEAN_AND_MEAN
2
+ #ifndef UNICODE
3
+ #define UNICODE
4
+ #endif
5
+ #ifndef _UNICODE
6
+ #define _UNICODE
7
+ #endif
8
+
9
+ #include <windows.h>
10
+ #include <bcrypt.h>
11
+ #include <stdint.h>
12
+ #include <stdio.h>
13
+ #include <stdlib.h>
14
+ #include <string.h>
15
+ #include <wchar.h>
16
+
17
+ #define CONFIG_MAGIC "IDNA_WINDOWS_JOB_KEEPER_V1"
18
+ #define MAX_LINE_BYTES (1024u * 1024u)
19
+ #define MAX_CONFIG_BYTES (8u * 1024u * 1024u)
20
+ #define MAX_ARGUMENTS 4096u
21
+ #define MAX_ENVIRONMENT_ENTRIES 4096u
22
+ #define MAX_ENVIRONMENT_WCHARS 32767u
23
+ #define MAX_COMMAND_LINE_WCHARS 32766u
24
+ #define MAX_DRAIN_GRACE_MS 300000u
25
+ #define MAX_OUTPUT_BYTES 9007199254740991ull
26
+ #define TERMINATE_EXIT_CODE 0xC000013Au
27
+
28
+ enum {
29
+ COMMAND_MODE_ARGV = 1,
30
+ COMMAND_MODE_VERBATIM = 2,
31
+ };
32
+
33
+ typedef struct {
34
+ wchar_t *executable;
35
+ wchar_t *cwd;
36
+ wchar_t *stdin_path;
37
+ wchar_t *stdout_path;
38
+ wchar_t *stderr_path;
39
+ int command_mode;
40
+ wchar_t *verbatim_command_line;
41
+ wchar_t **argv;
42
+ DWORD argc;
43
+ wchar_t **environment;
44
+ DWORD envc;
45
+ DWORD natural_drain_grace_ms;
46
+ DWORD forced_drain_grace_ms;
47
+ ULONGLONG max_output_bytes;
48
+ } keeper_config;
49
+
50
+ typedef struct {
51
+ HANDLE input;
52
+ HANDLE event;
53
+ volatile LONG reason;
54
+ } control_state;
55
+
56
+ typedef struct {
57
+ HANDLE input;
58
+ HANDLE output;
59
+ HANDLE event;
60
+ ULONGLONG max_bytes;
61
+ volatile LONG limit_exceeded;
62
+ volatile LONG failed;
63
+ DWORD error;
64
+ } output_state;
65
+
66
+ enum {
67
+ CONTROL_NONE = 0,
68
+ CONTROL_TERMINATE = 1,
69
+ CONTROL_EOF = 2,
70
+ CONTROL_PROTOCOL_ERROR = 3,
71
+ };
72
+
73
+ static void emit_error(const char *stage, DWORD error) {
74
+ fprintf(stderr,
75
+ "{\"schema_version\":\"intentdna.windows_job_keeper_fact.v1\","
76
+ "\"kind\":\"error\",\"stage\":\"%s\",\"win32_error\":%lu}\n",
77
+ stage, (unsigned long)error);
78
+ fflush(stderr);
79
+ }
80
+
81
+ static int read_line(HANDLE input, char **line, size_t *length, size_t *total) {
82
+ size_t capacity = 256;
83
+ size_t used = 0;
84
+ char *value = (char *)malloc(capacity);
85
+ if (value == NULL) {
86
+ SetLastError(ERROR_OUTOFMEMORY);
87
+ return -1;
88
+ }
89
+ for (;;) {
90
+ char byte;
91
+ DWORD read = 0;
92
+ if (!ReadFile(input, &byte, 1, &read, NULL)) {
93
+ DWORD error = GetLastError();
94
+ free(value);
95
+ if (error == ERROR_BROKEN_PIPE || error == ERROR_HANDLE_EOF) return 0;
96
+ SetLastError(error);
97
+ return -1;
98
+ }
99
+ if (read == 0) {
100
+ free(value);
101
+ return 0;
102
+ }
103
+ if (++(*total) > MAX_CONFIG_BYTES || used >= MAX_LINE_BYTES) {
104
+ free(value);
105
+ SetLastError(ERROR_BUFFER_OVERFLOW);
106
+ return -1;
107
+ }
108
+ if (byte == '\n') break;
109
+ if (byte == '\r' || byte == '\0') {
110
+ free(value);
111
+ SetLastError(ERROR_INVALID_DATA);
112
+ return -1;
113
+ }
114
+ if (used + 1 >= capacity) {
115
+ size_t next = capacity * 2;
116
+ char *grown = (char *)realloc(value, next);
117
+ if (grown == NULL) {
118
+ free(value);
119
+ SetLastError(ERROR_OUTOFMEMORY);
120
+ return -1;
121
+ }
122
+ value = grown;
123
+ capacity = next;
124
+ }
125
+ value[used++] = byte;
126
+ }
127
+ value[used] = '\0';
128
+ *line = value;
129
+ *length = used;
130
+ return 1;
131
+ }
132
+
133
+ static int hex_value(char value) {
134
+ if (value >= '0' && value <= '9') return value - '0';
135
+ if (value >= 'a' && value <= 'f') return value - 'a' + 10;
136
+ if (value >= 'A' && value <= 'F') return value - 'A' + 10;
137
+ return -1;
138
+ }
139
+
140
+ static wchar_t *decode_hex_utf8(const char *hex, size_t length, int allow_empty) {
141
+ if ((length == 0 && !allow_empty) || (length % 2) != 0) {
142
+ SetLastError(ERROR_INVALID_DATA);
143
+ return NULL;
144
+ }
145
+ if (length == 0) return (wchar_t *)calloc(1, sizeof(wchar_t));
146
+ size_t byte_count = length / 2;
147
+ char *bytes = (char *)malloc(byte_count);
148
+ if (bytes == NULL) {
149
+ SetLastError(ERROR_OUTOFMEMORY);
150
+ return NULL;
151
+ }
152
+ for (size_t i = 0; i < byte_count; i++) {
153
+ int high = hex_value(hex[i * 2]);
154
+ int low = hex_value(hex[i * 2 + 1]);
155
+ if (high < 0 || low < 0 || (high == 0 && low == 0)) {
156
+ free(bytes);
157
+ SetLastError(ERROR_INVALID_DATA);
158
+ return NULL;
159
+ }
160
+ bytes[i] = (char)((high << 4) | low);
161
+ }
162
+ int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, bytes,
163
+ (int)byte_count, NULL, 0);
164
+ if (count <= 0) {
165
+ free(bytes);
166
+ return NULL;
167
+ }
168
+ wchar_t *wide = (wchar_t *)calloc((size_t)count + 1, sizeof(wchar_t));
169
+ if (wide == NULL) {
170
+ free(bytes);
171
+ SetLastError(ERROR_OUTOFMEMORY);
172
+ return NULL;
173
+ }
174
+ if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, bytes, (int)byte_count,
175
+ wide, count) != count) {
176
+ DWORD error = GetLastError();
177
+ free(bytes);
178
+ free(wide);
179
+ SetLastError(error);
180
+ return NULL;
181
+ }
182
+ free(bytes);
183
+ return wide;
184
+ }
185
+
186
+ static int parse_hex_line(const char *line, size_t length, const char *prefix,
187
+ int allow_empty, wchar_t **output) {
188
+ size_t prefix_length = strlen(prefix);
189
+ if (length < prefix_length || memcmp(line, prefix, prefix_length) != 0) {
190
+ SetLastError(ERROR_INVALID_DATA);
191
+ return 0;
192
+ }
193
+ *output = decode_hex_utf8(line + prefix_length, length - prefix_length, allow_empty);
194
+ return *output != NULL;
195
+ }
196
+
197
+ static void free_config(keeper_config *config) {
198
+ free(config->executable);
199
+ free(config->cwd);
200
+ free(config->stdin_path);
201
+ free(config->stdout_path);
202
+ free(config->stderr_path);
203
+ free(config->verbatim_command_line);
204
+ if (config->argv != NULL) {
205
+ for (DWORD i = 0; i < config->argc; i++) free(config->argv[i]);
206
+ }
207
+ free(config->argv);
208
+ if (config->environment != NULL) {
209
+ for (DWORD i = 0; i < config->envc; i++) free(config->environment[i]);
210
+ }
211
+ free(config->environment);
212
+ memset(config, 0, sizeof(*config));
213
+ }
214
+
215
+ static int parse_bounded_decimal(const char *line, size_t length, const char *prefix,
216
+ DWORD maximum, DWORD *output) {
217
+ size_t prefix_length = strlen(prefix);
218
+ if (length <= prefix_length || memcmp(line, prefix, prefix_length) != 0) return 0;
219
+ DWORD parsed = 0;
220
+ for (size_t i = prefix_length; i < length; i++) {
221
+ unsigned digit = (unsigned)(line[i] - '0');
222
+ if (line[i] < '0' || line[i] > '9' || parsed > (maximum - digit) / 10) return 0;
223
+ parsed = parsed * 10 + digit;
224
+ }
225
+ *output = parsed;
226
+ return 1;
227
+ }
228
+
229
+ static int parse_bounded_u64(const char *line, size_t length, const char *prefix,
230
+ ULONGLONG maximum, ULONGLONG *output) {
231
+ size_t prefix_length = strlen(prefix);
232
+ if (length <= prefix_length || memcmp(line, prefix, prefix_length) != 0) return 0;
233
+ ULONGLONG parsed = 0;
234
+ for (size_t i = prefix_length; i < length; i++) {
235
+ unsigned digit = (unsigned)(line[i] - '0');
236
+ if (line[i] < '0' || line[i] > '9' || parsed > (maximum - digit) / 10) return 0;
237
+ parsed = parsed * 10 + digit;
238
+ }
239
+ if (parsed == 0) return 0;
240
+ *output = parsed;
241
+ return 1;
242
+ }
243
+
244
+ static int valid_environment_entry(const wchar_t *entry) {
245
+ const wchar_t *equals = wcschr(entry, L'=');
246
+ return equals != NULL && equals != entry;
247
+ }
248
+
249
+ static int environment_compare(const void *left, const void *right) {
250
+ const wchar_t *const *a = (const wchar_t *const *)left;
251
+ const wchar_t *const *b = (const wchar_t *const *)right;
252
+ return _wcsicmp(*a, *b);
253
+ }
254
+
255
+ static int read_config(HANDLE input, keeper_config *config) {
256
+ char *line = NULL;
257
+ size_t length = 0;
258
+ size_t total = 0;
259
+ int result = read_line(input, &line, &length, &total);
260
+ if (result != 1 || strcmp(line, CONFIG_MAGIC) != 0) goto invalid;
261
+ free(line);
262
+ line = NULL;
263
+
264
+ const char *prefixes[5] = { "exe:", "cwd:", "stdin:", "stdout:", "stderr:" };
265
+ wchar_t **values[5] = {
266
+ &config->executable, &config->cwd, &config->stdin_path,
267
+ &config->stdout_path, &config->stderr_path
268
+ };
269
+ const int allow_empty[5] = { 0, 1, 1, 0, 0 };
270
+ for (size_t i = 0; i < 5; i++) {
271
+ result = read_line(input, &line, &length, &total);
272
+ if (result != 1 || !parse_hex_line(line, length, prefixes[i], allow_empty[i], values[i]))
273
+ goto invalid;
274
+ free(line);
275
+ line = NULL;
276
+ }
277
+
278
+ result = read_line(input, &line, &length, &total);
279
+ if (result != 1 || !parse_bounded_u64(line, length, "max_output_bytes:",
280
+ MAX_OUTPUT_BYTES, &config->max_output_bytes))
281
+ goto invalid;
282
+ free(line);
283
+ line = NULL;
284
+
285
+ result = read_line(input, &line, &length, &total);
286
+ if (result != 1) goto invalid;
287
+ if (strcmp(line, "mode:argv") == 0) config->command_mode = COMMAND_MODE_ARGV;
288
+ else if (strcmp(line, "mode:verbatim") == 0) config->command_mode = COMMAND_MODE_VERBATIM;
289
+ else goto invalid;
290
+ free(line);
291
+ line = NULL;
292
+
293
+ if (config->command_mode == COMMAND_MODE_ARGV) {
294
+ result = read_line(input, &line, &length, &total);
295
+ if (result != 1 || !parse_bounded_decimal(line, length, "argc:", MAX_ARGUMENTS,
296
+ &config->argc)) goto invalid;
297
+ free(line);
298
+ line = NULL;
299
+ if (config->argc > 0) {
300
+ config->argv = (wchar_t **)calloc(config->argc, sizeof(wchar_t *));
301
+ if (config->argv == NULL) {
302
+ SetLastError(ERROR_OUTOFMEMORY);
303
+ return 0;
304
+ }
305
+ }
306
+ for (DWORD i = 0; i < config->argc; i++) {
307
+ result = read_line(input, &line, &length, &total);
308
+ if (result != 1 || !parse_hex_line(line, length, "arg:", 1, &config->argv[i]))
309
+ goto invalid;
310
+ free(line);
311
+ line = NULL;
312
+ }
313
+ } else {
314
+ result = read_line(input, &line, &length, &total);
315
+ if (result != 1 || !parse_hex_line(line, length, "command:", 0,
316
+ &config->verbatim_command_line))
317
+ goto invalid;
318
+ free(line);
319
+ line = NULL;
320
+ if (wcslen(config->verbatim_command_line) > MAX_COMMAND_LINE_WCHARS) goto invalid;
321
+ }
322
+
323
+ result = read_line(input, &line, &length, &total);
324
+ if (result != 1 || !parse_bounded_decimal(line, length, "envc:",
325
+ MAX_ENVIRONMENT_ENTRIES, &config->envc))
326
+ goto invalid;
327
+ free(line);
328
+ line = NULL;
329
+ if (config->envc > 0) {
330
+ config->environment = (wchar_t **)calloc(config->envc, sizeof(wchar_t *));
331
+ if (config->environment == NULL) {
332
+ SetLastError(ERROR_OUTOFMEMORY);
333
+ return 0;
334
+ }
335
+ }
336
+ size_t environment_wchars = 1;
337
+ for (DWORD i = 0; i < config->envc; i++) {
338
+ result = read_line(input, &line, &length, &total);
339
+ if (result != 1 || !parse_hex_line(line, length, "env:", 0,
340
+ &config->environment[i]) ||
341
+ !valid_environment_entry(config->environment[i])) goto invalid;
342
+ environment_wchars += wcslen(config->environment[i]) + 1;
343
+ if (environment_wchars > MAX_ENVIRONMENT_WCHARS) goto invalid;
344
+ free(line);
345
+ line = NULL;
346
+ }
347
+ if (config->envc > 1)
348
+ qsort(config->environment, config->envc, sizeof(wchar_t *), environment_compare);
349
+ for (DWORD i = 1; i < config->envc; i++) {
350
+ wchar_t *previous_equals = wcschr(config->environment[i - 1], L'=');
351
+ wchar_t *current_equals = wcschr(config->environment[i], L'=');
352
+ size_t previous_length = (size_t)(previous_equals - config->environment[i - 1]);
353
+ size_t current_length = (size_t)(current_equals - config->environment[i]);
354
+ if (previous_length == current_length &&
355
+ _wcsnicmp(config->environment[i - 1], config->environment[i], previous_length) == 0)
356
+ goto invalid;
357
+ }
358
+
359
+ result = read_line(input, &line, &length, &total);
360
+ if (result != 1 || !parse_bounded_decimal(line, length, "natural_drain_grace_ms:",
361
+ MAX_DRAIN_GRACE_MS,
362
+ &config->natural_drain_grace_ms)) goto invalid;
363
+ free(line);
364
+ line = NULL;
365
+ result = read_line(input, &line, &length, &total);
366
+ if (result != 1 || !parse_bounded_decimal(line, length, "forced_drain_grace_ms:",
367
+ MAX_DRAIN_GRACE_MS,
368
+ &config->forced_drain_grace_ms)) goto invalid;
369
+ free(line);
370
+ line = NULL;
371
+ result = read_line(input, &line, &length, &total);
372
+ if (result != 1 || strcmp(line, "end") != 0) goto invalid;
373
+ free(line);
374
+ if (config->cwd[0] == L'\0') {
375
+ free(config->cwd);
376
+ config->cwd = NULL;
377
+ }
378
+ return 1;
379
+
380
+ invalid:
381
+ free(line);
382
+ SetLastError(ERROR_INVALID_DATA);
383
+ return 0;
384
+ }
385
+
386
+ static int is_absolute_path(const wchar_t *path) {
387
+ if (path == NULL) return 0;
388
+ size_t length = wcslen(path);
389
+ int drive_absolute = length >= 3 &&
390
+ ((path[0] >= L'A' && path[0] <= L'Z') ||
391
+ (path[0] >= L'a' && path[0] <= L'z')) &&
392
+ path[1] == L':' && (path[2] == L'\\' || path[2] == L'/');
393
+ int unc_absolute = length >= 3 && path[0] == L'\\' && path[1] == L'\\'
394
+ && path[2] != L'\0';
395
+ return drive_absolute || unc_absolute;
396
+ }
397
+
398
+ static int open_provider_stdio(const keeper_config *config, HANDLE handles[3],
399
+ HANDLE output_reads[2], HANDLE output_files[2]) {
400
+ if ((config->cwd != NULL && !is_absolute_path(config->cwd)) ||
401
+ !is_absolute_path(config->executable) ||
402
+ (config->stdin_path[0] != L'\0' && !is_absolute_path(config->stdin_path)) ||
403
+ !is_absolute_path(config->stdout_path) || !is_absolute_path(config->stderr_path)) {
404
+ SetLastError(ERROR_BAD_PATHNAME);
405
+ return 0;
406
+ }
407
+ SECURITY_ATTRIBUTES security = { sizeof(security), NULL, TRUE };
408
+ const wchar_t *stdin_name = config->stdin_path[0] == L'\0' ? L"NUL" : config->stdin_path;
409
+ handles[0] = CreateFileW(stdin_name, GENERIC_READ, FILE_SHARE_READ, &security,
410
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
411
+ if (handles[0] == INVALID_HANDLE_VALUE) return 0;
412
+ SECURITY_ATTRIBUTES private_security = { sizeof(private_security), NULL, FALSE };
413
+ output_files[0] = CreateFileW(config->stdout_path, GENERIC_WRITE, 0, &private_security,
414
+ CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
415
+ if (output_files[0] == INVALID_HANDLE_VALUE) return 0;
416
+ output_files[1] = CreateFileW(config->stderr_path, GENERIC_WRITE, 0, &private_security,
417
+ CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
418
+ if (output_files[1] == INVALID_HANDLE_VALUE) return 0;
419
+ for (size_t i = 0; i < 2; i++) {
420
+ if (!CreatePipe(&output_reads[i], &handles[i + 1], &security, 0)) return 0;
421
+ if (!SetHandleInformation(output_reads[i], HANDLE_FLAG_INHERIT, 0)) return 0;
422
+ }
423
+ return 1;
424
+ }
425
+
426
+ static DWORD WINAPI output_thread(void *context) {
427
+ output_state *state = (output_state *)context;
428
+ unsigned char buffer[64 * 1024];
429
+ ULONGLONG written_total = 0;
430
+ for (;;) {
431
+ DWORD read = 0;
432
+ if (!ReadFile(state->input, buffer, sizeof(buffer), &read, NULL)) {
433
+ DWORD error = GetLastError();
434
+ if (error == ERROR_BROKEN_PIPE || error == ERROR_HANDLE_EOF) break;
435
+ state->error = error;
436
+ InterlockedExchange(&state->failed, 1);
437
+ SetEvent(state->event);
438
+ return 1;
439
+ }
440
+ if (read == 0) break;
441
+ ULONGLONG remaining = state->max_bytes - written_total;
442
+ DWORD accepted = remaining < read ? (DWORD)remaining : read;
443
+ DWORD offset = 0;
444
+ while (offset < accepted) {
445
+ DWORD written = 0;
446
+ if (!WriteFile(state->output, buffer + offset, accepted - offset, &written, NULL)
447
+ || written == 0) {
448
+ state->error = GetLastError();
449
+ InterlockedExchange(&state->failed, 1);
450
+ SetEvent(state->event);
451
+ return 1;
452
+ }
453
+ offset += written;
454
+ }
455
+ written_total += accepted;
456
+ if (accepted < read) {
457
+ InterlockedExchange(&state->limit_exceeded, 1);
458
+ SetEvent(state->event);
459
+ }
460
+ }
461
+ if (!FlushFileBuffers(state->output)) {
462
+ state->error = GetLastError();
463
+ InterlockedExchange(&state->failed, 1);
464
+ SetEvent(state->event);
465
+ return 1;
466
+ }
467
+ return 0;
468
+ }
469
+
470
+ static int append_wchar(wchar_t *buffer, size_t capacity, size_t *used, wchar_t value) {
471
+ if (*used + 1 >= capacity) {
472
+ SetLastError(ERROR_BUFFER_OVERFLOW);
473
+ return 0;
474
+ }
475
+ buffer[(*used)++] = value;
476
+ return 1;
477
+ }
478
+
479
+ static int append_quoted_arg(wchar_t *buffer, size_t capacity, size_t *used,
480
+ const wchar_t *arg) {
481
+ int quote = arg[0] == L'\0' || wcspbrk(arg, L" \t\"") != NULL;
482
+ if (!quote) {
483
+ while (*arg != L'\0') if (!append_wchar(buffer, capacity, used, *arg++)) return 0;
484
+ return 1;
485
+ }
486
+ if (!append_wchar(buffer, capacity, used, L'\"')) return 0;
487
+ size_t backslashes = 0;
488
+ for (; *arg != L'\0'; arg++) {
489
+ if (*arg == L'\\') {
490
+ backslashes++;
491
+ continue;
492
+ }
493
+ size_t count = *arg == L'\"' ? backslashes * 2 + 1 : backslashes;
494
+ for (size_t i = 0; i < count; i++)
495
+ if (!append_wchar(buffer, capacity, used, L'\\')) return 0;
496
+ backslashes = 0;
497
+ if (!append_wchar(buffer, capacity, used, *arg)) return 0;
498
+ }
499
+ for (size_t i = 0; i < backslashes * 2; i++)
500
+ if (!append_wchar(buffer, capacity, used, L'\\')) return 0;
501
+ return append_wchar(buffer, capacity, used, L'\"');
502
+ }
503
+
504
+ static wchar_t *build_command_line(const keeper_config *config) {
505
+ if (config->command_mode == COMMAND_MODE_VERBATIM) {
506
+ size_t length = wcslen(config->verbatim_command_line);
507
+ wchar_t *command = (wchar_t *)malloc((length + 1) * sizeof(wchar_t));
508
+ if (command == NULL) {
509
+ SetLastError(ERROR_OUTOFMEMORY);
510
+ return NULL;
511
+ }
512
+ memcpy(command, config->verbatim_command_line, (length + 1) * sizeof(wchar_t));
513
+ return command;
514
+ }
515
+ const size_t capacity = 32768;
516
+ wchar_t *command = (wchar_t *)calloc(capacity, sizeof(wchar_t));
517
+ if (command == NULL) {
518
+ SetLastError(ERROR_OUTOFMEMORY);
519
+ return NULL;
520
+ }
521
+ size_t used = 0;
522
+ if (!append_quoted_arg(command, capacity, &used, config->executable)) goto fail;
523
+ for (DWORD i = 0; i < config->argc; i++) {
524
+ if (!append_wchar(command, capacity, &used, L' ') ||
525
+ !append_quoted_arg(command, capacity, &used, config->argv[i])) goto fail;
526
+ }
527
+ return command;
528
+ fail:
529
+ free(command);
530
+ return NULL;
531
+ }
532
+
533
+ static wchar_t *build_environment_block(const keeper_config *config) {
534
+ size_t total = config->envc == 0 ? 2 : 1;
535
+ for (DWORD i = 0; i < config->envc; i++) total += wcslen(config->environment[i]) + 1;
536
+ if (total > MAX_ENVIRONMENT_WCHARS) {
537
+ SetLastError(ERROR_BUFFER_OVERFLOW);
538
+ return NULL;
539
+ }
540
+ wchar_t *block = (wchar_t *)calloc(total, sizeof(wchar_t));
541
+ if (block == NULL) {
542
+ SetLastError(ERROR_OUTOFMEMORY);
543
+ return NULL;
544
+ }
545
+ wchar_t *cursor = block;
546
+ for (DWORD i = 0; i < config->envc; i++) {
547
+ size_t length = wcslen(config->environment[i]);
548
+ memcpy(cursor, config->environment[i], (length + 1) * sizeof(wchar_t));
549
+ cursor += length + 1;
550
+ }
551
+ *cursor = L'\0';
552
+ return block;
553
+ }
554
+
555
+ static int sha256_bytes(const void *bytes, ULONG length, unsigned char digest[32]) {
556
+ BCRYPT_ALG_HANDLE algorithm = NULL;
557
+ BCRYPT_HASH_HANDLE hash = NULL;
558
+ int ok = 0;
559
+ if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, NULL, 0) < 0)
560
+ goto cleanup;
561
+ if (BCryptCreateHash(algorithm, &hash, NULL, 0, NULL, 0, 0) < 0) goto cleanup;
562
+ if (BCryptHashData(hash, (PUCHAR)bytes, length, 0) < 0) goto cleanup;
563
+ if (BCryptFinishHash(hash, digest, 32, 0) < 0) goto cleanup;
564
+ ok = 1;
565
+ cleanup:
566
+ if (hash != NULL) BCryptDestroyHash(hash);
567
+ if (algorithm != NULL) BCryptCloseAlgorithmProvider(algorithm, 0);
568
+ if (!ok) SetLastError(ERROR_GEN_FAILURE);
569
+ return ok;
570
+ }
571
+
572
+ static int create_job(HANDLE *job, HANDLE *completion_port,
573
+ ULONG_PTR *completion_key, char identifier[65]) {
574
+ unsigned char nonce[32];
575
+ if (BCryptGenRandom(NULL, nonce, sizeof(nonce), BCRYPT_USE_SYSTEM_PREFERRED_RNG) < 0) {
576
+ SetLastError(ERROR_GEN_FAILURE);
577
+ return 0;
578
+ }
579
+ *job = CreateJobObjectW(NULL, NULL);
580
+ if (*job == NULL) return 0;
581
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits;
582
+ memset(&limits, 0, sizeof(limits));
583
+ limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
584
+ if (!SetInformationJobObject(*job, JobObjectExtendedLimitInformation, &limits,
585
+ sizeof(limits))) goto fail;
586
+ *completion_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);
587
+ if (*completion_port == NULL) goto fail;
588
+ *completion_key = (ULONG_PTR)*job;
589
+ JOBOBJECT_ASSOCIATE_COMPLETION_PORT association;
590
+ association.CompletionKey = (PVOID)*completion_key;
591
+ association.CompletionPort = *completion_port;
592
+ if (!SetInformationJobObject(*job, JobObjectAssociateCompletionPortInformation,
593
+ &association, sizeof(association))) goto fail;
594
+ unsigned char digest[32];
595
+ if (!sha256_bytes(nonce, sizeof(nonce), digest)) goto fail;
596
+ static const char hex[] = "0123456789abcdef";
597
+ for (size_t i = 0; i < 32; i++) {
598
+ identifier[i * 2] = hex[digest[i] >> 4];
599
+ identifier[i * 2 + 1] = hex[digest[i] & 15];
600
+ }
601
+ identifier[64] = '\0';
602
+ return 1;
603
+ fail: {
604
+ DWORD error = GetLastError();
605
+ if (*completion_port != NULL) {
606
+ CloseHandle(*completion_port);
607
+ *completion_port = NULL;
608
+ }
609
+ CloseHandle(*job);
610
+ *job = NULL;
611
+ SetLastError(error);
612
+ return 0;
613
+ }
614
+ }
615
+
616
+ static int process_identity(HANDLE process, ULONGLONG *created) {
617
+ FILETIME creation, exit, kernel, user;
618
+ if (!GetProcessTimes(process, &creation, &exit, &kernel, &user)) return 0;
619
+ ULARGE_INTEGER value;
620
+ value.LowPart = creation.dwLowDateTime;
621
+ value.HighPart = creation.dwHighDateTime;
622
+ *created = value.QuadPart;
623
+ return 1;
624
+ }
625
+
626
+ static int wait_for_active_process_zero(HANDLE completion_port,
627
+ ULONG_PTR completion_key,
628
+ DWORD grace_ms, int *drained) {
629
+ ULONGLONG deadline = GetTickCount64() + grace_ms;
630
+ *drained = 0;
631
+ for (;;) {
632
+ ULONGLONG now = GetTickCount64();
633
+ DWORD remaining = now >= deadline ? 0 : (DWORD)(deadline - now);
634
+ DWORD message = 0;
635
+ ULONG_PTR received_key = 0;
636
+ LPOVERLAPPED overlapped = NULL;
637
+ if (!GetQueuedCompletionStatus(completion_port, &message, &received_key,
638
+ &overlapped, remaining)) {
639
+ DWORD error = GetLastError();
640
+ if (error == WAIT_TIMEOUT) return 1;
641
+ SetLastError(error);
642
+ return 0;
643
+ }
644
+ if (received_key == completion_key &&
645
+ message == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) {
646
+ *drained = 1;
647
+ return 1;
648
+ }
649
+ }
650
+ }
651
+
652
+ static DWORD WINAPI control_thread(void *context) {
653
+ control_state *state = (control_state *)context;
654
+ size_t total = 0;
655
+ char *line = NULL;
656
+ size_t length = 0;
657
+ int result = read_line(state->input, &line, &length, &total);
658
+ LONG reason = CONTROL_PROTOCOL_ERROR;
659
+ if (result == 0) reason = CONTROL_EOF;
660
+ else if (result == 1 && strcmp(line, "terminate") == 0) reason = CONTROL_TERMINATE;
661
+ free(line);
662
+ InterlockedCompareExchange(&state->reason, reason, CONTROL_NONE);
663
+ SetEvent(state->event);
664
+ return 0;
665
+ }
666
+
667
+ static int await_release(HANDLE input) {
668
+ size_t total = 0;
669
+ char *line = NULL;
670
+ size_t length = 0;
671
+ int result = read_line(input, &line, &length, &total);
672
+ int released = result == 1 && strcmp(line, "release") == 0;
673
+ free(line);
674
+ if (!released) SetLastError(result == 0 ? ERROR_BROKEN_PIPE : ERROR_INVALID_DATA);
675
+ return released;
676
+ }
677
+
678
+ int wmain(void) {
679
+ int exit_status = 1;
680
+ HANDLE input = GetStdHandle(STD_INPUT_HANDLE);
681
+ HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
682
+ HANDLE error_output = GetStdHandle(STD_ERROR_HANDLE);
683
+ keeper_config config;
684
+ memset(&config, 0, sizeof(config));
685
+ HANDLE stdio[3] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE };
686
+ HANDLE output_reads[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE };
687
+ HANDLE output_files[2] = { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE };
688
+ HANDLE output_threads[2] = { NULL, NULL };
689
+ HANDLE output_event = NULL;
690
+ output_state outputs[2];
691
+ memset(outputs, 0, sizeof(outputs));
692
+ HANDLE job = NULL;
693
+ HANDLE job_completion_port = NULL;
694
+ ULONG_PTR job_completion_key = 0;
695
+ HANDLE control_event = NULL;
696
+ HANDLE reader = NULL;
697
+ PROCESS_INFORMATION root;
698
+ memset(&root, 0, sizeof(root));
699
+ wchar_t *command_line = NULL;
700
+ wchar_t *environment_block = NULL;
701
+ LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL;
702
+ int attributes_initialized = 0;
703
+ int contained = 0;
704
+ char job_identifier[65];
705
+ DWORD root_exit_code = STILL_ACTIVE;
706
+ int root_exit_observed = 0;
707
+ const char *completion_reason = "root_exit";
708
+
709
+ if (input == NULL || input == INVALID_HANDLE_VALUE || output == NULL ||
710
+ output == INVALID_HANDLE_VALUE || error_output == NULL ||
711
+ error_output == INVALID_HANDLE_VALUE) {
712
+ emit_error("stdio", ERROR_INVALID_HANDLE);
713
+ goto cleanup;
714
+ }
715
+ if (!read_config(input, &config)) {
716
+ emit_error("config", GetLastError());
717
+ goto cleanup;
718
+ }
719
+ if (!open_provider_stdio(&config, stdio, output_reads, output_files)) {
720
+ emit_error("provider_stdio", GetLastError());
721
+ goto cleanup;
722
+ }
723
+ output_event = CreateEventW(NULL, TRUE, FALSE, NULL);
724
+ if (output_event == NULL) {
725
+ emit_error("output_event", GetLastError());
726
+ goto cleanup;
727
+ }
728
+ for (size_t i = 0; i < 2; i++) {
729
+ outputs[i].input = output_reads[i];
730
+ outputs[i].output = output_files[i];
731
+ outputs[i].event = output_event;
732
+ outputs[i].max_bytes = config.max_output_bytes;
733
+ output_threads[i] = CreateThread(NULL, 0, output_thread, &outputs[i], 0, NULL);
734
+ if (output_threads[i] == NULL) {
735
+ emit_error("output_thread", GetLastError());
736
+ goto cleanup;
737
+ }
738
+ }
739
+ if (!create_job(&job, &job_completion_port, &job_completion_key,
740
+ job_identifier)) {
741
+ emit_error("create_job", GetLastError());
742
+ goto cleanup;
743
+ }
744
+ command_line = build_command_line(&config);
745
+ if (command_line == NULL) {
746
+ emit_error("command_line", GetLastError());
747
+ goto cleanup;
748
+ }
749
+ environment_block = build_environment_block(&config);
750
+ if (environment_block == NULL) {
751
+ emit_error("environment_block", GetLastError());
752
+ goto cleanup;
753
+ }
754
+
755
+ SIZE_T attribute_bytes = 0;
756
+ InitializeProcThreadAttributeList(NULL, 1, 0, &attribute_bytes);
757
+ attributes = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attribute_bytes);
758
+ if (attributes == NULL) {
759
+ emit_error("attribute_memory", ERROR_OUTOFMEMORY);
760
+ goto cleanup;
761
+ }
762
+ if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attribute_bytes)) {
763
+ emit_error("attribute_list", GetLastError());
764
+ goto cleanup;
765
+ }
766
+ attributes_initialized = 1;
767
+ if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
768
+ stdio, sizeof(stdio), NULL, NULL)) {
769
+ emit_error("attribute_handles", GetLastError());
770
+ goto cleanup;
771
+ }
772
+ STARTUPINFOEXW startup;
773
+ memset(&startup, 0, sizeof(startup));
774
+ startup.StartupInfo.cb = sizeof(startup);
775
+ startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
776
+ startup.StartupInfo.hStdInput = stdio[0];
777
+ startup.StartupInfo.hStdOutput = stdio[1];
778
+ startup.StartupInfo.hStdError = stdio[2];
779
+ startup.lpAttributeList = attributes;
780
+ if (!CreateProcessW(config.executable, command_line, NULL, NULL, TRUE,
781
+ CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT |
782
+ EXTENDED_STARTUPINFO_PRESENT,
783
+ environment_block, config.cwd, &startup.StartupInfo, &root)) {
784
+ emit_error("create_process", GetLastError());
785
+ goto cleanup;
786
+ }
787
+ CloseHandle(stdio[1]);
788
+ stdio[1] = INVALID_HANDLE_VALUE;
789
+ CloseHandle(stdio[2]);
790
+ stdio[2] = INVALID_HANDLE_VALUE;
791
+ if (!AssignProcessToJobObject(job, root.hProcess)) {
792
+ emit_error("assign_process", GetLastError());
793
+ goto cleanup;
794
+ }
795
+ contained = 1;
796
+ ULONGLONG keeper_created = 0;
797
+ ULONGLONG root_created = 0;
798
+ if (!process_identity(GetCurrentProcess(), &keeper_created) ||
799
+ !process_identity(root.hProcess, &root_created)) {
800
+ emit_error("process_times", GetLastError());
801
+ goto cleanup;
802
+ }
803
+
804
+ printf("{\"schema_version\":\"intentdna.windows_job_keeper_fact.v1\","
805
+ "\"kind\":\"contained_ready\",\"job_identifier\":\"sha256:%s\","
806
+ "\"keeper_pid\":%lu,\"keeper_creation_filetime\":\"%llu\","
807
+ "\"root_pid\":%lu,\"root_creation_filetime\":\"%llu\"}\n",
808
+ job_identifier, (unsigned long)GetCurrentProcessId(),
809
+ (unsigned long long)keeper_created, (unsigned long)root.dwProcessId,
810
+ (unsigned long long)root_created);
811
+ fflush(stdout);
812
+
813
+ if (!await_release(input)) {
814
+ emit_error("release_gate", GetLastError());
815
+ goto cleanup;
816
+ }
817
+ if (ResumeThread(root.hThread) == (DWORD)-1) {
818
+ emit_error("resume_process", GetLastError());
819
+ goto cleanup;
820
+ }
821
+ printf("{\"schema_version\":\"intentdna.windows_job_keeper_fact.v1\","
822
+ "\"kind\":\"released\",\"root_pid\":%lu}\n",
823
+ (unsigned long)root.dwProcessId);
824
+ fflush(stdout);
825
+
826
+ control_event = CreateEventW(NULL, TRUE, FALSE, NULL);
827
+ if (control_event == NULL) {
828
+ emit_error("control_event", GetLastError());
829
+ goto cleanup;
830
+ }
831
+ control_state control = { input, control_event, CONTROL_NONE };
832
+ reader = CreateThread(NULL, 0, control_thread, &control, 0, NULL);
833
+ if (reader == NULL) {
834
+ emit_error("control_thread", GetLastError());
835
+ goto cleanup;
836
+ }
837
+
838
+ HANDLE waits[3] = { root.hProcess, control_event, output_event };
839
+ DWORD wait_result = WaitForMultipleObjects(3, waits, FALSE, INFINITE);
840
+ int force_termination = 0;
841
+ if (wait_result == WAIT_OBJECT_0) {
842
+ root_exit_observed = 1;
843
+ if (!GetExitCodeProcess(root.hProcess, &root_exit_code)) {
844
+ emit_error("root_exit_code", GetLastError());
845
+ goto cleanup;
846
+ }
847
+ ULONGLONG deadline = GetTickCount64() + config.natural_drain_grace_ms;
848
+ for (;;) {
849
+ ULONGLONG now = GetTickCount64();
850
+ DWORD wait_ms = now >= deadline ? 0 :
851
+ (DWORD)((deadline - now) < 10 ? deadline - now : 10);
852
+ int drained = 0;
853
+ if (!wait_for_active_process_zero(job_completion_port, job_completion_key,
854
+ wait_ms, &drained)) {
855
+ emit_error("drain_job", GetLastError());
856
+ goto cleanup;
857
+ }
858
+ if (drained) break;
859
+ if (WaitForSingleObject(output_event, 0) == WAIT_OBJECT_0) {
860
+ if (outputs[0].failed || outputs[1].failed) {
861
+ emit_error("provider_output", outputs[0].failed ? outputs[0].error : outputs[1].error);
862
+ goto cleanup;
863
+ }
864
+ completion_reason = "output_limit";
865
+ force_termination = 1;
866
+ break;
867
+ }
868
+ if (WaitForSingleObject(control_event, 10) == WAIT_OBJECT_0) {
869
+ LONG reason = InterlockedCompareExchange(&control.reason, CONTROL_NONE, CONTROL_NONE);
870
+ completion_reason = reason == CONTROL_TERMINATE ? "terminate" :
871
+ reason == CONTROL_EOF ? "parent_eof" : "protocol_error";
872
+ force_termination = 1;
873
+ break;
874
+ }
875
+ if (GetTickCount64() >= deadline) {
876
+ completion_reason = "drain_timeout";
877
+ force_termination = 1;
878
+ break;
879
+ }
880
+ }
881
+ } else if (wait_result == WAIT_OBJECT_0 + 1) {
882
+ LONG reason = InterlockedCompareExchange(&control.reason, CONTROL_NONE, CONTROL_NONE);
883
+ completion_reason = reason == CONTROL_TERMINATE ? "terminate" :
884
+ reason == CONTROL_EOF ? "parent_eof" : "protocol_error";
885
+ force_termination = 1;
886
+ } else if (wait_result == WAIT_OBJECT_0 + 2) {
887
+ if (outputs[0].failed || outputs[1].failed) {
888
+ emit_error("provider_output", outputs[0].failed ? outputs[0].error : outputs[1].error);
889
+ goto cleanup;
890
+ }
891
+ completion_reason = "output_limit";
892
+ force_termination = 1;
893
+ } else {
894
+ emit_error("wait", GetLastError());
895
+ goto cleanup;
896
+ }
897
+
898
+ if (force_termination) {
899
+ if (!TerminateJobObject(job, TERMINATE_EXIT_CODE)) {
900
+ emit_error("terminate_job", GetLastError());
901
+ goto cleanup;
902
+ }
903
+ int drained = 0;
904
+ if (!wait_for_active_process_zero(job_completion_port, job_completion_key,
905
+ config.forced_drain_grace_ms, &drained)) {
906
+ emit_error("drain_job", GetLastError());
907
+ goto cleanup;
908
+ }
909
+ if (!drained) {
910
+ SetLastError(WAIT_TIMEOUT);
911
+ emit_error("drain_job", GetLastError());
912
+ goto cleanup;
913
+ }
914
+ if (!root_exit_observed &&
915
+ WaitForSingleObject(root.hProcess, config.forced_drain_grace_ms) == WAIT_OBJECT_0) {
916
+ root_exit_observed = GetExitCodeProcess(root.hProcess, &root_exit_code) != 0;
917
+ }
918
+ }
919
+
920
+ if (WaitForMultipleObjects(2, output_threads, TRUE, INFINITE) != WAIT_OBJECT_0) {
921
+ emit_error("join_output", GetLastError());
922
+ goto cleanup;
923
+ }
924
+ if (outputs[0].failed || outputs[1].failed) {
925
+ emit_error("provider_output", outputs[0].failed ? outputs[0].error : outputs[1].error);
926
+ goto cleanup;
927
+ }
928
+ if (outputs[0].limit_exceeded || outputs[1].limit_exceeded)
929
+ completion_reason = "output_limit";
930
+
931
+ printf("{\"schema_version\":\"intentdna.windows_job_keeper_fact.v1\","
932
+ "\"kind\":\"completed\",\"reason\":\"%s\","
933
+ "\"root_exit_observed\":%s,\"root_exit_code\":%lu,"
934
+ "\"windows_signal\":null,\"active_processes\":0,"
935
+ "\"process_tree_drained\":true}\n",
936
+ completion_reason, root_exit_observed ? "true" : "false",
937
+ (unsigned long)root_exit_code);
938
+ fflush(stdout);
939
+ exit_status = strcmp(completion_reason, "protocol_error") == 0 ? 2 : 0;
940
+
941
+ cleanup:
942
+ if (exit_status != 0 && contained && job != NULL) {
943
+ TerminateJobObject(job, TERMINATE_EXIT_CODE);
944
+ int drained = 0;
945
+ wait_for_active_process_zero(job_completion_port, job_completion_key,
946
+ config.forced_drain_grace_ms, &drained);
947
+ } else if (exit_status != 0 && root.hProcess != NULL) {
948
+ if (!TerminateProcess(root.hProcess, TERMINATE_EXIT_CODE))
949
+ emit_error("terminate_uncontained_process", GetLastError());
950
+ if (WaitForSingleObject(root.hProcess, INFINITE) != WAIT_OBJECT_0)
951
+ emit_error("wait_uncontained_process", GetLastError());
952
+ }
953
+ if (reader != NULL) {
954
+ CancelSynchronousIo(reader);
955
+ WaitForSingleObject(reader, INFINITE);
956
+ }
957
+ if (root.hThread != NULL) CloseHandle(root.hThread);
958
+ if (root.hProcess != NULL) CloseHandle(root.hProcess);
959
+ if (reader != NULL) CloseHandle(reader);
960
+ if (control_event != NULL) CloseHandle(control_event);
961
+ if (output_event != NULL) CloseHandle(output_event);
962
+ if (job != NULL) CloseHandle(job);
963
+ if (job_completion_port != NULL) CloseHandle(job_completion_port);
964
+ for (size_t i = 0; i < 3; i++)
965
+ if (stdio[i] != INVALID_HANDLE_VALUE) CloseHandle(stdio[i]);
966
+ for (size_t i = 0; i < 2; i++) {
967
+ if (output_threads[i] != NULL) CloseHandle(output_threads[i]);
968
+ if (output_reads[i] != INVALID_HANDLE_VALUE) CloseHandle(output_reads[i]);
969
+ if (output_files[i] != INVALID_HANDLE_VALUE) CloseHandle(output_files[i]);
970
+ }
971
+ if (attributes != NULL) {
972
+ if (attributes_initialized) DeleteProcThreadAttributeList(attributes);
973
+ free(attributes);
974
+ }
975
+ free(command_line);
976
+ free(environment_block);
977
+ free_config(&config);
978
+ return exit_status;
979
+ }