deadbyte-mcp 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/MANIFEST.SHA256 +101 -39
  2. package/README.txt +10 -9
  3. package/acceptance/r40/desktop-live-drift.mjs +106 -0
  4. package/acceptance/r40/desktop-live-ground.mjs +110 -0
  5. package/acceptance/r40/desktop-live-input.mjs +128 -0
  6. package/acceptance/r40/desktop-live-observe.mjs +100 -0
  7. package/acceptance/r40/desktop-live-uia.mjs +121 -0
  8. package/bin/deadbyte-desktop-input-r40.exe +0 -0
  9. package/bin/deadbyte-desktop-input-r40.obj +0 -0
  10. package/bin/deadbyte-desktop-observer-r40.exe +0 -0
  11. package/bin/deadbyte-desktop-observer-r40.obj +0 -0
  12. package/bin/deadbyte-desktop-uia-r40.exe +0 -0
  13. package/bin/deadbyte-desktop-uia-r40.obj +0 -0
  14. package/controller/README.TXT +7 -5
  15. package/controller/deadbyte-controller.config.psd1.example +3 -0
  16. package/controller/deadbyte-controller.ps1 +149 -4
  17. package/controller/deadbyte-desktop-policy.json +41 -0
  18. package/docs/ARCHITECTURE.md +6 -1
  19. package/docs/superpowers/specs/2026-09-19-r40-desktop-computer-use-design.md +214 -0
  20. package/native/deadbyte-desktop-input.cpp +455 -0
  21. package/native/deadbyte-desktop-observer.cpp +253 -0
  22. package/native/deadbyte-desktop-uia.cpp +307 -0
  23. package/package.json +10 -8
  24. package/scripts/build-desktop-r40.ps1 +106 -0
  25. package/scripts/deferred-slot-operation-r39.mjs +124 -0
  26. package/scripts/deferred-slot-operation-r40.mjs +126 -0
  27. package/scripts/final-closure-r39.mjs +113 -0
  28. package/scripts/final-closure-r40.mjs +114 -0
  29. package/scripts/final-closure-verify-r39.mjs +208 -0
  30. package/scripts/final-closure-verify-r40.mjs +212 -0
  31. package/scripts/gate-windows-r39.ps1 +42 -0
  32. package/scripts/gate-windows-r40.ps1 +43 -0
  33. package/scripts/init-desktop-policy-r40.ps1 +79 -0
  34. package/scripts/r40-compact-ledger-probe.mjs +4 -0
  35. package/scripts/release-parity-r39.mjs +200 -0
  36. package/scripts/release-parity-r40.mjs +212 -0
  37. package/scripts/release-parity-tests-r39.ps1 +9 -0
  38. package/scripts/release-parity-tests-r40.ps1 +9 -0
  39. package/scripts/windows-gate-evidence-r39.mjs +52 -0
  40. package/scripts/windows-gate-evidence-r40.mjs +52 -0
  41. package/src/autonomous-context-engine.mjs +2 -1
  42. package/src/autonomous-planner-contracts.mjs +19 -1
  43. package/src/autonomous-planner.mjs +22 -0
  44. package/src/autonomous-runtime.mjs +144 -24
  45. package/src/capability-ledger.mjs +6 -1
  46. package/src/compact-gateway-mcp-tools.mjs +14 -2
  47. package/src/deadbyte-cli.mjs +79 -47
  48. package/src/desktop-contract.mjs +263 -0
  49. package/src/desktop-input-r40.mjs +148 -0
  50. package/src/desktop-mcp-tools-r40.mjs +126 -0
  51. package/src/desktop-memory-r40.mjs +272 -0
  52. package/src/desktop-observer-r40.mjs +231 -0
  53. package/src/desktop-policy-r40.mjs +110 -0
  54. package/src/desktop-runtime-r40.mjs +318 -0
  55. package/src/desktop-uia-r40.mjs +123 -0
  56. package/src/mcp-server.mjs +74 -19
  57. package/src/r40-compact-disarmed-probe.mjs +71 -0
  58. package/src/r40-full-surface-probe.mjs +34 -0
  59. package/src/release-generation.mjs +38 -5
  60. package/src/remote-console.mjs +41 -0
  61. package/src/remote-live-log.mjs +215 -45
  62. package/src/remote-mcp-desktop-r40-smoke-client.mjs +70 -0
  63. package/src/remote-mcp-r40-parity-client.mjs +200 -0
  64. package/src/remote-mcp-smoke-client.mjs +20 -6
  65. package/src/remote-result-journal.mjs +10 -2
  66. package/src/runtime-update.mjs +55 -17
  67. package/src/tool-audit.mjs +29 -2
  68. package/src/version.mjs +1 -1
  69. package/test/autonomous-capability-r36.test.mjs +43 -1
  70. package/test/autonomous-desktop-r40.test.mjs +188 -0
  71. package/test/capability-ledger-r36.test.mjs +1 -0
  72. package/test/cli-entrypoint.test.mjs +1 -1
  73. package/test/compact-catalog-r36.test.mjs +2 -2
  74. package/test/compact-gateway-r36.test.mjs +56 -4
  75. package/test/compact-max-catalog-r40.test.mjs +95 -0
  76. package/test/controller-desktop-r40.test.mjs +85 -0
  77. package/test/core.test.mjs +1 -1
  78. package/test/desktop-adversarial-r40.test.mjs +62 -0
  79. package/test/desktop-contract-r40.test.mjs +200 -0
  80. package/test/desktop-input-r40.test.mjs +63 -0
  81. package/test/desktop-mcp-tools-r40.test.mjs +61 -0
  82. package/test/desktop-memory-r40.test.mjs +141 -0
  83. package/test/desktop-observer-r40.test.mjs +119 -0
  84. package/test/desktop-policy-r40.test.mjs +111 -0
  85. package/test/desktop-runtime-r40.test.mjs +278 -0
  86. package/test/desktop-uia-r40.test.mjs +42 -0
  87. package/test/full-max-catalog-r40.test.mjs +91 -0
  88. package/test/helpers/autonomous-r34-fixture.mjs +3 -3
  89. package/test/production-docs.test.mjs +3 -3
  90. package/test/r33-closeout-regression.test.mjs +8 -8
  91. package/test/r33-finalization.test.mjs +14 -14
  92. package/test/r37-remote-tool-call-timeline.test.mjs +7 -6
  93. package/test/r39-finalization.test.mjs +74 -0
  94. package/test/r40-finalization.test.mjs +118 -0
  95. package/test/release-generation.test.mjs +41 -9
  96. package/test/release-version.test.mjs +19 -58
  97. package/test/remote-console-r40.test.mjs +38 -0
  98. package/test/remote-live-log-r37.test.mjs +89 -32
  99. package/test/remote-r24-catalog.test.mjs +12 -3
  100. package/test/remote-result-journal.test.mjs +15 -0
  101. package/test/remote-session-cli.test.mjs +112 -55
  102. package/test/runtime-self-update.test.mjs +44 -0
@@ -0,0 +1,455 @@
1
+ #define WIN32_LEAN_AND_MEAN
2
+ #include <windows.h>
3
+ #include <io.h>
4
+ #include <fcntl.h>
5
+ #include <cstdio>
6
+ #include <cstdint>
7
+ #include <cstdlib>
8
+ #include <string>
9
+ #include <vector>
10
+ #include <sstream>
11
+ #include <iomanip>
12
+ #include <algorithm>
13
+
14
+ static constexpr size_t kMaxRequestBytes = 128u * 1024u;
15
+ static constexpr size_t kMaxTextBytes = 64u * 1024u;
16
+ static constexpr size_t kMaxKeys = 5u;
17
+
18
+ struct Request {
19
+ std::string actionSha256;
20
+ std::string kind;
21
+ std::string expectedDesktop;
22
+ uintptr_t expectedHwnd = 0;
23
+ DWORD expectedPid = 0;
24
+ LONG left = 0;
25
+ LONG top = 0;
26
+ LONG width = 0;
27
+ LONG height = 0;
28
+ LONG x = 0;
29
+ LONG y = 0;
30
+ LONG scrollDelta = 0;
31
+ std::vector<std::string> keys;
32
+ std::string textUtf8;
33
+ };
34
+
35
+ static std::string Utf8(const wchar_t* value) {
36
+ if (!value) return {};
37
+ const int needed = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, nullptr, 0, nullptr, nullptr);
38
+ if (needed <= 0) return {};
39
+ std::string out(static_cast<size_t>(needed), '\0');
40
+ if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, out.data(), needed, nullptr, nullptr) <= 0) return {};
41
+ if (!out.empty() && out.back() == '\0') out.pop_back();
42
+ return out;
43
+ }
44
+
45
+ static std::string JsonEscape(const std::string& input) {
46
+ std::ostringstream out;
47
+ for (unsigned char ch : input) {
48
+ switch (ch) {
49
+ case '"': out << "\\\""; break;
50
+ case '\\': out << "\\\\"; break;
51
+ case '\b': out << "\\b"; break;
52
+ case '\f': out << "\\f"; break;
53
+ case '\n': out << "\\n"; break;
54
+ case '\r': out << "\\r"; break;
55
+ case '\t': out << "\\t"; break;
56
+ default:
57
+ if (ch < 0x20) out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<unsigned int>(ch) << std::dec;
58
+ else out << static_cast<char>(ch);
59
+ }
60
+ }
61
+ return out.str();
62
+ }
63
+
64
+ static std::string HwndHex(HWND hwnd) {
65
+ std::ostringstream out;
66
+ out << "0x" << std::hex << std::nouppercase << reinterpret_cast<uintptr_t>(hwnd);
67
+ return out.str();
68
+ }
69
+
70
+ static bool InputDesktopName(std::string& name) {
71
+ HDESK desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
72
+ if (!desktop) return false;
73
+ wchar_t buffer[512]{};
74
+ DWORD needed = 0;
75
+ const BOOL ok = GetUserObjectInformationW(desktop, UOI_NAME, buffer, sizeof(buffer), &needed);
76
+ CloseDesktop(desktop);
77
+ if (!ok) return false;
78
+ name = Utf8(buffer);
79
+ return !name.empty();
80
+ }
81
+
82
+ static bool Hex64(const std::string& value) {
83
+ if (value.size() != 64u) return false;
84
+ return std::all_of(value.begin(), value.end(), [](char c) {
85
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
86
+ });
87
+ }
88
+
89
+ static bool ParseLong(const std::string& value, LONG& out) {
90
+ if (value.empty()) return false;
91
+ char* end = nullptr;
92
+ errno = 0;
93
+ const long parsed = std::strtol(value.c_str(), &end, 10);
94
+ if (errno != 0 || end == value.c_str() || *end != '\0') return false;
95
+ out = static_cast<LONG>(parsed);
96
+ return true;
97
+ }
98
+
99
+ static bool ParseDword(const std::string& value, DWORD& out) {
100
+ if (value.empty()) return false;
101
+ char* end = nullptr;
102
+ errno = 0;
103
+ const unsigned long parsed = std::strtoul(value.c_str(), &end, 10);
104
+ if (errno != 0 || end == value.c_str() || *end != '\0') return false;
105
+ out = static_cast<DWORD>(parsed);
106
+ return true;
107
+ }
108
+
109
+ static bool ParseUintptrHex(const std::string& value, uintptr_t& out) {
110
+ if (value.size() < 3u || value[0] != '0' || value[1] != 'x') return false;
111
+ char* end = nullptr;
112
+ errno = 0;
113
+ const unsigned long long parsed = std::strtoull(value.c_str() + 2, &end, 16);
114
+ if (errno != 0 || end == value.c_str() + 2 || *end != '\0') return false;
115
+ out = static_cast<uintptr_t>(parsed);
116
+ return static_cast<unsigned long long>(out) == parsed;
117
+ }
118
+
119
+ static bool ReadAllStdin(std::string& bytes) {
120
+ if (_setmode(_fileno(stdin), _O_BINARY) == -1) return false;
121
+ std::vector<char> buffer(4096u);
122
+ for (;;) {
123
+ const size_t got = std::fread(buffer.data(), 1, buffer.size(), stdin);
124
+ if (got > 0u) {
125
+ if (bytes.size() + got > kMaxRequestBytes) return false;
126
+ bytes.append(buffer.data(), got);
127
+ }
128
+ if (got < buffer.size()) {
129
+ if (std::ferror(stdin)) return false;
130
+ break;
131
+ }
132
+ }
133
+ return !bytes.empty();
134
+ }
135
+
136
+ static bool NextLine(const std::string& bytes, size_t& offset, std::string& line) {
137
+ if (offset >= bytes.size()) return false;
138
+ const size_t end = bytes.find('\n', offset);
139
+ if (end == std::string::npos) return false;
140
+ line.assign(bytes, offset, end - offset);
141
+ if (!line.empty() && line.back() == '\r') line.pop_back();
142
+ offset = end + 1u;
143
+ return true;
144
+ }
145
+
146
+ static bool SplitField(const std::string& line, std::string& key, std::string& value) {
147
+ const size_t eq = line.find('=');
148
+ if (eq == std::string::npos || eq == 0u) return false;
149
+ key = line.substr(0u, eq);
150
+ value = line.substr(eq + 1u);
151
+ return true;
152
+ }
153
+
154
+ static bool SupportedKind(const std::string& kind) {
155
+ static const char* const kinds[] = {
156
+ "pointer_move","click_primary","click_secondary","double_click_primary",
157
+ "scroll","key_chord","text_input"
158
+ };
159
+ for (const char* value : kinds) if (kind == value) return true;
160
+ return false;
161
+ }
162
+
163
+ static WORD VirtualKey(const std::string& key) {
164
+ if (key.size() == 1u && key[0] >= 'A' && key[0] <= 'Z') return static_cast<WORD>(key[0]);
165
+ if (key.size() == 1u && key[0] >= '0' && key[0] <= '9') return static_cast<WORD>(key[0]);
166
+ if (key.size() >= 2u && key[0] == 'F') {
167
+ const int n = std::atoi(key.c_str() + 1);
168
+ if (n >= 1 && n <= 12) return static_cast<WORD>(VK_F1 + n - 1);
169
+ }
170
+ if (key == "ENTER") return VK_RETURN;
171
+ if (key == "ESC") return VK_ESCAPE;
172
+ if (key == "TAB") return VK_TAB;
173
+ if (key == "SPACE") return VK_SPACE;
174
+ if (key == "BACKSPACE") return VK_BACK;
175
+ if (key == "DELETE") return VK_DELETE;
176
+ if (key == "INSERT") return VK_INSERT;
177
+ if (key == "HOME") return VK_HOME;
178
+ if (key == "END") return VK_END;
179
+ if (key == "PAGEUP") return VK_PRIOR;
180
+ if (key == "PAGEDOWN") return VK_NEXT;
181
+ if (key == "LEFT") return VK_LEFT;
182
+ if (key == "RIGHT") return VK_RIGHT;
183
+ if (key == "UP") return VK_UP;
184
+ if (key == "DOWN") return VK_DOWN;
185
+ if (key == "CTRL") return VK_CONTROL;
186
+ if (key == "ALT") return VK_MENU;
187
+ if (key == "SHIFT") return VK_SHIFT;
188
+ if (key == "WIN") return VK_LWIN;
189
+ return 0;
190
+ }
191
+
192
+ static bool ParseRequest(const std::string& bytes, Request& request, std::string& reason) {
193
+ size_t offset = 0u;
194
+ std::string line;
195
+ if (!NextLine(bytes, offset, line) || line != "DEADBYTE_DESKTOP_INPUT_V1") {
196
+ reason = "magic mismatch"; return false;
197
+ }
198
+
199
+ bool seenAction=false,seenKind=false,seenDesktop=false,seenHwnd=false,seenPid=false;
200
+ bool seenLeft=false,seenTop=false,seenWidth=false,seenHeight=false,seenX=false,seenY=false;
201
+ bool seenDelta=false,seenKeyCount=false,seenTextBytes=false;
202
+ size_t declaredKeyCount=0u, declaredTextBytes=0u;
203
+
204
+ while (NextLine(bytes, offset, line)) {
205
+ std::string key,value;
206
+ if (!SplitField(line,key,value)) { reason="field malformed"; return false; }
207
+ if (key=="action_sha256") {
208
+ if (seenAction || !Hex64(value)) { reason="action_sha256 invalid"; return false; }
209
+ seenAction=true; request.actionSha256=value;
210
+ } else if (key=="kind") {
211
+ if (seenKind || !SupportedKind(value)) { reason="kind invalid"; return false; }
212
+ seenKind=true; request.kind=value;
213
+ } else if (key=="expected_desktop") {
214
+ if (seenDesktop || value.empty() || value.size()>128u || value.find('\0')!=std::string::npos) { reason="expected_desktop invalid"; return false; }
215
+ seenDesktop=true; request.expectedDesktop=value;
216
+ } else if (key=="expected_hwnd") {
217
+ if (seenHwnd || !ParseUintptrHex(value,request.expectedHwnd)) { reason="expected_hwnd invalid"; return false; }
218
+ seenHwnd=true;
219
+ } else if (key=="expected_pid") {
220
+ if (seenPid || !ParseDword(value,request.expectedPid) || request.expectedPid>0x7fffffffu) { reason="expected_pid invalid"; return false; }
221
+ seenPid=true;
222
+ } else if (key=="left") {
223
+ if (seenLeft || !ParseLong(value,request.left)) { reason="left invalid"; return false; } seenLeft=true;
224
+ } else if (key=="top") {
225
+ if (seenTop || !ParseLong(value,request.top)) { reason="top invalid"; return false; } seenTop=true;
226
+ } else if (key=="width") {
227
+ if (seenWidth || !ParseLong(value,request.width)) { reason="width invalid"; return false; } seenWidth=true;
228
+ } else if (key=="height") {
229
+ if (seenHeight || !ParseLong(value,request.height)) { reason="height invalid"; return false; } seenHeight=true;
230
+ } else if (key=="x") {
231
+ if (seenX || !ParseLong(value,request.x)) { reason="x invalid"; return false; } seenX=true;
232
+ } else if (key=="y") {
233
+ if (seenY || !ParseLong(value,request.y)) { reason="y invalid"; return false; } seenY=true;
234
+ } else if (key=="scroll_delta") {
235
+ if (seenDelta || !ParseLong(value,request.scrollDelta)) { reason="scroll_delta invalid"; return false; } seenDelta=true;
236
+ } else if (key=="key_count") {
237
+ if (seenKeyCount) { reason="key_count duplicate"; return false; }
238
+ DWORD count=0; if (!ParseDword(value,count) || count>kMaxKeys) { reason="key_count invalid"; return false; }
239
+ seenKeyCount=true; declaredKeyCount=static_cast<size_t>(count);
240
+ } else if (key=="key") {
241
+ if (request.keys.size()>=kMaxKeys || VirtualKey(value)==0) { reason="key invalid"; return false; }
242
+ if (std::find(request.keys.begin(),request.keys.end(),value)!=request.keys.end()) { reason="key duplicate"; return false; }
243
+ request.keys.push_back(value);
244
+ } else if (key=="text_bytes") {
245
+ if (seenTextBytes) { reason="text_bytes duplicate"; return false; }
246
+ DWORD count=0; if (!ParseDword(value,count) || count>kMaxTextBytes) { reason="text_bytes invalid"; return false; }
247
+ seenTextBytes=true; declaredTextBytes=static_cast<size_t>(count);
248
+ if (bytes.size()-offset != declaredTextBytes) { reason="text payload length mismatch"; return false; }
249
+ request.textUtf8.assign(bytes.data()+offset,declaredTextBytes);
250
+ offset=bytes.size();
251
+ break;
252
+ } else {
253
+ reason="unknown field"; return false;
254
+ }
255
+ }
256
+
257
+ if (!(seenAction&&seenKind&&seenDesktop&&seenHwnd&&seenPid&&seenLeft&&seenTop&&seenWidth&&seenHeight&&seenX&&seenY&&seenDelta&&seenKeyCount&&seenTextBytes)) {
258
+ reason="required field missing"; return false;
259
+ }
260
+ if (request.keys.size()!=declaredKeyCount) { reason="key_count mismatch"; return false; }
261
+ if (request.width<=0 || request.height<=0 || request.width>131072 || request.height>131072) { reason="bounds invalid"; return false; }
262
+ if (request.left < -131072 || request.top < -131072 || request.left+request.width > 262144 || request.top+request.height > 262144) { reason="bounds extent invalid"; return false; }
263
+
264
+ const bool pointer = request.kind=="pointer_move" || request.kind=="click_primary" || request.kind=="click_secondary" ||
265
+ request.kind=="double_click_primary" || request.kind=="scroll";
266
+ if (pointer) {
267
+ if (request.x<request.left || request.y<request.top || request.x>=request.left+request.width || request.y>=request.top+request.height) {
268
+ reason="point outside expected desktop"; return false;
269
+ }
270
+ } else if (request.x!=0 || request.y!=0) { reason="non-pointer coordinate must be zero"; return false; }
271
+
272
+ if (request.kind=="scroll") {
273
+ if (request.scrollDelta==0 || request.scrollDelta < -12000 || request.scrollDelta > 12000) { reason="scroll delta invalid"; return false; }
274
+ } else if (request.scrollDelta!=0) { reason="non-scroll delta must be zero"; return false; }
275
+
276
+ if (request.kind=="key_chord") {
277
+ if (request.keys.empty()) { reason="key chord empty"; return false; }
278
+ } else if (!request.keys.empty()) { reason="keys supplied for non-key action"; return false; }
279
+
280
+ if (request.kind=="text_input") {
281
+ if (request.textUtf8.empty()) { reason="text input empty"; return false; }
282
+ } else if (!request.textUtf8.empty()) { reason="text supplied for non-text action"; return false; }
283
+
284
+ return true;
285
+ }
286
+
287
+ static bool ValidateEnvironment(const Request& request, std::string& currentDesktop, HWND& foreground, DWORD& foregroundPid, std::string& reason) {
288
+ if (!InputDesktopName(currentDesktop)) { reason="input desktop unavailable"; return false; }
289
+ if (currentDesktop != request.expectedDesktop) { reason="input desktop changed"; return false; }
290
+
291
+ const LONG left=GetSystemMetrics(SM_XVIRTUALSCREEN);
292
+ const LONG top=GetSystemMetrics(SM_YVIRTUALSCREEN);
293
+ const LONG width=GetSystemMetrics(SM_CXVIRTUALSCREEN);
294
+ const LONG height=GetSystemMetrics(SM_CYVIRTUALSCREEN);
295
+ if (left!=request.left || top!=request.top || width!=request.width || height!=request.height) {
296
+ reason="virtual desktop bounds changed"; return false;
297
+ }
298
+
299
+ foreground=GetForegroundWindow();
300
+ foregroundPid=0;
301
+ if (foreground) GetWindowThreadProcessId(foreground,&foregroundPid);
302
+ if (reinterpret_cast<uintptr_t>(foreground)!=request.expectedHwnd || foregroundPid!=request.expectedPid) {
303
+ reason="foreground window changed"; return false;
304
+ }
305
+ return true;
306
+ }
307
+
308
+ static LONG NormalizeAbsolute(LONG value,LONG origin,LONG extent) {
309
+ if (extent<=1) return 0;
310
+ const long long numerator=static_cast<long long>(value-origin)*65535ll;
311
+ return static_cast<LONG>(numerator/static_cast<long long>(extent-1));
312
+ }
313
+
314
+ static INPUT MouseInput(DWORD flags,LONG x,LONG y,DWORD data=0) {
315
+ INPUT input{};
316
+ input.type=INPUT_MOUSE;
317
+ input.mi.dx=x;
318
+ input.mi.dy=y;
319
+ input.mi.mouseData=data;
320
+ input.mi.dwFlags=flags;
321
+ return input;
322
+ }
323
+
324
+ static INPUT KeyInput(WORD vk,DWORD flags=0) {
325
+ INPUT input{};
326
+ input.type=INPUT_KEYBOARD;
327
+ input.ki.wVk=vk;
328
+ input.ki.dwFlags=flags;
329
+ return input;
330
+ }
331
+
332
+ static INPUT UnicodeInput(wchar_t ch,DWORD flags=0) {
333
+ INPUT input{};
334
+ input.type=INPUT_KEYBOARD;
335
+ input.ki.wScan=static_cast<WORD>(ch);
336
+ input.ki.dwFlags=KEYEVENTF_UNICODE|flags;
337
+ return input;
338
+ }
339
+
340
+ static bool SendExact(const std::vector<INPUT>& inputs,size_t& sent,std::string& reason) {
341
+ size_t offset=0u;
342
+ while (offset<inputs.size()) {
343
+ const size_t remaining=inputs.size()-offset;
344
+ const UINT batch=static_cast<UINT>(remaining>512u?512u:remaining);
345
+ const UINT result=SendInput(batch,const_cast<INPUT*>(inputs.data()+offset),sizeof(INPUT));
346
+ if (result!=batch) {
347
+ sent+=result;
348
+ std::ostringstream out; out<<"SendInput partial result "<<result<<"/"<<batch<<" error="<<GetLastError();
349
+ reason=out.str(); return false;
350
+ }
351
+ sent+=result; offset+=batch;
352
+ }
353
+ return true;
354
+ }
355
+
356
+ static bool BuildInputs(const Request& request,std::vector<INPUT>& inputs,std::string& reason) {
357
+ const bool pointer=request.kind=="pointer_move" || request.kind=="click_primary" || request.kind=="click_secondary" ||
358
+ request.kind=="double_click_primary" || request.kind=="scroll";
359
+ LONG nx=0,ny=0;
360
+ if (pointer) {
361
+ nx=NormalizeAbsolute(request.x,request.left,request.width);
362
+ ny=NormalizeAbsolute(request.y,request.top,request.height);
363
+ inputs.push_back(MouseInput(MOUSEEVENTF_MOVE|MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_VIRTUALDESK,nx,ny));
364
+ }
365
+ if (request.kind=="click_primary") {
366
+ inputs.push_back(MouseInput(MOUSEEVENTF_LEFTDOWN,0,0));
367
+ inputs.push_back(MouseInput(MOUSEEVENTF_LEFTUP,0,0));
368
+ } else if (request.kind=="click_secondary") {
369
+ inputs.push_back(MouseInput(MOUSEEVENTF_RIGHTDOWN,0,0));
370
+ inputs.push_back(MouseInput(MOUSEEVENTF_RIGHTUP,0,0));
371
+ } else if (request.kind=="double_click_primary") {
372
+ for (int i=0;i<2;i++) {
373
+ inputs.push_back(MouseInput(MOUSEEVENTF_LEFTDOWN,0,0));
374
+ inputs.push_back(MouseInput(MOUSEEVENTF_LEFTUP,0,0));
375
+ }
376
+ } else if (request.kind=="scroll") {
377
+ inputs.push_back(MouseInput(MOUSEEVENTF_WHEEL,0,0,static_cast<DWORD>(request.scrollDelta)));
378
+ } else if (request.kind=="key_chord") {
379
+ for (const auto& key:request.keys) inputs.push_back(KeyInput(VirtualKey(key)));
380
+ for (auto it=request.keys.rbegin();it!=request.keys.rend();++it) inputs.push_back(KeyInput(VirtualKey(*it),KEYEVENTF_KEYUP));
381
+ } else if (request.kind=="text_input") {
382
+ if (request.textUtf8.size()>static_cast<size_t>(INT_MAX)) { reason="text too large"; return false; }
383
+ const int needed=MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,request.textUtf8.data(),static_cast<int>(request.textUtf8.size()),nullptr,0);
384
+ if (needed<=0) { reason="text UTF-8 invalid"; return false; }
385
+ std::wstring wide(static_cast<size_t>(needed),L'\0');
386
+ if (MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,request.textUtf8.data(),static_cast<int>(request.textUtf8.size()),wide.data(),needed)!=needed) {
387
+ reason="text UTF-8 conversion failed"; return false;
388
+ }
389
+ if (std::find(wide.begin(),wide.end(),L'\0')!=wide.end()) { reason="text contains NUL"; return false; }
390
+ if (wide.size()>32768u) { reason="text UTF-16 event bound exceeded"; return false; }
391
+ inputs.reserve(wide.size()*2u);
392
+ for (wchar_t ch:wide) {
393
+ inputs.push_back(UnicodeInput(ch));
394
+ inputs.push_back(UnicodeInput(ch,KEYEVENTF_KEYUP));
395
+ }
396
+ }
397
+ if (inputs.empty() || inputs.size()>65536u) { reason="input event count invalid"; return false; }
398
+ return true;
399
+ }
400
+
401
+ static int SelfTest() {
402
+ if (VirtualKey("A")!='A' || VirtualKey("F12")!=VK_F12 || VirtualKey("CTRL")!=VK_CONTROL || VirtualKey("NOPE")!=0) return 31;
403
+ if (NormalizeAbsolute(-1920,-1920,3840)!=0) return 32;
404
+ if (NormalizeAbsolute(1919,-1920,3840)!=65535) return 33;
405
+ std::printf("{\"schema\":\"deadbyte.desktop-native-input-selftest.v1\",\"status\":\"passed\"}\n");
406
+ return 0;
407
+ }
408
+
409
+ int wmain(int argc,wchar_t** argv) {
410
+ SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
411
+ if (argc==2 && std::wstring(argv[1])==L"--selftest") return SelfTest();
412
+ if (argc!=1) {
413
+ std::fprintf(stderr,"deadbyte-desktop-input: arguments are not accepted\n");
414
+ return 2;
415
+ }
416
+
417
+ std::string bytes;
418
+ if (!ReadAllStdin(bytes)) {
419
+ std::fprintf(stderr,"deadbyte-desktop-input: request read failed or exceeded bound\n");
420
+ return 10;
421
+ }
422
+ Request request;
423
+ std::string reason;
424
+ if (!ParseRequest(bytes,request,reason)) {
425
+ std::fprintf(stderr,"deadbyte-desktop-input: request rejected: %s\n",reason.c_str());
426
+ return 11;
427
+ }
428
+
429
+ std::string currentDesktop;
430
+ HWND foreground=nullptr;
431
+ DWORD foregroundPid=0;
432
+ if (!ValidateEnvironment(request,currentDesktop,foreground,foregroundPid,reason)) {
433
+ std::fprintf(stderr,"deadbyte-desktop-input: environment rejected: %s\n",reason.c_str());
434
+ return 12;
435
+ }
436
+
437
+ std::vector<INPUT> inputs;
438
+ if (!BuildInputs(request,inputs,reason)) {
439
+ std::fprintf(stderr,"deadbyte-desktop-input: input build rejected: %s\n",reason.c_str());
440
+ return 13;
441
+ }
442
+
443
+ size_t sent=0u;
444
+ if (!SendExact(inputs,sent,reason)) {
445
+ std::fprintf(stderr,"deadbyte-desktop-input: injection failed after %zu events: %s\n",sent,reason.c_str());
446
+ return 14;
447
+ }
448
+
449
+ std::printf("{\"schema\":\"deadbyte.desktop-native-input.v1\",\"injected\":true,\"backend\":\"win32-sendinput\","
450
+ "\"action_sha256\":\"%s\",\"kind\":\"%s\",\"event_count\":%zu,\"input_desktop\":\"%s\","
451
+ "\"foreground_hwnd\":\"%s\",\"foreground_pid\":%lu}\n",
452
+ request.actionSha256.c_str(),request.kind.c_str(),sent,JsonEscape(currentDesktop).c_str(),
453
+ HwndHex(foreground).c_str(),static_cast<unsigned long>(foregroundPid));
454
+ return 0;
455
+ }